Аварія починається непомітно. Контейнер стартує з NODE_ENV=development, або ваша база раптом приймає з’єднання з
паролем за замовчуванням. Ніби нічого не «змінювалося» в додатку. CI зелений. Ви відправили той самий
Compose-файл, що й минулого тижня.
Змінилася найкрихкіша частина розгортання: невидимий ланцюжок змінних середовища, що проходить через Docker Compose, ваш shell і маленький файл .env,
який ніхто не переглядає, бо «це не код».
Це код. Просто його не лінтують.
Модель мислення, яка не брешe вам
Docker Compose використовує змінні середовища двома різними способами, і більшість продакшн-помилок трапляються, коли команди
трактують їх як одне і те ж:
1) Змінні, які використовує сам Compose (час рендеру)
Ці змінні існують, щоб відрендерити конфігурацію Compose: наприклад ${IMAGE_TAG} всередині
compose.yaml, COMPOSE_PROJECT_NAME або COMPOSE_PROFILES.
Compose вирішує їх перед тим, як стартувати контейнери. Якщо Compose отримує їх неправильно, ваші контейнери можуть навіть
не відповідати тому, що ви вважаєте задеплоєним.
2) Змінні, передані в контейнери (час виконання)
Ці змінні є частиною середовища контейнера: те, що ваш додаток читає через getenv.
Вони надходять із environment:, env_file: і інколи з вашого shell через неявну передачу.
Змінні часу рендеру впливають на фінальний YAML. Змінні часу виконання впливають на поведінку процесу в контейнері.
Плутанина між цими двома призводить до ситуацій, коли ви «виправляєте» баг у контейнері, редагуючи профіль shell на хості,
а потім дізнаєтеся, що systemd не читає ваш shell-профіль.
Одна операційна істина: не можна «просто перевірити .env». Потрібно перевірити, що Compose
реально відрендерив і що контейнер фактично отримав.
Цитата для столу: paraphrased idea
— «Сподівання — не стратегія.» (парафраз ідеї,
часто приписують Gordon Sullivan)
Жарт №1: Змінні середовища як офісні плітки — ніхто не визнає, що почав їх, але вони якось є в кожній кімнаті.
Факти та історія, які варто знати (щоб перестати сперечатися з YAML)
- Факт 1: Файл
.env, який використовує Docker Compose, не автоматично має той самий синтаксис, що й shell-скрипт. Це простіший парсер «KEY=VALUE» зі своїми нюансами. - Факт 2: Compose походить від Fig (2014), і багато поведінки змінних — це спадкова зручність, а не витончена архітектура.
- Факт 3: Compose v2 реалізований як плагін Docker CLI, і поведінка може трохи відрізнятися між версіями, бо тепер він ближчий до екосистеми Docker CLI.
- Факт 4: Compose використовує змінні середовища для рендерингу конфігурації та для середовища контейнерів; для кожного шляху діють різні правила пріоритету.
- Факт 5: Інтерполяція змінних відбувається перед більшістю валідацій. Відсутня змінна може мовчки стати порожнім рядком і все одно утворити «валідне» YAML-значення.
- Факт 6:
env_file— це вхід для часу виконання контейнерів; він зазвичай не впливає на інтерполяцію Compose, якщо ви явно не завантажите змінні в shell або не використовуєте інструментарій, який це робить. - Факт 7: Команда
docker compose config— це найближче до «сироватки правди»: вона показує повністю відрендерену конфігурацію, яку запустить Compose. - Факт 8: Той самий проект на двох хостах може відрендеритись по-різному, бо Compose читає середовище хоста, поточний каталог і опційні входи
--env-file. - Факт 9:
COMPOSE_PROJECT_NAMEвпливає на імена мереж, томів і контейнерів. Зміна імені проекту може «осиротити» старі томи та створити нові.
Швидкий плейбук діагностики
Коли продакшн у вогні, філософія не допоможе. Потрібна послідовність, яка швидко звузить радіус ураження.
Ось порядок, який я використовую, бо він розділяє баги часу рендеру від багів часу виконання за кілька хвилин.
Першим: підтвердьте, що відрендерив Compose
-
Запустіть
docker compose configі перегляньте інтерпольовані значення (теги образів, порти, шляхи томів,
назву проекту, профілі). Якщо відрендерений конфіг неправий — не витрачайте час в контейнерах. - Перевірте на порожні рядки, «null»-подібні значення, неочікувані дефолти або дубльовані визначення сервісів через профілі.
Другим: підтвердьте, що контейнер фактично отримав
-
Перегляньте середовище контейнера (
docker inspect) або виведіть його всередині контейнера
(env). -
Порівняйте це з тим, що ви думаєте, що встановили через
env_fileіenvironment.
Третім: підтвердьте, який .env файл і яке середовище хоста використовувались
-
Перевірте робочий каталог і вибраний env файл. Якщо ви запускали Compose з невірного каталогу, можливо,
ви читаєте не той.env. -
Перевірте CI/CD: чи передається
--env-file? Чи експортуються змінні? Чи systemd обнуляє середовище?
Якщо зі сховищем або мережею щось дивно — підозрюйте назву проекту і імена томів
Зміна COMPOSE_PROJECT_NAME або імені каталогу може створити нові мережі й нові томи.
Додаток «втратив» дані, бо пише в інший том з іншим ім’ям.
Практичні завдання: команди, виводи та рішення
Це польові тести. Кожен включає: команду, що означає типовий вивід, і рішення, яке ви приймаєте.
Виконуйте їх у порядку, коли не впевнені, де захована правда.
Завдання 1: Перевірте версію Compose (поведінка відрізняється)
cr0x@server:~$ docker compose version
Docker Compose version v2.24.6
Значення: Ви на Compose v2.x. Добре — більшість сучасних поведінок і прапорів застосовні.
Якби це була v1, кілька прапорів і крайових випадків відрізнялися.
Рішення: Зафіксуйте цю версію в нотатках інциденту; якщо поведінка різниться між хостами, уніфікуйте версії.
Завдання 2: Подивіться, яку назву проекту бачить Compose
cr0x@server:~$ docker compose ls
NAME STATUS CONFIG FILES
payments-prod running(6) /srv/payments/compose.yaml
Значення: Проект — payments-prod. Мережі/томи будуть префіксовані цим іменем.
Рішення: Якщо ви очікували іншу назву проекту, зупиніться: можливо, ви працюєте з неправильним проектом.
Завдання 3: Відрендеріть повністю інтерпольований конфіг («правда»)
cr0x@server:~$ cd /srv/payments
cr0x@server:~$ docker compose config
services:
api:
environment:
DB_HOST: db
LOG_LEVEL: info
image: registry.local/payments-api:1.9.3
ports:
- mode: ingress
target: 8080
published: "8080"
protocol: tcp
db:
environment:
POSTGRES_DB: payments
image: postgres:15
volumes:
payments-prod_db-data: {}
networks:
default:
name: payments-prod_default
Значення: Інтерполяція відбулася. Це те, що Compose запустить.
Рішення: Якщо тег образу або порт тут неправильні, баг у розв’язанні змінних (не в часі виконання).
Завдання 4: Визначте, який env файл використовується
cr0x@server:~$ ls -la /srv/payments/.env
-rw------- 1 root root 412 Jan 2 09:11 /srv/payments/.env
Значення: Локальний .env існує в директорії проєкту.
Рішення: Переконайтеся, що ви запускаєте Compose з цього каталогу; інакше ви не читаєте цей файл.
Завдання 5: Знайдіть засідки з пробілами та лапками в .env
cr0x@server:~$ sed -n '1,120p' /srv/payments/.env
IMAGE_TAG=1.9.3
DB_PASSWORD=correct-horse-battery-staple
LOG_LEVEL=info
API_BASE_URL=https://payments.internal
BAD_SPACES =oops
QUOTED="literal quotes?"
Значення: BAD_SPACES =oops викликає підозру: багато парсерів трактують цей ключ як BAD_SPACES (із пробілом наприкінці) або відхиляють його.
QUOTED="literal quotes?" може зберігати лапки в залежності від парсера.
Рішення: Виправте формат: без пробілів навколо =, уникайте лапок, якщо не впевнені в поведінці парсера.
Завдання 6: Перевірте, чи змінна відсутня в час рендеру
cr0x@server:~$ grep -n 'IMAGE_TAG' -n /srv/payments/compose.yaml
12: image: registry.local/payments-api:${IMAGE_TAG}
Значення: Compose потребує IMAGE_TAG для рендерингу рядка образу.
Рішення: Переконайтеся, що IMAGE_TAG встановлений у правильному .env або експортований у середовищі, яке використовує Compose.
Завдання 7: Виявити мовчазну порожню інтерполяцію
cr0x@server:~$ IMAGE_TAG= docker compose config | grep -n 'image:'
7: image: registry.local/payments-api:
Значення: Порожній IMAGE_TAG рендерить майже недійсний рядок образу, який все ще може пройти YAML-парсинг.
Рішення: Додайте перевірки обов’язкових змінних, використайте дефолти в інтерполяції (див. далі) і робіть CI відмовою на пусті значення.
Завдання 8: Перегляньте середовище всередині запущеного контейнера
cr0x@server:~$ docker compose exec -T api env | egrep 'DB_|LOG_LEVEL|API_BASE_URL'
API_BASE_URL=https://payments.internal
DB_HOST=db
LOG_LEVEL=info
Значення: Контейнер отримав змінні. Якщо щось відсутнє — це проблема інжекції середовища у часі виконання.
Рішення: Порівняйте з docker compose config і вмістом env_file.
Завдання 9: Підтвердити, що Docker вважає середовище авторитетним
cr0x@server:~$ docker inspect payments-prod-api-1 --format '{{json .Config.Env}}'
["API_BASE_URL=https://payments.internal","DB_HOST=db","LOG_LEVEL=info","PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"]
Значення: Це те, що Docker передасть процесу. Якщо цього немає — ваш додаток його не побачить.
Рішення: Якщо в конфігурації Compose воно є, але в inspect — ні, у вас дрейф розгортання або проблема з пересозданням.
Завдання 10: Виявити проблему «контейнер не був пересозданий»
cr0x@server:~$ docker compose up -d
[+] Running 2/2
✔ Container payments-prod-db-1 Running
✔ Container payments-prod-api-1 Running
Значення: Compose не пересоздав контейнери. Зміни env не застосуються до запущеного контейнера, доки його не пересоздати.
Рішення: Якщо ви змінили змінні, примусово пересоздайте: docker compose up -d --force-recreate.
Завдання 11: Примусово пересоздати і підтвердити застосування нового env
cr0x@server:~$ docker compose up -d --force-recreate
[+] Running 2/2
✔ Container payments-prod-db-1 Running
✔ Container payments-prod-api-1 Started
Значення: API-контейнер був перезапущений/пересозданий.
Рішення: Повторіть Завдання 8/9, щоб підтвердити, що зміни середовища дійсно застосовані.
Завдання 12: Виявити дрейф імені проекту, що створює «нові» томи
cr0x@server:~$ docker volume ls | grep -E 'payments.*db-data'
local payments-prod_db-data
local payments_db-data
Значення: Існує два схожі томи — ймовірно з різних імен проектів.
Рішення: Підтвердіть, який том приєднано до запущеного DB-контейнера, перш ніж щось видаляти.
Завдання 13: Підтвердити, який том використовує контейнер
cr0x@server:~$ docker inspect payments-prod-db-1 --format '{{range .Mounts}}{{println .Name .Destination}}{{end}}'
payments-prod_db-data /var/lib/postgresql/data
Значення: БД використовує payments-prod_db-data.
Рішення: Якщо додаток «втратив дані», порівняйте з томом, який ви очікували. Не видаляйте томи, поки не доведете, що вони не використовуються.
Завдання 14: Визначити, який .env використовується, якщо ви запускаєте з іншого каталогу
cr0x@server:~$ cd /tmp
cr0x@server:~$ docker compose -f /srv/payments/compose.yaml config | head
services:
api:
image: registry.local/payments-api:
Значення: Запуск з /tmp ймовірно призвів до того, що Compose не знайшов потрібний /srv/payments/.env, тож інтерполяція провалилась.
Рішення: Завжди запускайте з директорії проєкту або вказуйте --env-file /srv/payments/.env.
Завдання 15: Доведіть пріоритет між хостом та .env
cr0x@server:~$ cd /srv/payments
cr0x@server:~$ export IMAGE_TAG=2.0.0
cr0x@server:~$ docker compose config | grep -n 'image:'
7: image: registry.local/payments-api:2.0.0
Значення: Експортована змінна хоста переважила значення в .env.
Рішення: У продакшні уникайте залежності від «що експортовано в shell». Зробіть джерело змінних явним.
Завдання 16: Виявити випадкові Windows CRLF у .env (так, ще буває)
cr0x@server:~$ file /srv/payments/.env
/srv/payments/.env: ASCII text, with CRLF line terminators
Значення: CRLF може прослизнути в ключі/значення, спричиняючи загадкові «змінна не знайдена» або значення з прихованим \r.
Рішення: Конвертуйте в LF: sed -i 's/\r$//' /srv/payments/.env, потім перерендеріть конфіг.
Завдання 17: Підтвердити приховані символи повернення рядка в конкретному значенні
cr0x@server:~$ python3 -c 'import os;print(repr(open("/srv/payments/.env","rb").read().splitlines()[-1]))'
b'QUOTED="literal quotes?"\r'
Значення: Той прихований \r реальний. Він може зламати токени авторизації, URL або паролі.
Рішення: Нормалізуйте кінці рядків у CI і трактуйте .env як текстовий артефакт, який потребує перевірок.
Завдання 18: Показати відмінності між env_file і environment у фінальному конфігу
cr0x@server:~$ docker compose config | sed -n '1,80p'
services:
api:
environment:
DB_HOST: db
LOG_LEVEL: info
image: registry.local/payments-api:1.9.3
Значення: Ви бачите inline-значення environment: явно. Якщо ви використовували env_file, воно може не розгорнутись у виводі так, як ви очікуєте.
Рішення: Якщо аудит залежить від видимості змінних, не покладайтеся лише на env_file; використовуйте явні кроки валідації конфігурації.
Пріоритети та область дії: хто перемагає при колізіях змінних
Більшість команд не може відповісти на це питання без гадання: «Якщо я встановив FOO в shell, в .env
і в environment:, що переможе?» Відповідь залежить від того, чи ви маєте на увазі час рендеру чи час виконання.
Саме тому це постійно ламає продакшн: люди говорять про різні речі.
Пріоритет рендеру (інтерполяція в compose.yaml)
Коли Compose інтерполює ${VAR} в YAML, він дивиться на свої джерела середовища. На практиці експортоване середовище процесу Compose — головний претендент. Локальний .env часто слугує запасним зручним джерелом.
Іншими словами: якщо ваш CI експортує IMAGE_TAG, воно зазвичай перекриє .env. Якщо ваш unit systemd запускає Compose з майже порожнім середовищем, він ігнорує те, що є в інтерактивному shell.
Операційне правило: змінні часу рендеру мають бути явними. Передавайте їх через CI контрольовано або вказуйте явний env файл через --env-file. Не дозволяйте випадковим shell-функціям вирішувати.
Пріоритет часу виконання (що отримує контейнер)
Середовище контейнера будується з визначень сервісів у Compose:
environment:записи явні й видимі у Compose-файлі.env_file:завантажує ключі/значення з файлу в середовище контейнера.- Деякі змінні можуть передаватися з хоста, якщо ви посилаєтеся на них у
environment:без значення, залежно від синтаксису.
Практичне правило: тримайте environment: як контракт API для того, що очікує контейнер, і
тримайте env_file як реалізацію того, як ви подаєте ці значення. Під час налагодження завжди перевіряйте, що справді прибуло через docker inspect.
Назва проекту — це прихована частина історії вашого середовища
COMPOSE_PROJECT_NAME здається «лише ім’ям». Це не так.
Воно змінює імена мереж і томів. Якщо ви прив’язуєте дані до томів і моніторинг до імен контейнерів,
назва проекту — це продакшн-змінна, чи визнаєте ви це, чи ні.
Інтерполяція та парсинг: гострі краї .env і Compose
Формат .env виглядає як shell. Він ним не є. Це файл ключ-значення з достатньою гнучкістю,
щоб зробити вас занадто впевненим.
Пробіли: тихий вбивця
Багато парсерів трактують KEY =value як інший ключ, ніж KEY=value, або відхиляють його.
В обох випадках ви отримаєте «KEY не встановлений», і Compose тихо підставить порожній рядок.
Не будьте «терпимими». Будьте суворими. Для продакшн-файлів env:
без пробілів навколо знака рівності, і ключі повинні відповідати [A-Z0-9_]+.
Лапки: інколи літеральні, інколи знімаються, завжди заплутують
В деяких екосистемах FOO="bar" означає значення bar без лапок. В інших — лапки є частиною значення.
Поведінка Compose може здивувати в залежності від парсера, який використовується.
Єдиний безпечний підхід: уникайте лапок у .env, якщо ви не перевірили поведінку через docker compose config та запущений контейнер.
Дефолти інтерполяції: використовуйте, але розумійте їх
Compose підтримує конструкції на кшталт:
${VAR:-default} та ${VAR?error} у багатьох контекстах.
Саме тут команди можуть перетворити невидиму помилку на голосну.
Якщо IMAGE_TAG має існувати в продакшні — зробіть його обов’язковим. Якщо LOG_LEVEL може мати дефолт — задайте його.
Спрацьовування відмов має бути явним для всього, що змінює поведінку невидимим чином.
Порожнє відрізняється від відсутнього
Інтерполяція Compose часто трактує порожню змінну як «встановлену», що може відбити дефолти. Якщо пайплайн встановив
IMAGE_TAG у порожній рядок (таке буває), ваш ${IMAGE_TAG:-latest} може поводитися не так, як ви очікуєте.
Тестуйте це явно у вашому середовищі.
.env проти env_file: схожий зовнішній вигляд, різна семантика
Файл .env (для Compose) використовується для інтерполяції змінних Compose і деяких налаштувань Compose.
Директива env_file: подає змінні в середовище контейнера під час виконання.
Люди плутають їх, бо файли виглядають однаково. Результат — надійний хаос.
Якщо ви хочете, щоб значення впливали на інтерполяцію, вони мають бути в середовищі, яке використовує Compose для рендеру
(експорт shell, явний --env-file або обробка env вашою оркестрацією). Якщо ви хочете значення всередині контейнера,
вони мають бути в environment: або env_file:.
Жарт №2: Файл .env як малюк — тиша не означає, що все добре, це означає, що треба негайно перевірити.
Три корпоративні історії з передової
Історія 1: Інцидент через хибне припущення
Команда фінтеху запускала клієнтський API на парі VM з Docker Compose. У репозиторії був .env
і окремий prod.env, збережений на хості. В їхніх головах «Compose завантажує env з env_file». Вони були
напівправі і повністю приречені.
Compose-файл використовував ${IMAGE_TAG} для фіксації образу API. Змінні середовища для часу виконання бралися з
env_file: ./prod.env. Потрібен був хотфикс для релізу, тому інженер оновив IMAGE_TAG
у prod.env, запустив docker compose up -d і очікував, що новий образ розгорнеться.
Нічого не сталося. Інтерполяція Compose не дивиться в env_file для рендерингу поля image:.
Контейнери залишилися на старому тегу. Тим часом інженер також оновив runtime-змінну в prod.env
і припустив, що контейнер її підхопив; він не підхопив, бо Compose не пересоздав контейнер. В результаті у них був
старий код, старе середовище і нове переконання.
Через дві години API почав кидати помилки, що виглядали як регресія хотфиксу. Це не була регресія — хотфикс
ніколи не задеплоївся. Моніторинг показував «деплой пройшов», бо джоб завершився; він не валідував
docker compose config або не перевіряв ID образів запущених контейнерів.
Виправлення було нудним: зробити теги образів обов’язковими змінними рендеру і встановлювати їх явно в команді деплою,
перевіряти через docker compose config, потім примусово пересоздати або коректно оновлювати контейнери.
Вони також перестали використовувати prod.env як магічний файл, що «контролює все». Він контролює
тільки те, що ви явно до нього підключили.
Історія 2: Оптимізація, яка відгукнулась боком
Медіакомпанія хотіла пришвидшити деплоя. Хтось помітив, що пересоздання контейнерів займає час, особливо для сервісу з багатьма sidecar-ами.
Вони змінили процес: оновлювати .env, а потім запускати docker compose up -d
без примусового пересоздання, щоб «уникнути даунтайму».
Деякий час здавалося, що це працює — бо більшість змін були змінами тегів образів, і Compose підтягував і перезапускав сервіси при виявленні нового образу.
Але змінні середовища — не образи. Критична конфігураційна зміна перемкнула feature-flag маршрутизації запитів. Половина фліта оновилася
(на вузлах, де контейнери випадково пересоздалися), половина — ні. Вийшов split-brain, де запити ішли різними шляхами залежно від VM.
Налагодження було болісним, бо Compose-файл виглядав правильно, .env — правильно, а контейнери — всі «up».
Баг був у процесі: вони оптимізували ту єдину дію, яка надійно застосовує зміни env.
Плейбук відновлення виявився простим: якщо змінено env — контейнери пересоздаються. Якщо ви прагнете безперервної роботи,
робіть це через балансувальники і поступові рестарти, а не сподівайтеся, що Compose виведе ваш намір.
Історія 3: Нудна, але правильна практика, що врятувала день
Команда B2B SaaS запускала Compose-стек для внутрішніх сервісів: метрики, джоб-ранери і легасі БД.
Вони не любили «хитрощі». Їхнє продакшн-розгортання вимагало трьох перевірок:
відрендерити конфіг, валідувати ID запущених образів і записати контрольну суму ефективного середовища.
Одної п’ятниці вмержили зміну, яка додала нову змінну RATE_LIMIT_MODE, використану в інтерполяції Compose
для вибору образу sidecar. Розробник додав її в .env.example, але забув у продакшн-джерелі.
CI-пайплайн теж її не експортував.
Job розгортання впав рано, бо їхній Compose-файл використовував ${RATE_LIMIT_MODE?must be set}.
Оце і є фокус: вони перетворили мовчазну порожню інтерполяцію на жорстку зупинку. Ніякого часткового деплою, ніякої містичної поведінки.
Вони виправили пайплайн, задеплоїли в понеділок, і ніхто не отримав пейджі. Було настільки нудно, що команда обурювалася.
Саме так розумієш, що все зроблено правильно.
Типові помилки: симптом → корінь → виправлення
1) Симптом: тег образу стає пустим або «latest» несподівано
Корінь: Відсутня або порожня змінна часу рендеру, Compose інтерполює в порожній рядок; або CI експортує порожню змінну, що перекриває .env.
Виправлення: Використовуйте обов’язкову інтерполяцію: image: myapp:${IMAGE_TAG?set IMAGE_TAG}. У CI відмовляйте, якщо IMAGE_TAG порожній. Валідуйте через docker compose config.
2) Симптом: «Я оновив .env, але контейнер не змінив поведінку»
Корінь: Контейнер не був пересозданий; запущений контейнер зберігає старе середовище.
Виправлення: Застосуйте зміни з docker compose up -d --force-recreate (або docker compose restart, якщо підходить, але пересоздання безпечніше для змін env). Перевірте через docker inspect ... Config.Env.
3) Симптом: прод використовує dev-налаштування хоча є prod.env
Корінь: Compose читає .env з поточного робочого каталогу, а не з потрібного шляху; або --env-file не передається в автоматизації.
Виправлення: У systemd/CI запускайте з директорії проєкту або вказуйте --env-file /srv/app/.env. Додайте перевірку контрольної суми env файлу під час деплою.
4) Симптом: автентифікація паролем не працює, хоча значення «виглядає правильно»
Корінь: CRLF або кінцеві пробіли в .env додають приховані символи (часто \r) у значення.
Виправлення: Нормалізуйте кінці рядків (sed -i 's/\r$//'), і валідуйте друком repr або hexdump значення у контрольованому тестовому контейнері.
5) Симптом: база «втратила» дані після редеплою
Корінь: Змінилася назва проекту (зміна імені каталогу, COMPOSE_PROJECT_NAME), створився новий том з іншим ім’ям.
Виправлення: Закріпіть назву проекту явно для продакшну. Аудитуйте docker volume ls і docker inspect mounts перед очищенням. Розглядайте імена томів як частину стану.
6) Симптом: змінні в контейнері не збігаються з тим, що в .env
Корінь: Плутанина між .env (рендеру) і env_file (runtime); або середовище хоста перекриває значення.
Виправлення: Визначте джерело авторитету. Для критичних runtime-значень використовуйте явні ключі environment: і подавайте їх із контрольованого env файлу. Для рендеру передавайте через --env-file та валідуйте вивід конфігурації.
7) Симптом: змінна з лапками поводиться дивно
Корінь: Лапки трактуються буквально або знімаються інакше, ніж очікували; різні парсери в ланцюжку.
Виправлення: Видаліть лапки в .env, якщо це можливо. Коли необхідні — перевірте через docker compose config і усередині контейнера.
8) Симптом: сервіс не стартує, мапінг портів нісенітниця
Корінь: Інтерполяція створила недійсний рядок порту (порожній, нечисловий, містить пробіли), але YAML все ще парситься.
Виправлення: Зробіть змінні обов’язковими і валідуйте порти в CI, грепаючи відрендерений конфіг. Використовуйте дефолти лише для безпечних дев-значень.
9) Симптом: «працює локально, падає в CI» з тим самим Compose-файлом
Корінь: Локальний shell експортує змінні, а CI — ні; або CI використовує іншу локаль/кінці рядків; або CI запускає з іншого каталогу.
Виправлення: Зробіть джерело env явним у CI. Друкуйте docker compose config (або принаймні релевантні рядки) і забезпечте детермінованість.
10) Симптом: секрети з’являються в логах або підтримці
Корінь: Зберігання секретів у .env та друк відрендереного конфігу або контейнерного env під час налагодження; змінні оточення легко витікають у логи, дампи і процес-лістинги.
Виправлення: Використовуйте Compose secrets коли можливо, або монтуйте файли з креденціалами з жорсткими правами. В інструментах інцидентів за замовчуванням редагуйте виводи env.
Чеклисти / покроковий план для продакшну
Чеклист A: Зробіть інтерполяцію Compose детермінованою
- Зафіксуйте джерело env: В автоматизації завжди запускайте з явним шляхом
--env-fileі фіксованим робочим каталогом. - Зробіть критичні змінні обов’язковими: Використовуйте
${VAR?message}для тегів образів, зовнішніх endpoint-ів і назв проектів. - Припиніть експортувати випадкові змінні: Очистіть середовище у CI job-ах. Якщо змінна потрібна — встановіть її явно.
- Рендерьте та порівнюйте: Зберігайте вихід
docker compose configяк артефакт збірки і робіть diff проти попередніх деплоїв.
Чеклист B: Зробіть середовище контейнера аудитованим
- Документуйте контракт: Перелічіть обов’язкові runtime-змінні для кожного сервісу (імена, призначення, дозволені значення).
- На користь явного
environment:: Це робить контракт видимим під час рев’ю коду. - Використовуйте
env_fileдля масових значень, а не для містичних речей: Тримайте його мінімальним і структурованим. Уникайте змішування «dev» і «prod» в одному файлі. - Пересоздавайте при зміні env: Якщо змінено runtime env — контейнери мають бути пересоздані. Плануйте даунтайм/поступові рестарти відповідно.
Чеклист C: Не допускайте дрейфу стану (томи/мережі)
- Зафіксуйте назву проекту: Встановіть
name:в моделі Compose абоCOMPOSE_PROJECT_NAMEу контрольованому джерелі env. - Оголосіть томи явно: Використовуйте іменовані томи для стану; уникайте ненавмисних анонімних томів.
- Аудитуйте перед очищенням: Завжди переглядайте mount-и і посилання контейнерів перед видаленням томів.
Чеклист D: Трактуйте .env як продакшн-код
- Права:
chmod 600 .env, якщо файл містить чутливі дані. - Нормалізуйте кінці рядків: Примусовий LF у CI.
- Правила лінтингу: Ніяких пробілів навколо
=, без табуляцій, без кінцевих пробілів, передбачувані шаблони ключів. - Контроль змін: Вимагайте рев’ю для змін env і зберігайте історію (навіть якщо файл зберігається безпечно поза Git).
Операційні поради, що запобігають більшості інцидентів з .env
Використовуйте дефолти лише для зручності розробника, а не для безпеки продакшну
Дефолти на кшталт ${LOG_LEVEL:-debug} підходять для локальної роботи. У продакшні вони можуть перетворити відсутню конфігурацію
на неочікувану поведінку. Віддавайте перевагу явним значенням у продакшн-джерелах env і обов’язковим змінним для всього, що впливає на цілісність даних, автентифікацію або маршрутизацію.
Провалюйте раніше на хості, а не пізно в контейнері
Якщо змінна обов’язкова — відмовляйте на часі рендеру. Ви хочете, щоб деплой зупинився до того, як буде тягнутися образ, до того, як торкнеться томів або перезапустить щось.
Це дешевше й безпечніше.
Перестаньте трактувати секрети як «просто змінні оточення»
Змінні оточення тягнуть витоки. Вони витікають у звіти про помилки, debug-endpoint-и, списки процесів, випадкові bundle-і підтримки і людські скриншоти. Вони також лишаються в метаданих контейнера довше, ніж ви очікуєте.
Використовуйте механізми секретів, коли можете. Якщо не можете — відокремте секрети від небезпечних налаштувань і проєктуйте діагностичні команди так, щоб за замовчуванням редагували.
Зробіть конфігурацію спостережуваною
Система має повідомляти ефективну версію конфігурації, не виливаючи секретів. Контрольна сума конфігу, git-SHA, дайджест образу і небезпечна «режим»-змінна зазвичай достатні, щоб підтвердити, що система — це те, що ви думаєте.
FAQ
1) Чи Compose автоматично завантажує .env?
Зазвичай так — .env у директорії проєкту використовується як зручне джерело для інтерполяції змінних Compose і деяких налаштувань Compose.
Але «директорія проєкту» залежить від того, де ви запускаєте команду і як ви вказуєте файл Compose. Якщо запустити з неправильного каталогу, ви можете мовчки завантажити невірний .env або взагалі жодного.
2) Чи .env — це те саме, що env_file?
Ні. .env зазвичай впливає на інтерполяцію часу рендеру Compose. env_file інжектить
змінні в контейнер під час виконання. Файли виглядають однаково; семантика різна. Плутанина між ними — класичний режим відмови.
3) Чому моя зміна .env не застосувалась після docker compose up -d?
Бо контейнери не поглинають нові змінні оточення автоматично. Якщо Compose не пересоздав контейнер,
запущене середовище залишиться старим. Використовуйте docker compose up -d --force-recreate, коли змінено env,
і перевіряйте через docker inspect.
4) Що переважає: експортовані shell-змінні чи .env?
У багатьох налаштуваннях експортовані змінні в середовищі процесу, що запускає Compose, перекривають значення з .env.
Ось чому «працює на моїй машині»: ваш shell експортує щось, чого CI не робить, або навпаки. Робіть джерело env явним в автоматизації.
5) Чи можна мати кілька env-файлів?
Так, але будьте навмисними в їх призначенні: один для рендеру (передається з --env-file) і, можливо, один або кілька для runtime-інжекції (env_file: на сервіс). Уникайте нашарувань стільки, щоб ніхто не міг передбачити результат.
6) Чому мій додаток бачить лапки у значеннях?
Тому що ваш парсер може трактувати лапки як літеральні. Формат .env не є універсальним стандартом і різні інструменти інтерпретують лапки й екрани по-різному. Якщо вам потрібні спеціальні символи, тестуйте точний шлях: рендер через docker compose config і runtime через docker inspect.
7) Як не допустити пустих змінних у продакшн?
Використовуйте обов’язкову інтерполяцію (${VAR?message}) для критичних значень і додавайте CI-перевірки, що відмовляють, якщо
відрендерений конфіг містить порожні теги образів, порти або імена хостів. Це одне з найефективніших виправлень.
8) Чому повторний деплой створив нові томи і «видалив» дані?
Ймовірно, змінилася назва проекту. Compose префіксує імена томів і мереж назвою проекту, яка походить від імені каталогу, явної конфігурації або середовища.
Зафіксуйте її для продакшну, щоб томи залишалися стабільними. Перед очищенням переконайтеся, що DB-контейнер приєднаний до очікуваного тому.
9) Чи безпечно друкувати docker compose config в CI-логах?
Не завжди. Якщо ви інлайните секрети в Compose-файлі або інтерполюєте їх у поля, що показуються у виводі, можна випадково злиті креденціали.
Якщо потрібно друкувати конфіг — редагуйте чутливі ключі або друкуйте лише конкретні рядки (референси образів, порти, небезпечні налаштування).
10) Коли використовувати Compose secrets замість env-змінних?
Використовуйте secrets коли можете: креденціали, API-токени, приватні ключі — усе, що ви пошкодуєте бачити в логах або дампах.
Змінні оточення підходять для нечутливої конфігурації і feature-toggle-ів. Якщо змушені тримати секрети в env — обмежте права доступу і мінімізуйте місця, де вони відображаються.
Наступні кроки, які ви можете зробити цього тижня
-
Додайте «render check» у CI: запускайте
docker compose configі відмовляйте на порожніх критичних полях
(теги образів, порти, хости). Зберігайте відрендерений конфіг як артефакт з редагованими секретами. -
Зробіть критичні змінні обов’язковими: перетворіть
${VAR}на${VAR?set VAR}для
ключових точок інтерполяції в продакшні. - Зафіксуйте назву проекту в продакшні: припиніть випадковий дрейф томів і мереж. Трактуйте її як стан.
-
Стандартизуйтесь щодо запуску деплою: фіксований робочий каталог, явний
--env-file,
і політика: зміни env вимагають пересоздання або поступового рестарту. -
Перестаньте зберігати секрети в випадкових .env: перенесіть їх у механізм секретів або в змонтовані файли і
налаштуйте діагностичні інструменти так, щоб не витікали під час інцидентів.
Docker Compose працює. Проблема — у непроявлених припущеннях навколо .env.
Робіть змінні явними, спостережуваним відрендерений конфіг і верифікуйте середовище контейнера.
Тоді наступна «містична регресія» стане п’ятихвилинним diff’ом замість уїк-енду дебагу.