Пріоритет змінних оточення Docker: чому ваша конфігурація ніколи не та, якою здається

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

Ви розгортаєте контейнер. Він завантажився. Він працює. Але поводиться не так. У логах видно, що він підключається до неправильної бази даних, використовує невірний рівень логування або прослуховує не той порт. Ви дивитеся на свій docker-compose.yml, ніби він щойно збрехав вам у лице.

Швидше за все, так і сталося — ненавмисно, через пріоритети. Docker і Docker Compose мають кілька шарів, які можуть встановлювати «ту ж» змінну оточення, і переможе далеко не той, кого ви очікували. Виправлення не в героїзмі: воно в знанні правил і в інструментуванні істини.

Ментальна модель: три різні проблеми зі “змінними оточення”

Більшість плутанини зі змінними оточення в Docker походить від змішування трьох окремих механізмів, які лише виглядають пов’язаними:

1) Середовище виконання контейнера (що процес фактично бачить)

Це набір змінних всередині запущеного контейнера, видимий PID 1 та іншим через env або /proc/1/environ. Воно походить із конфігурації контейнера Docker, яка сама може братися з дефолтів іміджа, Compose, параметрів CLI та інших входів.

2) Інтерполяція в Compose (що Compose підставляє в YAML)

Compose також використовує змінні оточення на вашому хості для заміни плейсхолдерів ${VAR} у YAML. Така підстановка відбувається до створення будь-якого контейнера. Це не те саме, що встановлення середовища виконання контейнера.

Саме тут люди обпікаються: вони ставлять environment: у Compose і вважають, що це також вплине на будь-який ${VAR} в файлі. Це не так.

3) Шарування конфігурації додатка (що ваш додаток вирішує дотримуватися)

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

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

Факти та історія: як ми дісталися до цього безладу

  • Docker не винайшов конфігурування через env; «12-factor app» популяризував змінні оточення для конфігурації на початку 2010-х, а контейнери зробили цю практику відчутною як універсальну.
  • ENV у Dockerfile з’явився раніше за Compose; автори образів закладали дефолти задовго до того, як більшість команд стандартизували розгортання через Compose-файли.
  • Інтерполяція змінних у Compose моделювалася за звичками shell: ${VAR}, значення за замовчуванням і «витягнути з поточного оточення». Зручно для розробки, погано для відтворюваності.
  • Ранні реалізації Compose автоматично підвантажували файл .env, і ця поведінка стала м’язовою пам’яттю — навіть коли команди почали розділяти файли .env для різних середовищ.
  • У Docker є дві концепції «env file»: CLI-параметр --env-file і Compose-поле env_file:. Вони схожі на вигляд, але не взаємозамінні, і в крайніх випадках поводяться по-різному.
  • Специфіка OCI зберігає Env у конфігурації іміджа; ENV в Dockerfile стає метаданими, запеченими в образі, і бере участь у runtime-мерджингу.
  • Управління секретами стало масовим після витоків через env; змінні оточення легко вивести, залогувати або випадково відкрити через ендпоїнти для налагодження. «Зручно» — не те саме, що «безпечно».
  • Compose еволюціонував у специфікацію; різні реалізації (плагін docker compose vs старий docker-compose) історично мали розбіжності в поведінці. Гострі краї здебільшого згладжені, але спадкові звички лишилися.

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

Карти пріоритетів: docker run, Dockerfile, Compose та інтерполяція

Dockerfile ENV vs рантайм-оверрайди

Автори образів встановлюють дефолти через ENV у Dockerfile. Вони стають Config.Env образу. Під час виконання Docker зливає змінні оточення з кількох джерел. Правило, яке варто запам’ятати:

Налаштування під час запуску переважають дефолти образу.

Отже, якщо в образі є ENV LOG_LEVEL=info і ви запускаєте:

cr0x@server:~$ docker run --rm -e LOG_LEVEL=debug alpine:3.20 env | grep LOG_LEVEL
LOG_LEVEL=debug

