Scale, resources, restart_policy
В реальной работе compose-стенда часто нужны три вещи: запустить N копий одного worker’а (для параллелизма), ограничить ресурсы каждого контейнера (чтобы один не съел всю память), и настроить restart-policy (чтобы упавший контейнер сам поднимался). В этом уроке закрываем эти три темы. После него у тебя есть полный compose-toolkit для compose-стенда production-уровня.
—scale: несколько копий одного сервиса
services:
worker:
image: myorg/celery-worker
depends_on:
- redis
environment:
CELERY_BROKER_URL: redis://redis:6379/0
docker compose up -d --scale worker=3
Compose создаст три контейнера: myproj-worker-1, myproj-worker-2, myproj-worker-3. Все три из одного образа, с одной конфигурацией. Caждый берёт задачи из очереди Redis независимо.
Можно зафиксировать в compose:
services:
worker:
image: myorg/celery-worker
deploy:
replicas: 3
Но в compose (non-swarm) replicas работает как hint — --scale его перебивает. В swarm-режиме replicas — авторитет.
Что НЕ масштабируется
--scale работает только для stateless сервисов. Не пробуй:
docker compose up --scale postgres=3
# Контейнеры создадутся, но все попытаются забиндить 5432 на хост — два упадут.
# Они все смонтируют один volume — конфликт.
Stateful сервисы (БД, очереди, координаторы) требуют другой подход — кластеризацию (Postgres-Citus, MinIO distributed, Kafka brokers), а это компетенция уже не базового compose.
Условия для скейлинга
- Сервис не должен публиковать порт фиксированно (
-p 8000:8000сломает, потому что hosts:8000 один). Альтернатива —ports: ["8000-8010:8000"](диапазон) или без публикации. - Сервис не должен зависеть от уникального имени hostname.
- Должна быть очередь или балансировщик, чтобы workers распределяли работу.
services:
web:
image: myorg/api
ports:
- "8000-8010:8000" # range — каждая копия получает свой порт хоста
worker:
image: myorg/celery-worker
# без ports — стандартный stateless паттерн
deploy.resources: лимиты и резервации
services:
app:
image: myapp
deploy:
resources:
limits:
cpus: '0.5'
memory: 512M
reservations:
cpus: '0.25'
memory: 256M
- limits — максимум, который контейнер может использовать. Если попытается больше — CPU будет throttled, память — OOM kill.
- reservations — минимум гарантированных ресурсов. Если хост не имеет столько свободно — контейнер не стартует.
CPU
cpus: '0.5' — половина одного CPU-core. cpus: '2' — два полных core. На уровне ядра реализуется через cgroups CPU quota.
Memory
memory: 512M — 512 мегабайт. 512m, 512MB, 512MiB — тоже понимается. Поддерживаются K, M, G суффиксы.
Где это работает
- В swarm-mode
deploy.resources— авторитетная декларация. - В compose standalone (наша область) — работает, но через прокси-mapping в нативные docker run флаги
--cpus,--memory. Есть мелкие отличия от swarm в edge-cases.
В compose v2 для standalone-стенда deploy.resources работают, но раньше (compose v1) — нет. Если ты на v1.29 — лимиты надо указывать через cpus: и mem_limit: на верхнем уровне сервиса. На v2 — лучше через deploy.resources, формат более явный.
restart policies
services:
app:
image: myapp
restart: unless-stopped
Опции:
| restart | Поведение |
|---|---|
no (default) | Никогда не перезапускать |
always | Всегда перезапускать, даже после успешного завершения |
on-failure | Только при exit code != 0 |
unless-stopped | Перезапускать, кроме случая когда ты сам остановил контейнер |
Когда что использовать
no: one-shot job, миграция, скрипт. Запустился, отработал, завершился.always: реже всего. Бывает нужен для daemon, который должен работать даже послеdocker stop.on-failure: long-running job, которая должна переподниматься на ошибке, но не должна крутиться, если успешно завершилась.unless-stopped: главное для production-сервисов. Подняли — работает, упало — поднимется. Остановили вручную — лежит.
on-failure с лимитом
restart: on-failure:5
Перезапустить максимум 5 раз. После 5-го фейла остановится. Полезно, чтобы не было infinite-loop при битой конфигурации.
В docker run / docker-compose v2 это --restart on-failure:5, в синтаксисе compose-файла поле restart поддерживает суффикс через двоеточие.
Полный пример
Production-like compose для DE-стенда с worker pool, лимитами и restart:
services:
postgres:
image: postgres:17
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
volumes:
- pgdata:/var/lib/postgresql/data
secrets:
- db_password
restart: unless-stopped
deploy:
resources:
limits: { cpus: '2', memory: 2G }
reservations: { memory: 1G }
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
redis:
image: redis:7-alpine
restart: unless-stopped
deploy:
resources:
limits: { memory: 512M }
worker:
image: myorg/celery-worker:1.0
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
environment:
CELERY_BROKER_URL: redis://redis:6379/0
DATABASE_URL: postgresql://postgres@postgres:5432/etl
DATABASE_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
restart: on-failure:3
deploy:
replicas: 2
resources:
limits: { cpus: '1', memory: 1G }
secrets:
db_password:
file: ./secrets/db_password.txt
volumes:
pgdata:
docker compose up -d
docker compose ps
# postgres ... healthy
# redis ... up
# worker-1 ... up
# worker-2 ... up
# Скейл worker'ов на лету:
docker compose up -d --scale worker=4
docker compose ps
# worker-1, worker-2, worker-3, worker-4 — все up.
# Скейл вниз:
docker compose up -d --scale worker=1
docker compose ps
# Только worker-1.
Мониторинг ресурсов
docker stats
Live-таблица, показывающая CPU/Memory/Net/Disk I/O каждого контейнера в реальном времени. Полезно понять, кто упирается в лимит.
docker stats --no-stream myproj-worker-1
# Один snapshot, без watch-режима.
docker compose stats
# В compose v2 (новее ~2.30) — то же, но автоматически для всех сервисов стенда.
OOM kill: что когда происходит
Если контейнер пытается использовать память больше limits.memory, ядро его убивает (SIGKILL):
docker compose ps
worker-2 ... Exited (137)
137 = 128 + 9 = SIGKILL. В логах ядра (dmesg) видно событие OOM с PID.
В docker inspect:
docker inspect worker-2 | jq '.[0].State'
# {
# "OOMKilled": true,
# "ExitCode": 137,
# ...
# }
Это сигнал: либо увеличить лимит, либо чинить утечку памяти в приложении.
CPU throttling vs OOM
CPU-лимит работает мягко: контейнер не получает больше CPU-time, чем лимит, но не убивается. Просто медленно работает. Это легко спутать с «контейнер завис»:
docker stats myproj-app-1
# CPU % 500% означает, что контейнер реально использует 5 cores
Если CPU limit '1', а stats показывает 100% — это означает «использует свой полный лимит, throttled на пределе». Решение — либо поднять limit, либо оптимизировать код, либо смириться, что приложение тормозит.
Попробуй сам
mkdir -p scale-demo && cd scale-demo
cat > compose.yml <<'YAML'
services:
redis:
image: redis:7-alpine
worker:
image: alpine:3.20
depends_on: [redis]
command: sh -c 'while true; do echo "$$(hostname) tick"; sleep 5; done'
deploy:
resources:
limits: { memory: 64M }
restart: on-failure
YAML
# 1. Один worker.
docker compose up -d
docker compose ps
# worker-1
# 2. Scale до 3.
docker compose up -d --scale worker=3
docker compose ps
# worker-1, worker-2, worker-3
# 3. Логи всех.
docker compose logs --tail 3 worker
# worker-1: <hostname-1> tick
# worker-2: <hostname-2> tick
# worker-3: <hostname-3> tick
# 4. docker stats (Ctrl+C для выхода).
docker stats --no-stream $(docker compose ps -q worker)
# 5. Scale обратно к 1.
docker compose up -d --scale worker=1
docker compose ps
# Только worker-1.
# 6. Memory limit demo. Создадим контейнер, который жрёт память.
docker run --rm --memory 100M alpine sh -c 'apk add --no-cache stress-ng 2>/dev/null; stress-ng --vm 1 --vm-bytes 200M --timeout 5s' || echo "killed"
# OOM kill — потому что 200M > 100M limit.
# Cleanup.
docker compose down
cd .. && rm -rf scale-demo
Production-практика: для compose-стенда DE-данных всегда ставь limits.memory явно. Хост с 16 GB RAM, на котором крутятся Airflow + Postgres + Kafka, без лимитов запросто впадает в OOM-каскад, когда одно приложение скушает всё, ядро убивает других, цепной отказ. С лимитами — поведение предсказуемо: OOM убивает виновного, остальные работают.
На этом модуль advanced compose завершён. У тебя теперь полный набор: профили, override-файлы, secrets, scale + resources, restart policies. В следующем модуле — реальные data-services: Postgres, MinIO, Redis локально с типичными настройками.