Skip to main content
AllDevToolsHub
2026-04-28
Last reviewed: Aug 2026
DEVOPS
Est Read: 09_MIN

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

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

#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

  1. 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.
  2. .env belongs in .gitignore. Always. Without exception.
  3. .env.example is committed. It lists every variable the app needs, with placeholder values. New developers clone the repo, copy .env.example to .env, fill in values, and the app works.
  4. 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

bash
# .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
bash
# .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:

bash
node --env-file=.env server.js
# Or for multiple files (later files override earlier)
node --env-file=.env --env-file=.env.local server.js
json
// 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)

javascript
// 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:

bash
# .env
BASE_URL=https://api.example.com
USERS_URL=${BASE_URL}/users    # interpolated by dotenv-expand

#3Bun

bash
bun run --env-file=.env src/index.ts

Bun 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

bash
deno run --env-file=.env src/main.ts
# Or use the standard library
typescript
import { 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:

FileLoaded when
.envAll environments
.env.localAll environments, local override (not committed)
.env.developmentNODE_ENV=development
.env.development.localLocal development override
.env.productionNODE_ENV=production
.env.production.localLocal production override (rare)
.env.testNODE_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.

bash
# 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:

typescript
// 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 URL

This 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

ToolBest forCostIntegration
GitHub SecretsCI/CD pipelines, GitHub ActionsFree${{ secrets.MY_SECRET }}
DopplerTeams, multi-environment syncFree tierCLI, SDKs, native integrations
AWS Secrets ManagerAWS-native apps$0.40/secret/monthSDK, IAM roles
HashiCorp VaultSelf-hosted, compliance-heavyOpen sourceSDK, agent sidecar
InfisicalOpen-source Doppler alternativeFree self-hostedCLI, SDKs

#3Doppler, the easiest team solution

bash
# 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 up

With 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

bash
# 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

yaml
# ✅ 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
yaml
# ❌ WRONG, hardcoded credentials in docker-compose.yml
services:
  api:
    environment:
      DATABASE_URL: "postgresql://admin:supersecret@db:5432/prod"

#2Kubernetes, ConfigMaps vs Secrets

yaml
# 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 secret access 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.DEBUG is always "true" or "false", never a boolean. Parse with process.env.DEBUG === 'true' or use Zod's z.coerce.boolean().
  • Different variable names across environments. If local uses DB_URL and production uses DATABASE_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 prefixed NEXT_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?

bash
# 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.

RuntimeLoad time (50 vars)Multiline supportVariable interpolationNotes
Node.js + dotenv0.8 msQuoted only ("line1\nline2")NoStandard behavior
Next.js 14.21.2 msQuoted onlyYes ($VAR and ${VAR}).env.local overrides .env
Python + python-dotenv1.1 msQuoted onlyNofind_dotenv() walks up directory tree
Go + godotenv0.6 msQuoted onlyNoMust call godotenv.Load() explicitly
Deno 1.440.9 msQuoted onlyNoDeno.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 \n for newlines.
  • Next.js variable interpolation ($VAR in .env) is unique to Next.js. It caught us off guard: a .env file 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 from dotenv (which only loads .env and .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

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

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.
Use Cases

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.
Watch out

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.
FAQ

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.

RJRahul JalavadiyaFounder & Lead Engineer
Published 2026-04-28Last reviewed 2026-08-23

Tools Mentioned in This Article

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.

Last reviewed: 2026-08-23
Security Memo
AT

Rahul Jalavadiya

Engineering Protocol V1

Specializing in local-first architecture and Zero-Trust developer workflows. No data leaves the machine.