Пріоритети docker run (практичний погляд)

Коли ви стартуєте контейнер через docker run, ефективне середовище фактично формується так:

  1. Дефолти образу (ENV у Dockerfile)
  2. Змінні з --env-file (якщо використано)
  3. Явні прапорці -e KEY=VALUE перекривають попередні значення

Є нюанси (наприклад, -e KEY означає «взяти значення з клієнтського оточення»), але операційно: явне перемагає неявне.

Compose одночасно грає дві різні гри пріоритету

Compose ускладнює ситуацію, бо грає одразу дві ролі:

  • Інтерполює змінні в Compose-файл (на стороні хоста): вирішення ${VAR}.
  • Визначає runtime-середовище сервісу (всередині контейнера): environment:, env_file: плюс те, що вже в образі.

Пріоритет інтерполяції Compose (на стороні хоста)

Для підстановки ${VAR} у YAML Compose зазвичай слідує такій інтуїції:

  1. Змінні з вашого shell-оточення (де ви запускаєте docker compose)
  2. Змінні з проектного файлу .env (якщо він присутній у робочій директорії / директорії проекту)
  3. Значення за замовчуванням у виразі, наприклад ${VAR:-default}

Якщо запам’ятати одну річ: environment: не підживлює інтерполяцію. Якщо ви пишете:

cr0x@server:~$ cat docker-compose.yml
services:
  api:
    image: alpine:3.20
    environment:
      DB_HOST: db
    command: ["sh", "-lc", "echo ${DB_HOST}"]

Те ${DB_HOST} вирішується на хості, а не всередині контейнера. Якщо на хості DB_HOST не встановлений (і ваш .env також), ви отримаєте порожній рядок або попередження залежно від версії/налаштувань Compose.

Пріоритет середовища виконання в Compose (всередині контейнера)

Для середовища, яке потрапляє всередину контейнера, звичайний порядок переможця такий:

  1. Дефолти образу з ENV
  2. Змінні, підвантажені через env_file:
  3. Змінні, задані в environment:, перекривають env_file
  4. Деякі реалізації також дозволяють CLI-оверрайди через docker compose run -e, які б’ють по файлу

Також: якщо ви вказали кілька записів env_file, пізніші файли перекривають попередні. Це зручно для шарування. Це також шлях, яким ви випадково відправите налаштування staging до production, бо файл перетелефонували в diff.

Null, порожній і «присутній але пустий»

Існує неприємна різниця між:

  • Не встановлено: змінна не існує в оточенні
  • Порожній: змінна існує зі значенням ""
  • Літеральний рядок “null”: існує і дорівнює null (поширено при шаблонізації YAML)

Compose YAML може виражати порожні значення так, що вони виглядають нешкідливо:

  • FOO: (порожнє)
  • FOO: "" (явний порожній рядок)

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

Цитата, що тримає вас у правді

Надія — не стратегія. — генерал Гордон Р. Салліван

Пріоритет змінних оточення — це місце, де надія помирає. Замість неї інструментуйте.

Де народжується дрейф конфігурації: підводні камені, що болять у проді

Підводний камінь: .env — це не ваше середовище контейнера

Файл .env, який використовує Compose для інтерполяції, — це зручна фіча. Він не автоматично підтягується в контейнер, якщо ви явно не вказали його через env_file: або не змепили значення в environment:.

Ось чому трапляється «на моєму ноуті працювало»: на ноуті у проектній директорії є .env, у CI його нема, а в production запускають Compose з іншої робочої директорії або через обгортку.

Підводний камінь: «Я змінив змінну, чому запущений контейнер не змінився?»

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

Підводний камінь: healthchecks і сайдкари бачать інший світ

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

Підводний камінь: проксі-змінні та «допоміжні» дефолти

