Streamline building Docker containers from lerna monorepo projects.
lerna-docker solves a bit of a niche problem, albeit a frustrating one. If you:
- Are using
lerna
. - Have multiple deployable projects that depend on common packages.
- Need those deployable projects built into Docker containers.
- Want to only build those deployable projects when there are changes to it or it's dependencies.
...this is the library for you
There are multiple ways to use lerna-docker
but you'll most likely use it within CI in some fashion.
yarn add @halfmatthalfcat/lerna-docker
or
npm i @halfmatthalfcat/lerna-docker
After installing into your project, you can either add a script into your package.json
and call
lerna-docker
from there or you can call ./node_modules/@halfmatthalfcat/lerna-docker/index.js
directly.
lerna-docker
comes out-of-the-box as a Github Action however, you still must install it into your project as
a dev dependency. This is mostly to avoid committing node_modules into this repo. To use it in a Github Workflow, can do
the following:
jobs:
build:
steps:
# all your usual build steps
- name: Dockerize
uses: ./node_modules/@halfmatthalfcat/lerna-docker
env:
# any/all the configuration options
Variable | Required | Description |
---|---|---|
VERSION_PREFIX | X | The SemVer prerelease prefix (rc, dev, pr) |
VERSION_PRERELEASE | The SemVer prerelease to "force" | |
GIT_EMAIL | X | The git email to use when tagging |
GIT_USERNAME | X | The git username to use when tagging |
DOCKER_REGISTRY | X | The docker registry to publish to |
DOCKER_REGISTRY_USERNAME | An optional username to use when auth-ing against a docker registry | |
DOCKER_REGISTRY_PASSWORD | An optional password to use when auth-ing against a docker registry | |
DOCKER_IMAGE_PREFIX | An optional docker image prefix to use in the final image path | |
DOCKER_BUILD_ARG_* | Optional build args to use when building | |
DOCKER_LABEL_* | Optional image labels to attach when building |
There are certain requirements/concessions necessary in order for lerna-docker to work.
- You are already using
lerna
.- You technically don't have to be using lerna, however a properly configured
lerna.json
file at the root of your project is necessary.
- You technically don't have to be using lerna, however a properly configured
- You have a top-level
/docker
directory that contains Dockerfiles for the projects which require containerizing.- The Dockerfiles should directly map to the package name. Packages that do not have an associated Dockerfile will not be built.
- You are comfortable with a time-based SemVer tag pattern (
year
.month
.date
).
- Changes are made to the repository to any or all dependencies.
- Those changes are pushed to the origin.
lerna changed
is ran to determine which packages changed from the previous tag.- If any packages that have changed have an associated Dockerfile under
/docker
, that Dockerfile is built against the root of the project. - A new SemVer tag is created based on the current date and current build against the trunk.
- The container(s) are pushed to their respective registries.
- The created tag is push to the origin.
- This is a manual step done by the pipeline after successful pushes
Consider the following directory structure:
project_root/
├─ docker/
│ ├─ user_service.Dockerfile/
│ ├─ auth_service.Dockerfile/
├─ packages/
│ ├─ common/
│ ├─ models/
│ │ ├─ dependencies: common
│ ├─ user_service/
│ │ ├─ dependencies: common, models
│ ├─ auth_service/
│ │ ├─ dependencies: common, models
├─ .dockerignore
├─ lerna.json
We have four packages (common, models, user_service and auth_service) that all have various dependencies on each other. We want to deploy user_service and auth_service both independently and if/when their dependencies on other packages change.
Direct changes to auth_service
will trigger a build of auth_service
alone during CI. A new container of auth_service
will be created with the latest tag and pushed to the registry.
Both auth_service
and user_service
have dependencies on models
and thus, will be built, tagged with the latest version
and pushed to the registry.
Since everything is dependent on common
, everything has "changed". However, models
does not have an associated Dockerfile
under /docker
, so no container will be built for models
however, containers will be built, tagged and pushed for
user_service
and auth_service
.
lerna-docker
takes an opinionated approach to versioning. The primary reasoning behind this is
traditional major.minor.patch
versioning is difficult in a monorepo. How do we decide what to bump when?
Projects like Conventional Commits
try to accomplish this through commit messages however lerna-docker
strives to streamline this process.
Because of this, it's probably less appropriate to use lerna-docker
for packaging libraries that need to abide by
major.minor.patch
to telegraph actual changes to the project.
The versioning for lerna-docker
operates as so:
{year}. - the current year
{month}. - the current month
{day}. - the current day
-{prefix}. - a user-defined prerelease prefix (rc, dev, pr)
{prerelease} - a prerelease version (automatic or manual)
Imagine it's the first day of the year and we've designated builds against main
to hold the prefix rc
, for
release candidate. The resulting first version of the first commit on main would be 2023.1.1-rc.0
.
Any subsequent builds for that day would increment the rc
version monotonically. The value would reset to 0
on the following day.
Now let's imagine we want to build containers for each commit of a pull request. For this we'll designate the
prefix to be pr
. We can manually define the prerelease version (as an environment variable) instead of letting
lerna-docker
manually increment one. For example, we could use the Pull Request number and Workflow run as our
prerelease, resulting in a tag such as 2023.1.1-pr.22.53
. The prerelease
value should be a valid SemVer prerelease value.