Keep secrets in files: NAME_FILE
Passwords and keys do not have to sit in the environment or in a .env file. Any setting can instead name a file that holds its value, which is how Docker secrets and Kubernetes secret volumes deliver them. This page is for whoever runs a Node or Docker site and wants secrets kept apart from the rest of the configuration.
How it works
For a setting called NAME, set NAME_FILE to the path of a file. When the server starts it reads the file and uses its contents as the value of NAME.
ADMIN_PASSWORD_FILE=/run/secrets/admin_password
The rules:
-
NAMEwins. IfNAMEis already set to something,NAME_FILEis ignored. ANAMEthat is set but empty counts as not set. -
One line break at the end of the file is removed, so a file made with
echoworks. Nothing else is trimmed: spaces, and any further blank lines, stay part of the value. -
It works for every setting the site reads, not only secrets, and for the email settings in their
TRANSACTIONAL_andANNOUNCEMENT_forms:ANNOUNCEMENT_SMTP_PASS_FILEis valid. A name the site does not know, such asVAULT_TOKEN_FILE, is not read. -
The file is read once, at start. Changing it has no effect until the site restarts.
-
A file that cannot be read does not stop the server. It logs a line and carries on without the value:
[secrets] could not read ADMIN_PASSWORD_FILE=/run/secrets/admin_password: ENOENT: no such file or directory, open '/run/secrets/admin_password'The site then behaves as if the setting were missing, so check the log after the first start, or run the configuration check.
PORT_FILE and DATA_DIR_FILE reach the server but not the launcher that starts it. Set those two in the environment itself.
Docker secrets in Compose
Put each secret in a file of its own, outside any repository:
mkdir -p secrets
printf '%s' 'a-long-random-password' > secrets/admin_password.txt
printf '%s' 're_your_key' > secrets/resend_api_key.txt
chmod 600 secrets/*.txt
Name them in the Compose file and point the settings at where Docker mounts them, which is /run/secrets/ followed by the secret's name:
services:
app:
image: choir-manager
restart: unless-stopped
environment:
NODE_ENV: production
PORT: 3001
SITE_URL: https://choir.example.org
ADMIN_USERNAME: admin
ADMIN_PASSWORD_FILE: /run/secrets/admin_password
EMAIL_PROVIDER: resend
EMAIL_FROM: "Harmony Community Choir <noreply@example.org>"
RESEND_API_KEY_FILE: /run/secrets/resend_api_key
SQLITE_PATH: /app/data/choir.sqlite
STORAGE_LOCAL_DIR: /app/data/storage
secrets:
- admin_password
- resend_api_key
ports:
- "8080:3001"
volumes:
- data:/app/data
secrets:
admin_password:
file: ./secrets/admin_password.txt
resend_api_key:
file: ./secrets/resend_api_key.txt
volumes:
data:
The image runs as the user node (user id 1000), not as root. Compose mounts a file secret with the owner and permissions the file has on the host, so the file must be readable by user id 1000. If the log shows EACCES: permission denied for a secret, that is the cause: change the file's owner to 1000, or loosen its permissions and protect the folder instead.
Check that the values arrived:
docker compose exec app npm run config:check
admin.password should read ******** (set).
The database address holds a password too. DATABASE_URL_FILE works the same way, with the whole address in the file.
Kubernetes secret volumes
Mount the Secret as a volume and point the settings at the files, one per key:
containers:
- name: app
image: choir-manager
env:
- name: ADMIN_PASSWORD_FILE
value: /etc/choir-secrets/ADMIN_PASSWORD
- name: RESEND_API_KEY_FILE
value: /etc/choir-secrets/RESEND_API_KEY
volumeMounts:
- name: choir-secrets
mountPath: /etc/choir-secrets
readOnly: true
volumes:
- name: choir-secrets
secret:
secretName: choir-secrets
Kubernetes updates the mounted files when the Secret changes, but the site only reads them at start, so restart the pod afterwards. A Secret given to the container as environment variables (envFrom) needs no _FILE settings at all.
File permissions on a plain server
On a server without containers, keep each file readable only by the account the site runs as:
sudo install -d -m 700 -o choir -g choir /etc/choirmaster/secrets
sudo install -m 600 -o choir -g choir /dev/null /etc/choirmaster/secrets/admin_password
sudoedit /etc/choirmaster/secrets/admin_password
Here choir stands for your service account. Keep the files out of the data folder, so that they do not travel with your backups.
Other secret tools
Tools that have no file to mount usually have a command that fetches the secrets, puts them in the environment and then starts a program. Use it to start the site, and no _FILE setting is needed:
op run --env-file=.env.tpl -- npm start
That is the 1Password command-line tool; Bitwarden Secrets Manager's bws run -- npm start works the same way. For HashiCorp Vault, AWS Secrets Manager, Doppler and Infisical the site can fetch the secrets itself: Use a secrets manager.