HTTP_PROXY, NO_PROXY та інші часто встановлені на корпоративних ноутбуках, билд-агентах і навіть в systemd-юнитах демона Docker. Вони приховано впливають на поведінку під час збірки і виконання. Ви отримуєте контейнери, що можуть виходити в інтернет лише в певних середовищах, і ніхто не знає чому.

Жарт №1: Змінні оточення як офісні плітки: розповсюджуються скрізь, рідко документуються і завжди з’являються в найгірший момент.

Підводний камінь: у вашого додатка свої пріоритети

Поширені шаблони:

  • Фреймворки, що надають перевагу конфіг-файлам над env змінними, якщо не встановлено перемикач «use env».
  • Бібліотеки, що читають DATABASE_URL, якщо він є, інакше складають значення з DB_HOST/DB_USER тощо.
  • Додатки, які неконсистентно трактують 0, false і no.

У продакшені вам не потрібна «магія». Потрібен явний конфігураційний контракт: яка змінна перемагає і як її валідовати під час старту.

Практичні завдання: команди, що показують, що справжнє (і що робити далі)

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

Завдання 1: Інспектуйте ефективне середовище контейнера (швидка істина)

cr0x@server:~$ docker inspect -f '{{range .Config.Env}}{{println .}}{{end}}' api-1 | sort | sed -n '1,10p'
DB_HOST=db-prod
LOG_LEVEL=info
PORT=8080
TZ=UTC

Що це означає: Це змінні оточення, записані в конфіг контейнера. Саме це процес отримує під час старту.

Рішення: Якщо тут значення неправильне — перестаньте звинувачувати додаток. Виправте Compose/параметри run/дефолти образу і пересоздайте контейнер.

Завдання 2: Перевірте, що процес реально бачить (на випадок, якщо entrypoint змінює env)

cr0x@server:~$ docker exec api-1 sh -lc 'tr "\0" "\n" < /proc/1/environ | sort | grep -E "DB_HOST|LOG_LEVEL|PORT"'
DB_HOST=db-prod
LOG_LEVEL=info
PORT=8080

Що це означає: Середовище PID 1. Якщо це відрізняється від docker inspect, щось всередині контейнера це змінило (entrypoint-скрипт, супервізор тощо).

Рішення: Якщо PID 1 відрізняється, аудитуйте entrypoint-скрипти та стартовий тулінг. Це проблема додатка/образу, а не Compose.

Завдання 3: Побачити повністю відрендерений конфіг Compose (інтерполяція вирішена)

cr0x@server:~$ docker compose config | sed -n '/services:/,/networks:/p' | sed -n '1,80p'
services:
  api:
    command:
    - sh
    - -lc
    - ./start-api
    environment:
      DB_HOST: db-prod
      LOG_LEVEL: info
    image: myorg/api:1.8.4

Що це означає: Це фінальна інтерпретація Compose після злиття файлів та розв’язання ${VAR}.

Рішення: Якщо docker compose config показує неправильне значення — ваші інтерполяційні входи неправильні (shell env, .env, дефолти) або файли-оверрайди не ті, що ви думаєте.

Завдання 4: Перелічіть контейнери і підтвердіть, що ви дебагуєте правильний

cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}'
NAMES     IMAGE              STATUS          PORTS
api-1     myorg/api:1.8.4    Up 2 hours      0.0.0.0:8080->8080/tcp
db-1      postgres:16        Up 2 hours      5432/tcp

Що це означає: Імена, образи, аптайм, порти. «Неправильна конфігурація» часто — це «неправильний контейнер».

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

Завдання 5: Перевірте, над яким проектом Compose ви працюєте

cr0x@server:~$ docker compose ls
NAME            STATUS              CONFIG FILES
billing         running(6)          /srv/billing/docker-compose.yml
billing-dev     running(6)          /home/cr0x/billing/docker-compose.yml

Що це означає: Імена проектів Compose — частина ідентичності. Той самий YAML, інший проект — інші контейнери.

