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
- Public Site
- 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.