Если вы храните пароли БД, токены API и ключи в обычных env-переменных, вероятность утечки выше, чем кажется. В продакшене утечки чаще происходят не от «хакеров в плаще», а из‑за логов, дампов и невнимательного девелопера. У Docker и Compose есть штатные механизмы для работы с секретами, а значит — можно минимизировать поверхность атаки и упростить ротацию. Разбираем по шагам, без теории ради теории.
Почему env-переменные с секретами — плохая идея
Env-переменные исторически удобны: положил DB_PASSWORD в .env или environment: и поехали. Но для секретов это компромисс с безопасностью. Ключевые проблемы:
- Просматриваемость процессом. Переменные окружения видны процессу и часто любой с достаточными правами может их получить через
/proc/<pid>/environили инструменты диагностики. - Логи и дампы. Секреты легко утекут в логи приложения, CI/CD, отладочные дампы, при панике/stacktrace.
- Docker inspect. Значения из
environmentпопадут в метаданные контейнера и могут быть показаны командам диагностики. - Файлы
.env. Обычно лежат рядом сcompose.yml, и их часто коммитят в репозиторий или пересылают в мессенджеры. - Кэш сборки. Путают
ARGиENV, затем секреты случайно попадают в слои образа при сборке.
Гораздо безопаснее подавать чувствительные данные в контейнер файлами, причём с жёсткими правами и минимальным временем «жизни» в памяти процесса. Здесь на сцену выходят secrets.
Docker secrets: Compose против Swarm
Есть два близких, но разных случая:
- Compose (standalone). Современный
docker composeподдерживает секциюsecrets:. Compose монтирует секреты в контейнер как файлы (по умолчанию в/run/secrets/<name>) с правами чтения только для владельца. Источник секрета — локальный файл на хосте. Шифрования «на диске Docker» нет, поэтому защищаем сам хост и каталог с секретами. - Swarm. Секреты хранятся в менеджерах кластера и передаются по TLS только узлам, где они нужны. Важно: секрет нельзя обновить «на месте» — создаём новый и переобновляем сервис. Есть rolling update из коробки.
В статье фокус на Compose, как самом распространённом сценарии на одиночных серверах и маленьких кластерах. На одиночном сервере (например, на VDS) это быстрый и практичный минимум безопасности.

