How to build a docker image from a nodejs project in a monorepo with yarn workspaces How to build a docker image from a nodejs project in a monorepo with yarn workspaces docker docker

How to build a docker image from a nodejs project in a monorepo with yarn workspaces


I've worked on a project following a structure similar to yours, it was looking like:

project├── package.json├── packages│   ├── package1│   │   ├── package.json│   │   └── src│   ├── package2│   │   ├── package.json│   │   └── src│   └── package3│       ├── package.json│       └── src├── services│   ├── service1│   │   ├── Dockerfile│   │   ├── package.json│   │   └── src│   └── service2│       ├── Dockerfile│       ├── package.json│       └── src└── yarn.lock

The services/ folder contains one service per sub-folder. Every service is written in node.js and has its own package.json and Dockerfile.They are typically web server or REST API based on Express.

The packages/ folder contains all the packages that are not services, typically internal libraries.

A service can depend on one or more package, but not on another service.A package can depend on another package, but not on a service.

The main package.json (the one at the project root folder) only contains some devDependencies, such as eslint, the test runner etc.

An individual Dockerfile looks like this, assuming service1 depends on both package1 & package3:

FROM node:8.12.0-alpine AS baseWORKDIR /projectFROM base AS dependencies# We only copy the dependencies we needCOPY packages/package1 packages/package1COPY packages/package3 packages/package3COPY services/services1 services/services1# The global package.json only contains build dependenciesCOPY package.json .COPY yarn.lock .RUN yarn install --production --pure-lockfile --non-interactive --cache-folder ./ycache; rm -rf ./ycache

The actual Dockerfiles I used were more complicated, as they had to build the sub-packages, run the tests etc. But you should get the idea with this sample.

As you can see the trick was to only copy the packages that are needed for a specific service.The yarn.lock file contains a list of package@version with the exact version and dependencies resolved. To copy it without all the sub-packages is not a problem, yarn will use the version resolved there when installing the dependencies of the included packages.

In your case the react-native project will never be part of any Dockerfile, as it is the dependency of none of the services, thus saving a lot of space.

For sake of conciseness, I omitted a lot of details in that answer, feel free to ask for precision in the comment if something isn't really clear.


After a lot of trial and error I've found that using that careful use of the file .dockerignore is a great way to control your final image. This works great when running under a monorepo to exclude "other" packages.

For each package, we have a similar named dockerignore file that replaces the live .dockerignore file just before the build.

e.g.,cp admin.dockerignore .dockerignore

Below is an example of admin.dockerignore. Note the * at the top of that file that means "ignore everything". The ! prefix means "don't ignore", i.e., retain. The combination means ignore everything except for the specified files.

*# Build specific keep!packages/admin# Common Keep!*.json!yarn.lock!.yarnrc!packages/common**/.circleci**/.editorconfig**/.dockerignore**/.git**/.DS_Store**/.vscode**/node_modules


We put our backend services to a monorepo recently and this was one of a few points that we had to solve. Yarn doesn't have anything that would help us in this regard so we had to look elsewhere.

First we tried @zeit/ncc, there were some issues but eventually we managed to get the final builds. It produces one big file that includes all your code and also all your dependencies code. It looked great. I had to copy to the docker image only a few files (js, source maps, static assets). Images were much much smaller and the app worked. BUT the runtime memory consumption grew a lot. Instead of ~70MB the running container consumed ~250MB. Not sure if we did something wrong but I haven't found any solution and there's only one issue mentioning this. I guess Node.js load parses and loads all the code from the bundle even though most of it is never used.

All we needed is to separate each of the packages production dependencies to build a slim docker image. It seems it's not so simple to do but we found a tool after all.

We're now using fleggal/monopack. It bundles our code with Webpack and transpile it Babel. So it produces also one file bundle but it doesn't contain all the dependencies, just our code. This step is something we don't really needed but we don't mind it's there. For us the important part is - Monopack copies only the package's production dependency tree to the dist/bundled node_modules. That's exactly what we needed. Docker images now have 100MB-150MB instead of 700MB.

There's one easier way. If you have only a few really big npm modules in your node_modules you can use nohoist in your root package.json. That way yarn keeps these modules in package's local node_modules and it doesn't have to be copied to Docker images of all other services.

eg.:

"nohoist": [  "**/puppeteer",  "**/puppeteer/**",  "**/aws-sdk",  "**/aws-sdk/**"]