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 .