Docker Compose w homelabie — 6 pułapek które mnie kosztowały
Docker compose to mój główny sposób na orchestrację serwisów u siebie. Pokazuję 6 konkretnych pułapek na które wpadłem przez ostatni rok i jak ich uniknąć.

Mam u siebie ~25 docker-compose'ów. Każdy reprezentuje serwis. Po roku regularnego użycia zebrałem listę pułapek na które wpadłem. Każda kosztowała mi co najmniej godzinę. Pokazuję je tutaj żeby zaoszczędzić Tobie te godziny.
Pułapka #1: Python rebuild vs restart
Najgłośniejsza w mojej liście. Edytuję plik .py w kontenerze, robię docker compose restart, kod nadal stary.
# ŹLE - restart nie ładuje zmian w .py
docker compose restart backend
# DOBRZE - rebuild zmusza do nowego image'a
docker compose up -d --build backendPowód: Python kompiluje do bytecode przy buildzie, zostaje w image. Restart container = stary bytecode. Rebuild = nowy bytecode.
To samo dotyczy: TypeScript po tsc, Java po mvn package, Go po go build, wszystko co jest pre-compiled w image.
Pułapka #2: bind mount + per-inode reload
Caddy. Edytuję Caddyfile na hoście, robię docker exec caddy caddy reload. Nic.
Powód: większość edytorów (vim, VSCode) zapisuje przez "write to temp + rename". Rename zmienia inode. Bind mount w Dockerze pamięta inode oryginalny. Docker widzi stary plik.
# Workaround:
docker restart caddyAlbo edytuj przez docker exec caddy vi (mało ergonomiczne). Albo użyj tmpfs mountu zamiast bind (wtedy rename działa, ale tracisz persystencję).
Pułapka #3: NO_PROXY i wewnętrzny DNS
Mam SOCKS5 proxy w niektórych projektach. Pewnego dnia wszystkie wewnętrzne hostname'y w docker compose zaczęły zwracać "name not resolved".
Powód: SOCKS5 proxy próbuje resolve'ować zewnętrznie. Wewnętrzne (np. bh-backend, hostname w docker network) tam nie istnieją.
# .bashrc lub /etc/environment
export NO_PROXY="172.16.0.0/12,localhost,127.0.0.1,*.local,bh-*,*.kamilkaletka.dev"Każdy nowy projekt z dockerową siecią dopisuję wzorzec hostname'ów do NO_PROXY.
Pułapka #4: depends_on a wait-for-ready
depends_on: [postgres] mówi Dockerowi "uruchom postgres pierwszy". NIE mówi "poczekaj aż postgres będzie gotowy do połączeń".
Aplikacja startuje 2 sekundy po postgresie, postgres potrzebuje 5 sekund na initialize → app crash na "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 rozwiązuje. Dłuższy boot, ale brak race condition'ów.
Pułapka #5: volumes vs bind mount dla danych
Pierwsza wersja:
volumes:
- ./data:/app/dataPo roku przyszedł czas na backup. Robię tar ./data, ważyło 80GB. OK. Restoruję na drugiej maszynie. Permissions wszystko popsute.
Powód: bind mount dziedziczy permissions hosta. Postgres (UID 999 w container) zapisał z UID 999, na drugim hoście tego user'a może nie być.
Lepsza wersja:
volumes:
postgres-data:
driver: local
services:
postgres:
volumes:
- postgres-data:/var/lib/postgresql/dataNamed volume. Docker zarządza permissions. Backup przez docker run --rm -v postgres-data:/data alpine tar czf - /data | gzip > backup.tar.gz.
Pułapka #6: networks i domyślny bridge
Mam dwa compose'y. Jeden uruchamia service-a, drugi service-b. Chcę żeby się widziały.
Domyślnie każdy compose tworzy własną sieć. service-a nie widzi service-b.
# compose-a.yml
services:
service-a:
networks:
- shared-net
networks:
shared-net:
external: true# compose-b.yml — to samo
services:
service-b:
networks:
- shared-net
networks:
shared-net:
external: trueNetwork shared-net muszę utworzyć ręcznie:
docker network create shared-netPo tym oba compose'y używają tej samej sieci, hostname'y się resolve'ują między.
Bonus: porty i firewall
Domyślnie ports: ["3000:3000"] exposuje na WSZYSTKIE interfejsy hosta. W homelabie zwykle nie chcesz.
# Tylko localhost — bezpieczniej
ports:
- "127.0.0.1:3000:3000"
# Tylko LAN, nie publiczny WAN
ports:
- "192.168.1.100:3000:3000"Jak masz Cloudflare tunnel jako jedyny exposure, używaj 127.0.0.1: żeby nawet host firewall nie był single point of failure.
Anti-pattern: latest tag
image: postgres:latest # ŹLE
image: postgres:15.4 # DOBRZE:latest znaczy "co Docker akurat ma cached". W homelabie to czasem wstrzyknie ci postgres 16 zamiast 15 i migracja nie zadziała. Pin major.minor minimum.
Docker compose to świetne narzędzie na poziomie homelab. Ale każde z tych 6 pułapek mnie kosztowało godzinę. Razem ~6 godzin straconego życia. Jeśli oszczędzę Tobie chociaż 2 godziny, post warto pisać.