env.dev

Environment Variable Best Practices

A comprehensive guide to managing environment variables securely and effectively across development, staging, and production environments.

Naming Conventions

Consistent naming is the foundation of a maintainable environment variable strategy. Every environment variable should use SCREAMING_SNAKE_CASE — all uppercase letters with underscores separating words. This convention is universally recognized across operating systems, languages, and tooling, and it makes environment variables immediately distinguishable from other identifiers in your codebase.

Beyond casing, prefix your variables by service or domain to avoid collisions and improve discoverability. For example, use DATABASE_URL, DATABASE_POOL_SIZE, and DATABASE_SSL_MODErather than generic names like URL or POOL_SIZE. When integrating with third-party services, adopt their conventional prefixes: AWS_ for Amazon Web Services,AZURE_ for Microsoft Azure, GCP_ or GOOGLE_ for Google Cloud.

Avoid abbreviations that sacrifice clarity. SMTP_SERVER_HOST is better than SMTP_SRV_H. Your future self and your teammates will thank you when debugging production issues at 2 AM.

Never Commit Secrets to Git

This is the single most important rule in environment variable management. API keys, database passwords, tokens, and private keys must never appear in version control. A secret committed to git is a secret that lives forever — even if you delete the file, it remains in the repository history and can be extracted by anyone with access.

Start by adding .env, .env.local, .env.*.local, and any other files that may contain secrets to your .gitignore. Do this before your first commit to the repository — not after secrets have already been committed.

Use pre-commit hooks to catch accidental secret leaks. Tools like git-secrets,detect-secrets, or gitleaks can scan staged files for patterns that look like API keys, tokens, or passwords. Configure them as mandatory pre-commit hooks so that no developer can accidentally push sensitive material:

yaml
# .pre-commit-config.yaml
repos:
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.18.0
    hooks:
      - id: gitleaks

If a secret has already been committed, rotate it immediately. Do not rely on rewriting git history alone — assume the secret has been compromised and generate a new one.

Use .env Files for Local Development Only

The .env file pattern, popularized by the dotenv library, is an excellent tool for local development. It lets developers configure their environment without modifying system-level settings or passing long lists of flags to startup commands. However, .env files should never be used in production.

In production, environment variables should be injected by your deployment platform, container orchestrator, or secrets manager. Kubernetes uses ConfigMaps and Secrets. AWS offers Systems Manager Parameter Store and Secrets Manager. Azure has Key Vault. Google Cloud provides Secret Manager. These systems offer encryption at rest, access control, audit logging, and automatic rotation — none of which a plain text .env file provides.

Provide a .env.example file (committed to the repository) that documents every required variable with placeholder values. This serves as both documentation and a template for new developers:

bash
# .env.example — copy to .env and fill in real values
DATABASE_URL=postgres://user:password@localhost:5432/myapp
REDIS_URL=redis://localhost:6379
API_KEY=your-api-key-here
LOG_LEVEL=debug

Validate Environment Variables at Startup

Applications should validate every required environment variable before accepting traffic. The fail-fast pattern prevents your application from starting in a broken state, where it might serve errors or corrupt data because a critical configuration value is missing or malformed.

Check for required variables and verify their format at the very beginning of your application lifecycle. IfDATABASE_URL is missing or PORT is not a valid number, throw an error with a clear message and exit immediately. Do not let the application limp along with undefined values that surface as cryptic runtime errors minutes or hours later.

typescript
// validate.ts — run at application startup
function requireEnv(name: string): string {
  const value = process.env[name];
  if (!value) {
    throw new Error(`Missing required environment variable: ${name}`);
  }
  return value;
}

export const config = {
  databaseUrl: requireEnv('DATABASE_URL'),
  port: Number.parseInt(requireEnv('PORT'), 10),
  nodeEnv: requireEnv('NODE_ENV'),
};

if (Number.isNaN(config.port)) {
  throw new Error('PORT must be a valid number');
}

Libraries like envalid (Node.js), pydantic-settings (Python), or envconfig(Go) provide structured validation with type coercion and default values built in.

Typed and Centralized Configuration Objects

