Главная/Docker Compose/Урок

9.1. Основы Compose

Цели

После этого материала вы сможете:

  • написать compose.yaml по актуальной спецификации и объяснить, почему ключ version больше не нужен;
  • предсказать имена container'ов, сетей и volumes, которые создаст Compose;
  • задать имя проекта четырьмя способами и назвать их приоритет;
  • использовать docker compose config как основной инструмент диагностики;
  • различать up, run и exec и выбирать нужное;
  • объяснить, что такое orphan containers и откуда они берутся.

Предварительные знания

Ключевые термины

ТерминОбъяснение
Compose SpecificationАктуальный формат файла, объединивший версии 2.x и 3.x
projectНабор ресурсов Compose с общим префиксом имени
serviceОписание того, как запускать container'ы одного типа
orphanContainer проекта, которого больше нет в файле
interpolationПодстановка переменных вида ${VAR} в значения

Теория

Что такое Compose Specification

Исторически существовали форматы 1, 2.x и 3.x, различавшиеся набором ключей. Ключ version в начале файла выбирал формат.

Сейчас действует единая Compose Specification, вобравшая возможности всех версий. Ключ version объявлен устаревшим:

yaml
version: "3.8"     # УСТАРЕЛО: игнорируется, вызывает предупреждение
services:
  ...
УтверждениеСтатус
version обязателенНеверно с 2020 года
version влияет на поведениеНе влияет — игнорируется
Его наличие безвредноВызывает предупреждение при каждом запуске
Ключи 3.x работают без негоДа

Просто не пишите version. Файл начинается со services.

Отдельно: docker-compose (Python, версия 1) снят с поддержки. Курс использует только docker compose — плагин Docker CLI.

Имена файлов

Compose ищет в текущем каталоге в таком порядке:

ПриоритетИмя
1compose.yaml
2compose.yml
3docker-compose.yaml
4docker-compose.yml

Предпочтительное имя — compose.yaml: оно рекомендовано спецификацией. Старые имена поддерживаются для совместимости.

Найдя compose.yaml, Compose дополнительно подхватывает compose.override.yaml, если он есть (урок 9.6).

Структура файла

yaml
name: myproject                # имя проекта (необязательно)

services:                      # обязательный раздел
  api:
    image: python:3.13-slim
    command: ["python", "-m", "http.server", "8000"]
    ports:
      - "8000:8000"

networks:                      # объявления сетей
  backend:

volumes:                       # объявления volumes
  db-data:

configs:                       # файлы конфигурации
secrets:                       # секреты

Обязателен только services. Разделы networks и volumes нужны, когда вы объявляете собственные: сеть по умолчанию создаётся без объявления.

Имя проекта определяет всё остальное

Compose добавляет имя проекта к именам всех создаваемых ресурсов. Это механизм изоляции: два проекта с одинаковыми именами сервисов не конфликтуют.

РесурсШаблон имениПример
Container<проект>-<сервис>-<номер>myapp-api-1
Сеть по умолчанию<проект>_defaultmyapp_default
Объявленная сеть<проект>_<имя>myapp_backend
Volume<проект>_<имя>myapp_db-data
Метка проектаcom.docker.compose.projectmyapp

Обратите внимание на разделители: в именах container'ов — дефис, в именах сетей и volumes — подчёркивание. Это не опечатка, а следствие истории формата.

Имя проекта задаётся четырьмя способами, в порядке убывания приоритета:

ПриоритетСпособ
1Флаг -p / --project-name
2Ключ name: в файле
3Переменная COMPOSE_PROJECT_NAME
4Имя каталога с файлом (в нижнем регистре, без спецсимволов)

Четвёртый вариант — умолчание, и он источник неожиданностей: переименование каталога делает проект другим. Прежние container'ы и volumes остаются, но Compose их больше не видит.

Отсюда практическое правило: задавайте name: явно в файле. Тогда проект не зависит от того, как назван каталог у конкретного разработчика.

Команды жизненного цикла

КомандаЧто делает
upСоздаёт и запускает всё; при изменениях пересоздаёт
up -dТо же в фоне
up --buildПересобирает образы перед запуском
downОстанавливает и удаляет container'ы и сети
down -vДополнительно удаляет объявленные volumes
stop / startОстанавливает и запускает без удаления
restartПерезапускает container'ы
psСписок container'ов проекта
logs -fЛоги всех сервисов вперемешку с префиксами
execКоманда в работающем container'е
runНовый container из описания сервиса
buildТолько сборка
configИтоговая конфигурация после слияния и подстановок
topПроцессы в container'ах проекта

