Optimize
Optimizing Docker for production goes beyond just "making it run." It requires a shift from development convenience to operational stability, security, and efficiency.
This guide covers the core modifications needed to transition a Docker setup from a local development environment to a production-ready system.
1. Image Optimization (The Build Phase)
The goal is to create images that are small, secure, and fast to build.
A. Multi-Stage Builds
Never ship build tools (compilers, headers, SDKs) to production. Use multi-stage builds to compile in a heavy image and copy only the binary/artifacts to a lightweight runtime image.
Example (Golang):
# STAGE 1: Builder
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY . .
# Build the binary, stripping debug information (-w -s) to reduce size
RUN go build -ldflags="-w -s" -o myapp main.go
# STAGE 2: Runtime
# Use "scratch" (empty image) or "alpine" for minimal footprint
FROM alpine:3.19
WORKDIR /app
# Copy only the compiled binary from the builder stage
COPY --from=builder /app/myapp .
# Run as non-root (see Security section)
USER 1001
CMD ["./myapp"]B. Layer Caching Strategy
Docker caches layers based on the instruction string. Order your Dockerfile instructions from least frequent changes to most frequent changes to maximize cache hits.
Bad: Copy source code, then install dependencies. (Every code change breaks the dependency cache).
Good: Install dependencies, then copy source code.
C. The .dockerignore File
This is mandatory. It prevents large local files (like .git, node_modules, logs, local secrets) from being sent to the Docker daemon. This speeds up builds and prevents secret leakage.
Create a .dockerignore file:
2. Security Hardening (The "Ship" Phase)
Default Docker settings are permissive. Production containers must be restrictive.
A. Run as Non-Root User
By default, Docker containers run as root. If an attacker breaks out of the application, they have root access to the container (and potentially the host).
Fix: Create a user in the Dockerfile and switch to it.
B. Read-Only Filesystems
Prevent attackers from modifying application files or installing malware by making the container's root filesystem read-only.
Runtime Command:
Note: If your app needs to write temporary files, mount a
tmpfsvolume at that location (e.g.,/tmpor/run).
C. Drop Capabilities
Linux "root" is actually a collection of capabilities (like CAP_CHOWN, CAP_NET_ADMIN). Most apps don't need them. Drop all capabilities and add back only what is necessary.
Runtime Command:
3. Runtime & Stability (The "Run" Phase)
Ensure your containers behave well when running on the host server.
A. Resource Limits (CPU & Memory)
Crucial: Without limits, a single leaky container can consume 100% of the host's RAM/CPU, crashing the server.
Memory: Set a hard limit. If the app exceeds this, the OOM (Out of Memory) Killer kills the container, not the host OS.
CPU: Limit CPU cycles.
B. PID 1 and Init Systems
In a standard Linux system, PID 1 (systemd/init) handles signal propagation (like SIGTERM) and reaps zombie processes. In a Docker container, your app is PID 1. If your app doesn't handle these tasks, zombie processes will accumulate and docker stop will hang (forcing a SIGKILL).
Fix: Use the --init flag to wrap your process with a tiny init system (tini) that handles this for you.
C. Logging Drivers
Default Docker logging (json-file) can fill up the host disk if not rotated.
Configure Log Rotation (Global via daemon.json or per container):
Summary Checklist for Production
Image
Size
Use alpine or distroless base images.
Image
Build
Use Multi-stage builds to strip build tools.
Image
Context
Use .dockerignore to exclude .git and secrets.
Security
User
NEVER run as root. Use USER appuser.
Security
Network
Don't use --net=host. Expose only necessary ports.
Security
Secrets
Never bake secrets into the image. Use ENV vars or Secret Mounts.
Runtime
Limits
Set --memory and --cpus limits.
Runtime
Lifecycle
Use --init to handle zombie processes and signals correctly.
Runtime
Storage
Use Docker Volumes for persistent data, not the container layer.
Last updated