Reproducible builds with npm
Background
Previously, we’ve been using a combination of git submodule
, npm install
, and npm install --force
to build the blog. This has led to non-reproducible builds and a lot of confusion. Here’s how we can fix it.
One: switch from git submodule
to npm
for theme package
Previously, we used git submodule
to add the theme to the blog.
This caused some issues, as the theme was also a npm package with its own dependencies and build process. hexo generate
would build them concurrently.
We added the theme git repo to package.json.
Two: switch from npm install
to npm ci
in base target
Current blog is packaged with npm [1]: https://docs.npmjs.com/about-packages-and-modules. The base needs to provide the necessary dependencies for the blog to build.
Two files are important here:
- package.json: specifies the package name, version, and dependencies.
- package-lock.json
We do not want to use npm install
here because:
- it does not delete
node_modules
tree. - it modifies
package.json
if a new package is added. - it generates
package-lock.json
to describe the exact tree that was generated.
Leading to non-reproducible builds.
For instance, consider calling the following problematic Dockerfile
1 | FROM node:22.5.1-alpine3.19 AS base |
A correct base
Dockerfile target is:
1 | FROM node:22.5.1-alpine3.19 AS base |
npm ci
installs the exact dependencies listed in package-lock.json
and does not modify package.json
or package-lock.json
.
Just build
The build target should not contain any npm install
or npm ci
commands. It should only contain the build command as everything should be installed in the base target.
1 | FROM base AS build |
Three: handling updates to package.json
and package-lock.json
This required a little bit of creativity. Here’s what we want to accomplish:
- A dev env a.k.a “change, build, test” workflow in “real-time”.
- Avoid maintain two separate Dockerfiles.
- Allow changes to
package.json
andpackage-lock.json
to git-commit.
The dev
service
From the base
target, mount the source tree (instead of COPY
). Ergo, the dev can/must run npm install
/ npm ci
to update dependencies or node modules.
Then npm run build
to build the site with any new changes.
The service runs a persistent shell process.
[!NOTE]
If you wanted to use docker
to re-build then you should probably use the prod target of the main Dockerfile instead.
The serve
service
Since we cannot now serve (i.e. run nginx
) from the same container we are goingto use a separate service for that.
The serice as such binds to the source tree where public/
is generated by dev
service and runs nginx with default args.
Notes:
npm ci
disregardsnode_modules
. Hence, using devcontainer will not affect production builds unlesspackage*.json
is/are updated.