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 tmpfs volume at that location (e.g., /tmp or /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

Area
Check
Action

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