Run an application
Store environment values and secrets
Keep production configuration outside the repository and make secret files readable only by the service account.
The database URL, the API keys, the session secret: none of that goes in the release directory. Configuration lives in one file outside the code, at /etc/notes-app.env. Two reasons. The same code can then run in staging and production with different values. And rolling back a release can’t accidentally roll back to an old, revoked credential.
Create the file with the right permissions
Create an empty file owned by root, readable by the notes-app group, then edit it:
sudo install -m 640 -o root -g notes-app /dev/null /etc/notes-app.env
sudoedit /etc/notes-app.env
install with /dev/null as the source is a trick to create an empty file with mode, owner and group set in one step. Mode 640 means root can read and write, the group can read, and everyone else gets nothing.
Write plain NAME=value lines:
NODE_ENV=production
PORT=3000
DATABASE_URL=replace-with-the-real-secret
No export keyword. This file is read by systemd through EnvironmentFile, not by a shell, so shell syntax doesn’t belong here. If a value contains spaces or special characters, check systemd’s quoting rules for environment files and test the app. Don’t guess.
Verify without printing the secrets
Check ownership and mode, then test who can read the file:
sudo stat -c '%U %G %a %n' /etc/notes-app.env
sudo -u notes-app test -r /etc/notes-app.env && echo readable
sudo -u deploy test -r /etc/notes-app.env && echo readable-by-deploy
The first line prints root notes-app 640 /etc/notes-app.env. The second prints readable. The third prints nothing, unless you’ve added deploy to the notes-app group. Whether the deploy account should read production secrets is a real decision. Make it on purpose, not by accident.
Notice I never ran cat on the file. Every time you print secrets to a terminal they end up in scrollback, screenshots, chat logs and shell history. The same goes for Git, frontend bundles, process arguments (visible in ps) and debug commands that dump the whole environment.
The classic failure
You create the file as root with mode 600. The service runs as notes-app, can’t read it, and starts with an empty environment or dies with Permission denied. The fix is the group and the mode above. The wrong fix, which I’ve seen many times, is running the app as root so it can read anything.
Rotating a secret is a deploy
Treat it like one. Create the new credential at the provider first, if it allows two active at once. Update the file, restart the service, hit the health endpoint. Only then revoke the old credential. That order gives you a rollback window.
Create the file with placeholder values and prove its permissions with stat and the two test commands. Then, in the app repository, add a .env.example listing every variable name with no real values. New developers see what’s needed, and nothing secret is committed.
Lesson completed