Конфигурация и секреты

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

Конфигурация снаружи образа — принцип, по которому образ остаётся неизменным артефактом, а всё, что отличает окружения (адреса БД, ключи, флаги), подаётся извне: переменными окружения, файлами и секретами.

Зачем это на практике

Образ — это собранный артефакт, который вы тестируете в staging и тот же самый катите в прод. Если внутрь зашиты адрес продовой базы или API-ключ, образ перестаёт быть переносимым: для другого окружения нужно пересобирать, а секрет навсегда остаётся в истории слоёв. Правильный подход: образ один, а конфигурация подаётся снаружи в момент запуска. Это прямо вытекает из методологии 12-factor app.

12-factor: конфигурация в окружении

Фактор III («Config») гласит: храните конфигурацию в окружении, а не в коде. Критерий простой — конфигурация это то, что меняется между деплоями (dev/staging/prod), а код одинаков везде. Адреса сервисов, учётные данные, режимы (DEBUG) — всё это конфигурация и должно жить вне образа. Тогда один артефакт ходит по всем стендам без пересборки.

Переменные окружения vs файлы конфигурации

Переменные окружения — самый простой способ передать настройку. При запуске:

docker run \
  -e APP_ENV=production \
  -e DATABASE_URL=postgres://db:5432/app \
  myapp:latest

В Compose их удобно вынести в .env и подставлять (а не хардкодить в compose.yaml):

services:
  app:
    image: myapp:latest
    environment:
      APP_ENV: ${APP_ENV}
      DATABASE_URL: ${DATABASE_URL}
    env_file:
      - .env

ENV-переменные хороши для коротких скалярных значений. Но для объёмной конфигурации (TLS-сертификаты, длинные YAML/JSON-настройки) удобнее файл, смонтированный томом — его проще читать, версионировать и не упираться в лимиты длины окружения:

docker run \
  -v /etc/myapp/config.yaml:/app/config.yaml:ro \
  myapp:latest

Суффикс :ro монтирует файл только для чтения. В коде приложение само решает приоритет: например, читает базовые значения из файла, а критичные переопределяет из переменных окружения.

Почему секреты не в образе и не в ENV

Секрет (пароль БД, токен, приватный ключ) нельзя класть в Dockerfile — ни через ENV, ни через COPY secret.txt. Причина в устройстве образа: он состоит из слоёв, и значение, попавшее в любой слой, остаётся в истории навсегда, даже если в следующей инструкции вы его «удалили». Любой, кто скачал образ, вытащит секрет командой docker history или распаковав слои.

Передавать секрет через ENV в рантайме тоже плохо: переменные окружения видны в docker inspect, утекают в логи и в дочерние процессы, а в дампах падений нередко печатается всё окружение целиком. Поэтому секреты подают отдельным, более защищённым каналом.

СпособПодходит для секретов?
ENV в Dockerfileнет — навсегда оседает в слоях образа
COPY secret в образнет — извлекается из слоёв
-e при запускенежелательно — видно в inspect/логах
файл-секрет / docker secretда — монтируется в память, не в образ

Docker secrets и файлы-секреты

Идея: секрет лежит на хосте (или в хранилище оркестратора) и монтируется внутрь контейнера как файл во временную область, а приложение читает его с диска. В Compose это блок secrets:

services:
  app:
    image: myapp:latest
    secrets:
      - db_password

secrets:
  db_password:
    file: ./db_password.txt

Docker смонтирует содержимое db_password.txt в файл /run/secrets/db_password внутри контейнера. Приложение читает оттуда — секрет не попадает ни в образ, ни в переменные окружения, ни в docker inspect. Удобный приём — переменная вида DB_PASSWORD_FILE=/run/secrets/db_password: в окружении лежит лишь путь к секрету (это не тайна), а само значение приложение читает из файла:

# внутри контейнера приложение делает примерно так
DB_PASSWORD=$(cat "$DB_PASSWORD_FILE")

В полноценном Swarm/Kubernetes секреты хранятся зашифрованными в хранилище кластера и доставляются в контейнер через tmpfs (в памяти, не на диске узла) — но потребительский контракт тот же: приложение читает секрет из файла под /run/secrets/.

Как это работает под капотом

Каждая инструкция Dockerfile, меняющая файловую систему (COPY, RUN, ENV), порождает отдельный read-only слой. Слои неизменяемы и складываются в стек — «удаление» файла в следующем слое лишь прячет его в финальной выборке, но данные физически остаются в нижнем слое и извлекаются. Именно поэтому секрет в любом слое скомпрометирован безвозвратно. Файлы-секреты обходят это: они не часть образа, а tmpfs-точка монтирования, наполняемая в момент запуска и исчезающая вместе с контейнером — на диск узла секрет не пишется.

Частые ошибки

  • ENV API_KEY=... в Dockerfile. Ключ навсегда в истории образа; вытащить можно через docker history.
  • Закоммитить .env в git. Файл с секретами утекает в репозиторий. Добавляйте .env в .gitignore, а в репозитории держите только .env.example без значений.
  • Считать ENV безопасным для секретов. Переменные видны в docker inspect, в логах и дочерних процессах.
  • Хардкодить значения прямо в compose.yaml. Файл попадает в git; выносите изменяемое в .env и подставляйте через ${VAR}.
  • Один образ на окружение. Если приходится пересобирать образ под прод — конфигурация утекла внутрь; вынесите её наружу.

Итоги

  • Образ — неизменный артефакт; всё, что отличает окружения, подавайте снаружи (12-factor, фактор Config).
  • ENV-переменные — для коротких значений; объёмную конфигурацию монтируйте файлом (:ro).
  • Секреты никогда не кладите в образ (слои сохраняют их навсегда) и старайтесь не передавать через ENV.
  • Используйте файлы-секреты / docker secrets: значение монтируется в /run/secrets/ и читается приложением с диска.
  • .env — в .gitignore; в репозитории — только .env.example без реальных значений.
Проверьте себя
1. Почему секрет нельзя класть в образ через ENV или COPY в Dockerfile?
AЭто замедляет сборку образа
BОбраз состоит из слоёв, и значение остаётся в истории слоёв навсегда — его извлекут через docker history, даже если файл потом удалить
CENV и COPY не поддерживают строки с паролями
DDocker автоматически шифрует такие значения, поэтому смысла нет
2. Что предписывает фактор Config из методологии 12-factor app?
AХранить всю конфигурацию в коде приложения
BСобирать отдельный образ под каждое окружение
CХранить конфигурацию в окружении (вне образа), чтобы один артефакт работал во всех стендах
DЗапрещать любые переменные окружения
3. Как приложение в контейнере получает значение из docker secret?
AОно зашито в слой образа на этапе сборки
BDocker монтирует секрет файлом (например в /run/secrets/), и приложение читает значение с диска
CСекрет всегда передаётся через переменную окружения -e
DСекрет печатается в docker logs при старте