Рішення: Якщо ви в неправильному проекті — зупиніться. Перейдіть в іншу директорію, використайте -p або приберіть дублі.

Завдання 6: Перевірте, чи контейнер пересоздавався після зміни конфігурації

cr0x@server:~$ docker inspect -f '{{.Name}} {{.Created}}' api-1
/api-1 2026-01-03T08:12:54.123456789Z

Що це означає: Час створення. Якщо ви відредагували конфіг о 09:00, а контейнер створено о 08:12, нічого не змінилося.

Рішення: Пересоздайте: docker compose up -d --force-recreate (або хоча б up -d, якщо Compose виявив різницю).

Завдання 7: Підтвердіть, які env-файли застосовано (і в якому порядку)

cr0x@server:~$ docker compose config --services
api
db
cr0x@server:~$ grep -nE 'env_file|environment' -n docker-compose.yml
14:    env_file:
15:      - ./env/common.env
16:      - ./env/prod.env
17:    environment:
18:      LOG_LEVEL: info

Що це означає: Кілька env-файлів означають шарування. Пізній виграє. environment: перекриває обидва.

Рішення: Якщо значення має бути авторитетним, помістіть його в environment: (або в один фінальний env-файл) і забезпечте порядок файлів.

Завдання 8: Виявити значення інтерполяції на боці хоста (звідки Compose бере значення)

cr0x@server:~$ env | grep -E '^DB_HOST=|^LOG_LEVEL='
LOG_LEVEL=debug

Що це означає: Ваш шел вже має LOG_LEVEL=debug. Якщо ваш Compose-файл використовує ${LOG_LEVEL}, ви щойно перезаписали продакшн-конфіг своїм історичним терміналом.

Рішення: Запускайте Compose із чистим оточенням для продакшен-операцій або явно задавайте потрібні змінні у контрольованому файлі.

Завдання 9: Показати, які змінні відсутні під час інтерполяції (виявити мовчазні пусті)

cr0x@server:~$ docker compose config 2>&1 | grep -i warning
WARNING: The "DB_PASSWORD" variable is not set. Defaulting to a blank string.

Що це означає: Compose підставив порожній рядок. YAML тепер валідний, але система — ні.

Рішення: Ставте це як помилку розгортання. Прив’яжіть CI до падіння при відсутніх змінних або використайте шаблони з обов’язковими змінними.

Завдання 10: Підтвердіть дефолти образу (ENV, запечений у образі)

cr0x@server:~$ docker image inspect myorg/api:1.8.4 -f '{{json .Config.Env}}'
["PORT=8080","LOG_LEVEL=warn","TZ=UTC"]

Що це означає: Образ постачається з LOG_LEVEL=warn. Якщо ви думали «unset означає дефолт», автор образу вже вирішив інакше.

Рішення: Або явно перекривайте в Compose, або видаліть дефолт з образу, якщо він викликає сюрпризи (краще — документація + явна конфігурація).

Завдання 11: Перевірте, чи змінна встановлена порожньою або не встановлена (всередині контейнера)

cr0x@server:~$ docker exec api-1 sh -lc 'if printenv OPTIONAL_FLAG >/dev/null 2>&1; then echo "present:[$OPTIONAL_FLAG]"; else echo "unset"; fi'
present:[]

Що це означає: Змінна існує, але порожня. Багато додатків трактують це як «сконфігуровано» і потрапляють у дивні гілки коду.

Рішення: Якщо пусте має поводитися як відсутнє — не встановлюйте її взагалі. Прибери її з environment: або з ваших env-файлів.

Завдання 12: Знайти джерело значення, дифуючи відрендерений конфіг із рантаймом

cr0x@server:~$ docker compose config --format json | jq -r '.services.api.environment'
{
  "DB_HOST": "db-prod",
  "LOG_LEVEL": "info"
}
cr0x@server:~$ docker inspect -f '{{range .Config.Env}}{{println .}}{{end}}' api-1 | grep -E 'DB_HOST|LOG_LEVEL'
DB_HOST=db-prod
LOG_LEVEL=info

