## Intro

Running Docker in production is about repeatable builds, secure defaults, solid observability, and safe change management. The payoff is higher reliability with less rework and fewer surprises. Use this checklist to standardize your approach and accelerate delivery.

## Workflow Overview

Follow these steps for each service:

- Build minimal, pinned images

- Configure runtime safely (user, caps, filesystem)

- Isolate networks and expose only what you need

- Set resource limits and healthchecks

- Wire logs and metrics with bounded retention

- Persist state with named volumes and test backups

- Upgrade with rollbacks, verify with probes

- Maintain hosts and artifacts on a schedule

Tip: small, observable steps reduce rework and make issues easier to isolate. For example, add a healthcheck before you enable a restart policy so the container is marked healthy before traffic is sent.

## Configuration baseline

Checklist:

- Multi‑stage builds to keep images small

- Pin base images and tools by version (or digest)

- Non‑root user with a fixed UID/GID

- HEALTHCHECK in the image or Compose

- Minimal files via a tight .dockerignore

- Labels for provenance (org, version, vcs, build date)

Example Dockerfile (Go service):

# Build stage
FROM golang:1.22-alpine AS build
WORKDIR /src
RUN adduser -D -u 10001 app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags='-s -w' -o /out/app ./cmd/server

# Runtime stage
FROM alpine:3.20
RUN addgroup -g 10001 app \
 && adduser -D -u 10001 -G app app \
 && apk add --no-cache ca-certificates curl
WORKDIR /app
COPY --from=build /out/app /app/app
USER 10001:10001
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
 CMD curl -fsS http://127.0.0.1:8080/health || exit 1
ENTRYPOINT [
/
/app/app/
] 

## Security hardening

 Checklist:

- Run as a non‑root UID/GID

- Read‑only root filesystem; temp write via tmpfs

- Drop all Linux capabilities, add back only what is required

- no-new-privileges to block privilege escalation

- Avoid host PID/net namespaces and privileged mode

- Keep secrets as files, not env vars

Example docker‑compose.yml security defaults:

version: '3.9'
services:
 app:
 image: myorg/myapp:1.2.3
 user: '10001:10001'
 read_only: true
 tmpfs:
 - /tmp:rw,noexec,nosuid,size=64m
 cap_drop:
 - ALL
 security_opt:
 - no-new-privileges:true
 restart: always
 stop_grace_period: 30s 
 Secrets as files with Compose:

services:
 app:
 secrets:
 - source: db_password
 target: db_password
 mode: 0400
secrets:
 db_password:
 file: ./secrets/db_password.txt 
 Your app should read the secret at /run/secrets/db_password .

## Resource limits and healthchecks

Checklist:

- Add a container healthcheck

- Set CPU and memory limits

- Configure ulimits (nofile, nproc)

- Use a sensible restart policy (always or unless‑stopped)

Compose example (non‑Swarm):

services:
 app:
 # Build or image here
 healthcheck:
 test: ['CMD', 'wget', '-q', '-O', '-', 'http://127.0.0.1:8080/health']
 interval: 30s
 timeout: 3s
 retries: 3
 start_period: 10s
 restart: always
 mem_limit: 512m
 cpus: '0.50'
 ulimits:
 nofile:
 soft: 65536
 hard: 65536
 nproc: 4096 
 Note: deploy.resources is for Swarm. For standalone Docker Compose, use mem_limit and cpus as shown.

## Networking and ingress

Checklist:

- Use a user‑defined bridge network per app or domain

- Map only required ports to the host

- Keep internal services private on the app network

- Terminate TLS at an ingress proxy (for example Nginx) and mount certificates as read‑only files

Example network isolation:

services:
 app:
 networks: [app-net]
 nginx:
 image: nginx:1.25
 depends_on: [app]
 ports:
 - '80:80'
 - '443:443'
 volumes:
 - ./nginx.conf:/etc/nginx/nginx.conf:ro
 - ./certs:/etc/nginx/certs:ro
 networks: [app-net]
networks:
 app-net:
 driver: bridge 
 Minimal nginx.conf upstream example:

