Skip to content
Unverified — AI-generated content. Help verify this page

Production Dockerfiles ​

Why It Exists ​

Most Dockerfiles found in tutorials and quickstarts are designed for development simplicity, not production reliability. They run as root, use :latest tags, install dev dependencies, skip health checks, and produce images 10-50x larger than necessary. In production, every one of these shortcuts becomes a security vulnerability, a deployment bottleneck, or a debugging nightmare.

This page provides battle-tested Dockerfiles for the three most common backend languages — Node.js (TypeScript), Go, and Python — along with the reasoning behind every line. These are not toy examples — they include signal handling, health checks, security hardening, caching optimization, and multi-architecture support.

First Principles ​

The Production Dockerfile Requirements ​

Every production Dockerfile must satisfy:

RequirementWhy
Non-root userLimit blast radius of container escape
Specific image tags (with SHA)Reproducible builds, no surprise updates
Multi-stage buildMinimize image size and attack surface
Health checkOrchestrator-aware lifecycle management
Signal handling (PID 1)Graceful shutdown, no zombie processes
Read-only filesystem compatiblePrevent malware persistence
No secrets in layersPrevent credential leaks
Layer cache optimizationFast CI/CD builds
Minimal runtime dependenciesFewer CVEs, smaller images
Metadata labelsImage provenance and traceability

The Production Image Size Target ​

Core Mechanics — Node.js (TypeScript) ​

Complete Production Dockerfile ​

dockerfile
# syntax=docker/dockerfile:1

# ============================================================
# Node.js TypeScript Production Dockerfile
# ============================================================
# Image size: ~120MB (Alpine) or ~90MB (distroless)
# Build time: ~30s (cold) / ~10s (cached code change)
# Security: non-root, read-only FS, no dev deps, tini
# ============================================================

# ------ Stage 1: Base ------
# Pinned to specific SHA for reproducibility
FROM node:20.11.1-alpine3.19 AS base
RUN apk add --no-cache tini
WORKDIR /app

# ------ Stage 2: Dependencies ------
FROM base AS deps
COPY package.json package-lock.json ./

# Use cache mount for faster rebuilds
RUN --mount=type=cache,target=/root/.npm \
    npm ci --ignore-scripts && \
    npm cache clean --force

# ------ Stage 3: Build ------
FROM deps AS builder
COPY tsconfig.json ./
COPY src/ ./src/

# Run TypeScript compilation
RUN npm run build

# Prune dev dependencies after build
RUN npm prune --production

# ------ Stage 4: Production ------
FROM base AS production

# Security: create application user
RUN addgroup -g 10001 -S appgroup && \
    adduser -u 10001 -S appuser -G appgroup \
    -h /home/appuser -s /sbin/nologin

# Copy production node_modules from builder (already pruned)
COPY --from=builder --chown=appuser:appgroup /app/node_modules ./node_modules

# Copy compiled JavaScript
COPY --from=builder --chown=appuser:appgroup /app/dist ./dist

# Copy package.json for runtime metadata
COPY --chown=appuser:appgroup package.json ./

# Create writable directories for read-only FS compatibility
RUN mkdir -p /tmp/app && chown appuser:appgroup /tmp/app

# Environment
ENV NODE_ENV=production
ENV PORT=3000

# OCI metadata labels
LABEL org.opencontainers.image.title="API Server"
LABEL org.opencontainers.image.description="Production Node.js API"
LABEL org.opencontainers.image.vendor="Company"
LABEL org.opencontainers.image.source="https://github.com/company/api-server"

# Security: switch to non-root user
USER appuser

# Expose port
EXPOSE 3000

# Health check
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
    CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1

# Use tini for proper signal handling and zombie reaping
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "dist/server.js"]

Corresponding Application Code ​

typescript
// src/server.ts — Production-ready server entry point
import http from 'http';

const PORT = parseInt(process.env.PORT ?? '3000', 10);