Ключевое различие, которое путают чаще всего:

execrun
Использует работающий containerДаНет, создаёт новый
Требует, чтобы сервис был запущенДаНет
Публикует портыНет (без --service-ports)
Запускает зависимостиДа (без --no-deps)
Удаляется после выходаТолько с --rm

Для отладки работающего сервиса нужен exec. Для разовой задачи — миграции, команды управления — run --rm.

docker compose config — главный инструмент диагностики

Команда печатает конфигурацию после слияния файлов, подстановки переменных и применения умолчаний. Это то, что Compose собирается выполнить.

bash
docker compose config
docker compose config --services      # только имена сервисов
docker compose config --volumes       # только volumes
docker compose config --images        # образы, которые будут использованы

Практическая ценность: большинство вопросов «почему не работает» решаются взглядом на вывод этой команды. Переменная не подставилась, файл-override переопределил не то, путь развернулся не туда — всё видно сразу.

Привычка, экономящая часы: перед up при непонятном поведении выполните config.

Orphan containers

Сценарий: в файле были сервисы api и worker. Вы удалили worker из файла и выполнили up. Container worker продолжает работать — Compose о нём больше не знает, но метка проекта на нём осталась.

text
WARN[0000] Found orphan containers ([myapp-worker-1]) for this project.
If you removed or renamed this service in your compose file, you can run
this command with the --remove-orphans flag to clean it up.
Причина появленияПояснение
Сервис удалён из файлаСамый частый случай
Сервис переименованСтарое имя осталось container'ом
Профиль перестал быть активнымContainer от прошлого запуска (урок 9.6)
Другой файл того же проекта-f указал на другой набор сервисов

Устранение — docker compose up --remove-orphans или docker compose down --remove-orphans.

Что удаляет down

Ресурсdowndown -v
Container'ыУдаляетУдаляет
Сети проектаУдаляетУдаляет
Объявленные volumesОставляетУдаляет
Anonymous volumesУдаляетУдаляет
Внешние volumes (external: true)ОставляетОставляет
ОбразыОставляетОставляет (нужен --rmi)

Третья строка — то, что делает down безопасным для данных: перезапуск стека не уничтожает базу. И она же объясняет, почему down -v требует осторожности (урок 7.6).


Внутренний механизм

Как Compose узнаёт свои ресурсы

Каждый созданный объект получает метки:

text
com.docker.compose.project=myapp
com.docker.compose.service=api
com.docker.compose.container-number=1
com.docker.compose.project.working_dir=/home/user/myapp
com.docker.compose.project.config_files=/home/user/myapp/compose.yaml

Команды ps, down, logs работают через фильтр по первой метке — поэтому имя проекта и определяет, что Compose считает «своим».

Метка config_files объясняет ещё одно поведение: Compose помнит, из какого файла создан container, и это видно в docker inspect.

Когда container пересоздаётся

docker compose up не перезапускает то, что не изменилось. Пересоздание происходит, если изменились:

ЧтоПример
Конфигурация сервисаДругой command, environment, ports
ОбразПересобран или подтянут новый
Присоединённые сети или volumesДобавлено монтирование

Сравнение идёт по хешу конфигурации, который Compose хранит в метке container'а. Принудительное пересоздание — up --force-recreate.


Команды и примеры

Минимальный файл

bash
mkdir -p /tmp/comp-basics && cd /tmp/comp-basics

cat > compose.yaml <<'EOF'
name: basics

services:
  web:
    image: python:3.13-slim
    command: ["python", "-m", "http.server", "8000", "--bind", "0.0.0.0"]
    ports:
      - "18800:8000"

  worker:
    image: python:3.13-slim
    command: ["sh", "-c", "while true; do echo tick; sleep 5; done"]
EOF

docker compose up -d
docker compose ps --format 'table {{.Name}}\t{{.Service}}\t{{.Status}}\t{{.Ports}}'

Ожидаемый вывод:

text
[+] Running 3/3
 ✔ Network basics_default     Created
 ✔ Container basics-web-1     Started
 ✔ Container basics-worker-1  Started
NAME               SERVICE   STATUS         PORTS
basics-web-1       web       Up 2 seconds   0.0.0.0:18800->8000/tcp
basics-worker-1    worker    Up 2 seconds   

