core code helper

Docker Compose: застосунок із кількох контейнерів одним файлом

compose.yaml від простого до робочого: сервіси, build, змінні й .env, depends_on з healthcheck, томи й мережі, профілі, override-файли, watch, секрети, команди на кожен день і деплой на сервер.

0 імпортували Імпортувати собі

Навіщо Compose

Застосунок рідко складається з одного контейнера: бекенд, база, кеш, черга, проксі. Запускати кожен через docker run з десятком прапорців незручно і ненадійно. Compose описує все в одному файлі compose.yaml і керує ним однією командою.

Схема: compose.yaml описує сервіси web, db і redis з томом; docker compose up -d створює мережу, три контейнери і том

Мінімальний приклад

# compose.yaml
services:
  web:
    image: nginx:stable-alpine
    ports:
      - "8080:80"
    volumes:
      - ./site:/usr/share/nginx/html:ro
docker compose up -d     # створити й запустити
docker compose ps
docker compose down      # зупинити й прибрати контейнери та мережу
Без version і з пробілом

Рядок version: "3.8" на початку файлу застарів і ігнорується (Compose лише попереджає the attribute version is obsolete). Команда — docker compose (плагін), а не docker-compose (стара програма на Python, більше не підтримується). Файл бажано називати compose.yaml; docker-compose.yml теж працює.

Робочий приклад: застосунок + Postgres + Redis

# compose.yaml
name: notes

services:
  web:
    build:
      context: .
      args:
        APP_VERSION: ${APP_VERSION:-dev}
    image: ghcr.io/olena/notes-app:${APP_VERSION:-dev}
    ports:
      - "127.0.0.1:8080:3000"
    environment:
      DATABASE_URL: postgres://notes:${POSTGRES_PASSWORD:?Задайте POSTGRES_PASSWORD}@db:5432/notes
      REDIS_URL: redis://redis:6379
      TZ: Europe/Kyiv
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:3000/healthz"]
      interval: 30s
      timeout: 3s
      retries: 3
      start_period: 20s
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: "1.0"

  db:
    image: postgres:17-alpine
    environment:
      POSTGRES_USER: notes
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: notes
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U notes -d notes"]
      interval: 5s
      timeout: 3s
      retries: 10
    restart: unless-stopped

  redis:
    image: redis:7-alpine
    command: ["redis-server", "--appendonly", "yes"]
    volumes:
      - redisdata:/data
    restart: unless-stopped

volumes:
  pgdata:
  redisdata:
# .env — поряд з compose.yaml, у .gitignore!
POSTGRES_PASSWORD=change-me-please
APP_VERSION=1.4.0

Що тут відбувається:

  • name: notes — ім'я проєкту. Від нього залежать імена: контейнери notes-web-1, мережа notes_default, томи notes_pgdata. Без name береться назва папки.
  • Сервіси бачать одне одного за іменами (db, redis) — Compose створює спільну мережу з DNS.
  • БД без ports — недоступна ззовні, лише для web. Порт web опубліковано на 127.0.0.1 — назовні його віддає reverse proxy.
  • ${POSTGRES_PASSWORD:?…} — якщо змінна не задана, Compose зупиниться з цим повідомленням, а не запустить базу з порожнім паролем.

Перевикористання

  backend:
    build: ./backend
    image: server-monitor-backend:latest
    # ...решта без змін

  collector:
    image: server-monitor-backend:latest
    pull_policy: never
    command: ["python", "./collector.py"]
    # ...environment, depends_on, networks без змін, рядок build прибираєш
  • Коли в сервісі є і build, і image, compose збирає образ і дає йому назву з image. Без image назва генерується сама (<проєкт>-<сервіс>), через це й з'явилися два образи.
  • У collector немає build, тож він просто запускає образ, який уже існує під цією назвою.
  • pull_policy: never каже compose, що цей образ є тільки локально. Без цього рядка compose спершу спробує стягнути server-monitor-backend з Docker Hub і покаже помилку pull access denied. Вона не зупиняє запуск, але плутає.

Змінні середовища

Дві різні речі з схожими назвами:

Механізм Для чого Де діє
файл .env поряд з compose підстановка ${VAR} у сам compose.yaml лише при розборі файлу
environment: змінні всередині контейнера у процесі застосунку
env_file: змінні всередині контейнера з файлу у процесі застосунку

Синтаксис підстановки:

Запис Значення
${VAR} значення або порожньо (з попередженням)
${VAR:-default} default, якщо не задано або порожнє
${VAR:?повідомлення} помилка, якщо не задано

Подивитися, як Compose розгорнув файл з усіма змінними:

docker compose config

depends_on і готовність

depends_on без умов лише задає порядок старту: web стартує після того, як запустився контейнер db. Але Postgres після запуску ще кілька секунд ініціалізується — і застосунок падає з connection refused.

З condition: service_healthy Compose чекає, поки healthcheck бази стане healthy:

