Skip to content

3. A Dockerfile instead of automatic build detection

Context

Deployments originally used Nixpacks, which inspects a repository and generates a build automatically. It worked, but produced two problems.

The image was around 2 GB, because automatic detection keeps the entire build toolchain in the final image. And production was effectively running the development preview server, because the build emits a web-standard fetch handler rather than a listening server, and nothing in the generated setup knew how to host that handler properly.

Decision

Write an explicit multi-stage Dockerfile, and a small production server (serve.mjs) that hosts the built fetch handler on Node and serves the built client assets alongside it.

Consequences

The image is roughly a third of the size, which matters on a server whose disk has filled once already.

Production runs a real server rather than a preview tool intended for local use.

The build is explicit and reviewable. What goes into the image is visible in a file in the repository rather than inferred by a tool.

More is now the project's responsibility. Base image versions, dependency installation flags, and the runtime entry point are all maintained by hand. Automatic detection would have adjusted to changes on its own.

Build-time variables became a visible concern. The Dockerfile declares exactly three build arguments, all of them public by design. This made it obvious that marking other variables as build-time available writes their values into build logs, which is how credentials were exposed on this project.

What the test caught

The Dockerfile was built and run locally before being deployed, deliberately, rather than pushed and observed in production.

The first build failed at runtime: every server-rendered route returned an error because the runtime image had no node_modules. An earlier check for bare imports in the bundle had used a search pattern that missed them, leading to the wrong conclusion that the bundle was self-contained. It imports twenty-seven packages.

Deploying that directly would have taken the site down. The separate dependency stage exists because of this. The general lesson is recorded here because it generalises: verify a build locally before it becomes the only way the site runs.

Internal engineering documentation.