Refactoring a Node.js-Express Project into Multiple Docker Services Using Monorepo and Lerna

Azmi Ahmad
4 min readMar 13, 2023

If you’re working on a Node.js Express project that needs to be split into multiple independent services, you may be wondering how to manage shared dependencies and common code. One solution is to use a monorepo and Lerna to manage your project, which can help to simplify dependency management, testing, and deployment.

In this story, we’ll walk through how to refactor a Node.js-Express project into two Docker images using a monorepo and Lerna. We’ll cover how to create a monorepo, split your code into independent packages, manage dependencies between packages, and build and deploy Docker images for each package.

Setting up the Monorepo

The first step is to set up a new directory for your monorepo and initialize it with a package.json file:

mkdir my-monorepo
cd my-monorepo
npm init -y

Next, install Lerna as a development dependency:

npm install --save-dev lerna

Initialize Lerna with a default configuration:

npx lerna init

Creating Independent Packages

To split your code into independent packages, create a new directory for each package in the packages directory. In this example, we'll create two packages: main-service and sub-service.

cd packages
mkdir main-service
mkdir sub-service

Next, create a package.json file for each package, specifying the package name and any required dependencies:

// packages/main-service/package.json
{
"name": "main-service",
"dependencies": {
"lodash": "^4.17.21",
"my-shared-module": "^1.0.0"
}
}

// packages/sub-service/package.json
{
"name": "sub-service",
"dependencies": {
"lodash": "^4.17.21",
"my-shared-module": "^1.0.0"
}
}

Note that both packages depend on a shared module called my-shared-module, which we'll create later.

Create a new package called my-shared-module in the packages directory, and move any common code or models into this package:

mkdir my-shared-module

Create a package.json file for the my-shared-module package, specifying the package name and any required dependencies:

// packages/my-shared-module/package.json
{
"name": "my-shared-module"
}

Add the my-shared-module package as a dependency in the package.json files for the main-service and sub-service packages:

// packages/main-service/package.json
{
"name": "main-service",
"dependencies": {
"lodash": "^4.17.21",
"my-shared-module": "^1.0.0"
}
}

// packages/sub-service/package.json
{
"name": "sub-service",
"dependencies": {
"lodash": "^4.17.21",
"my-shared-module": "^1.0.0"
}
}

Linking Packages

To allow the main-service and sub-service packages to use the my-shared-module package, we need to use npm link to link the packages together.

First, navigate to the my-shared-module directory and run npm link:

cd packages/my-shared-module
npm link

Next, navigate to the main-service directory and run npm link my-shared-module to link the package:

cd ../main-service
npm link my-shared-module

Do the same for the sub-service package:

cd ../sub-service
npm link my-shared-module

Now, the main-service and sub-service packages can use the my-shared-module package as if it were installed locally.

Building and Running Docker Images

To build and run Docker images for each package, we’ll use a Dockerfile in each package directory. Here’s an example Dockerfile for the main-service package:

# Dockerfile for main-service package
FROM node:18-bullseye-slim

WORKDIR /app

COPY package*.json ./

RUN npm install

COPY . .

EXPOSE 3000

CMD [ "npm", "start" ]

Note that we start with a Node.js 18 base image, install dependencies, copy the code into the image, expose port 3000, and start the application using npm start.

Create a similar Dockerfile for the sub-service package.

Next, we’ll use Lerna to build and publish Docker images for each package. First, add the following scripts to the root package.json file:

{
"scripts": {
"build": "lerna run build",
"docker:build": "lerna run docker:build",
"docker:publish": "lerna run docker:publish"
}
}

These scripts will allow us to build all packages, build Docker images for each package, and publish Docker images to a registry.

Add the following docker scripts to the package.json files for the main-service and sub-service packages:

// packages/main-service/package.json
{
"name": "main-service",
"scripts": {
"docker:build": "docker build -t my-registry/main-service .",
"docker:publish": "docker push my-registry/main-service"
}
}

// packages/sub-service/package.json
{
"name": "sub-service",
"scripts": {
"docker:build": "docker build -t my-registry/sub-service .",
"docker:publish": "docker push my-registry/sub-service"
}
}

Note that we’re using a private registry called my-registry to publish the images. You'll need to modify these scripts to use your own registry.

To build and publish the Docker images, run the following commands:

npm run build
npm run docker:build
npm run docker:publish

This will build all packages, build Docker images for each package, and publish the images to the registry.

Conclusion

In this article, we’ve walked through how to refactor a Node.js Express project into two Docker images using a monorepo and Lerna. We’ve covered how to create a monorepo, split your code into independent packages, manage dependencies between packages, and build and deploy Docker images for each package.

Using a monorepo and Lerna can help to simplify dependency management, testing, and deployment, and can make it easier to split your project into independent services.

--

--

Azmi Ahmad

Technology Enthusiast | Solution Architect | Entrepreneur | Co-founder at Plavaga