Language-specific multi-stage Dockerfile patterns for minimal, secure production images.
Go compiles to a static binary, making it ideal for scratch or distroless images.
# ---- Build Stage ----
FROM golang:1.22-alpine AS builder
WORKDIR /build
# Cache dependencies
COPY go.mod go.sum ./
RUN go mod download
# Build static binary
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
-ldflags="-s -w -X main.version=1.0.0" \
-o /app ./cmd/server
# ---- Runtime Stage ----
FROM scratch
# Import CA certificates for HTTPS
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
# Import timezone data
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
# Copy binary
COPY --from=builder /app /app
# Run as non-root (numeric UID since scratch has no /etc/passwd)
USER 65534:65534
EXPOSE 8080
ENTRYPOINT ["/app"]
Result: ~10-15 MB image (vs ~800 MB with full golang image).
FROM golang:1.22 AS builder
WORKDIR /build
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /app ./cmd/server
# distroless/static includes CA certs and tzdata
FROM gcr.io/distroless/static:nonroot
COPY --from=builder /app /app
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/app"]
When to use distroless over scratch:
gcr.io/distroless/static:debug) for troubleshootingFROM golang:1.22 AS builder
WORKDIR /build
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o /app ./cmd/server
# Need glibc for CGO
FROM gcr.io/distroless/base:nonroot
COPY --from=builder /app /app
USER nonroot:nonroot
ENTRYPOINT ["/app"]
cargo-chef separates dependency compilation from source compilation, enabling Docker layer caching for Rust dependencies.
# ---- Chef Stage: Prepare dependency recipe ----
FROM rust:1.77-slim AS chef
RUN cargo install cargo-chef
WORKDIR /build
# ---- Planner Stage: Analyze dependencies ----
FROM chef AS planner
COPY . .
RUN cargo chef prepare --recipe-path recipe.json
# ---- Builder Stage: Build dependencies then source ----
FROM chef AS builder
# Build dependencies (cached unless Cargo.toml/Cargo.lock change)
COPY --from=planner /build/recipe.json recipe.json
RUN cargo chef cook --release --recipe-path recipe.json
# Build application
COPY . .
RUN cargo build --release --bin server
# ---- Runtime Stage ----
FROM gcr.io/distroless/cc:nonroot
COPY --from=builder /build/target/release/server /server
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/server"]
Why cargo-chef? Without it, changing any source file recompiles all dependencies (~5-20 min). With cargo-chef, dependency compilation is cached unless Cargo.toml or Cargo.lock changes.
FROM rust:1.77 AS builder
# Add musl target for static linking
RUN rustup target add x86_64-unknown-linux-musl
RUN apt-get update && apt-get install -y musl-tools && rm -rf /var/lib/apt/lists/*
WORKDIR /build
COPY Cargo.toml Cargo.lock ./
COPY src/ ./src/
RUN cargo build --release --target x86_64-unknown-linux-musl
# Fully static binary -> scratch is fine
FROM scratch
COPY --from=builder /build/target/x86_64-unknown-linux-musl/release/server /server
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
USER 65534:65534
ENTRYPOINT ["/server"]
# ---- Build Stage ----
FROM node:20-alpine AS builder
WORKDIR /app
# Install dependencies (cached unless package files change)
COPY package.json package-lock.json ./
RUN npm ci
# Build application (TypeScript, bundler, etc.)
COPY tsconfig.json ./
COPY src/ ./src/
RUN npm run build
# ---- Production Dependencies Stage ----
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
# ---- Runtime Stage ----
FROM node:20-slim
# Install tini for proper signal handling
RUN apt-get update && apt-get install -y --no-install-recommends tini \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Create non-root user
RUN groupadd -r appuser && useradd -r -g appuser appuser
# Copy production dependencies and built code
COPY --from=deps --chown=appuser:appuser /app/node_modules ./node_modules
COPY --from=builder --chown=appuser:appuser /app/dist ./dist
COPY --chown=appuser:appuser package.json ./
USER appuser
ENV NODE_ENV=production
EXPOSE 3000
ENTRYPOINT ["tini", "--"]
CMD ["node", "dist/server.js"]
Key decisions:
npm ci over npm install for reproducible builds--omit=dev to exclude devDependencies from runtime imagetini to handle PID 1 responsibilities (signal forwarding, zombie reaping)node:20-slim over alpine to avoid musl compatibility issues with native modulesFROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
# next.config.js must have: output: 'standalone'
RUN npm run build
FROM node:20-alpine
WORKDIR /app
RUN addgroup -S appuser && adduser -S -G appuser appuser
# Copy only the standalone output
COPY --from=builder --chown=appuser:appuser /app/.next/standalone ./
COPY --from=builder --chown=appuser:appuser /app/.next/static ./.next/static
COPY --from=builder --chown=appuser:appuser /app/public ./public
USER appuser
ENV NODE_ENV=production
ENV HOSTNAME=0.0.0.0
EXPOSE 3000
CMD ["node", "server.js"]
# ---- Build Stage ----
FROM python:3.12-slim AS builder
# Install uv
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
WORKDIR /app
# Install dependencies into a virtual env (cached layer)
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev --no-install-project
# Copy application source
COPY src/ ./src/
COPY pyproject.toml ./
RUN uv sync --frozen --no-dev
# ---- Runtime Stage ----
FROM python:3.12-slim
WORKDIR /app
# Create non-root user
RUN groupadd -r appuser && useradd -r -g appuser -d /app appuser
# Copy virtual environment from builder
COPY --from=builder --chown=appuser:appuser /app/.venv /app/.venv
COPY --from=builder --chown=appuser:appuser /app/src ./src
# Ensure venv binaries are on PATH
ENV PATH="/app/.venv/bin:$PATH"
ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1
USER appuser
EXPOSE 8000
CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]
FROM python:3.12-slim AS builder
WORKDIR /app
# Create virtual environment
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
FROM python:3.12-slim
WORKDIR /app
RUN groupadd -r appuser && useradd -r -g appuser -d /app appuser
# Copy only the virtual environment
COPY --from=builder --chown=appuser:appuser /opt/venv /opt/venv
COPY --chown=appuser:appuser src/ ./src/
ENV PATH="/opt/venv/bin:$PATH"
ENV PYTHONUNBUFFERED=1
USER appuser
EXPOSE 8000
CMD ["gunicorn", "src.main:app", "-w", "4", "-b", "0.0.0.0:8000"]
Use ARG to parameterize builds without baking values into the final image.
# Build-time arguments
ARG GO_VERSION=1.22
ARG APP_VERSION=dev
FROM golang:${GO_VERSION}-alpine AS builder
ARG APP_VERSION
WORKDIR /build
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w -X main.version=${APP_VERSION}" \
-o /app ./cmd/server
FROM scratch
COPY --from=builder /app /app
ENTRYPOINT ["/app"]
# Pass args at build time
docker build --build-arg APP_VERSION=1.2.3 --build-arg GO_VERSION=1.23 -t myapp .
Important: ARG values before FROM are only available in FROM lines. Redeclare ARG after FROM to use in RUN commands.
Build images for multiple platforms (amd64, arm64) from a single machine.
# Create a builder instance with multi-platform support
docker buildx create --name multiarch --driver docker-container --use
docker buildx inspect --bootstrap
# BUILDPLATFORM = host platform (where build runs)
# TARGETPLATFORM = target platform (where image runs)
FROM --platform=$BUILDPLATFORM golang:1.22-alpine AS builder
ARG TARGETOS
ARG TARGETARCH
WORKDIR /build
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \
go build -ldflags="-s -w" -o /app ./cmd/server
FROM --platform=$TARGETPLATFORM gcr.io/distroless/static:nonroot
COPY --from=builder /app /app
USER nonroot:nonroot
ENTRYPOINT ["/app"]
# Build for multiple platforms and push to registry
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t myregistry/myapp:1.0 \
--push .
# Build for local use (single platform)
docker buildx build \
--platform linux/arm64 \
-t myapp:1.0 \
--load .
| Variable | Example Value | Available In |
|---|---|---|
BUILDPLATFORM |
linux/amd64 |
FROM --platform= |
TARGETPLATFORM |
linux/arm64 |
FROM --platform= |
BUILDOS |
linux |
RUN (after ARG) |
BUILDARCH |
amd64 |
RUN (after ARG) |
TARGETOS |
linux |
RUN (after ARG) |
TARGETARCH |
arm64 |
RUN (after ARG) |