Docker, but boring on purpose
Reproducible beats clever. A pragmatic baseline for containers that you can hand to anyone.
Reproducible beats clever. The most reliable container I ever shipped was also the most boring one — a base image everyone recognized, three layers, no entrypoint script doing anything surprising. The clever ones, the multi-stage acrobatics tuned to shave forty megabytes, were the ones that woke someone up at 2 a.m. when a base image moved and nobody could remember why.
A Dockerfile is not a place to be interesting. It is a contract: this is what runs, these are its inputs, and anyone on the team should be able to read it top to bottom and predict the result. Optimize for the next person who has to debug it at 2 a.m., because that person is probably you.
Pin everything, then pin it again
The fastest way to break a build six months from now is to trust a moving tag. node:latest is a promise that the floor will shift under you. Even node:20 drifts as patch releases land. Pin to a digest, or at minimum a full patch version, and let a bot bump it on purpose rather than by accident.
- →Pin the base image to a specific tag, ideally a digest.
- →Pin your package manager lockfile and copy it in before installing, so the dependency layer caches on its own.
- →Set a fixed working directory and a non-root user. Both are one line. Both save you later.
The goal is that docker build on your laptop today and on CI in March produce the same bytes for the same inputs. When they don't, you want the diff to be in your code, not in some upstream tag you never chose to upgrade.
Layers are a cache, so order them like one
Docker caches layers in order, and the first line that changes busts everything below it. So put the things that rarely change at the top and the things that change every commit at the bottom. Copy the lockfile and install dependencies before you copy your source. Do that one reordering and most builds drop from minutes to seconds, because the dependency layer survives untouched across every code change.
A Dockerfile you can read in thirty seconds is worth more than one that builds thirty seconds faster.
Multi-stage builds earn their keep here, and this is the one place I'll spend complexity: build in a fat image with the compilers and the dev dependencies, then copy only the artifact into a slim runtime image. The build stage can be as messy as it needs to be. The stage that ships should contain exactly what runs in production and nothing you'd have to explain in a security review.
Boring on purpose — lockfile first, source last, slim runtime.
Make the container honest about itself
A good container tells you how it's doing without you having to guess. Add a healthcheck so the orchestrator knows the difference between started and ready. Log to stdout and stderr and let the platform collect it — no log files inside the container to fill a disk you forgot about. Pass configuration through environment variables, never baked in, so the same image runs in staging and production unchanged.
That last point is the one teams skip, and it's the one that matters most. The same image, byte for byte, should move from your laptop through CI to production. If staging and production run different images, you are not testing what you ship. The whole promise of containers — that it works the same everywhere — only holds if you let one artifact travel the whole way.
Boring containers don't win demos. They win the quiet Tuesday when a deploy goes out, behaves exactly like the last one, and nobody has to think about it. That predictability is the entire point. Hand someone a Dockerfile they can read, trust, and reproduce, and you've given them something better than clever — you've given them one less thing to worry about.