Три ресурса созданы, имена собраны по шаблону: basics — имя проекта из ключа name:.

Проверим, что сервис работает и сеть создана:

bash
curl -s -o /dev/null -w 'HTTP %{http_code}\n' http://localhost:18800/
docker network ls --filter name=basics --format '  {{.Name}}  ({{.Driver}})'

Ожидаемый вывод:

text
HTTP 200
  basics_default  (bridge)

Имя проекта и его источники

bash
cd /tmp/comp-basics
echo "═══ из ключа name: ═══"
docker compose config --format json 2>/dev/null | python3 -c "
import json, sys
print('  ', json.load(sys.stdin).get('name'))
"

echo "═══ переменная окружения перекрывает файл? ═══"
COMPOSE_PROJECT_NAME=fromenv docker compose config --format json 2>/dev/null | python3 -c "
import json, sys
print('  ', json.load(sys.stdin).get('name'))
"

echo "═══ флаг -p перекрывает всё ═══"
docker compose -p fromflag config --format json 2>/dev/null | python3 -c "
import json, sys
print('  ', json.load(sys.stdin).get('name'))
"

echo "═══ без name: — имя каталога ═══"
mkdir -p /tmp/comp-basics/sub && cd /tmp/comp-basics/sub
cat > compose.yaml <<'EOF'
services:
  x:
    image: alpine:3.21
    command: ["true"]
EOF
docker compose config --format json 2>/dev/null | python3 -c "
import json, sys
print('  ', json.load(sys.stdin).get('name'))
"
cd /tmp/comp-basics

Ожидаемый вывод:

text
═══ из ключа name: ═══
   basics
═══ переменная окружения перекрывает файл? ═══
   basics
═══ флаг -p перекрывает всё ═══
   fromflag
═══ без name: — имя каталога ═══
   sub

Второй блок показывает важное: ключ name: в файле сильнее переменной окружения. Порядок приоритета не совпадает с интуицией «окружение всегда главнее».

Четвёртый блок демонстрирует умолчание — имя каталога. Именно поэтому переименование каталога «теряет» проект.

docker compose config раскрывает всё

bash
cd /tmp/comp-basics
cat > compose.yaml <<'EOF'
name: basics

services:
  web:
    image: "python:${PY_VERSION:-3.13}-slim"
    command: ["python", "-m", "http.server", "${PORT:-8000}", "--bind", "0.0.0.0"]
    ports:
      - "18800:${PORT:-8000}"
    environment:
      MODE: "${MODE:?переменная MODE обязательна}"
    volumes:
      - ./data:/data
EOF

echo "═══ без MODE — обязательная переменная ═══"
docker compose config 2>&1 | tail -2 | sed 's/^/  /'

echo "═══ с MODE ═══"
MODE=dev docker compose config | sed 's/^/  /'

Ожидаемый вывод:

text
═══ без MODE — обязательная переменная ═══
  error: required variable MODE is missing a value: переменная MODE обязательна
═══ с MODE ═══
  name: basics
  services:
    web:
      command:
        - python
        - -m
        - http.server
        - "8000"
        - --bind
        - 0.0.0.0
      environment:
        MODE: dev
      image: python:3.13-slim
      networks:
        default: null
      ports:
        - mode: ingress
          target: 8000
          published: "18800"
          protocol: tcp
      volumes:
        - type: bind
          source: /tmp/comp-basics/data
          target: /data
          bind:
            create_host_path: true
  networks:
    default:
      name: basics_default

Вывод показывает то, что обычно скрыто:

НаблюдениеЗначение
python:3.13-slimУмолчание ${PY_VERSION:-3.13} подставлено
source: /tmp/comp-basics/dataОтносительный путь развёрнут в абсолютный
ports в длинной формеКороткая запись разобрана на поля
networks: default: nullСервис подключён к сети по умолчанию
name: basics_defaultИтоговое имя сети

Первый блок демонстрирует синтаксис обязательной переменной ${VAR:?сообщение}: Compose отказывается работать, пока она не задана (урок 9.5).

up не пересоздаёт то, что не менялось

bash
cd /tmp/comp-basics
cat > compose.yaml <<'EOF'
name: basics

services:
  web:
    image: python:3.13-slim
    command: ["python", "-m", "http.server", "8000", "--bind", "0.0.0.0"]
    ports:
      - "18800:8000"
