Раздел 9. Практические задания
Задания выполняются в реальной системе. Разбор открывайте только после самостоятельной попытки.
Обозначения: [обяз.] — обязательное, [доп.] — дополнительное, [★] — повышенной сложности, [диаг.] — диагностическое.
Подготовка:
mkdir -p ~/docker-course/09-compose && cd ~/docker-course/09-compose
# docker pull принимает РОВНО один образ:
# `docker pull a b` отвечает «docker pull requires 1 argument»
for img in python:3.13-slim postgres:17-alpine redis:8-alpine; do docker pull -q "$img"; done
docker compose version
docker network ls && docker volume ls | head -3 # зафиксируйте состояние
Задание 1. Первый Compose-файл [обяз.]
Постановка. Напишите compose.yaml из двух сервисов: HTTP-приложение и Redis. Требуется:
- Имя проекта задано в файле, а не наследуется от каталога.
- Приложение доступно с host по опубликованному порту.
- Приложение обращается к Redis по имени сервиса.
- Ключ
versionотсутствует.
Ожидаемый результат. Работающий стек и вывод docker compose ps с именами по шаблону проекта.
Проверка:
docker compose ps --format 'table {{.Name}}\t{{.Service}}\t{{.Status}}'
docker compose config --format json | python3 -c 'import json,sys; print(json.load(sys.stdin)["name"])'
Разбор — в уроке 9.1.
Задание 2. Имя проекта и изоляция [обяз.]
Постановка. Покажите четыре вещи:
- Имя проекта по умолчанию равно имени каталога.
- Ключ
name:его переопределяет. - Флаг
-pпереопределяет и ключ. - Два проекта с одинаковыми именами сервисов работают одновременно и не конфликтуют.
Дополнительно объясните, почему COMPOSE_PROJECT_NAME слабее ключа name:.
Ожидаемый результат. Четыре наблюдения и объяснение приоритета.
Проверка:
docker ps --format '{{.Names}}' | sort
docker network ls --format '{{.Name}}' | grep -c default
Разбор — в уроке 9.1.
Задание 3. exec против run [обяз.]
Постановка. На работающем сервисе покажите:
execвыполняется в существующем container'е — совпадаетhostname.runсоздаёт новый container со своим PID 1.runне публикует порты без--service-ports.runбез--rmоставляет остановленный container.
Ожидаемый результат. Четыре различия, подтверждённые выводом команд.
Проверка:
docker ps -a --filter label=com.docker.compose.project=<проект> --format '{{.Names}}\t{{.Status}}'
Разбор — в уроке 9.1.
Задание 4. Изоляция сетей [обяз.]
Постановка. Постройте три сервиса — proxy, api, db — и две сети так, чтобы:
proxyиapiвидели друг друга;apiиdbвидели друг друга;proxyне виделdb— ни по имени, ни по адресу;dbне имела выхода в интернет.
Различайте gaierror и таймаут в проверках.
Ожидаемый результат. Карта сетей и четыре подтверждённых утверждения.
Проверка:
docker inspect <container> --format '{{range $n, $v := .NetworkSettings.Networks}}{{$n}} {{end}}'
docker network inspect <проект>_<сеть> --format '{{.Internal}}'
Разбор — в уроке 9.3.
Задание 5. Healthcheck и порядок запуска [обяз.]
Постановка. Воспроизведите и устраните гонку при старте:
- Сервис с
depends_on: [db]без условия падает сECONNREFUSED. - Измерьте, сколько секунд база реально поднимается.
- Добавьте healthcheck со
start_periodи условиеservice_healthy. - Покажите, что теперь соединение устанавливается с первой попытки.
- Покажите, что без
start_periodта же база успевает получитьunhealthy.
Ожидаемый результат. Пять наблюдений; пункт 5 — на втором экземпляре той же базы.
Проверка:
docker inspect <container> --format '{{.State.Health.Status}} {{.State.Health.FailingStreak}}'
Разбор — в уроке 9.4.
Задание 6. Миграции как отдельный сервис [доп.]
Постановка. Реализуйте паттерн: db → migrate → api. Требуется показать:
migrateстартует только послеservice_healthyу базы;apiстартует только послеservice_completed_successfullyуmigrate;- при провале миграции container
apiне создаётся вовсе; - повторный
upвыполняет миграции снова, но схема не ломается.
Ожидаемый результат. Четыре подтверждения; пункт 3 — подсчётом container'ов, а не только кодом возврата.
Проверка:
docker ps -a --filter label=com.docker.compose.service=api -q | wc -l
Разбор — в уроке 9.4.
Задание 7. Секреты вместо переменных [доп.]
Постановка. Передайте пароль базе двумя способами и сравните:
- через
environment— найдите пять мест, где он виден; - через Compose secret — покажите, что ни в одном из них его нет;
- используйте соглашение
_FILEу официального образаpostgres; - реализуйте поддержку
_FILEв своём приложении.
Ожидаемый результат. Таблица утечек и рабочая конфигурация без них.
Проверка:
docker inspect <container> --format '{{json .Config.Env}}' | grep -c "<пароль>"
docker compose exec <сервис> ls -l /run/secrets/
Разбор — в уроке 9.5.
Задание 8. Три окружения из одной базы [доп.]
Постановка. Соберите compose.yaml, compose.override.yaml и compose.prod.yaml так, чтобы:
- разработка подхватывалась автоматически: bind mount, отладочный порт;
- production снимал публикацию и bind mount, добавлял лимиты и реплики;
compose.yamlне содержал настроек, специфичных для окружения;- общие фрагменты задавались один раз.
Покажите, почему для пункта 2 нужны теги !reset и !override.
Ожидаемый результат. Три конфигурации и демонстрация того, что без тегов порты складываются.
Проверка:
docker compose -f compose.yaml -f compose.prod.yaml config | grep -c published
Разбор — в уроке 9.6.
Задание 9. Диагностика: стек не поднимается [диаг.]
Постановка. Дан файл:
version: "3.8"
services:
web:
build: .
container_name: web
ports:
- 8000:8000
environment:
- DATABASE_URL=postgresql://postgres:secret@localhost:5432/appdb
- REDIS_URL=redis://cache:6379/0
depends_on:
- db
networks:
- frontend
db:
image: postgres:17-alpine
environment:
POSTGRES_PASSWORD: secret
volumes:
- ./pgdata:/var/lib/postgresql/data
cache:
image: redis:8-alpine
networks:
- frontend
networks:
frontend:
Приложение не запускается, а после docker compose down данные базы исчезают. Найдите все дефекты, объясните каждый и предложите исправленный файл.
Ожидаемый результат. Список дефектов с доказательством каждого и рабочая конфигурация.
Разбор
В этом файле семь независимых дефектов. Разберём по порядку срабатывания.
Дефект 1. Ключ version устарел.
cd ~/docker-course/09-compose && mkdir -p diag9 && cd diag9
# (файл из условия сохранён как compose.yaml)
docker compose config 2>&1 | grep -i 'version' | head -2 | sed 's/^/ /'
Ожидаемый вывод:
WARN[0000] /путь/к/проекту/09-compose/diag9/compose.yaml: the attribute
`version` is obsolete, it will be ignored, please remove it to avoid potential confusion
Не ошибка, но предупреждение при каждом запуске. Удалить (урок 9.1).
Дефект 2. localhost в DATABASE_URL.
Внутри container'а localhost — это сам container, а не db. Симптом: ECONNREFUSED мгновенно.
docker compose run --rm -T web python -c "
import socket
s = socket.socket(); s.settimeout(2)
try:
s.connect(('localhost', 5432)); print(' ok')
except OSError as e:
print(f' {type(e).__name__}: localhost — это сам container')
" 2>/dev/null
Ожидаемый вывод:
ConnectionRefusedError: localhost — это сам container
Исправление: @db:5432 (урок 8.1).
Дефект 3. web и db в разных сетях.
web объявил networks: [frontend], db не объявил ничего и остался в default. Связи между ними нет.
docker compose up -d > /dev/null 2>&1
docker network ls --filter name=diag9 --format ' сеть: {{.Name}}'
for s in web db cache; do
printf ' %-6s %s\n' "$s" \
"$(docker inspect "$(docker compose ps -q $s 2>/dev/null)" \
--format '{{range $n, $v := .NetworkSettings.Networks}}{{$n}} {{end}}' 2>/dev/null)"
done
Ожидаемый вывод:
сеть: diag9_default
сеть: diag9_frontend
web diag9_frontend
db diag9_default
cache diag9_frontend
Две сети вместо одной, db в стороне. Правило: либо сети объявляют все сервисы, либо ни один (урок 9.3).
Дефект 4. depends_on без условия.
Даже после исправления сетей приложение стартует раньше, чем PostgreSQL примет соединения. Нужны healthcheck и condition: service_healthy (урок 9.4).
Дефект 5. Bind mount для каталога данных базы.
ls -ldn ./pgdata 2>/dev/null | awk '{print " владелец каталога: " $3 ":" $4}'
docker compose run --rm -T db id 2>/dev/null | sed 's/^/ процесс в container: /'
Ожидаемый вывод:
владелец каталога: 1000:1000
процесс в container: uid=70(postgres) gid=70(postgres) groups=70(postgres)
UID не совпадают — PostgreSQL не сможет писать в каталог (урок 7.5). Named volume наследует владельца из образа и этой проблемы не создаёт.
Он же — причина второй жалобы: данные «исчезают». На самом деле они остаются в ./pgdata, но при следующем запуске база не может их прочитать либо инициализируется заново.
Дефект 6. container_name: web.
docker compose up -d --scale web=2 2>&1 | tail -2 | sed 's/^/ /'
Ожидаемый вывод:
Error response from daemon: Conflict. The container name "/web" is already
in use by container "..."
Масштабирование невозможно, и два проекта на одной машине конфликтуют (урок 9.2).
Дефект 7. Пароль в открытом виде и порт без кавычек.
POSTGRES_PASSWORD: secret попадёт в docker inspect и в репозиторий. Запись - 8000:8000 без кавычек противоречит рекомендации документации.
Исправленный файл:
name: diag9
services:
web:
build: .
ports:
- "127.0.0.1:8000:8000"
environment:
DATABASE_URL: "postgresql://postgres@db:5432/appdb"
DATABASE_PASSWORD_FILE: /run/secrets/db_password
REDIS_URL: "redis://cache:6379/0"
secrets:
- db_password
networks: [backend]
depends_on:
db:
condition: service_healthy
cache:
condition: service_healthy
db:
image: postgres:17-alpine
environment:
POSTGRES_DB: appdb
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
networks: [backend]
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d appdb"]
interval: 3s
timeout: 3s
retries: 10
start_period: 30s
start_interval: 1s
cache:
image: redis:8-alpine
networks: [backend]
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 3s
retries: 10
start_period: 10s
networks:
backend:
volumes:
db-data:
secrets:
db_password:
file: ./secrets/db_password.txt
Проверка исправления:
mkdir -p secrets && head -c 24 /dev/urandom | base64 | tr -d '\n' > secrets/db_password.txt
docker compose up -d > /dev/null 2>&1
sleep 25
docker compose ps --format 'table {{.Service}}\t{{.Status}}' | sed 's/^/ /'
docker compose exec -T web python -c "
import socket
print(' web → db:', socket.gethostbyname('db'))
print(' web → cache:', socket.gethostbyname('cache'))
" 2>/dev/null
docker compose exec -T db psql -U postgres -d appdb -q -c 'CREATE TABLE t (id int); INSERT INTO t VALUES (1);'
docker compose down > /dev/null 2>&1
docker compose up -d > /dev/null 2>&1
sleep 25
printf ' записей после down/up: %s\n' \
"$(docker compose exec -T db psql -U postgres -d appdb -tAc 'SELECT count(*) FROM t' | tr -d ' \r')"
docker compose down -v > /dev/null 2>&1
Ожидаемый вывод:
SERVICE STATUS
cache Up 25 seconds (healthy)
db Up 25 seconds (healthy)
web Up 12 seconds
web → db: 172.33.0.3
web → cache: 172.33.0.2
записей после down/up: 1
Данные пережили down — это и было второй жалобой.
Сводка дефектов:
| № | Дефект | Симптом | Исправление |
|---|---|---|---|
| 1 | Ключ version | Предупреждение | Удалить |
| 2 | localhost вместо db | ECONNREFUSED мгновенно | Имя сервиса |
| 3 | Сети объявлены части сервисов | gaierror | Объявить всем или никому |
| 4 | depends_on без условия | Падение через раз | service_healthy плюс healthcheck |
| 5 | Bind mount для данных базы | Права, потеря данных | Named volume |
| 6 | container_name | Нет масштабирования | Убрать |
| 7 | Пароль в открытом виде | Утечка в inspect | Compose secret |
Обратите внимание: дефекты 2 и 3 дают разные ошибки — ECONNREFUSED и gaierror. Различение типа отказа сразу указывает, какой из них перед вами (урок 8.6).
Задание 10. Полный production-stack [★]
Постановка. Соберите систему из пяти сервисов и подтвердите девять свойств.
Состав: api (HTTP), worker (очередь), migrate (одноразовая задача), db, cache.
Требования:
- Один образ обслуживает
api,workerиmigrate; различаются командой. - Цепочка запуска:
dbhealthy →migratecompleted →apiиworker. - Две сети;
dbиcacheнедоступны из внешней сети и не имеют выхода в интернет. - Пароль передаётся secret'ом; в
docker inspectего нет. HEALTHCHECKпроверяет liveness; остановка базы не делаетapiнездоровым.- Полный путь работает: HTTP → очередь → worker → база → HTTP.
workerзавершается штатно с кодом0при остановке во время задачи.- Три окружения из одной базы; в production нет публикации и bind mount.
- Профиль с опциональным сервисом, не запускающимся по умолчанию.
Скрипт проверки возвращает ненулевой код при любом расхождении.
Подсказки
Подсказка 1
Требование 1 удобнее выполнить якорем &app-service с переопределением command.
Подсказка 2
Для требования 5 нужны два разных endpoint'а: /healthz и /readyz.
Подсказка 3
Требование 7 требует stop_grace_period больше длительности задачи.
Подсказка 4
В production для снятия публикации нужен ports: !reset [], обычное переопределение списки складывает.
Решение
Показать решение
Готовая реализация — resources/examples/compose-stack/. Разберите её файлы, затем воспроизведите проверку:
cd resources/examples/compose-stack
make up
./check.sh
echo "КОД: $?"
Ожидаемый вывод:
═══ 1. Миграции выполнились до старта приложения ═══
код migrate: 0
✓ migrate завершился успешно
═══ 2. Изоляция сетей ═══
✓ api → db
✓ worker → db
✓ api → cache
✓ backend изолирована (internal)
✓ у db нет маршрута наружу
═══ 3. Пароль не утекает в метаданные ═══
вхождений пароля в docker inspect: 0
✓ пароля нет в переменных окружения
═══ 4. Приложение работает ═══
✓ обе пробы отвечают
═══ 5. Очередь: api ставит задачу, worker обрабатывает ═══
результатов в базе: 0 → 3
✓ worker обработал задачи и записал в базу
═══ 6. Liveness не зависит от базы ═══
база остановлена: /healthz=200 /readyz=503 health=healthy
✓ api остался healthy — каскадного отказа нет
✓ readiness честно сообщает о деградации
✓ работа восстановилась без перезапуска api
═══ 7. Graceful shutdown worker'а ═══
остановка заняла 3.4 c, код выхода 0
✓ worker завершился штатно, а не по SIGKILL
═══ 8. Production-конфигурация отличается правильно ═══
api в production: ports=0 volumes=0 replicas=2 read_only=True
✓ публикация снята тегом !reset
✓ bind mount убран тегом !override
✓ реплики и read-only заданы
═══ 9. Профиль tools не активен по умолчанию ═══
✓ tools не запускается по умолчанию
✓ профиль включает tools
═══ ИТОГ ═══
все проверки пройдены
КОД: 0
Разбор каждого решения — в уроке 9.7.
Три места, где чаще всего ошибаются при самостоятельной реализации.
stop_grace_period у worker. Значение по умолчанию — 10 секунд. Задача, идущая 30 секунд, будет оборвана SIGKILL, и требование 7 не выполнится. Симптом: код выхода 137 вместо 0. Число берётся из максимальной длительности задачи плюс запас, а не из головы.
HEALTHCHECK на /readyz вместо /healthz. Конфигурация будет работать и выглядеть правильнее — до первой остановки базы. Требование 5 проверяет именно этот случай, и оно провалится: api станет unhealthy, хотя процесс жив и отвечает.
Отсутствие !reset у ports в production. Обычное переопределение списки складывает, а не заменяет. Требование 8 покажет ports=1 вместо ports=0, и production окажется с публикацией, которую вы считали снятой.
Что стоит сделать сверх задания. Добавьте в check.sh десятую группу: измерьте пиковое потребление памяти под нагрузкой и сравните с заданными лимитами. Это превратит лимиты из скопированных чисел в обоснованные (урок 6.13).
Очистка после раздела
docker compose down -v --remove-orphans 2>/dev/null || true
# ВНИМАНИЕ: НЕ `docker ps -aq | xargs -r docker rm -f`.
# Такая строка удаляет ВСЕ container'ы на машине, включая чужие:
# базу коллеги, кластер kind, работающий стенд. Удаляем только
# созданные из образов этого раздела.
for img in python:3.13-slim postgres:17-alpine redis:8-alpine; do
docker ps -aq --filter "ancestor=$img" | xargs -r docker rm -f
done
docker network prune -f
docker volume prune -f
docker image prune -f
docker network ls && docker volume ls | head -3
Сравните с состоянием, зафиксированным в начале раздела.
Вторая строка удаляет все container'ы на машине, третья и четвёртая — все неиспользуемые сети и volumes. На рабочей машине выполняйте выборочно (урок 7.6).
Критерии завершения
Раздел закрыт, когда выполнены обязательные задания 1–5 и вы можете без подсказок ответить на вопросы из MAIN.md раздела.
Дальше: Quiz 09, затем Проект 3. Multiservice stack.
Навигация
← Предыдущий материал
Вернуться к разделу
Следующий раздел → Development workflow
Главное оглавление