01 Install Docker from the official repository
Distribution packages lag significantly. The convenience script is fine on a fresh instance.
curl -fsSL https://get.docker.com | sh
systemctl enable --now docker 02 Write the Compose file
Caddy obtains and renews certificates automatically, which removes the single most common source of production breakage.
services:
caddy:
image: caddy:2-alpine
restart: unless-stopped
ports: ["80:80", "443:443", "443:443/udp"]
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
app:
build: .
restart: unless-stopped
depends_on: { db: { condition: service_healthy } }
environment:
DATABASE_URL: postgres://app:${DB_PASS}@db:5432/app
db:
image: postgres:17-alpine
restart: unless-stopped
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: ${DB_PASS}
volumes: [pgdata:/var/lib/postgresql/data]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app"]
interval: 10s
volumes: { caddy_data: {}, pgdata: {} } 03 Configure Caddy
Three lines is a complete TLS-terminating reverse proxy with automatic certificate renewal.
app.example.com {
encode zstd gzip
reverse_proxy app:8000
} 04 Back up the database, not the container
A file copy of a running Postgres data directory produces a corrupt backup that appears to succeed. Dump it properly.
docker compose exec -T db pg_dump -U app app | zstd > /backups/app-$(date +%F).sql.zst 05 Snapshot before every deploy
Free, instant, and it converts a failed deployment from an incident into a rollback.