· 4 min read
How to Write a Dockerfile That Builds Fast
Heshan Fernando
Co-founder & COO
Every commit triggers a build that reinstalls all your dependencies from scratch. It takes four minutes, it happens twenty times a day, and the fix is moving two lines.
Docker’s layer cache is the single largest lever on build time, and it’s controlled entirely by the order of instructions in the file.
How the layer cache works
Each instruction in a Dockerfile creates a layer. Docker caches them, and on a rebuild it reuses every layer up to the first one whose inputs have changed — then rebuilds that layer and everything after it.
So the order determines what survives a code change. Consider the wrong version:
COPY . .
RUN npm ci
Copying the source first means any code change invalidates that layer, which invalidates the install below it. Every commit reinstalls everything.
Now the right version:
COPY package*.json ./
RUN npm ci
COPY . .
The install layer depends only on the lockfile. Change application code and Docker reuses the cached install, going straight to the copy. Four minutes becomes fifteen seconds.
The general rule: put the things that change rarely near the top, and the things that change constantly near the bottom.
Multi-stage builds
A build needs compilers, dev dependencies and build tooling. A running container needs none of them.
Multi-stage builds use one stage to build and a second to run, copying only the output across. The final image contains the artefact and the runtime, and nothing else — often a fraction of the size, with a correspondingly smaller attack surface.
Non-root by default
By default a container process runs as root inside the container. That isn’t the same as root on the host, but it widens what a compromised process or a mounted volume can reach.
Adding a user and switching to it is two lines, and it removes a whole class of risk for no operational cost. There’s no good reason for an application container to run as root.
| Practice | Cost | Benefit |
|---|---|---|
| Dependency layer above source | Two lines reordered | Minutes per build |
| Multi-stage build | A few extra lines | Much smaller image |
| Non-root user | Two lines | Smaller blast radius |
.dockerignore | One file | Faster builds, no leaked files |
Common mistakes to avoid
- Copying the whole source before installing dependencies — the classic slow-build cause.
- Omitting
.dockerignore, sonode_modules,.gitand local artefacts are sent as build context and sometimes end up in the image. - Using
latestas a base image tag, which makes builds non-reproducible. - Running
apt-get updatein a separate layer fromapt-get install, so a cached update layer serves stale package lists. - Adding secrets as build arguments, which persist in the image history.
How to do it with Dockerfile Builder
The Dockerfile Builder generates a file with the ordering and the user already correct.
- Choose the runtime and whether the build needs a compile step.
- Keep the dependency install above the source copy — the generated file does this.
- Copy both the Dockerfile and the generated
.dockerignore; the ignore file is half the size saving. - Build once and check the image size, then change something small and confirm the cache holds.
Docker’s own best practices guide covers the full set. Other developer tools are in the tools directory.
Frequently asked questions
Why run as a non-root user?
Because a process compromised inside the container is root inside it by default, which widens what an escape or a mounted volume exposes. Adding a user is two lines.
Why does layer order matter so much?
Docker invalidates the cache from the first changed layer onward. Installing dependencies before copying source keeps that expensive step cached across code changes.
Is .dockerignore really necessary?
Yes. Without it the build context includes node_modules, .git and local artefacts, which slows every build and can leak files into the image.
Final thought
If your builds are slow, look at the order before anything else. Almost every four-minute rebuild is one misplaced COPY away from being fifteen seconds.