Devops
Difficulty: Advanced
20 min read

Advanced Docker: Multi-stage Builds, Healthchecks and Security

Advanced Docker guide for production: multi-stage builds for images 10x lighter, reliable healthchecks, image and runtime security, production-ready Docker Compose, and a multi-arch CI/CD pipeline.

Back to tutorials
Prerequisites
This tutorial assumes basic knowledge of Docker (images, containers, Dockerfile, docker compose). If you are a beginner, first check out the Docker installation and configuration guide and the Docker installation guide.

Why "basic" Docker is not enough in production

The vast majority of teams adopting Docker start with a simple Dockerfile: an official image, a few RUN instructions, a CMD, and it goes to production. It works. Until the day it doesn't.

The typical problems with naive Dockerfiles in production are always the same. A 2 GB image that takes 8 minutes to build in CI. A container running as root and exposing useless system binaries. A service that declares itself "up" while it no longer responds to requests. Secrets passed as ARG visible in docker history. A docker compose up that starts the application before the database is ready.

These problems have proven and documented solutions. This guide covers them exhaustively:

  • Multi-stage builds: images 5 to 100x lighter depending on the language
  • Layer optimization: fast builds thanks to caching
  • Production-ready healthchecks: reliable service ordering
  • Image security: non-root user, minimal images, vulnerability scanning
  • Runtime security: capabilities, seccomp, network isolation
  • Production Docker Compose: override files, secrets, resource limits
  • Registry and CI/CD: multi-arch, signing, GitHub Actions pipeline

1. Multi-stage builds

The principle of multi-stage builds is simple: use several successive images in a single Dockerfile to separate the compilation phase from the execution phase. The final image contains only what is strictly necessary to run the application.

Complete example: Go application

Go is the perfect example to illustrate the gains: the Go SDK weighs around 850 MB. A statically compiled binary weighs a few megabytes.

# syntax=docker/dockerfile:1

# ── Stage 1: Build ────────────────────────────────────────────────────
FROM golang:1.22-alpine AS builder

WORKDIR /app

# Copy the dependency files first for caching
COPY go.mod go.sum ./
RUN go mod download

# Copy the source code and compile a static binary
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
    go build -ldflags="-s -w" -trimpath -o /app/server ./cmd/server

# ── Stage 2: Runtime ──────────────────────────────────────────────────
FROM scratch

# Copy the TLS certificates (required for HTTPS calls)
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/

# Copy only the compiled binary
COPY --from=builder /app/server /server

EXPOSE 8080

ENTRYPOINT ["/server"]

Result: the final image weighs between 8 and 15 MB instead of 850 MB. The -s -w ldflags options strip the debug symbols and DWARF information. -trimpath removes local paths from the binary for reproducibility.

Complete example: Node.js application

For Node.js, the challenge is to separate the devDependencies (build tools, TypeScript, etc.) from the runtime dependencies.

# syntax=docker/dockerfile:1

# ── Stage 1: Install all dependencies ──────────────────
FROM node:20-alpine AS deps

WORKDIR /app
COPY package*.json ./

# Install ALL dependencies (dev + prod) for the build
RUN --mount=type=cache,target=/root/.npm \
    npm ci

# ── Stage 2: TypeScript build ─────────────────────────────────────────
FROM deps AS builder

COPY . .
RUN npm run build

# ── Stage 3: Minimal runtime ──────────────────────────────────────────
FROM node:20-alpine AS runner

RUN addgroup -S nodejs && adduser -S nextjs -G nodejs

WORKDIR /app

# Copy only the build artifacts and the production dependencies
COPY --from=builder --chown=nextjs:nodejs /app/dist ./dist
COPY --from=builder --chown=nextjs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nextjs:nodejs /app/package.json ./

USER nextjs

EXPOSE 3000
ENV NODE_ENV=production

CMD ["node", "dist/index.js"]

Pattern with a specific target --target

A single Dockerfile can serve several environments thanks to targets. Each stage inherits from the previous one and adds its specific layer.

# syntax=docker/dockerfile:1

FROM python:3.12-slim AS base
WORKDIR /app
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install --no-compile -r requirements.txt

# ── Target dev: debug tools ───────────────────────────────────────
FROM base AS dev
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install debugpy pytest ipdb watchfiles
COPY . .
CMD ["python", "-m", "uvicorn", "main:app", "--reload", "--host", "0.0.0.0"]

# ── Target test: running the tests ──────────────────────────────────
FROM base AS test
COPY requirements-test.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements-test.txt
COPY . .
CMD ["pytest", "-v", "--cov=app", "--cov-report=xml"]

# ── Target prod: minimal secure image ─────────────────────────────
FROM python:3.12-slim AS prod
COPY --from=base /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
WORKDIR /app
COPY . .
RUN groupadd -r appuser && useradd -r -g appuser -s /sbin/nologin appuser
USER appuser
EXPOSE 8000
CMD ["gunicorn", "main:app", "-w", "4", "-k", "uvicorn.workers.UvicornWorker", "-b", "0.0.0.0:8000"]
# Build a specific target
docker buildx build --target dev -t myapp:dev .
docker buildx build --target test -t myapp:test .
docker buildx build --target prod -t myapp:prod .

# In CI/CD: build only the prod target
docker buildx build --target prod --push -t registry.example.com/myapp:1.2.3 .
Typical gains per language
Go: from 850 MB to 8-15 MB (static binary on scratch). Node.js: from 1.1 GB to 150-250 MB. Python: from 900 MB to 180-250 MB. Java: from 700 MB to 80-120 MB with jlink. The gains are proportionally larger for compiled languages.

Premium Content

This advanced tutorial is reserved for premium members.

9,90€ / month
  • All advanced tutorials
  • New content every week
  • Progress tracking
  • Cancel anytime

Written by

Morgann Riu

Cybersecurity and Linux administration expert. I share my knowledge through free tutorials and training to help system administrators and developers secure their infrastructures.

Share this tutorial

Did you enjoy this article?

Comments

Checklist Sécurité Linux

30 points essentiels pour sécuriser un serveur Linux. Recevez aussi les nouveaux tutoriels par email.

Pas de spam. Désabonnement en 1 clic.