Базовый пример: PostgreSQL и приложение на Compose
Задача: поднять PostgreSQL и приложение так, чтобы пароль не «светился» в environment, а лежал в /run/secrets. Официальные образы многих СУБД и приложений поддерживают «файловые» переменные формата NAME_FILE — это удобно.
Готовим секреты на хосте
Секрет — это просто файл с содержимым. Важно, чтобы права не были слишком широкими, и чтобы редактор/оболочка не добавили перевод строки.
# создаём каталог для секретов проекта
mkdir -m 0700 -p ./secrets
# пароль без завершающего \n
printf "%s" "S3cr3t_Pg_Pass" > ./secrets/db_password
chmod 0400 ./secrets/db_password
Права 0400 на файл и 0700 на каталог — здравый минимум, чтобы случайно его не прочитали другие пользователи на хосте.
Compose-файл с secrets
Ниже пример compose.yml. PostgreSQL прочитает пароль из файла через POSTGRES_PASSWORD_FILE. Приложению передаём путь к файлу пароля БД, чтобы оно само открывало файл при старте.
version: "3.9"
secrets:
db_password:
file: ./secrets/db_password
services:
db:
image: postgres:16
environment:
POSTGRES_USER: app
POSTGRES_DB: app
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- source: db_password
target: db_password
mode: 0400
volumes:
- dbdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d app"]
interval: 10s
timeout: 5s
retries: 5
app:
image: ghcr.io/example/app:latest
environment:
DB_HOST: db
DB_USER: app
DB_NAME: app
DB_PASSWORD_FILE: /run/secrets/db_password
depends_on:
db:
condition: service_healthy
secrets:
- source: db_password
target: db_password
mode: 0400
# пример: запускаем 2 реплики для обновлений без простоя
deploy:
replicas: 2
volumes:
dbdata:
Ключевые моменты:
secretsобъявляются на верхнем уровне и подключаются в каждую службу черезservice.secrets.targetзадаёт имя файла в контейнере, обычно в/run/secrets. Compose монтирует файл только на чтение.- Опция
mode— это права внутри контейнера (например,0400). - Для БД используем
POSTGRES_PASSWORD_FILE. Для своего приложения — считываем файл в рантайме.
Проверить, как секрет появился в контейнере, можно так:
docker compose up -d
docker compose exec app ls -l /run/secrets
Подходит связка healthcheck + поэтапный рестарт. Подробно про проверку готовности и политику перезапуска — в разборе healthcheck и restart-policy.
Как читать секрет из файла в приложении
Идеально, когда приложение умеет само читать секреты из файлов. Если нет — используйте тонкий стартовый скрипт, который читает файл и записывает его в приватный конфиг приложения или запускает процесс с нужным аргументом, не печатая значение в логи.
#!/bin/sh
set -eu
# читаем файл пароля и сохраняем в локальный конфиг
DB_PASS="$(cat "/run/secrets/db_password")"
# пример: генерируем конфиг приложения
cat > /app/config.local <<EOF
[db]
user=app
password=${DB_PASS}
host=db
name=app
EOF
chmod 0600 /app/config.local
# запускаем приложение ровно как обычно
exec /app/bin/server --config /app/config.local
Здесь пароль побывает в памяти процесса оболочки, но не попадёт в переменные окружения и не «засветится» в ps. Следите, чтобы скрипт не делал echo секретов в логи.
Ротация секретов в Compose
В Compose нет «горячей» замены секрета внутри уже работающего контейнера. Если поменять содержимое исходного файла на хосте, старый контейнер продолжит видеть прежний инстанс файла. Надёжный путь — создать новый секрет, обновить сервис и пересоздать контейнеры. Чтобы не уронить трафик — готовим стратегию.
Паттерны ротации без простоя
- Две реплики + последовательное обновление. Разверните сервис в 2+ экземплярах через
deploy.replicasили масштабирование. Обновляйте по одному контейнеру, проверяя здоровье. Подойдёт для stateless-части. - Blue/Green. Поднимите новый сервис с новым секретом (например,
app_v2), прогрейте, переключите трафик в прокси и затем снимите старый. - Двойная учётка БД. Создайте нового пользователя/пароль в БД с теми же правами. Обновите приложение на новый секрет. После стабилизации отключите старого пользователя.
Покажу процесс на последовательном обновлении с двумя репликами.
Шаги ротации
- Создайте новый файл секрета. Назовём его версионированно.
printf "%s" "N3w_S3cr3t" > ./secrets/db_password_v2
chmod 0400 ./secrets/db_password_v2
- Добавьте секрет в compose. И временно подключите его вторым, если приложение может выбрать путь. Чаще проще заменить старый монтируемый секрет на новый.
secrets:
db_password:
file: ./secrets/db_password
db_password_v2:
file: ./secrets/db_password_v2
services:
app:
# ...
secrets:
- source: db_password_v2
target: db_password
mode: 0400
- Обновите сервис с минимальным простоем. Если у вас 2 реплики, перезапуск произойдёт поочерёдно.
docker compose up -d --no-deps --scale app=2 app
Флаг --no-deps исключит лишние перезапуски зависимостей. После обновления проверьте метрики и логи. Когда убедились, что всё стабильно, удалите старый секрет из файла Compose и приберите файл с диска.
- Удалите старый секрет из проекта. Никогда не храните «пенсионеров» бесконечно.
shred -u ./secrets/db_password
Обратите внимание: точная пошаговая ротация зависит от приложения. Если оно кеширует соединение к БД, поможет мягкая перезагрузка процесса (SIGHUP) или поэтапная пересборка реплик.