EOF

docker compose up -d > /dev/null 2>&1
id1="$(docker compose ps -q web)"
started1="$(docker inspect "$id1" --format '{{.State.StartedAt}}')"
printf '  первый запуск:  %s\n' "${id1:0:12}"

echo "═══ повторный up без изменений ═══"
docker compose up -d 2>&1 | tail -2 | sed 's/^/  /'
id2="$(docker compose ps -q web)"
printf '  тот же container: %s\n' "$([ "$id1" = "$id2" ] && echo да || echo нет)"

echo "═══ меняем command ═══"
sed -i 's/8000", "--bind/8000", "--directory", "\/tmp", "--bind/' compose.yaml
docker compose up -d 2>&1 | tail -2 | sed 's/^/  /'
id3="$(docker compose ps -q web)"
printf '  тот же container: %s\n' "$([ "$id1" = "$id3" ] && echo да || echo нет)"

echo "═══ метка с хешем конфигурации ═══"
docker inspect "$id3" --format '  {{index .Config.Labels "com.docker.compose.config-hash"}}' | cut -c1-50

Ожидаемый вывод:

text
  первый запуск:  a3f8c91e2d47
═══ повторный up без изменений ═══
   ✔ Container basics-web-1  Running
  тот же container: да
═══ меняем command ═══
   ✔ Container basics-web-1  Recreated
  тот же container: нет
  метка с хешем конфигурации:
  7b2e9f1a4c8d3e6b0f5a2c9d1e8b4f7a

Статус в выводе показывает решение Compose: Running — ничего не делал, Recreated — пересоздал. Основание — хеш конфигурации в метке.

exec против run

bash
cd /tmp/comp-basics
echo "═══ exec: в работающем container ═══"
docker compose exec -T web sh -c 'echo "PID 1: $(cat /proc/1/comm)"; hostname'

echo "═══ run: новый container ═══"
docker compose run --rm -T web sh -c 'echo "PID 1: $(cat /proc/1/comm)"; hostname'

echo "═══ сколько container'ов сейчас ═══"
docker ps --filter label=com.docker.compose.project=basics --format '  {{.Names}}'

echo "═══ run без --rm оставляет container ═══"
docker compose run -T web true > /dev/null 2>&1
docker ps -a --filter label=com.docker.compose.project=basics --format '  {{.Names}}\t{{.Status}}'

Ожидаемый вывод:

text
═══ exec: в работающем container ═══
PID 1: python3
basics-web-1
═══ run: новый container ═══
PID 1: sh
a7f2c81b9e34
═══ сколько container'ов сейчас ═══
  basics-web-1
═══ run без --rm оставляет container ═══
  basics-web-1	Up 2 minutes
  basics-run-8f3a2c1e	Exited (0) 1 second ago

Различия видны в трёх местах: exec попал в существующий container (hostname совпадает с именем сервиса, PID 1 — python), run создал новый со случайным именем и своим PID 1.

Последний блок — типичная причина накопления мусора: run без --rm оставляет остановленный container после каждого вызова.

Порты у run не публикуются

bash
cd /tmp/comp-basics
echo "═══ run без --service-ports ═══"
docker compose run -d --name run-noports web > /dev/null 2>&1
sleep 2
printf '  docker port: %s\n' "$(docker port run-noports 2>/dev/null || echo 'пусто')"
docker rm -f run-noports > /dev/null 2>&1

echo "═══ run --service-ports ═══"
docker compose run -d --service-ports --name run-ports web > /dev/null 2>&1 || \
    echo "  (порт 18800 занят сервисом web — это ожидаемо)"
docker rm -f run-ports > /dev/null 2>&1

Ожидаемый вывод:

text
═══ run без --service-ports ═══
  docker port: пусто
═══ run --service-ports ═══
  (порт 18800 занят сервисом web — это ожидаемо)

Умолчание объяснимо: run предназначен для разовых задач, и публикация портов вызвала бы конфликт с работающим сервисом — что и произошло во втором блоке.

Orphan containers

bash
cd /tmp/comp-basics
cat > compose.yaml <<'EOF'
name: basics

services:
  web:
    image: python:3.13-slim
    command: ["python", "-m", "http.server", "8000", "--bind", "0.0.0.0"]
  extra:
    image: alpine:3.21
    command: ["sleep", "600"]
