Docker

Docker Compose — оркестрація контейнерів

Декларативне управління multi-container застосунками через docker-compose.yml

Docker Compose — оркестрація контейнерів

Проблема ручного управління контейнерами

Уявіть, що ви розробляєте веб-застосунок з типовою архітектурою: фронтенд на React, бекенд на .NET, база даних PostgreSQL, Redis для кешування, Nginx як reverse proxy. У попередній статті ми навчилися запускати кожен компонент як окремий контейнер та з'єднувати їх через Docker networks.

Але виникає проблема: Щоб запустити весь застосунок, вам потрібно виконати десятки команд у правильній послідовності:

Ручний запуск контейнерів (bash)
$ # 1. Створити мережі
$ docker network create frontend-network
$ docker network create backend-network
$ # 2. Створити volumes
$ docker volume create postgres-data
$ docker volume create redis-data
$ # 3. Запустити PostgreSQL
$ docker run -d --name db --network backend-network -e POSTGRES_PASSWORD=mysecret -e POSTGRES_DB=myapp -v postgres-data:/var/lib/postgresql/data postgres:16
$ # 4. Запустити Redis
$ docker run -d --name cache --network backend-network -v redis-data:/data redis:7-alpine
$ # 5. Запустити Backend
$ docker run -d --name backend --network frontend-network --network backend-network -e DATABASE_URL=postgres://postgres:mysecret@db:5432/myapp -e REDIS_URL=redis://cache:6379 myapp-api:latest
$ # 6. Запустити Frontend
$ docker run -d --name frontend --network frontend-network -p 3000:3000 myapp-frontend:latest
$ # 7. Запустити Nginx
$ docker run -d --name nginx --network frontend-network -p 80:80 -v ./nginx.conf:/etc/nginx/nginx.conf:ro nginx:alpine

Це лише запуск. Тепер уявіть, що вам потрібно:

  • Зупинити весь застосунок — 5 команд docker stop
  • Видалити контейнери — 5 команд docker rm
  • Переглянути логи всіх сервісів — 5 команд docker logs
  • Оновити один сервіс — зупинити, видалити, пересобрати образ, запустити знову
  • Передати колезі — надіслати 50 рядків bash-команд і сподіватися, що він не зробить помилку

Це неефективно, схильне до помилок та нескалабельно. Ви забудете створити volume, підключите контейнер не до тієї мережі, або запустите сервіси у неправильному порядку (backend до того, як база даних готова).

Рішення: Docker Compose — інструмент, що дозволяє описати всю архітектуру застосунку у одному YAML-файлі та керувати нею однією командою. Замість 50 рядків bash-скриптів — 50 рядків декларативної конфігурації. Замість docker run && docker run && ... — просто docker compose up.

У цій статті ми детально розглянемо Docker Compose: від базового синтаксису docker-compose.yml до просунутих сценаріїв з profiles, extends, та multi-stage deployments. Ви навчитеся організовувати development та production environments, керувати залежностями між сервісами, та автоматизувати lifecycle застосунку.

Ця стаття передбачає розуміння Docker basics (контейнери, образи, volumes, networks) з попередніх статей. Тут ми зосередимося на оркестрації через Compose.

Що таке Docker Compose

Визначення та призначення

Docker Compose — це інструмент для визначення та запуску multi-container Docker-застосунків. Ви описуєте архітектуру застосунку у файлі docker-compose.yml (YAML-формат), а Compose автоматично створює мережі, volumes, запускає контейнери у правильному порядку та керує їхнім життєвим циклом.

Ключові концепції:

  1. Декларативність — ви описуєте що має бути (desired state), а не як це зробити (imperative commands)
  2. Ідемпотентністьdocker compose up можна запускати кілька разів — Compose створить лише те, чого немає
  3. Одна командаdocker compose up для запуску, docker compose down для зупинки та очищення
  4. Портабельністьdocker-compose.yml можна передати колезі, і він отримає ідентичне середовище

Compose vs Kubernetes

Питання: Чи не є Docker Compose "іграшковою" версією Kubernetes?

Відповідь: Ні. Compose та Kubernetes вирішують різні завдання:

АспектDocker ComposeKubernetes
ПризначенняЛокальна розробка, single-host deploymentProduction orchestration, multi-host clusters
СкладністьПростий YAML, 50-100 рядківСкладні manifests, 500+ рядків
МасштабуванняОбмежене (один хост)Автоматичне (кілька хостів, auto-scaling)
High AvailabilityНемає (якщо хост падає — все падає)Так (self-healing, replication)
Навчання1-2 дні2-4 тижні
Use CaseDevelopment, testing, small productionLarge-scale production, microservices

Коли використовувати Compose:

  • Локальна розробка (замість ручного запуску контейнерів)
  • CI/CD pipelines (integration tests з реальними сервісами)
  • Малі production deployments (1-2 сервери, до 10-20 контейнерів)
  • Прототипування перед переходом на Kubernetes

Коли використовувати Kubernetes:

  • Production з високими вимогами до availability (99.9%+)
  • Масштабування на десятки/сотні серверів
  • Складні microservices-архітектури (50+ сервісів)
  • Потреба в auto-scaling, rolling updates, service mesh
Best Practice: Використовуйте Compose для розробки, навіть якщо production буде на Kubernetes. Compose простіший для локального тестування, а перехід на Kubernetes можна зробити пізніше через інструменти типу Kompose (конвертує docker-compose.yml у Kubernetes manifests).

Встановлення Docker Compose

Docker Compose V2 (сучасна версія) вбудований у Docker Desktop та Docker Engine 20.10+. Команда: docker compose (без дефісу).

Перевірити версію:

docker compose version
# Вивід: Docker Compose version v2.24.5

Якщо Compose не встановлено (старі версії Docker):

# Linux
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose

# Перевірка
docker-compose --version
Compose V1 vs V2: Стара версія використовувала команду docker-compose (з дефісом) та була окремим Python-додатком. Нова версія (docker compose без дефісу) написана на Go, інтегрована у Docker CLI та швидша. У цій статті ми використовуємо V2.

Перший docker-compose.yml

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

Почнемо з найпростішого застосунку: один контейнер з Nginx.

Створити файл docker-compose.yml:

services:
  web:
    image: nginx:alpine
    ports:
      - "8080:80"

Запустити:

docker compose up

Вивід:

docker compose up
$ docker compose up
[+] Running 2/2
✔ Network myapp_default Created
✔ Container myapp-web-1 Started
web-1 | /docker-entrypoint.sh: Configuration complete; ready for start up

Що відбулося:

  1. Compose створив default network з назвою myapp_default (де myapp — назва директорії)
  2. Compose запустив контейнер з іменем myapp-web-1 (де web — назва сервісу з YAML)
  3. Порт 80 контейнера пробросено на 8080 хоста
  4. Логи контейнера виводяться у термінал

Перевірка:

# У новому терміналі
curl http://localhost:8080
# Вивід: <!DOCTYPE html><html>...

# Переглянути запущені контейнери
docker compose ps

Зупинити (Ctrl+C у терміналі з логами):

# Або у новому терміналі
docker compose down

Вивід:

docker compose down
$ docker compose down
[+] Running 2/2
✔ Container myapp-web-1 Removed
✔ Network myapp_default Removed

Compose видалив контейнер та мережу. Все чисто.

Анатомія docker-compose.yml

Розберемо структуру файлу:

version: '3.8'# Версія синтаксису Compose була обов'язковою у V1, але у V2 ігнорується.
# Ви можете безпечно видаляти цей рядок з усіх ваших файлів.

services:       # Список сервісів (контейнерів)
  web:          # Назва сервісу (довільна, використовується як DNS-ім'я)
    image: nginx:alpine  # Образ для контейнера
    ports:      # Проброс портів
      - "8080:80"  # HOST:CONTAINER

Ключові секції верхнього рівня:

services
object
Список сервісів (контейнерів). Кожен сервіс — це один або кілька контейнерів з однаковою конфігурацією. Це єдина обов'язкова секція у файлі.
networks
object
Визначення кастомних мереж. Якщо не вказано, Compose автоматично створює default bridge network та підключає туди всі сервіси.
volumes
object
Визначення named volumes для persistent storage. Зручно для баз даних, щоб дані не зникали при перезапуску контейнерів.

Назви контейнерів:

Compose автоматично генерує імена контейнерів за шаблоном: <project>-<service>-<replica>.

  • <project> — назва директорії (або вказана через -p)
  • <service> — назва сервісу з YAML
  • <replica> — номер репліки (1, 2, 3...) для масштабування

Приклад: myapp-web-1, myapp-db-1.


Multi-container застосунок

Тепер створимо реальний застосунок з кількома сервісами: веб-сервер та база даних.

Приклад: WordPress + MySQL

docker-compose.yml:

services:
  db:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: rootpassword
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wpuser
      MYSQL_PASSWORD: wppassword
    volumes:
      - db-data:/var/lib/mysql
    networks:
      - backend

  wordpress:
    image: wordpress:latest
    ports:
      - "8080:80"
    environment:
      WORDPRESS_DB_HOST: db:3306
      WORDPRESS_DB_USER: wpuser
      WORDPRESS_DB_PASSWORD: wppassword
      WORDPRESS_DB_NAME: wordpress
    volumes:
      - wp-data:/var/www/html
    networks:
      - backend
      - frontend
    depends_on:
      - db

volumes:
  db-data:
  wp-data:

networks:
  frontend:
  backend:

Пояснення конфігурації:

1. Сервіс db (MySQL):

db:
  image: mysql:8.0
  environment:
    MYSQL_ROOT_PASSWORD: rootpassword
    MYSQL_DATABASE: wordpress
  • image — використовувати офіційний образ MySQL версії 8.0
  • environment — змінні середовища для конфігурації MySQL (аналог -e у docker run)
  • volumes — монтувати named volume db-data до /var/lib/mysql (де MySQL зберігає дані)
  • networks — підключити до мережі backend

2. Сервіс wordpress:

wordpress:
  image: wordpress:latest
  ports:
    - "8080:80"
  environment:
    WORDPRESS_DB_HOST: db:3306
  • ports — пробросити порт 80 контейнера на 8080 хоста
  • WORDPRESS_DB_HOST: db:3306 — WordPress підключається до MySQL за іменем сервісу db (Docker DNS резолвить це в IP)
  • depends_on — запустити wordpress лише після запуску db
  • networks — підключити до backend (для доступу до db) та frontend (для ізоляції)

3. Volumes:

volumes:
  db-data:
  wp-data:

Compose створить named volumes myapp_db-data та myapp_wp-data (з префіксом назви проєкту).

4. Networks:

networks:
  frontend:
  backend:

Compose створить дві bridge-мережі: myapp_frontend та myapp_backend.

Запуск застосунку

# Запустити у фоновому режимі (-d = detached)
docker compose up -d

Вивід:

docker compose up -d
$ docker compose up -d
[+] Running 5/5
✔ Network myapp_frontend Created
✔ Network myapp_backend Created
✔ Volume myapp_db-data Created
✔ Volume myapp_wp-data Created
✔ Container myapp-db-1 Started
✔ Container myapp-wordpress-1 Started

Перевірка:

# Переглянути запущені сервіси
docker compose ps

# Переглянути логи всіх сервісів
docker compose logs

# Переглянути логи конкретного сервісу
docker compose logs wordpress

# Слідкувати за логами в реальному часі
docker compose logs -f

Відкрити у браузері:

http://localhost:8080

Ви побачите інсталятор WordPress. Compose автоматично створив базу даних, підключив WordPress до неї, і все працює.

Приклад: Fastify + MongoDB + Mongo Express

Цей стек є типовим представником документно-орієнтованої архітектури у Node.js-екосистемі. Fastify обраний замість Express не випадково: він суттєво швидший завдяки схемній валідації через JSON Schema та оптимізованому маршрутизатору, а його плагінна система забезпечує строгу інкапсуляцію залежностей. MongoDB як документна база даних природньо поєднується з JavaScript — дані зберігаються у форматі BSON, який є розширенням JSON, що усуває необхідність у маппінгу між об'єктами та рядками реляційної таблиці.

Mongo Express виконує роль адміністративного вебінтерфейсу, аналогічного до phpMyAdmin для MySQL. Принципово важливо, що цей сервіс підключається лише через профіль debug — він не повинен потрапляти у production-середовище.

Починаючи з Docker Compose V2 (вбудованого у Docker Engine 20.10+), поле version у docker-compose.yml є застарілим і ігнорується. Специфікація формату файлу більше не прив'язана до версії — Compose автоматично розпізнає всі підтримувані директиви. Видалення цього поля є рекомендованою практикою для нових проєктів.

Структура проєкту:

fastify-app/
├── docker-compose.yml
└── app/
    ├── package.json
    └── index.js

docker-compose.yml:

services:
  api:
    image: node:20-alpine
    working_dir: /app
    volumes:
      - ./app:/app
      - /app/node_modules    command: sh -c "npm install && node index.js"
    ports:
      - "3000:3000"
    environment:
      MONGO_URL: mongodb://mongo:27017/mydb
      NODE_ENV: development
    networks:
      - backend
    depends_on:
      mongo:
        condition: service_healthy

  mongo:
    image: mongo:7
    volumes:
      - mongo-data:/data/db
    networks:
      - backend
    healthcheck:
      test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
      interval: 10s
      timeout: 5s
      retries: 5

  mongo-express:
    image: mongo-express:latest
    ports:
      - "8081:8081"
    environment:
      ME_CONFIG_MONGODB_SERVER: mongo
      ME_CONFIG_BASICAUTH_USERNAME: admin
      ME_CONFIG_BASICAUTH_PASSWORD: admin
    networks:
      - backend
    depends_on:
      mongo:
        condition: service_healthy
    profiles:
      - debug

volumes:
  mongo-data:

networks:
  backend:

Зверніть увагу на виділений рядок — анонімний volume /app/node_modules. Це класичний прийом у Node.js-розробці з Docker: bind mount ./app:/app перезапише всі файли директорії /app у контейнері вмістом хоста, включно з відсутньою папкою node_modules (якщо вона не встановлена локально або її вміст відрізняється). Анонімний volume, оголошений після bind mount, «виграє» у пріоритеті для конкретного шляху /app/node_modules — Docker зберігає там пакети, встановлені під час збірки контейнера, і ніколи не перезаписує їх з хоста.

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

MONGO_URL
string
Рядок підключення до MongoDB. Використовує назву сервісу mongo як hostname — Docker DNS автоматично резолвить його у внутрішню IP-адресу контейнера всередині мережі backend.
NODE_ENV
string
Режим виконання Node.js. Значення development вмикає детальне логування та вимикає деякі оптимізації продуктивності.

app/index.js:

import Fastify from 'fastify'
import { MongoClient } from 'mongodb'

const fastify = Fastify({ logger: true })
const client = new MongoClient(process.env.MONGO_URL)

await client.connect()
const db = client.db()

fastify.get('/', async () => {
  const count = await db.collection('visits').countDocuments()
  await db.collection('visits').insertOne({ at: new Date() })
  return { message: 'Hello from Fastify!', visits: count + 1 }
})

await fastify.listen({ port: 3000, host: '0.0.0.0' })
Прослуховування на 0.0.0.0 є обов'язковим у контейнері. За замовчуванням Fastify (як і більшість фреймворків) прив'язується до 127.0.0.1 — адреси loopback-інтерфейсу, яка недоступна ззовні контейнера. Якщо залишити localhost, застосунок буде недосяжним навіть після проброса порту.

app/package.json:

{
  "name": "fastify-mongo-demo",
  "type": "module",
  "dependencies": {
    "fastify": "^4.28.1",
    "mongodb": "^6.6.0"
  }
}

Запуск та перевірка:

Запустити стек

docker compose up -d

Compose послідовно: 1) створить мережу backend та volume mongo-data, 2) запустить mongo та дочекається успішного health check, 3) лише потім запустить api.

Перевірити відповідь API

curl http://localhost:3000
# {"message":"Hello from Fastify!","visits":1}

Кожен виклик збільшує лічильник — дані зберігаються у MongoDB між запитами.

Відкрити Mongo Express (опціонально)

docker compose --profile debug up -d mongo-express
open http://localhost:8081
# Логін: admin / admin
Hot-reload для розробки: Замініть node index.js на npx nodemon index.js у полі command — Fastify перезапускатиметься автоматично при кожній зміні файлів у ./app. Nodemon встановлювати окремо не потрібно — npx завантажить його тимчасово.

Приклад: ASP.NET Core + PostgreSQL + pgAdmin

Цей стек представляє канонічну архітектуру серверного застосунку у C#-екосистемі. ASP.NET Core 8 Minimal API надає лаконічний спосіб визначення HTTP-ендпоінтів без церемоніального шаблонного коду контролерів, тоді як Entity Framework Core виконує роль ORM-прошарку між доменними об'єктами та реляційною схемою PostgreSQL.

З точки зору оркестрації через Compose цей приклад демонструє важливий патерн: restart: on-failure у поєднанні з condition: service_healthy. Навіть коли health check PostgreSQL пройшов успішно, ASP.NET Core потребує певного часу для виконання міграцій EF Core та прогріву Dependency Injection-контейнера. Якщо з якоїсь причини стартова послідовність порушиться — Compose автоматично перезапустить сервіс api, не вимагаючи ручного втручання.

Структура проєкту:

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

ASPNETCORE_ENVIRONMENT
string
Визначає середовище виконання ASP.NET Core. У режимі Development активується детальне відображення помилок, Swagger UI та інші діагностичні інструменти. У Production ці функції вимкнені з міркувань безпеки.
ConnectionStrings__DefaultConnection
string
Рядок підключення до PostgreSQL у форматі Npgsql. Подвійне підкреслення __ є стандартним роздільником для ієрархічних конфігурацій ASP.NET Core через змінні середовища (замінює двокрапку : у appsettings.json). Значення підставляються зі змінних .env через синтаксис ${VAR:-default}.

Ключові архітектурні рішення:

Multi-stage Dockerfile

Образ sdk (близько 800 МБ) використовується лише на етапі компіляції. Фінальний образ базується на aspnet (близько 200 МБ) — він містить лише runtime, без компілятора та SDK-інструментів. Це зменшує attack surface та час завантаження образу.

Health Check + restart: on-failure

Поєднання двох механізмів стійкості: condition: service_healthy гарантує, що PostgreSQL прийнятий запити до старту API; restart: on-failure підхоплює рідкісні race condition, якщо PostgreSQL ще ініціалізує схему, коли API вже намагається виконати міграції.

pgAdmin через profiles

Адміністративний інтерфейс pgAdmin додається виключно через профіль debug. Це усуває як ризик безпеки (відкритий порт 5050 у production), так і зайве споживання ресурсів. У production-середовищі docker compose up -d не запустить pgAdmin взагалі.

Запуск та перевірка:

Підготувати конфігурацію

cp .env.example .env  # якщо є, або створити вручну

Заповніть .env реальними значеннями. Для локальної розробки можна використовувати дефолтні.

Запустити стек

docker compose up -d

Compose збере образ api з Dockerfile, запустить db з PostgreSQL, дочекається health check, а потім запустить api. EF Core автоматично створить таблицю Products при першому старті.

Переглянути логи міграцій

docker compose logs api

У виводі мають бути рядки EF Core: Applying migration '...' — підтвердження, що схема створена.

Протестувати API

curl http://localhost:5000/

curl -X POST http://localhost:5000/products \
  -H "Content-Type: application/json" \
  -d '{"id":0,"name":"Laptop","price":999.99}'

curl http://localhost:5000/products

Відкрити pgAdmin (опціонально)

docker compose --profile debug up -d pgadmin
open http://localhost:5050
# admin@admin.com / admin
EF Core міграції у production: Виклик db.Database.Migrate() у Program.cs зручний для development, але у production рекомендується виносити міграції в окремий ініціалізаційний крок або Kubernetes Job. Це дозволяє відкатити міграцію перед стартом нової версії застосунку.

Приклад: Next.js (Fullstack) + PostgreSQL + Redis

Next.js у режимі fullstack-фреймворку поєднує server-side rendering, статичну генерацію та API Routes в одному процесі. Це дозволяє уникнути окремого бекенд-сервісу для відносно простих застосунків, де фронтенд і API мають спільну кодову базу та типи. PostgreSQL забезпечує надійне реляційне зберігання через Prisma ORM, тоді як Redis відіграє роль кешу за патерном Cache-Aside (Lazy Loading): застосунок спочатку перевіряє кеш, і лише у разі промаху (cache miss) звертається до бази даних, зберігаючи результат у Redis для наступних запитів.

З точки зору Compose-конфігурації цей стек демонструє важливу відмінність у depends_on: для db використовується condition: service_healthy (PostgreSQL повинен бути готовим до прийняття підключень), тоді як для cachecondition: service_started (Redis стартує майже миттєво і не потребує складного health check). Ця асиметрія відображає реальну різницю у часі ініціалізації сервісів.

Архітектура запитів (Cache-Aside):

Loading diagram...
graph LR
    A["Браузер"] --> B["Next.js<br/>API Route"]
    B --> C{"Redis<br/>Cache"}
    C -- "HIT<br/>X-Cache: HIT" --> A
    C -- "MISS" --> D["PostgreSQL"]
    D --> E["Зберегти<br/>у Redis (60s)"]
    E -- "X-Cache: MISS" --> A
    style A fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style B fill:#64748b,stroke:#334155,color:#ffffff
    style C fill:#f59e0b,stroke:#b45309,color:#ffffff
    style D fill:#64748b,stroke:#334155,color:#ffffff
    style E fill:#64748b,stroke:#334155,color:#ffffff

Структура проєкту:

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

DATABASE_URL
string
Рядок підключення Prisma до PostgreSQL. Використовує назву сервісу db як hostname. Формат: postgresql://user:password@host:port/database.
REDIS_URL
string
Адреса Redis-сервера. cache — це назва сервісу у Compose, яка резолвиться Docker DNS. Порт 6379 є стандартним для Redis.
NEXTAUTH_SECRET
string
Секрет для підпису JWT-токенів NextAuth.js. У production обов'язково має бути замінений на криптографічно стійке значення (openssl rand -base64 32). Значення за замовчуванням слугує лише для локальної розробки.
NEXTAUTH_URL
string
Базова URL-адреса застосунку. NextAuth.js використовує її для формування redirect URI після автентифікації. У production замінюється на реальний домен.

Параметри Redis:

Команда redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru задає три важливі параметри:

  • --appendonly yes — вмикає AOF (Append-Only File) персистентність: кожна операція запису логується на диск, що дозволяє відновити кеш після перезапуску контейнера
  • --maxmemory 256mb — жорстке обмеження пам'яті, щоб Redis не з'їв увесь доступний RAM хоста
  • --maxmemory-policy allkeys-lru — при досягненні ліміту видаляти найменш нещодавно використані ключі (Least Recently Used). Найбільш підходяща політика для кешу

Запуск та перевірка:

# Запустити стек (Next.js у режимі hot-reload)
docker compose up -d

# Переглянути логи
docker compose logs -f web

# Перевірити Cache-Aside
curl -v http://localhost:3000/api/posts
# X-Cache: MISS  ← перший запит, читається з PostgreSQL

curl -v http://localhost:3000/api/posts
# X-Cache: HIT   ← з Redis кешу

# Відкрити Redis Insight для інспекції ключів
docker compose --profile debug up -d redis-insight
open http://localhost:5540
Анонімний volume /app/.next захищає директорію збірки Next.js аналогічно до node_modules. Без нього bind mount ./web:/app перезапише збірку з хоста (де її може не бути), і застосунок не запуститься. Docker зберігає скомпільований .next всередині контейнера та ніколи не синхронізує його з хостом.

Зупинка та очищення

Зупинка та очищення

Зупинка контейнерів
$ # Зупинити контейнери (volumes та мережі залишаються)
$ docker compose stop
$ # Запустити знову
$ docker compose start
$ # Зупинити та видалити контейнери + мережі (volumes залишаються)
$ docker compose down
$ # Видалити все, включно з volumes (УВАГА: дані будуть втрачені)
$ docker compose down -v
docker compose down -v видаляє volumes: Всі дані у базі даних та WordPress будуть втрачені. Використовуйте цю команду лише для повного очищення development-середовища.

Конфігурація сервісів: детальний розбір

Build: збірка образів через Compose

Замість використання готових образів (image), Compose може збирати образи з Dockerfile.

Структура проєкту:

myapp/
├── docker-compose.yml
├── backend/
│   ├── Dockerfile
│   └── src/
└── frontend/
    ├── Dockerfile
    └── src/

docker-compose.yml:

services:
  backend:
    build:
      context: ./backend
      dockerfile: Dockerfile
      args:
        - NODE_ENV=development
    ports:
      - "5000:5000"
    environment:
      - DATABASE_URL=postgres://db:5432/myapp

  frontend:
    build: ./frontend  # Скорочений синтаксис
    ports:
      - "3000:3000"
    environment:
      - REACT_APP_API_URL=http://localhost:5000

Пояснення build:

context
string
Шлях до директорії, яка буде використовуватися як build context (там, де знаходяться файли для збірки).
dockerfile
string
Альтернативна назва файлу інструкцій збірки (за замовчуванням Docker шукає Dockerfile у context).
args
array|object
Build arguments (аргументи збірки), які передаються у Dockerfile як директиви ARG. Вони існують лише на етапі збірки і не потрапляють у фінальний образ (на відміну від environment).

Збірка образів:

# Зібрати образи та запустити
docker compose up --build

# Лише зібрати образи (без запуску)
docker compose build

# Зібрати конкретний сервіс
docker compose build backend

Коли використовувати build:

  • Розробка власного застосунку (не готовий образ з Docker Hub)
  • Потреба у кастомізації базового образу
  • Multi-stage builds для оптимізації розміру

Коли використовувати image:

  • Використання готових сервісів (PostgreSQL, Redis, Nginx)
  • Production deployment (образи вже зібрані у CI/CD)
Комбінація build + image: Ви можете вказати обидва параметри — Compose зібере образ та присвоїть йому вказане ім'я:
backend:
  build: ./backend
  image: myapp-backend:latest
Це зручно для подальшого push до registry.

Environment: змінні середовища

Згідно з третім правилом всесвітньо визнаної методології The Twelve-Factor App, конфігурація застосунку (паролі, URL-адреси баз даних, API ключі) має зберігатися виключно у змінних середовища (Environment Variables).Хардкод конфігурації в коді або навіть жорстко зашиті значення у docker-compose.yml (без можливості перевизначення) — це антипатерн. Образ вашого застосунку має бути абсолютно ідентичним (portable) для Development, Staging та Production. Різниця між цими середовищами має полягати лише у переданих змінних середовища.

Compose підтримує кілька способів передачі змінних середовища у контейнери.

1. Inline у docker-compose.yml:

services:
  web:
    image: myapp
    environment:
      - NODE_ENV=production
      - API_KEY=secret123

2. Через .env файл (рекомендовано):

Створити .env у тій же директорії:

NODE_ENV=production
API_KEY=secret123
DATABASE_URL=postgres://db:5432/myapp

docker-compose.yml:

services:
  web:
    image: myapp
    environment:
      - NODE_ENV=${NODE_ENV}
      - API_KEY=${API_KEY}
      - DATABASE_URL

Compose автоматично підставить значення з .env.

3. Через окремий env-файл:

services:
  web:
    image: myapp
    env_file:
      - ./config/production.env
      - ./config/secrets.env

Пріоритет змінних:

  1. Змінні з environment у docker-compose.yml (найвищий)
  2. Змінні з env_file
  3. Змінні з .env файлу
  4. Змінні середовища хоста
Не комітьте .env з секретами: Додайте .env до .gitignore. Для команди створіть .env.example з placeholder-значеннями:
NODE_ENV=development
API_KEY=your_api_key_here
DATABASE_URL=postgres://db:5432/myapp

Volumes: монтування даних

Контейнер слід сприймати не як віртуальну машину з жорстким диском, а як тимчасовий процес. Будь-які файли, які контейнер створює чи змінює у своїй внутрішній файловій системі (наприклад, записи в базі даних, завантажені картинки), існують лише доти, доки існує сам контейнер. Якщо контейнер видалити (docker compose down), ці дані зникнуть назавжди.Метафора: Уявіть, що контейнер — це комп'ютер без власного диска, який працює лише в оперативній пам'яті. Volume — це "USB-флешка" або зовнішній диск, який ми підключаємо до цього комп'ютера. Навіть якщо комп'ютер вимкнути чи замінити на інший, дані на "флешці" залишаться неушкодженими.

Compose підтримує три типи volumes:

1. Named Volumes (керовані Docker):

services:
  db:
    image: postgres:16
    volumes:
      - db-data:/var/lib/postgresql/data

volumes:
  db-data:  # Compose створить volume

2. Bind Mounts (директорії хоста):

services:
  web:
    image: nginx
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro  # read-only
      - ./html:/usr/share/nginx/html

3. Anonymous Volumes:

services:
  app:
    image: myapp
    volumes:
      - /app/node_modules  # Анонімний volume

Синтаксис:

  • Named volume: volume-name:/path/in/container
  • Bind mount: ./host/path:/container/path
  • Read-only: додати :ro в кінці

Коли що використовувати:

Named Volume

Найкраще для: Persistent data (бази даних, завантажені файли користувачів). Docker повністю керує життєвим циклом, дані не залежать від ОС хоста. Приклад: postgres-data:/var/lib/postgresql/data

Bind Mount

Найкраще для: Розробки (hot-reload), підключення конфігураційних файлів. Монтує конкретну директорію хоста. Усі зміни миттєво синхронізуються в обидві сторони. Приклад: ./src:/app/src

Anonymous Volume

Найкраще для: Тимчасових даних, захисту директорій (наприклад node_modules) від перезапису через Bind Mount. Docker створює volume з випадковим хеш-іменем. Видаляється разом з контейнером (якщо запущено down -v). Приклад: /app/node_modules

Hot-reload для розробки:

services:
  backend:
    build: ./backend
    volumes:
      - ./backend/src:/app/src  # Зміни у коді миттєво у контейнері
      - /app/node_modules        # Не перезаписувати node_modules
    command: npm run dev

Тепер зміни у ./backend/src на хості миттєво відображаються у контейнері, і npm run dev перезапускає сервер.

Networks: мережева конфігурація

Новачки часто припускаються помилки, намагаючись підключити бекенд до бази даних за адресою localhost:5432. Важливо розуміти: кожен контейнер має свій власний localhost. Якщо бекенд звертається на localhost, він шукає базу даних всередині свого власного контейнера, де її, звісно, немає.Замість цього, Docker Compose автоматично створює для вашого проєкту ізольовану bridge-мережу і запускає внутрішній DNS-сервер. Цей DNS-сервер дозволяє контейнерам звертатися один до одного за іменами сервісів (наприклад, db, cache, api), які автоматично резолвляться у правильні внутрішні IP-адреси.

Створення кастомних мереж:

services:
  frontend:
    image: nginx
    networks:
      - frontend-net

  backend:
    image: myapp-api
    networks:
      - frontend-net
      - backend-net

  db:
    image: postgres:16
    networks:
      - backend-net

networks:
  frontend-net:
    driver: bridge
  backend-net:
    driver: bridge

Налаштування subnet та gateway:

networks:
  custom-net:
    driver: bridge
    ipam:
      config:
        - subnet: 172.28.0.0/16
          gateway: 172.28.0.1

Статична IP-адреса для контейнера:

services:
  web:
    image: nginx
    networks:
      custom-net:
        ipv4_address: 172.28.0.10

networks:
  custom-net:
    driver: bridge
    ipam:
      config:
        - subnet: 172.28.0.0/16

Підключення до існуючої мережі:

networks:
  existing-network:
    external: true
    name: my-pre-existing-network

Compose не створить цю мережу, а підключиться до вже існуючої.

Ports: проброс портів

Синтаксис:

services:
  web:
    image: nginx
    ports:
      - "8080:80"           # HOST:CONTAINER
      - "127.0.0.1:8443:443"  # Прив'язка до localhost
      - "3000-3005:3000-3005" # Діапазон портів
      - "9000"              # Випадковий порт хоста

Long syntax (детальний контроль):

services:
  web:
    image: nginx
    ports:
      - target: 80        # Порт контейнера
        published: 8080   # Порт хоста
        protocol: tcp
        mode: host

Expose (без проброса на хост):

services:
  backend:
    image: myapp-api
    expose:
      - "5000"  # Доступно лише всередині Docker-мережі

expose документує, які порти використовує сервіс, але не пробросує їх на хост.

Depends_on: залежності між сервісами

Базовий синтаксис:

services:
  web:
    image: nginx
    depends_on:
      - backend
      - db

  backend:
    image: myapp-api
    depends_on:
      - db

  db:
    image: postgres:16

Що робить depends_on:

  1. Порядок запуску: Compose запустить dbbackendweb
  2. Порядок зупинки: Compose зупинить webbackenddb

Що НЕ робить depends_on:

  • Не чекає готовності сервісу — Compose запустить backend одразу після запуску контейнера db, але PostgreSQL всередині може ще ініціалізуватися

Проблема: Гонки станів (Race Conditions) Бекенд спробує підключитися до бази даних за мілісекунди після старту контейнера. Але базі даних потрібні секунди на ініціалізацію (виділення пам'яті, створення файлів). Відбувається гонка: бекенд приходить швидше, ніж база готова приймати з'єднання, і падає з помилкою.

Сучасна архітектура мікросервісів базується на принципі Crash-Only Software. Це означає, що ваш бекенд має бути готовим до того, що база даних впаде в будь-який момент, або ще не буде готова при старті. Вирішувати цю проблему виключно засобами інфраструктури (наприклад, змушуючи Compose довго чекати) — це антипатерн. Ваш код має вміти самостійно відновлювати з'єднання.

Рішення 1: Retry logic у додатку

Додайте логіку повторних спроб підключення у код:

// C# приклад
var retries = 5;
while (retries > 0)
{
    try
    {
        await dbContext.Database.CanConnectAsync();
        break;
    }
    catch
    {
        retries--;
        await Task.Delay(2000);
    }
}

Рішення 2: Health checks (Compose V3.4+)

services:
  db:
    image: postgres:16
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5

  backend:
    image: myapp-api
    depends_on:
      db:
        condition: service_healthy

Тепер Compose почекає, поки db пройде health check, перед запуском backend.

Рішення 3: wait-for-it.sh (legacy)

Використовуйте скрипт wait-for-it.sh для очікування доступності порту:

backend:
  image: myapp-api
  command: ["./wait-for-it.sh", "db:5432", "--", "npm", "start"]
Best Practice: Використовуйте health checks для критичних залежностей (бази даних, черги повідомлень). Для некритичних — retry logic у коді.

Restart Policy: автоматичний перезапуск

services:
  web:
    image: nginx
    restart: always

  backend:
    image: myapp-api
    restart: on-failure

  worker:
    image: myapp-worker
    restart: unless-stopped

Опції:

no
string
(За замовчуванням) Не перезапускати контейнер за жодних обставин.
always
string
Завжди перезапускати контейнер, якщо він зупинився. Якщо контейнер було зупинено вручну (через docker compose stop), він все одно буде перезапущений після рестарту Docker daemon. Використовуйте для критичних сервісів (веб-сервер, API).
on-failure
string
Перезапускати лише якщо контейнер завершив роботу з помилкою (exit code != 0). Ідеально для worker-процесів або бекендів, що можуть тимчасово падати через нестачу пам'яті.
unless-stopped
string
Перезапускати завжди, окрім випадків, коли контейнер було зупинено вручну. Це найпопулярніший вибір для більшості сервісів, які мають працювати постійно.

Команди Docker Compose

Основні команди життєвого циклу

1. docker compose up — запустити застосунок

# Запустити у foreground (логи у терміналі)
docker compose up

# Запустити у background (detached mode)
docker compose up -d

# Пересобрати образи перед запуском
docker compose up --build

# Запустити лише конкретні сервіси
docker compose up backend db

2. docker compose down — зупинити та видалити

# Зупинити контейнери, видалити контейнери та мережі
docker compose down

# Також видалити volumes (УВАГА: втрата даних)
docker compose down -v

# Також видалити образи
docker compose down --rmi all

3. docker compose start/stop/restart — керування без видалення

# Зупинити контейнери (не видаляти)
docker compose stop

# Запустити зупинені контейнери
docker compose start

# Перезапустити контейнери
docker compose restart

# Перезапустити конкретний сервіс
docker compose restart backend

Моніторинг та діагностика

4. docker compose ps — список сервісів

docker compose ps

# Вивід:
# NAME                IMAGE           STATUS          PORTS
# myapp-backend-1     myapp-api       Up 2 minutes    0.0.0.0:5000->5000/tcp
# myapp-db-1          postgres:16     Up 2 minutes    5432/tcp

5. docker compose logs — перегляд логів

# Логи всіх сервісів
docker compose logs

# Логи конкретного сервісу
docker compose logs backend

# Слідкувати за логами в реальному часі
docker compose logs -f

# Останні 100 рядків
docker compose logs --tail=100

# Логи з timestamps
docker compose logs -t

6. docker compose exec — виконати команду у контейнері

# Відкрити shell у контейнері backend
docker compose exec backend sh

# Виконати команду без інтерактивного режиму
docker compose exec backend npm run migrate

# Виконати як root
docker compose exec -u root backend apt-get update

7. docker compose top — процеси у контейнерах

docker compose top

# Показує PID, USER, TIME, COMMAND для кожного сервісу

Управління образами та volumes

8. docker compose build — збірка образів

# Зібрати всі сервіси з build-секцією
docker compose build

# Зібрати конкретний сервіс
docker compose build backend

# Зібрати без кешу
docker compose build --no-cache

# Паралельна збірка
docker compose build --parallel

9. docker compose pull — завантажити образи

# Завантажити всі образи з registry
docker compose pull

# Завантажити конкретний сервіс
docker compose pull db

10. docker compose push — відправити образи до registry

# Push всіх образів з image-секцією
docker compose push

# Push конкретного сервісу
docker compose push backend

Масштабування

11. docker compose up --scale — масштабування сервісів

# Запустити 3 репліки backend
docker compose up -d --scale backend=3

# Масштабувати кілька сервісів
docker compose up -d --scale backend=3 --scale worker=5

Обмеження: Не можна масштабувати сервіси з пробросом портів (конфлікт портів). Рішення — використовувати load balancer або не вказувати конкретний порт хоста.

Приклад для масштабування:

services:
  backend:
    image: myapp-api
    # Не вказувати ports для масштабування
    expose:
      - "5000"

  nginx:
    image: nginx
    ports:
      - "80:80"
    # Nginx як load balancer для backend

Просунуті можливості Compose

Extends: повторне використання конфігурації

Проблема: У вас є спільна конфігурація для кількох сервісів (наприклад, logging, restart policy).

Рішення: Винесіть спільну конфігурацію у базовий файл та розширюйте її за допомогою директиви extends.

Обидва сервіси успадкують restart та logging з base-service.

Profiles: умовний запуск сервісів

Сценарій: У вас є основні сервіси (backend, db) та допоміжні (adminer для перегляду БД, mailhog для тестування email). Допоміжні потрібні не завжди.

Рішення: Використовуйте profiles.

docker-compose.yml:

services:
  backend:
    image: myapp-api
    # Немає profile — запускається завжди

  db:
    image: postgres:16
    # Немає profile — запускається завжди

  adminer:
    image: adminer
    ports:
      - "8080:8080"
    profiles:
      - debug

  mailhog:
    image: mailhog/mailhog
    ports:
      - "8025:8025"
    profiles:
      - debug

Запуск:

# Запустити лише основні сервіси (backend, db)
docker compose up -d

# Запустити з debug-профілем (backend, db, adminer, mailhog)
docker compose --profile debug up -d

# Запустити кілька профілів
docker compose --profile debug --profile monitoring up -d

Use cases:

  • debug — інструменти для розробки (Adminer, Mailhog, Redis Commander)
  • test — сервіси для integration tests
  • monitoring — Prometheus, Grafana

Override Files: різні середовища

Проблема: Конфігурація для development відрізняється від production (різні порти, volumes для hot-reload, debug-інструменти).

Рішення: Використовуйте override-файли.

Базова конфігурація (docker-compose.yml):

services:
  backend:
    image: myapp-api:latest
    environment:
      - NODE_ENV=production

  db:
    image: postgres:16
    environment:
      - POSTGRES_PASSWORD=changeme

Development override (docker-compose.override.yml):

services:
  backend:
    build: ./backend  # Збирати локально замість image
    volumes:
      - ./backend/src:/app/src  # Hot-reload
    environment:
      - NODE_ENV=development
    command: npm run dev

  db:
    ports:
      - "5432:5432"  # Проброс для доступу з хоста

Production override (docker-compose.prod.yml):

services:
  backend:
    restart: always
    environment:
      - NODE_ENV=production
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 512M

  db:
    restart: always
    volumes:
      - db-data:/var/lib/postgresql/data

volumes:
  db-data:

Використання:

# Development (автоматично використовує docker-compose.override.yml)
docker compose up -d

# Production (явно вказати файл)
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

# Кілька override-файлів (застосовуються послідовно)
docker compose -f docker-compose.yml -f docker-compose.override.yml -f docker-compose.local.yml up -d

Пріоритет:

  1. docker-compose.yml (базова конфігурація)
  2. docker-compose.override.yml (якщо існує, застосовується автоматично)
  3. Додаткові файли через -f (застосовуються у порядку вказання)
Naming convention:
  • docker-compose.yml — базова конфігурація (спільна для всіх середовищ)
  • docker-compose.override.yml — development (не комітити, якщо містить локальні налаштування)
  • docker-compose.prod.yml — production
  • docker-compose.test.yml — testing/CI

Secrets: управління чутливими даними

Проблема: Паролі та API-ключі у docker-compose.yml або .env — небезпечно для production.

Рішення: Docker Secrets (для Docker Swarm) або зовнішні secret managers.

Docker Secrets (Swarm mode):

services:
  db:
    image: postgres:16
    secrets:
      - db_password
    environment:
      - POSTGRES_PASSWORD_FILE=/run/secrets/db_password

secrets:
  db_password:
    file: ./secrets/db_password.txt

Файл ./secrets/db_password.txt:

my_super_secret_password

Docker монтує секрет як файл у /run/secrets/db_password всередині контейнера. PostgreSQL читає пароль з файлу.

Для non-Swarm (альтернатива):

Використовуйте зовнішні secret managers (HashiCorp Vault, AWS Secrets Manager) або змінні середовища з CI/CD.

services:
  backend:
    image: myapp-api
    environment:
      - DATABASE_PASSWORD=${DATABASE_PASSWORD}  # З CI/CD

У CI/CD (GitHub Actions, GitLab CI):

# .github/workflows/deploy.yml
env:
  DATABASE_PASSWORD: ${{ secrets.DATABASE_PASSWORD }}
Не комітьте секрети: Додайте secrets/ до .gitignore. Для команди створіть secrets.example/ з placeholder-файлами.

Реальний приклад: Full-stack застосунок

Цей приклад демонструє створення повноцінного production-ready застосунку. Замість того, щоб сприймати його як один великий файл, давайте розглянемо його архітектуру пошарово:

  1. Шар роутингу (Nginx): Приймає трафік з інтернету, віддає статику (Frontend) і проксіює API-запити на Backend.
  2. Шар бізнес-логіки (Backend & Frontend): .NET API та React застосунок.
  3. Шар даних (DB & Cache): PostgreSQL для стійкого зберігання та Redis для тимчасового кешування.

graph TD
    Client([Клієнт / Browser]) -->|HTTP :80| Nginx[Nginx Reverse Proxy]
    
    subgraph frontend-net [Frontend Network]
        Nginx -->|/| React[React Frontend]
        Nginx -->|/api| API[.NET Backend]
    end
    
    subgraph backend-net [Backend Network]
        API -->|TCP 5432| DB[(PostgreSQL)]
        API -->|TCP 6379| Cache[(Redis)]
    end
    
    classDef proxy fill:#f9f,stroke:#333,stroke-width:2px;
    class Nginx proxy;

Структура проєкту

docker-compose.yml (базова конфігурація)

services:
  # PostgreSQL Database
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: ${POSTGRES_DB:-myapp}
      POSTGRES_USER: ${POSTGRES_USER:-postgres}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-changeme}
    volumes:
      - db-data:/var/lib/postgresql/data
    networks:
      - backend-net
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5

  # Redis Cache
  cache:
    image: redis:7-alpine
    command: redis-server --appendonly yes
    volumes:
      - redis-data:/data
    networks:
      - backend-net
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5

  # .NET Backend API
  backend:
    build:
      context: ./backend
      dockerfile: Dockerfile
    image: myapp-backend:latest
    environment:
      - ASPNETCORE_ENVIRONMENT=${ASPNETCORE_ENVIRONMENT:-Production}
      - ConnectionStrings__DefaultConnection=Host=db;Database=${POSTGRES_DB:-myapp};Username=${POSTGRES_USER:-postgres};Password=${POSTGRES_PASSWORD:-changeme}
      - Redis__ConnectionString=cache:6379
    networks:
      - frontend-net
      - backend-net
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_healthy

  # React Frontend
  frontend:
    build:
      context: ./frontend
      dockerfile: Dockerfile
      args:
        - REACT_APP_API_URL=${REACT_APP_API_URL:-http://localhost/api}
    image: myapp-frontend:latest
    networks:
      - frontend-net
    depends_on:
      - backend

  # Nginx Reverse Proxy
  nginx:
    image: nginx:alpine
    ports:
      - "${NGINX_PORT:-80}:80"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
    networks:
      - frontend-net
    depends_on:
      - frontend
      - backend

networks:
  frontend-net:
    driver: bridge
  backend-net:
    driver: bridge

volumes:
  db-data:
  redis-data:

docker-compose.override.yml (development)

services:
  db:
    ports:
      - "5432:5432"  # Доступ до БД з хоста

  cache:
    ports:
      - "6379:6379"  # Доступ до Redis з хоста

  backend:
    build:
      context: ./backend
      target: development  # Multi-stage Dockerfile
    volumes:
      - ./backend:/app  # Hot-reload
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
    ports:
      - "5000:5000"  # Прямий доступ до API

  frontend:
    build:
      context: ./frontend
      target: development
    volumes:
      - ./frontend/src:/app/src  # Hot-reload
      - /app/node_modules
    environment:
      - REACT_APP_API_URL=http://localhost:5000
    ports:
      - "3000:3000"  # Прямий доступ до React dev server

  # Adminer для перегляду БД
  adminer:
    image: adminer
    ports:
      - "8080:8080"
    networks:
      - backend-net
    profiles:
      - debug

docker-compose.prod.yml (production)

services:
  db:
    restart: always
    deploy:
      resources:
        limits:
          cpus: '1'
          memory: 1G

  cache:
    restart: always
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 512M

  backend:
    restart: always
    environment:
      - ASPNETCORE_ENVIRONMENT=Production
    deploy:
      replicas: 2  # Масштабування
      resources:
        limits:
          cpus: '1'
          memory: 1G

  frontend:
    restart: always

  nginx:
    restart: always
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./ssl:/etc/nginx/ssl:ro  # SSL сертифікати

.env.example

# Database
POSTGRES_DB=myapp
POSTGRES_USER=postgres
POSTGRES_PASSWORD=changeme

# Backend
ASPNETCORE_ENVIRONMENT=Development

# Frontend
REACT_APP_API_URL=http://localhost/api

# Nginx
NGINX_PORT=80

nginx/nginx.conf

events {
    worker_connections 1024;
}

http {
    upstream backend {
        server backend:5000;
    }

    upstream frontend {
        server frontend:3000;
    }

    server {
        listen 80;
        server_name localhost;

        # Frontend
        location / {
            proxy_pass http://frontend;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
        }

        # Backend API
        location /api/ {
            proxy_pass http://backend/;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
        }

        # Health check
        location /health {
            access_log off;
            return 200 "OK\n";
            add_header Content-Type text/plain;
        }
    }
}

Використання

Development:

# Створити .env з .env.example
cp .env.example .env

# Запустити (автоматично використовує override)
docker compose up -d

# З debug-інструментами
docker compose --profile debug up -d

# Переглянути логи
docker compose logs -f backend

# Зупинити
docker compose down

Production:

# Встановити production-змінні у .env
nano .env

# Запустити з production-конфігурацією
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

# Перевірити статус
docker compose ps

# Масштабувати backend
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --scale backend=3

CI/CD (GitHub Actions приклад):

name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Build images
        run: docker compose -f docker-compose.yml -f docker-compose.prod.yml build
      
      - name: Push to registry
        run: |
          echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
          docker compose push
      
      - name: Deploy to server
        run: |
          ssh user@server "cd /app && docker compose pull && docker compose up -d"

Troubleshooting Docker Compose

Типові проблеми

Діагностичні команди

# Перевірити конфігурацію (merged YAML)
docker compose config

# Перевірити, які образи будуть використані
docker compose config --images

# Перевірити, які volumes будуть створені
docker compose config --volumes

# Валідація синтаксису
docker compose config --quiet

# Переглянути події
docker compose events

# Статистика ресурсів
docker stats $(docker compose ps -q)

Найкращі практики Docker Compose

1. Використовуйте .env для конфігурації

Погано:

services:
  db:
    environment:
      - POSTGRES_PASSWORD=hardcoded_password  # Небезпечно

Добре:

services:
  db:
    environment:
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
# .env
POSTGRES_PASSWORD=secure_password_from_env

2. Розділяйте конфігурацію за середовищами

Використовуйте override-файли для development/production замість одного великого файлу з умовами.

3. Використовуйте health checks

services:
  db:
    healthcheck:
      test: ["CMD-SHELL", "pg_isready"]
      interval: 10s
      timeout: 5s
      retries: 5

Це дозволяє depends_on чекати готовності сервісу.

4. Обмежуйте ресурси у production

services:
  backend:
    deploy:
      resources:
        limits:
          cpus: '1'
          memory: 1G
        reservations:
          cpus: '0.5'
          memory: 512M

5. Використовуйте named volumes для даних

Погано:

volumes:
  - /var/lib/postgresql/data  # Anonymous volume

Добре:

volumes:
  - db-data:/var/lib/postgresql/data

volumes:
  db-data:

6. Документуйте архітектуру

Додайте коментарі у docker-compose.yml та створіть README з інструкціями.

services:
  # Backend API - .NET 8 Web API
  # Доступний на http://localhost:5000 (development)
  # Підключається до PostgreSQL та Redis
  backend:
    build: ./backend
    # ...

7. Використовуйте .dockerignore

Створіть .dockerignore у кожній директорії з Dockerfile:

node_modules
.git
.env
*.log

Це прискорює збірку та зменшує розмір build context.

8. Версіонуйте образи

Погано:

services:
  db:
    image: postgres:latest  # Непередбачувано

Добре:

services:
  db:
    image: postgres:16.2-alpine  # Конкретна версія

Резюме

Docker Compose — це потужний інструмент для управління multi-container застосунками, що значно спрощує розробку та deployment.

Ключові концепції:

  1. Декларативність — описуєте архітектуру у YAML, Compose керує lifecycle
  2. docker-compose.yml — єдине джерело правди для всієї архітектури застосунку
  3. Services — логічні компоненти застосунку (backend, db, cache), кожен може мати кілька реплік
  4. Networks — ізоляція та комунікація між сервісами через Docker DNS
  5. Volumes — persistent storage для даних, що мають пережити перезапуск контейнерів

Основні команди:

  • docker compose up -d — запустити застосунок у background
  • docker compose down — зупинити та видалити контейнери
  • docker compose logs -f — переглянути логи в реальному часі
  • docker compose ps — статус сервісів
  • docker compose exec <service> <command> — виконати команду у контейнері

Просунуті можливості:

  • Override files — різні конфігурації для development/production
  • Profiles — умовний запуск сервісів (debug-інструменти, monitoring)
  • Health checks — чекати готовності залежних сервісів
  • Extends — повторне використання конфігурації
  • Secrets — безпечне управління чутливими даними

Найкращі практики:

  • Використовуйте .env для конфігурації, не хардкодьте значення
  • Розділяйте конфігурацію за середовищами через override-файли
  • Додавайте health checks для критичних сервісів
  • Обмежуйте ресурси у production через deploy.resources
  • Версіонуйте образи (не використовуйте latest)
  • Документуйте архітектуру у README та коментарях

Що далі:

У наступній статті ми розглянемо Docker у production — best practices для deployment, моніторинг, логування, security hardening, та інтеграцію з CI/CD pipelines. Ви навчитеся готувати Docker-застосунки до реального production-середовища з високими вимогами до надійності та безпеки.


Практичні завдання

Рівень 1: Базове розуміння

Завдання 1.1: Перший docker-compose.yml

Створіть docker-compose.yml для простого веб-сервера Nginx.

Вимоги:

  • Використати образ nginx:alpine
  • Пробросити порт 8080 хоста на 80 контейнера
  • Змонтувати локальну директорію ./html до /usr/share/nginx/html

Перевірка:

echo "<h1>Hello from Compose!</h1>" > html/index.html
docker compose up -d
curl http://localhost:8080

Завдання 1.2: Multi-container з базою даних

Створіть застосунок з двома сервісами: WordPress та MySQL.

Вимоги:

  • MySQL з environment variables для налаштування
  • WordPress підключений до MySQL через Docker DNS
  • Named volumes для persistent storage
  • WordPress доступний на http://localhost:8080

Підказка: Використайте приклад з розділу "Перший docker-compose.yml".

Завдання 1.3: Логи та діагностика

Запустіть застосунок з попереднього завдання та:

  • Переглянути логи MySQL
  • Виконати SQL-запит всередині контейнера MySQL
  • Перезапустити лише WordPress (без MySQL)
docker compose logs db
docker compose exec db mysql -u root -p
docker compose restart wordpress

Рівень 2: Практичне застосування

Завдання 2.1: Development environment з hot-reload

Створіть development-середовище для Node.js застосунку з hot-reload.

Структура:

myapp/
├── docker-compose.yml
├── app/
│   ├── package.json
│   └── index.js

Вимоги:

  • Використати образ node:20-alpine
  • Змонтувати ./app до /app у контейнері
  • Запустити npm run dev (з nodemon для hot-reload)
  • Порт 3000 доступний на хості

index.js:

const express = require('express');
const app = express();

app.get('/', (req, res) => {
  res.send('Hello from Docker Compose!');
});

app.listen(3000, '0.0.0.0', () => {
  console.log('Server running on port 3000');
});

Перевірка: Змініть текст у index.js — сервер має автоматично перезапуститися.

Завдання 2.2: Мережева ізоляція

Створіть застосунок з трьома сервісами та двома мережами:

  • frontend-net: nginx, backend
  • backend-net: backend, postgres

Вимоги:

  • Nginx не може підключитися до postgres
  • Backend може підключитися до обох
  • Використати health checks для postgres

Перевірка:

docker compose exec nginx ping postgres
# Має бути: bad address 'postgres'

docker compose exec backend ping postgres
# Має працювати

Завдання 2.3: Override files

Створіть базову конфігурацію та два override-файли: development та production.

Вимоги:

  • docker-compose.yml — базова конфігурація (образи, мережі)
  • docker-compose.override.yml — development (bind mounts, проброс портів БД)
  • docker-compose.prod.yml — production (restart policies, resource limits)

Перевірка:

# Development
docker compose up -d

# Production
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

Рівень 3: Архітектура та оптимізація

Завдання 3.1: Full-stack застосунок

Створіть повноцінний full-stack застосунок з усіма компонентами:

Компоненти:

  1. Frontend (React або Vue)
  2. Backend (Node.js Express або .NET)
  3. Database (PostgreSQL)
  4. Cache (Redis)
  5. Reverse Proxy (Nginx)

Вимоги:

  • Три мережі: frontend-net, backend-net, cache-net
  • Health checks для всіх stateful сервісів
  • Named volumes для persistent data
  • Development override з hot-reload
  • Production override з resource limits та restart policies

Архітектура:

Internet → Nginx → Frontend (React)
                ↓
              Backend (API) → PostgreSQL
                ↓
              Redis (Cache)

Завдання 3.2: CI/CD інтеграція

Створіть GitHub Actions workflow для автоматичного deployment.

Вимоги:

  • Build образів через docker compose build
  • Push образів до Docker Hub
  • Deploy на сервер через SSH
  • Використання secrets для credentials

Завдання 3.3: Моніторинг та логування

Додайте до застосунку з завдання 3.1 моніторинг та централізоване логування.

Додаткові сервіси:

  • Prometheus (збір метрик)
  • Grafana (візуалізація)
  • Loki (логування)
  • Promtail (збір логів)

Вимоги:

  • Використати profiles для моніторингу (--profile monitoring)
  • Налаштувати Prometheus для scraping метрик з backend
  • Налаштувати Grafana dashboard
  • Централізувати логи всіх сервісів у Loki

Запуск:

docker compose --profile monitoring up -d

Підказка: Для завдань рівня 3 використовуйте приклад "Реальний приклад: Full-stack застосунок" як основу та розширюйте його.