Every developer knows .env files. Most developers use them in production. Most developers are doing it wrong.
.env files work fine for local development. In production, they're a security liability: no rotation, no audit trail, no access control, and a single git add . accident from leaking your database credentials to GitHub.
This guide covers how professional teams actually manage secrets in production — from the simplest setup that's already safer than a .env file, to the full enterprise approach with rotation and audit logging.
What's Wrong With .env in Production
1. Accidental commits
# This happens more than you think
git add .
git commit -m "fix: update config"
git push # 🔥 .env just went to GitHub
Even if you delete the file in the next commit, it's in the git history. If it's a public repo, it's been scraped by secret-scanning bots within minutes.
2. No rotation mechanism
When a secret leaks — and eventually one will — how do you rotate it? With .env files: update the file, redeploy. Every environment. Manually. Under pressure. During an incident.
3. No audit trail
Who added this database password? When? Who has access to it right now? With .env files: no idea.
4. Flat access control
Every developer who can SSH into the server can read every secret. There's no way to say "frontend devs can access the Stripe publishable key but not the database root password."
5. Environment drift
Production .env diverges from staging. Staging diverges from local. Three months later, you have no idea what environment has what version of which secret.
The Spectrum: From Simple to Serious
.env in production ← stop doing this
↓
Platform env vars ← minimum viable for any serious project
↓
Doppler / Infisical ← team standard, sync across environments
↓
AWS Secrets Manager ← enterprise, full rotation + audit
↓
HashiCorp Vault ← maximum control, self-hosted
Most projects land between platform env vars and Doppler. Let's cover each.
Level 1: Platform Environment Variables
The simplest upgrade from .env. Every major deployment platform has built-in env var management.
Vercel
# CLI: add a secret to production
vercel env add DATABASE_URL production
# Or via dashboard: Project → Settings → Environment Variables
Vercel env vars are encrypted at rest, never exposed in build logs, scoped to environment, and accessible to team members based on project access.
This alone eliminates the accidental commit risk.
// In your Next.js app — same as before, nothing changes in the code
const db = new Client({
connectionString: process.env.DATABASE_URL,
})
Railway, Render, Fly.io
# Railway CLI
railway variables set DATABASE_URL="postgresql://..."
# Fly.io
fly secrets set DATABASE_URL="postgresql://..."
When to use: solo projects, small teams, simple deployments where you don't need to sync secrets across multiple services.
Level 2: Doppler — The Team Standard
Doppler is what most professional dev teams settle on. It's a secrets manager that sits in front of your deployment platform — you manage all secrets in Doppler, and Doppler syncs them to Vercel, Railway, AWS, or wherever you deploy.
Why Doppler over platform env vars:
- Single source of truth across all environments and platforms
- Version history — you can see every change and who made it
- Access control — developers can have read access to staging but not production
- Automatic sync to your deployment platform
- Free tier for small teams (up to 5 users)
Setup for Next.js
npm install -g @dopplerhq/cli
doppler login
doppler setup
This creates a .doppler.yaml in your project:
setup:
project: my-saas
config: dev
Now instead of .env, you run local dev with:
doppler run -- npm run dev
# Doppler injects all secrets as env vars before starting the dev server
Syncing to Vercel
doppler integrations vercel
Now when you change a secret in Doppler, it automatically pushes to Vercel. Change DATABASE_URL in Doppler → it updates in Vercel production, preview, and development automatically.
Access control in Doppler
Production config → only lead devs + CI/CD
Staging config → all developers (read), lead devs (write)
Development config → all developers (read + write)
A new developer onboards: give them Doppler access to staging and dev, they have everything they need immediately. No sending passwords over Slack.
Level 3: AWS Secrets Manager
When you're on AWS and need the full enterprise feature set: automatic rotation, fine-grained IAM access control, CloudTrail audit logging.
Storing a secret
aws secretsmanager create-secret \
--name "myapp/production/database" \
--secret-string '{"url":"postgresql://...","password":"..."}'
Reading in your application
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager'
const client = new SecretsManagerClient({ region: 'us-east-1' })
async function getDatabaseUrl(): Promise<string> {
const response = await client.send(
new GetSecretValueCommand({ SecretId: 'myapp/production/database' })
)
const secret = JSON.parse(response.SecretString!)
return secret.url
}
Cache this at startup — don't call Secrets Manager on every request:
let cachedSecrets: Record<string, string> | null = null
export async function getSecrets() {
if (cachedSecrets) return cachedSecrets
cachedSecrets = await loadFromSecretsManager()
return cachedSecrets
}
Automatic rotation
aws secretsmanager rotate-secret \
--secret-id "myapp/production/database" \
--rotation-rules AutomaticallyAfterDays=30
AWS handles generating the new password, updating the database, and updating the secret — all without you doing anything.
Practical Patterns for Any Setup
Pattern 1: Validate secrets at startup
Don't let your app start with missing secrets. Fail loudly at startup rather than cryptically at runtime:
import { z } from 'zod'
const envSchema = z.object({
DATABASE_URL: z.string().url(),
STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
JWT_SECRET: z.string().min(32),
NODE_ENV: z.enum(['development', 'test', 'production']),
})
export const env = envSchema.parse(process.env)
This gives you type-safe environment variables throughout your codebase, with validation that runs at startup.
Pattern 2: Never log secrets
// ❌ Logs the entire request including auth headers
console.log('Incoming request:', req)
// ✅ Log only what you need
console.log('Incoming request:', {
method: req.method,
url: req.url,
userId: req.user?.id,
})
Set up your logger to scrub sensitive fields:
import pino from 'pino'
const logger = pino({
redact: {
paths: ['req.headers.authorization', 'body.password', 'body.token'],
censor: '[REDACTED]',
},
})
Pattern 3: Separate secrets by environment
Never share secrets between production and staging.
myapp/production/stripe → live Stripe keys
myapp/staging/stripe → test Stripe keys
myapp/development/stripe → test Stripe keys
This prevents the classic mistake of accidentally running a test against the production Stripe account.
Pattern 4: Rotate after team member departure
Every time a developer with production access leaves the team, rotate all secrets they had access to. This is non-negotiable.
With Doppler: revoke their access in the dashboard → rotate affected secrets → Doppler syncs automatically.
The .env File You Should Keep
Keep a .env.example file in your repo (committed) with all the variables your app needs, but with placeholder values:
# .env.example — commit this
DATABASE_URL=postgresql://localhost:5432/myapp
STRIPE_SECRET_KEY=sk_test_...
JWT_SECRET=your-secret-here-minimum-32-chars
NEXTAUTH_SECRET=generate-with-openssl-rand-base64-32
REDIS_URL=redis://localhost:6379
And add .env* to .gitignore:
# .gitignore
.env
.env.local
.env.*.local
# Do NOT ignore .env.example
The Minimum Viable Setup for a Production App
If you're launching soon and need the simplest reasonable setup:
-
Delete your
.envfrom the server if it's there - Move all secrets to your platform (Vercel, Railway, Fly.io)
-
Add
.env*to.gitignoreand verify it's not tracked - Add startup validation to catch missing variables immediately
-
Keep a
.env.examplefor documentation
That's it. You're already in a significantly better position than most production apps.
Quick Reference: When to Use What
| Situation | Recommendation |
|---|---|
| Solo project, one environment | Platform env vars (Vercel/Railway) |
| Small team, multiple environments | Doppler (free tier) |
| Multiple platforms | Doppler with platform integrations |
| AWS-native infrastructure | AWS Secrets Manager |
| Enterprise, compliance requirements | AWS Secrets Manager + CloudTrail |
| Self-hosted, maximum control | HashiCorp Vault |
Full guide at stacknotice.com/blog/secrets-management-production-2026
Top comments (0)