Docker Compose: оркестрация для одного сервера

dockerdocker-composedevops

Docker Compose часто воспринимают как инструмент для локальной разработки — поднял docker compose up, поработал, снёс. Но на практике Compose отлично справляется с продакшен-деплоем на одном сервере. Не каждому проекту нужен Kubernetes. Если у вас один VPS и десяток сервисов — Compose закрывает 90% задач оркестрации. Надо только знать, какие рычаги у него есть.

Зависимости и healthcheck-и

Самая распространённая проблема — порядок запуска. Приложение стартует раньше базы данных, ловит connection refused, падает. depends_on без условий гарантирует только порядок запуска контейнеров, но не готовность сервиса внутри.

Правильный подход — depends_on с condition:

services:
  api:
    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 app -d myapp"]
      interval: 5s
      timeout: 3s
      retries: 10
      start_period: 10s

  redis:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5

  migrations:
    image: myapp:latest
    command: ["alembic", "upgrade", "head"]
    depends_on:
      db:
        condition: service_healthy

Здесь api не запустится, пока PostgreSQL не пройдёт pg_isready, Redis не ответит на ping, а миграции не завершатся успешно. start_period даёт базе время на инициализацию — healthcheck-и в этот период не считаются за fail.

Паттерн с service_completed_successfully — мощная штука. Миграции, seed-данные, генерация конфигов — всё это можно вынести в отдельные сервисы, которые запускаются один раз и завершаются.

Сервисы Docker Compose: зависимости, healthcheck-и и порядок запуска

Тома: named vs bind vs tmpfs

Docker Compose поддерживает три типа монтирования, и каждый нужен для своего:

services:
  app:
    volumes:
      # Bind mount — код для hot reload (dev)
      - ./src:/app/src

      # Named volume — персистентные данные (prod)
      - app_uploads:/app/uploads

      # tmpfs — временные файлы в RAM (кэш, сессии)
      - type: tmpfs
        target: /tmp
        tmpfs:
          size: 100M

  db:
    volumes:
      # Named volume с указанием драйвера
      - pg_data:/var/lib/postgresql/data
      # Init-скрипт (read-only bind mount)
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro

volumes:
  app_uploads:
  pg_data:
    driver: local
    driver_opts:
      type: none
      device: /srv/postgres-data
      o: bind

Последний пример — named volume, привязанный к конкретной директории на хосте. Это даёт контроль над расположением данных (удобно для бэкапов), но с семантикой named volume (Docker управляет жизненным циклом).

Переменные окружения и .env

Жёстко вбивать пароли в compose-файл — очевидный антипаттерн. Docker Compose автоматически загружает .env файл из текущей директории:

# .env
POSTGRES_PASSWORD=supersecret
REDIS_PASSWORD=anothersecret
APP_VERSION=1.4.2
services:
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}

  api:
    image: myapp:${APP_VERSION}
    env_file:
      - .env
      - .env.local  # override для локальных настроек

env_file загружает переменные внутрь контейнера, а ${VAR} подставляет значения в сам compose-файл. Это разные механизмы — не путайте. Для продакшена храните .env вне репозитория и деплойте отдельно (через Ansible, например).

Профили: dev и prod в одном файле

Вместо поддержки двух отдельных compose-файлов удобнее использовать профили:

services:
  api:
    image: myapp:${APP_VERSION}
    ports:
      - "8000:8000"

  db:
    image: postgres:16-alpine
    volumes:
      - pg_data:/var/lib/postgresql/data

  # Только для разработки
  adminer:
    image: adminer
    ports:
      - "8080:8080"
    profiles:
      - dev

  mailhog:
    image: mailhog/mailhog
    ports:
      - "1025:1025"
      - "8025:8025"
    profiles:
      - dev

  # Только для продакшена
  backup:
    image: prodrigestivill/postgres-backup-local
    volumes:
      - ./backups:/backups
    environment:
      POSTGRES_HOST: db
      SCHEDULE: "@daily"
    profiles:
      - prod

volumes:
  pg_data:
# Для разработки — поднимаем dev-профиль
docker compose --profile dev up

# Для продакшена
docker compose --profile prod up -d

# Без профиля — только api и db
docker compose up -d

Сервисы без profiles запускаются всегда. С указанным профилем — только при явной активации. Чисто и без дублирования.

Ресурсные лимиты и restart policies

В продакшене обязательно ограничивайте ресурсы. Один утекающий по памяти сервис не должен ронять весь сервер:

services:
  api:
    image: myapp:latest
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: "1.0"
        reservations:
          memory: 256M
          cpus: "0.25"
      restart_policy:
        condition: on-failure
        delay: 5s
        max_attempts: 5
        window: 120s
    # Или короткая форма для restart
    restart: unless-stopped

