I've reviewed over 200 Docker Compose files. Most of them have the same problems.
Not bugs — they work fine. But they're ticking time bombs for production, security, or your sanity at 3 AM.
Here are the 7 most common mistakes and how to fix them.
1. Using latest Tag
# BAD
services:
db:
image: postgres:latest
# GOOD
services:
db:
image: postgres:16.2-alpine
Why it matters: latest today is not latest tomorrow. Your staging and production will run different versions. Debugging will be hell.
Rule: Always pin to a specific version. Use Alpine variants for smaller images.
2. No Health Checks
# BAD
services:
api:
depends_on:
- db
# GOOD
services:
db:
image: postgres:16.2-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
api:
depends_on:
db:
condition: service_healthy
Why it matters: depends_on only waits for the container to start, not for the service to be ready. Your API crashes because Postgres isn't accepting connections yet.
3. Hardcoded Credentials
# BAD
services:
db:
environment:
POSTGRES_PASSWORD: mysecretpassword123
# GOOD
services:
db:
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
env_file:
- .env
And add .env to .gitignore. I've found production database passwords in public GitHub repos more times than I'd like to admit.
4. No Resource Limits
# BAD — container can eat all your RAM
services:
api:
image: myapp
# GOOD
services:
api:
image: myapp
deploy:
resources:
limits:
memory: 512M
cpus: '0.5'
Why it matters: One runaway container shouldn't kill your entire server. Set limits based on actual usage (check with docker stats).
5. Not Using Named Volumes
# BAD — bind mount
services:
db:
volumes:
- ./data:/var/lib/postgresql/data
# GOOD — named volume
services:
db:
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
Why it matters: Bind mounts have permission issues across OS. Named volumes are managed by Docker, portable, and easier to backup.
Use bind mounts only for development (code sync). Use named volumes for data.
6. Running as Root
# In your Dockerfile
# BAD
CMD ["node", "server.js"]
# GOOD
RUN addgroup -S app && adduser -S app -G app
USER app
CMD ["node", "server.js"]
In compose:
services:
api:
user: "1000:1000"
Why it matters: If your container gets compromised, the attacker has root access to the container filesystem. Running as non-root limits the blast radius.
7. No Restart Policy
# BAD — container dies, stays dead
services:
api:
image: myapp
# GOOD
services:
api:
image: myapp
restart: unless-stopped
Options:
-
no— never restart (default) -
always— restart no matter what -
on-failure— restart only on non-zero exit -
unless-stopped— restart unless you explicitly stopped it
For production: unless-stopped. For dev: no or on-failure.
Bonus: The Template
I maintain a collection of battle-tested Docker Compose configs:
Docker Compose Templates — PostgreSQL, Redis, MongoDB, Elasticsearch, Grafana, MinIO. Copy-paste and go.
More DevOps tools: GitHub Actions Templates | Awesome Developer Tools 2026
What's the worst Docker mistake you've seen in production? I've got stories. 👇
DevOps articles at dev.to/0012303
Top comments (0)