# Секреты приложений через `*_FILE`, а не inline `environment:` **Приоритет:** средний miniflux сделан правильно — роль `secrets` кладёт каждую переменную vault в отдельный файл, каталог монтируется `:ro`, а compose ссылается на `*_FILE` (`files/miniflux/docker-compose.template.yml`). Остальные держат секреты прямыми значениями в `environment:`: их читает любой член группы `docker`, любой процесс с доступом к `/proc//environ`, и они попадают в `docker inspect`. tuwunel из списка убран: в его compose секретов нет (сверено 2026-07-25). memos и calibre тоже чистые. ## Матрица поддержки (проверено по исходникам, 2026-07-25) Проверялись **закреплённые в compose версии**, не `main`. | Приложение | Секрет | Файл | Механизм | |---|---|---|---| | **outline** `1.9.2` | `SECRET_KEY`, `UTILS_SECRET`, OIDC, SMTP | да | generic: `environment.ts` оборачивает `process.env` в Proxy, пустая переменная читается из `_FILE`. С v1.7.0 | | | пароль postgres | да, с оговоркой | `DATABASE_URL_FILE` выносит URL целиком; чтобы вынести только пароль — раздельные `DATABASE_HOST/PORT/NAME/USER` + `DATABASE_PASSWORD_FILE` (взаимоисключимы с `DATABASE_URL`) | | | пароль сайдкара postgres | да | штатный `POSTGRES_PASSWORD_FILE` официального образа | | **wakapi** `2.17.5` | salt, SMTP, OIDC | да | два пути: generic `_FILE` в `loadSecretFiles()` (с 2.17.2, в README список устарел) либо `config.yml` в `/app/config.yml` | | **gitea** `1.27.0` | SMTP | да, частично | `GITEA__mailer__PASSWD__FILE`, работает для любого ключа `app.ini`, с 1.20 | | **gramps** `26.7.0` | `GRAMPSWEB_SECRET_KEY` | да | entrypoint читает `/app/secret/secret`, если переменная не задана. **Путь уже смонтирован** в нашем compose | | | SMTP | нет | обходной путь есть, но недокументированный: `config.cfg` исполняется Flask как Python, значит `EMAIL_HOST_PASSWORD = open(...).read()` сработает | | **authelia** `4.39.20` | jwt-секрет, session, ключ хранилища, hmac OIDC, SMTP | да | `AUTHELIA_..._FILE` | | | jwks-ключ, `client_secret` × 4 | нет через `_FILE` | `IsSecretKey()` отвергает ключи с `[]`. Покрывается template-фильтром: `X_AUTHELIA_CONFIG_FILTERS=template` + `{{ secret "путь" }}` (с 4.38) | | **tududi** `1.2.4` | все четыре | **нет** | всё через `process.env`; ни `_FILE`, ни конфига, ни CLI | | **wanderer** `0.18.3` | мастер-ключ meilisearch | **нет** | только CLI/env/инлайн в `config.toml`; апстрим отклонил запрос в 2023 | | | ключ шифрования pocketbase | **нет** | собственная переменная wanderer, не штатный `--encryptionEnv` pocketbase; ENTRYPOINT в exec-форме, подстановку не сделать | ## Что из этого следует Самое дешёвое — **gramps**: каталог `/app/secret` уже смонтирован, достаточно записать туда значение и убрать переменную из `environment:`. Оба сервиса (`gramps_app` и `gramps_celery`) читают тот же файл через общий entrypoint. **Authelia** выгоднее переводить не на `_FILE`, а целиком на template-фильтр: `_FILE` покрывает пять полей из семи, фильтр — все разом, и смешивать их в одном поле нельзя (`errFmtSecretAlreadyDefined`). Отдельная находка: четыре `client_secret` штатно хранятся **хэшем** `$pbkdf2-sha512$...` (у нас уже есть `inv authelia-gen-secret-and-hash`), плейнтекст с 4.38 вызывает warning валидатора. То есть эти поля можно вообще перестать считать секретами. **Gitea** даёт частичный выигрыш: секрет уходит из `environment:`, `docker inspect` и окружения процесса, но `environment-to-ini` пишет его открытым текстом в `app.ini` на диске — открытая проблема апстрима (#35316). **tududi и wanderer не умеют ничего.** Для них остаётся либо принять env, либо перенести переменные в файл на диске: у tududi `backend/app.js` зовёт `dotenv` при рабочей директории `/app/backend`, поэтому смонтированный туда `.env` уберёт секреты хотя бы из `docker inspect` — в отличие от `env_file:`, где они попадают в `Config.Env`. У wanderer доступен только `env_file:`, то есть косметика. Попутная находка в закомментированном OIDC-блоке wakapi (ловушка на будущее, не живой баг): имена `WAKAPI_OIDC_PROVIDER_*` устарели, в 2.17.5 разбирается `WAKAPI_OIDC_PROVIDERS_(\d+)_(...)`; кроме того `renameEnvVars()` режет строку по первому `=`, так что секрет с base64-паддингом через env поедет битым. Оба довода — за `config.yml`. ## Docker secrets — разобрано, покрытия не расширяет `secrets:` в compose вне swarm — это bind-mount файла в `/run/secrets/`, то есть **тот же способ доставки**, что и `*_FILE`. Приложение, умеющее только переменную окружения, из `/run/secrets` не прочитает — ограничение в приложении, а не в доставке. На таблицу выше не влияет. Что даёт: секрет объявляется на сервис, а не монтируется каталогом. У miniflux сейчас `{{ secrets_dir }}:/secrets:ro` висит на обоих сервисах, и postgres видит все шесть файлов, включая секреты OIDC, хотя нужен ему один пароль. Улучшение поверх образца, а не замена ему. `uid`/`gid`/`mode` вне swarm игнорируются — права остаются с хост-файла, их ставит роль `secrets`. ## Границы задачи Она про то, где секрет **лежит в рантайме** (env против файла). Про то, чтобы он не печатался в терминал при деплое, — [no-secrets-in-playbook-output](no-secrets-in-playbook-output.md), и она идёт первой: `no_log` дёшев, закрывает все приложения разом и не зависит от этой матрицы. Для tududi и wanderer он вообще единственный ответ. Матрица показала, что задача переросла в эпик: каждая строка — отдельное приложение со своим механизмом, и каждое мерджится независимо и само по себе даёт пользу. Договорились разбить по приложениям отдельным заходом (2026-07-25); до этого задачу в работу не брать целиком. Из sec-ревью (fable, 2026-07-13). Связка с [service-users-docker-group](service-users-docker-group.md). Связано: files/outline, files/wakapi, files/tududi, files/wanderer, files/gramps, files/gitea, files/authelia, files/miniflux (образец), roles/secrets.