Профілі Docker Compose: dev і prod стеки без дублювання YAML

Було корисно?

Ви знаєте цю картину: репозиторій із docker-compose.yml, docker-compose.dev.yml, docker-compose.prod.yml,
і ще один файл, який хтось створив «тимчасово» під час інциденту. Минув рік. Тепер ваш «dev» стек випадково вмикає продовий
reverse proxy, або в проді тихо працює образ із відладкою, бо ланцюжок оверрайдів накладений неправильно.

Профілі Compose — це доросла відповідь: один файл Compose, кілька стеків, передбачувана поведінка. Менше археології YAML, менше сюрпризів «в мене працює»,
і значно менше відкатів у п’ятницю ввечері.

Що таке профілі Compose (і чого вони не роблять)

Профіль Compose — це мітка, яку ви прив’язуєте до сервісу (іноді до інших ресурсів), щоб він запускався лише коли цей профіль увімкнений.
Це по суті умовна інклюзія. Файл Compose залишається єдиною узгодженою моделлю; профілі вирішують, які частини активні під час запуску.

Ось основна ментальна модель:

  • Без профілів: запуск docker compose up запускає всі сервіси у файлі (з урахуванням залежностей).
  • З профілями: сервіси з теги профілів виключені, якщо ці профілі не ввімкнені через
    --profile або COMPOSE_PROFILES.
  • Сервіси за замовчуванням: сервіси без ключа profiles: поводяться як «завжди ввімкнені».

Чого профілі не є: повноцінною системою шаблонізації, менеджером секретів або заміною для справжнього інструмента деплою. Вони не зупинять вас
від ризикових дій; вони лише ускладнюють випадкові помилки.

Рекомендація: використовуйте профілі як фіч-ґейти для топології під час виконання. Використовуйте їх для додавання/видалення сайдкарів, інструментів,
залежностей для розробки та операційних помічників. Не намагайтеся приховати фундаментально різні продакшн-архітектури під профілями.
Якщо прод працює в Kubernetes, а dev — у Compose, профілі все одно корисні для локальної валідації. Але не приписуйте профілям чудес — вони роблять процес
дисциплінованим.

Цитата для тримання голови холодною під час наступної дискусії «просто запусти це»:
Hope is not a strategy. — General Gordon R. Sullivan

Жарт №1: Якщо ваші dev і prod файли Compose довго розходяться, вони врешті подадуть окремі податкові декларації.

Факти та історія: навіщо потрібні профілі

Зараз профілі здаються очевидними, але вони з’явилися як відповідь на роки хаосу. Контекст допомагає зрозуміти гострі кути.

8 конкретних фактів, що важливі на практиці

  1. Compose починався як Fig (епоха 2013–2014): він був створений для локальних мультиконтейнерних додатків, а не для корпоративного деплою.
    Профілі — це більш пізня поступка факту того, як люди реально його використовують.
  2. Файли-оверрайди стали стандартним обхідним шляхом: docker-compose.override.yml був зручністю,
    яка випадково навчила команди розмножувати конфігурацію.
  3. Профілі з’явилися, щоб зменшити розростання YAML: вони дозволяють одному файлу представляти кілька форм без купи оверрайдів.
  4. Compose V2 інтегрувався в Docker CLI: docker compose (з пробілом) замінив docker-compose (з дефісом)
    у більшості сучасних установок. Профілі там підтримуються найпослідовніше.
  5. Розв’язання профілів відбувається на боці клієнта: CLI Compose вирішує, що створювати. Engine не знає вашого наміру.
    Тобто «джерелом істини» є конфіг, який ви фактично виконали.
  6. Профілі взаємодіють із залежностями неочевидно: сервіс із профілем може бути підтягнутий через залежність іншого сервісу
    (залежно від способу запуску). Треба тестувати шляхи старту.
  7. Дрейф між середовищами — це проблема доступності: дублікати YAML не лише марнують час — вони створюють невідомі ризики,
    що виявляються під час інцидентів.
  8. Профілі добре поєднуються з «операційними» контейнерами: задачі бекапу, ранери міграцій, лог-шіпери та адміністративні UI
    можуть бути опційними, не заражаючи базовий стек.

Принципи проєктування: як структурувати один файл для dev/prod

Один файл Compose може бути чистим або проклятим. Профілі не врятують вас, якщо ви проєктуєте під хаос. Проєктуйте під передбачуваність.

1) Відокремлюйте «завжди увімкнене» від «контекстних» сервісів

Покладіть додаток, його базу даних і все, що потрібно для старту, у набір за замовчуванням (без профілю).
Розробницькі зручності (live reload, адмін-UI, фейковий SMTP, локальне S3, шел для відладки) помістіть за dev.
Вирішення лише для проду (реальний TLS edge, правила reverse proxy схожі на WAF, форвардери логів) — за prod або ops.

