Skip to main content

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:

  • NAME wins. If NAME is already set to something, NAME_FILE is ignored. A NAME that is set but empty counts as not set.

  • One line break at the end of the file is removed, so a file made with echo works. 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_ and ANNOUNCEMENT_ forms: ANNOUNCEMENT_SMTP_PASS_FILE is valid. A name the site does not know, such as VAULT_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.