$docker.recipes
·15 min read·Updated August 2025

Docker Compose Security Checklist for Production

A practical security checklist for hardening your Docker Compose deployments, from basic hygiene to advanced network isolation techniques.

securitydocker-composeproductionhardening

01Security Isn't Optional, Even for Side Projects

I learned this the hard way. In 2021, I left a Redis instance exposed to the internet without authentication. Within 48 hours, it was compromised and being used as part of a cryptocurrency mining botnet. The attacker used a well-known Redis exploit to write a cron job that downloaded a miner. The fix took 10 minutes. The lesson was permanent: every service you expose to the internet will be found and probed by automated scanners, usually within hours. Security isn't something you add later — it's something you build in from the start. This checklist covers the most important security practices for Docker Compose deployments, ordered from basic hygiene (do these first) to advanced hardening.

021. Never Hardcode Secrets

This is the most common mistake I see in docker-compose.yml files shared online. Database passwords, API keys, and tokens are written directly in the compose file, which then gets committed to Git. Always use environment variables via a .env file or Docker secrets:
[docker-compose.yml]
1# BAD: Hardcoded secrets
2services:
3 db:
4 image: postgres:16
5 environment:
6 - POSTGRES_PASSWORD=supersecret123
7
8# GOOD: Environment variable from .env file
9services:
10 db:
11 image: postgres:16
12 environment:
13 - POSTGRES_PASSWORD=${DB_PASSWORD}
14
15# BETTER: Docker secrets (Compose v2)
16services:
17 db:
18 image: postgres:16
19 secrets:
20 - db_password
21 environment:
22 - POSTGRES_PASSWORD_FILE=/run/secrets/db_password
23
24secrets:
25 db_password:
26 file: ./secrets/db_password.txt

Add .env and secrets/ to your .gitignore immediately. Leaked credentials are the number one cause of Docker container compromises.

032. Isolate Your Networks

By default, all services in a Docker Compose file share a network, which means every container can talk to every other container. This is unnecessary and dangerous. If one service is compromised, the attacker gets access to everything. Create separate networks for services that need to communicate:
[docker-compose.yml]
1services:
2 # Web app only needs to talk to the database
3 app:
4 image: myapp:latest
5 networks:
6 - frontend
7 - backend
8
9 # Database should NOT be accessible from the internet
10 db:
11 image: postgres:16
12 networks:
13 - backend # Only on the backend network
14
15 # Reverse proxy only needs frontend access
16 traefik:
17 image: traefik:v3.0
18 networks:
19 - frontend
20 ports:
21 - "443:443"
22
23networks:
24 frontend:
25 backend:
26 internal: true # No external access

Use 'internal: true' on networks that should never have external access. This prevents containers on that network from reaching the internet, which limits what an attacker can do if they compromise a container.

043. Pin Image Versions and Use Minimal Images

Using :latest tags in production is a gamble. A new image version could introduce breaking changes, vulnerabilities, or unexpected behavior — and you won't know which version is running. Always pin to a specific version. Prefer Alpine-based images when available, as they have a smaller attack surface: Instead of postgres:latest, use postgres:16.3-alpine. Instead of node:latest, use node:22.4-alpine. Instead of nginx:latest, use nginx:1.27-alpine. Regularly scan your images for known vulnerabilities:
[terminal]
1# Scan an image with Docker Scout
2docker scout cves postgres:16.3-alpine
3
4# Or use Trivy (free, open source)
5docker run --rm aquasec/trivy image postgres:16.3-alpine
6
7# Update images regularly
8docker compose pull
9docker compose up -d

054. Set Resource Limits

Without resource limits, a single runaway container can consume all CPU and memory on your host, taking down every other service. This can happen from a memory leak, a DDoS attack, or an application bug. Always set memory and CPU limits:
[docker-compose.yml]
1services:
2 app:
3 image: myapp:latest
4 deploy:
5 resources:
6 limits:
7 cpus: "2.0"
8 memory: 1G
9 reservations:
10 cpus: "0.5"
11 memory: 256M
12 # Also prevent fork bombs
13 ulimits:
14 nproc: 100
15 nofile:
16 soft: 65536
17 hard: 65536

065. Minimize Container Privileges

Containers should have the minimum privileges needed to function. By default, Docker containers run as root, which is dangerous if an attacker breaks out of the container. Key hardening measures: Run as a non-root user whenever possible. Most well-maintained images support this via a USER directive or environment variable. Use read-only root filesystems. This prevents an attacker from modifying the container's filesystem, installing tools, or writing malicious scripts. Drop all Linux capabilities and only add back the ones you need. Capabilities like NET_RAW (needed for ping) and SYS_ADMIN (needed for mounting) are often unnecessary and can be exploited.
[docker-compose.yml]
1services:
2 app:
3 image: myapp:latest
4 user: "1000:1000"
5 read_only: true
6 tmpfs:
7 - /tmp
8 - /var/run
9 security_opt:
10 - no-new-privileges:true
11 cap_drop:
12 - ALL
13 cap_add:
14 - NET_BIND_SERVICE # Only if binding to ports < 1024

Test your security settings gradually. Start with read_only and no-new-privileges (these rarely break things), then try dropping capabilities and running as non-root. Some applications need specific capabilities to function.

076. Additional Security Measures

Beyond container-level security, consider these infrastructure practices: Use a firewall: UFW or iptables on your host to restrict which ports are accessible. Docker bypasses UFW by default — you need to configure Docker's iptables integration or use the --iptables=false Docker daemon flag with manual rules. Enable automatic security updates on your host OS. Unattended-upgrades on Ubuntu/Debian handles this well. Use Fail2ban to block IP addresses that repeatedly fail authentication. This protects exposed services from brute-force attacks. Implement log monitoring. At minimum, use docker compose logs to regularly review your service logs. For more sophisticated monitoring, deploy a Loki + Grafana stack to aggregate and search logs from all containers. Regular backups — and test them. A compromise isn't always about stealing data; sometimes it's about destroying it. Backups are your last line of defense. Review our security category for Docker Compose configurations of security-focused tools like CrowdSec, Fail2ban, and Authelia that can protect your self-hosted services. Security is a process, not a destination. Start with the basics (secrets management, network isolation, pinned versions), then progressively harden as you become more comfortable. Even implementing half of this checklist puts you ahead of most self-hosted deployments.

About the Author

Frank Pegasus

DevOps engineer and self-hosting enthusiast with over a decade of experience running containerized workloads in production. Creator of docker.recipes.