2) Тримайте порти простими, стабільними та свідомими

У dev ви, ймовірно, публікуєте порти на хості. У проді часто не публікують; сервіс підключається до мережі, а ingress обробляє reverse proxy.
Використовуйте профілі, щоб уникнути інцидентів типу «prod випадково прив’язався до 0.0.0.0:5432».

3) Віддавайте перевагу іменованим томам; зробіть збереження явним

Збереження — це місце, де відрізняються dev/prod і виникає втрата даних. Іменовані томи підходять для локалу, але в проді має бути монтування шляхів або керований драйвер томів
та чітко визначені процеси бекапу/відновлення.

4) Сприймайте змінні оточення як API, а не як комірку для мотлоху

Використовуйте файли .env, але не дозволяйте їм стати другою мовою конфігурації. Вказуйте явні значення за замовчуванням, документуйте обов’язкові змінні
і валідуйте їх у entrypoint, якщо додаток ваш.

5) Compose — не оркестратор; не прикидайтесь

Compose може перезапускати контейнери, виконувати healthchecks і визначати залежності. Він не планує на кількох вузлах, не робить поступове розгортання
і не управляє секретами у масштабі. Використовуйте його як надійний «запускач стеків». Якщо потрібно більше — переходьте на інше рішення, а не колгоспьте купу скриптів.

Жарт №2: «Ще один файлик override» — це як закликати YAML-полтергейста.

Еталонний Compose-файл із профілями (dev/prod/ops)

Це реалістична база: вебдодаток, Postgres, кеш і опційні помічники. Мета не в тому, щоб бути химерним.
Мета — бути важким у неправильному використанні.

cr0x@server:~$ cat compose.yml
services:
  app:
    image: ghcr.io/acme/demo-app:1.8.2
    environment:
      APP_ENV: ${APP_ENV:-dev}
      DATABASE_URL: postgres://app:${POSTGRES_PASSWORD:-devpass}@db:5432/app
      REDIS_URL: redis://redis:6379/0
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    networks: [backend]
    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://localhost:8080/healthz"]
      interval: 10s
      timeout: 2s
      retries: 12

  db:
    image: postgres:16
    environment:
      POSTGRES_DB: app
      POSTGRES_USER: app
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-devpass}
    volumes:
      - db_data:/var/lib/postgresql/data
    networks: [backend]
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d app"]
      interval: 5s
      timeout: 2s
      retries: 20

  redis:
    image: redis:7
    command: ["redis-server", "--save", "", "--appendonly", "no"]
    networks: [backend]

  # Dev-only: bind ports, live reload, friendly tools
  app-dev:
    profiles: ["dev"]
    image: ghcr.io/acme/demo-app:1.8.2
    environment:
      APP_ENV: dev
      LOG_LEVEL: debug
      DATABASE_URL: postgres://app:${POSTGRES_PASSWORD:-devpass}@db:5432/app
      REDIS_URL: redis://redis:6379/0
    command: ["./run-dev.sh"]
    volumes:
      - ./src:/app/src:delegated
    ports:
      - "8080:8080"
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    networks: [backend]

  mailhog:
    profiles: ["dev"]
    image: mailhog/mailhog:v1.0.1
    ports:
      - "8025:8025"
    networks: [backend]

  adminer:
    profiles: ["dev"]
    image: adminer:4
    ports:
      - "8081:8080"
    networks: [backend]

  # Prod-ish: reverse proxy and tighter exposure
  edge:
    profiles: ["prod"]
    image: nginx:1.27
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
    ports:
      - "80:80"
    depends_on:
      app:
        condition: service_healthy
    networks: [frontend, backend]

  # Ops-only: migrations and backups
  migrate:
    profiles: ["ops"]
    image: ghcr.io/acme/demo-app:1.8.2
    command: ["./migrate.sh"]
    environment:
      APP_ENV: ${APP_ENV:-prod}
      DATABASE_URL: postgres://app:${POSTGRES_PASSWORD}@db:5432/app
    depends_on:
      db:
        condition: service_healthy
    networks: [backend]

  pg-backup:
    profiles: ["ops"]
    image: postgres:16
    environment:
      PGPASSWORD: ${POSTGRES_PASSWORD}
    entrypoint: ["/bin/sh", "-lc"]
    command: >
      pg_dump -h db -U app -d app
      | gzip -c
      > /backup/app-$(date +%F_%H%M%S).sql.gz
    volumes:
      - ./backup:/backup
    depends_on:
      db:
        condition: service_healthy
    networks: [backend]

networks:
  frontend: {}
  backend: {}

volumes:
  db_data: {}