Scattering process.env.SOME_VAR calls throughout your codebase is a maintenance nightmare. Instead, create a single configuration module that reads, validates, and exports typed configuration values. This gives you a single source of truth, type safety, and a clear inventory of every environment variable your application uses.

The configuration module should be the only place in your codebase that reads from process.env. Every other module imports from the configuration object, never directly from the environment. This makes it trivial to mock configuration in tests and to refactor how variables are sourced in the future.

typescript
// config.ts
export const config = {
  database: {
    url: requireEnv('DATABASE_URL'),
    poolSize: Number.parseInt(process.env.DATABASE_POOL_SIZE ?? '10', 10),
    sslMode: process.env.DATABASE_SSL_MODE ?? 'prefer',
  },
  server: {
    port: Number.parseInt(process.env.PORT ?? '3000', 10),
    host: process.env.HOST ?? '0.0.0.0',
  },
  features: {
    enableNewDashboard: process.env.FEATURE_NEW_DASHBOARD === 'true',
  },
} as const;

Secrets Rotation Strategies

Secrets have a shelf life. API keys get leaked. Employees leave. Compliance policies require periodic rotation. Your infrastructure should support rotating any secret without downtime.

Design your application to tolerate overlapping credentials during rotation. For database passwords, this typically means supporting two valid passwords simultaneously: the old one remains active while the new one propagates across all instances. Cloud providers and managed databases often have built-in dual-credential rotation support.

Use your cloud provider's secrets manager to automate rotation. AWS Secrets Manager, Azure Key Vault, and Google Secret Manager all support automatic rotation schedules with Lambda functions or equivalent. For self-managed secrets, establish a rotation calendar and track compliance.

Never hard-code rotation periods into your application. The rotation schedule belongs in your infrastructure configuration, not in application code. Your application should simply read the current secret from the environment or secrets manager at runtime.

Environment-Specific Configuration

Most applications run in at least three environments: development, staging, and production. Each environment has different requirements for logging, error reporting, database connections, feature flags, and external service endpoints.

Use a single environment variable like NODE_ENV or APP_ENV to identify the current environment, but do not branch your application logic on it. Instead, let each environment provide the correct values for every configuration variable. Staging should connect to a staging database not because your code checks if (NODE_ENV === 'staging'), but because DATABASE_URL points to the staging database in that environment.

This approach keeps your code environment-agnostic and makes it easy to add new environments (QA, demo, performance testing) without modifying application code. The environment is defined entirely by its configuration, not by conditional branches in your source.

Principle of Least Privilege

Service accounts and API keys should have the minimum permissions required to perform their function. A background worker that reads from a queue should not have admin access to your entire cloud account. A web server that reads from a database should use a read-only connection string unless it genuinely needs to write.

Create separate credentials for separate concerns. Your CI/CD pipeline needs deployment permissions but not database access. Your application server needs database access but not deployment permissions. By isolating credentials, you limit the blast radius when any single secret is compromised.

Audit your environment variables periodically. Remove credentials that are no longer used. Downgrade permissions where full access is no longer needed. Treat every environment variable containing a secret as an attack surface that should be minimized.

Document Required Environment Variables

Every project should have a clear, up-to-date list of all required and optional environment variables. This documentation is critical for onboarding new developers, debugging deployment issues, and maintaining operational clarity.

At minimum, document the variable name, a description of its purpose, whether it is required or optional, its default value (if any), and an example value. Keep this in your README.md or a dedicatedCONFIGURATION.md file at the root of your repository:

markdown
| Variable          | Required | Default | Description                        |
|-------------------|----------|---------|------------------------------------|
| DATABASE_URL      | Yes      | —       | PostgreSQL connection string        |
| PORT              | No       | 3000    | HTTP server port                   |
| LOG_LEVEL         | No       | info    | Logging level (debug/info/warn)    |
| REDIS_URL         | Yes      | —       | Redis connection string             |
| FEATURE_NEW_UI    | No       | false   | Enable new UI feature flag          |

Keep the documentation in sync with your configuration module. When you add a new environment variable to your code, add it to the documentation in the same commit. When you remove one, remove it from the docs. Stale documentation is worse than no documentation — it actively misleads.