Containerization is one of the most high-leverage skills in backend development in 2026. A good Docker image gives you reproducible builds, easy deploys to any cloud or Kubernetes cluster, and isolated dependencies that stop "works on my machine" arguments. A bad image is bloated, insecure, and slow to build. This guide shows you the difference.
What changed in 2026
- Docker Desktop 5.x includes BuildKit by default and supports
docker build --cache-from with OCI cache backends, making remote cache sharing straightforward.
- Distroless images from Google and Chainguard are now a standard hardening choice — they contain only the runtime, no shell, no package manager.
- OCI Image Format is the universal standard;
podman and nerdctl are drop-in alternatives to the docker CLI.
- Sigstore / cosign for image signing is expected in regulated environments; supply-chain security is a real concern in 2026.
docker init (introduced in Docker v24) generates a production-ready Dockerfile for common stacks — useful starting point.
Basic Dockerfile (Node example)
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
This works but has problems: it is single-stage, the image includes npm and the full Node image, and it runs as root.
Production multi-stage Dockerfile (Node)
# ---- build stage ----
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# ---- runtime stage ----
FROM node:22-alpine AS runtime
WORKDIR /app
# Create non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
COPY --from=builder /app/dist ./dist
USER appuser
EXPOSE 3000
CMD ["node", "dist/server.js"]
The runtime image has no build tools, no source files, and runs as a non-root user.
Multi-stage Python example
FROM python:3.12-slim AS builder
WORKDIR /build
COPY requirements.txt .
RUN pip install --prefix=/install -r requirements.txt
FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /install /usr/local
COPY src/ ./src/
RUN useradd -m appuser
USER appuser
CMD ["python", "-m", "src.main"]
.dockerignore — what to always exclude
node_modules/
.git/
.env
.env.*
*.log
dist/ # if you build inside Docker
__pycache__/
.pytest_cache/
.coverage
README.md
A missing .dockerignore can accidentally send hundreds of megabytes of context to the Docker daemon.
Image size comparison
| Approach |
Typical size (Node app) |
node:22 single stage |
~1.1 GB |
node:22-alpine single stage |
~180 MB |
| Multi-stage, alpine runtime |
~80 MB |
| Distroless runtime |
~50 MB |
Alpine and multi-stage together get you 10–20× smaller than the naive baseline.
Layer caching strategy
Order Dockerfile instructions from least-changing to most-changing:
# 1. Base image (cached for weeks)
FROM node:22-alpine
# 2. System deps (change rarely)
RUN apk add --no-cache dumb-init
# 3. Package manifest (changes when you add deps)
COPY package*.json ./
RUN npm ci --only=production
# 4. Source code (changes every commit)
COPY . .
If you copy source before npm ci, every code change busts the dependency cache and reinstalls everything.
How to pick a base image
| Base |
When to use |
node:22-alpine |
Most Node apps; small, well-maintained |
python:3.12-slim |
Python apps; slim drops unnecessary Debian packages |
golang:1.23-alpine as builder only |
Build stage; ship the binary in scratch or distroless |
gcr.io/distroless/base |
Hardened runtime, no shell — good for compiled languages |
ubuntu:24.04 |
Only if you need apt packages unavailable in alpine |
Common mistakes
No .dockerignore. Node's node_modules folder alone can be hundreds of MB sent to the build daemon on every build.
Secrets baked into image layers. Never COPY .env or RUN export SECRET=... — secrets are visible in docker history. Use build args only for non-secret config; pass real secrets at runtime via environment variables or secret mounts.
Single-stage builds with build tools in production. Compilers, bundlers, and test runners bloat the image and expand the attack surface.
latest tag in FROM. Pin to a specific version tag; CI pipelines that pull latest can silently break when a major version ships.
Not adding a health check. Docker and Kubernetes need to know your container is healthy:
HEALTHCHECK --interval=30s --timeout=5s \
CMD curl -f http://localhost:3000/health || exit 1
What to skip
- Installing an SSH server inside a container — use
docker exec or ephemeral debug containers instead.
- Running multiple processes with a generic process manager like
supervisord unless you have a very specific reason; prefer one process per container.
- Building images on production machines — build in CI, push to a registry, pull to prod.
FAQ
How do I pass environment variables at runtime?
Use docker run -e KEY=value or --env-file .env. Never bake secrets into the image.
What is dumb-init and do I need it?
dumb-init is a minimal init that properly forwards signals and reaps zombie processes. Use it when your CMD is a Node or Python process that does not handle signals well on its own.
How do I reduce build times in CI?
Enable BuildKit (DOCKER_BUILDKIT=1), use --cache-from pointing to your registry, and structure the Dockerfile for maximum cache reuse (deps before source).
When should I use Podman instead of Docker?
Podman is daemonless and runs containers rootless by default, which is preferred in some enterprise Linux environments. The Dockerfile syntax is identical.
Where to go next