Структура compose.yml: services, build, ports, volumes
Compose-файл — это YAML, который описывает желаемое состояние стенда: какие сервисы запустить, из каких образов, как их связать. В этом уроке разбираем все основные секции на примере реального DE-стенда: Python app + Postgres. После этого ты сможешь читать чужие compose-файлы и писать свои без шпаргалки.
Базовый скелет
services:
<service-name>:
image: <name>:<tag> # ИЛИ build: <context>
ports: ["..."]
environment: ...
volumes: [...]
networks: [...]
depends_on: [...]
healthcheck: { ... }
restart: unless-stopped
volumes:
<volume-name>:
networks:
<network-name>:
Топ-уровневые секции:
services:— обязательная. Что запускаем.volumes:— объявление named volumes, чтобы compose их создавал.networks:— объявление сетей, если нужны сегментированные сети (фронт/бэк).
В простых стендах volumes: и networks: могут отсутствовать — compose создаст всё под капотом.
image vs build
Два способа получить образ для сервиса.
image — берём готовый из registry:
services:
db:
image: postgres:17
Compose сделает docker pull postgres:17, если образа нет локально.
build — собираем из Dockerfile:
services:
app:
build:
context: ./app
dockerfile: Dockerfile
args:
UID: "1000"
target: production
context: ./app — папка с Dockerfile. target: production — какой stage из multi-stage Dockerfile собирать.
Сокращённая форма для простого случая:
services:
app:
build: ./app
— то же, что build: { context: ./app }.
Можно сочетать:
services:
app:
build: ./app
image: myorg/etl-app:dev # tag для собранного образа
После compose build образ получит тэг myorg/etl-app:dev локально.
docker compose build собирает указанные сервисы. docker compose up --build собирает И запускает. На CI обычно делают compose build отдельно, чтобы лучше видеть прогресс и кэшировать слои.
TCP 3-way handshake — SYN, SYN-ACK, ACK и sequence numbers
ports — публикация
Короткий синтаксис:
ports:
- "8080:80" # host 0.0.0.0:8080 -> container 80
- "127.0.0.1:5432:5432" # только localhost
- "127.0.0.1::5432" # host random -> container 5432
- "8080:80/udp"
Длинный (явный):
ports:
- target: 80
published: 8080
protocol: tcp
mode: host
Если порт не нужен снаружи (доступ только из соседних compose-сервисов) — секцию ports: не пиши. Сервис всё равно будет доступен по имени для других контейнеров compose.
environment — переменные окружения
Два синтаксиса. Список:
environment:
- POSTGRES_PASSWORD=secret
- POSTGRES_DB=etl
- DEBUG=1
Map:
environment:
POSTGRES_PASSWORD: secret
POSTGRES_DB: etl
DEBUG: "1"
Map читается лучше, но требует кавычек для чисел и булеанов: DEBUG: "1", не DEBUG: 1 (compose воспримет как int, что иногда не то).
Можно подтянуть из shell:
environment:
- DB_PASSWORD # без значения = compose возьмёт из текущего env shell
- GIT_SHA=${GIT_SHA} # интерполяция
Или из файла:
env_file:
- .env.app
env_file: — это переменные, передаваемые в контейнер. Это не то же, что .env файл в корне проекта — тот используется compose для интерполяции переменных в YAML.
.env в корне (рядом с compose.yml) — для compose-самого: значения подставляются в ${VAR} внутри YAML. env_file: внутри сервиса — это другое: переменные передаются в env контейнера. Об этом подробнее в модуле 12.
volumes — монтирование
Короткий синтаксис:
volumes:
- pgdata:/var/lib/postgresql/data # named volume
- ./src:/app/src # bind mount (host path)
- ./init:/docker-entrypoint-initdb.d:ro # bind read-only
- /tmp/cache # анонимный volume
Длинный — для bind:
volumes:
- type: bind
source: ./src
target: /app/src
read_only: true
Длинный — для tmpfs:
volumes:
- type: tmpfs
target: /run/secrets
tmpfs:
size: 8m
Если используешь named volume — его надо объявить в top-level:
services:
db:
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
Без объявления compose выкинет ошибку.
networks — сегментация
По умолчанию compose создаёт одну сеть <project>_default, все сервисы туда подключаются. Если нужно несколько:
services:
proxy:
image: nginx
networks: [public, internal]
app:
image: myapp
networks: [internal]
db:
image: postgres:17
networks: [internal]
networks:
public:
internal:
proxy достучится до app и db через internal, а к внешнему миру — через public. app и db друг друга видят, но не видят public-сегмент.
healthcheck
Compose поддерживает healthcheck per-сервис:
services:
postgres:
image: postgres:17
environment:
POSTGRES_PASSWORD: secret
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 3s
retries: 5
start_period: 10s
test:— команда для проверки.CMD-SHELL "..."запускает вsh -c,CMD ["cmd", "arg"]— exec.interval:— как часто проверять (default 30s).timeout:— сколько ждать ответа (default 30s).retries:— сколько неуспехов подряд перед признанием unhealthy (default 3).start_period:— grace-period в начале (default 0). Failures здесь не считаются.
Healthcheck нужен для depends_on: { condition: service_healthy }.
restart policy
restart: no # default: не перезапускать
restart: always # всегда перезапускать
restart: on-failure # только если упал с ненулевым exit code
restart: unless-stopped # перезапускать, кроме случая когда я сам остановил
Для dev-стенда — restart: no (default). Для long-running prod-сервисов — unless-stopped.
Полный реалистичный пример
services:
postgres:
image: postgres:17
environment:
POSTGRES_PASSWORD: secret
POSTGRES_DB: etl
volumes:
- pgdata:/var/lib/postgresql/data
- ./init:/docker-entrypoint-initdb.d:ro
ports:
- "127.0.0.1:5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d etl"]
interval: 5s
timeout: 3s
retries: 5
start_period: 10s
app:
build:
context: ./app
target: dev
depends_on:
postgres:
condition: service_healthy
environment:
DATABASE_URL: postgresql://postgres:secret@postgres:5432/etl
PYTHONUNBUFFERED: "1"
volumes:
- ./app/src:/app/src
ports:
- "127.0.0.1:8000:8000"
restart: unless-stopped
volumes:
pgdata:
Что описано:
postgres— БД с volume для данных, init-скриптами, healthcheck’ом.app— Python-приложение, build из локальной папки, зависит от healthy postgres, монтирует код live-edit.- Один named volume
pgdata. - Сеть compose создаст автоматически.
Структура DAG для этого compose
Попробуй сам
# 1. Создай мини-проект.
mkdir -p ./pg-app && cd ./pg-app
mkdir -p init
cat > init/01-schema.sql <<'SQL'
CREATE TABLE messages (id SERIAL, text TEXT, created_at TIMESTAMPTZ DEFAULT NOW());
INSERT INTO messages(text) VALUES ('hello'), ('world');
SQL
cat > compose.yml <<'YAML'
services:
postgres:
image: postgres:17
environment:
POSTGRES_PASSWORD: secret
POSTGRES_DB: app
volumes:
- pgdata:/var/lib/postgresql/data
- ./init:/docker-entrypoint-initdb.d:ro
ports:
- "127.0.0.1:5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d app"]
interval: 5s
retries: 5
start_period: 10s
client:
image: postgres:17
depends_on:
postgres:
condition: service_healthy
command: |
sh -c "psql -h postgres -U postgres -d app -c 'SELECT count(*) FROM messages;'"
environment:
PGPASSWORD: secret
volumes:
pgdata:
YAML
# 2. Запусти.
docker compose up
# Видишь, как стартует postgres, потом — после healthy — стартует client, делает SELECT, выходит.
# 3. Cleanup.
docker compose down -v
cd ..
rm -rf pg-app
Compose читает comments в YAML (# ...). Используй комментарии — твой compose-файл потом будут читать другие люди (и ты сам через месяц).
В следующем уроке — команды управления стендом: up/down/logs/exec/ps. Когда нужен --build, что делает -d, как заходить в контейнеры.