Docker Compose: застосунок із кількох контейнерів одним файлом
compose.yaml від простого до робочого: сервіси, build, змінні й .env, depends_on з healthcheck, томи й мережі, профілі, override-файли, watch, секрети, команди на кожен день і деплой на сервер.
Навіщо Compose
Застосунок рідко складається з одного контейнера: бекенд, база, кеш, черга, проксі. Запускати кожен через docker run з десятком прапорців незручно і ненадійно. Compose описує все в одному файлі compose.yaml і керує ним однією командою.
Мінімальний приклад
# compose.yaml
services:
web:
image: nginx:stable-alpine
ports:
- "8080:80"
volumes:
- ./site:/usr/share/nginx/html:rodocker compose up -d # створити й запустити
docker compose ps
docker compose down # зупинити й прибрати контейнери та мережуБез version і з пробіломРядок
version: "3.8"на початку файлу застарів і ігнорується (Compose лише попереджаєthe attribute version is obsolete). Команда —docker compose(плагін), а неdocker-compose(стара програма на Python, більше не підтримується). Файл бажано називатиcompose.yaml;docker-compose.ymlтеж працює.
Робочий приклад: застосунок + Postgres + Redis
# compose.yaml
name: notes
services:
web:
build:
context: .
args:
APP_VERSION: ${APP_VERSION:-dev}
image: ghcr.io/olena/notes-app:${APP_VERSION:-dev}
ports:
- "127.0.0.1:8080:3000"
environment:
DATABASE_URL: postgres://notes:${POSTGRES_PASSWORD:?Задайте POSTGRES_PASSWORD}@db:5432/notes
REDIS_URL: redis://redis:6379
TZ: Europe/Kyiv
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3000/healthz"]
interval: 30s
timeout: 3s
retries: 3
start_period: 20s
deploy:
resources:
limits:
memory: 512M
cpus: "1.0"
db:
image: postgres:17-alpine
environment:
POSTGRES_USER: notes
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: notes
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U notes -d notes"]
interval: 5s
timeout: 3s
retries: 10
restart: unless-stopped
redis:
image: redis:7-alpine
command: ["redis-server", "--appendonly", "yes"]
volumes:
- redisdata:/data
restart: unless-stopped
volumes:
pgdata:
redisdata:# .env — поряд з compose.yaml, у .gitignore!
POSTGRES_PASSWORD=change-me-please
APP_VERSION=1.4.0Що тут відбувається:
name: notes— ім'я проєкту. Від нього залежать імена: контейнериnotes-web-1, мережаnotes_default, томиnotes_pgdata. Безnameбереться назва папки.- Сервіси бачать одне одного за іменами (
db,redis) — Compose створює спільну мережу з DNS. - БД без
ports— недоступна ззовні, лише дляweb. Портwebопубліковано на127.0.0.1— назовні його віддає reverse proxy. ${POSTGRES_PASSWORD:?…}— якщо змінна не задана, Compose зупиниться з цим повідомленням, а не запустить базу з порожнім паролем.
Перевикористання
backend:
build: ./backend
image: server-monitor-backend:latest
# ...решта без змін
collector:
image: server-monitor-backend:latest
pull_policy: never
command: ["python", "./collector.py"]
# ...environment, depends_on, networks без змін, рядок build прибираєш- Коли в сервісі є і
build, іimage, compose збирає образ і дає йому назву зimage. Безimageназва генерується сама (<проєкт>-<сервіс>), через це й з'явилися два образи. - У collector немає
build, тож він просто запускає образ, який уже існує під цією назвою. pull_policy: neverкаже compose, що цей образ є тільки локально. Без цього рядка compose спершу спробує стягнутиserver-monitor-backendз Docker Hub і покаже помилкуpull access denied. Вона не зупиняє запуск, але плутає.
Змінні середовища
Дві різні речі з схожими назвами:
| Механізм | Для чого | Де діє |
|---|---|---|
файл .env поряд з compose |
підстановка ${VAR} у сам compose.yaml |
лише при розборі файлу |
environment: |
змінні всередині контейнера | у процесі застосунку |
env_file: |
змінні всередині контейнера з файлу | у процесі застосунку |
Синтаксис підстановки:
| Запис | Значення |
|---|---|
${VAR} |
значення або порожньо (з попередженням) |
${VAR:-default} |
default, якщо не задано або порожнє |
${VAR:?повідомлення} |
помилка, якщо не задано |
Подивитися, як Compose розгорнув файл з усіма змінними:
docker compose configdepends_on і готовність
depends_on без умов лише задає порядок старту: web стартує після того, як запустився контейнер db. Але Postgres після запуску ще кілька секунд ініціалізується — і застосунок падає з connection refused.
З condition: service_healthy Compose чекає, поки healthcheck бази стане healthy:
depends_on:
db:
condition: service_healthy
restart: true # перезапустити web, якщо db перезапустили| Умова | Чекає |
|---|---|
service_started |
контейнер запущено (типово) |
service_healthy |
healthcheck пройшов |
service_completed_successfully |
одноразовий сервіс завершився з кодом 0 (міграції) |
Міграції окремим сервісомmigrate: image: ghcr.io/olena/notes-app:${APP_VERSION:-dev} command: ["npm", "run", "migrate"] depends_on: db: { condition: service_healthy } web: depends_on: migrate: { condition: service_completed_successfully }
Команди на кожен день
| Команда | Що робить |
|---|---|
docker compose up -d |
створити/оновити й запустити все у фоні |
docker compose up -d --build |
перезібрати образи перед запуском |
docker compose up -d web |
лише сервіс web (і його залежності) |
docker compose ps |
стан сервісів |
docker compose logs -f web |
логи сервісу наживо |
docker compose logs --since 10m |
усі логи за 10 хвилин |
docker compose exec web sh |
оболонка в працюючому сервісі |
docker compose run --rm web npm test |
одноразовий контейнер сервісу |
docker compose restart web |
перезапустити (без перестворення!) |
docker compose stop / start |
зупинити / запустити, не видаляючи |
docker compose down |
видалити контейнери й мережу (томи лишаються) |
docker compose down -v |
разом із томами — дані зникнуть |
docker compose pull |
завантажити свіжі образи |
docker compose build --no-cache web |
перезібрати без кешу |
docker compose config |
фінальний конфіг після підстановок |
docker compose top |
процеси в усіх контейнерах |
docker compose up -d --scale worker=3 |
три копії сервісу worker |
restart не перечитує конфігЗмінили
environmentчиportsу compose.yaml —docker compose restartнічого не застосує, бо лише перезапускає існуючий контейнер. Потрібноdocker compose up -d: Compose сам помітить зміни й перестворить лише змінені сервіси.
Кілька середовищ: override-файли
Compose автоматично зливає compose.yaml і compose.override.yaml (якщо він є). Зручно: базовий файл — як у проді, override — для локальної розробки.
# compose.override.yaml — лише локально
services:
web:
build: .
ports:
- "3000:3000"
volumes:
- ./src:/app/src
environment:
LOG_LEVEL: debug
db:
ports:
- "127.0.0.1:5432:5432" # щоб підключитися з DBeaverЯвні файли для інших середовищ:
docker compose -f compose.yaml -f compose.prod.yaml up -dЦей сайт, до речі, так і запускається локально: docker compose -f docker-compose.yml -f docker-compose.local.yml up -d --build.
Профілі: опціональні сервіси
services:
adminer:
image: adminer
ports: ["127.0.0.1:8081:8080"]
profiles: [tools]
mailpit:
image: axllent/mailpit
profiles: [tools]docker compose up -d # без adminer і mailpit
docker compose --profile tools up -d # з нимиWatch: автооновлення під час розробки
services:
web:
build: .
develop:
watch:
- action: sync # змінені файли копіюються в контейнер
path: ./src
target: /app/src
- action: rebuild # змінився package.json — перезібрати образ
path: package.jsondocker compose watchПрацює швидше й надійніше за bind mount на Windows і macOS.
Секрети
Пароль у environment видно в docker inspect. Compose вміє монтувати секрети файлами в /run/secrets/:
services:
db:
image: postgres:17-alpine
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
secrets:
db_password:
file: ./secrets/db_password.txt # не комітити!Багато офіційних образів (postgres, mysql, mariadb) розуміють змінні з суфіксом _FILE.
Деплой на сервер з Compose
Найпростіший робочий процес без Kubernetes:
# на сервері, у папці з compose.yaml і .env
docker compose pull # нові образи з реєстру
docker compose up -d --remove-orphans # перестворити змінені сервіси
docker image prune -f # прибрати старі шариВідкат — повернути попередній APP_VERSION у .env і повторити up -d. Про збірку образів у CI і реєстр — у нотатці «Docker у продакшені». PaaS на кшталт Dokploy чи Coolify роблять те саме, плюс інтерфейс, домени й HTTPS.
Шпаргалка
| Команда | Що робить |
|---|---|
docker compose up -d [--build] |
запустити / оновити |
docker compose down [-v] |
прибрати [з томами] |
docker compose ps / logs -f S |
стан / логи |
docker compose exec S sh |
зайти в сервіс |
docker compose run --rm S CMD |
одноразова команда |
docker compose pull && docker compose up -d |
оновити з реєстру |
docker compose config |
перевірити підстановки |
docker compose -f a.yaml -f b.yaml up -d |
кілька файлів |
docker compose --profile tools up -d |
з опціональними сервісами |
docker compose watch |
автосинхронізація коду |
Збережіть статтю собі
Увійдіть, щоб імпортувати її у власні нотатки, відмічати вивчене, ставити лайки й питати спільноту. Реєстрація безкоштовна: вхід через Google займає кілька секунд.
Увійти через Google Інші способи входу