Developers

Environment Variables and Secrets: Keeping Passwords Out of Your Code

Where to store database passwords and API keys on a server: environment variables, env files, systemd, Docker and PHP-FPM, plus the mistakes that leak secrets.

By Zebra Hosting Cloud

Every application has settings that differ between machines: database credentials, API keys, the address of a mail server, a flag that switches debugging on. Hard-coding them in source files is the classic mistake, because the code ends up in Git, in backups, in screenshots and on other people's laptops. The widely used alternative is to keep configuration in the environment of the running process. This article explains how that works on a Linux server and how to handle the sensitive parts properly.

What an environment variable is

An environment variable is a named string that the operating system hands to a process when it starts. Programs read it with a standard call: process.env.DB_PASSWORD in Node.js, os.environ["DB_PASSWORD"] in Python, getenv('DB_PASSWORD') in PHP. The same code then runs in development and production, and only the values change. This idea is one part of the well-known "twelve-factor app" approach to building deployable software.

Not everything in the environment is a secret. Separate the two kinds in your head:

  • Settings: APP_ENV=production, a port number, a feature flag. Harmless if seen.
  • Secrets: database passwords, API tokens, signing keys. Anyone who sees them gains access to something.

Where to set them on a server

Exporting a variable in your login shell only affects that session, so production services need a persistent method that fits how they are started:

  • systemd services: use EnvironmentFile=/etc/myapp.env in the unit file. The file contains simple KEY=value lines, one per line, with no export keyword.
  • Docker and Compose: use env_file: .env in the Compose file, or docker run --env-file.
  • PHP-FPM: add lines such as env[DB_PASSWORD] = value to the pool configuration, and make sure clear_env = no if you rely on inherited variables.
  • Frameworks with dotenv: libraries read a .env file from the project folder at startup. This is convenient, but the file then sits next to your code, so keep it out of the web root and out of Git.

Protect the file that holds them

A secrets file is only as safe as its permissions. Create it so that only the owner can read it, and give it to the right account:

sudo touch /etc/myapp.env
sudo chown root:root /etc/myapp.env
sudo chmod 600 /etc/myapp.env
sudo nano /etc/myapp.env

systemd reads EnvironmentFile as root before dropping to the service user, so a root-owned file readable only by root works for services that run as another user. For tools that need to read the file as a normal user, set the owner to that user instead. Never make it world-readable, and check backups so the file is not copied somewhere with looser permissions.

Keep secrets out of Git, logs and command lines

Add real environment files to .gitignore and commit a template instead, a file such as .env.example with the variable names and fake values:

DB_HOST=127.0.0.1
DB_NAME=appdb
DB_USER=appuser
DB_PASSWORD=change-me

New developers can copy it and fill in their own values. Also avoid these quieter leaks:

  • Command-line arguments are visible to other users in the process list, so passing a password with a flag is unsafe on a shared machine.
  • Debug pages and error reports often print the whole environment. Turn debug mode off in production.
  • Logs sometimes record request data or startup configuration. Check that your logging does not print secrets.
  • Chat and tickets: pasting a key into a support request puts it in a system you do not control.

If a secret is committed or leaked, do not simply delete it from the repository. Treat it as compromised: create a new one, update the server, and revoke the old one.

Limits of environment variables

Environment variables are a good baseline, not a vault. Anyone who can run commands as the same user or as root can read a process's environment (on Linux through /proc/<pid>/environ), and child processes inherit them. For larger teams or higher-value secrets, a dedicated secrets manager adds access control, audit trails and rotation. For a single VPS, a root-owned file with strict permissions, plus disciplined habits, covers most needs.

Good habits

  1. Use a different value for each environment. Staging must never share a production database password or live payment keys.
  2. Give each secret the least privilege it needs, such as a database user limited to one database.
  3. Generate long random values, for example with openssl rand -base64 32, instead of inventing passwords.
  4. Rotate secrets when staff leave or after any suspected exposure, and note when each was last changed.
  5. Fail loudly: make the app refuse to start when a required variable is missing, so a typo is found at deployment and not in production traffic.

A worked example: the app and the file

Suppose your Node.js service reads DB_PASSWORD from the environment. You create /etc/myapp.env with the line DB_PASSWORD=value, set its permissions to 600, and reference it in the unit file with EnvironmentFile=/etc/myapp.env. After sudo systemctl daemon-reload and a restart, the service sees the value, while the repository contains only a placeholder in .env.example. To confirm without printing the secret, check the service's environment names with sudo systemctl show myapp -p Environment, and remember that this property lists values set inline in the unit, not those from the file. Our knowledge base has more server setup guidance.

Secrets and security on Zebra Hosting

Careful handling of credentials is part of a wider approach to website protection, described on our security page. A server where you control the environment, such as a VPS, lets you apply these practices fully.