// Track server state for graceful shutdown
let isShuttingDown = false;
let activeConnections = new Set<http.ServerResponse>();

const server = http.createServer((req, res) => {
  // Track active connections
  activeConnections.add(res);
  res.on('close', () => activeConnections.delete(res));

  // Health endpoints
  if (req.url === '/health' || req.url === '/health/live') {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ status: 'ok', uptime: process.uptime() }));
    return;
  }

  if (req.url === '/health/ready') {
    if (isShuttingDown) {
      res.writeHead(503, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ status: 'shutting_down' }));
      return;
    }
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ status: 'ready' }));
    return;
  }

  // Application routes...
  res.writeHead(200, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify({ message: 'Hello from production' }));
});

// Graceful shutdown handler
function gracefulShutdown(signal: string) {
  console.log(`Received ${signal}, starting graceful shutdown...`);
  isShuttingDown = true;

  // Stop accepting new connections
  server.close(() => {
    console.log('Server closed, all connections drained');
    process.exit(0);
  });

  // Close idle keep-alive connections
  for (const res of activeConnections) {
    if (!res.headersSent) {
      res.setHeader('Connection', 'close');
    }
  }

  // Force exit after timeout
  // Leave buffer before SIGKILL: terminationGracePeriodSeconds(30) - preStop(5) = 25s
  setTimeout(() => {
    console.error('Forced shutdown after timeout');
    process.exit(1);
  }, 20_000);
}

process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));

server.listen(PORT, '0.0.0.0', () => {
  console.log(`Server listening on port ${PORT}`);
});

NestJS Variant ​

dockerfile
# syntax=docker/dockerfile:1

FROM node:20-alpine AS base
RUN apk add --no-cache tini
WORKDIR /app

FROM base AS deps
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci

FROM deps AS builder
COPY tsconfig.json tsconfig.build.json nest-cli.json ./
COPY src/ ./src/
RUN npm run build
RUN npm prune --production

FROM base AS production
RUN addgroup -g 10001 -S appgroup && \
    adduser -u 10001 -S appuser -G appgroup -s /sbin/nologin

COPY --from=builder --chown=appuser:appgroup /app/node_modules ./node_modules
COPY --from=builder --chown=appuser:appgroup /app/dist ./dist
COPY --chown=appuser:appgroup package.json ./

ENV NODE_ENV=production
USER appuser
EXPOSE 3000

HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
    CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1

ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "dist/main.js"]

Core Mechanics — Go ​

Complete Production Dockerfile ​

dockerfile
# syntax=docker/dockerfile:1

# ============================================================
# Go Production Dockerfile
# ============================================================
# Image size: ~12MB (scratch) or ~15MB (distroless)
# Build time: ~20s (cold) / ~5s (cached code change)
# Security: static binary, scratch base, no shell
# ============================================================

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

# Install CA certificates and timezone data (for scratch image)
RUN apk add --no-cache ca-certificates tzdata git

WORKDIR /app

# Cache Go modules
COPY go.mod go.sum ./
RUN go mod download && go mod verify

# Copy source code
COPY . .

# Build static binary with all optimizations
# -ldflags: -w removes DWARF debug info, -s removes symbol table
# CGO_ENABLED=0: static binary (no libc dependency)
# -trimpath: removes build paths from binary
ARG VERSION=dev
ARG COMMIT=unknown
ARG BUILD_TIME=unknown

RUN CGO_ENABLED=0 GOOS=linux go build \
    -ldflags="-w -s \
    -X main.Version=${VERSION} \
    -X main.Commit=${COMMIT} \
    -X main.BuildTime=${BUILD_TIME}" \
    -trimpath \
    -o /app/server ./cmd/server

# ------ Stage 2: Test (CI only) ------
FROM builder AS tester
RUN go test -v -race -coverprofile=coverage.out ./...

# ------ Stage 3: Production (scratch) ------
FROM scratch AS production

