Learning Platform
Глоссарий Troubleshooting
Урок 12.02 · 24 мин
Начальный
dockercomposeyamlservices

Структура 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 локально.

TIP

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.

WARNING

.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

Compose-стенд: что compose делает по docker compose up
docker compose upКоманда читает compose.yml, проверяет валидность YAML, строит план
Create network etl_defaultUser-defined bridge с именем <project>_default. Embedded DNS включен, сервисы будут резолвиться по имени
Create volume etl_pgdataNamed volume. Если уже существует — переиспользуется
Build app imageЕсли build: указан, compose делает docker build. Кэш слоёв используется как обычно
Start postgresdepends_on пустой -> стартует первым. healthcheck начинает работать через start_period
wait healthy
Start appdepends_on: postgres + service_healthy. Compose ждёт перехода postgres в healthy, потом стартует app

Попробуй сам

# 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
NOTE

Compose читает comments в YAML (# ...). Используй комментарии — твой compose-файл потом будут читать другие люди (и ты сам через месяц).

В следующем уроке — команды управления стендом: up/down/logs/exec/ps. Когда нужен --build, что делает -d, как заходить в контейнеры.


Проверка знанийKnowledge check
Объясни разницу между build: ./app , build: { context: ./app, target: production } и image: myorg/etl-app:v1.0 в секции сервиса compose. Когда какой вариант подходит и что произойдёт, если задать build и image одновременно?
ОтветAnswer
build: ./app — короткая форма, эквивалент build: { context: ./app, dockerfile: Dockerfile }. Compose соберёт образ из ./app/Dockerfile, без указания stage (соберётся последний stage в Dockerfile). build: { context, target: production } — длинная форма с указанием явного multi-stage target. Полезно если Dockerfile содержит stages dev/build/production и для compose нужен конкретный. image: myorg/etl-app:v1.0 — берётся готовый образ из registry или local cache. Сборки не происходит, делается docker pull. Это правильно для production-стендов с заранее зачёткарованным образом. Если задать build и image одновременно — compose будет: (1) собирать образ из build-секции, (2) тэгировать собранный образ по image-значению локально. Удобно для CI: build один раз, push под нужным тэгом, потом другие compose-файлы используют только image. Также удобно для dev: можно сделать docker compose pull image (если он есть в registry), или docker compose build чтобы локально пересобрать тот же тэг.

Проверьте понимание

Результат: 0 из 0
Прикладной
Вопрос 1 из 4. В чём разница между секциями image: и build: в compose, и что произойдёт, если задать оба?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5