Що дає така структура

  • За замовчуванням безпечно: app, db, redis запускаються без публікації портів на хості.
  • Dev зручний: увімкніть dev, щоб отримати live-reload додаток, тестування пошти та Adminer.
  • Prod під контролем: увімкніть prod, щоб додати edge proxy; все ще немає випадкових dev-портів.
  • Ops явні: міграції та бекапи не «постійно працюють»; їх викликають цілеспрямовано.

Зверніть увагу на свідоме дублювання: app і app-dev — окремі сервіси. Це не лінь.
Це межа безпеки. Dev-сервіс прив’язує порти й монтує код; продоподібний — ні.
Ви можете використовувати один тег образу, розділяючи поведінку під час виконання.

Практичні дії: 12+ реальних команд, виводів і рішень

Нижче — конкретні операційні кроки, які ви справді будете використовувати. У кожному: команда, що типовий вивід означає, і яке рішення приймати далі.
Запускайте їх у корені репозиторію, де лежить compose.yml.

Задача 1: Перевірте, чи ваш Compose підтримує профілі (і яка версія)

cr0x@server:~$ docker compose version
Docker Compose version v2.27.0

Значення: встановлено Compose V2. Профілі підтримуються.
Якщо ви бачите «command not found» або старий v1 бінар, очікуйте непослідовної поведінки.

Рішення: Стандартизуйте використання docker compose в команді/CI. Змішування v1/v2 породжує квитки «а вчора працювало».

Задача 2: Відрендерте ефективну конфігурацію для профілю (впіймайте сюрпризи до старту)

cr0x@server:~$ docker compose -f compose.yml --profile dev config
services:
  adminer:
    image: adminer:4
    networks:
      backend: null
    ports:
    - mode: ingress
      target: 8080
      published: "8081"
      protocol: tcp
  app:
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    environment:
      APP_ENV: dev
      DATABASE_URL: postgres://app:devpass@db:5432/app
      REDIS_URL: redis://redis:6379/0
    healthcheck:
      test:
      - CMD
      - curl
      - -fsS
      - http://localhost:8080/healthz
    image: ghcr.io/acme/demo-app:1.8.2
    networks:
      backend: null
  app-dev:
    command:
    - ./run-dev.sh
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    environment:
      APP_ENV: dev
      DATABASE_URL: postgres://app:devpass@db:5432/app
      LOG_LEVEL: debug
      REDIS_URL: redis://redis:6379/0
    image: ghcr.io/acme/demo-app:1.8.2
    networks:
      backend: null
    ports:
    - mode: ingress
      target: 8080
      published: "8080"
      protocol: tcp
    volumes:
    - type: bind
      source: /home/cr0x/src
      target: /app/src
      bind:
        create_host_path: true
  db:
    environment:
      POSTGRES_DB: app
      POSTGRES_PASSWORD: devpass
      POSTGRES_USER: app
    healthcheck:
      test:
      - CMD-SHELL
      - pg_isready -U app -d app
    image: postgres:16
    networks:
      backend: null
    volumes:
    - type: volume
      source: db_data
      target: /var/lib/postgresql/data
  mailhog:
    image: mailhog/mailhog:v1.0.1
    networks:
      backend: null
    ports:
    - mode: ingress
      target: 8025
      published: "8025"
      protocol: tcp
  redis:
    command:
    - redis-server
    - --save
    - ""
    - --appendonly
    - "no"
    image: redis:7
    networks:
      backend: null
networks:
  backend: {}
volumes:
  db_data: {}

Значення: ви бачите точно, які сервіси з’являються під dev. Це ваша перевірка на випадкову публікацію портів,
відсутні змінні оточення або неправильні образи.

Рішення: Якщо відрендерена конфігурація містить те, чого ви не хотіли, виправте файл Compose перед запуском контейнерів.
Не налагоджуйте опечатку в конфігу під час виконання.

Задача 3: Запустіть безпечний стек за замовчуванням (без профілів)

cr0x@server:~$ docker compose -f compose.yml up -d
[+] Running 4/4
 ✔ Network server_backend  Created
 ✔ Volume "server_db_data" Created
 ✔ Container server-db-1   Started
 ✔ Container server-redis-1 Started
 ✔ Container server-app-1  Started

Значення: запустилися лише сервіси за замовчуванням. Жодних dev-інструментів, жодного edge proxy.

Рішення: Використовуйте це як базу для CI smoke-тестів і «prod-подібних» локальних запусків. Чим прісніше — тим стабільніше під час інцидентів.

Задача 4: Явно увімкніть dev-досвід

cr0x@server:~$ docker compose -f compose.yml --profile dev up -d
[+] Running 3/3
 ✔ Container server-mailhog-1 Started
 ✔ Container server-adminer-1 Started
 ✔ Container server-app-dev-1 Started

