2026-03-27 · DockerSecurityDevOpsChecklist

The Docker Compose Security Checklist Before You Go Live

Most Docker Compose files are written to get the service running. Security comes later — usually when something goes wrong. This is the 20% of checks that catch 80% of the real issues.

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

Paste your docker-compose.yml to catch exposed ports, hardcoded secrets, missing healthchecks, and resource limits automatically.

Related guides