WierdX — Programming Reference All tutorials →
Developer reference · Practical tutorials · CS fundamentals
Tools & Techniques

Environment Variables and Secrets Management

Hardcoded credentials in source code are the most common source of accidental credential leaks. Environment variables are the first line of defence, but they're only part of the solution.

Published May 1, 2026

An environment variable is a named value that lives in the operating system's process environment, outside the application code. Every process inherits its environment from its parent; you can set variables for a single command, for a shell session, or system-wide.

# Set a variable for a single command
DATABASE_URL=postgres://localhost/mydb python manage.py migrate

# Set a variable in the current shell session
export API_KEY=abc123
python app.py   # can now read API_KEY

# Read environment variables in Python
import os
db_url = os.environ.get("DATABASE_URL")
if not db_url:
    raise RuntimeError("DATABASE_URL is required")

# In Node.js
const apiKey = process.env.API_KEY;

The 12-Factor App principle

The 12-Factor methodology (from Heroku, 2011) codified the practice: store config in the environment, not in code. "Config" means everything that varies between environments: database URLs, API keys, feature flags, service endpoints. Code that works in development should work in production by changing only environment variables, not code.

The practical rule: if you'd have to change the code to deploy to a different environment, that value should be an environment variable instead.

The .env file pattern

Typing environment variables on the command line before every run is impractical. The .env file is the solution: a plain text file with one KEY=VALUE pair per line, loaded by your application (or a library) at startup.

# .env (never commit this file)
DATABASE_URL=postgres://user:password@localhost:5432/mydb
SECRET_KEY=dev-only-not-for-production
DEBUG=true
STRIPE_SECRET_KEY=sk_test_abc123

Libraries that load .env files: python-dotenv (Python), dotenv (Node.js), godotenv (Go), vlucas/phpdotenv (PHP).

The single most important rule: add .env to your .gitignore immediately when you create it. A .env.example file listing the required variable names (with no values) should be committed so teammates know what variables they need to configure.

# .gitignore
.env
.env.local
.env.production

# .env.example (committed to the repo)
DATABASE_URL=
SECRET_KEY=
DEBUG=
STRIPE_SECRET_KEY=

What happens when you accidentally commit secrets

GitHub scans every push for known secret patterns and will alert you (and sometimes invalidate the credential). But the damage is already done: the secret is in your git history. Rotating the credential is mandatory. Removing it from history requires git filter-repo or BFG Repo Cleaner, and you must force-push — a disruptive operation if others have cloned the repo.

Prevention is dramatically cheaper than recovery. Consider installing a pre-commit hook (like detect-secrets or gitleaks) that scans staged files for secret patterns before allowing a commit.

Secrets managers for production

For production systems, environment variables set at the OS level are often not secure enough: they can be read by other processes on the same machine, and managing them across many servers is fragile. Secrets managers are the production-grade solution:

  • AWS Secrets Manager / AWS Parameter Store — managed secrets with fine-grained IAM access control and automatic rotation.
  • HashiCorp Vault — open-source, self-hosted, supports dynamic secrets (credentials generated on demand and automatically expired).
  • GCP Secret Manager, Azure Key Vault — equivalents for those cloud platforms.

Applications fetch secrets from the manager at startup (or on demand) rather than receiving them as environment variables. This means no secret ever touches a file system or shell history.

Variable naming conventions

Environment variable names are traditionally UPPER_SNAKE_CASE. Use descriptive, unambiguous names. Prefix variables that belong to a specific service to avoid collisions:

DATABASE_URL           # clear
POSTGRES_HOST          # specific
STRIPE_PUBLIC_KEY      # prefixed by service
STRIPE_SECRET_KEY      # clear which key is which
SENDGRID_API_KEY
REDIS_URL

Document every required environment variable, what format it expects, and what happens if it's missing. Failing fast with a clear error at startup is much better than producing a confusing error deep in the application when the missing variable is first used.

Validating environment variables at startup

Read and validate all required environment variables when the application starts, before it begins handling requests. This fails loudly and immediately if configuration is missing, rather than failing silently on the first request that hits the relevant code path.

required = ["DATABASE_URL", "SECRET_KEY", "STRIPE_SECRET_KEY"]
missing = [k for k in required if not os.environ.get(k)]
if missing:
    raise EnvironmentError(f"Missing required env vars: {', '.join(missing)}")

The fifteen seconds it takes to add this check at the top of your application's startup code will save hours of debugging when a deployment fails because a variable wasn't set.