EOF
docker compose up -d > /dev/null 2>&1
docker compose ps --format '  {{.Name}}'

echo "═══ удаляем сервис extra из файла ═══"
cat > compose.yaml <<'EOF'
name: basics

services:
  web:
    image: python:3.13-slim
    command: ["python", "-m", "http.server", "8000", "--bind", "0.0.0.0"]
EOF

docker compose up -d 2>&1 | grep -i orphan | sed 's/^/  /'
echo "  container всё ещё работает:"
docker ps --filter label=com.docker.compose.project=basics --format '    {{.Names}}'

echo "═══ уборка ═══"
docker compose up -d --remove-orphans 2>&1 | grep -iE 'removed|orphan' | sed 's/^/  /'
docker ps --filter label=com.docker.compose.project=basics --format '    {{.Names}}'

Ожидаемый вывод:

text
  basics-extra-1
  basics-web-1
═══ удаляем сервис extra из файла ═══
  WARN[0000] Found orphan containers ([basics-extra-1]) for this project. If you
  removed or renamed this service in your compose file, you can run this command
  with the --remove-orphans flag to clean it up.
  container всё ещё работает:
    basics-extra-1
    basics-web-1
═══ уборка ═══
   ✔ Container basics-extra-1  Removed
    basics-web-1

Compose предупреждает, но сам ничего не удаляет — правильное поведение: удаление данных не должно происходить неявно.

Что переживает down

bash
cd /tmp/comp-basics
cat > compose.yaml <<'EOF'
name: basics

services:
  db:
    image: alpine:3.21
    command: ["sh", "-c", "echo данные > /vol/file.txt; sleep 600"]
    volumes:
      - db-data:/vol
      - anon-test:/anon

volumes:
  db-data:
  anon-test:
EOF

docker compose up -d > /dev/null 2>&1
sleep 2

echo "═══ созданные ресурсы ═══"
docker volume ls --filter name=basics --format '  volume: {{.Name}}'
docker network ls --filter name=basics --format '  сеть:   {{.Name}}'

echo "═══ после docker compose down ═══"
docker compose down > /dev/null 2>&1
docker volume ls --filter name=basics --format '  volume: {{.Name}}'
docker network ls --filter name=basics --format '  сеть:   {{.Name}}' || true
printf '  container:ов: %s\n' "$(docker ps -aq --filter label=com.docker.compose.project=basics | wc -l)"

echo "═══ данные на месте ═══"
docker run --rm -v basics_db-data:/v alpine:3.21 cat /v/file.txt | sed 's/^/  /'

echo "═══ после docker compose down -v ═══"
docker compose up -d > /dev/null 2>&1
sleep 1
docker compose down -v > /dev/null 2>&1
docker volume ls --filter name=basics --format '  volume: {{.Name}}'
printf '  volumes осталось: %s\n' "$(docker volume ls -q --filter name=basics | wc -l)"

Ожидаемый вывод:

text
═══ созданные ресурсы ═══
  volume: basics_anon-test
  volume: basics_db-data
  сеть:   basics_default
═══ после docker compose down ═══
  volume: basics_anon-test
  volume: basics_db-data
  container:ов: 0
═══ данные на месте ═══
  данные
═══ после docker compose down -v ═══
  volumes осталось: 0

down убрал container'ы и сеть, но volumes оставил — данные читаются. down -v удалил и их.

Это различие стоит запомнить точно: down безопасен, down -v необратим.

bash
cd /tmp && rm -rf /tmp/comp-basics

Практическое упражнение

Задание. Соберите Compose-проект из трёх сервисов и подтвердите пять утверждений о поведении Compose.

  1. Имена всех ресурсов соответствуют шаблону с именем проекта; имя задано в файле, а не наследуется от каталога.
  2. docker compose config показывает подставленные переменные и развёрнутые пути.
  3. Повторный up без изменений не пересоздаёт container'ы; изменение конфигурации — пересоздаёт.
  4. down сохраняет данные в named volume, down -v — удаляет.
  5. Удаление сервиса из файла оставляет orphan; --remove-orphans его убирает.

Каждое утверждение подтвердите командой, а не рассуждением.

Подсказки

Подсказка 1

Для пункта 1 попробуйте переименовать каталог и посмотреть, изменилось ли имя проекта.

Подсказка 2

Пункт 3 проверяется сравнением ID container'а до и после.

