Skip to content

Docker — Security & Production

Non-Root Containers

Running as root inside a container = running as root on the host (if the container escapes).

# Create and switch to non-root user
RUN groupadd -r appgrp && useradd -r -g appgrp -u 1000 appusr
RUN chown -R appusr:appgrp /app
USER appusr
# Alpine variant
RUN addgroup -S appgrp && adduser -S appusr -G appgrp
USER appusr
# Distroless — built-in nonroot user (UID 65532)
FROM gcr.io/distroless/static-debian12:nonroot
USER nonroot:nonroot

Verify at runtime:

docker exec myapp whoami                         # Should NOT print "root"
docker exec myapp id                             # Check UID/GID

Secrets Management

Never put secrets in: - ENV or ARG in Dockerfile (visible in docker inspect) - Docker image layers - Git-tracked .env files

Docker Secrets (Compose)

services:
  api:
    secrets:
      - db_password
      - api_key
    environment:
      DB_PASSWORD_FILE: /run/secrets/db_password

secrets:
  db_password:
    file: ./secrets/db_password.txt
  api_key:
    environment: API_KEY_VALUE

Application reads from /run/secrets/<name> (mounted as tmpfs, in-memory only).

BuildKit Secret Mounts

# Secret available only during build, never stored in layer
RUN --mount=type=secret,id=pip_conf,target=/root/.config/pip/pip.conf \
    pip install --no-cache-dir -r requirements.txt
docker build --secret id=pip_conf,src=pip.conf -t myapp .

Image Scanning

docker scout cves myapp:latest                   # Docker Scout (built-in)
docker scout recommendations myapp:latest        # Upgrade suggestions

trivy image myapp:latest                         # Trivy (popular open-source)
trivy image --severity HIGH,CRITICAL myapp:latest

grype myapp:latest                               # Grype (Anchore)

Integrate scanning in CI — fail builds on HIGH/CRITICAL vulnerabilities.

Read-Only Filesystem

services:
  api:
    read_only: true
    tmpfs:
      - /tmp
      - /app/cache

Container filesystem is immutable; only /tmp and /app/cache are writable.

Resource Limits

services:
  api:
    deploy:
      resources:
        limits:
          cpus: "1.0"
          memory: 512M
        reservations:
          cpus: "0.25"
          memory: 128M
docker run -d --cpus 1.0 --memory 512m myapp     # CLI equivalent
docker stats --no-stream                          # Verify limits are applied

Without limits, a single container can consume all host resources.

Capabilities & Security Options

services:
  api:
    cap_drop:
      - ALL                  # Drop all Linux capabilities
    cap_add:
      - NET_BIND_SERVICE     # Add only what's needed
    security_opt:
      - no-new-privileges:true

Principle of least privilege: Drop all capabilities, add back only required ones.

Logging in Production

services:
  api:
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "5"
        tag: "{{.Name}}"

Without max-size, logs grow unbounded and fill disk.

Driver Use Case
json-file Default, local debugging
syslog Centralized syslog server
fluentd EFK stack
awslogs AWS CloudWatch
gcplogs GCP Cloud Logging

Restart Policies

Policy Behavior
no Never restart (default)
always Always restart (survives daemon restarts, even after docker stop)
unless-stopped Restart unless explicitly stopped; does not restart after docker stop
on-failure Restart only on non-zero exit code
services:
  api:
    restart: unless-stopped
  worker:
    restart: on-failure:5      # max retries in regular restart policy

Production Checklist

Area Check
User Non-root user in Dockerfile
Image Minimal base (distroless / alpine / slim)
Secrets No secrets in ENV/ARG/layers
Scanning CI scans for HIGH/CRITICAL CVEs
Resources CPU + memory limits set
Logging Log rotation configured (max-size/max-file)
Restart unless-stopped or on-failure
Health Healthcheck defined
Filesystem read_only: true where possible
Capabilities cap_drop: ALL + minimal cap_add
Privileges no-new-privileges: true
Network Custom bridge, multi-network isolation
Tags Pinned image versions (no :latest in prod)