Значення: Compose додав лише сервіси профілю dev; сервіси за замовчуванням уже працювали.

Рішення: Введіть правило «dev — за явним вибором». Якщо хтось захоче дірки для відладки в проді, він має це озвучити через прапорець профілю.

Задача 5: Доведіть, які профілі увімкнені (корисно в CI-логах)

cr0x@server:~$ COMPOSE_PROFILES=prod docker compose -f compose.yml config --profiles
prod

Значення: CLI підтверджує, які профілі будуть враховані. Це невеликий трюк, який запобігає великим непорозумінням.

Рішення: У CI виводьте ефективні профілі на початку роботи. Ви купуєте у майбутнього себе менше клопоту під час інциденту.

Задача 6: Перелік контейнерів проєкту та виявлення сервісів профілів

cr0x@server:~$ docker compose -f compose.yml ps
NAME              IMAGE                         COMMAND                  SERVICE    STATUS          PORTS
server-adminer-1   adminer:4                     "entrypoint.sh php …"   adminer    running         0.0.0.0:8081->8080/tcp
server-app-1       ghcr.io/acme/demo-app:1.8.2   "./start.sh"            app        running (healthy)
server-app-dev-1   ghcr.io/acme/demo-app:1.8.2   "./run-dev.sh"          app-dev    running         0.0.0.0:8080->8080/tcp
server-db-1        postgres:16                   "docker-entrypoint…"    db         running (healthy) 5432/tcp
server-mailhog-1   mailhog/mailhog:v1.0.1        "MailHog"               mailhog    running         0.0.0.0:8025->8025/tcp
server-redis-1     redis:7                       "docker-entrypoint…"    redis      running         6379/tcp

Значення: видно, які сервіси запущені і які порти опубліковані. Колонка PORTS — ваш аудит «що ми виставили назовні?».

Рішення: Якщо ви бачите порти, опубліковані в середовищах, де їх не повинно бути — зупиніться і виправте файл. Не нормалізуйте випадкову експозицію.

Задача 7: Підтвердьте, чому сервіс не запускається (перевірка залежностей і healthcheck)

cr0x@server:~$ docker compose -f compose.yml logs --no-log-prefix --tail=30 app
curl: (7) Failed to connect to localhost port 8080: Connection refused

Значення: healthcheck не проходить. Або додаток не слухає, або слухає на іншому порту, або падає перед биндом.

Рішення: Перевірте docker compose logs app на помилки старту, потім зайдіть через docker exec в контейнер і перевірте порт прослуховування.
Не торкайтеся БД відразу; більшість помилок healthcheck — це конфіг додатку, а не зберігання.

Задача 8: Інспектуйте ефективні змінні оточення (швидко знайдіть «не той .env»)

cr0x@server:~$ docker compose -f compose.yml exec -T app env | egrep 'APP_ENV|DATABASE_URL|REDIS_URL'
APP_ENV=dev
DATABASE_URL=postgres://app:devpass@db:5432/app
REDIS_URL=redis://redis:6379/0

Значення: контейнер бачить ті значення, що ви думаєте. Якщо пароль відсутній або порожній, ваш .env не завантажений або ім’я змінної неправильне.

Рішення: Якщо змінні оточення неправильні, виправляйте на стороні виклику (експорт в шеллі, ін’єкція секретів у CI або файл Compose).
Не робіть «гарячі правки» в контейнерах.

Задача 9: Визначте дрейф образів між dev і prod сервісами

cr0x@server:~$ docker compose -f compose.yml images
CONTAINER         REPOSITORY                   TAG     IMAGE ID       SIZE
server-app-1      ghcr.io/acme/demo-app        1.8.2   7a1d0f2c9a33   212MB
server-app-dev-1  ghcr.io/acme/demo-app        1.8.2   7a1d0f2c9a33   212MB
server-db-1       postgres                     16      5e2c6e1e12b8   435MB
server-redis-1    redis                        7       1c90a3f8e3a4   118MB

Значення: обидва app-сервіси використовують однаковий image ID. Це гарно: ваша dev-поведінка відрізняється командами/маунтами/портами, а не незадокументованим кодом.

Рішення: Якщо image ID несподівано різняться, вирішіть, чи це навмисно. Якщо ні — уніфікуйте теги або припиніть вдавати, що середовища порівнянні.

Задача 10: Доведіть, які сервіси насправді належать профілю (корисно під час рефакторингу)

cr0x@server:~$ docker compose -f compose.yml config --services
adminer
app
app-dev
db
edge
mailhog
migrate
pg-backup
redis

Значення: це перелік усіх сервісів у файлі, включно з тими, що під профілями. Тепер можна звірити володіння і видалити мертві шматки.