# 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

# Create non-root user (scratch has no useradd)
COPY --from=builder /etc/passwd /etc/passwd
COPY --from=builder /etc/group /etc/group

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

# Labels
LABEL org.opencontainers.image.title="API Server"
LABEL org.opencontainers.image.source="https://github.com/company/api-server"

# Run as non-root (must exist in /etc/passwd copied above)
USER nobody:nogroup

EXPOSE 8080

# No health check in scratch (no tools available)
# Use Kubernetes probes instead

ENTRYPOINT ["/server"]

Alternative with distroless (when you need gRPC health checks):

dockerfile
# For gRPC services, use distroless which includes grpc_health_probe
FROM gcr.io/distroless/static-debian12 AS production
COPY --from=builder /app/server /server
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/server"]

Corresponding Go Application ​

go
// cmd/server/main.go
package main

import (
	"context"
	"fmt"
	"log/slog"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"
)

var (
	Version   = "dev"
	Commit    = "unknown"
	BuildTime = "unknown"
)

func main() {
	logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
		Level: slog.LevelInfo,
	}))
	slog.SetDefault(logger)

	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
	}

	mux := http.NewServeMux()

	// Health endpoints
	mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		fmt.Fprintf(w, `{"status":"ok","version":"%s","commit":"%s"}`, Version, Commit)
	})

	mux.HandleFunc("GET /health/live", func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
		w.Write([]byte(`{"status":"ok"}`))
	})

	mux.HandleFunc("GET /health/ready", func(w http.ResponseWriter, r *http.Request) {
		// Check dependencies here (DB, cache, etc.)
		w.WriteHeader(http.StatusOK)
		w.Write([]byte(`{"status":"ready"}`))
	})

	// Application routes
	mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		w.Write([]byte(`{"message":"Hello from Go production"}`))
	})

	server := &http.Server{
		Addr:              ":" + port,
		Handler:           mux,
		ReadHeaderTimeout: 10 * time.Second,
		ReadTimeout:       30 * time.Second,
		WriteTimeout:      30 * time.Second,
		IdleTimeout:       120 * time.Second,
		MaxHeaderBytes:    1 << 20, // 1MB
	}

	// Start server in goroutine
	go func() {
		slog.Info("Server starting",
			"port", port,
			"version", Version,
			"commit", Commit,
			"build_time", BuildTime,
		)
		if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			slog.Error("Server failed to start", "error", err)
			os.Exit(1)
		}
	}()

	// Wait for shutdown signal
	quit := make(chan os.Signal, 1)
	signal.Notify(quit, syscall.SIGTERM, syscall.SIGINT)
	sig := <-quit

	slog.Info("Shutdown signal received", "signal", sig.String())

	// Graceful shutdown with timeout
	ctx, cancel := context.WithTimeout(context.Background(), 25*time.Second)
	defer cancel()

	if err := server.Shutdown(ctx); err != nil {
		slog.Error("Server forced to shutdown", "error", err)
		os.Exit(1)
	}

	slog.Info("Server shutdown complete")
}

Core Mechanics — Python ​

Complete Production Dockerfile ​

dockerfile
# syntax=docker/dockerfile:1

# ============================================================
# Python Production Dockerfile (FastAPI/Uvicorn)
# ============================================================
# Image size: ~150MB (Alpine) or ~120MB (slim)
# Build time: ~45s (cold) / ~10s (cached code change)
# Security: non-root, venv, no dev deps
# ============================================================

# ------ Stage 1: Build dependencies ------
FROM python:3.12-slim AS builder

# Install build dependencies (for compiled packages like psycopg2)
RUN apt-get update && apt-get install -y --no-install-recommends \
    build-essential \
    libpq-dev \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app

# Create virtual environment
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

# Install Python dependencies (cache mount for pip)
COPY requirements.txt ./
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install --no-compile -r requirements.txt