reservations — гарантированный минимум ресурсов. limits — потолок. Если контейнер превысит лимит по памяти, Docker его убьёт (OOMKill). По CPU — просто тротлинг.

restart: unless-stopped — контейнер перезапускается автоматически при падении и после ребута сервера (если не остановлен вручную). Для продакшена — must have.

Сети между compose-проектами

Типичная ситуация: у вас отдельные compose-файлы для разных проектов, и одному нужно обратиться к другому. Решение — общая внешняя сеть:

docker network create infra-net
# Проект 1: traefik/docker-compose.yml
services:
  traefik:
    image: traefik:v3.0
    networks:
      - infra-net
    ports:
      - "80:80"
      - "443:443"

networks:
  infra-net:
    external: true
# Проект 2: myapp/docker-compose.yml
services:
  api:
    image: myapp:latest
    networks:
      - default
      - infra-net
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.myapp.rule=Host(`app.example.com`)"

networks:
  infra-net:
    external: true

Сетевое взаимодействие между compose-проектами через внешнюю сеть

Traefik видит контейнеры из других compose-проектов через общую infra-net и маршрутизирует трафик на основе лейблов. Каждый проект при этом сохраняет свою изолированную default-сеть для внутренних коммуникаций.

Реальный пример: full-stack приложение

Собираем всё вместе. FastAPI-бэкенд + PostgreSQL + Redis + Nginx в качестве reverse proxy:

services:
  nginx:
    image: nginx:1.25-alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/certs:/etc/nginx/certs:ro
      - static_files:/var/www/static:ro
    depends_on:
      api:
        condition: service_healthy
    restart: unless-stopped
    deploy:
      resources:
        limits:
          memory: 128M

  api:
    build:
      context: ./backend
      dockerfile: Dockerfile
    environment:
      DATABASE_URL: postgresql+asyncpg://${DB_USER}:${DB_PASS}@db:5432/${DB_NAME}
      REDIS_URL: redis://redis:6379/0
      SECRET_KEY: ${SECRET_KEY}
    volumes:
      - static_files:/app/static
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 15s
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
      migrations:
        condition: service_completed_successfully
    restart: unless-stopped
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: "1.0"

  migrations:
    build:
      context: ./backend
      dockerfile: Dockerfile
    command: ["alembic", "upgrade", "head"]
    environment:
      DATABASE_URL: postgresql+asyncpg://${DB_USER}:${DB_PASS}@db:5432/${DB_NAME}
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16-alpine
    volumes:
      - pg_data:/var/lib/postgresql/data
      - ./init-scripts:/docker-entrypoint-initdb.d:ro
    environment:
      POSTGRES_DB: ${DB_NAME}
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASS}
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"]
      interval: 5s
      timeout: 3s
      retries: 10
      start_period: 10s
    restart: unless-stopped
    deploy:
      resources:
        limits:
          memory: 1G

  redis:
    image: redis:7-alpine
    command: redis-server --maxmemory 128mb --maxmemory-policy allkeys-lru
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5
    restart: unless-stopped
    deploy:
      resources:
        limits:
          memory: 192M

volumes:
  pg_data:
  redis_data:
  static_files:

Этот стек покрывает типичный веб-проект: Nginx терминирует TLS и раздаёт статику, API обрабатывает бизнес-логику, PostgreSQL хранит данные, Redis кэширует горячие запросы. Миграции выполняются автоматически при деплое. Все сервисы с healthcheck-ами и лимитами ресурсов.

Полезные паттерны

Sidecar-контейнер для логов или метрик:

services:
  api:
    image: myapp:latest
    volumes:
      - app_logs:/var/log/app

  log-shipper:
    image: grafana/promtail:latest
    volumes:
      - app_logs:/var/log/app:ro
      - ./promtail.yml:/etc/promtail/config.yml:ro

Общий init-контейнер для подготовки данных:

services:
  init:
    image: myapp:latest
    command: ["./prepare-data.sh"]
    volumes:
      - shared_data:/data

  worker-1:
    image: myapp:latest
    command: ["./process.sh", "--shard=1"]
    volumes:
      - shared_data:/data:ro
    depends_on:
      init:
        condition: service_completed_successfully

Итог

Docker Compose — это полноценный инструмент оркестрации для single-node деплоя. Healthcheck-и, профили, ресурсные лимиты, внешние сети — всё это позволяет строить надёжные production-ready стеки без Kubernetes. Для большинства проектов на одном сервере этого более чем достаточно. А когда перерастёте — у вас уже будет хорошее понимание концепций, которые напрямую транслируются в Kubernetes.

© 2026 Terminal Notes. Built with SvelteKit.