Docker Compose in a homelab — 6 traps that cost me time
Docker compose is my main service orchestration in the homelab. Showing 6 concrete traps I hit over the last year and how to avoid them.

I have ~25 docker-composes. Each represents a service. After a year of regular use I collected a list of traps I hit. Each cost me at least an hour. Listed here so you can save those hours.
Trap #1: Python rebuild vs restart
The loudest one. I edit a .py file in the container, run docker compose restart, code is still the old one.
# WRONG - restart doesn't load .py changes
docker compose restart backend
# RIGHT - rebuild forces a new image
docker compose up -d --build backendReason: Python compiles to bytecode at build time, lives in the image. Container restart = old bytecode. Rebuild = new bytecode.
Same for: TypeScript after tsc, Java after mvn package, Go after go build, anything pre-compiled in the image.
Trap #2: bind mount + per-inode reload
Caddy. I edit Caddyfile on host, run docker exec caddy caddy reload. Nothing.
Reason: most editors (vim, VSCode) save via "write to temp + rename". Rename changes the inode. Bind mount in Docker remembers the original inode. Docker sees the old file.
# Workaround:
docker restart caddyOr edit via docker exec caddy vi (poor ergonomics). Or use tmpfs instead of bind (rename works but you lose persistence).
Trap #3: NO_PROXY and internal DNS
I have SOCKS5 proxy in some projects. One day all internal hostnames in docker compose started returning "name not resolved".
Reason: SOCKS5 proxy tries to resolve externally. Internal ones (e.g., bh-backend, hostname inside Docker network) don't exist there.
# .bashrc or /etc/environment
export NO_PROXY="172.16.0.0/12,localhost,127.0.0.1,*.local,bh-*,*.kamilkaletka.dev"For every new project with a Docker network I add the hostname pattern to NO_PROXY.
Trap #4: depends_on vs wait-for-ready
depends_on: [postgres] tells Docker "start postgres first". It does NOT say "wait until postgres is ready for connections".
App starts 2 seconds after postgres, postgres needs 5 seconds to initialize → app crashes on "connection refused".
services:
postgres:
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
retries: 10
app:
depends_on:
postgres:
condition: service_healthyHealthcheck + condition: service_healthy solves. Longer boot, but no race conditions.
Trap #5: volumes vs bind mount for data
First version:
volumes:
- ./data:/app/dataA year later I needed a backup. tar ./data, weighed 80GB. OK. Restore on a second machine. Permissions all broken.
Reason: bind mount inherits host permissions. Postgres (UID 999 inside container) wrote as UID 999, on a second host that user may not exist.
Better:
volumes:
postgres-data:
driver: local
services:
postgres:
volumes:
- postgres-data:/var/lib/postgresql/dataNamed volume. Docker manages permissions. Backup via docker run --rm -v postgres-data:/data alpine tar czf - /data | gzip > backup.tar.gz.
Trap #6: networks and the default bridge
Two composes. One runs service-a, the other service-b. I want them to see each other.
By default each compose creates its own network. service-a can't see service-b.
# compose-a.yml
services:
service-a:
networks:
- shared-net
networks:
shared-net:
external: true# compose-b.yml — same
services:
service-b:
networks:
- shared-net
networks:
shared-net:
external: trueNetwork shared-net I create manually:
docker network create shared-netAfter that both composes use the same network, hostnames resolve between them.
Bonus: ports and firewall
By default ports: ["3000:3000"] exposes on ALL host interfaces. In a homelab you usually don't want that.
# Localhost only — safer
ports:
- "127.0.0.1:3000:3000"
# LAN only, no public WAN
ports:
- "192.168.1.100:3000:3000"If you have a Cloudflare tunnel as your only exposure, use 127.0.0.1: so the host firewall isn't a single point of failure either.
Anti-pattern: latest tag
image: postgres:latest # WRONG
image: postgres:15.4 # RIGHT:latest means "whatever Docker has cached right now". In a homelab this sometimes drops in postgres 16 instead of 15 and the migration breaks. Pin major.minor minimum.
Docker compose is great at homelab scale. But each of those 6 traps cost me an hour. ~6 hours of life lost total. If I save you even 2 hours, the post earns its place.