Рішення: Якщо ніхто не може пояснити, навіщо потрібен сервіс — видаліть його або перемістіть за ops і вимагайте явного виклику.

Задача 11: Локально запустіть профіль prod без dev-експозицій

cr0x@server:~$ COMPOSE_PROFILES=prod docker compose -f compose.yml up -d
[+] Running 1/1
 ✔ Container server-edge-1  Started

Значення: додався лише сервіс edge; сервіси за замовчуванням уже були присутні.

Рішення: Використовуйте це для валідації налаштувань nginx з тим самим app/db, які ви використовуєте в інших середовищах, без dev-інструментів.

Задача 12: Виконайте одноразові ops-задачі без залишкових контейнерів

cr0x@server:~$ COMPOSE_PROFILES=ops docker compose -f compose.yml run --rm migrate
Running migrations...
Migrations complete.

Значення: контейнер міграції відпрацював і був видалений. Немає довгоживучого сервісу, немає несподіваних перезапусків.

Рішення: Тримайте «ops-дії» як run --rm задачі. Якщо міграції робити як постійний сервіс — ви отримаєте самостворений pager.

Задача 13: Зробіть бекап із профілем ops і перевірте наявність файлу

cr0x@server:~$ COMPOSE_PROFILES=ops docker compose -f compose.yml run --rm pg-backup
cr0x@server:~$ ls -lh backup | tail -n 2
-rw-r--r-- 1 cr0x cr0x  38M Jan  3 01:12 app-2026-01-03_011230.sql.gz

Значення: бекап зберігся на файловій системі хоста. Ось різниця між «у нас є бекапи» і «у нас є втішна історія».

Рішення: Якщо файл відсутній, не продовжуйте ризикові зміни. Виправте маунти/права. Бекапи без можливості відновлення — це просто мистецтво продуктивності.

Задача 14: Виявляйте колізії портів до того, як звинувачувати Docker

cr0x@server:~$ ss -ltnp | egrep ':8080|:8081|:8025' || true
LISTEN 0      4096         0.0.0.0:8080      0.0.0.0:*    users:(("docker-proxy",pid=22419,fd=4))
LISTEN 0      4096         0.0.0.0:8081      0.0.0.0:*    users:(("docker-proxy",pid=22455,fd=4))
LISTEN 0      4096         0.0.0.0:8025      0.0.0.0:*    users:(("docker-proxy",pid=22501,fd=4))

Значення: порти хоста вже зайняті процесами docker-proxy. Якщо наступний up впаде з помилкою «port is already allocated», ось чому.

Рішення: Або зупиніть конфліктний стек, або змініть публікувані порти. Не «вирішуйте» це, запускаючи все під привілеями і сподіваючись.

Три корпоративні міні-історії з практики

Міні-історія 1: Інцидент через неправильне припущення

Команда середнього SaaS тримала два файли Compose: один для dev, один для «prod-подібного». Припущення було чемним і смертельним:
«Вони майже однакові; prod-like просто додає nginx.» Ніхто не перевіряв це після десятого дрібного зміни.

Новий інженер додав контейнер Redis тільки у dev-файл, бо в додатку був feature-flag і «prod ще не використовує його».
Через тижні прод почав включати цей флаг у канарці. CI, що використовував prod-like стек, не мав Redis.
Тести проходили, бо релевантні тести пропускалися при відсутності Redis.

Потім пішло розгортання, і флаг розповсюдився ширше, ніж планували. Поведінка додатку при невдачі підключення до Redis була агресивне повторення.
CPU підскочив, латентність зросла, і кілька вузлів почали отримувати OOM-kill. Не всі, але достатньо, щоб створити поступове збійне зниження, що виглядало як «мережеві перебої».

Виправлення не було героїчним. Вони повернулися до одного файлу Compose і використали профілі: Redis став дефолтним у стеку для CI, а експериментальні залежності були за новим профілем.
Це змусило приймати свідоме рішення: якщо додаток може використовувати Redis у проді — Redis має бути у prod-like моделі.

Урок: припущення про парність середовищ — як молоко. Вони тихо псуються, а потім голосно псують вам день.

Міні-історія 2: Оптимізація, що дала зворотний ефект

Велика команда намагалася «оптимізувати UX для розробника», використовуючи профілі, щоб підміняти цілі образи:
легкий debug-образ для dev і загартований образ для prod. На папері це зменшувало локальний час збірки і робило прод-образ суворішим.
На практиці вони створили розгалужений всесвіт.

Dev-образ мав додаткові пакети: curl, netcat, Python та кілька CA-бандлів, що «просто робили все». Prod-образ був тоншим: менше бібліотек, менше інструментів.
Мета гідна.
Але додаток мав приховану залежність від системних CA-сертифікатів через сторонній SDK для TLS-запитів.