Подсказка 3

Для пункта 4 запишите в volume данные и прочитайте их вспомогательным container'ом после down.

Решение

Показать решение
bash
mkdir -p /tmp/compproof && cd /tmp/compproof

cat > compose.yaml <<'EOF'
name: compproof

services:
  api:
    image: "python:${PY_TAG:-3.13-slim}"
    command:
      - python
      - -c
      - |
        import http.server, os, pathlib
        pathlib.Path('/data/marker.txt').write_text(os.environ.get('MARKER', 'нет') + '\n')
        http.server.HTTPServer(('0.0.0.0', 8000),
                               http.server.SimpleHTTPRequestHandler).serve_forever()
    environment:
      MARKER: "${MARKER:-по умолчанию}"
    ports:
      - "18900:8000"
    volumes:
      - api-data:/data
      - ./local:/local:ro

  cache:
    image: redis:8-alpine
    command: ["redis-server", "--save", "", "--appendonly", "no"]

  extra:
    image: alpine:3.21
    command: ["sleep", "600"]

volumes:
  api-data:
EOF

mkdir -p local && echo "с host" > local/host.txt

fail=0
ok()  { printf '  ✓ %s\n' "$1"; }
bad() { printf '  ✗ %s\n' "$1"; fail=1; }

printf '\n═══ 1. Имена ресурсов ═══\n'
docker compose up -d > /dev/null 2>&1
sleep 3
docker compose ps --format '    {{.Name}}' | sort
docker network ls --filter name=compproof --format '    сеть:   {{.Name}}'
docker volume ls  --filter name=compproof --format '    volume: {{.Name}}'

names_ok=1
for expect in compproof-api-1 compproof-cache-1 compproof-extra-1; do
    docker ps --format '{{.Names}}' | grep -qx "$expect" || names_ok=0
done
docker network ls --format '{{.Name}}' | grep -qx compproof_default || names_ok=0
docker volume ls  --format '{{.Name}}' | grep -qx compproof_api-data || names_ok=0
[ "$names_ok" -eq 1 ] && ok "все имена по шаблону <проект>-<сервис>-<номер>" \
    || bad "имена не совпали с ожидаемыми"

# Имя не зависит от каталога
cd /tmp && mv compproof compproof-renamed && cd compproof-renamed
proj="$(docker compose config --format json 2>/dev/null | python3 -c 'import json,sys; print(json.load(sys.stdin)["name"])')"
printf '    после переименования каталога имя проекта: %s\n' "$proj"
[ "$proj" = "compproof" ] && ok "имя из файла, не из каталога" || bad "имя изменилось вместе с каталогом"

printf '\n═══ 2. config: подстановки и пути ═══\n'
MARKER=из-окружения docker compose config 2>/dev/null | grep -E 'MARKER|source:|image:' | sed 's/^/    /'
MARKER=из-окружения docker compose config 2>/dev/null | grep -q 'MARKER: из-окружения' \
    && ok "переменная подставлена" || bad "переменная не подставилась"
docker compose config 2>/dev/null | grep -q "source: /tmp/compproof-renamed/local" \
    && ok "относительный путь развёрнут в абсолютный" || bad "путь не развернулся"

printf '\n═══ 3. Пересоздание ═══\n'
id_before="$(docker compose ps -q api)"
docker compose up -d 2>&1 | grep -E 'api' | sed 's/^/    /'
id_same="$(docker compose ps -q api)"
[ "$id_before" = "$id_same" ] && ok "без изменений container тот же" || bad "пересоздался без причины"

sed -i 's/MARKER: "\${MARKER:-по умолчанию}"/MARKER: "${MARKER:-изменено}"/' compose.yaml
docker compose up -d 2>&1 | grep -E 'api' | sed 's/^/    /'
id_after="$(docker compose ps -q api)"
[ "$id_before" != "$id_after" ] && ok "после изменения конфигурации пересоздан" \
    || bad "изменение не привело к пересозданию"

printf '\n═══ 4. down против down -v ═══\n'
docker compose exec -T api sh -c 'echo "важные данные" > /data/payload.txt'
docker compose down > /dev/null 2>&1
printf '    volumes после down: %s\n' "$(docker volume ls -q --filter name=compproof | tr '\n' ' ')"
content="$(docker run --rm -v compproof_api-data:/v alpine:3.21 cat /v/payload.txt 2>/dev/null)"
printf '    содержимое: %s\n' "${content:-ПУСТО}"
[ "$content" = "важные данные" ] && ok "down сохранил данные" || bad "down потерял данные"

