docker compose — više kontejnera odjednom

Aplikacija retko radi sama. Uz nju ide baza, možda Redis, možda reverse proxy. Sa docker run to znači tri duge komande, ručno pravljenje mreže i pamćenje redosleda pokretanja.

Compose sve to opisuje u jednom fajlu. Jedna komanda diže ceo sklop, jedna ga ruši.

docker-compose ili docker compose

Stariji alat je bio zaseban program pisan u Pythonu i zvao se docker-compose, sa crticom. Danas je Compose ugrađen u Docker kao poddomanda — docker compose, sa razmakom.

$ docker compose version
Docker Compose version v2.29.1

Ako ti to ne radi, instaliraj dodatak:

# dnf install -y docker-compose-plugin

Fajl se danas zove compose.yaml. Staro ime docker-compose.yml i dalje radi i videćeš ga svuda.

Prvi compose fajl

$ mkdir moj-sajt && cd moj-sajt
$ vi compose.yaml
services:
  web:
    image: nginx:1.25-alpine
    ports:
      - "8080:80"
    volumes:
      - ./html:/usr/share/nginx/html:ro
$ mkdir html
$ echo "<h1>Radi!</h1>" > html/index.html
$ docker compose up -d
$ curl localhost:8080

Rušenje:

$ docker compose down

Nekoliko reči o YAML-u

Compose fajl je YAML, a YAML je osetljiv na razmake. Dva pravila koja rešavaju devedeset posto problema:

  • Tabulatori nisu dozvoljeni. Samo razmaci.
  • Uvlačenje mora biti dosledno — po dva razmaka po nivou.

Ako ti compose kaže „yaml: line 12: did not find expected key", skoro sigurno je uvlačenje. Proveri fajl bez pokretanja:

$ docker compose config

Ispisuje kompletnu, razrešenu konfiguraciju — ili grešku sa brojem reda.

Aplikacija i baza

Ovo je razlog zbog kojeg Compose postoji:

services:
  web:
    build: .
    ports:
      - "8000:8000"
    environment:
      DATABASE_URL: postgresql://app:tajna@baza:5432/appdb
    depends_on:
      - baza
    restart: unless-stopped

  baza:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: tajna
      POSTGRES_DB: appdb
    volumes:
      - podaci:/var/lib/postgresql/data
    restart: unless-stopped

volumes:
  podaci:

Obrati pažnju na DATABASE_URL — adresa baze je baza, ime servisa. Compose pravi zajedničku mrežu u kojoj se servisi vide po imenu, pa ti ne treba ni IP adresa ni ručno povezivanje.

Servis web nema image nego build: . — gradi se iz Dockerfile-a u tekućem direktorijumu.

Ključne stavke

image i build

image: postgres:16-alpine

build: .

build:
  context: .
  dockerfile: Dockerfile.prod
  args:
    VERZIJA: "2.0"

ports

ports:
  - "8080:80"        # port hosta : port kontejnera
  - "127.0.0.1:5432:5432"

Drugi oblik vezuje port samo za loopback, pa baza nije dostupna spolja. Za sve što ne mora biti javno, ovo je pravilo a ne preporuka.

Ako servisu port ne treba spolja — a bazi obično ne treba — jednostavno ga ne objavljuj. Ostali servisi mu i dalje pristupaju kroz zajedničku mrežu.

volumes

volumes:
  - podaci:/var/lib/postgresql/data     # imenovani volumen
  - ./html:/usr/share/nginx/html:ro     # bind mount, samo za čitanje
  - ./config/nginx.conf:/etc/nginx/nginx.conf:ro

Imenovani volumen je za podatke kojima upravlja Docker — baze, otpremljeni fajlovi. Mora se prijaviti u odeljku volumes: na dnu fajla.

Bind mount povezuje direktorijum sa hosta. Odličan je u razvoju, jer menjaš fajl i odmah vidiš efekat. Sufiks :ro znači da kontejner ne može da piše — koristi ga za konfiguracije uvek.

Detaljnije o samim volumenima u lekciji docker volume.

environment i env_file

environment:
  APP_MODE: production
  DATABASE_URL: postgresql://app:tajna@baza:5432/appdb

Lozinke ipak ne drži u compose fajlu — on završi u Git-u. Izdvoji ih:

env_file:
  - .env

.env:

POSTGRES_PASSWORD=stvarna-lozinka
DATABASE_URL=postgresql://app:stvarna-lozinka@baza:5432/appdb

Zatim .env obavezno u .gitignore i .dockerignore.

Vrednosti iz .env mogu se koristiti i u samom compose fajlu:

    environment:
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?mora biti postavljena}

restart

restart: unless-stopped
VrednostPonašanje
nopodrazumevano, ne restartuje
on-failuresamo ako je izašao sa greškom
alwaysuvek, uključujući posle ručnog zaustavljanja
unless-stoppeduvek, osim ako si ga ti zaustavio

Za servise na serveru koristi unless-stopped.

depends_on i zašto nije dovoljan

depends_on:
  - baza

Ovo određuje samo redosled pokretanja. Kontejner baze je pokrenut — ali PostgreSQL unutra možda još učitava. Aplikacija se javi, dobije odbijenu vezu i padne.

Rešenje je provera zdravlja:

services:
  web:
    build: .
    depends_on:
      baza:
        condition: service_healthy

  baza:
    image: postgres:16-alpine
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app"]
      interval: 5s
      timeout: 3s
      retries: 5
      start_period: 10s

Sad web čeka da baza zaista odgovara. Ovo je razlika između sklopa koji se digne iz prve i onog koji zahteva da ga „pokreneš još jednom".

Komande

$ docker compose up -d              # pokreni u pozadini
$ docker compose up                 # pokreni i prati logove
$ docker compose down               # zaustavi i ukloni
$ docker compose down -v            # i obriši volumene (podaci nestaju!)

$ docker compose ps                 # šta radi
$ docker compose logs -f            # logovi svih servisa
$ docker compose logs -f web        # logovi jednog servisa

$ docker compose restart web
$ docker compose stop
$ docker compose start

$ docker compose build              # sagradi image-e
$ docker compose up -d --build      # sagradi pa pokreni
$ docker compose pull               # povuci novije verzije

Ulazak u kontejner:

$ docker compose exec web /bin/bash
$ docker compose exec baza psql -U app appdb

Jednokratna komanda u novom kontejneru:

$ docker compose run --rm web python manage.py migrate

Razlika je bitna: exec ulazi u kontejner koji već radi, run pravi novi. Za migracije baze hoćeš run --rm.

Pazi na down -v — briše imenovane volumene, dakle i sadržaj baze. Bez -v podaci preživljavaju.

Skaliranje

$ docker compose up -d --scale web=3

Radi samo ako servis nema fiksno objavljen port — tri kontejnera ne mogu da dele port 8080 na hostu. Otuda i potreba za nečim ispred, poput HAProxy-ja ili nginx-a kao reverse proxy-ja.

Mreže

Compose sam pravi jednu mrežu i sve servise stavlja u nju. Ako hoćeš da odvojiš bazu od spoljnog sloja:

services:
  proxy:
    image: nginx:1.25-alpine
    networks:
      - spolja

  web:
    build: .
    networks:
      - spolja
      - unutra

  baza:
    image: postgres:16-alpine
    networks:
      - unutra

networks:
  spolja:
  unutra:
    internal: true

Oznaka internal: true znači da ta mreža nema izlaz na internet. Baza je dostupna aplikaciji, ali ne i spolja — niti sama može napolje.

Razvoj i produkcija u istom projektu

Compose spaja više fajlova, pri čemu kasniji dopunjuje raniji:

compose.yaml:

services:
  web:
    build: .
    environment:
      APP_MODE: production

compose.override.yaml:

services:
  web:
    volumes:
      - .:/app
    environment:
      APP_MODE: development
    ports:
      - "8000:8000"

Fajl sa override u imenu Compose učitava automatski, pa lokalno dobijaš razvojne postavke. Na serveru ga jednostavno nema, ili navedeš fajlove izričito:

$ docker compose -f compose.yaml -f compose.prod.yaml up -d

Kompletan primer

Aplikacija, baza, keš i reverse proxy:

services:
  proxy:
    image: nginx:1.25-alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
    depends_on:
      - web
    restart: unless-stopped

  web:
    build: .
    env_file:
      - .env
    depends_on:
      baza:
        condition: service_healthy
      redis:
        condition: service_started
    restart: unless-stopped

  baza:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?nedostaje lozinka}
      POSTGRES_DB: appdb
    volumes:
      - podaci:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app"]
      interval: 5s
      timeout: 3s
      retries: 5
    restart: unless-stopped

  redis:
    image: redis:7-alpine
    command: redis-server --save 60 1
    volumes:
      - keš:/data
    restart: unless-stopped

volumes:
  podaci:
  keš:

Nijedan servis osim proxy ne objavljuje port. Aplikacija, baza i Redis dostupni su samo unutar mreže — spolja se ulazi isključivo kroz nginx.

Backup baze iz Compose sklopa

Spoj sa onim iz bash serije:

#!/usr/bin/env bash
set -euo pipefail

cd /opt/moja-app

datum=$(date +%Y%m%d_%H%M)
arhiva="/backup/appdb_${datum}.sql.gz"

docker compose exec -T baza \
    pg_dump -U app appdb | gzip > "$arhiva"

echo "Napravljeno: $arhiva"

Zastavica -T isključuje dodelu terminala — bez nje komanda pokrenuta iz cron-a ne radi.

Compose ili Kubernetes

Compose je za jedan server. Sve dok ti aplikacija staje na jednu mašinu, on je jednostavniji, brži i sasvim dovoljan — i mnogo manjih produkcija godinama radi upravo tako.

Kubernetes rešava ono što Compose ne pokušava: raspoređivanje po više čvorova, automatsko skaliranje, oporavak od pada mašine. Cena je znatno veća složenost. O tome u Kubernetes lekcijama.

Vežba

Sklopi Compose fajl sa:

  • WordPress-om na portu 8080
  • MySQL bazom koja ne objavljuje port
  • imenovanim volumenima za bazu i za wp-content
  • lozinkama u .env, ne u compose fajlu
  • healthcheck-om na bazi i uslovnim depends_on-om

Zatim ga sruši sa docker compose down (bez -v), pa ponovo digni — sadržaj mora ostati.


Povezano: Dockerfile i docker build · docker volume · docker network · DevOps

Comments

Popular posts from this blog

Konverzija tipova podataka u Pythonu

Početak u Linuxu — šta je i zašto se uči

groupadd