У dev баг не проявлявся, бо debug-образ мав правильний CA chain. У prod — проявився: TLS-рукопотискання інколи падали в залежності від кінцевої точки, і помилки оберталися в неясні виключення.
Інцидент затягувався, бо інженери постійно відтворювали проблему в dev, де все працювало.

Вони зберегли профілі, але змінили правило: профілі можуть змінювати команди, маунти і порти, але не базову OS-композицію runtime-образу без формального тесту, що проганяє prod-образ у dev-процесі.
Також додали профіль «prod-image», який змушує локально використовувати prod-образ.

Урок: оптимізація швидкості через зміну часу виконання — найшвидший шлях до тривалих інцидентів.

Міні-історія 3: Нудна, але правильна практика, що врятувала день

Команда платіжного напрямку використовувала Compose для локального dev і для невеликого on-prem «лабораторного» середовища для інтеграцій з партнерами.
Їхня практика була неефектна: кожна зміна в Compose мусила включати оновлений артефакт виводу docker compose config у CI-логах для кожного профілю.
Не зберігали назавжди, просто прикріплювали до підсумку job-а.

Одного ранку зміна перемістила мапінг порту з сервісу лише для dev у сервіс за замовчуванням. Це був не злий намір;
це була помилка копіпейсту при рефакторингу. Сервіс виявився UI для адміністрування бази. Ви знаєте, куди це веде.

Лабораторне середовище мало строгий фаєрвол, тож воно не було загальнодоступним. Але воно було доступне великій корпоративній мережі,
що само по собі — свій вид дикості. Команда помітила помилку до деплою, бо CI-артефакт для дефолтного профілю
раптово показав опублікований порт, якого не було вчора.

Вони відкотили зміну, а потім ввели її правильно за профілем dev. Жодного інциденту, жодного сорому, жодного «ми виправимо пізніше».
Просто маленький, нудний запобіжник, що зробив свою роботу.

Урок: друкування ефективної конфігурації — це операційний еквівалент миття рук. Це не гламурно, але запобігає інфекціям.

Швидкий плейбук діагностики: що перевіряти спочатку/друге/третє

Коли стек Compose «не працює», найшвидший шлях — припинити гадати, що зробив Compose, і подивитися, що він насправді зробив.
Профілі додають ще один рівень плутанини, тому ваша тріаж має бути чіткою.

Спочатку: підтвердіть набір профілів та відрендерену конфігурацію

  • Запустіть docker compose --profile X config і перевірте:

    • неочікувані опубліковані порти
    • відсутні сервіси, які ви вважали доступними (кеш, брокер повідомлень, reverse proxy)
    • значення env за замовчуванням, які ви забули
  • Якщо конфіг виводить сюрпризи — зупиніться. Виправте конфігурацію перед пошуком причин у рантаймі.

Друге: перевірте стан контейнерів і їхнє здоров’я, а не тільки «running»

  • Запустіть docker compose ps. Шукайте (healthy) і цикли перезапуску.
  • Сервіс може бути «Up» і одночасно бути мертвим всередині. Healthchecks — ваш дешевий детектор брехні.

Третє: визначте, чи це проблема залежності або додатку

  • Якщо DB нездорова: перевірте зберігання, права і маунти томів.
  • Якщо DB здорова, але додаток — ні: перевірте логи додатку і змінні оточення.
  • Якщо все здорова, але запити не проходять: перевірте мережу, опубліковані порти і конфіг reverse proxy (особливо якщо профіль prod додає edge).

Бонус: ізолюйте, вимкнувши профілі

Якщо профіль dev вносить поломку, запустіть лише стек за замовчуванням. Якщо дефолт працює, регресія в дев-сервісах, маунтах або конфлікті портів.
Профілі роблять цю ізоляцію тривіальною — якщо тримати дефолт чистим.

Поширені помилки: симптом → корінь → виправлення

Помилка 1: «Чому мій dev-інструмент працює в prod?»

Симптом: Admin UI, MailHog або кінцеві точки відладки з’являються в середовищах, де їм не місце.

Корінь: Сервіс позбавлений profiles: ["dev"], або середовище глобально встановлює COMPOSE_PROFILES=dev.

Виправлення: Додайте профіль до сервісу і проведіть аудит CI/хостів на предмет витоків COMPOSE_PROFILES. У прод-скриптах явно встановлюйте COMPOSE_PROFILES=prod.

Помилка 2: «Увімкнення профілю нічого не запустило»

Симптом: docker compose --profile ops up не показує нових контейнерів, або запускає лише дефолтні.

