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:
- Deps: install all dependencies, including dev ones, with a cached layer.
- Builder: copy sources and build the project (TypeScript to JS, Next.js build).
- 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
Building a resilient CI/CD pipeline on GitHub Actions
A pipeline on GitHub Actions with security checks, zero-downtime deploys and logs delivered to Telegram.
ArchitectureMoving from a legacy monolith to services without downtime
A practical account of splitting a legacy monolith for a logistics company without a single maintenance window.
BackendNestJS vs Express: why we choose a framework with architecture
The end of spaghetti backends: what dependency injection and a strict structure actually buy you.
Deploys slow and servers running out of disk?
We review images, pipelines and the production environment, then make releases predictable instead of eventful.