Build a service
Pass configuration and secrets
Give a service its runtime configuration without embedding secrets in the unit or source repository.
8 minute lesson
A service does not inherit the full environment of your interactive shell. The PATH tweaks and exported variables in your .bashrc do not exist when systemd starts the process. Define every required value deliberately, in the unit.
Environment and EnvironmentFile
Environment= suits small non-secret values, written directly in the unit:
[Service]
Environment=NODE_ENV=production
EnvironmentFile=/etc/demo-api/app.env
ExecStart=/usr/bin/node /opt/demo-api/server.js
EnvironmentFile= loads KEY=VALUE lines from a separate file. That keeps configuration out of the unit and out of your repository:
# /etc/demo-api/app.env
PORT=3000
DATABASE_URL=postgres://[email protected]/app
Make that file root-owned and unreadable to others:
sudo chown root:root /etc/demo-api/app.env
sudo chmod 600 /etc/demo-api/app.env
If the file is missing, the service fails to start. Prefix the path with a dash (EnvironmentFile=-/etc/demo-api/local.env) when the file is optional.
Why environment variables are weak for secrets
Even with a root-owned file, environment variables may still be exposed through process inspection or debugging. systemctl show demo-api -p Environment prints them. Child processes inherit them. Crash reports and debug tooling can dump them.
For sensitive data, prefer application-supported credential files. systemd has a mechanism built for this:
[Service]
LoadCredential=db-password:/etc/demo-api/db-password
The service reads the secret from $CREDENTIALS_DIRECTORY/db-password, a private per-service path. The value never appears in the environment or in systemctl show. The application needs to support reading a secret from a file, but most database drivers and libraries do.
Do the inventory
List every value your application needs to run. Classify each one as ordinary configuration or secret. Ordinary configuration goes in Environment= or an EnvironmentFile=. Secrets go in a credential file with tight ownership and mode, loaded through LoadCredential= or the application’s own secret-file option.
Doing this classification once, on paper, is faster than discovering a leaked DATABASE_URL in a process listing later.
Lesson completed