How to Build Multiple Microservices with a Single Dockerfile Using PNPM Workspaces

Sharon Sahadevan
5 min readMar 20, 2023

--

Microservices architecture is a popular approach to building modern applications, where each microservice is a small, self-contained unit that can be developed, deployed, and scaled independently. However, managing the dependencies and builds of multiple microservices can be challenging, especially when each microservice has its own set of dependencies and configurations.

This article will show you how to use the pnpm workspace concept to build multiple microservices with a single Dockerfile. PNPM is a fast, disk-space-efficient package manager for Node.js that supports workspaces, allowing you to manage multiple packages in a single repository.

I assume the following steps are already completed.

  • Docker installed on your machine
  • Working knowledge of Docker and containerization
  • A basic understanding of Node.js and package management

Step 1: Setup PNPM Workspace

First, we need to set up a PNPM workspace containing all of our microservices. To do this, create a new directory that will serve as the root of the workspace and run the following command:

pnpm init -w

This command will create a new package.json file with a workspace property listing the directories containing our microservices. For example:

{
"name": "my-workspace",
"version": "1.0.0",
"workspaces": [
"services/*"
]
}

This JSON file tells PNPM to treat the services directory as a workspace, install dependencies, and run scripts in each subdirectory of services.

Step 2: Create Microservices

Next, create one or more microservices in the services directory, each with its package.json file and dependencies. For example, you might create two microservices named service-a and service-b, with the following directory structure:

services/
service-a/
package.json
index.js
service-b/
package.json
index.js

Each package.json file should define its dependencies and scripts and any other configurations specific to that microservice.

Step 3: Create Dockerfile

Next, create a Dockerfile in the root of the workspace directory that will build all of our microservices.

Here's an example Dockerfile:

## Prepare workspace

FROM node:18-alpine AS workspace
WORKDIR /app

RUN corepack enable && corepack prepare pnpm@7.29.3 --activate

COPY pnpm-lock.yaml .
RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store pnpm fetch

COPY . .
RUN pnpm install --recursive --frozen-lockfile


## Create minimal deployment for given package

FROM workspace AS pruned
ARG service
WORKDIR /app
RUN pnpm --recursive run build
RUN pnpm --filter ${service} deploy --prod pruned



## Production image

FROM node:18-alpine
WORKDIR /app

ENV NODE_ENV=production

COPY --from=pruned /app/pruned/dist dist
COPY --from=pruned /app/pruned/package.json package.json
COPY --from=pruned /app/pruned/node_modules node_modules

ENTRYPOINT ["node", "dist"]

This Dockerfile has three parts. Let’s break it down and explain each of them separately.

Part 1: Prepare Workspace

FROM node:18-alpine AS workspace
WORKDIR /app

RUN corepack enable && corepack prepare pnpm@7.29.3 --activate

COPY pnpm-lock.yaml .
RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store pnpm fetch

COPY . .
RUN pnpm install --recursive --frozen-lockfile

This part sets up the workspace and installs the dependencies for all of our microservices. It uses the node:18-alpine image as the base image, creates a new workspace directory /app, installs PNPM, and copies the pnpm-lock.yaml file and the entire project directory into the workspace. It then installs the dependencies using pnpm install --recursive --frozen-lockfile.

Part 2: Create Minimal Deployment for Given Package

FROM workspace AS pruned
ARG service
WORKDIR /app
RUN pnpm --recursive run build
RUN pnpm --filter ${service} deploy --prod pruned

This part creates a minimal deployment for a given microservice. It uses the workspace image as the base image, sets the working directory to /app, builds all of the microservices using pnpm --recursive run build, and deploys only the specified microservice using pnpm --filter ${service} deploy --prod pruned.

Part 3: Production Image

FROM node:18-alpine
WORKDIR /app

ENV NODE_ENV=production

COPY --from=pruned /app/pruned/dist dist
COPY --from=pruned /app/pruned/package.json package.json
COPY --from=pruned /app/pruned/node_modules node_modules

ENTRYPOINT ["node", "dist"]

This part creates the final production image. It uses the node:18-alpine base image, sets the working directory to /app, sets the NODE_ENV environment variable to production, and copies the files from the pruned image, including the microservice’s dist directory, package.json file, and node_modules directory. It then sets the entry point to run the microservice using node dist.

Step 4: Build and Run Docker Image

Finally, build the Docker image using the following command:

docker build -t

This will run the microservice and map the container’s port 3000 to the host’s port 3000.

Advantages of using a single Dockerfile for multiple microservices

Single Dockerfile for multiple microservices is that it simplifies the build and deployment process. With separate Dockerfiles, you would need to build and deploy each microservice individually, which can be time-consuming and error-prone, especially if you have many microservices. With a single Dockerfile, you can build and deploy all your microservices simultaneously, saving time and effort.

Another advantage is that it reduces code duplication and manual effort. For example, with separate Dockerfiles, you would need to duplicate a lot of code between them, such as installing dependencies and configuring the environment. With a single Dockerfile, you can centralize this code and reuse it across all your microservices, reducing the amount of manual effort required.

Now, let’s talk about dependency management. Managing dependencies in a microservices architecture can be challenging, especially when each has its dependencies and configurations. PNPM workspaces solve this problem by allowing you to have multiple packages in a single repository and install dependencies more efficiently.

With PNPM workspaces, you can define a workspace in your package.json file that lists the directories that contain your microservices. PNPM will then install the dependencies for each microservice in a shared node_modules directory at the workspace’s root, reducing the amount of disk space required and making it easier to manage dependencies.

Using PNPM workspaces combined with a single Dockerfile for multiple microservices can simplify the dependency management process and reduce the manual effort required. By installing dependencies in a shared node_modules directory, you can reduce the amount of disk space required and make managing dependencies across your microservices easier.

Conclusion

This article showed you how to use the pnpm workspace concept to build multiple microservices with a single Dockerfile. Using a single Dockerfile simplifies the build and deployment process and reduces the amount of code duplication and manual effort required. In addition, with PNPM workspaces, you can manage multiple packages in a single repository and install dependencies more efficiently, making it easier to scale and maintain your microservices architecture.

--

--

Sharon Sahadevan
Sharon Sahadevan

Written by Sharon Sahadevan

Founder @ kubenatives.com | I write about Kubernetes and Cloud Native echo system | https://www.linkedin.com/in/sharonsahadevan 🚀✍️

Responses (1)