What Environment Variables Actually Are
Every application needs configuration that changes between environments — a database URL that's different on your laptop versus production, an API key that shouldn't be checked into source control, a feature flag that's on in staging but off in prod. Environment variables are how you pass that configuration to a running process without baking it into your code.
At the OS level, an environment variable is just a key-value string that a process inherits from its parent. When your shell runs DATABASE_URL=postgres://localhost/myapp node server.js, Node sees process.env.DATABASE_URL set to that value. Python reads it via os.environ["DATABASE_URL"]. Go uses os.Getenv("DATABASE_URL"). Every runtime has the same basic mechanism.
The reason they exist isn't just convenience — it's the clean separation between code and configuration. Your code doesn't contain secrets. Your secrets aren't in your Git history. Deploy the same artifact to staging and production with completely different behavior just by swapping the environment.
The .env File Pattern
Passing every variable on the command line gets tedious fast. That's why .env files became standard. You write key-value pairs one per line, and a library like dotenv (Node), python-dotenv, or godotenv loads them into the process environment at startup.
DATABASE_URL=postgres://localhost:5432/myapp
REDIS_URL=redis://localhost:6379
API_KEY=sk-dev-abc123
PORT=3000
DEBUG=true
Load it at the very top of your entry point:
// Node.js
import 'dotenv/config';
// or
require('dotenv').config();
# Python
from dotenv import load_dotenv
load_dotenv()
The library reads .env, sets any variables not already present in the environment, and your code picks them up through the normal process.env / os.environ interface. If a variable is already set in the real environment, dotenv doesn't overwrite it. That's exactly what you want — production env vars win over file-based ones.
The .gitignore Rule Everyone Forgets
Add .env to your .gitignore before you create the file, not after. Sounds obvious until you've pushed secrets to GitHub and spent an afternoon rotating keys.
# .gitignore
.env
.env.local
.env.*.local
The convention to also add .env.development.local, .env.test.local, and .env.production.local is borrowed from Create React App, but the principle applies everywhere. Anything with actual credentials never goes in version control.
What does go in version control is .env.example — a committed template that lists every variable the app needs with placeholder values or documentation comments:
# Required — get from team 1Password vault
DATABASE_URL=postgres://user:password@host:5432/dbname
REDIS_URL=redis://localhost:6379
# Get from Stripe dashboard > Developers > API keys
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
# Optional — leave empty to disable
SENTRY_DSN=
New teammates clone the repo, copy .env.example to .env, fill in real values, and they're running. The example file is the onboarding guide for your configuration.
12-Factor App Principles
The 12-factor app methodology from Heroku formalized this pattern in 2011 and it's still the clearest statement of why it matters. Factor III: "Store config in the environment." The test is simple — could you open-source the codebase right now without exposing credentials? If the answer is no, your config isn't properly separated from your code.
The 12-factor approach also means your app should read config from environment variables, not from config files with environment-specific sections. Instead of config/database.yml with development: and production: blocks, you have one DATABASE_URL env var that holds a different value in each environment. The app has no concept of "development mode" baked in — it just responds to the config it receives.
Dev / Staging / Prod: Different Secrets for Different Environments
Never share credentials between environments. Your local database doesn't need production data. Your staging Stripe integration should use test-mode keys, not live ones. More work up front, yes — but it limits the blast radius when something goes wrong.
A practical setup looks like this:
- Development:
.envfile on each developer's machine, pointing to local services or sandboxed dev accounts. Secrets are low-value and easily rotated. - Staging: Environment variables injected by your deployment platform (Heroku config vars, Railway variables, etc.), pointing to dedicated staging databases and sandbox API keys.
- Production: Managed secret store or platform secrets, with access tightly controlled. Only CI/CD systems and operations tooling can read them.
If you need to encode a config value for transport or storage, Base64 Encoder is handy for quick encoding without leaving your browser.
Secret Rotation
Secrets have an expiry date, whether you set one or not. Keys get leaked in logs, former employees retain access, services recommend periodic rotation. Build rotation into your workflow — don't wait for a crisis to force it.
For database passwords and API keys, the pattern is: create the new secret, update all services to accept both old and new (if the service supports it), deploy with the new secret, then revoke the old one. For services that don't support dual-credentials, schedule a maintenance window.
How long should a secret be? The NIST guidance for API keys recommends at least 128 bits of entropy. The Hash Generator on this site can generate SHA-256 hashes, which give you 256-bit output — more than enough to build a high-entropy key with a tool like openssl rand -hex 32.
AWS Secrets Manager and HashiCorp Vault
File-based secrets don't scale past a certain team size. You can't audit who accessed what, you can't auto-rotate credentials, and distributing .env files securely between people becomes its own problem.
AWS Secrets Manager solves this by storing secrets centrally, integrating with IAM for access control, and supporting automatic rotation for RDS databases and other AWS services. You fetch the secret at runtime via the SDK rather than at deploy time via env vars.
HashiCorp Vault takes a similar approach but runs anywhere — on-prem, any cloud, or your own infrastructure. It supports dynamic secrets (credentials generated on-demand that expire automatically), fine-grained ACLs, and detailed audit logs. The learning curve is steeper, but the capabilities are significantly broader.
For most small teams, a platform like Railway, Render, or Heroku that has first-class secrets management built into its deploy pipeline is the practical middle ground. Platform secrets are encrypted at rest, not stored in your repo, and injected as env vars at runtime.
What to Do If You Accidentally Commit a Secret
It happens to everyone. The moment you realize a secret is in your Git history, treat it as compromised and rotate it immediately — before anything else. A push to GitHub triggers dozens of automated bots that harvest secrets from public repos within seconds.
After rotating the secret, clean it from history. git filter-branch works but is slow and error-prone. The modern approach is git-filter-repo:
pip install git-filter-repo
git filter-repo --path-glob '*.env' --invert-paths
Or to remove a specific string across all commits, write the replacement expressions to a file first (one per line, in old==>new format), then pass the file:
echo 'sk-prod-badkey123==>REDACTED' > replacements.txt
git filter-repo --replace-text replacements.txt
After rewriting history you'll need to force-push all branches and have all collaborators re-clone. GitHub's secret scanning feature will alert you if a known-format token (AWS key, Stripe key, GitHub PAT, etc.) is pushed to any repo — it's worth enabling.
If the repo was public at any point, assume the secret is already in someone's hands and act accordingly. History rewrites don't undo what's already been indexed.
The Takeaway
Environment variables solve a real problem cleanly: the same codebase runs differently in different environments without any code changes. The discipline is straightforward — never commit secrets, always provide a documented .env.example, use a secrets manager once you outgrow files, and rotate credentials on a schedule instead of in a panic. For generating secure tokens and encoding config values, the Hash Generator and Base64 Encoder tools on this site are quick browser-based utilities that don't send your data to any server.
For more context on how secrets flow over the wire, see How TLS and HTTPS Work.