Every application has secrets: database passwords, API keys, SMTP credentials, encryption keys, OAuth client secrets. Leaked secrets are among the most common causes of breaches. They get committed to Git, left in world-readable files, printed in logs or baked into Docker images. Here’s how to handle them properly on your own servers, from simple setups to dedicated tools.

Rule 1: never commit secrets to Git

Even in private repositories, secrets in Git history spread to every clone, CI system and backup, and they stay in the history even after you delete them.

# .gitignore
.env
.env.*
!.env.example
*.pem
*.key

Commit a .env.example with placeholder values instead. Add a secret scanner (gitleaks, trufflehog, or GitHub’s built-in push protection) to catch mistakes before they’re pushed.

If a secret was committed: rotate it immediately. Rewriting history (git filter-repo) is a cleanup step, not a fix, because the secret must be assumed exposed.

Rule 2: lock down .env files on the server

The .env file approach is fine for small deployments, if the file is protected:

sudo chown root:myapp /srv/myapp/.env
sudo chmod 640 /srv/myapp/.env        # readable by root and the app's group only
  • Keep it outside the web root. A .env inside public/ can be downloaded. Bots scan for /.env constantly. See Fail2Ban for Nginx.
  • Block dotfiles in Nginx as a safety net:
location ~ /\.(?!well-known) { deny all; }
  • Use different secrets per environment (dev, staging, production).

Rule 3: don’t leak through the side doors

  • Process arguments are public: anything passed on the command line (mysql -pSecret, --api-key=…) is visible in ps to all users, and lands in shell history. Use config files, environment files or prompts. See history.
  • Logs: don’t log full request bodies, headers (Authorization) or config dumps. Scrub error reports (Sentry and similar have scrubbing options).
  • Docker images: never COPY .env or ARG secrets into an image. Anyone with the image can read every layer. Pass secrets at runtime instead.
  • Debug pages: framework debug modes (APP_DEBUG=true, Django DEBUG=True) can print environment variables to visitors. Always disable them in production.
  • Backups: your backups contain your secrets. Encrypt them. See GPG encryption.

Better options for services

systemd: EnvironmentFile and credentials

[Service]
EnvironmentFile=/etc/myapp/secrets.env       # root:root 600 is fine; systemd reads it as root

Newer systemd versions offer credentials, which are passed to the service as files in a private directory, not as environment variables, and can be encrypted on disk:

[Service]
LoadCredential=db_password:/etc/myapp/db_password
# the app reads $CREDENTIALS_DIRECTORY/db_password

systemd-creds encrypt can encrypt them, bound to the machine (and optionally its TPM). See systemd unit files.

Docker Compose secrets

services:
  app:
    image: myapp
    secrets:
      - db_password
    environment:
      DB_PASSWORD_FILE: /run/secrets/db_password
secrets:
  db_password:
    file: ./secrets/db_password.txt

Secrets are mounted as files under /run/secrets/. Many official images (PostgreSQL, MySQL) support *_FILE variables. See Docker environment variables.

Encrypted secrets in Git: SOPS / age

SOPS encrypts the values in YAML, JSON or .env files with age or GPG keys, so encrypted config can live in Git and is decrypted at deploy time:

sops --encrypt --age age1… secrets.yaml > secrets.enc.yaml
sops --decrypt secrets.enc.yaml > /etc/myapp/secrets.yaml

Ansible Vault does the same for Ansible projects. See Ansible playbooks.

Secret managers

For teams and many services: HashiCorp Vault / OpenBao, Infisical, Doppler, or cloud secret managers. They provide central storage, access policies, audit logs and dynamic secrets (short-lived database credentials). They add operational complexity, which is worth it at scale, but often overkill for one VPS.

Rotation

Plan for rotating secrets before you need to:

  • Use separate credentials per app and per environment, so one rotation doesn’t break everything.
  • Database users per application, with only the privileges they need. See MySQL users and privileges.
  • Keep a list of where each secret is used.
  • Rotate on staff changes, on suspected leaks, and periodically for high-value keys.

When a secret leaks

  1. Revoke or rotate it at the source (API provider, database) immediately.
  2. Deploy the new secret.
  3. Check the provider’s logs for misuse during the exposure window.
  4. Remove the leak source (Git history, logs, public files), and add a scanner or other control to prevent a repeat.

Quick reference

Do Don’t
.env outside the web root, chmod 640 Commit .env to Git
Secrets via files or credentials at runtime Bake secrets into Docker images
Separate secrets per environment Pass secrets as command-line arguments
Encrypt backups Log request headers or full configs
Rotate after leaks and staff changes Leave debug mode on in production

Related: Linux server hardening and GitHub Actions deploy.

  • Security
  • DevOps
  • Best Practices