Корінь: Сервіси визначені з іншим ім’ям профілю, ніж ви передали (опечатка), або ви очікували, що run-тип задачі з’являться під up.

Виправлення: Виконайте docker compose config --services і перегляньте секції profiles. Для одноразових задач використовуйте docker compose run --rm SERVICE.

Помилка 3: «Додаток не може підключитися до бази в dev, але в prod працює»

Симптом: Відмови підключення/таймаути тільки в профілі dev.

Корінь: Dev-сервіс використовує інший DATABASE_URL, або випадково вказаний localhost замість сервісного імені db.

Виправлення: У контейнерах використовуйте DNS-імена сервісів у мережі Compose: db:5432. Підтвердіть з docker compose exec app env.

Помилка 4: «Port is already allocated» з’являється раптово

Симптом: Старт dev-профілю падає з помилкою бинду порту.

Корінь: Інший стек вже прив’язав порт, або ви запустили два профілі, які обидва публікують той самий порт (поширено при app і app-dev, якщо обидва публікують 8080).

Виправлення: Публікуйте порти лише в одному з сервісів (зазвичай у dev). Перевірте колізії через ss -ltnp.

Помилка 5: «depends_on не зачекав; додаток стартував зарано»

Симптом: Додаток стартує до того, як DB готова, що викликає цикли падіння.

Корінь: Ви використали depends_on без умов здоров’я, або відсутній/неправильний healthcheck для DB.

Виправлення: Додайте healthchecks і використовуйте condition: service_healthy. Також зробіть додаток толерантним і з повторними спробами; Compose — не ваш шар надійності.

Помилка 6: «Ми думали, що сервіс профілю не створено, але він існує»

Симптом: Сервіс, захований за профілем, існує як контейнер/мережевий артефакт, навіть коли профіль не увімкнено.

Корінь: Ви раніше запускали з цим профілем; ресурси залишилися до видалення. Або автоматизація використовує docker compose up з встановленими змінними оточення.

Виправлення: Використовуйте docker compose down (і опційно -v лише в dev). Трактуйте «те, що зараз працює», як стан, а не намір.

Помилка 7: «Наші бекапи пройшли, але відновлення впало»

Симптом: Завдання бекапу пройшло без помилок; відновлення пізніше не вдалось або дало пусті дані.

Корінь: Контейнер бекапу записав у шлях всередині контейнера, який не був змонтований, або права завадили записувати на хост.

Виправлення: Зберігайте бекапи на хості через маунт. Після бекапу перевіряйте наявність і розмір файлу через ls -lh. Періодично тестуйте відновлення.

Чеклісти / покроковий план

Покроково: міграція з кількох Compose-файлів в один з профілями

  1. Інвентар сервісів по файлах. Перелічіть сервіси і зафіксуйте відмінності (порти, томи, теги образів, команди).
  2. Визначте профілі за рішеннями, а не людьми.
    Використовуйте назви на кшталт dev, prod, ops, debug. Уникайте alice або newthing.
  3. Виберіть «безпечний дефолт» стек. Без дев-інструментів, без публікації портів бази (часто — зовсім без портів).
  4. Перенесіть dev-only сервіси за dev. MailHog, Adminer, фейковий S3, локальні UI трасування тощо.
  5. Розділяйте сервіси, коли поведінка під час виконання відрізняється істотно.
    Якщо dev потребує bind-маунтів і іншої команди — створіть app-dev, замість нагромаджувати логіку через змінні.
  6. Тримайте стабільність ідентичності образів, де можливо. Бажано використовувати той самий образ для app і app-dev; змінюйте команду/маунти/порти.
  7. Відрендерюйте конфіги в CI для кожного профілю. Зберігайте виводи docker compose config у логах збірки.
  8. Документуйте команди запуску. Зробіть їх скопійованими для вставки; люди все одно їх копіюватимуть.
  9. Протестуйте три шляхи: тільки дефолт, --profile dev, --profile prod (або prod-like).
  10. Видаліть старі файли. Не тримайте їх «на всяк випадок». Саме так повертається дрейф.

Операційний чекліст: перед тим як вважати стратегію профілів «завершеною»

  • Дефолтний профіль стартує і функціонує без опублікованих портів бази.
  • docker compose config вивід стабільний і переглянутий для кожного профілю.
  • Dev-профіль не змінює базові образи без явного плану тестування.
  • Ops-задачі використовують run --rm і записують результати на хост-монтування.
  • Мапінги портів унікальні для сервісів, що можуть працювати разом.
  • Існують healthchecks для stateful залежностей (DB) і додатку.
  • Секрети не закомічені, а прод-запуски явно встановлюють профілі.

