Purpose
Prevent misconfigurations from reaching production. Validate environment variables at startup, manage secrets safely, and keep config portable across local dev, CI, and production.
.env File Patterns
File Hierarchy
.env holds shared defaults. Commit this to the repo with safe placeholder values.
.env.local holds developer-specific overrides. Add to .gitignore.
.env.production, .env.staging, .env.test hold environment-specific values.
- Load order (most frameworks):
.env < .env.local < .env.[environment] < .env.[environment].local.
Naming Conventions
- Prefix variables with the app or service name:
MYAPP_DATABASE_URL, not just DATABASE_URL.
- Use SCREAMING_SNAKE_CASE. No dots, no dashes.
- Boolean values: use
true/false, not 1/0 or yes/no.
- Group related variables with a common prefix:
MYAPP_REDIS_HOST, MYAPP_REDIS_PORT.
Template Files
- Maintain a
.env.example with every variable, documented with comments.
- Use placeholder values that make the expected format obvious:
MYAPP_API_KEY=sk_test_xxxxxxxxxxxx.
- Script a setup step that copies
.env.example to .env if .env does not exist.
Validation with Zod
import { z } from 'zod';
const envSchema = z.object({
NODE_ENV: z.enum(['development', 'staging', 'production']),
PORT: z.coerce.number().int().min(1).max(65535).default(3000),
DATABASE_URL: z.string().url(),
API_KEY: z.string().min(10),
ENABLE_CACHE: z.coerce.boolean().default(false),
});
export const env = envSchema.parse(process.env);
- Call this at application startup, before any other initialization.
- Fail fast with a clear error listing all missing or invalid variables.
- Export the parsed result as a typed object. Never access
process.env directly elsewhere.
- Use
z.coerce for numbers and booleans since env vars are always strings.
Validation with Envalid
import { cleanEnv, str, port, bool, url } from 'envalid';
export const env = cleanEnv(process.env, {
NODE_ENV: str({ choices: ['development', 'staging', 'production'] }),
PORT: port({ default: 3000 }),
DATABASE_URL: url(),
API_KEY: str(),
ENABLE_CACHE: bool({ default: false }),
});
- Envalid strips
NODE_ENV awareness into the validator. Use env.isDev, env.isProd.
- Built-in validators:
str, bool, num, port, url, email, json, host.
- Custom validators: pass a function to
makeValidator.
Secret Management
Principles
- Never commit secrets to version control. Not even in private repos.
- Rotate secrets on a schedule. Automate rotation where possible.
- Use short-lived tokens over long-lived API keys.
- Audit secret access. Log who accessed what and when.
Tools
- Local dev: Use
.env.local (gitignored) or a secrets manager CLI.
- CI/CD: Use the platform's built-in secrets (GitHub Actions secrets, GitLab CI variables).
- Production: Use a dedicated secrets manager: HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager, or Doppler.
- Encryption at rest: Use SOPS or age to encrypt secrets files that must live in the repo.
Git Protection
- Add a pre-commit hook with gitleaks or trufflehog to scan for secrets.
- Configure
.gitignore to exclude all .env* files except .env.example.
- If a secret is committed accidentally, rotate it immediately. Removing it from history is not enough.
12-Factor Config
Core Principles
- Store config in environment variables, not in code or config files baked into the image.
- Config varies between deploys (staging, production). Code does not.
- Never group config into named environments inside the app ("the staging config object"). Use individual env vars.
Implementation
- One env var per config value. Compose connection strings from parts if needed.
- Default values should be safe for local development, never for production.
- Validate all config at startup. Log which config source was used (env var, default, file).
Docker Environment Injection
Build-time vs Runtime
ARG in Dockerfile: available only during build. Use for build tools, versions.
ENV in Dockerfile: baked into the image. Use for static defaults.
- Runtime env vars (
docker run -e): override ENV values. Use for secrets and deploy-specific config.
Compose
services:
app:
environment:
- NODE_ENV=production
- PORT=3000
env_file:
- .env.production
environment in compose takes precedence over env_file.
- Use
env_file for bulk variables. Use environment for overrides.
- Never put secrets in
docker-compose.yml. Use Docker secrets or an external env file.
Multi-stage Builds
- Do not copy
.env files into Docker images.
- Pass build-time config via
ARG and --build-arg.
- Inject runtime config via environment variables at container start.
dotenv-vault
Encrypted .env files for teams. Alternative to secrets managers for small projects.
# Install
npm install dotenv-vault-core
# Setup
npx dotenv-vault new
npx dotenv-vault push production # Upload .env to vault
# In production, use DOTENV_KEY instead of .env file
DOTENV_KEY="dotenv://vault-key-here" node server.js
// Load in app
require('dotenv-vault-core').config();
console.log(process.env.DATABASE_URL); // Decrypted from vault
When to use: Small teams (< 10 people), need encryption at rest, want git-based workflow without committing secrets.
Cloud Provider Env Patterns
AWS Systems Manager (SSM) Parameter Store
// Fetch secrets at startup
import { SSMClient, GetParametersCommand } from "@aws-sdk/client-ssm";
const client = new SSMClient({ region: "us-east-1" });
const response = await client.send(new GetParametersCommand({
Names: ["/myapp/database-url", "/myapp/api-key"],
WithDecryption: true,
}));
const env = {};
for (const param of response.Parameters) {
const key = param.Name.split('/').pop().toUpperCase().replace('-', '_');
env[key] = param.Value;
}
// Now use env.DATABASE_URL, env.API_KEY
Vercel Environment Variables
- Set env vars in the Vercel dashboard or via
vercel env add.
- Prefix client-exposed vars with
NEXT_PUBLIC_ in Next.js.
- Use different values per environment (Production, Preview, Development).
- Pull env vars to local:
vercel env pull .env.local.
GitHub Actions
- Store secrets in Settings > Secrets and variables > Actions.
- Access via
${{ secrets.MY_SECRET }} in workflow files.
- Use environment-level secrets for deploy targets that need different credentials.
Kubernetes
- Use ConfigMaps for non-sensitive config, Secrets for sensitive data.
- Mount as environment variables or files depending on the consumer.
- Use External Secrets Operator to sync from cloud secret managers.
Troubleshooting
- Variable not loading: check file encoding (must be UTF-8, no BOM), line endings (LF not CRLF).
- Variable undefined in browser: ensure the framework prefix is correct (NEXT_PUBLIC_, VITE_, REACT_APP_).
- Docker env not applying: check that env_file path is relative to the compose file, not the build context.
- Validation failing in CI: ensure CI secrets are set for the correct environment (not just production).
1---2name: env-config3description: Environment and configuration management covering .env patterns, runtime validation with zod and envalid, secret handling, 12-factor config principles, and Docker env injection.4---56## Purpose78Prevent misconfigurations from reaching production. Validate environment variables at startup, manage secrets safely, and keep config portable across local dev, CI, and production.910## .env File Patterns1112### File Hierarchy1314- `.env` holds shared defaults. Commit this to the repo with safe placeholder values.15- `.env.local` holds developer-specific overrides. Add to `.gitignore`.16- `.env.production`, `.env.staging`, `.env.test` hold environment-specific values.17- Load order (most frameworks): `.env` < `.env.local` < `.env.[environment]` < `.env.[environment].local`.1819### Naming Conventions2021- Prefix variables with the app or service name: `MYAPP_DATABASE_URL`, not just `DATABASE_URL`.22- Use SCREAMING_SNAKE_CASE. No dots, no dashes.23- Boolean values: use `true`/`false`, not `1`/`0` or `yes`/`no`.24- Group related variables with a common prefix: `MYAPP_REDIS_HOST`, `MYAPP_REDIS_PORT`.2526### Template Files2728- Maintain a `.env.example` with every variable, documented with comments.29- Use placeholder values that make the expected format obvious: `MYAPP_API_KEY=sk_test_xxxxxxxxxxxx`.30- Script a setup step that copies `.env.example` to `.env` if `.env` does not exist.3132## Validation with Zod3334```typescript35import { z } from 'zod';3637const envSchema = z.object({38 NODE_ENV: z.enum(['development', 'staging', 'production']),39 PORT: z.coerce.number().int().min(1).max(65535).default(3000),40 DATABASE_URL: z.string().url(),41 API_KEY: z.string().min(10),42 ENABLE_CACHE: z.coerce.boolean().default(false),43});4445export const env = envSchema.parse(process.env);46```4748- Call this at application startup, before any other initialization.49- Fail fast with a clear error listing all missing or invalid variables.50- Export the parsed result as a typed object. Never access `process.env` directly elsewhere.51- Use `z.coerce` for numbers and booleans since env vars are always strings.5253## Validation with Envalid5455```typescript56import { cleanEnv, str, port, bool, url } from 'envalid';5758export const env = cleanEnv(process.env, {59 NODE_ENV: str({ choices: ['development', 'staging', 'production'] }),60 PORT: port({ default: 3000 }),61 DATABASE_URL: url(),62 API_KEY: str(),63 ENABLE_CACHE: bool({ default: false }),64});65```6667- Envalid strips `NODE_ENV` awareness into the validator. Use `env.isDev`, `env.isProd`.68- Built-in validators: `str`, `bool`, `num`, `port`, `url`, `email`, `json`, `host`.69- Custom validators: pass a function to `makeValidator`.7071## Secret Management7273### Principles7475- Never commit secrets to version control. Not even in private repos.76- Rotate secrets on a schedule. Automate rotation where possible.77- Use short-lived tokens over long-lived API keys.78- Audit secret access. Log who accessed what and when.7980### Tools8182- **Local dev:** Use `.env.local` (gitignored) or a secrets manager CLI.83- **CI/CD:** Use the platform's built-in secrets (GitHub Actions secrets, GitLab CI variables).84- **Production:** Use a dedicated secrets manager: HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager, or Doppler.85- **Encryption at rest:** Use SOPS or age to encrypt secrets files that must live in the repo.8687### Git Protection8889- Add a pre-commit hook with gitleaks or trufflehog to scan for secrets.90- Configure `.gitignore` to exclude all `.env*` files except `.env.example`.91- If a secret is committed accidentally, rotate it immediately. Removing it from history is not enough.9293## 12-Factor Config9495### Core Principles9697- Store config in environment variables, not in code or config files baked into the image.98- Config varies between deploys (staging, production). Code does not.99- Never group config into named environments inside the app ("the staging config object"). Use individual env vars.100101### Implementation102103- One env var per config value. Compose connection strings from parts if needed.104- Default values should be safe for local development, never for production.105- Validate all config at startup. Log which config source was used (env var, default, file).106107## Docker Environment Injection108109### Build-time vs Runtime110111- `ARG` in Dockerfile: available only during build. Use for build tools, versions.112- `ENV` in Dockerfile: baked into the image. Use for static defaults.113- Runtime env vars (`docker run -e`): override `ENV` values. Use for secrets and deploy-specific config.114115### Compose116117```yaml118services:119 app:120 environment:121 - NODE_ENV=production122 - PORT=3000123 env_file:124 - .env.production125```126127- `environment` in compose takes precedence over `env_file`.128- Use `env_file` for bulk variables. Use `environment` for overrides.129- Never put secrets in `docker-compose.yml`. Use Docker secrets or an external env file.130131### Multi-stage Builds132133- Do not copy `.env` files into Docker images.134- Pass build-time config via `ARG` and `--build-arg`.135- Inject runtime config via environment variables at container start.136137## dotenv-vault138139Encrypted .env files for teams. Alternative to secrets managers for small projects.140141```bash142# Install143npm install dotenv-vault-core144145# Setup146npx dotenv-vault new147npx dotenv-vault push production # Upload .env to vault148149# In production, use DOTENV_KEY instead of .env file150DOTENV_KEY="dotenv://vault-key-here" node server.js151```152153```javascript154// Load in app155require('dotenv-vault-core').config();156console.log(process.env.DATABASE_URL); // Decrypted from vault157```158159**When to use:** Small teams (< 10 people), need encryption at rest, want git-based workflow without committing secrets.160161## Cloud Provider Env Patterns162163### AWS Systems Manager (SSM) Parameter Store164165```javascript166// Fetch secrets at startup167import { SSMClient, GetParametersCommand } from "@aws-sdk/client-ssm";168169const client = new SSMClient({ region: "us-east-1" });170const response = await client.send(new GetParametersCommand({171 Names: ["/myapp/database-url", "/myapp/api-key"],172 WithDecryption: true,173}));174175const env = {};176for (const param of response.Parameters) {177 const key = param.Name.split('/').pop().toUpperCase().replace('-', '_');178 env[key] = param.Value;179}180181// Now use env.DATABASE_URL, env.API_KEY182```183184### Vercel Environment Variables185186- Set env vars in the Vercel dashboard or via `vercel env add`.187- Prefix client-exposed vars with `NEXT_PUBLIC_` in Next.js.188- Use different values per environment (Production, Preview, Development).189- Pull env vars to local: `vercel env pull .env.local`.190191### GitHub Actions192193- Store secrets in Settings > Secrets and variables > Actions.194- Access via `${{ secrets.MY_SECRET }}` in workflow files.195- Use environment-level secrets for deploy targets that need different credentials.196197### Kubernetes198199- Use ConfigMaps for non-sensitive config, Secrets for sensitive data.200- Mount as environment variables or files depending on the consumer.201- Use External Secrets Operator to sync from cloud secret managers.202203## Troubleshooting204205- Variable not loading: check file encoding (must be UTF-8, no BOM), line endings (LF not CRLF).206- Variable undefined in browser: ensure the framework prefix is correct (NEXT_PUBLIC_, VITE_, REACT_APP_).207- Docker env not applying: check that env_file path is relative to the compose file, not the build context.208- Validation failing in CI: ensure CI secrets are set for the correct environment (not just production).