Конфигурация и секреты
Один и тот же образ должен работать и в 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без реальных значений.