Compose je textový soubor, který popisuje několik kontejnerů a jejich propojení, a příkaz, který uvede skutečnost do souladu se souborem. Na jednom serveru to má větší cenu, než se zdá. Bez něj je třívrstvý stack tři řádky docker run s dvaceti přepínači, které si po šesti týdnech pamatuješ špatně. S ním stačí docker compose up -d, konfigurace je soubor, který můžeš držet v gitu, a postavit stack na novém stroji znamená git clone a jeden příkaz.
Čím Compose není, je plánovač. Nepřesune kontejner na jiný hostitel, nerestartuje nic, co přestalo být zdravé, a o výpadcích nemá žádný názor. Na jednom VDS tě to nic nestojí, protože existuje jen jeden hostitel a alternativou byl shellový skript. Tento článek postaví realistický stack - reverse proxy, aplikaci a databázi PostgreSQL - a pak projde části, které rozhodují o tom, jestli přežije rok: kde jsou data, co maže down, jak se dostanou dovnitř tajné hodnoty a co dělat, když stack nenaběhne.
Co je Compose a jaký soubor čte#
Compose v2 je plugin, který se volá jako docker compose s mezerou. Starý pythonový docker-compose s pomlčkou je samostatný, ukončený program; pokud návod používá pomlčku, je starší než současné nástroje, i když formát souboru je z velké části stejný.
Compose hledá v tomto pořadí: compose.yaml, compose.yml, docker-compose.yaml, docker-compose.yml. První název je současná konvence a poslední ten, který má každý. Funguje oboje. Co už k ničemu není, je řádek version: "3.8" na začátku starých souborů - v Compose v2 je zastaralý, ignoruje se a vypisuje varování. Smaž ho.
Než dává soubor smysl, je třeba pochopit jeden pojem: projekt. Compose všechno, co vytvoří, označuje názvem projektu, který je ve výchozím stavu název adresáře. Soubor compose.yaml v /srv/notes vytvoří kontejnery notes-app-1 a notes-db-1, síť notes_default a volume notes_dbdata. Dva projekty ve dvou adresářích se nepoperou. Proto také přesun nebo přejmenování adresáře může náhle způsobit, že Compose uvidí zcela nový stack bez volumes - pokud se adresář může přesouvat, zafixuj název klíčem name: na nejvyšší úrovni.
Jeden soubor, tři služby#
Tady je stack, který opravdu funguje: vpředu Caddy pro TLS, aplikace v Node a za oběma PostgreSQL.
name: notesservices: proxy: image: caddy:2 restart: unless-stopped ports: - "80:80" - "443:443" volumes: - ./Caddyfile:/etc/caddy/Caddyfile:ro - caddy_data:/data - caddy_config:/config depends_on: - app app: image: ghcr.io/example/notes:1.4.2 restart: unless-stopped env_file: - app.env environment: DATABASE_URL: postgres://notes:${DB_PASSWORD}@db:5432/notes NODE_ENV: production depends_on: db: condition: service_healthy db: image: postgres:17 restart: unless-stopped environment: POSTGRES_USER: notes POSTGRES_PASSWORD: ${DB_PASSWORD} POSTGRES_DB: notes volumes: - dbdata:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U notes -d notes"] interval: 10s timeout: 5s retries: 5 start_period: 30svolumes: dbdata: caddy_data: caddy_config:A konfigurace proxy, kterou kontejner připojuje. Má tři řádky, protože Caddy si certifikát získá a obnovuje sám, jakmile DNS jméno míří na stroj:
notes.example.com { encode gzip reverse_proxy app:3000}Všimni si, co chybí. Aplikace nemá žádný záznam ports: a databáze také ne. Ani jedna z nich není zvenku vůbec dosažitelná. Něco publikuje jen proxy, a to dva porty, které internet už očekává.
Síť a proč port publikuje jen proxy#
Compose vytvoří pro každý projekt jednu síť a připojí k ní všechny služby. V ní vestavěné DNS Dockeru překládá názvy služeb na jejich kontejnery, takže app se k databázi dostane na hostname db a portu 5432 bez jakékoli konfigurace a proxy se k aplikaci dostane na app:3000. Nic nemusí znát IP adresu, a ty se stejně při každém znovuvytvoření mění.
Důležitý důsledek: porty mezi službami ve stejné síti Compose jsou už otevřené. ports: není o tom, aby spolu kontejnery mohly mluvit; je o tom, že prorazí díru z hostitele do kontejneru. Každý záznam ports: je nový veřejný listener, takže je přidávej jen pro vstupní bránu.
Tohle řeší i problém, na který člověk narazí poprvé. Publikovaný port obchází UFW - ports: - "5432:5432" u databáze zpřístupní PostgreSQL z internetu i při firewallu s výchozím zákazem, protože paket se přeposílá do kontejneru místo doručení hostiteli. Celý mechanismus je v průvodci firewallem UFW a stručnější verze v článku Docker na VDS. Pokud musíš port kvůli ladění publikovat, publikuj ho na loopback - "127.0.0.1:5432:5432" - a připojuj se přes SSH tunel.
Pokud ti víc vyhovuje nginx než Caddy, platí stejný tvar s delším konfiguračním souborem, včetně hlaviček, které proxovaná aplikace potřebuje, aby viděla skutečného klienta. Ten server blok najdeš v průvodci reverse proxy v nginxu a co dělá reverse proxy vysvětluje, proč vůbec existuje X-Forwarded-For.
Proměnné prostředí a dva druhy env souboru#
Tohle plete skoro každého, protože existují dva mechanismy s podobným jménem, které dělají různou práci.
Soubor `.env` v adresáři projektu čte samotný Compose, než cokoli spustí, a jeho hodnoty se dosazují za zástupné ${VAR} uvnitř compose.yaml. Konfiguruje soubor, ne kontejnery. Pokud .env obsahuje DB_PASSWORD=hunter2, pak se ${DB_PASSWORD} v compose souboru změní na tuto hodnotu.
`env_file:` u služby předá kontejneru soubor řádků KEY=value jako proměnné prostředí. Compose do něj nikdy nenahlíží. Sem patří konfigurace aplikace.
DB_PASSWORD=a-long-random-stringAPP_TAG=1.4.2SESSION_SECRET=another-long-random-stringSMTP_HOST=smtp.example.comLOG_LEVEL=infoOba soubory obsahují tajné hodnoty, takže oba chtějí chmod 600 a oba patří do .gitignore, s commitnutým souborem app.env.example, který uvádí klíče bez hodnot. Proměnné nastavené pod environment: přebíjejí cokoli z env_file:, což je priorita, kterou si zapamatuj, když hodnota záhadně neodpovídá souboru.
Než začneš obviňovat aplikaci, zkontroluj, co Compose opravdu vyhodnotil:
$ docker compose config$ docker compose config --servicesdocker compose config vypíše plně sloučený a dosazený soubor. Prázdná hodnota tam, kde čekáš heslo, znamená, že soubor .env není tam, kde ho Compose hledá, a to je skoro vždy tím, že jsi příkaz spustil ze špatného adresáře. Proměnné prostředí a tajné hodnoty řeší širší otázku, co má být proměnnou prostředí vůbec.
Pro cokoli, co nechceš mít v prostředí procesu, Compose podporuje tajné hodnoty v souborech: blok secrets: pojmenuje soubor na hostiteli a služba ho dostane připojený jen pro čtení v /run/secrets/<name>. Mnoho images má variantu ..._FILE proměnné s heslem právě proto, abys ji mohl nasměrovat tam.
Volumes: co musí přežít down#
Každá služba ve výše uvedeném příkladu je zahoditelná, kromě toho, co leží v pojmenovaných volumes. Je to záměr a je to model, který si drž v hlavě: kontejnery jsou dobytek, volumes jsou stádo.
| Cesta | Druh | Proč |
|---|---|---|
dbdata:/var/lib/postgresql/data | pojmenovaný volume | Databáze. Ztráta znamená ztratit všechno |
caddy_data:/data | pojmenovaný volume | Vydané certifikáty a klíče účtu ACME |
./Caddyfile:/etc/caddy/Caddyfile:ro | bind mount | Upravuješ ho, takže ho drž v repozitáři |
Na caddy_data se snadno zapomene a je otravné o něj přijít: bez něj si každé znovuvytvoření proxy žádá certifikáty od začátku a certifikační autority omezují vydávání na doménu za týden. Pár nedbalých přestaveb a na doméně, která obsluhuje skutečný provoz, nedostaneš nové certifikáty několik dní.
Zálohy jsou dump databáze podle plánu, ne kopie datového adresáře, do kterého se zrovna zapisuje:
$ docker compose exec -T db pg_dump -U notes -Fc notes > /srv/backups/notes-$(date +%F).dump$ docker compose exec -T db psql -U notes -d notes < /srv/backups/restore-test.sql-T je důležité: bez něj exec přidělí pseudoterminál a přesměrovaný binární dump poškodí převodem konců řádků. První řádek dej do cronu nebo systemd timeru, výsledky kopíruj mimo stroj a pak jeden obnov někde neškodném, abys dokázal, že soubor je skutečný. Záloha, kterou nikdo neobnovil, je hypotéza - zálohy a obnova databází je to delší zdůvodnění.
depends_on, healthchecky a pořadí startu#
depends_on v krátké podobě řídí jen pořadí startu. Čeká, až se kontejner vytvoří a spustí, ne až bude software uvnitř připraven odpovídat. Kontejner PostgreSQL při prvním spuštění inicializuje několik sekund, takže aplikace, která se připojuje při startu, selže proti databázi, která technicky běží.
Dlouhá podoba to řeší čekáním na healthcheck:
depends_on: db: condition: service_healthyTo vyžaduje, aby databáze nějaký definovala, jako to v příkladu dělá přes pg_isready. Čtyři parametry stojí za pochopení: interval je, jak často se kontrola spouští, timeout je, jak dlouho smí trvat jeden pokus, retries je, kolik po sobě jdoucích selhání znamená stav nezdravý, a start_period je úvodní odklad, během kterého se selhání nepočítají. Databáze, která při prvním startu obnovuje velký dump, potřebuje štědrý start_period, jinak ji prohlásí za nezdravou, zatímco dělá přesně to, co má.
Dostupné jsou tři podmínky: service_started (výchozí chování krátké podoby), service_healthy a service_completed_successfully, z nichž poslední je způsob, jak spustit migrační úlohu před startem aplikace.
I při tom všem napiš aplikaci tak, aby opakovala připojení k databázi s krátkým odstupem. Pořadí startu vyřeší prvních třicet sekund; restart databáze ve tři ráno je ten druhý případ a služba, která se natrvalo ukončí, protože se jednou nepřipojila, je služba, která ve tři ráno potřebuje člověka. Migrace bez výpadku pokrývá stejný problém ze strany schématu.
Příkazy, které budeš opravdu psát#
$ docker compose up -d # create or update everything, detached$ docker compose ps # what is running, and its health$ docker compose logs -f --tail 100 app$ docker compose exec app sh # a shell in the running container$ docker compose run --rm app npm run migrate$ docker compose restart app # restart without recreating$ docker compose stop # stop, keep everything$ docker compose down # remove containers and networkup -d je idempotentní a je to příkaz, který používáš téměř na všechno. Porovná soubor se skutečností a znovu vytvoří jen služby, jejichž definice se změnila, takže úprava tagu image jedné služby a up -d se dotkne jen této služby a ostatní nechá běžet.
Rozdíl mezi exec a run lidi mate: exec vstoupí do kontejneru, který už běží, run spustí nový ze stejné definice. Pro jednorázové úkoly jako migrace nebo správcovský příkaz používej run --rm a exec pro pohled na živý stav.
Ještě dvě věci stojí za znalost. docker compose pull stáhne novější images, aniž by se dotkl běžícího stacku, takže stahovat můžeš ve vhodnou chvíli a přepnout v klidnější. A docker compose --profile debug up -d spustí služby označené klíčem profiles:, což je způsob, jak mít v jednom souboru administrační nástroj nebo grafické rozhraní databáze, aniž by běželo pořád.
Aktualizace, rollback a verze, kterou budeš litovat#
Aktualizace jsou dva příkazy a bezpečnými je dělá tag v souboru:
$ docker compose pull app$ docker compose up -d appProtože jsou images připnuté na 1.4.2 místo na latest, pull stáhne něco nového, jen když jsi soubor upravil - a o to jde. Nasazení je pak úprava jednoho řádku, commit a up -d. Rollback je stejná úprava obráceně a funguje, protože jsi starý image ještě nesmazal; docker image ls ho ukáže. Nech si alespoň předchozí tag, dokud ten nový nepřežije den.
Dvě věci tento jednoduchý postup nedává. Několik sekund trvá mezera, kdy se starý kontejner zastavuje a nový startuje, a proxy v tu chvíli nemá s kým mluvit, takže návštěvníci dostanou 502. U hobby stacku je to přijatelné; pokud ne, nasazení bez výpadku na malém serveru popisuje vzory, které mezeru zavřou. A databáze není verzovaná spolu s aplikací, takže vydání, které vyžaduje změnu schématu, potřebuje migraci spustit jako samostatný krok, ideálně takový, který je bezpečný pro starý i nový kód.
Aby stack po restartu naběhl, obvykle stačí restart: unless-stopped u každé služby, protože démon Dockeru startuje při bootu a vezme je s sebou. Pokud chceš stack navázat na vlastní pořadí startu stroje nebo ho nechat počkat na připojený souborový systém, zabal docker compose up -d místo toho do malé jednotky systemd - služby systemd pro tvoje aplikace má ten soubor.
Když stack nenaběhne#
`service "app" depends on undefined service db`. Překlep nebo chyba v odsazení. YAML je citlivé na bílé znaky a konvence jsou dvě mezery; tabulátor kdekoli v souboru je syntaktická chyba. docker compose config zachytí obojí dřív, než cokoli spustíš.
Port is already allocated. Port na hostiteli drží jiný proces nebo jiný projekt Compose. Pojmenuje ho sudo ss -lntp. Dva projekty nemohou oba publikovat port 443; proto existuje proxy.
Aplikace nepřeloží `db`. Obě služby musí být ve stejném projektu a stejné síti. Pokud stack rozdělíš do dvou compose souborů, dostanou dvě sítě - deklaruj v obou externí síť, nebo je drž v jednom souboru.
Databáze startuje pokaždé od nuly. Volume není připojený tam, kde ho image očekává, takže data jdou do zapisovatelné vrstvy kontejneru a umírají s ním. docker compose exec db ls -la /var/lib/postgresql/data by měl ukázat soubory a docker volume ls volume s prefixem projektu.
Změna `POSTGRES_PASSWORD` nic neudělala. Oficiální images databází tyto proměnné používají jen při inicializaci prázdného datového adresáře. Na existujícím volume je heslo takové, jaké bylo první den, a mění se příkazem ALTER USER uvnitř databáze.
Všechno je zdravé a web vrací 502. Proxy míří na špatný port nebo špatné jméno. Ověř, že aplikace skutečně naslouchá na 0.0.0.0 uvnitř kontejneru, a ne na 127.0.0.1, protože loopback kontejneru se s proxy nesdílí.
Došlo místo na disku. Staré images a build cache. docker system df ukáže, kam se ztratilo; docker image prune -a a docker builder prune ho bezpečně uvolní. Přepínači --volumes se vyhni, pokud si nejsi jistý.
Takový stack se pohodlně vejde na malé VDS: proxy potřebuje skoro nic, PostgreSQL je u malého webu spokojený s gigabajtem, pokud ho vyladíš, a aplikace je, jaká je. Ladění PostgreSQL na malých serverech je ta stránka s nastavením. Co ti VDS dává a spravovaný tarif nemůže, je právě tohle: jádro, démon Dockeru a root, takže stack ve tvém souboru je stack, který běží.
FAQ#
Je Compose přehnaný pro jeden kontejner?
Trochu, ale i tak se vyplatí. Soubor s jednou službou zaznamená přepínače, které bys jinak psal znovu, dělá z aktualizace dva příkazy a za běhu nestojí nic. Ve chvíli, kdy přidáš databázi, je to samozřejmý způsob.
Mám použít build:, nebo hotový image?
Pokud můžeš, sestav image jinde a stáhni označený tag. Build na serveru váže nasazení na procesor a disk stroje, plní build cache a znamená, že po neúspěšném buildu ti nic neběží. Sestavení na místě je u malého projektu přijatelné, pokud si ponecháš předchozí image.
Smaže docker compose down moji databázi?
Samo o sobě ne. Odstraní kontejnery a sítě a pojmenované volumes ponechá. docker compose down -v volumes smaže a to databázi bez potvrzení opravdu zničí.
Jak provozovat několik stacků na jednom serveru?
Každý v jednom adresáři, každý s vlastním compose.yaml a porty 80 a 443 publikuje jen jeden z nich. Sdílenou proxy dej do vlastního projektu na externí síť, ke které se ostatní připojí, nebo dej každé aplikaci jiný interní port a směruj podle hostname v jedné proxy.
Můžu používat Compose na spravovaném hostingovém tarifu?
Ne. Compose ovládá démon Dockeru a spravovaný herní nebo aplikační tarif je sám kontejner bez něj. Je to jeden z jasnějších důvodů přejít na VDS, kde je démon i účet root tvůj.
Co nahradí Compose, až stack naroste?
Obvykle po dlouhou dobu nic. Jeden server s hrstkou služeb je přesně velikost, která Compose sedí. Až opravdu potřebuješ víc než jeden stroj, další krok je plánovač a po něm tým, který ho udržuje - obojí jsou velké náklady, které je snadné převzít příliš brzy.




Komentáře
Zcela anonymně: žádný účet, žádný e-mail, žádné cookie. Ukládáme jen jméno, které napíšeš, text a čas - nic dalšího. Počet odkazů je omezený a značkování se nevykresluje.