Production Docker setup for 2026.

Host setup (Ubuntu/Debian)

# Install Docker
curl -fsSL https://get.docker.com | sh
usermod -aG docker $USER

# Verify
docker info
docker compose version

/etc/docker/daemon.json

{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3",
    "compress": "true"
  },
  "default-ulimits": {
    "nofile": { "soft": 65536, "hard": 65536 }
  },
  "live-restore": true,
  "userns-remap": "default",
  "dns": ["1.1.1.1", "8.8.8.8"],
  "metrics-addr": "127.0.0.1:9323",
  "experimental": false,
  "no-new-privileges": true
}
systemctl restart docker

Project layout

/srv/app/
├── compose.yml
├── compose.prod.yml
├── .env                  # secrets (chmod 600)
├── nginx/
│   ├── Caddyfile
│   └── certs/
├── backups/
└── data/                 # persisted volumes

compose.prod.yml

services:
  proxy:
    image: caddy:2
    ports: ["80:80", "443:443"]
    volumes:
      - ./nginx/Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
      - caddy_config:/config
    networks: [public, app]
    restart: unless-stopped
  
  web:
    image: ghcr.io/me/myapp:${TAG:-latest}
    networks: [app]
    expose: ["8000"]
    env_file: .env
    depends_on:
      db: { condition: service_healthy }
      redis: { condition: service_started }
    restart: unless-stopped
    deploy:
      resources:
        limits:
          cpus: "2"
          memory: 1G
    logging:
      driver: json-file
      options: { max-size: "10m", max-file: "3" }
  
  worker:
    image: ghcr.io/me/myapp:${TAG:-latest}
    command: celery -A config worker -l info --concurrency=4
    networks: [app]
    env_file: .env
    depends_on: [db, redis]
    restart: unless-stopped
  
  db:
    image: postgres:16
    volumes:
      - pg_data:/var/lib/postgresql/data
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    networks: [app]
    secrets: [db_password]
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      retries: 5
    restart: unless-stopped
  
  redis:
    image: redis:7-alpine
    networks: [app]
    volumes: [redis_data:/data]
    command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru
    restart: unless-stopped
  
  watchtower:
    image: containrrr/watchtower
    volumes: ["/var/run/docker.sock:/var/run/docker.sock"]
    command: --interval 300 --cleanup --label-enable
    restart: unless-stopped

networks:
  public:
  app:
    internal: false

volumes:
  pg_data:
  redis_data:
  caddy_data:
  caddy_config:

secrets:
  db_password:
    file: ./secrets/db_password.txt

Caddyfile

example.com {
    encode gzip
    
    @api path /api/*
    handle @api {
        reverse_proxy web:8000
    }
    
    handle {
        reverse_proxy web:8000
    }
    
    log {
        output file /data/access.log
    }
}

Auto-HTTPS via Let’s Encrypt.

Systemd service

# /etc/systemd/system/myapp.service
[Unit]
Description=My App
Requires=docker.service
After=docker.service

[Service]
Type=oneshot
RemainAfterExit=true
WorkingDirectory=/srv/app
ExecStart=/usr/bin/docker compose -f compose.yml -f compose.prod.yml up -d
ExecStop=/usr/bin/docker compose -f compose.yml -f compose.prod.yml down
TimeoutStartSec=300

[Install]
WantedBy=multi-user.target
systemctl enable myapp
systemctl start myapp

Backup cron

# /etc/cron.d/backup
0 2 * * * root /srv/app/backup.sh

Backup script: see Cheatsheet 19.

Log rotation (besides Docker’s)

# /etc/logrotate.d/docker
/var/lib/docker/containers/*/*.log {
    daily
    rotate 7
    compress
    missingok
    delaycompress
    notifempty
    copytruncate
}

Backup in case Docker’s own rotation fails.

Monitoring stack

services:
  cadvisor:
    image: gcr.io/cadvisor/cadvisor:v0.49.1
    volumes:
      - /:/rootfs:ro
      - /var/run:/var/run:rw
      - /sys:/sys:ro
      - /var/lib/docker:/var/lib/docker:ro
    ports: ["8081:8080"]
    restart: unless-stopped
  
  node-exporter:
    image: prom/node-exporter
    network_mode: host
    pid: host
    restart: unless-stopped
  
  prometheus:
    image: prom/prometheus
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
      - prom_data:/prometheus
    restart: unless-stopped
  
  grafana:
    image: grafana/grafana
    ports: ["3000:3000"]
    volumes: [grafana_data:/var/lib/grafana]
    restart: unless-stopped

Dashboards: cAdvisor + node-exporter combos available on grafana.com.

Deployment

# CI builds + pushes:
docker build -t ghcr.io/me/myapp:$SHA .
docker push ghcr.io/me/myapp:$SHA

# Deploy to host via SSH:
ssh deploy@server "cd /srv/app && \
  TAG=$SHA docker compose -f compose.yml -f compose.prod.yml pull web && \
  TAG=$SHA docker compose -f compose.yml -f compose.prod.yml up -d --no-deps web && \
  docker image prune -f"

Zero-downtime: rolling update via Swarm or external proxy.

Firewall

ufw default deny incoming
ufw allow 22
ufw allow 80
ufw allow 443
ufw enable

Or use cloud security groups. Don’t expose Docker socket (2375).

SSH access

# /etc/ssh/sshd_config
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes

Use SSH keys only.

Disk monitoring

docker system df
df -h
du -sh /var/lib/docker/*

Alert at 80% full.

Periodic cleanup

0 3 * * 0 docker system prune -af --filter "until=168h"

SSL renewal

Caddy handles automatically. For manual nginx setup:

docker run --rm \
  -v $(pwd)/certs:/etc/letsencrypt \
  certbot/certbot certonly --webroot -w /var/www -d example.com

Common conventions

  • Run as non-root user inside container.
  • Pin base image SHAs.
  • One image per service.
  • Tag with SHA in CI.
  • Backup with retention + off-site.
  • Restart=unless-stopped.
  • Resource limits on every container.
  • Log rotation always configured.
  • Caddy for TLS by default.

Read this next

That’s 20 Docker cheatsheets. Next category: Kubernetes.

If you want my full prod Docker stack (compose + Caddy + backup + monitoring), it’s at rajpoot.dev .


Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .