Уявіть, що ви розробляєте веб-застосунок з типовою архітектурою: фронтенд на React, бекенд на .NET, база даних PostgreSQL, Redis для кешування, Nginx як reverse proxy. У попередній статті ми навчилися запускати кожен компонент як окремий контейнер та з'єднувати їх через Docker networks.
Але виникає проблема: Щоб запустити весь застосунок, вам потрібно виконати десятки команд у правильній послідовності:
Це лише запуск. Тепер уявіть, що вам потрібно:
docker stopdocker rmdocker logsЦе неефективно, схильне до помилок та нескалабельно. Ви забудете створити 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 Compose — це інструмент для визначення та запуску multi-container Docker-застосунків. Ви описуєте архітектуру застосунку у файлі docker-compose.yml (YAML-формат), а Compose автоматично створює мережі, volumes, запускає контейнери у правильному порядку та керує їхнім життєвим циклом.
Ключові концепції:
docker compose up можна запускати кілька разів — Compose створить лише те, чого немаєdocker compose up для запуску, docker compose down для зупинки та очищенняdocker-compose.yml можна передати колезі, і він отримає ідентичне середовищеПитання: Чи не є Docker Compose "іграшковою" версією Kubernetes?
Відповідь: Ні. Compose та Kubernetes вирішують різні завдання:
| Аспект | Docker Compose | Kubernetes |
|---|---|---|
| Призначення | Локальна розробка, single-host deployment | Production orchestration, multi-host clusters |
| Складність | Простий YAML, 50-100 рядків | Складні manifests, 500+ рядків |
| Масштабування | Обмежене (один хост) | Автоматичне (кілька хостів, auto-scaling) |
| High Availability | Немає (якщо хост падає — все падає) | Так (self-healing, replication) |
| Навчання | 1-2 дні | 2-4 тижні |
| Use Case | Development, testing, small production | Large-scale production, microservices |
Коли використовувати Compose:
Коли використовувати Kubernetes:
docker-compose.yml у Kubernetes manifests).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
docker-compose (з дефісом) та була окремим Python-додатком. Нова версія (docker compose без дефісу) написана на Go, інтегрована у Docker CLI та швидша. У цій статті ми використовуємо V2.Почнемо з найпростішого застосунку: один контейнер з Nginx.
Створити файл docker-compose.yml:
services:
web:
image: nginx:alpine
ports:
- "8080:80"
Запустити:
docker compose up
Вивід:
Що відбулося:
myapp_default (де myapp — назва директорії)myapp-web-1 (де web — назва сервісу з YAML)80 контейнера пробросено на 8080 хостаПеревірка:
# У новому терміналі
curl http://localhost:8080
# Вивід: <!DOCTYPE html><html>...
# Переглянути запущені контейнери
docker compose ps
Зупинити (Ctrl+C у терміналі з логами):
# Або у новому терміналі
docker compose down
Вивід:
Compose видалив контейнер та мережу. Все чисто.
Розберемо структуру файлу:
version: '3.8'# Версія синтаксису Compose була обов'язковою у V1, але у V2 ігнорується.
# Ви можете безпечно видаляти цей рядок з усіх ваших файлів.
services: # Список сервісів (контейнерів)
web: # Назва сервісу (довільна, використовується як DNS-ім'я)
image: nginx:alpine # Образ для контейнера
ports: # Проброс портів
- "8080:80" # HOST:CONTAINER
Ключові секції верхнього рівня:

Назви контейнерів:
Compose автоматично генерує імена контейнерів за шаблоном: <project>-<service>-<replica>.
<project> — назва директорії (або вказана через -p)<service> — назва сервісу з YAML<replica> — номер репліки (1, 2, 3...) для масштабуванняПриклад: myapp-web-1, myapp-db-1.
Тепер створимо реальний застосунок з кількома сервісами: веб-сервер та база даних.
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.0environment — змінні середовища для конфігурації MySQL (аналог -e у docker run)volumes — монтувати named volume db-data до /var/lib/mysql (де MySQL зберігає дані)networks — підключити до мережі backend2. Сервіс 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 лише після запуску dbnetworks — підключити до 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 ps
# Переглянути логи всіх сервісів
docker compose logs
# Переглянути логи конкретного сервісу
docker compose logs wordpress
# Слідкувати за логами в реальному часі
docker compose logs -f
Відкрити у браузері:
http://localhost:8080
Ви побачите інсталятор WordPress. Compose автоматично створив базу даних, підключив WordPress до неї, і все працює.
Цей стек є типовим представником документно-орієнтованої архітектури у Node.js-екосистемі. Fastify обраний замість Express не випадково: він суттєво швидший завдяки схемній валідації через JSON Schema та оптимізованому маршрутизатору, а його плагінна система забезпечує строгу інкапсуляцію залежностей. MongoDB як документна база даних природньо поєднується з JavaScript — дані зберігаються у форматі BSON, який є розширенням JSON, що усуває необхідність у маппінгу між об'єктами та рядками реляційної таблиці.
Mongo Express виконує роль адміністративного вебінтерфейсу, аналогічного до phpMyAdmin для MySQL. Принципово важливо, що цей сервіс підключається лише через профіль debug — він не повинен потрапляти у production-середовище.
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 як hostname — Docker DNS автоматично резолвить його у внутрішню IP-адресу контейнера всередині мережі backend.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.
curl http://localhost:3000
# {"message":"Hello from Fastify!","visits":1}
Кожен виклик збільшує лічильник — дані зберігаються у MongoDB між запитами.
docker compose --profile debug up -d mongo-express
open http://localhost:8081
# Логін: admin / admin
node index.js на npx nodemon index.js у полі command — Fastify перезапускатиметься автоматично при кожній зміні файлів у ./app. Nodemon встановлювати окремо не потрібно — npx завантажить його тимчасово.Цей стек представляє канонічну архітектуру серверного застосунку у 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:
Development активується детальне відображення помилок, Swagger UI та інші діагностичні інструменти. У Production ці функції вимкнені з міркувань безпеки.__ є стандартним роздільником для ієрархічних конфігурацій 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
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 '...' — підтвердження, що схема створена.
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
docker compose --profile debug up -d pgadmin
open http://localhost:5050
# admin@admin.com / admin
db.Database.Migrate() у Program.cs зручний для development, але у production рекомендується виносити міграції в окремий ініціалізаційний крок або Kubernetes Job. Це дозволяє відкатити міграцію перед стартом нової версії застосунку.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 повинен бути готовим до прийняття підключень), тоді як для cache — condition: service_started (Redis стартує майже миттєво і не потребує складного health check). Ця асиметрія відображає реальну різницю у часі ініціалізації сервісів.
Архітектура запитів (Cache-Aside):
Структура проєкту:
Змінні середовища сервісу web:
db як hostname. Формат: postgresql://user:password@host:port/database.cache — це назва сервісу у Compose, яка резолвиться Docker DNS. Порт 6379 є стандартним для Redis.openssl rand -base64 32). Значення за замовчуванням слугує лише для локальної розробки.Параметри 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
# Побудувати production-образ
docker compose -f docker-compose.yml -f docker-compose.prod.yml build
# Запустити (Next.js у standalone-режимі)
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
docker-compose.prod.yml має перевизначати target: production у build, щоб Compose використовував production-стадію Dockerfile з мінімальним образом.
/app/.next захищає директорію збірки Next.js аналогічно до node_modules. Без нього bind mount ./web:/app перезапише збірку з хоста (де її може не бути), і застосунок не запуститься. Docker зберігає скомпільований .next всередині контейнера та ніколи не синхронізує його з хостом.docker compose down -v видаляє volumes: Всі дані у базі даних та WordPress будуть втрачені. Використовуйте цю команду лише для повного очищення development-середовища.Замість використання готових образів (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:
Dockerfile у context).ARG. Вони існують лише на етапі збірки і не потрапляють у фінальний образ (на відміну від environment).Збірка образів:
# Зібрати образи та запустити
docker compose up --build
# Лише зібрати образи (без запуску)
docker compose build
# Зібрати конкретний сервіс
docker compose build backend
Коли використовувати build:
Коли використовувати image:
build + image: Ви можете вказати обидва параметри — Compose зібере образ та присвоїть йому вказане ім'я:backend:
build: ./backend
image: myapp-backend:latest
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
Пріоритет змінних:
environment у docker-compose.yml (найвищий)env_file.env файлу.env з секретами: Додайте .env до .gitignore. Для команди створіть .env.example з placeholder-значеннями:NODE_ENV=development
API_KEY=your_api_key_here
DATABASE_URL=postgres://db:5432/myapp
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
Синтаксис:
volume-name:/path/in/container./host/path:/container/path:ro в кінціКоли що використовувати:
Named Volume
postgres-data:/var/lib/postgresql/dataBind Mount
./src:/app/srcAnonymous Volume
node_modules) від перезапису через Bind Mount.
Docker створює volume з випадковим хеш-іменем. Видаляється разом з контейнером (якщо запущено down -v).
Приклад: /app/node_modulesHot-reload для розробки:
services:
backend:
build: ./backend
volumes:
- ./backend/src:/app/src # Зміни у коді миттєво у контейнері
- /app/node_modules # Не перезаписувати node_modules
command: npm run dev
Тепер зміни у ./backend/src на хості миттєво відображаються у контейнері, і npm run dev перезапускає сервер.
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 не створить цю мережу, а підключиться до вже існуючої.
Синтаксис:
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 документує, які порти використовує сервіс, але не пробросує їх на хост.
Базовий синтаксис:
services:
web:
image: nginx
depends_on:
- backend
- db
backend:
image: myapp-api
depends_on:
- db
db:
image: postgres:16
Що робить depends_on:
db → backend → webweb → backend → dbЩо НЕ робить depends_on:
backend одразу після запуску контейнера db, але PostgreSQL всередині може ще ініціалізуватисяПроблема: Гонки станів (Race Conditions) Бекенд спробує підключитися до бази даних за мілісекунди після старту контейнера. Але базі даних потрібні секунди на ініціалізацію (виділення пам'яті, створення файлів). Відбувається гонка: бекенд приходить швидше, ніж база готова приймати з'єднання, і падає з помилкою.
Рішення 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"]

services:
web:
image: nginx
restart: always
backend:
image: myapp-api
restart: on-failure
worker:
image: myapp-worker
restart: unless-stopped
Опції:
docker compose stop), він все одно буде перезапущений після рестарту Docker daemon. Використовуйте для критичних сервісів (веб-сервер, API).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 для кожного сервісу
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
Проблема: У вас є спільна конфігурація для кількох сервісів (наприклад, logging, restart policy).
Рішення: Винесіть спільну конфігурацію у базовий файл та розширюйте її за допомогою директиви extends.
Обидва сервіси успадкують restart та logging з base-service.
Сценарій: У вас є основні сервіси (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 testsmonitoring — Prometheus, Grafana
Проблема: Конфігурація для 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
Пріоритет:
docker-compose.yml (базова конфігурація)docker-compose.override.yml (якщо існує, застосовується автоматично)-f (застосовуються у порядку вказання)docker-compose.yml — базова конфігурація (спільна для всіх середовищ)docker-compose.override.yml — development (не комітити, якщо містить локальні налаштування)docker-compose.prod.yml — productiondocker-compose.test.yml — testing/CIПроблема: Паролі та 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-файлами.Цей приклад демонструє створення повноцінного production-ready застосунку. Замість того, щоб сприймати його як один великий файл, давайте розглянемо його архітектуру пошарово:

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;
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:
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
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 сертифікати
# 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
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"
Симптоми:
docker compose logs backend
# Помилка: Connection refused to db:5432
Можливі причини:
Рішення:
# Перевірити мережі
docker compose config | grep networks -A 10
# Перевірити health check
docker compose ps
# Додати health check та depends_on
Симптоми: Змінили код, але контейнер використовує стару версію.
Можливі причини:
Рішення:
# Пересобрати образи
docker compose up --build
# Пересобрати без кешу
docker compose build --no-cache
# Додати bind mount для development
Симптоми:
docker compose up
# Error: Bind for 0.0.0.0:8080 failed: port is already allocated
Рішення:
# Знайти процес, що використовує порт
sudo lsof -i :8080
# Або змінити порт у docker-compose.yml
ports:
- "8081:80" # Використати інший порт хоста
Симптоми: Контейнер не може писати у bind mount.
Рішення:
services:
app:
image: myapp
user: "${UID}:${GID}" # Використати UID/GID хоста
volumes:
- ./data:/app/data
# Встановити UID/GID у .env
echo "UID=$(id -u)" >> .env
echo "GID=$(id -g)" >> .env
# Перевірити конфігурацію (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)
Погано:
services:
db:
environment:
- POSTGRES_PASSWORD=hardcoded_password # Небезпечно
Добре:
services:
db:
environment:
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
# .env
POSTGRES_PASSWORD=secure_password_from_env
Використовуйте override-файли для development/production замість одного великого файлу з умовами.
services:
db:
healthcheck:
test: ["CMD-SHELL", "pg_isready"]
interval: 10s
timeout: 5s
retries: 5
Це дозволяє depends_on чекати готовності сервісу.
services:
backend:
deploy:
resources:
limits:
cpus: '1'
memory: 1G
reservations:
cpus: '0.5'
memory: 512M
Погано:
volumes:
- /var/lib/postgresql/data # Anonymous volume
Добре:
volumes:
- db-data:/var/lib/postgresql/data
volumes:
db-data:
Додайте коментарі у docker-compose.yml та створіть README з інструкціями.
services:
# Backend API - .NET 8 Web API
# Доступний на http://localhost:5000 (development)
# Підключається до PostgreSQL та Redis
backend:
build: ./backend
# ...
Створіть .dockerignore у кожній директорії з Dockerfile:
node_modules
.git
.env
*.log
Це прискорює збірку та зменшує розмір build context.
Погано:
services:
db:
image: postgres:latest # Непередбачувано
Добре:
services:
db:
image: postgres:16.2-alpine # Конкретна версія
Docker Compose — це потужний інструмент для управління multi-container застосунками, що значно спрощує розробку та deployment.
Ключові концепції:
Основні команди:
docker compose up -d — запустити застосунок у backgrounddocker compose down — зупинити та видалити контейнериdocker compose logs -f — переглянути логи в реальному часіdocker compose ps — статус сервісівdocker compose exec <service> <command> — виконати команду у контейнеріПросунуті можливості:
Найкращі практики:
.env для конфігурації, не хардкодьте значенняdeploy.resourceslatest)Що далі:
У наступній статті ми розглянемо Docker у production — best practices для deployment, моніторинг, логування, security hardening, та інтеграцію з CI/CD pipelines. Ви навчитеся готувати Docker-застосунки до реального production-середовища з високими вимогами до надійності та безпеки.
Завдання 1.1: Перший docker-compose.yml
Створіть docker-compose.yml для простого веб-сервера Nginx.
Вимоги:
nginx:alpine8080 хоста на 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.
Вимоги:
http://localhost:8080Підказка: Використайте приклад з розділу "Перший docker-compose.yml".
Завдання 1.3: Логи та діагностика
Запустіть застосунок з попереднього завдання та:
docker compose logs db
docker compose exec db mysql -u root -p
docker compose restart wordpress
Завдання 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: Мережева ізоляція
Створіть застосунок з трьома сервісами та двома мережами:
Вимоги:
Перевірка:
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.1: Full-stack застосунок
Створіть повноцінний full-stack застосунок з усіма компонентами:
Компоненти:
Вимоги:
Архітектура:
Internet → Nginx → Frontend (React)
↓
Backend (API) → PostgreSQL
↓
Redis (Cache)
Завдання 3.2: CI/CD інтеграція
Створіть GitHub Actions workflow для автоматичного deployment.
Вимоги:
docker compose buildЗавдання 3.3: Моніторинг та логування
Додайте до застосунку з завдання 3.1 моніторинг та централізоване логування.
Додаткові сервіси:
Вимоги:
--profile monitoring)Запуск:
docker compose --profile monitoring up -d