Що це означає: Compose і рантайм погоджуються. Якщо поведінка все ще неправильна, додаток перевизначає конфіг або неправильно парсить.

Рішення: Зсуньте розслідування до додатка: логи запуску, ендпоїнти для дампу конфігурації, пріоритети бібліотек.

Завдання 13: Підтвердіть, що Compose вважає зміну (щоб уникнути no-op деплоїв)

cr0x@server:~$ docker compose up -d
[+] Running 0/0

Що це означає: Compose не побачив що робити. Ваша змінна могло не бути в дефініції сервісу (або була лише в інтерполяції, але вихід не змінився).

Рішення: Використайте --force-recreate або явно пересоздайте сервіс після підтвердження, що відрендерений конфіг дійсно змінився.

Завдання 14: Зауважити випадкове успадкування проксі (класична корпоративна пастка)

cr0x@server:~$ docker exec api-1 sh -lc 'env | grep -i proxy'
HTTP_PROXY=http://proxy.corp:3128
NO_PROXY=localhost,127.0.0.1,db

Що це означає: Контейнер використовує проксі. Це може ламати виклики сервісів, валідацію сертифікатів і впливати на латентність.

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

План швидкої діагностики

Це порядок дій, який мінімізує час до істини, коли контейнер «сконфігурований неправильно». Не імпровізуйте. Використовуйте фільтр.

Перший крок: визначте істину рантайму

  1. Підтвердіть цільовий контейнер: docker ps і перевірте name/image/uptime.
  2. Інспектуйте ефективне env: docker inspect ... .Config.Env.
  3. Перевірте env PID 1: docker exec ... /proc/1/environ.

Якщо рантайм-оточення неправильне — ви в зоні Docker/Compose. Якщо рантайм правильний — проблема в додатку.

Другий крок: підтвердіть відрендерений намір Compose

  1. Відрендеріть конфіг: docker compose config.
  2. Перевірте входи інтерполяції: ваше шел-оточення і проектний .env.
  3. Перевірте шарування: кілька compose-файлів, порядок env_file, перекриття environment.

Третій крок: підтвердіть, що зміна реально відправлена

  1. Час створення: docker inspect ... .Created.
  2. Пересоздайте за потреби: docker compose up -d --force-recreate для сервісу.
  3. Перевірте знову: інспектуйте env після пересоздання.

Жарт №2: У Docker єдине послідовне налаштування — це те, яке ви випадково не мали наміру перекрити.

Три корпоративні міні-історії (анонімізовано, технічно реальні)

Інцидент: хибне припущення («Compose env перекриває все, так?»)

Середня компанія запустила платіжний API в Docker Compose на декількох VM-хостах. Робочий процес виглядав чисто на папері: базовий docker-compose.yml плюс файл-оверрайд на кожне середовище. Додаток приймав конфіг через env змінні. Класика.

Під час п’ятничного деплою вклали нову змінну PAYMENTS_PROVIDER_TIMEOUT_MS. Інженер додав її в environment: для продакшен-оверрайду. Сервіс все одно таймаутив під навантаженням — потім почав агресивно повторювати запити, викликавши обмеження зверху.

Команда думала, що значення «не застосувалось». Вони збільшили його ще раз. Та сама поведінка. Справжня проблема була в тому, що образ вже мав ENV PAYMENTS_PROVIDER_TIMEOUT_MS=2000, а додаток мав конфіг-файл, запечений в образі, який мав пріоритет над env змінними, якщо не встановлено USE_ENV_CONFIG=true. У стенді цей прапорець встановлювали через шел-експорт розробника і інтерполювали в оверрайді. У продакшені цього не було. Compose підставив пусте. Контейнер стартував з дефолтною поведінкою: ігнорувати env-конфіг.

