Docker Secrets (*_FILE)¶
Every env var the three apps read supports a <NAME>_FILE variant that points at a file on disk instead of an inline value. The server reads the file at boot, trims one trailing newline, and treats the contents as if you had set <NAME> directly. It is the pattern Postgres, Redis, and a lot of other well-behaved container images use, and it is the cleanest way to keep secrets out of docker-compose.yml, .env, docker inspect output, and shell history.
When to reach for it¶
Three concrete cases:
- Docker Swarm secrets. Swarm mounts secrets as files under
/run/secrets/<name>.<NAME>_FILEis the direct way to consume them. - Bind-mounted secret files. Handy on a single-host Compose install where you already keep sensitive material in a
secrets/directory managed bysopsorgit-cryptand do not want the plaintext to touch.env. - Rotating a value without editing compose. Change the file on disk, restart the container, done. No YAML diff, no risk of committing a real key to git.
Setting both <NAME> and <NAME>_FILE at the same time is a boot-time error. Pick one.
Common examples¶
JWT_SECRET_FILE=/run/secrets/jwt
TOKEN_ENC_KEY_FILE=/run/secrets/token_enc
SMTP_PASS_FILE=/run/secrets/smtp_pass
AI_API_KEY_FILE=/run/secrets/ai_key
OIDC_CLIENT_SECRET_FILE=/run/secrets/oidc_client_secret
The suffix works for every server-read env var in every app. That includes RECOVERY_TOKEN_FILE, per-provider OIDC secrets (OIDC_PROVIDER_2_CLIENT_SECRET_FILE), and anything else in the Environment reference.
Docker Swarm compose stanza¶
Full worked example for a Swarm stack. The top-level secrets: block declares two secrets sourced from disk; the service block mounts them and points the app at /run/secrets/*.
version: "3.9"
secrets:
jwt:
file: ./secrets/jwt.txt
smtp_pass:
file: ./secrets/smtp_pass.txt
services:
cooktrace:
image: ghcr.io/traceapps/cooktrace:1
environment:
NODE_ENV: production
JWT_SECRET_FILE: /run/secrets/jwt
SMTP_HOST: smtp.example.com
SMTP_USER: cooktrace@example.com
SMTP_PASS_FILE: /run/secrets/smtp_pass
secrets:
- jwt
- smtp_pass
volumes:
- ./data/db:/data/db
- ./data/uploads:/data/uploads
ports:
- "3000:3001"
Same shape works for LiftTrace (ghcr.io/traceapps/lifttrace:1, container port 3003) and NutriTrace (ghcr.io/traceapps/nutritrace:1, container port 3001).
Gotchas¶
- The file must be readable by the container process. All three images run as a non-root
nodeuser; if you keep the file0400 root:rooton the host, the container cannot read it. - One trailing newline is trimmed; the rest of the file is used verbatim. A leading blank line will end up in the value.
- Docker Compose outside Swarm mode also supports the
secrets:block withfile:sources. Swarm is not required to use<NAME>_FILE.