9.1. Основы Compose
Цели
После этого материала вы сможете:
- написать
compose.yamlпо актуальной спецификации и объяснить, почему ключversionбольше не нужен; - предсказать имена container'ов, сетей и volumes, которые создаст Compose;
- задать имя проекта четырьмя способами и назвать их приоритет;
- использовать
docker compose configкак основной инструмент диагностики; - различать
up,runиexecи выбирать нужное; - объяснить, что такое orphan containers и откуда они берутся.
Предварительные знания
- 7.2. Volumes;
- 8.2. Bridge networks;
- базовое понимание YAML: отображения, списки, отступы.
Ключевые термины
| Термин | Объяснение |
|---|---|
Compose Specification | Актуальный формат файла, объединивший версии 2.x и 3.x |
project | Набор ресурсов Compose с общим префиксом имени |
service | Описание того, как запускать container'ы одного типа |
orphan | Container проекта, которого больше нет в файле |
interpolation | Подстановка переменных вида ${VAR} в значения |
Теория
Что такое Compose Specification
Исторически существовали форматы 1, 2.x и 3.x, различавшиеся набором ключей. Ключ version в начале файла выбирал формат.
Сейчас действует единая Compose Specification, вобравшая возможности всех версий. Ключ version объявлен устаревшим:
version: "3.8" # УСТАРЕЛО: игнорируется, вызывает предупреждение
services:
...
| Утверждение | Статус |
|---|---|
version обязателен | Неверно с 2020 года |
version влияет на поведение | Не влияет — игнорируется |
| Его наличие безвредно | Вызывает предупреждение при каждом запуске |
| Ключи 3.x работают без него | Да |
Просто не пишите version. Файл начинается со services.
Отдельно: docker-compose (Python, версия 1) снят с поддержки. Курс использует только docker compose — плагин Docker CLI.
Имена файлов
Compose ищет в текущем каталоге в таком порядке:
| Приоритет | Имя |
|---|---|
| 1 | compose.yaml |
| 2 | compose.yml |
| 3 | docker-compose.yaml |
| 4 | docker-compose.yml |
Предпочтительное имя — compose.yaml: оно рекомендовано спецификацией. Старые имена поддерживаются для совместимости.
Найдя compose.yaml, Compose дополнительно подхватывает compose.override.yaml, если он есть (урок 9.6).
Структура файла
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 |
| Сеть по умолчанию | <проект>_default | myapp_default |
| Объявленная сеть | <проект>_<имя> | myapp_backend |
| Volume | <проект>_<имя> | myapp_db-data |
| Метка проекта | com.docker.compose.project | myapp |
Обратите внимание на разделители: в именах 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'ах проекта |
Ключевое различие, которое путают чаще всего:
exec | run | |
|---|---|---|
| Использует работающий container | Да | Нет, создаёт новый |
| Требует, чтобы сервис был запущен | Да | Нет |
| Публикует порты | — | Нет (без --service-ports) |
| Запускает зависимости | — | Да (без --no-deps) |
| Удаляется после выхода | — | Только с --rm |
Для отладки работающего сервиса нужен exec. Для разовой задачи — миграции, команды управления — run --rm.
docker compose config — главный инструмент диагностики
Команда печатает конфигурацию после слияния файлов, подстановки переменных и применения умолчаний. Это то, что Compose собирается выполнить.
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 о нём больше не знает, но метка проекта на нём осталась.
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
| Ресурс | down | down -v |
|---|---|---|
| Container'ы | Удаляет | Удаляет |
| Сети проекта | Удаляет | Удаляет |
| Объявленные volumes | Оставляет | Удаляет |
| Anonymous volumes | Удаляет | Удаляет |
Внешние volumes (external: true) | Оставляет | Оставляет |
| Образы | Оставляет | Оставляет (нужен --rmi) |
Третья строка — то, что делает down безопасным для данных: перезапуск стека не уничтожает базу. И она же объясняет, почему down -v требует осторожности (урок 7.6).
Внутренний механизм
Как Compose узнаёт свои ресурсы
Каждый созданный объект получает метки:
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.
Команды и примеры
Минимальный файл
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}}'
Ожидаемый вывод:
[+] 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:.
Проверим, что сервис работает и сеть создана:
curl -s -o /dev/null -w 'HTTP %{http_code}\n' http://localhost:18800/
docker network ls --filter name=basics --format ' {{.Name}} ({{.Driver}})'
Ожидаемый вывод:
HTTP 200
basics_default (bridge)
Имя проекта и его источники
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
Ожидаемый вывод:
═══ из ключа name: ═══
basics
═══ переменная окружения перекрывает файл? ═══
basics
═══ флаг -p перекрывает всё ═══
fromflag
═══ без name: — имя каталога ═══
sub
Второй блок показывает важное: ключ name: в файле сильнее переменной окружения. Порядок приоритета не совпадает с интуицией «окружение всегда главнее».
Четвёртый блок демонстрирует умолчание — имя каталога. Именно поэтому переименование каталога «теряет» проект.
docker compose config раскрывает всё
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/^/ /'
Ожидаемый вывод:
═══ без 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 не пересоздаёт то, что не менялось
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
Ожидаемый вывод:
первый запуск: a3f8c91e2d47
═══ повторный up без изменений ═══
✔ Container basics-web-1 Running
тот же container: да
═══ меняем command ═══
✔ Container basics-web-1 Recreated
тот же container: нет
метка с хешем конфигурации:
7b2e9f1a4c8d3e6b0f5a2c9d1e8b4f7a
Статус в выводе показывает решение Compose: Running — ничего не делал, Recreated — пересоздал. Основание — хеш конфигурации в метке.
exec против run
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}}'
Ожидаемый вывод:
═══ 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 не публикуются
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
Ожидаемый вывод:
═══ run без --service-ports ═══
docker port: пусто
═══ run --service-ports ═══
(порт 18800 занят сервисом web — это ожидаемо)
Умолчание объяснимо: run предназначен для разовых задач, и публикация портов вызвала бы конфликт с работающим сервисом — что и произошло во втором блоке.
Orphan containers
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}}'
Ожидаемый вывод:
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
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)"
Ожидаемый вывод:
═══ созданные ресурсы ═══
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 необратим.
cd /tmp && rm -rf /tmp/comp-basics
Практическое упражнение
Задание. Соберите Compose-проект из трёх сервисов и подтвердите пять утверждений о поведении Compose.
- Имена всех ресурсов соответствуют шаблону с именем проекта; имя задано в файле, а не наследуется от каталога.
docker compose configпоказывает подставленные переменные и развёрнутые пути.- Повторный
upбез изменений не пересоздаёт container'ы; изменение конфигурации — пересоздаёт. downсохраняет данные в named volume,down -v— удаляет.- Удаление сервиса из файла оставляет orphan;
--remove-orphansего убирает.
Каждое утверждение подтвердите командой, а не рассуждением.
Подсказки
Подсказка 1
Для пункта 1 попробуйте переименовать каталог и посмотреть, изменилось ли имя проекта.
Подсказка 2
Пункт 3 проверяется сравнением ID container'а до и после.
Подсказка 3
Для пункта 4 запишите в volume данные и прочитайте их вспомогательным container'ом после down.
Решение
Показать решение
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"
Ожидаемый вывод:
═══ 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). Не покрыт и случай, когда два проекта работают одновременно: изоляция по имени проекта здесь предполагается, но не проверяется запуском второго стека с теми же именами сервисов.
Проверка результата
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'ы держат порты и память |
Контрольные вопросы
На понимание:
- Почему ключ
versionбольше не нужен и что происходит, если он есть? - Как Compose именует container'ы, сети и volumes?
- Каков приоритет четырёх способов задать имя проекта?
- Чем
execотличается отrun? Назовите три различия. - Что удаляет
down, а что толькоdown -v?
На применение:
- Как посмотреть итоговую конфигурацию после подстановки переменных?
- Как сделать так, чтобы имя проекта не зависело от имени каталога?
- Как убрать container сервиса, удалённого из файла?
На диагностику:
- После переименования каталога
docker compose psпоказывает пустой список. Что произошло? upпересоздаёт container при каждом запуске. Где искать причину?
Краткое резюме
- Действует единая Compose Specification; ключ
versionустарел и игнорируется. - Предпочтительное имя файла —
compose.yaml. - Обязателен только раздел
services; сеть по умолчанию создаётся без объявления. - Имя проекта — префикс имён всех ресурсов и основа изоляции проектов.
- Приоритет:
-p→name:в файле →COMPOSE_PROJECT_NAME→ имя каталога. - Умолчание по имени каталога означает, что переименование «теряет» проект.
- Container'ы именуются через дефис, сети и volumes — через подчёркивание.
docker compose configпоказывает итоговую конфигурацию и решает большинство вопросов «почему не работает».execработает в существующем container'е,runсоздаёт новый без публикации портов.upпересоздаёт container только при изменении хеша конфигурации.downсохраняет объявленные volumes;down -vудаляет их безвозвратно.- Orphan — container удалённого из файла сервиса; убирается флагом
--remove-orphans.
Официальные источники
| Источник | Ссылка | Что подтверждает |
|---|---|---|
| Compose Specification | https://docs.docker.com/reference/compose-file/ | Формат файла, статус version |
| Compose: overview | https://docs.docker.com/compose/ | Назначение, отличие от Engine |
| Compose: project name | https://docs.docker.com/compose/how-tos/project-name/ | Четыре способа и их приоритет |
| Compose: CLI reference | https://docs.docker.com/reference/cli/docker/compose/ | Команды и флаги |
Compose: config | https://docs.docker.com/reference/cli/docker/compose/config/ | Итоговая конфигурация |
Compose: down | https://docs.docker.com/reference/cli/docker/compose/down/ | Что удаляется, флаг -v |
| Compose: migrate to v2 | https://docs.docker.com/compose/releases/migrate/ | Статус Compose v1 |
Навигация
← Вернуться к разделу
Следующий материал → Справочник по services
Главное оглавление