The 5-Minute Docker Compose Security Checklist We Run for Every Client

3 security holes we find in every Docker Compose file. Exposed ports, root containers, no resource limits. 5-minute fix with copy-paste configs.

Y
Yash Pritwani
4 min read read

# The 5-Minute Docker Compose Security Checklist We Run for Every Client

We've reviewed Docker Compose configurations for over 30 startups. These three security holes appear in every single one. Without exception.

They're trivial to fix. Most teams just never do because nobody tells them until something goes wrong.

Hole #1: Ports Bound to 0.0.0.0

The most common Docker Compose pattern:

services:
  postgres:
    image: postgres:16
    ports:
      - "5432:5432"  # ← This is 0.0.0.0:5432

That "5432:5432" is shorthand for "0.0.0.0:5432:5432". Your database is now accessible from every network interface — including the public internet if your host has a public IP.

We've seen production Postgres instances exposed to the internet with default credentials. One client's Redis was mining crypto for 3 days before anyone noticed.

The Fix

services:
  postgres:
    image: postgres:16
    ports:
      - "127.0.0.1:5432:5432"  # ← Only accessible from localhost

For services that only talk to each other via Docker network, remove the port binding entirely:

services:
  postgres:
    image: postgres:16
    # No ports section at all — only reachable via Docker internal DNS
    networks:
      - backend

Rule: Only expose ports you need from outside Docker. If the service is internal-only, don't map it.

Hole #2: Running as Root

Check your running containers right now:

docker compose exec app whoami
# Output: root

If an attacker achieves container escape (CVE-2024-21626 in runc, for example), they land on the host as root. Full control. Game over.

The Fix

services:
  app:
    image: myapp:latest
    user: "1000:1000"
    security_opt:
      - no-new-privileges:true
    read_only: true
    tmpfs:
      - /tmp

What each line does:

user: "1000:1000" — runs as non-root UID
no-new-privileges — prevents privilege escalation via setuid binaries
read_only: true — container filesystem is immutable
tmpfs: /tmp — gives the app a writable temp directory without persistent write access

Common objection: "My app needs to write files." Use volumes for specific writable paths. Don't give the entire filesystem write access.

Hole #3: No Resource Limits

Without limits, a single container with a memory leak eats the entire host:

# Container using 14GB on a 16GB host
docker stats --no-stream
CONTAINER  CPU %  MEM USAGE / LIMIT     MEM %
app        340%   14.2GiB / 15.6GiB     91.03%

When this happens, the OOM killer starts murdering other containers. Your database goes down. Your monitoring goes down. Everything cascades.

The Fix

services:
  app:
    image: myapp:latest
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: '1.0'
        reservations:
          memory: 256M
          cpus: '0.25'

Limits = hard ceiling. Container gets OOM-killed if it exceeds this. Reservations = guaranteed minimum. Docker won't schedule other work into this space.

Rule of thumb: Set memory limit at 2x your app's normal working set. If your Node.js app uses 200MB normally, set limit to 512M. Enough headroom for spikes, tight enough to prevent runaway.

The Complete Hardened Template

Here's our baseline docker-compose.yml security config that we apply to every project:

services:
  app:
    image: myapp:latest
    user: "1000:1000"
    read_only: true
    security_opt:
      - no-new-privileges:true
    cap_drop:
      - ALL
    cap_add:
      - NET_BIND_SERVICE  # Only if binding port <1024
    tmpfs:
      - /tmp
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: '1.0'
    networks:
      - backend
    # No port binding — reverse proxy handles external access

  postgres:
    image: postgres:16-alpine
    user: "999:999"  # postgres user UID
    read_only: true
    security_opt:
      - no-new-privileges:true
    cap_drop:
      - ALL
    volumes:
      - pgdata:/var/lib/postgresql/data
    tmpfs:
      - /tmp
      - /run/postgresql
    deploy:
      resources:
        limits:
          memory: 1G
          cpus: '2.0'
    networks:
      - backend
    # No ports exposed — app connects via Docker DNS

  traefik:
    image: traefik:v3
    ports:
      - "0.0.0.0:443:443"   # Only HTTPS exposed publicly
      - "127.0.0.1:8080:8080"  # Dashboard localhost only
    # ... rest of config

Bonus: Automated Scanning

Add this to your CI to catch these issues before deploy:

# Install docker-compose-linter
pip install docker-compose-linter

# Scan for security issues
docker-compose-lint --security docker-compose.yml

Or use Trivy for image scanning:

trivy config docker-compose.yml

How We Can Help

We run free 15-minute Docker security reviews. Share your docker-compose.yml (redact credentials), and we'll tell you exactly what's exposed, what's at risk, and how to fix it.

No pitch. Just fixes.

Book a review: techsaas.cloud/contacttechsaas.cloud/contacthttps://techsaas.cloud/contact

#docker#security#devsecops#containers#devops

Need help with security?

TechSaaS provides expert consulting and managed services for cloud infrastructure, DevOps, and AI/ML operations.