Most Docker Compose files are written to get the service running. Security comes later — usually when something goes wrong. This checklist covers everything to check before a Docker Compose setup goes anywhere near a public-facing server.
It is not exhaustive. It is the 20% of checks that catch 80% of the real issues.
1. Port bindings — the most common mistake
Every port in your Compose file that uses HOST:CONTAINER format binds to 0.0.0.0 — all interfaces, including your public IP. UFW does not protect these ports. Docker bypasses UFW entirely.
# Bad — publicly accessible: ports: - "6379:6379" - "5432:5432" - "27017:27017" # Good — localhost only, access via reverse proxy: ports: - "127.0.0.1:6379:6379" - "127.0.0.1:5432:5432"
Databases, caches, and internal APIs should never be bound to 0.0.0.0. Only your reverse proxy (Nginx, Traefik, Caddy) needs public port access — and only on 80 and 443.
2. Hardcoded credentials
# Bad — credentials in compose file:
environment:
POSTGRES_PASSWORD: mysecretpassword
API_KEY: sk-abc123
# Good — reference from .env:
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
API_KEY: ${API_KEY}
The .env file must be in .gitignore and must never be committed. Check your git history if you're not sure:
git log --all -p | grep -i "password|secret|api_key" | head -20
3. Missing healthchecks
Without a healthcheck, Docker marks a container as healthy the moment it starts — even if the application inside is still booting, running migrations, or has crashed. Dependent services start immediately against an unready backend.
healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/health"] interval: 30s timeout: 10s retries: 3 start_period: 40s
The start_period is the grace period before failures count — essential for Java apps, services running DB migrations, or anything with a slow startup.
4. Missing resource limits
A memory leak or runaway process in one container can OOM-kill everything else on the host. Set limits.
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
reservations:
memory: 256M
Compose v3 resource limits require docker compose (not docker-compose v1) or Swarm mode. For standalone Compose v2 format use mem_limit: 512m and cpus: '1.0' at the service level.
5. Log rotation
Docker's default logging driver writes to JSON files with no size limit. A verbose service running 24/7 fills your disk. When the disk fills, containers start failing in confusing ways.
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
Or set it globally in /etc/docker/daemon.json so every container gets rotation by default:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
6. network_mode: host — never use this in production
network_mode: host removes Docker's network isolation entirely. The container shares the host's network stack directly. Every port the container opens is immediately accessible on the host's public IP with no Docker NAT or port mapping involved — bypassing every network security layer.
# Red flag — remove this: network_mode: host
It exists for performance-sensitive cases (high-frequency trading, certain monitoring tools). For a web service, API, or database — there is no valid reason to use it.
7. Privileged mode
# Red flag: privileged: true # Also red flag: cap_add: - SYS_ADMIN - NET_ADMIN
Privileged containers have root access to the host kernel. A vulnerability in a privileged container is a host compromise. The only legitimate uses are very specific system-level tools. If a tutorial tells you to add privileged: true for a web app, find a better tutorial.
8. Volume mounts — be specific
# Dangerous — mounts entire host filesystem: volumes: - /:/host # Also risky — mounts Docker socket (root equivalent): volumes: - /var/run/docker.sock:/var/run/docker.sock # Better — mount only what the container needs: volumes: - ./data:/app/data - ./config:/app/config:ro
Mounting /var/run/docker.sock gives the container full control over the Docker daemon — it can start, stop, and modify any container on the host. Only monitoring tools (Portainer, Watchtower) legitimately need this.
The full checklist
- All database/cache ports bound to 127.0.0.1, not 0.0.0.0
- No hardcoded credentials in environment: blocks — all use ${VAR} references
- .env file is in .gitignore and not committed
- Every service has a healthcheck defined
- Resource limits set on all services (memory + CPU)
- Log rotation configured (max-size + max-file)
- No network_mode: host unless specifically required
- No privileged: true unless specifically required
- Volume mounts are specific — no / or /etc mounts
- Docker socket not mounted unless required by monitoring tool
Paste your docker-compose.yml to catch exposed ports, hardcoded secrets, missing healthchecks, and resource limits automatically.