Docker Compose в 2026: 12 паттернов для production
Docker Compose — инструмент, который большинство команд используют для разработки и стараются не трогать в production. Зря. Compose v2 с профилями, watch-режимом и нативными secrets закрывает значительную часть продакшн-сценариев без Kubernetes. Ниже — 12 паттернов, которые мы используем в реальных проектах. Без «запустите docker compose up» — только то, что реально влияет на надёжность.
Паттерн 1–3: Зависимости и healthchecks
1. depends_on с condition вместо голого порядка запуска. Базовый depends_on: db гарантирует только то, что контейнер запущен, но не то, что база готова принимать соединения. Правильная форма:
services:
app:
image: myapp:latest
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
migrations:
condition: service_completed_successfully
db:
image: postgres:16-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $POSTGRES_USER -d $POSTGRES_DB"]
interval: 5s
timeout: 5s
retries: 10
start_period: 10s # дать время на инициализацию
migrations:
image: myapp:latest
command: ["python", "manage.py", "migrate", "--no-input"]
depends_on:
db:
condition: service_healthy
restart: "no" # run-once контейнер
2. Healthcheck с реальной бизнес-логикой. pg_isready проверяет только TCP — PostgreSQL может принимать соединения, но ещё не завершить recovery. Для приложений — проверяйте /health эндпоинт, а не просто порт:
app:
healthcheck:
test: ["CMD", "curl", "-f", "-s", "http://localhost:8000/health"]
interval: 10s
timeout: 5s
retries: 3
start_period: 30s
3. restart: unless-stopped вместо always. always перезапускает контейнер даже если вы его остановили вручную — например, для дебага. unless-stopped сохраняет ручное управление и при этом поднимает сервис автоматически после reboot хоста.
Паттерн 4–5: Profiles для окружений
4. Profiles для разделения dev/prod/tools. Profiles позволяют держать один compose.yml и активировать нужные сервисы через флаг. Никаких отдельных файлов для каждого окружения:
services:
app:
image: myapp:latest
# без profiles — запускается всегда
db:
image: postgres:16-alpine
# без profiles — запускается всегда
# --- Dev only ---
pgadmin:
image: dpage/pgadmin4:latest
profiles: [dev]
ports:
- "5050:80"
environment:
PGADMIN_DEFAULT_EMAIL: dev@local.com
PGADMIN_DEFAULT_PASSWORD: dev
# --- Prod only ---
nginx:
image: nginx:alpine
profiles: [prod]
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- certbot_certs:/etc/letsencrypt:ro
# --- Tools ---
backup:
image: myapp:latest
profiles: [tools]
command: ["python", "scripts/backup.py"]
restart: "no"
volumes:
certbot_certs:
# Запуск для разработки
docker compose --profile dev up -d
# Production
docker compose --profile prod up -d
# Разовый backup
docker compose --profile tools run --rm backup
5. Наследование через extends для общих конфигураций. Вместо дублирования переменных окружения в каждом сервисе:
# base.yml
services:
base_app: &base_app
image: myapp:latest
env_file: .env
networks: [internal]
logging:
driver: json-file
options:
max-size: "50m"
max-file: "5"
# compose.yml
services:
web:
<<: *base_app
command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
ports: ["8000:8000"]
worker:
<<: *base_app
command: ["celery", "-A", "app.celery", "worker", "-c", "4"]
Паттерн 6–7: Compose Watch и live reload
6. develop.watch вместо bind-mount всего проекта. Bind-mount исходников в контейнер работает, но даёт непредсказуемые права на файлы и медленно на macOS. watch — нативный механизм синхронизации с гибкими правилами:
services:
app:
build: .
develop:
watch:
- action: sync
path: ./src
target: /app/src
ignore:
- "**/__pycache__"
- "**/*.pyc"
- action: rebuild
path: ./pyproject.toml
- action: rebuild
path: ./Dockerfile
- action: sync+restart
path: ./config
target: /app/config
# Запуск с watch
docker compose watch
sync — копирует файл без перезапуска. sync+restart — копирует и рестартует контейнер. rebuild — пересобирает образ. Это в 3–4 раза быстрее полного bind-mount на macOS с VirtioFS.
7. Multi-stage build + .dockerignore как первая линия защиты. Без .dockerignore каждый COPY . . тащит в образ node_modules, .git, .env и всё остальное. Минимальный набор:
# .dockerignore
.git
.gitignore
.env*
*.pyc
__pycache__
.pytest_cache
.mypy_cache
node_modules
dist
build
*.log
.DS_Store
README.md
docs/
# Multi-stage: сборка и runtime раздельно
FROM python:3.12-slim AS builder
WORKDIR /build
COPY pyproject.toml poetry.lock ./
RUN pip install poetry==1.8.0 \
&& poetry export -f requirements.txt -o requirements.txt --without-hashes
RUN pip install --prefix=/runtime --no-deps -r requirements.txt
FROM python:3.12-slim AS runtime
WORKDIR /app
COPY --from=builder /runtime /usr/local
COPY src/ ./src/
RUN useradd -r -s /bin/false appuser
USER appuser
CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]
Результат: образ 180 MB вместо 1.2 GB. Время сборки при изменении только кода — 15 секунд вместо 3 минут (слои с зависимостями кэшируются).
Паттерн 8–9: Volumes и backup
8. Named volumes с явными driver options. Анонимные volume (те, что без имени в volumes:) — антипаттерн. После docker compose down они остаются, но найти их по проекту невозможно без docker volume ls. Named volumes с явными настройками:
services:
db:
image: postgres:16-alpine
volumes:
- db_data:/var/lib/postgresql/data
environment:
POSTGRES_DB: myapp
POSTGRES_USER: myapp
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
volumes:
db_data:
driver: local
driver_opts:
type: none
o: bind
device: /opt/myapp/data/postgres # явный путь на хосте
# удобно для backup скриптов
9. Backup-контейнер как часть compose-стека. Вместо cron на хосте — контейнер с расписанием внутри стека:
db_backup:
image: postgres:16-alpine
profiles: [prod]
entrypoint: |
sh -c 'while true; do
sleep 86400
PGPASSWORD=$$POSTGRES_PASSWORD \
pg_dump -h db -U $$POSTGRES_USER $$POSTGRES_DB \
| gzip > /backup/db_$$(date +%Y%m%d_%H%M%S).sql.gz
find /backup -name "*.sql.gz" -mtime +7 -delete
echo "Backup done: $$(date)"
done'
environment:
POSTGRES_USER: myapp
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
POSTGRES_DB: myapp
volumes:
- /opt/myapp/backups:/backup
depends_on:
db:
condition: service_healthy
Паттерн 10–11: Безопасность — secrets и сеть
10. Docker secrets вместо переменных окружения для чувствительных данных. Переменные окружения видны в docker inspect, логах и дочерних процессах. Secrets монтируются как файл в /run/secrets/ и доступны только внутри контейнера:
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: myapp
POSTGRES_DB: myapp
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
app:
image: myapp:latest
environment:
DATABASE_URL_FILE: /run/secrets/database_url
SECRET_KEY_FILE: /run/secrets/secret_key
secrets:
- database_url
- secret_key
secrets:
db_password:
file: ./secrets/db_password.txt
database_url:
file: ./secrets/database_url.txt
secret_key:
file: ./secrets/secret_key.txt
# Чтение secret в приложении
import os
from pathlib import Path
def read_secret(name: str, env_var: str | None = None) -> str:
"""Читает secret из файла или fallback к env var."""
secret_file = os.environ.get(f"{name.upper()}_FILE")
if secret_file:
return Path(secret_file).read_text().strip()
if env_var:
return os.environ[env_var]
raise RuntimeError(f"Secret {name} not configured")
11. Network isolation — разделение internal/external трафика. Все сервисы в одной сети по умолчанию видят друг друга. В production разделяйте сети явно:
services:
nginx:
image: nginx:alpine
networks: [external, internal]
ports: ["80:80", "443:443"]
app:
image: myapp:latest
networks: [internal]
# порты НЕ пробрасываем на хост — только через nginx
db:
image: postgres:16-alpine
networks: [db_net]
# db недоступна ни из internal, только из app
app:
image: myapp:latest
networks: [internal, db_net]
networks:
external:
driver: bridge
internal:
driver: bridge
internal: true # нет доступа в интернет из этой сети
db_net:
driver: bridge
internal: true
Паттерн 12: Resource limits и logging
12. Resource limits + structured logging. Без лимитов один контейнер может съесть всю память хоста. Без настройки логирования — диск заполнится за неделю.
services:
app:
image: myapp:latest
deploy:
resources:
limits:
cpus: "2.0"
memory: 1G
reservations:
cpus: "0.5"
memory: 256M
logging:
driver: json-file
options:
max-size: "100m"
max-file: "10"
labels: "service,environment"
tag: "{{.Name}}/{{.ID}}"
db:
image: postgres:16-alpine
deploy:
resources:
limits:
cpus: "1.0"
memory: 2G
reservations:
memory: 512M
logging:
driver: json-file
options:
max-size: "50m"
max-file: "5"
Если у вас есть централизованный logging (Loki, Elastic) — переключитесь на driver: loki или driver: fluentd. Для json-file с ротацией — этой конфигурации достаточно для большинства production-сервисов.
Env_file best practice: всегда используйте env_file: .env.production вместо inline environment: для конфигурации окружения. Держите .env.example в репозитории с дефолтными значениями и комментариями. Никогда не коммитьте реальные .env файлы — добавьте их в .gitignore. Для секретов — только Docker secrets или внешний vault.
Docker Compose в production — не компромисс, а осознанный выбор. Для сервисов с предсказуемой нагрузкой на одном или двух серверах Compose с правильными паттернами даёт 90% возможностей Kubernetes при 10% операционной сложности. Ключевые три: healthcheck с condition, profiles для изоляции окружений, secrets вместо env-переменных. Остальные девять — слои, которые добавляются по мере роста требований.