depends_on:
  db:
    condition: service_healthy
    restart: true     # перезапустити web, якщо db перезапустили
Умова Чекає
service_started контейнер запущено (типово)
service_healthy healthcheck пройшов
service_completed_successfully одноразовий сервіс завершився з кодом 0 (міграції)
Міграції окремим сервісом
migrate:
  image: ghcr.io/olena/notes-app:${APP_VERSION:-dev}
  command: ["npm", "run", "migrate"]
  depends_on:
    db: { condition: service_healthy }
web:
  depends_on:
    migrate: { condition: service_completed_successfully }

Команди на кожен день

Команда Що робить
docker compose up -d створити/оновити й запустити все у фоні
docker compose up -d --build перезібрати образи перед запуском
docker compose up -d web лише сервіс web (і його залежності)
docker compose ps стан сервісів
docker compose logs -f web логи сервісу наживо
docker compose logs --since 10m усі логи за 10 хвилин
docker compose exec web sh оболонка в працюючому сервісі
docker compose run --rm web npm test одноразовий контейнер сервісу
docker compose restart web перезапустити (без перестворення!)
docker compose stop / start зупинити / запустити, не видаляючи
docker compose down видалити контейнери й мережу (томи лишаються)
docker compose down -v разом із томами — дані зникнуть
docker compose pull завантажити свіжі образи
docker compose build --no-cache web перезібрати без кешу
docker compose config фінальний конфіг після підстановок
docker compose top процеси в усіх контейнерах
docker compose up -d --scale worker=3 три копії сервісу worker
restart не перечитує конфіг

Змінили environment чи ports у compose.yaml — docker compose restart нічого не застосує, бо лише перезапускає існуючий контейнер. Потрібно docker compose up -d: Compose сам помітить зміни й перестворить лише змінені сервіси.

Кілька середовищ: override-файли

Compose автоматично зливає compose.yaml і compose.override.yaml (якщо він є). Зручно: базовий файл — як у проді, override — для локальної розробки.

# compose.override.yaml — лише локально
services:
  web:
    build: .
    ports:
      - "3000:3000"
    volumes:
      - ./src:/app/src
    environment:
      LOG_LEVEL: debug
  db:
    ports:
      - "127.0.0.1:5432:5432"   # щоб підключитися з DBeaver

Явні файли для інших середовищ:

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

Цей сайт, до речі, так і запускається локально: docker compose -f docker-compose.yml -f docker-compose.local.yml up -d --build.

Профілі: опціональні сервіси

services:
  adminer:
    image: adminer
    ports: ["127.0.0.1:8081:8080"]
    profiles: [tools]
  mailpit:
    image: axllent/mailpit
    profiles: [tools]
docker compose up -d                    # без adminer і mailpit
docker compose --profile tools up -d    # з ними

Watch: автооновлення під час розробки

services:
  web:
    build: .
    develop:
      watch:
        - action: sync           # змінені файли копіюються в контейнер
          path: ./src
          target: /app/src
        - action: rebuild        # змінився package.json — перезібрати образ
          path: package.json
docker compose watch

Працює швидше й надійніше за bind mount на Windows і macOS.

Секрети

Пароль у environment видно в docker inspect. Compose вміє монтувати секрети файлами в /run/secrets/:

services:
  db:
    image: postgres:17-alpine
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password

secrets:
  db_password:
    file: ./secrets/db_password.txt     # не комітити!

Багато офіційних образів (postgres, mysql, mariadb) розуміють змінні з суфіксом _FILE.

Деплой на сервер з Compose

Найпростіший робочий процес без Kubernetes:

# на сервері, у папці з compose.yaml і .env
docker compose pull                     # нові образи з реєстру
docker compose up -d --remove-orphans   # перестворити змінені сервіси
docker image prune -f                   # прибрати старі шари

Відкат — повернути попередній APP_VERSION у .env і повторити up -d. Про збірку образів у CI і реєстр — у нотатці «Docker у продакшені». PaaS на кшталт Dokploy чи Coolify роблять те саме, плюс інтерфейс, домени й HTTPS.

Шпаргалка

Команда Що робить
docker compose up -d [--build] запустити / оновити
docker compose down [-v] прибрати [з томами]
docker compose ps / logs -f S стан / логи
docker compose exec S sh зайти в сервіс
docker compose run --rm S CMD одноразова команда
docker compose pull && docker compose up -d оновити з реєстру
docker compose config перевірити підстановки
docker compose -f a.yaml -f b.yaml up -d кілька файлів
docker compose --profile tools up -d з опціональними сервісами
docker compose watch автосинхронізація коду

Збережіть статтю собі

Увійдіть, щоб імпортувати її у власні нотатки, відмічати вивчене, ставити лайки й питати спільноту. Реєстрація безкоштовна: вхід через Google займає кілька секунд.

Увійти через Google Інші способи входу