worker_processes auto;
events { worker_connections 1024; }
http {
 upstream app_upstream { server app:8080; }
 server {
 listen 80;
 listen 443 ssl;
 ssl_certificate /etc/nginx/certs/server.crt;
 ssl_certificate_key /etc/nginx/certs/server.key;
 location / {
 proxy_pass http://app_upstream;
 proxy_set_header Host $host;
 proxy_set_header X-Forwarded-For $remote_addr;
 }
 }
} 

## Logging and metrics

 Checklist:

- Use a consistent logging driver with rotation

- Emit structured logs (JSON lines) from the app, including timestamp, level, service name, and request id

- Capture container‑level CPU, memory, and restart counts

Compose logging example:

services:
 app:
 logging:
 driver: json-file
 options:
 max-size: '10m'
 max-file: '3' 
 Operational tips:

- docker logs --since 10m app

- docker stats app

- docker inspect --format '{{.State.Health}}' app

## Storage and backups

Checklist:

- Use named volumes for state

- Keep configuration and secrets outside the image

- Document and test backup and restore; schedule regular backups (e.g., daily via cron)

Create and back up a named volume:

# Create a volume
docker volume create mydata

# Backup to a timestamped tarball in the current dir
docker run --rm \
 -v mydata:/data \
 -v "$PWD:/backup" \
 busybox sh -c 'tar czf /backup/mydata-$(date +%F).tgz -C / data'

# List backups
ls -lh mydata-*.tgz

# Restore from a backup
docker run --rm \
 -v mydata:/data \
 -v "$PWD:/backup" \
 busybox sh -c 'tar xzf /backup/mydata-2026-07-13.tgz -C /' 
 For databases, prefer native tools (for example pg_dump , mysqldump ) to ensure consistency.

## Upgrades and rollbacks

Checklist:

- Pin images by version or digest

- Upgrade in small increments

- Verify health and logs before switching traffic

- Keep a fast rollback path

Pin by tag and digest in Compose:

services:
 nginx:
 image: nginx:1.25.5@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 
 Rolling upgrade with two projects (canary style):

# Deploy next version
docker compose -p myapp-next up -d
# Verify health
docker compose -p myapp-next ps
# Switch traffic at ingress (update upstream or DNS)
# Decommission previous
docker compose -p myapp-current down 
 Simple in‑place upgrade:

docker compose pull
docker compose up -d
docker compose ps 

## Maintenance and troubleshooting

 Checklist:

- Monitor disk usage and prune safely

- Rotate logs and keep bounds

- Keep only necessary images and networks

Useful commands:

# Show space usage
docker system df

# List dangling images first
docker images -f dangling=true

# Prune safely (confirm prompt)
docker image prune

# Full prune of unused images and build cache (be careful)
docker image prune -af

# Inspect recent restarts
docker events --since 1h --filter 'event=restart' 

## Local Pilot Plan

 Goal: prove the checklist on one stateless service with clear acceptance criteria.

Scope:

- One service behind Nginx

- Healthcheck, restart policy, memory and CPU limits

- Log rotation via json-file driver

- A small named volume and a tested backup

Steps:

- Implement the Compose file with security defaults, limits, and logging

- Add a health endpoint and verify docker ps shows healthy

- Simulate a crash and verify restart: docker kill -s SIGKILL app

- Capture baseline metrics with docker stats and startup time

- Back up and restore the volume, verify data integrity

Acceptance criteria:

- Healthcheck passes within 10 seconds of container start

- No container logs exceed 30 MB after rotation

- Memory and CPU remain within configured limits under a basic load

- Backup restore recreates the expected state

## Common production pitfalls and fixes

- Using :latest tags → Fix: pin tags or digests and update intentionally.

- Running as root → Fix: create a non‑root user and set user in image and Compose.

- Missing healthchecks → Fix: add HEALTHCHECK in the image or Compose.

- Unbounded logs → Fix: set json‑file rotation or ship logs to a central store with retention.

- Writing state to the container filesystem → Fix: use named volumes and verify backup‑restore.

- Exposing more ports than needed → Fix: keep internal services on a private network.

- Overlooking ulimits → Fix: set nofile and nproc to match workload needs.

- Relying on deploy.resources in non‑Swarm Compose → Fix: use mem_limit and cpus for standalone Docker.

## Conclusion

Adopt the checklist, start with the local pilot, and expand service by service. Keep changes small, measurable, and reversible. With secure defaults, clear limits, and proven backups, your Docker production operations will be safer, faster, and easier to troubleshoot.