feat: support *_FILE env variables for secrets - #209
Conversation
Any variable known to the config schema can be provided in a file by setting <VARIABLE>_FILE (e.g. APP_SECRET_FILE=/run/secrets/app_secret), the convention official postgres/mysql images use, so secrets can be mounted from Docker Compose `secrets:` instead of being kept in plaintext .env. Resolution happens in every process that reads env directly: env validation (api/scheduler/processor), prisma.config.ts (migrate deploy, db seed) and the rescue CLI. Setting both <VARIABLE> and <VARIABLE>_FILE aborts the startup, values are never logged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
PR Summary
|
Greptile SummaryThis PR adds Docker-style
Confidence Score: 4/5The PR should not merge until Prisma commands reject empty secret files consistently instead of failing later or silently falling back to another connection. The application, CLI, and seed paths enforce the advertised empty-secret contract, but the production migration path uses separate logic that accepts an empty value and can silently replace an empty direct connection URL. Files Needing Attention: prisma.config.ts Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
Secret["X_FILE environment variable"] --> AppLoader["Shared secret loader"]
Secret --> PrismaLoader["Prisma config loader"]
AppLoader --> Nest["Nest configuration validation"]
AppLoader --> CLI["CLI and seed clients"]
PrismaLoader --> Migrate["Prisma migrate deploy"]
PrismaLoader --> Seed["Prisma db seed"]
AppLoader --> Env["process.env X"]
PrismaLoader --> Env
Reviews (1): Last reviewed commit: "feat: support *_FILE env variables for s..." | Re-trigger Greptile |
The Nest-side loader already refuses a file that holds only newlines. The inlined copy in prisma.config.ts did not, so an empty DIRECT_URL_FILE was silently replaced by DATABASE_URL and an empty DATABASE_URL_FILE surfaced as an unrelated datasource error. Both now fail with the same message as the loader. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Opening this as a PR for concreteness — happy to move the discussion to an issue first, as CONTRIBUTING asks, if you prefer.
Problem
Every secret the panel needs —
APP_SECRET,DATABASE_URL(which carries the database password),TELEGRAM_BOT_TOKEN,METRICS_PASS,WEBHOOK_SECRET_HEADER— can only be passed as a plain environment variable, so on a Docker/Podman host they sit in a plaintext.envnext to the compose file and show up indocker inspectand in every process environment.Docker Compose already has
secrets:for this: the value is mounted as a file in/run/secrets/<name>. The officialpostgres/mysqlimages consume it through the<VARIABLE>_FILEconvention. This PR teaches the panel the same convention.What it does
For any variable
Xknown to the config schema, the value can be supplied in a file by settingX_FILE:XandX_FILEboth set → startup aborts with a clear message instead of silently picking one;X_FILEpointing at an unreadable or empty file → startup aborts; the message carries the path and the errno, never the contents;Backward compatibility
Nothing happens unless a
*_FILEvariable is set — the helper walks the known keys, finds no*_FILE, and returns the config untouched. Existing.envdeployments behave exactly as before.Why resolution sits in three places
Resolved values go into
process.envas well as into the validated config, because several processes readprocess.envdirectly:common-config.module.ts(validate)PrismaClientsees the resolvedDATABASE_URLprisma.config.tsprisma migrate deployandprisma db seedrun fromdocker-entrypoint.shas a separate process before the app starts. Inlined (~10 lines) on purpose: only this file, notsrc/, is copied into the runtime imagecli.ts,config.seed.tsPrismaClient/Redisfromprocess.envat import timeDoing it in
docker-entrypoint.shalone would not cover the app:docker-compose-advanced-prod.ymlsetsentrypoint: []for the rest-api and processor services, so those containers never run that script.Compose example
The variables handed over to secrets must then be removed from
.env.postgressupportsPOSTGRES_PASSWORD_FILEnatively, so the db service can share the same secret files.How it was verified
npm run build,npm run build:seedandoxlintare clean;oxfmt --checkreports the same 18 pre-existing files asmaindoes, none of them touched here.*_FILEread with trailing newline stripped; both set → throws without leaking either value; missing file → message has path +ENOENTand no contents; empty file → throws; empty*_FILEtreated as unset.node dist/app.jswith plainAPP_SECRET/DATABASE_URLand withAPP_SECRET_FILE/DATABASE_URL_FILE— both pass env validation and reachNestFactory(they stop later at Redis/DB, which were not running locally);❌ APP_SECRET and APP_SECRET_FILE are both set...; missing file →❌ APP_SECRET_FILE points to "...", which can not be read: ENOENT;prisma migrate deploywith onlyDATABASE_URL_FILEset connects to the host from the file (P1001at that address); the same env without this change fails withP1012: Environment variable not found: DATABASE_URL;node dist/cli.jsandnode dist/seed.jswith onlyDATABASE_URL_FILEset pick the URL up from the file.Happy to trim the scope (drop the CLI/seed call sites, reword the
.env.samplenote) if you would rather keep the surface smaller.🤖 Generated with Claude Code