# ------ Stage 2: Production ------
FROM python:3.12-slim AS production

# Install runtime dependencies only (not build tools)
RUN apt-get update && apt-get install -y --no-install-recommends \
    libpq5 \
    curl \
    tini \
    && rm -rf /var/lib/apt/lists/* \
    && apt-get purge -y --auto-remove

# Create non-root user
RUN groupadd -g 10001 appgroup && \
    useradd -u 10001 -g appgroup -s /usr/sbin/nologin -r appuser && \
    mkdir -p /home/appuser && chown appuser:appgroup /home/appuser

WORKDIR /app

# Copy virtual environment from builder
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

# Copy application code
COPY --chown=appuser:appgroup . .

# Create writable directories
RUN mkdir -p /tmp/app && chown appuser:appgroup /tmp/app

# Labels
LABEL org.opencontainers.image.title="Python API Server"
LABEL org.opencontainers.image.source="https://github.com/company/python-api"

# Switch to non-root user
USER appuser

EXPOSE 8000

# Health check
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
    CMD curl -f http://localhost:8000/health || exit 1

# Use tini for PID 1
ENTRYPOINT ["tini", "--"]

# Run with uvicorn (production ASGI server)
CMD ["uvicorn", "app.main:app", \
    "--host", "0.0.0.0", \
    "--port", "8000", \
    "--workers", "4", \
    "--no-access-log", \
    "--proxy-headers", \
    "--forwarded-allow-ips", "*"]

Python with Poetry ​

dockerfile
# syntax=docker/dockerfile:1

FROM python:3.12-slim AS builder

RUN pip install poetry==1.7.1
ENV POETRY_NO_INTERACTION=1 \
    POETRY_VIRTUALENVS_IN_PROJECT=1 \
    POETRY_VIRTUALENVS_CREATE=1 \
    POETRY_CACHE_DIR=/tmp/poetry_cache

WORKDIR /app

COPY pyproject.toml poetry.lock ./
RUN --mount=type=cache,target=/tmp/poetry_cache \
    poetry install --without dev --no-root

FROM python:3.12-slim AS production

RUN groupadd -g 10001 appgroup && \
    useradd -u 10001 -g appgroup -s /usr/sbin/nologin -r appuser

WORKDIR /app

# Copy the virtual environment
COPY --from=builder /app/.venv /app/.venv
ENV PATH="/app/.venv/bin:$PATH"
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

COPY --chown=appuser:appgroup . .
USER appuser
EXPOSE 8000

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

Implementation — Supporting Files ​

.dockerignore (Universal) ​

# Version control
.git
.gitignore

# Docker files
Dockerfile*
docker-compose*.yml
.dockerignore

# IDE
.vscode
.idea
*.swp
*.swo

# Documentation
*.md
LICENSE

# CI/CD
.github
.gitlab-ci.yml
Jenkinsfile

# Testing
coverage
.nyc_output
__pycache__
.pytest_cache
.coverage
htmlcov

# Node.js
node_modules
dist
.env*

# Go
vendor

# Python
*.pyc
*.pyo
__pycache__
.venv
*.egg-info

# OS
.DS_Store
Thumbs.db

Docker Compose for Development ​

yaml
# docker-compose.yml
services:
  api:
    build:
      context: .
      dockerfile: Dockerfile
      target: development  # Use development stage
    ports:
      - "3000:3000"
      - "9229:9229"  # Node.js debug port
    volumes:
      - ./src:/app/src:ro
      - ./package.json:/app/package.json:ro
    environment:
      - NODE_ENV=development
      - DATABASE_URL=postgresql://postgres:postgres@db:5432/app
      - REDIS_URL=redis://redis:6379
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: app
    volumes:
      - pg_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 5s
      retries: 5

volumes:
  pg_data:

Edge Cases and Failure Modes ​

1. Alpine DNS Resolution Delay ​

Alpine uses musl libc which handles DNS differently from glibc. Go applications on Alpine may experience 5-second DNS resolution delays due to musl's handling of dual A/AAAA queries.

dockerfile
# Fix for Go on Alpine: use CGO_ENABLED=0 (pure Go DNS resolver)
RUN CGO_ENABLED=0 go build -o /server ./cmd/server

# Or use the Go net resolver explicitly in code
# import _ "net" // ensure Go DNS resolver
# os.Setenv("GODEBUG", "netdns=go")

2. Node.js Heap Memory in Containers ​

Node.js does not automatically detect container memory limits. It uses host memory for default heap calculations:

dockerfile
# Set Node.js to respect container memory limits
ENV NODE_OPTIONS="--max-old-space-size=384"
# For a 512MB container: leave ~128MB for non-heap (stack, buffers, native)

# Or use the automatic flag (Node.js 19+)
ENV NODE_OPTIONS="--max-old-space-size=0"
# 0 means "auto-detect" which respects cgroup limits

3. Python pip and Wheel Caching ​

dockerfile
# BAD: Downloads wheels every build
RUN pip install -r requirements.txt

# GOOD: Uses cache mount
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements.txt

# GOOD: Pre-compiled wheels for reproducibility
RUN pip install --no-cache-dir --only-binary=:all: -r requirements.txt

4. Timezone Data in Scratch Images ​

dockerfile
# Go binary that needs timezone support
FROM scratch
# Must copy timezone data from build stage
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
ENV TZ=UTC

5. Signal Handling in Python ​

python
# Python does not handle SIGTERM by default
import signal
import sys
import uvicorn

def handle_signal(signum, frame):
    print(f"Received signal {signum}, shutting down...")
    sys.exit(0)

signal.signal(signal.SIGTERM, handle_signal)
signal.signal(signal.SIGINT, handle_signal)

# Uvicorn handles signals internally when run as main
if __name__ == "__main__":
    uvicorn.run("app.main:app", host="0.0.0.0", port=8000)

Performance Characteristics ​

Image Size Comparison ​

ApplicationNaive DockerfileOptimized Multi-StageWith Distroless
Node.js API1.2 GB120 MB90 MB
Go API850 MB12 MB (scratch)15 MB
Python FastAPI1.5 GB150 MB130 MB
Next.js SSR1.8 GB180 MBN/A
Rust API1.5 GB8 MB (scratch)10 MB

Build Time Comparison ​

ScenarioNaiveOptimized (cold)Optimized (cached)
Node.js clean build180s90sN/A
Node.js code change180s15s15s
Node.js dep change180s90s45s (cache mount)
Go clean build120s60sN/A
Go code change120s10s10s
Python clean build240s120sN/A
Python code change240s10s10s

Pull Time (100Mbps network) ​

Tpull=ScompressedBW+Textract
ImageCompressed SizePull TimeExtract TimeTotal
1.2GB Node (naive)~450MB36s15s51s
120MB Node (optimized)~45MB3.6s3s6.6s
12MB Go (scratch)~5MB0.4s0.5s0.9s

Mathematical Foundations ​

Image Layer Deduplication Savings ​

When running N microservices sharing the same base image and runtime:

Sshared=Sbase+Sruntime+∑i=1NSappiSnaive=N×(Sbase+Sruntime+Sapp)Savings=1−SsharedSnaive=1−Sbase+Sruntime+N×SappN×(Sbase+Sruntime+Sapp)

For 10 Node.js services (base+runtime: 100MB, app: 20MB each):

Savings=1−100+20010×120=1−3001200=75%

Disk savings: 900MB (stored 300MB instead of 1200MB).

Build Cache Efficiency ​

Efficiency=∑icachedi×costi∑icosti

For a 6-layer Dockerfile with costs [5s,1s,2s,60s,3s,15s] and cache hits [1,1,1,1,0,0] (code change only):

Efficiency=5+1+2+605+1+2+60+3+15=6886=79%

Time saved: 68s out of 86s total = build runs in 18s instead of 86s.

Real-World War Stories ​

War Story — The 15-Minute Container Start

A Java application in Docker took 15 minutes to start because it was using a fat JAR with all dependencies extracted into the filesystem. The overlay2 copy-on-write overhead for 50,000 small files was enormous. The startup involved touching each file, triggering 50,000 CoW operations.

Fix: Used a layered JAR approach where Spring Boot's layers were separate Docker layers, and the frequently-changing application code was in the top layer. Startup dropped to 30 seconds.

War Story — The Go Binary That Crashed on Alpine

A Go service built on macOS with GOOS=linux worked in development but crashed with a segfault on Alpine. The issue: CGO was enabled (default on macOS), and the binary linked against glibc, but Alpine uses musl.

Fix: Added CGO_ENABLED=0 to the build command, producing a fully static binary that works on any Linux distribution, including scratch.

War Story — The Python Image That Doubled

A Python team noticed their image size doubled from 150MB to 300MB after adding a single dependency (pandas). Investigation revealed that pandas pulled in numpy, which included pre-compiled C extensions for every CPU architecture. The fix was to use --only-binary=:all: to ensure pre-compiled wheels matched the target architecture, and adding numpy and pandas to a separate layer for caching.

Decision Framework ​

Base Image Selection ​

Dockerfile Checklist ​

Use this checklist before deploying any Dockerfile to production:

  • [ ] Uses specific base image tag (not :latest)
  • [ ] Multi-stage build separates build from runtime
  • [ ] Non-root user created and set with USER
  • [ ] COPY --chown used for application files
  • [ ] .dockerignore excludes unnecessary files
  • [ ] Dependencies cached via layer ordering
  • [ ] HEALTHCHECK defined
  • [ ] Signal handling configured (tini or application-level)
  • [ ] Read-only filesystem compatible
  • [ ] No secrets in any layer
  • [ ] OCI labels added
  • [ ] Production dependencies only (no dev deps)
  • [ ] Vulnerability scan passes (zero critical CVEs)

Advanced Topics ​

Reproducible Builds with SHA Pinning ​

dockerfile
# Pin EVERYTHING by digest for complete reproducibility
FROM node:20.11.1-alpine3.19@sha256:abc123... AS base

# Pin apk packages by version
RUN apk add --no-cache tini=0.19.0-r2

# Pin npm packages via lockfile (npm ci)
COPY package-lock.json ./
RUN npm ci  # Respects exact versions in lockfile

Multi-Platform Build Matrix ​

bash
# Build for all platforms in one command
docker buildx build \
  --platform linux/amd64,linux/arm64 \
  --build-arg VERSION=$(git describe --tags) \
  --build-arg COMMIT=$(git rev-parse HEAD) \
  --build-arg BUILD_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \
  -t ghcr.io/company/myapp:$(git describe --tags) \
  -t ghcr.io/company/myapp:latest \
  --push .

Kaniko for Building in Kubernetes ​

When you cannot mount the Docker socket (security requirement), use Kaniko:

yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: build-image
spec:
  template:
    spec:
      containers:
        - name: kaniko
          image: gcr.io/kaniko-project/executor:latest
          args:
            - "--dockerfile=Dockerfile"
            - "--context=git://github.com/company/myapp.git#refs/heads/main"
            - "--destination=ghcr.io/company/myapp:latest"
            - "--cache=true"
            - "--cache-repo=ghcr.io/company/myapp/cache"
          volumeMounts:
            - name: docker-config
              mountPath: /kaniko/.docker
      volumes:
        - name: docker-config
          secret:
            secretName: docker-registry-credentials
      restartPolicy: Never

Next: Compose Patterns — Development, staging, and production Docker Compose patterns for complex multi-service architectures.

"What I cannot create, I do not understand." — Richard Feynman