docker compose up -d > /dev/null 2>&1
sleep 2
docker compose down -v > /dev/null 2>&1
left="$(docker volume ls -q --filter name=compproof | wc -l)"
printf '    volumes после down -v: %s\n' "$left"
[ "$left" -eq 0 ] && ok "down -v удалил volumes" || bad "volumes остались"

printf '\n═══ 5. Orphan containers ═══\n'
docker compose up -d > /dev/null 2>&1
sleep 2
python3 - <<'PY'
import pathlib, re
p = pathlib.Path("compose.yaml")
t = p.read_text()
t = re.sub(r"\n  extra:\n(?:    .*\n)+", "\n", t)
p.write_text(t)
PY
printf '    сервисов в файле: %s\n' "$(docker compose config --services | tr '\n' ' ')"
warn="$(docker compose up -d 2>&1 | grep -ci orphan || true)"
printf '    предупреждений об orphan: %s\n' "$warn"
still="$(docker ps --format '{{.Names}}' | grep -c compproof-extra || true)"
printf '    extra всё ещё работает: %s\n' "$still"
[ "$warn" -ge 1 ] && [ "$still" -eq 1 ] && ok "orphan обнаружен, но не удалён автоматически" \
    || bad "поведение не совпало с ожидаемым"

docker compose up -d --remove-orphans > /dev/null 2>&1
gone="$(docker ps -a --format '{{.Names}}' | grep -c compproof-extra || true)"
printf '    после --remove-orphans осталось: %s\n' "$gone"
[ "$gone" -eq 0 ] && ok "--remove-orphans убрал container" || bad "container остался"

printf '\n═══ ИТОГ ═══\n'
[ "$fail" -eq 0 ] && echo "  все пять утверждений подтверждены" || echo "  ЕСТЬ ПРОВАЛЫ"

docker compose down -v --remove-orphans > /dev/null 2>&1
cd /tmp && rm -rf /tmp/compproof-renamed
exit "$fail"

Ожидаемый вывод:

text
═══ 1. Имена ресурсов ═══
    compproof-api-1
    compproof-cache-1
    compproof-extra-1
    сеть:   compproof_default
    volume: compproof_api-data
  ✓ все имена по шаблону <проект>-<сервис>-<номер>
    после переименования каталога имя проекта: compproof
  ✓ имя из файла, не из каталога

═══ 2. config: подстановки и пути ═══
      image: python:3.13-slim
        MARKER: из-окружения
        source: /tmp/compproof-renamed/local
  ✓ переменная подставлена
  ✓ относительный путь развёрнут в абсолютный

═══ 3. Пересоздание ═══
     ✔ Container compproof-api-1  Running
  ✓ без изменений container тот же
     ✔ Container compproof-api-1  Recreated
  ✓ после изменения конфигурации пересоздан

═══ 4. down против down -v ═══
    volumes после down: compproof_api-data 
    содержимое: важные данные
  ✓ down сохранил данные
    volumes после down -v: 0
  ✓ down -v удалил volumes

═══ 5. Orphan containers ═══
    сервисов в файле: api cache 
    предупреждений об orphan: 1
    extra всё ещё работает: 1
  ✓ orphan обнаружен, но не удалён автоматически
    после --remove-orphans осталось: 0
  ✓ --remove-orphans убрал container

═══ ИТОГ ═══
  все пять утверждений подтверждены

Все пять утверждений подтверждены.

Три решения, определяющие качество.

Пункт 1 проверяется переименованием каталога. Совпадение имён с шаблоном ничего не говорит об их источнике: при каталоге compproof имя проекта было бы тем же и без ключа name:. Только переименование разделяет две гипотезы — и подтверждает, что имя действительно берётся из файла.

Пункт 3 проверяет обе стороны: и что пересоздания не происходит, и что оно происходит. Проверка только второй половины прошла бы и для реализации, которая пересоздаёт container при каждом up, — а это совсем другое поведение с другими последствиями для времени простоя.

Пункт 4 читает данные вспомогательным container'ом, а не сервисом Compose. После down сервиса не существует, и запустить его заново означало бы проверять уже другой container. Отдельный docker run -v compproof_api-data:/v обращается прямо к volume и отвечает на заданный вопрос: пережили ли данные удаление стека.

