Creating Docker Multi Stage Builds for a monorepo project

Learn how to create a Docker Multi Stage Build for a monorepo project with shared dependencies

Scenario

Let's say you have multiple projects in a monorepo (eg lerna or nx) that share dependencies. In order to create docker images for your apps your first thought might be to create a separate docker image for each app. While this works just fine, imagine having 10+ apps on the same monorepo.

Building 10 images at once every time you need to deploy your project can be very irritating (especially when you want to get those bugfixes up ASAP!). Therefore it is a good practise to optimize your docker images smartly and save some time and space in your development process.

Technologies used in this example

For this scenario I will be using docker to build two Angular apps that share the same dependencies. Replace the commands that build the apps in this example to match your scenario and you are good to go.

Now let's give our applications a name so that we can communicate.

Applications

  1. Public Site
  2. API

Creating the dockerfiles

First lets create the dockerfile that will build our dependencies.

FROM node:14-alpine

USER root

WORKDIR /app

COPY package.json .

RUN npm install

The dockerfile above will pull an alpine image with node:14 preinstalled. You can also do node:latest-alpine but it is always better to have control over the version as you may want to build on the same version that you use for your local development.

Let's build our dependencies.dockerfile.

docker build --pull --rm -f "dependencies.dockerfile" -t my-project-dependencies:latest .

After the build command is finished you will have a local image called my-project-dependencies and tagget latest

We are now ready to build our apps. The trick now is to use the image we created earlier so the application images won't have to go through installing dependencies again.

# Start from the dependencies image
FROM my-project-dependencies:latest as dependencies

FROM node:14-alpine

WORKDIR /app

# This step will copy things like `node_modules` over to the image's WORKDIR
COPY --from=dependencies /app  .

# Copy your local project to the image's WORKDIR
COPY . .

# Run any commands you need to build your app
RUN npm run build:public-site

EXPOSE 4200

# The following command assumes that your projects builds on the dist/apps/public-site/ directory
# and your entry point is main.js. Use whatever makes sense for your kind of app.
CMD [ "node", "dist/apps/public-site/main.js"]

Similarly, we are going to create our image for the api app

FROM my-project-dependencies:latest as dependencies

FROM node:14-alpine

WORKDIR /app

COPY --from=dependencies /app  .
COPY . .

# You can set some environment variables if required for your app
ENV PORT=9000

RUN npm run build:api

EXPOSE 9000

CMD [ "node", "dist/apps/api/index.js"]

Remember that you also propably need a .dockerignore file so that docker build commands will skip copying things like your local dependencies or env files to the production image

Example

node_modules
./dist
/tmp
/out-tsc
.env

Summary

So there you have it! We created an image my-project-dependencies:latest that we can use as a starting point for our apps. Our apps start their build from my-project-dependencies:latest and already have their dependencies installed. We only need to build our apps and start them rather than rebuilding the dependencies again for each app. Just remember to rebuild your dependencies image first every time you add new dependencies to your project.

More posts in DevOps