Ротация в Docker Swarm (кратко)
В Swarm секреты иммутабельны: вы не обновите значение, только создадите новый. Алгоритм тот же: создаём db_password_v2, привязываем к сервису, удаляем старый. Приятный бонус — rolling update из коробки.
# создаём новый секрет в Swarm
printf "%s" "N3w_S3cr3t" | docker secret create db_password_v2 -
# обновляем сервис на новый секрет (примерно)
docker service update --secret-rm db_password --secret-add source=db_password_v2,target=db_password,mode=0400 --update-parallelism 1 --update-delay 10s app
# после успешного обновления удаляем старый секрет
docker secret rm db_password
Контроль доступа, права и аудит
Несколько практических правил, которые экономят часы расследований:
- Права на хосте. Каталогу с секретами —
0700, файлам —0400. Владелец — пользователь, от имени которого запускаетеdocker compose. - Права в контейнере. Через
service.secrets[].modeзадайте0400. Проверяйте:ls -l /run/secrets. - Не логируйте секреты. Отключите отладочные дампы, фильтруйте параметры в middleware логгера, используйте маскировку значений.
- .gitignore и шифрование. Никогда не коммитьте «чистые» секреты. Если храните в репозитории — применяйте шифрование на уровне файлов и хранение ключей отдельно. В CI/CD держите секреты в безопасных хранилищах.
- Бэкапы. Секреты попадают в бэкап томов и каталогов. Шифруйте бэкапы и контролируйте доступы к ним.
- Учёт и сроки жизни. Ведите инвентаризацию: владелец, место применения, срок действия, дата последней ротации, контакт для уточнений.
Дополнительно продумайте сетевую сегментацию и фильтрацию трафика контейнеров — см. практику по настройке firewall для Docker (iptables/nftables).
От env к secrets: пошаговая миграция
- Найдите все места, где секреты в
environment.docker compose configпоможет увидеть финальную конфигурацию. - Проверьте поддержку
*_FILE. Многие официальные образы и популярные приложения уже умеют читать секреты из файлов. - Вынесите значения в файлы в каталоге
./secrets. Задайте жёсткие права и владельца. - Опишите
secrets:в Compose. Подключите их в соответствующие сервисы. Проверьте права в контейнере. - Сделайте деплой без простоя. Как минимум — 2 реплики сервиса, затем обновление по одной. Либо временный Blue/Green.
- Уберите старые env-переменные. После ввода секрета удалите «наследие» из
.env, конфигов и CI.
Частые вопросы
Где физически хранятся secrets в Compose?
В Compose-режиме источником служит ваш файл на хосте. Docker не шифрует его «сам по себе». Поэтому защищённость каталога, права и бэкапы критичны. В контейнере секрет виден как файл только на чтение, обычно под /run/secrets.
Можно ли обновить секрет без рестарта контейнера?
Как правило, нет. Даже если вы перезапишете исходный файл на хосте, контейнер продолжит видеть старое содержимое до его пересоздания. Некоторые приложения умеют перечитывать файл по сигналу (SIGHUP), если вы меняете не секрет в /run/secrets, а «ссылку» в своём конфиге, но это уже вне механизма Docker.
Чем отличаются secrets и configs?
Технически оба монтируются как файлы. Семантически secrets — чувствительные данные, которым всегда выставляют строгие права, не логируют и часто не попадают в образы. configs — публичные конфиги, которые безопасно хранить в репозитории.
Чеклист безопасности
- Секреты только файлами через
secrets, а не вenvironment. - Проверяйте права: каталог
0700, файлы0400, внутри контейнера0400. - Используйте
*_FILE, если образ это поддерживает. - Минимум 2 реплики для безостановочных обновлений или Blue/Green.
- Версионируйте секреты:
_v2,_2025-10, фиксируйте дату ротации. - Не храните секреты в чистом виде в Git и CI-логах, шифруйте бэкапы.
- Чистите старые секреты после успешной ротации — и в Compose, и на диске.
Итоги
Переменные окружения удобны, но для секретов рискованны. Перевод чувствительных значений на Docker secrets с монтированием файлов в контейнер, строгими правами и продуманной ротацией резко снижает вероятность утечки. В Compose это просто: источник — локальные файлы, потребитель — приложение через *_FILE или собственный конфиг. Для безостановочного обновления держите минимум две реплики, обновляйте по одной и сразу убирайте старые секреты. Чем раньше вы вычистите секреты из environment, тем спокойнее будут ночи дежурного админа.


