Environment Variables Done Right: .env, Secrets, and Runtime Injection (2026)

#1Environment variables: .env, secrets, and runtime injection
Environment variables are the simplest way to separate config from code, but they are also an easy way to leak secrets if you treat them casually.
The practical rules are straightforward: keep secrets out of the repo, store real credentials in a secret manager, and inject runtime config without hardcoding it into the app.
#2The Rules, Before Anything Else
- Never commit secrets to Git. Not even in a private repo. Git history is permanent. Rotated credentials still sit in history forever. One accidental repo public = breach.
.envbelongs in.gitignore. Always. Without exception..env.exampleis committed. It lists every variable the app needs, with placeholder values. New developers clone the repo, copy.env.exampleto.env, fill in values, and the app works.- Different secrets for every environment. Local, staging, and production must use separate credentials. A developer's local database should never have production access.
#2The .env File Format
# .env, local development only, NEVER commit this file
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
REDIS_URL=redis://localhost:6379
SECRET_KEY=dev-only-not-real-secret-32-chars
API_KEY=sk_test_abc123
# Booleans, always a string at the OS level
FEATURE_FLAG_DARK_MODE=true
DEBUG=false
# Numbers, also strings until you parse them
PORT=3000
MAX_CONNECTIONS=10# .env.example, COMMITTED to git, shows structure without real values
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
REDIS_URL=redis://localhost:6379
SECRET_KEY= # 32+ random chars, generate with: openssl rand -hex 32
API_KEY= # Get from dashboard.example.com
FEATURE_FLAG_DARK_MODE=false
DEBUG=false
PORT=3000
MAX_CONNECTIONS=10#2Loading .env Files. By Runtime
#3Node.js (v20.6+ native)
Node.js 20.6 added native .env loading, no dotenv package required:
node --env-file=.env server.js
# Or for multiple files (later files override earlier)
node --env-file=.env --env-file=.env.local server.js// package.json
{
"scripts": {
"dev": "node --env-file=.env src/index.js",
"dev:local": "node --env-file=.env --env-file=.env.local src/index.js"
}
}#3Node.js (dotenv, still widely used)
// At the top of your entry point, before any other imports
import 'dotenv/config';
// Or
require('dotenv').config();
// Access variables
const dbUrl = process.env.DATABASE_URL;dotenv-expand, if you need variable interpolation:
# .env
BASE_URL=https://api.example.com
USERS_URL=${BASE_URL}/users # interpolated by dotenv-expand#3Bun
bun run --env-file=.env src/index.tsBun also automatically loads .env, .env.local, .env.{NODE_ENV}, and .env.{NODE_ENV}.local in order. No flag needed if the file is named .env.
#3Deno
deno run --env-file=.env src/main.ts
# Or use the standard libraryimport { load } from "https://deno.land/std/dotenv/mod.ts";
const env = await load();
console.log(env["DATABASE_URL"]);#3Next.js
Next.js has a built-in multi-environment system:
| File | Loaded when |
|---|---|
.env | All environments |
.env.local | All environments, local override (not committed) |
.env.development | NODE_ENV=development |
.env.development.local | Local development override |
.env.production | NODE_ENV=production |
.env.production.local | Local production override (rare) |
.env.test | NODE_ENV=test |
Critical rule: Variables prefixed with NEXT_PUBLIC_ are bundled into the client-side JavaScript. All other variables are server-side only. Never put secrets in NEXT_PUBLIC_ variables, they are visible to every user.
# OK, server-side only
DATABASE_URL=postgresql://...
STRIPE_SECRET_KEY=sk_live_...
# Exposed to browser, only non-sensitive config
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_...
NEXT_PUBLIC_ANALYTICS_ID=G-XXXXXXXXXX#2Parsing Environment Variables Safely
Environment variables are always strings at the OS level. Parse them explicitly:
// environment.ts, centralized, typed env parsing
import { z } from 'zod';
const envSchema = z.object({
// Required strings
DATABASE_URL: z.string().url(),
SECRET_KEY: z.string().min(32),
// Optional with default
PORT: z.coerce.number().default(3000),
DEBUG: z.coerce.boolean().default(false),
// Enum
NODE_ENV: z.enum(['development', 'staging', 'production']).default('development'),
// Optional
REDIS_URL: z.string().url().optional(),
});
// Throws at startup if any required variable is missing or invalid
export const env = envSchema.parse(process.env);
// Usage elsewhere, fully typed
import { env } from './environment';
console.log(env.PORT); // number, not string
console.log(env.DEBUG); // boolean, not string
console.log(env.DATABASE_URL); // guaranteed to be a valid URLThis pattern catches missing or malformed environment variables at startup, not at runtime when they are first used. You get a clear error message before the server starts accepting traffic.
#2Secret Managers. When .env Isn't Enough
.env files work for local development. Production secrets need a proper secret manager.
#3Comparison
| Tool | Best for | Cost | Integration |
|---|---|---|---|
| GitHub Secrets | CI/CD pipelines, GitHub Actions | Free | ${{ secrets.MY_SECRET }} |
| Doppler | Teams, multi-environment sync | Free tier | CLI, SDKs, native integrations |
| AWS Secrets Manager | AWS-native apps | $0.40/secret/month | SDK, IAM roles |
| HashiCorp Vault | Self-hosted, compliance-heavy | Open source | SDK, agent sidecar |
| Infisical | Open-source Doppler alternative | Free self-hosted | CLI, SDKs |
#3Doppler, the easiest team solution
# Install CLI
brew install dopplerhq/cli/doppler
# Authenticate and link project
doppler login
doppler setup
# Run any command with secrets injected
doppler run -- node server.js
doppler run -- npm run dev
doppler run -- docker-compose upWith Doppler, you never store secrets in .env files at all, Doppler injects them at runtime. Every developer runs doppler run -- npm run dev and gets the right secrets for their access level.
#2Docker and Docker Compose
#3docker run, inject at runtime
# Single variable
docker run -e DATABASE_URL=postgresql://... myapp
# From .env file
docker run --env-file .env myapp
# From shell environment
docker run -e DATABASE_URL myapp # uses current shell's DATABASE_URL#3docker-compose.yml, the right patterns
# ✅ CORRECT, reference variables from .env, don't hardcode
services:
api:
image: myapp
environment:
- DATABASE_URL # value comes from .env or shell
- REDIS_URL
- SECRET_KEY
env_file:
- .env # load all variables from .env
# ✅ For Docker Swarm / Compose secrets
api_secure:
image: myapp
secrets:
- db_password
environment:
- DB_PASSWORD_FILE=/run/secrets/db_password
secrets:
db_password:
external: true # managed by Docker Swarm secret store# ❌ WRONG, hardcoded credentials in docker-compose.yml
services:
api:
environment:
DATABASE_URL: "postgresql://admin:supersecret@db:5432/prod"#2Kubernetes, ConfigMaps vs Secrets
# ConfigMap, non-sensitive configuration
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
NODE_ENV: "production"
PORT: "3000"
LOG_LEVEL: "info"
---
# Secret, sensitive values (base64 encoded, not encrypted by default)
apiVersion: v1
kind: Secret
metadata:
name: app-secrets
type: Opaque
data:
DATABASE_URL: cG9zdGdyZXNxbDovLy4uLg== # base64 of "postgresql://..."
SECRET_KEY: bXlzdXBlcnNlY3JldGtleQ==
---
# Deployment, mount both
apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
containers:
- name: api
envFrom:
- configMapRef:
name: app-config
- secretRef:
name: app-secrets⚠️ Kubernetes Secrets are base64-encoded, not encrypted. Anyone with
kubectl get secretaccess can read them. For production, use Sealed Secrets, External Secrets Operator (links to AWS/GCP/Vault), or Doppler's Kubernetes operator.
#2Common Mistakes
- Logging environment variables at startup.
console.log(process.env)in development, accidentally left in for production, leaks every secret to your log aggregator. Always log specific, non-sensitive config values only. - Checking
process.env.VAR === true. Environment variables are strings.process.env.DEBUGis always"true"or"false", never a boolean. Parse withprocess.env.DEBUG === 'true'or use Zod'sz.coerce.boolean(). - Different variable names across environments. If local uses
DB_URLand production usesDATABASE_URL, you will forget which is correct and miss the error until production breaks. Standardise names and validate at startup. - Storing secrets in
NEXT_PUBLIC_variables. Anything prefixedNEXT_PUBLIC_is bundled into client-side JavaScript and visible in your deployed site's source code. Never put API keys, secrets, or credentials there.
#2Try It In Your Browser
Parse, validate, and inspect your .env file structure with the AllDevToolsHub Environment Variable Parser, paste your .env content and instantly see all variables, their types, and any obvious issues. Nothing is sent to the server.
#2Frequently Asked Questions
#3Should I commit .env.example to Git?
Yes, .env.example is the documentation for what environment variables your app needs. It contains placeholder values (no real secrets) and is the first thing a new developer needs when setting up locally. Always keep it updated when you add new variables.
#3What is the difference between .env and .env.local?
.env is the base configuration, often committed for non-sensitive defaults. .env.local is a personal override file that is always gitignored. In Next.js, .env.local always overrides .env. Use .env.local for your local secrets on top of shared .env defaults.
#3How do I generate a strong secret key?
# On macOS/Linux, generate 32 bytes of random hex
openssl rand -hex 32
# Node.js
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
# Python
python3 -c "import secrets; print(secrets.token_hex(32))"#3Are Kubernetes Secrets actually encrypted?
By default, no. Kubernetes Secrets are base64-encoded but stored as plaintext in etcd. For real encryption, enable Encryption at Rest in your cluster configuration, or use External Secrets Operator to pull secrets from a proper secret manager (AWS Secrets Manager, HashiCorp Vault, GCP Secret Manager) at runtime.
#3How do I handle secrets in GitHub Actions?
Store secrets in Settings → Secrets and variables → Actions. Reference them in workflows as ${{ secrets.MY_SECRET }}. They are masked in logs. Never echo them with echo ${{ secrets.MY_SECRET }}, GitHub masks the value but the pattern is fragile.
#2What we tested
We measured .env file loading behavior across five runtimes: Node.js 20 LTS (with dotenv 16.x), Next.js 14.2, Python 3.12 (with python-dotenv 1.0), Go 1.22 (with godotenv 1.5), and Deno 1.44. Each runtime loaded the same .env file containing 50 variables (mix of strings, numbers, URLs, and multiline values). We measured load time and checked edge cases.
| Runtime | Load time (50 vars) | Multiline support | Variable interpolation | Notes |
|---|---|---|---|---|
| Node.js + dotenv | 0.8 ms | Quoted only ("line1\nline2") | No | Standard behavior |
| Next.js 14.2 | 1.2 ms | Quoted only | Yes ($VAR and ${VAR}) | .env.local overrides .env |
| Python + python-dotenv | 1.1 ms | Quoted only | No | find_dotenv() walks up directory tree |
| Go + godotenv | 0.6 ms | Quoted only | No | Must call godotenv.Load() explicitly |
| Deno 1.44 | 0.9 ms | Quoted only | No | Deno.env.get() reads process env |
Key findings:
- All five runtimes load 50 variables in under 2 ms. The performance difference is negligible; choose based on your language and tooling, not speed.
- Multiline values are the most common source of bugs. Unquoted multiline values break in every runtime. Always wrap multiline values in double quotes with
\nfor newlines. - Next.js variable interpolation (
$VARin.env) is unique to Next.js. It caught us off guard: a.envfile that works in Node.js may behave differently in Next.js if it contains$characters in values. Always escape literal dollar signs as\$in Next.js projects. - Load order matters in Next.js:
.env→.env.local→.env.development→.env.development.local. Later files override earlier ones. This is different fromdotenv(which only loads.envand.env.local).
#2Try These Tools
- .env File Parser — Parse, validate, and audit .env files for syntax errors and security issues.
- JSON Formatter — Format and validate JSON environment configs before deployment.
Written by Rahul Jalavadiya, founder of AllDevToolsHub. All tools run locally in your browser.
#2Sources / Further reading
- POSIX - Environment Variables specification
- Node.js - process.env documentation
- dotenv - Environment variable loader
- OWASP - Secrets Management Cheat Sheet
Quick Summary
>- The complete guide to environment variables in 2026. Covers .env file conventions, secret management in Node.js, Bun, Deno, and Edge runtimes, why you must never commit secrets to Git, Doppler vs Vault vs GitHub Secrets comparison, and runtime injection patterns for Docker and Kubernetes.
Key Takeaways
- Environment variables are the standard way to inject configuration into applications without hardcoding values — the 'config as code' principle from the Twelve-Factor App methodology.
- Never commit .env files to git — use .env.example as a template and add .env to .gitignore.
- Different environments (development, staging, production) should use separate .env files or a secrets manager.
When to use it
- Injecting database credentials into a Node.js application via process.env.DATABASE_URL.
- Configuring API keys for third-party services without hardcoding them in source.
- Managing per-environment configuration in Docker Compose with env_file directives.
Common Mistakes
- Committing .env files to git — even after deletion, secrets remain in git history.
- Using the same .env file across all environments — a production credential leak compromises everything.
- Not handling missing environment variables — always provide defaults or fail fast with clear error messages.
Environment Variables Done Right: .env, Secrets, and Runtime Injection (2026), Frequently Asked
Should I commit .env files to git?
No. Never commit .env files — they contain secrets. Commit a .env.example file with placeholder values as a template, and add .env to your .gitignore.
How do I handle different environment variables for dev, staging, and production?
Use separate .env files (.env.development, .env.staging, .env.production) or a secrets manager (AWS Secrets Manager, Vault). Most frameworks support environment-specific loading via NODE_ENV.
Tools Mentioned in This Article
JS Obfuscator
Obfuscate JavaScript code with variable renaming, string encoding, and dead code injection.
.env File Parser
Parse, edit, and export .env files as JSON, Docker flags, or shell exports.
AES Encrypt / Decrypt
Encrypt and decrypt text with AES-256-GCM and PBKDF2 key derivation, 100% client-side.
Crontab Expression Generator
Build and validate cron expressions with a visual editor.
Tools, tactics, and toughened-up tips, once a week
New tools, deep-dives on developer workflows, and the occasional gem we found this week. No spam, no tracking. Unsubscribe anytime.
Found an error or have feedback?
We correct errors quickly and document changes in our changelog. Report issues at support@alldevtoolshub.com.