Sharing Code in a Node Monorepo with Docker

loading views

This tutorial shows how to share common application code like utilities, services or helpers between different Docker services in a NodeJS monorepo.

The goal is to have a single Docker image for every service in the repository. That means every service should have its own Dockerfile to make the image as slim as possible.

Before writing this I tried to find help for this problem myself. The most tutorials recommended using something like Lerna or Yarn workspaces. Both of this solutions felt way to bloated for this simple goal of sharing a little bit of common source code. After all the repo should stay small and clean.

Another solution that often got recommended was to create a private NPM package or a self hosted package via git that includes all of the shared source code. My goal was to keep this a monorepo so that was not really an option.

NPM and local packages

As many of you probably already know there is a simple way in npm to install local packages as dependencies. So you can type something like npm install ../shared to install a local package without hosting it on NPM or using a second git repository. While developing you can even use npm link ../shared inside of Docker to always have the latest version of your shared directory.

In your services you then can use something like const { service } = require('shared') to access the shared package.

The only thing left to do is creating a Dockerfile that is able to install the shared package. To be able to do this the first change has to be to change the Docker context to the root folder of the repository so that Docker has access to all the files it needs.

That said you only have to change the directory paths and copy in the shared folder path like COPY ./shared ./shared

The resulting Dockerfile looks something like this:

FROM mhart/alpine-node:14

WORKDIR /srv

COPY worker/package.json worker/package-lock.json ./worker/
COPY ./shared ./shared

RUN cd worker && npm ci

COPY ./worker ./worker

WORKDIR /srv/worker

CMD NODE_ENV=production node index.js

Important is that before you can built this Dockerfile you have to run npm install ../shared to add a new entry to the package.json of your service. Your package.json should have a new entry like "shared": "file:../shared" In the same step NPM will link all needed dependencies in package-lock.json. This step has to be repeated every time the dependencies of the shared package will change.

Conclusion

Using a NodeJS Docker monorepo with locally shared dependencies does not have to be hard. You don't have to use Lerna or Yarn workspaces. It is enough to use the built-in tools from NPM.

In my opinion having the shared dependency in the same repository as all the services has a few advantages:

  • With a rock solid testing suite changes on the shared package can be made much faster and more confident
  • Enables testing the whole application at once
  • Updates in the shared package automatically apply for all services that use it
  • Not dealing with extra NPM packages or git dependencies