A multi-stage build uses several FROM blocks in one Dockerfile: heavy "builder" stages compile and produce artifacts, and a lightweight final stage copies only the artifacts. Build tools, compilers, caches, and test dependencies never reach the shipped image — making it smaller and reducing the attack surface.
# ---- Stage 1: build ----
FROM node:20 AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# ---- Stage 2: runtime ----
FROM node:20-slim
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
EXPOSE 3000
USER node
CMD ["node", "dist/server.js"]
Notes:
COPY --from=build pulls files from a previous stage instead of the build context.docker build --target build . (useful for CI test stages).docker image ls # sizes at a glance
docker history my-api:1.0 # size contributed by each layer
docker buildx build --sbom=true . # SBOM (with BuildKit)
# interactive layer-by-layer exploration:
# dive my-api:1.0 (github.com/wagoodman/dive)
Shrinking techniques, in order of impact:
-slim, -alpine, distroless, or FROM scratch for static binaries.RUN: apt-get update && apt-get install -y --no-install-recommends ... && rm -rf /var/lib/apt/lists/*; pip install --no-cache-dir..dockerignore so tests, docs, and .git never enter the context.COPY what the app needs to run — not the whole repository.Treat images as dependencies: scan them continuously in CI and on schedule.
docker scout cves my-api:1.0 # Docker Scout (built into Docker Desktop)
trivy image my-api:1.0 # Trivy — very common in CI pipelines
grype my-api:1.0 # Grype (Anchore)
FROM node:20-slim@sha256:abcd....RUN useradd -r app && USER app in the Dockerfile, or docker run --user 1000:1000. Containers running as root can escape more easily on kernel vulnerabilities.docker run --read-only --tmpfs /tmp ... — the app can only write to explicitly mounted volumes.--cap-drop=ALL --cap-add=NET_BIND_SERVICE — grant back only what is needed (e.g., binding port 80).--security-opt no-new-privileges./var/run/docker.sock into a container unless it is a trusted CI tool — it is effectively root on the host.Rules of thumb: never COPY secrets into images, never put them in ENV/ARG (they persist in image metadata and docker history), and never commit them. Options:
# Build-time secret via BuildKit — never stored in a layer:
# syntax=docker/dockerfile:1
RUN --mount=type=secret,id=npm_token \
NPM_TOKEN=$(cat /run/secrets/npm_token) npm ci
# docker build --secret id=npm_token,src=.npmrc .
_FILE convention), Docker Swarm secrets, Kubernetes Secrets (ideally with encryption/external backing), or a vault (Vault, AWS Secrets Manager, Azure Key Vault).# Self-hosted registry (with basic auth via reverse proxy or htpasswd):
docker run -d -p 5000:5000 --restart always --name registry registry:2
# Cloud registries: AWS ECR, Azure ACR, Google GAR/Artifact Registry
docker login myregistry.corp.com
docker tag my-api:1.0 myregistry.corp.com/team/my-api:1.0
docker push myregistry.corp.com/team/my-api:1.0
A typical pipeline per commit: build → test (inside containers) → scan → push → deploy.
# GitHub Actions example
- name: Set up Buildx
uses: docker/setup-buildx-action@v3
- name: Login to registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: |
ghcr.io/org/app:${{ github.sha }}
ghcr.io/org/app:latest
cache-from: type=registry,ref=ghcr.io/org/app:cache
cache-to: type=registry,ref=ghcr.io/org/app:cache,mode=max
git SHA) deployed in production; semantic versions for releases; avoid relying on latest for deployments.Docker alone manages a single host. You need an orchestrator when you require multi-host scheduling, self-healing, load balancing, rolling deployments, and scaling:
| Option | Best for | Notes |
|---|---|---|
| Docker Swarm | Small clusters, simplicity | Built into Docker; declining adoption |
| Kubernetes | The de-facto standard at any serious scale | Steep learning curve, huge ecosystem |
| Managed services | AWS/Azure workloads without cluster ops | ECS/EKS/Fargate on AWS, AKS on Azure, GKE on GCP |
Related pages in this knowledge base:
Images and runtimes are standardized by the Open Container Initiative (OCI): an image spec and a runtime spec. Consequences:
docker build output runs unchanged (alias docker=podman).docker stop grace period / stop_grace_period tuned).--memory, --cpus) and a restart policy set.latest.