← все статьи
10 мин

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-переменных. Остальные девять — слои, которые добавляются по мере роста требований.

← все статьи Следующая →SQLite как продакшн-БД: серьёзно