HostiServer
2026-09-21 11:18
Docker Compose: кілька сервісів разом
📚 Серія «Docker на VPS», частина 2 з 5:
- Docker на VPS: встановлення і перший контейнер
- Docker Compose: кілька сервісів разом ← ви тут
- Docker у production
- Docker + CI/CD: автодеплой з GitHub Actions / GitLab CI
- Gitea / Forgejo у Docker: повний self-hosted стек
Docker Compose: кілька сервісів разом
У першій статті серії ми навчились запускати один контейнер командою docker run. Реальний застосунок рідко живе сам: до нього додається база даних, кеш, а часто ще й черга повідомлень. Три сервіси — це вже три окремі команди docker run, у кожній до десятка прапорів, плюс мережа, яку треба створити вручну заздалегідь: docker network create app-net. Порядок запуску при цьому важливий, але нічим не гарантований.
Проблема стає особливо відчутною після перезавантаження сервера. Контейнери з restart: unless-stopped піднімаються самі, але Docker не знає, який сервіс має стартувати першим. Якщо застосунок піднявся раніше за базу даних, перший же запит впаде з ECONNREFUSED — і доведеться вручну перезапускати застосунок, коли база вже готова.
Docker Compose вирішує це, змінюючи сам спосіб мислення. Замість послідовності команд ви описуєте бажаний стан у файлі docker-compose.yml: які сервіси мають існувати, як вони пов'язані, що від чого залежить. Порядок запуску, мережі та volumes Compose бере на себе.
Один технічний момент варто зняти одразу. Сучасна команда — docker compose (два слова, плагін на Go, вбудований у Docker Engine з версії 20.10+). Стара docker-compose (через дефіс, окрема утиліта на Python, гілка v1) офіційно зняли з підтримки у травні 2023. Перевірити, яка версія стоїть:
docker compose version
Очікуваний вивід — щось на кшталт Docker Compose version v2.x.x. Якщо команда не знайдена, плагін ставиться окремо: apt install docker-compose-plugin.
2. Структура docker-compose.yml
2.1 Про рядок version:
У більшості старих туторіалів файл починається з version: "3.8". У Compose v2 цей рядок ігнорується з попередженням про застарілість — його можна сміливо прибирати, файл стає коротшим і без зайвого шуму в логах.
2.2 services: — ключ, ім'я і DNS одночасно
Назва кожного сервісу під ключем services: виконує подвійну роль: за замовчуванням з неї складається ім'я контейнера (<проєкт>-<сервіс>-1), і водночас це DNS-ім'я, за яким сервіс видно іншим контейнерам у тій самій мережі. Якщо задати container_name: my-app явно, ім'я стане передбачуваним, але тоді сервіс не можна масштабувати командою docker compose up --scale app=3 — Compose не зможе створити кілька контейнерів з однаковим фіксованим ім'ям.
2.3 image: чи build:
image: postgres:16-alpine завантажує готовий образ з реєстру. build: . збирає образ з Dockerfile у поточній директорії. Якщо Dockerfile лежить не в корені проєкту, вказується явно:
build:
context: ./app
dockerfile: Dockerfile.prod
2.4 environment: — два формати запису
Змінні середовища можна писати як map (DATABASE_URL: postgres://...) або як список (- DATABASE_URL=postgres://...). Формат map читається легше при великій кількості змінних. Вираз ${VAR} у будь-якому з форматів підставляється зі значення в .env-файлі чи з shell-оточення в момент запуску Compose.
2.5 ports: — тільки те, що дійсно потрібно ззовні
Формат "3000:3000" означає порт хоста : порт контейнера. Без секції ports: сервіс лишається недоступним ззовні, але доступним іншим сервісам у тій самій мережі Compose. Для бази даних у production ports:, як правило, не потрібен і небажаний — відкривати порт СУБД в інтернет без крайньої необхідності не варто.
2.6 volumes: у сервісі
Коротка форма — pgdata:/var/lib/postgresql/data. Довга форма дозволяє явно вказати тип — type: volume для named volume або type: bind для bind mount. Named volume, використаний у сервісі, має відповідний блок на верхньому рівні файлу: volumes: pgdata: — саме тут Compose бере на себе керування його життєвим циклом.
2.7 networks: — своя мережа за замовчуванням
Без явного оголошення Compose створює мережу з ім'ям <проєкт>_default і підключає до неї всі сервіси. Кастомна мережа задається так: у сервісі — networks: [app-net], і внизу файлу — networks: app-net: (порожній блок означає bridge-мережу з налаштуваннями за замовчуванням).
2.8 restart: — чотири варіанти
| Значення | Поведінка |
|---|---|
no |
За замовчуванням — не перезапускається взагалі |
always |
Перезапускається завжди, навіть після docker compose stop |
unless-stopped |
Перезапускається, крім випадку, коли зупинений вручну |
on-failure |
Тільки якщо процес завершився з ненульовим кодом |
Для production за замовчуванням варто обирати unless-stopped: сервіс переживе перезавантаження хоста, але не буде намагатись піднятись знову, якщо ви зупинили його свідомо.
2.9 command: і entrypoint:
Обидві директиви перевизначають те, що задано в Dockerfile через CMD чи ENTRYPOINT. Типове застосування — окремий dev-режим: command: npm run dev замість production-команди запуску, без потреби тримати два різні Dockerfile.
3. Реальний стек: Node.js + PostgreSQL + Redis
Зведемо все в один робочий приклад — застосунок на Node.js, база даних PostgreSQL і кеш Redis:
services:
app:
build: .
ports: ["3000:3000"]
env_file: .env
environment:
DATABASE_URL: postgres://user:${DB_PASSWORD}@db:5432/mydb
REDIS_URL: redis://cache:6379
depends_on:
db:
condition: service_healthy
cache:
condition: service_started
networks: [app-net]
restart: unless-stopped
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: mydb
volumes: [pgdata:/var/lib/postgresql/data]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d mydb"]
interval: 5s
timeout: 5s
retries: 10
networks: [app-net]
restart: unless-stopped
cache:
image: redis:7-alpine
command: redis-server --maxmemory 128mb --maxmemory-policy allkeys-lru
networks: [app-net]
restart: unless-stopped
volumes:
pgdata:
networks:
app-net:
3.1 Змінні POSTGRES_*
Офіційний образ postgres зчитує POSTGRES_USER, POSTGRES_PASSWORD і POSTGRES_DB лише один раз — при першій ініціалізації кластера на порожньому volume. При наступних запусках, коли volume вже існує, ці змінні ігноруються. Змінити пароль пізніше зміною змінної не вийде — доведеться виконати ALTER USER всередині вже працюючої бази.
3.2 depends_on з умовою service_healthy
Секція depends_on: db: condition: service_healthy означає, що app не почне стартувати, доки healthcheck бази не поверне успіх. Простий запис depends_on: [db], без умови, чекає лише на старт самого процесу контейнера — а PostgreSQL у цей момент ще може ініціалізувати кластер і не приймати з'єднання. Саме це і призводить до ECONNREFUSED при першому запиті після старту стека.
3.3 Чому retries: 10, а не 5
При interval: 5s і retries: 10 Compose готовий чекати на healthy-стан бази до 50 секунд сумарно. Це не з запасом: перша ініціалізація кластера PostgreSQL сама по собі може займати 10-20 секунд, і при retries: 5 (25 секунд) Compose може позначити базу як unhealthy ще до того, як вона реально піднялась.
3.4 redis:7-alpine і ліміт пам'яті
Тег redis:latest означає, що вміст образу може змінитись у будь-який момент; redis:7-alpine фіксує мажорну версію і важить приблизно 40 MB. Прапор --maxmemory 128mb у command: критично важливий у продакшні: без явного ліміту Redis за замовчуванням намагається використати всю доступну пам'ять хоста, і при активному кешуванні це може призвести до OOM-вбивства сусідніх контейнерів.
3.5 Як сервіси знаходять один одного за іменем
Рядок @db:5432 у DATABASE_URL працює завдяки вбудованому DNS Compose: ім'я сервісу db резолвиться у внутрішню IP-адресу відповідного контейнера. Це працює тільки для сервісів, які перебувають в одній секції networks:, і не працює з хоста — з термінала сервера ім'я db нічого не означає.
4. .env — змінні та секрети
4.1 Автоматичне підхоплення
Файл .env у тій самій директорії, що й docker-compose.yml, підхоплюється Compose автоматично, без додаткових налаштувань:
DB_PASSWORD=supersecret123
NODE_ENV=production
4.2 Два різних механізми — не плутати
Тут легко наплутати два схожі, але різні шляхи потрапляння значень у контейнер:
${DB_PASSWORD}у самому compose-файлі — підстановка відбувається під час парсингу YAML самим Compose; значення потрапляє в конфігурацію, але не автоматично в змінні середовища контейнера.env_file: .envу секції сервісу — файл цілком передається всередину контейнера як змінні середовища, доступні застосунку через, наприклад,process.env.DB_PASSWORD.
4.3 Перевірка підстановки
docker compose config
Ця команда виводить фінальний compose-файл уже після підстановки всіх ${VAR} — зручний спосіб переконатись, що значення з .env дійсно підхопились, ще до запуску стека.
4.4 Пріоритет джерел значень
Від вищого пріоритету до нижчого: змінна shell-оточення → значення з файлу .env → дефолт у самому виразі, якщо він заданий як ${VAR:-default}.
4.5 .gitignore
Файл .env обов'язково додається в .gitignore. Розумний шаблон — *.env і .env*, щоб заодно виключити .env.local і .env.production, які часто заводять пізніше.
4.6 .dockerignore теж
.env так само має бути в .dockerignore — інакше COPY . . у Dockerfile скопіює секрети прямо всередину образу. Перевірити це легко:
docker run --rm my-app cat /app/.env
Якщо команда виводить вміст файлу — секрет уже "запечений" в образ, і проблему не вирішити видаленням файлу з наступного коміту: попередній шар образу все одно його містить.
4.7 Права доступу до файлу
chmod 600 .env обмежує читання файлу тільки власником — для одного VPS цього достатньо. Для сценаріїв складніших за один сервер (Swarm, кілька команд з різним рівнем довіри) варто дивитись у бік Docker Secrets або HashiCorp Vault.
5. Мережі: як сервіси знаходять один одного
5.1 Ізоляція за проєктами
Кожен Compose-проєкт — це, як правило, окрема директорія — отримує власну ізольовану мережу. Сервіс app з одного проєкту не бачить db іншого проєкту, навіть якщо обидва запущені на тому самому хості.
5.2 Ім'я проєкту
За замовчуванням ім'я проєкту береться з назви директорії. Його можна задати явно прапором docker compose -p myproject up або рядком name: myproject на верхньому рівні compose-файлу — це впливає на імена контейнерів і мереж, а отже і на те, чи "побачать" один одного два стеки.
5.3 Мінімум відкритих портів
Правило просте: ports: прописується тільки тим сервісам, яким дійсно потрібен доступ ззовні. У прикладі вище це app (3000:3000); db і cache такого блоку не мають. Зайвий рядок "5432:5432" у секції бази даних відкриває PostgreSQL для всього інтернету — це одна з найчастіших причин компрометації бази на VPS.
5.4 Спілкування між різними стеками
Якщо два незалежні Compose-проєкти мають взаємодіяти — наприклад, окремий стек моніторингу, який повинен бачити метрики застосунку, — використовується підключення до вже існуючої мережі:
networks:
monitoring-net:
external: true
6. Ключові команди
| Команда | Що робить |
|---|---|
docker compose up -d |
Запустити всі сервіси у фоні; при першому запуску створює мережі, volumes, тягне образи й збирає build:; при повторному — перезапускає тільки змінені сервіси |
docker compose up -d --build |
Примусово перезібрати образи перед запуском — потрібно після змін у Dockerfile чи коді, інакше Compose бере кешований образ |
docker compose up -d --no-deps app |
Перезапустити тільки app, не чіпаючи db і cache — зручно при деплої нової версії застосунку |
docker compose down |
Зупинити і видалити контейнери й мережі; named volumes лишаються — дані в pgdata збережені |
docker compose down -v |
Те саме, плюс видалення named volumes — незворотно для production: pgdata зникне разом з усіма даними бази |
docker compose stop / start |
Зупинити чи запустити без видалення контейнерів — швидше за down+up, volumes і мережі залишаються |
docker compose restart app |
Перезапустити конкретний сервіс; compose-файл при цьому не перечитується — для застосування змін конфігурації потрібен up -d |
docker compose ps |
Статус усіх сервісів: ім'я, image, стан, порти, health |
docker compose logs -f app |
Follow-логи конкретного сервісу; без імені сервісу — логи всіх одразу з кольоровими префіксами; --tail=100 обмежує вивід останніми рядками |
docker compose exec db psql -U user -d mydb |
Відкрити psql усередині контейнера бази даних для дебагу |
docker compose run --rm app npm run migrate |
Запустити одноразову команду в новому контейнері сервісу app і видалити його одразу після виконання — типово для міграцій БД |
docker compose pull && docker compose up -d |
Оновити образи з реєстру і перезапустити — основний патерн деплою в CI/CD (стаття 4 серії) |
7. Висновок
Тепер весь стек — застосунок, база даних, кеш — описаний в одному файлі. git clone плюс docker compose up -d відтворюють ідентичне середовище на будь-якому сервері з Docker за одну-дві хвилини, без жодного ручного кроку і без питання "у якому порядку це запускати".
Лишається кілька речей, які docker-compose.yml сам по собі не вирішує. HTTPS і SSL-сертифікати — стеку потрібен reverse proxy на кшталт Nginx чи Traefik перед ним. Кілька доменів на одній IP-адресі теж вирішується на рівні proxy, а не Compose. Ліміти ресурсів (mem_limit: 512m, cpus: "0.5") поки що відсутні — а без них один сервіс, що вийшов з-під контролю, здатен забрати всю пам'ять сервера в інших. І логи поки що доступні лише через docker compose logs, без ротації і без збереження після видалення контейнера.
Саме ці прогалини — тема наступної статті: перед стеком стає Nginx або Traefik, з'являється сертифікат Let's Encrypt, а автозапуск переходить з ручного docker compose up -d на systemd-юніт, який піднімає стек після перезавантаження сервера сам, без участі людини.
📚 Навігація серії:
Ви читаєте частину 2 з 5 «Docker Compose: кілька сервісів разом».
Попередня: Частина 1. Docker на VPS: встановлення і перший контейнер ←
Далі: Частина 3. Docker у production →
🚀 VPS під ваш Compose-стек
Кілька контейнерів одночасно — це вже не одне-два ядра "про запас": застосунок, база даних і кеш працюють паралельно і кожен тримає власну пам'ять. Hostiserver дає ресурси, розраховані саме на такий стек.
🖥️ Виділені Сервери
- Від $90/міс, повний контроль над залізом для важчих production-стеків
- Без сусідів по ресурсах: healthcheck бази не залежить від чужого навантаження
- 24/7 підтримка: допоможемо з мережами, volumes і систeмним автозапуском стека
💻 Cloud (VPS) Хостинг
- Від $19.95/міс, KVM-ізоляція, виділені vCPU і RAM
- NVMe-диски: PostgreSQL і Redis з диском, що не стає вузьким місцем
- Достатньо RAM з першого тарифу: app + db + cache одночасно без свопу
- Легко масштабувати: більше пам'яті під новий сервіс у тому самому compose-файлі
💬 Не впевнені, який варіант потрібен?
💬 Напишіть нам — допоможемо з вибором!
Часті запитання
- Чим docker compose (v2) відрізняється від docker-compose (v1)?
docker-compose (через дефіс) — окрема утиліта на Python, гілка v1, офіційно знята з підтримки у травні 2023 року. docker compose (два слова) — плагін на Go, вбудований у Docker Engine, починаючи з версії 20.10+, і саме він розвивається зараз. Синтаксис compose-файлу практично не змінився, різниця переважно в самій команді запуску та швидкості роботи.
- Застосунок падає з ECONNREFUSED одразу після старту — що не так?
Найчастіша причина — простий
depends_on: [db]без умови: Compose чекає лише на старт процесу контейнера бази даних, а не на її готовність приймати з'єднання. Рішення —depends_on: db: condition: service_healthyразом із налаштованимhealthcheck:у самому сервісі бази даних.
- Чи потрібен рядок version: у сучасному compose-файлі?
Ні. У Compose v2 він ігнорується з попередженням про застарілість і не впливає на роботу файлу. Його можна прибрати з будь-якого проєкту без наслідків — файл стане коротшим.
- docker compose down чи down -v — у чому різниця?
downвидаляє контейнери й мережі, але лишає named volumes — дані бази даних збережені.down -vдодатково видаляє й volumes, а разом з ними всі дані всередині. На production-сервері-vварто застосовувати тільки свідомо і після перевірки, що бекап дійсно є.
- Як передати пароль бази даних, не зберігаючи його в git?
Пароль виноситься у файл
.envпоруч ізdocker-compose.yml, а сам файл додається в.gitignore(і в.dockerignore, щоб не потрапити в образ). У compose-файлі значення підставляється через${DB_PASSWORD}. Для сценаріїв з кількома серверами чи командами варто розглянути Docker Secrets або зовнішнє сховище на кшталт HashiCorp Vault.
- Чи можна відкрити порт бази даних назовні для дебагу?
Технічно можна, додавши
ports: ["5432:5432"], але для production це небажано — порт стає доступним усьому інтернету. Для дебагу безпечніший шлях —docker compose exec db psql -U user -d mydb, який відкриває сесію psql прямо всередині контейнера без відкриття порту назовні, або тимчасовий SSH-тунель на час роботи.
- Скільки пам'яті виділяти кожному сервісу?
У базовому
docker-compose.ymlбез явних лімітів кожен сервіс може використати всю доступну пам'ять хоста — саме тому Redis у прикладі вище запущений з--maxmemory 128mb. Для системних лімітів на рівні Compose використовуютьсяmem_limitіcpusу кожному сервісі; докладніше про це — у наступній статті серії, присвяченій production-налаштуванням.