This guide is a practical, copy-paste-friendly checklist for running Docker in production. It covers configuration, security, resource limits, networking, observability, backups, upgrades, and maintenance. Each section includes concrete examples you can adapt.
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.
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)
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
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;
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
- 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
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:
# Deploy next
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, mem 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.