docker.recipes
Fundamentals10 min read

Common Docker Compose Mistakes in Self-Hosted Setups

Avoid the most frequent pitfalls that cause frustration, security issues, and downtime in Docker Compose deployments.

01Introduction

Everyone makes mistakes when learning Docker Compose. Some cause minor annoyances, others lead to data loss or security breaches. Here are the most common mistakes we see in self-hosted setups—and how to avoid them.

02Mistake #1: Using :latest Tags in Production

The :latest tag seems convenient, but it's unpredictable. Your container might work today and break tomorrow when the image updates. Updates happen when you least expect them—usually during docker compose pull.
1# BAD: Unpredictable
2services:
3 postgres:
4 image: postgres:latest
5
6 app:
7 image: nginx:latest
8
9# GOOD: Pinned versions
10services:
11 postgres:
12 image: postgres:15.4-alpine
13
14 app:
15 image: nginx:1.25.3-alpine
16
17# ACCEPTABLE: Major version pinning (still gets security updates)
18services:
19 postgres:
20 image: postgres:15-alpine

Check image changelogs before updating. Pin at least the major version in production.

03Mistake #2: Forgetting Restart Policies

Without a restart policy, containers stay down after a crash or server reboot. Your services won't automatically recover.
1# BAD: No restart policy
2services:
3 app:
4 image: myapp:1.0
5 # Container stays dead after crash or reboot
6
7# GOOD: Automatic restart
8services:
9 app:
10 image: myapp:1.0
11 restart: unless-stopped # Restarts unless you manually stop it
12
13# Other options:
14# restart: always # Always restart, even after manual stop
15# restart: on-failure # Only restart on non-zero exit code
16# restart: "no" # Never restart (default)

Use unless-stopped for most services. Use on-failure for one-shot jobs or init containers.

04Mistake #3: Exposing Ports to the World

By default, ports published with ports: bind to 0.0.0.0, accessible from any network interface—including the public internet if you don't have a firewall.
1# BAD: Database accessible from internet
2services:
3 postgres:
4 image: postgres:15
5 ports:
6 - "5432:5432" # Binds to 0.0.0.0:5432
7
8# GOOD: Only accessible from localhost
9services:
10 postgres:
11 image: postgres:15
12 ports:
13 - "127.0.0.1:5432:5432"
14
15# BETTER: Don't expose at all, use Docker networks
16services:
17 app:
18 networks:
19 - backend
20
21 postgres:
22 networks:
23 - backend
24 # No ports exposed - only app can reach it
25
26networks:
27 backend:

Never expose database ports publicly. Use 127.0.0.1 binding or Docker networks.

05Mistake #4: Forgetting Persistent Volumes

Container filesystems are ephemeral. Without volumes, data disappears when containers are recreated. This catches many beginners off guard.
1# BAD: Database data lost on container recreation
2services:
3 postgres:
4 image: postgres:15
5 environment:
6 - POSTGRES_PASSWORD=secret
7 # No volume = data gone after "docker compose down"
8
9# GOOD: Data persists
10services:
11 postgres:
12 image: postgres:15
13 environment:
14 - POSTGRES_PASSWORD=secret
15 volumes:
16 - postgres_data:/var/lib/postgresql/data
17
18volumes:
19 postgres_data:

Check the image's documentation for which paths need persistence. Look for VOLUME instructions in the Dockerfile.

06Mistake #5: Mounting Docker Socket Carelessly

Some containers need the Docker socket (/var/run/docker.sock) for legitimate purposes. But mounting it gives the container root-equivalent access to your host.
1# RISKY: Full Docker access
2services:
3 portainer:
4 image: portainer/portainer-ce
5 volumes:
6 - /var/run/docker.sock:/var/run/docker.sock
7
8# The container can:
9# - Start/stop any container
10# - Mount any host directory
11# - Effectively run any command as root
12
13# SAFER: Use with trusted images only, consider alternatives
14# - Use Portainer's agent mode
15# - Use Docker socket proxy (tecnativa/docker-socket-proxy)
16# - Limit with read-only mode (still risky)

Only mount Docker socket in containers you fully trust. Consider using a socket proxy to limit accessible API endpoints.

07Mistake #6: Hardcoding Secrets

Putting passwords directly in docker-compose.yml makes them visible to anyone with file access and causes problems when sharing configurations.
1# BAD: Secrets visible in plain text
2services:
3 db:
4 image: postgres:15
5 environment:
6 - POSTGRES_PASSWORD=my_super_secret_password123
7
8# GOOD: Use environment variables
9services:
10 db:
11 image: postgres:15
12 environment:
13 - POSTGRES_PASSWORD=${DB_PASSWORD}
14
15# .env file (not committed to git)
16# DB_PASSWORD=my_super_secret_password123

08Mistake #7: Not Understanding Docker Networking

Services in the same compose file share a network by default. But cross-project communication requires explicit networks, and localhost doesn't work like you expect.
1# Common mistake: Using localhost to reach other containers
2services:
3 app:
4 image: myapp:1.0
5 environment:
6 # BAD: localhost means the app container itself
7 - DATABASE_URL=postgres://localhost:5432/mydb
8
9 # GOOD: Use the service name
10 - DATABASE_URL=postgres://db:5432/mydb
11
12 db:
13 image: postgres:15
14
15# Cross-project communication requires external networks
16networks:
17 shared:
18 external: true
19 name: shared_network

Inside Docker, use service names as hostnames. localhost refers to the container itself, not the host or other containers.

09Mistake #8: No Resource Limits

A misbehaving container can consume all CPU and memory, crashing your entire server. Set limits to contain the blast radius.
1services:
2 app:
3 image: myapp:1.0
4 deploy:
5 resources:
6 limits:
7 cpus: '2'
8 memory: 2G
9 reservations:
10 cpus: '0.5'
11 memory: 512M
12
13# Note: deploy.resources works with "docker compose up"
14# For older compose, use:
15# mem_limit: 2g
16# cpus: 2

Start with generous limits and tighten based on actual usage. Use docker stats to monitor resource consumption.

10Mistake #9: Not Managing Logs

Docker logs grow unbounded by default. A chatty application can fill your disk in days.
1services:
2 app:
3 image: myapp:1.0
4 logging:
5 driver: "json-file"
6 options:
7 max-size: "10m"
8 max-file: "3"
9
10# Or set globally in /etc/docker/daemon.json
11# {
12# "log-driver": "json-file",
13# "log-opts": {
14# "max-size": "10m",
15# "max-file": "3"
16# }
17# }

11Mistake #10: Running as Root

Many containers run as root by default. If compromised, attackers get root access inside the container (and potentially escape to the host).
1# BETTER: Run as non-root user
2services:
3 app:
4 image: myapp:1.0
5 user: "1000:1000" # Run as UID/GID 1000
6
7 # Many images support PUID/PGID
8 linuxserver-app:
9 image: linuxserver/sonarr
10 environment:
11 - PUID=1000
12 - PGID=1000
13
14 # Check if image supports non-root
15 # docker run --rm myapp:1.0 id
16 # If it shows "uid=0(root)", the image runs as root by default

Prefer images that run as non-root by default. The linuxserver.io images are excellent for this.