Чего решение не делает. Оно не проверяет поведение при нескольких Compose-файлах и профилях — там правила слияния и активации меняют и состав сервисов, и появление orphan'ов (урок 9.6). Не покрыт и случай, когда два проекта работают одновременно: изоляция по имени проекта здесь предполагается, но не проверяется запуском второго стека с теми же именами сервисов.

Проверка результата

bash
mkdir -p /tmp/cc && cd /tmp/cc
cat > compose.yaml <<'EOF'
name: cc
services:
  a:
    image: alpine:3.21
    command: ["sleep", "60"]
EOF
docker compose up -d
docker compose ps --format '{{.Name}}'
docker compose config --services
docker compose down
cd /tmp && rm -rf /tmp/cc

Ожидается имя cc-a-1 и сервис a в выводе config.

Типичные ошибки

ОшибкаПричинаИсправление
Ключ version в файлеСтарые примерыУстарел; вызывает предупреждение
docker-compose вместо docker composeПривычкаv1 снята с поддержки
Имя проекта не заданоРаботает по умолчаниюЗависит от имени каталога; задать name:
COMPOSE_PROJECT_NAME в расчёте на приоритетЛогично предположитьКлюч name: в файле сильнее
run вместо exec для отладкиПохожие командыСоздаётся новый container
run без --rmЗабыли флагОстановленные container'ы копятся
Ожидают публикацию портов у runПо аналогии с upНужен --service-ports
down -v для перезапуска стекаКажется «более чистым»Удаляет данные; достаточно down
Не читают вывод config при проблемахНе знают о командеПоказывает итоговую конфигурацию
Игнорируют предупреждение об orphanСтек работаетЛишние container'ы держат порты и память

Контрольные вопросы

На понимание:

  1. Почему ключ version больше не нужен и что происходит, если он есть?
  2. Как Compose именует container'ы, сети и volumes?
  3. Каков приоритет четырёх способов задать имя проекта?
  4. Чем exec отличается от run? Назовите три различия.
  5. Что удаляет down, а что только down -v?

На применение:

  1. Как посмотреть итоговую конфигурацию после подстановки переменных?
  2. Как сделать так, чтобы имя проекта не зависело от имени каталога?
  3. Как убрать container сервиса, удалённого из файла?

На диагностику:

  1. После переименования каталога docker compose ps показывает пустой список. Что произошло?
  2. up пересоздаёт container при каждом запуске. Где искать причину?

Краткое резюме

  1. Действует единая Compose Specification; ключ version устарел и игнорируется.
  2. Предпочтительное имя файла — compose.yaml.
  3. Обязателен только раздел services; сеть по умолчанию создаётся без объявления.
  4. Имя проекта — префикс имён всех ресурсов и основа изоляции проектов.
  5. Приоритет: -pname: в файле → COMPOSE_PROJECT_NAME → имя каталога.
  6. Умолчание по имени каталога означает, что переименование «теряет» проект.
  7. Container'ы именуются через дефис, сети и volumes — через подчёркивание.
  8. docker compose config показывает итоговую конфигурацию и решает большинство вопросов «почему не работает».
  9. exec работает в существующем container'е, run создаёт новый без публикации портов.
  10. up пересоздаёт container только при изменении хеша конфигурации.
  11. down сохраняет объявленные volumes; down -v удаляет их безвозвратно.
  12. Orphan — container удалённого из файла сервиса; убирается флагом --remove-orphans.

Официальные источники

ИсточникСсылкаЧто подтверждает
Compose Specificationhttps://docs.docker.com/reference/compose-file/Формат файла, статус version
Compose: overviewhttps://docs.docker.com/compose/Назначение, отличие от Engine
Compose: project namehttps://docs.docker.com/compose/how-tos/project-name/Четыре способа и их приоритет
Compose: CLI referencehttps://docs.docker.com/reference/cli/docker/compose/Команды и флаги
Compose: confighttps://docs.docker.com/reference/cli/docker/compose/config/Итоговая конфигурация
Compose: downhttps://docs.docker.com/reference/cli/docker/compose/down/Что удаляется, флаг -v
Compose: migrate to v2https://docs.docker.com/compose/releases/migrate/Статус Compose v1

Навигация

← Вернуться к разделу
Следующий материал → Справочник по services
Главное оглавление

Markdown на GitHub ↗