DevOps

Docker multi-stage builds: cutting images by 90%

22 May 2024 · 6 min read · SmartCG

Getting rid of ENOSPC alerts and making deploys fast again with a proper Dockerfile.

The fat image problem

When a Dockerfile does COPY . . and RUN npm install, the final image carries node_modules with everything: dev dependencies, TypeScript sources, tests, build tools. The image weighs gigabytes, servers fill up, and every deploy pushes all of it over the network again.

The fix: multi-stage builds

We split the Dockerfile into stages:

  1. Deps: install all dependencies, including dev ones, with a cached layer.
  2. Builder: copy sources and build the project (TypeScript to JS, Next.js build).
  3. Runner: start from a minimal image (node:alpine), copy only compiled files and production dependencies.

The final image has no sources and no build tooling. Size drops by 80–90%, and the deploy no longer spends most of its time transferring layers that will never be executed.

Two details that matter

Run the container as a non-root user — it costs one line and removes a whole class of escalation paths. And add a health check, so an orchestrator restarts a container that is alive but not serving instead of routing traffic to it.

Services involved

Read next

Deploys slow and servers running out of disk?

We review images, pipelines and the production environment, then make releases predictable instead of eventful.