How to create a Generic Docker Base Image with Monorepo and Semantic versioning? Part 1

In this article, we will go through the entire process of create a generic docker base image for building nodejs applications using Monorepo and Github actions. Step-by-step, we will be learning how to create package to build and push a base docker image using semantic versioning. You will be able to use it as a reference for manage different versions of your docker base images using only one git repository.

Given the above, let's have a look at the solution design:

We will use nx monorepo as a single repository to hold different packages which represents a nodejs docker image. Also, Github actions to build & push the docker image and finally, share it to Docker Hub registry.

Setting up the monorepo

First, we will create the monorepo. For that we will use nx monorepo:

npx create-nx-workspace@latest

If you don't have installed the "create-nx-workspace" package, the terminal will asks you to install it before continue.

Then, answer the questions in order to make theses answers:

NX   Let's create a new workspace [https://nx.dev/getting-started/intro]

✔ Where would you like to create your workspace? · docker-nodejs-base-image-monorepo
✔ Which stack do you want to use? · none
✔ Package-based monorepo, integrated monorepo, or standalone project? · package-based
✔ Set up CI with caching, distribution and test deflaking · skip
✔ Would you like remote caching to make your build faster? · skip

 NX   Creating your v19.4.1 workspace.

✔ Installing dependencies with npm
✔ Successfully created the workspace: docker-nodejs-base-image-monorepo.

Create a package

To handle independently each Docker Base Image we are going to create a package folder with the following structure:

── packages
    └── package-name
        ├── .dockerignore
        ├── Dockerfile
        ├── project.json
        └── README.md
  • packages: The root folder for all packages, if you don't have one, you have to create it.

  • package-name: For our first package we will called it as node-18 which represent the Base Images of Node v18

  • .dockerignore: We will use this file to exclude files or directories from the docker build context. We can add or ignore files according to the docker base image customization.

  • Dockerfile: This file contains all the commands that we will use to assemble an image.

  • project.json: Every package must have an individual project level configuration file which the semantic versioning value and the package name.

  • README.md: Pure documentation file (don't forget add some magic words for your readers).

Node-18 package

We are going to add some content in the node-18 package files. Firstly, create the project.json file with this json content:

{
    "name": "node-18",
    "version": "1.0.0",
    "nodeImage": "18.20.3",
    "$schema": "../../node_modules/nx/schemas/project-schema.json",
    "sourceRoot": "packages/node-18",
    "targets": {
        "build": {}
    }
}
  • name: Package name.... the plan is use: node-18, node-19, node-20, etc. To reflect what major nodejs version each package represents.

  • version: Semantic version of the package which will allow to have x.x.x version of the package related to the node version. Ex. for package node-18: image-name:18-x.x.x

  • nodeImage: This version must match the docker base image version that the Dockerfile uses.

  • $schema: Internal used by Nx.

  • sourceRoot: The location of project's sources relative to the root of the workspace

  • targets: Package tasks. We can use this section to create task for unit test, publish package, build, etc. For this project, we will leave it blank since we don't need any special task to build the docker image. However, the build task have to be defined. Don't remove it. We will need it later.

Test the package and Monorepo

We will check if our package configuration and monorepo are working as expected. So modify the targets section of "package/node-18/project.json" file with this:

 "targets": {
    "build": {
      "executor": "nx:run-commands",
      "options": {
        "commands": [
          "echo Hello World Package node-18"
        ]
      }
    }
 }
  • nx:run-commands: Allows us to run any custom commands with Nx.

Once we already set up the new package, we can run the build command of the new package direct in the root folder of the mono repo project:

npx nx build node-18 --verbose

if you get an error like "NX Failed to process project graph", Run "nx reset" or "npx nx reset" to fix this.

After run the command, you should get something like this:

> nx run node-18:build

> echo Hello World Package node-18

Hello World Package node-18

Finally, remove the content of the build task from the project.json file. Leave it empty (we don't need say "hello world" anymore):

 "targets": {
    "build": {}
 }

Now that you’ve read the first part, you know exactly how to set up packaged based monorepo and what we wanted to achieve with this article.

Ready to read the second part? Subscribe to my newsletter and don't miss the second part of this article. See you then.

0
Subscribe to my newsletter

Read articles from Max Martínez Cartagena directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Max Martínez Cartagena
Max Martínez Cartagena

I'm an enthusiastic Chilean software engineer in New Zeland. I mostly focus on the back-end of the systems. This is my site, Señor Developer, where I share my knowledge and experience.