Вони дебагали провайдера, мережу і БД, перш ніж хтось виконав docker compose config і побачив попередження про відсутню змінну. Виправлення було нудне: зробити USE_ENV_CONFIG явним у дефініції сервісу Compose і валити деплой, якщо його немає. Потім пересобрали образ, щоб перестати відправляти неоднозначний конфіг-файл.

Висновок постмортему: не вважайте «змінна існує» рівнозначною «додаток використовує env». Це два різні контракти — і один із них ніколи не був написаний.

Оптимізація, що відбилася: «зменшимо дублювання конфігу спільним env-файлом»

Велика команда втомилась повторювати конфіг для 20 сервісів. Вони ввели спільний env/common.env і включили його через env_file: всюди. Потім додали env/prod.env, env/stage.env тощо. Шум у дифах зменшився. Люди святкували. Так починається історія.

Через три місяці додали новий сервіс. Хтось скопіював існуючий блок і забув включити env/prod.env. Сервіс піднявся з дефолтами з образу і з common.env. Він «працював», але підключався до staging-кешу, бо CACHE_HOST був у prod.env для більшості сервісів і у common.env для одного спадкового. Обидва значення були правдоподібними хостнеймами. Немає аварії — просто неправильні шляхи даних.

Вони намагались «виправити», перемістивши ще більше в common.env. Це збільшило площу ураження. Тепер випадкова відсутність або включення файлу змінювала поведінку у багатьох середовищах. Виник «дрейф середовища через включення файлів». Це особливий вид помилки: не опечатка, а відсутній шар.

Відновлення полягало в тому, щоб перестати вдавати, що один файл env підходить для всіх сервісів. Вони зберегли спільний файл, але обмежили його справді глобальними, неризиковими значеннями (часовий пояс, формат логів, загальні feature toggles), і вимагали від кожного сервісу мати явний файл для конкретного середовища. CI валідував, що кожен сервіс має очікувані джерела env. Дедуплікація лишилась, але з огорожами.

Нудна, але правильна практика: pin, render, verify (і вона врятувала день)

Менша команда підтримувала платформу підтримки клієнтів, де падіння помітне за хвилини. У них була параноїдна, але корисна звичка: кожен деплой продукував артефакт, що містив повністю відрендерений Compose-конфіг (docker compose config) і фінальний список env-змінних по сервісу, і вони зберігали це поруч з метаданими білду.

Одного дня API почав відхиляти запити, бо думав, що в «режимі технічного обслуговування». Цим керував MAINTENANCE=true. Ніхто його не встановлював. Ніхто цього не визнавав. Slack робив своє.

Вони порівняли поточний артефакт з попереднім. Відрендерений конфіг прямо показав, що MAINTENANCE=true був інтерполяційно підставлений із хост-оточення під час ручного хотфікс-деплою. Інженер експортнув його раніше для локального тесту і забув. Compose покірно підставив його в YAML, який використовував ${MAINTENANCE} для зручності.

Виправлення зайняло п’ять хвилин: перезапустити деплой із чистим оточенням і прибрати інтерполяцію хост-оточення для того прапорця. Урок запам’ятався, бо був вимірним: їхня «параноїдна» практика перетворила туманну загадку на один рядок у дифі. Ніякої героїки. Просто доказ.

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

1) Симптом: змінна правильна в docker-compose.yml, але контейнер має інше значення

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

Виправлення: Перевірте docker compose ls, підтвердіть час створення контейнера, потім docker compose up -d --force-recreate api.

2) Симптом: ${VAR} стає пустим, хоча ви задали його під environment:

Корінь: Плутанина між інтерполяцією на боці хоста і runtime-середовищем контейнера. Compose інтерполює з shell і .env, а не з environment:.

Виправлення: Перенесіть значення в хост-оточення (контрольоване) або припиніть інтерполювати і використайте літерали. Валідовуйте через docker compose config.

