Build optimized, secure Dockerfiles and docker-compose setups — multi-stage builds, layer caching, security hardening, health checks, networking, volumes, and production-ready templates for Python, Node.js, Go, Rust, and static sites.
Build production-ready Docker images and compose stacks. Covers multi-stage builds, layer optimization, security hardening, and templates for common tech stacks.
1. Order layers by change frequency — system deps → app deps → source code
2. Use slim/distroless base images — smaller surface area, fewer CVEs
3. Never run as root — always create and switch to non-root user
4. Multi-stage builds — separate build tools from runtime
5. Pin versions — base image tags, package versions, everything
6. .dockerignore — exclude .git, __pycache__, .env, node_modules, .venv
7. Health checks — every service needs one
8. No secrets in images — use build args, env vars, or secret mounts
.git
.gitignore
.env
.env.*
*.md
LICENSE
docker-compose*.yml
Dockerfile*
.dockerignore
__pycache__
*.pyc
.pytest_cache
.mypy_cache
.ruff_cache
.venv
venv
node_modules
.next
dist
build
coverage
.DS_Store
*.log
# ---- Build stage ----
FROM python:3.12-slim AS builder
WORKDIR /build
# System build deps (compiled packages like psycopg2, numpy)
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
# Install Python deps into a prefix we can copy
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
# ---- Runtime stage ----
FROM python:3.12-slim
# Runtime system deps only (no compiler)
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq5 \
curl \
&& rm -rf /var/lib/apt/lists/*
# Copy installed packages from builder
COPY --from=builder /install /usr/local
WORKDIR /app
# Copy application code
COPY src/ src/
# Non-root user
RUN useradd -r -s /bin/false -d /app appuser \
&& chown -R appuser:appuser /app
USER appuser
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD curl -f http://localhost:8000/health || exit 1
CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
FROM python:3.12-slim AS builder
WORKDIR /build
COPY pyproject.toml .
COPY src/ src/
RUN pip install --no-cache-dir --prefix=/install .
FROM python:3.12-slim
COPY --from=builder /install /usr/local
WORKDIR /app
COPY src/ src/
RUN useradd -r -s /bin/false appuser
USER appuser
EXPOSE 8000
CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]
FROM python:3.12-slim
# Install uv
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
WORKDIR /app
# Install deps (cached unless lock changes)
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev --no-editable
# Copy app code
COPY src/ src/
RUN useradd -r -s /bin/false appuser
USER appuser
EXPOSE 8000
CMD ["uv", "run", "uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]
FROM python:3.12-slim AS builder
WORKDIR /build
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
FROM python:3.12-slim
COPY --from=builder /install /usr/local
WORKDIR /app
COPY . .
# Collect static files at build time
RUN python manage.py collectstatic --noinput
RUN useradd -r -s /bin/false appuser \
&& chown -R appuser:appuser /app
USER appuser
EXPOSE 8000
CMD ["gunicorn", "myproject.wsgi:application", \
"--bind", "0.0.0.0:8000", \
"--workers", "4", \
"--timeout", "120", \
"--access-logfile", "-"]
# ---- Build stage ----
FROM node:20-slim AS builder
WORKDIR /build
# Install deps (cached unless lock changes)
COPY package.json package-lock.json ./
RUN npm ci --ignore-scripts
# Build
COPY . .
RUN npm run build
# Prune dev dependencies
RUN npm prune --production
# ---- Runtime stage ----
FROM node:20-slim
# Security: non-root user (node image has 'node' user built in)
USER node
WORKDIR /home/node/app
# Copy only production deps + built output
COPY --from=builder --chown=node:node /build/node_modules ./node_modules
COPY --from=builder --chown=node:node /build/dist ./dist
COPY --from=builder --chown=node:node /build/package.json .
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1))"
CMD ["node", "dist/server.js"]
FROM node:20-slim AS builder
WORKDIR /build
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-slim
USER node
WORKDIR /home/node/app
# Next.js standalone output (minimal)
COPY --from=builder --chown=node:node /build/.next/standalone ./
COPY --from=builder --chown=node:node /build/.next/static ./.next/static
COPY --from=builder --chown=node:node /build/public ./public
EXPOSE 3000
ENV HOSTNAME=0.0.0.0
CMD ["node", "server.js"]
FROM oven/bun:1 AS builder
WORKDIR /build
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile
COPY . .
RUN bun run build
FROM oven/bun:1-slim
USER bun
WORKDIR /home/bun/app
COPY --from=builder --chown=bun:bun /build/dist ./dist
COPY --from=builder --chown=bun:bun /build/node_modules ./node_modules
COPY --from=builder --chown=bun:bun /build/package.json .
EXPOSE 3000
CMD ["bun", "run", "dist/server.js"]
# ---- Build stage ----
FROM golang:1.22-alpine AS builder
WORKDIR /build
# Cache deps
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" -o /app ./cmd/server
# ---- Runtime stage (scratch = empty image) ----
FROM scratch
# TLS certificates (needed for HTTPS calls)
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
# Binary
COPY --from=builder /app /app
EXPOSE 8080
ENTRYPOINT ["/app"]
FROM golang:1.22-alpine 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
FROM gcr.io/distroless/static-debian12
COPY --from=builder /app /app
EXPOSE 8080
ENTRYPOINT ["/app"]
# ---- Planner ----
FROM rust:1.77-slim AS planner
RUN cargo install cargo-chef
WORKDIR /build
COPY . .
RUN cargo chef prepare --recipe-path recipe.json
# ---- Builder ----
FROM rust:1.77-slim AS builder
RUN cargo install cargo-chef
WORKDIR /build
# Cache deps (only re-runs when Cargo.toml/lock changes)
COPY --from=planner /build/recipe.json recipe.json
RUN cargo chef cook --release --recipe-path recipe.json
# Build app
COPY . .
RUN cargo build --release
# ---- Runtime ----
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder /build/target/release/myapp /usr/local/bin/myapp
RUN useradd -r -s /bin/false appuser
USER appuser
EXPOSE 8080
CMD ["myapp"]
# ---- Build ----
FROM node:20-slim AS builder
WORKDIR /build
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# ---- Serve ----
FROM nginx:1.25-alpine
# Custom nginx config
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Built assets
COPY --from=builder /build/dist /usr/share/nginx/html
# Non-root (nginx alpine supports this)
RUN chown -R nginx:nginx /usr/share/nginx/html \
&& chown -R nginx:nginx /var/cache/nginx \
&& chown -R nginx:nginx /var/log/nginx \
&& touch /var/run/nginx.pid \
&& chown nginx:nginx /var/run/nginx.pid
USER nginx
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s \
CMD wget -qO- http://localhost:8080/ || exit 1
server {
listen 8080;
server_name _;
root /usr/share/nginx/html;
index index.html;
# SPA fallback
location / {
try_files $uri $uri/ /index.html;
}
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# Gzip
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml;
}