План CI: мінімально, але ефективно

  1. Відрендерити конфіг для default + dev + prod і зберегти в логах.
  2. Запустити дефолтний стек, виконати smoke-тести, зупинити.
  3. Запустити dev-стек (або підмножину), виконати unit/integration тести, зупинити.
  4. Запустити ops-міграції як одноразову задачу у тимчасовому середовищі.

FAQ

1) Використовувати профілі чи файли-оверрайди?

Використовуйте профілі для змін топології (які сервіси існують) і для випадків «dev-інструменти опціональні».
Файли-оверрайди застосовуйте обережно для локальних машинних налаштувань (наприклад, персональний порт) і лише якщо ви готові терпіти дрейф.
Якщо треба обрати одне — профілі легше розуміти й аудитувати.

2) Чи може сервіс належати кільком профілям?

Так. Можна вказати profiles: ["dev", "ops"] для сервісу, корисного в обох контекстах.
Будьте обережні: багатопрофільне членство може перетворитися на логічну головоломку під час інцидентів.
Зберігайте це рідкісним і обґрунтованим.

3) Що стане, якщо я запущу docker compose up без зазначення профілю?

Сервіси без ключа profiles запускаються. Сервіси з ключем profiles ігноруються.
Тому ваші сервіси за замовчуванням мають бути безпечними і мінімальними.

4) Чи може увімкнення профілю випадково запустити додаткові сервіси через залежності?

Може, залежно від того, як ви запускаєте стек і як оголошено залежності. Ваше завдання — тестувати шляхи старту:
запуск «тільки додатку», запуск повного стеку та запуск сервісів профілю.
Припускайте, що люди під час інцидентів виконуватимуть дивні команди.

5) Чи впливають профілі на мережі і томи?

Профілі гейтять сервіси. Мережі і томи зазвичай створюються у разі потреби сервісів, що їх посилають.
Якщо том посилається лише профільним сервісом, він не буде створений, доки профіль не активний.

6) Як запобігти експозиції dev-портів, коли хтось запускає неправильний профіль?

Зробіть дефолт безпечним і робіть виклики в проді явними. У скриптах встановлюйте COMPOSE_PROFILES=prod
замість покладання на змінні середовища. Також уникайте публікації портів у дефолтних сервісах, якщо це не потрібно.

7) Як обробляти міграції з профілями?

Розмістіть міграції у профілі ops як одноразову задачу і запускайте їх через docker compose run --rm migrate.
Не перетворюйте міграції на довгоживучий сервіс. Якщо воно перезапускається — рано чи пізно ви виконаєте міграцію двічі.

8) Чи підходять профілі для «проду на одному VM»?

Так, за умови дисципліни. Профілі допомагають тримати операційні інструменти поза базою і не допустити випадкову експозицію.
Але не плутайте «працює на одному VM» з «орchestrated production platform».
Додавайте моніторинг, бекапи і явні процедури відкату. Compose їх не вигадає за вас.

9) Який найбільш чистий спосіб перемикатися між dev і prod поведінкою для одного додатку?

Надавайте перевагу окремим сервісам (наприклад, app і app-dev), коли відмінності значущі (bind-маунти, команди, порти).
По можливості використовуйте той самий тег образу. Розділяйте поведінку — спільний артефакт.

10) Чи тримати профіль debug?

Так, якщо використовуєте відповідально. debug для епhemeral-інструментів (tcpdump-контейнер, shell-контейнер, агент профілювання)
може зменшити час на розуміння проблеми. Просто не дозволяйте йому перетворитися на «prod з тренувальними колесами».

Висновок: практичні подальші кроки

Профілі Compose — найпростіший спосіб припинити дублювання YAML, одночасно запускаючи різні стеки для різних контекстів.
Вони не усувають складність; вони роблять її видимою і контрольованою. Ось у чому суть.

Зробіть це далі, в порядку

  1. Виберіть безпечний дефолт-стек без dev-інструментів і мінімальною експозицією портів хоста.
  2. Додайте профілі dev, prod і ops, щоб відсікти опційні, ризикові або одноразові елементи.
  3. Включіть вивід docker compose config у CI-логи для кожного профілю. Розглядайте це як аудиторський слід.
  4. Перетворіть утиліти для міграцій/бекапів на run --rm задачі під ops.
  5. Видаліть зайві Compose-файли після перевірки підходу з одним файлом. Дрейф любить сентиментальні зв’язки.

Коли ви на виклику, вам потрібно менше рухомих частин і менше недокументованих гілок поведінки. Профілі дають це — якщо ви тримаєте дефолт чистим
і профілі — свідомими. Менше магії. Більше передбачуваності.

← Попередня
Видалення знімків ZFS: чому вони не видаляються і як це виправити
Наступна →
Тренування DR із ZFS send/receive: відпрацьовуємо відновлення, а не лише резервні копії

Залишити коментар