3) Симптом: значення відрізняється між staging і prod при тих самих Compose-файлах

Корінь: Різне шел-оточення під час деплою (CI-агент vs ручний шел) або різний .env у робочих директоріях.

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

4) Симптом: додаток поводиться так, ніби налаштування «увімкнене», навіть якщо ви встановили false

Корінь: Додаток некоректно парсить булеві значення («непорожній рядок — це truthy»), або використовує іншу назву змінної, ніж ви думаєте.

Виправлення: Підтвердіть, зливши ефективний конфіг на старті додатка. Використовуйте суворе парсування в додатку. Віддавайте перевагу 0/1, якщо додаток нестрогий.

5) Симптом: секрети з’являються в логах або support-бандлах

Корінь: Секрети, передані через env змінні, легко вивести через env, ендпоїнти для налагодження або дампи аварій.

Виправлення: Використовуйте Docker secrets (або змонтовані файли секретів) і передавайте шляхи, а не значення. Щонайменше, редагуйте і захищайте діагностику.

6) Симптом: запити випадково йдуть через проксі або відпадають тільки на деяких хостах

Корінь: Змінні проксі успадковані з хоста, systemd-юнита або CI-раннера.

Виправлення: Явно встановіть/скиньте HTTP_PROXY/NO_PROXY в Compose для продакшена. Перевірте всередині контейнера.

7) Симптом: Compose попереджає «variable not set, defaulting to blank string», але деплой все одно пройшов

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

Виправлення: Блокуйте деплой при відсутніх змінних, парсячи вивід docker compose config в CI або валідуючи список обов’язкових змінних перед запуском Compose.

8) Симптом: змінна з env_file не застосовується

Корінь: Вона перекрита environment:, пізнішим env_file або docker compose run -e.

Виправлення: Відрендерьте конфіг, перевірте порядок і вирішіть, який шар авторитетний. Зробіть це явним.

Контрольні списки / покроковий план

Контрольний список: зробіть пріоритет змінних оточення банальним (мета)

  1. Припиніть використовувати інтерполяцію хоста для runtime-налаштувань, якщо у вас немає контрольованого середовища деплою. Якщо воно змінюється — має змінюватися у версованому конфігу.
  2. Віддавайте перевагу явному environment: для критичних значень сервісу (ендпоінти БД, фіч-таґли, що можуть порушити безпеку, режими як maintenance тощо).
  3. Використовуйте env_file: для масових дефолтів, але тримайте його малим і передбачуваним. Уникайте «всіх помістити в один файл», який використовують усі сервіси.
  4. Використовуйте один, специфічний для середовища env-файл на сервіс, якщо мусите використовувати env-файли. Шарування добре, але воно вимагає дисципліни та валідації.
  5. Не відправляйте значущі дефолти в образ, якщо ви цього не маєте на увазі. Дефолти образу невидимі для читачів Compose і люблять вас дивувати.
  6. Робіть відсутні змінні фатальними. Порожній рядок рідко є безпечним дефолтом для креденшелів, ендпоінтів або фіч-тоглів.
  7. Пересоздавайте контейнери при зміні env. Випікайте це в процедурі деплою; не покладайтесь на людську пам’ять.
  8. Записуйте відрендерений Compose-конфіг для кожного деплою, щоб ви могли зробити диф між тим, що хотіли, і тим, що відправили.
  9. Тримайте секрети поза env змінними. Використовуйте файли/сервіси секретів, передавайте шляхи і аудитіть те, що ваші діагностики викидають.
  10. Додайте лог на старті або ендпоїнт, який друкує неконфіденційний конфіг (очищений), щоб ви могли підтвердити інтерпретацію додатка.

