MAATRIX / Блог / Docker Compose profiles: несколько окружений

Docker Compose profiles: несколько окружений

Docker Compose profiles: несколько окружений

MAATRIX

Рано или поздно в проекте на Docker Compose заводится зоопарк файлов: docker-compose.yml, docker-compose.override.yml, docker-compose.dev.yml, docker-compose.staging.yml, docker-compose.prod.yml — и уже никто не помнит, какой файл с каким мержится и в каком порядке. Фича profiles, стабильная в Compose Specification с 2021 года, решает эту задачу иначе: один файл, сервисы помечены тегами-профилями, а нужный набор запускается флагом при старте. Разберём, как это работает на практике.

Что такое profiles и зачем они нужны

profiles — это список меток у сервиса в docker-compose.yml. Сервис с непустым списком profiles запускается только тогда, когда хотя бы один из его профилей явно включён при вызове docker compose. Сервисы без ключа profiles (или с пустым списком) запускаются всегда, независимо от того, какие профили включены — это ваш «базовый» набор.

Смысл в том, чтобы описать *все возможные* сервисы проекта в одном файле — веб-приложение, базу, кеш, воркеры, инструменты отладки, тестовые заглушки — а решение о том, что реально поднимать, принимать в момент запуска, а не заранее в отдельном YAML.

Плюсы такого подхода:

  • один источник правды по конфигурации сервисов — не нужно синхронизировать образы, версии, сети между несколькими файлами;
  • явное перечисление окружений прямо в файле — открыл docker-compose.yml и видишь все профили, которые есть в проекте;
  • легко добавить временный набор сервисов (например, debug или seed) без риска зацепить продакшен-конфигурацию.

Минус тоже есть: если сервисов много и профили пересекаются нетривиально, файл разрастается и требует дисциплины в именовании. Об этом — ниже, в разделе про ограничения.

Синтаксис: как назначить сервис профилю

Профили назначаются ключом profiles внутри описания сервиса. У сервиса может быть несколько профилей — тогда он включается, если активен хотя бы один из них:

services:
  web:
    build: .
    ports:
      - "8000:8000"
    depends_on:
      - db

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

  adminer:
    image: adminer
    profiles: ["dev"]
    ports:
      - "8080:8080"

  debug-tools:
    image: busybox
    profiles: ["dev", "debug"]
    command: sleep infinity

  seed:
    build: ./seed
    profiles: ["staging", "dev"]
    depends_on:
      - db

volumes:
  db_data:

Здесь web и db — база, они не зависят от профиля и стартуют всегда. adminer (веб-интерфейс для БД) включается только с профилем dev. debug-tools включается и в dev, и отдельно в debug. seed (наполнение тестовыми данными) — в dev и staging, но не в проде.

Важный нюанс: depends_on не активирует профиль сам по себе. Если web зависит от сервиса с профилем, а нужный профиль не включён, Compose до версии с фиксом мог падать с ошибкой о недостающей зависимости — сейчас это поведение задокументировано так: зависимость с профилем должна либо сама попадать под активный профиль, либо не быть в depends_on сервиса, который стартует без профилей. Проще говоря: не завязывайте всегда-стартующие сервисы на сервисы с профилями через depends_on — используйте profiles последовательно по всей цепочке зависимостей.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать VPS

Пример: dev, staging и production в одном файле

Практическая раскладка для типового проекта — веб-приложение, БД, Redis, воркер очередей и набор инструментов:

services:
  web:
    build: .
    ports:
      - "8000:8000"

  db:
    image: postgres:16
    profiles: []           # база нужна всегда, ключ можно не указывать

  redis:
    image: redis:7

  worker:
    build: .
    command: celery -A app worker
    profiles: ["staging", "prod"]

  adminer:
    image: adminer
    profiles: ["dev"]

  mailhog:
    image: mailhog/mailhog
    profiles: ["dev"]

  nginx:
    image: nginx:1.27
    volumes:
      - ./nginx/prod.conf:/etc/nginx/nginx.conf:ro
    profiles: ["prod"]

Логика: web, db, redis — костяк, нужны везде. worker подключаем только там, где реально нужна фоновая обработка задач (staging и prod, но не в dev, где разработчик гоняет задачи синхронно). adminer и mailhog (перехват почты для локальной разработки) — только в dev. nginx как reverse proxy перед приложением — только в prod, потому что на dev-машине разработчика он не нужен.

Такая раскладка не требует пересобирать образы под окружение — меняется только набор *запущенных* сервисов, а не конфигурация внутри контейнеров (переменные окружения для разных стадий по-прежнему стоит держать в разных .env-файлах, это отдельная задача).

Опциональные сервисы для отладки и разработки

Отдельный частый сценарий — не разные окружения, а опциональные инструменты внутри одного окружения. Например, вы хотите держать под рукой pgAdmin, но не гонять его постоянно на сервере разработки, потому что он ест память:

services:
  pgadmin:
    image: dpage/pgadmin4
    profiles: ["tools"]
    environment:
      PGADMIN_DEFAULT_EMAIL: admin@example.com
      PGADMIN_DEFAULT_PASSWORD: admin
    ports:
      - "5050:80"

  jaeger:
    image: jaegertracing/all-in-one
    profiles: ["tools", "debug"]
    ports:
      - "16686:16686"

Так вы держите основной набор сервисов лёгким по умолчанию (docker compose up их не тронет), а тяжёлые или узкоспециализированные инструменты поднимаете отдельной командой, когда они реально нужны — и гасите вместе с остальными через docker compose down, не забывая ни про один контейнер.

Запуск конкретного профиля

Профиль включается флагом --profile при любой команде docker compose:

# только dev-профиль (плюс сервисы без профиля)
docker compose --profile dev up -d

# несколько профилей сразу
docker compose --profile dev --profile debug up -d

# через переменную окружения — удобно для CI и .env
COMPOSE_PROFILES=staging docker compose up -d

# посмотреть, что реально будет запущено
docker compose --profile staging config --services

Флаг --profile можно повторять или задавать через COMPOSE_PROFILES (список через запятую) — это удобно прописать в .env-файле проекта, чтобы не набирать флаг каждый раз вручную:

# .env
COMPOSE_PROFILES=dev

Тогда обычный docker compose up -d в каталоге проекта сам подхватит нужный набор сервисов. Для CI/CD стоит явно передавать профиль в команде деплоя, а не полагаться на .env на сервере — так меньше шансов случайно поднять staging-набор на проде из-за забытого файла.

Чтобы запустить вообще всё, что описано в файле, есть флаг-джокер:

docker compose --profile "*" up -d

Полезно для локальной проверки, что весь файл валиден и ни один сервис не сломан — но на боевом сервере так делать не стоит, там должен запускаться конкретный явный набор.

Profiles против отдельных compose-файлов с override

До появления profiles стандартным способом было разносить окружения по файлам и мержить их флагом -f:

docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
КритерийНесколько файлов + overrideprofiles в одном файле
Источник правдыРазбросан по файлам, легко разойтисьОдин файл, видно всё сразу
Порядок примененияВажен порядок -f, легко ошибитьсяПорядка нет, включаете набор меток
Изменение параметров сервиса между окружениями (env, реплики)Удобно — override переопределяет любое полеНеудобно — profiles включают/выключают сервис целиком, а не меняют его параметры
Добавление нового окруженияНовый файл + не забыть про все остальныеНовая метка в существующих сервисах
Git-историяDiff размазан по файламDiff локализован в одном файле

Ключевое отличие: profiles решают вопрос «какие сервисы включены», а override-файлы решают вопрос «какие у сервиса параметры». Это не взаимоисключающие механизмы — их часто комбинируют: базовая структура и профили в основном файле, а точечные отличия по переменным окружения или лимитам ресурсов между dev и prod — через docker-compose.override.yml или -f. Если вам нужно не просто включать/выключать сервисы, а менять их конфигурацию по окружениям (реплики, лимиты CPU, домены), почитайте про настройку docker compose для продакшена — там разобран как раз этот сценарий.

Если у вас в основном меняется набор сервисов (включить воркер, отладочные тулы, тестовые данные) — profiles проще и меньше кода. Если меняются в основном параметры одних и тех же сервисов — override-файлы гибче. Для больших проектов рабочая комбинация: profiles для «включить/выключить», плюс .env-файлы под каждое окружение для значений переменных.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать VPS

Частые вопросы

Можно ли комбинировать profiles с несколькими compose-файлами?

Да, это не конкурирующие механизмы. Можно объявить profiles в базовом файле и одновременно передавать -f docker-compose.override.yml для переопределения отдельных полей — они применяются независимо друг от друга.

Что будет, если запустить docker compose up без флага --profile?

Стартуют только сервисы без ключа profiles (или с пустым списком). Все сервисы, у которых указан хотя бы один профиль, будут пропущены — это поведение по умолчанию и оно безопасно: случайно не поднимете лишнее на проде.

Как остановить сервисы конкретного профиля?

docker compose --profile dev down остановит и удалит контейнеры, относящиеся к активному набору (включая сервисы без профиля). Если нужно погасить именно один сервис — проще docker compose stop имя_сервиса.

Профили можно комбинировать с docker compose run?

Да, docker compose run --profile debug имя_сервиса учитывает зависимости внутри указанного профиля так же, как up.

Работает ли --profile в старых версиях Docker Compose?

Фича доступна начиная с Compose v2 (и Compose file format версии 3.9+ для v1-совместимого синтаксиса). Если у вас legacy docker-compose v1, сначала обновите Compose — заодно проверьте, что версия Docker Engine на сервере это поддерживает.

Нужно ли перечислять профиль явно у зависимых сервисов, если зависимость уже под профилем?

Да. Compose не подтягивает профиль автоматически через depends_on — если сервис A зависит от B, а B под профилем dev, то и A должен быть под тем же профилем (или запускаться вместе с ним осознанно), иначе при активации dev вы получите ошибку about a missing dependency у не входящих в профиль сервисов.