Покроково: коли потрібно безпечно змінити значення

  1. Змініть авторитетний шар (зазвичай environment: або один фінальний env-файл).
  2. Відрендерьте результат: запустіть docker compose config і підтвердіть, що значення з’явилося там, де ви очікуєте.
  3. Відправте зміну: docker compose up -d --force-recreate service.
  4. Підтвердіть на рантаймі: docker inspect і /proc/1/environ.
  5. Підтвердіть інтерпретацію додатком: прочитайте логи старту або зверніться до ендпоїнта статусу/конфігу.
  6. Запишіть правило пріоритету для цього налаштування (навіть у одне речення), щоб ніхто не повторив інцидент.

FAQ

1) Чи стає .env автоматично змінними оточення контейнера?

Ні. .env здебільшого використовується Compose для підстановки змінних у Compose-файл. Щоб інжектувати значення в контейнер, використовуйте env_file: або environment:.

2) Що перемагає: env_file чи environment?

environment перемагає. Якщо обидва визначають FOO, значення з environment: — те, що отримає контейнер.

3) Що перемагає: ENV в образі чи environment в Compose?

Compose environment перемагає. Дефолти образу — базовий шар; runtime-конфігурація їх перекриває.

4) Чому ${VAR} іноді підставляється в пустий рядок без помилки?

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

5) Я встановив змінну і перезапустив контейнер. Чому вона не застосувалась?

Перезапуск не змінює конфігурацію контейнера. Потрібно пересоздати контейнер, щоб Docker зберіг нові змінні у конфігурації контейнера.

6) Чи достатньо docker exec env, щоб знати, що сконфігуровано?

Це добре, але перевіряйте середовище PID 1 (/proc/1/environ), якщо підозрюєте, що entrypoint-скрипти або супервізори змінюють env. Також підтверджуйте через docker inspect, щоб побачити, що Docker вважає конфігом.

7) Чи безпечні змінні оточення для секретів?

Вони зручні, але не безпечні. Їх можна прогнати через списки процесів, дампи аварій, ендпоїнти налагодження і support-бандли. Краще використовувати монтажі файлів/секретів і передавати шляхи через змінні, якщо потрібно.

8) Чому різні машини дають різні відрендерені Compose-конфіги?

Бо інтерполяція залежить від середовища, де запускається Compose: shell-змінні, локальний .env і інколи різні робочі директорії або обгортки. Відрендерений конфіг — це артефакт збірки; ставтесь до нього як до артефакта.

9) Якщо мій додаток читає DATABASE_URL і також DB_HOST, що робити?

Виберіть один контракт і наполягайте на ньому. Якщо мусите підтримувати обидва, визначте суворий пріоритет у додатку і логгируйте, яке джерело перемогло під час старту (без виводу секретів).

10) Як зупинити шел розробників від впливу на продакшен-деплои?

Проводьте деплои з CI або з виділеного середовища деплою з санітайзнутим оточенням. Уникайте інтерполяції ${VAR} для runtime-критичних налаштувань, якщо входи не контрольовані і не провалідовані.

Висновок: наступні кроки, що зупиняють кровотечу

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

  1. Почніть використовувати docker compose config як перший клас артефакту деплою. Якщо воно не відрендерене — воно нереальне.
  2. Зробіть валідацію рантайму рутинною: docker inspect для конфіга контейнера, /proc/1/environ для істини процесу.
  3. Зведіть непотрібні шари: менше env-файлів, менше оверрайдів, менше «магічних дефолтів» у образах.
  4. Розбивайте деплой при відсутніх змінних. Попередження про порожні дефолти мають трактуватись як зламаний білд.
  5. Виносьте секрети з env змінних. Ваш звіт про інцидент у майбутньому скаже вам дякую.

Коли система поводиться дивно, найшвидший шлях рідко — глибша інтуїція. Це питати рантайм, що він зробив, а потім ускладнити людям випадково робити не те.

← Попередня
BIOS-коди звукових сигналів: діагностика апаратних відмов за допомогою звуку (і паніки)
Наступна →
Ubuntu 24.04: обмеження завантаження PHP — виправте upload_max_filesize там, де це справді важливо (випадок №10)

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