Environment variables let you configure your application without hardcoding sensitive or environment-specific values into your source code. They're how you keep API keys out of GitHub, switch between dev/staging/production databases, and build apps that run anywhere.
Quick-reference table
| Task | Node.js | Python | Go | PHP |
|---|---|---|---|---|
| Read a variable | process.env.NAME |
os.environ['NAME'] |
os.Getenv("NAME") |
getenv('NAME') |
| Read with default | process.env.NAME || 'default' |
os.environ.get('NAME', 'default') |
getEnvOr("NAME", "default") |
getenv('NAME') ?: 'default' |
| Check if set | 'NAME' in process.env |
'NAME' in os.environ |
os.Getenv("NAME") != "" |
getenv('NAME') !== false |
Load a .env file |
dotenv package |
python-dotenv |
godotenv |
vlucas/phpdotenv |
What are environment variables?
Environment variables are key-value pairs available to any process running on your machine (or container). They live outside your code, in the operating system's process environment.
NAME=value
DATABASE_URL=postgres://user:password@localhost/mydb
API_KEY=sk-abc123
NODE_ENV=production
Every process inherits the environment of its parent. When you start a Node.js server, it inherits all variables from the shell that launched it.
Why use them?
- Keep secrets (API keys, passwords) out of source code
- Different values for dev, staging, and production without code changes
- 12-Factor App methodology — configuration via environment
- Works the same way in every language and on every platform
The .env file
A .env file is a plain text file in your project root that stores local development variables. It's loaded by a library at startup — it's not automatically read by the OS.
# .env
DATABASE_URL=postgres://localhost/myapp_dev
API_KEY=sk-test-abc123
PORT=3000
DEBUG=true
REDIS_URL=redis://localhost:6379
Critical rules:
- Add
.envto.gitignore— never commit it - Commit
.env.examplewith dummy values as documentation - Never put
.envcontents in logs or error messages
# .gitignore
.env
.env.local
.env.*.local
# .env.example (commit this)
DATABASE_URL=postgres://localhost/myapp_dev
API_KEY=your_api_key_here
PORT=3000
DEBUG=false
Node.js
Node.js exposes environment variables via the global process.env object.
Reading variables
// Read an env var
const port = process.env.PORT;
// Read with a fallback default
const port = process.env.PORT || 3000;
// Read required var (crash fast if missing)
const apiKey = process.env.API_KEY;
if (!apiKey) {
throw new Error('API_KEY environment variable is required');
}
Loading .env with dotenv
npm install dotenv
// index.js — call this before anything else
import 'dotenv/config'; // ES module
// or
require('dotenv').config(); // CommonJS
// Now process.env has your .env values
console.log(process.env.DATABASE_URL);
Node.js 20.6+ has built-in .env support without any library:
node --env-file=.env index.js
Multiple environments
.env # local dev (gitignored)
.env.test # test environment (gitignored)
.env.production # production (gitignored)
.env.example # committed, shows required vars
// Load based on NODE_ENV
const envFile = `.env.${process.env.NODE_ENV || 'development'}`;
require('dotenv').config({ path: envFile });
Validate on startup
// config.js — validate all required vars at boot
const required = ['DATABASE_URL', 'API_KEY', 'JWT_SECRET'];
for (const key of required) {
if (!process.env[key]) {
throw new Error(`Missing required environment variable: ${key}`);
}
}
export const config = {
databaseUrl: process.env.DATABASE_URL,
apiKey: process.env.API_KEY,
jwtSecret: process.env.JWT_SECRET,
port: parseInt(process.env.PORT ?? '3000', 10),
debug: process.env.DEBUG === 'true',
};
Python
Python reads environment variables via the os module.
import os
# Read a variable (returns None if not set)
db_url = os.environ.get('DATABASE_URL')
# Read with default
port = int(os.environ.get('PORT', '3000'))
# Read required var (raises KeyError if missing)
api_key = os.environ['API_KEY']
# Check if set
if 'DEBUG' in os.environ:
print('Debug mode on')
Loading .env with python-dotenv
pip install python-dotenv
from dotenv import load_dotenv
import os
# Load .env before reading any vars
load_dotenv()
db_url = os.environ.get('DATABASE_URL')
api_key = os.environ['API_KEY']
For Django and Flask, call load_dotenv() in settings.py / app.py before other imports use env vars.
# Validate required vars
from dotenv import load_dotenv
import os
import sys
load_dotenv()
REQUIRED = ['DATABASE_URL', 'API_KEY', 'SECRET_KEY']
missing = [k for k in REQUIRED if not os.environ.get(k)]
if missing:
print(f"Missing env vars: {', '.join(missing)}", file=sys.stderr)
sys.exit(1)
config = {
'database_url': os.environ['DATABASE_URL'],
'api_key': os.environ['API_KEY'],
'port': int(os.environ.get('PORT', '3000')),
'debug': os.environ.get('DEBUG', '').lower() == 'true',
}
Go
Go reads environment variables with the os package.
import (
"fmt"
"os"
"strconv"
)
// Read a variable (empty string if not set)
dbURL := os.Getenv("DATABASE_URL")
// Helper: read with default
func getEnvOr(key, fallback string) string {
if val := os.Getenv(key); val != "" {
return val
}
return fallback
}
port := getEnvOr("PORT", "3000")
portNum, _ := strconv.Atoi(port)
Loading .env with godotenv
go get github.com/joho/godotenv
import "github.com/joho/godotenv"
func main() {
// Load .env — ignore error in production (vars set by platform)
_ = godotenv.Load()
dbURL := os.Getenv("DATABASE_URL")
apiKey := os.Getenv("API_KEY")
fmt.Println("Starting on port", getEnvOr("PORT", "3000"))
}
Validate at startup
func mustGetenv(key string) string {
val := os.Getenv(key)
if val == "" {
panic(fmt.Sprintf("required environment variable %q is not set", key))
}
return val
}
// In main() or init()
dbURL := mustGetenv("DATABASE_URL")
apiKey := mustGetenv("API_KEY")
PHP
PHP reads environment variables via getenv() or the $_ENV superglobal.
// Read a variable
$dbUrl = getenv('DATABASE_URL');
// Read with default (PHP 8+ nullsafe)
$port = getenv('PORT') ?: '3000';
// $_ENV superglobal (requires variables_order="EGPCS" in php.ini)
$apiKey = $_ENV['API_KEY'] ?? null;
// Check if set
if (getenv('DEBUG') !== false) {
// debug mode
}
Loading .env with phpdotenv
composer require vlucas/phpdotenv
require_once __DIR__ . '/vendor/autoload.php';
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->load();
// Validate required vars (throws exception if missing)
$dotenv->required(['DATABASE_URL', 'API_KEY']);
$dbUrl = $_ENV['DATABASE_URL'];
$apiKey = $_ENV['API_KEY'];
$port = $_ENV['PORT'] ?? '3000';
Docker and Docker Compose
Pass env vars to a container
# Pass individual vars
docker run -e DATABASE_URL=postgres://... myapp
# Pass from current shell
docker run -e DATABASE_URL myapp # uses $DATABASE_URL from your shell
# Pass an entire .env file
docker run --env-file .env myapp
Docker Compose
# docker-compose.yml
services:
app:
image: myapp
env_file:
- .env # load from file
environment:
NODE_ENV: production # override or add
PORT: 3000
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} # from host env or .env
POSTGRES_DB: myapp
Build-time vs runtime
# ARG = build-time only (baked into image layer)
ARG NODE_VERSION=20
FROM node:${NODE_VERSION}
# ENV = runtime (visible to the running process)
ENV NODE_ENV=production
ENV PORT=3000
# Don't bake secrets into the image with ARG or ENV
# Pass them at runtime via -e or --env-file
Setting variables in the shell
# Set for current shell session
export DATABASE_URL=postgres://localhost/myapp
# Set for a single command
PORT=8080 node server.js
# Set permanently (add to ~/.bashrc or ~/.zshrc)
echo 'export API_KEY=sk-abc123' >> ~/.zshrc
source ~/.zshrc
# Unset a variable
unset API_KEY
On Windows (PowerShell):
# Set for current session
$env:DATABASE_URL = "postgres://localhost/myapp"
# Set permanently (user scope)
[System.Environment]::SetEnvironmentVariable("DATABASE_URL", "postgres://localhost/myapp", "User")
Platform-specific configuration
Most hosting platforms have a UI or CLI for setting env vars:
| Platform | How to set |
|---|---|
| Vercel | Dashboard → Project → Settings → Environment Variables |
| Railway | Dashboard → Project → Variables |
| Heroku | heroku config:set KEY=value |
| AWS Lambda | Console → Function → Configuration → Environment variables |
| GitHub Actions | Repository → Settings → Secrets and variables |
| Fly.io | fly secrets set KEY=value |
| Render | Dashboard → Service → Environment |
In CI/CD pipelines, use secret management — never hardcode secrets in workflow files:
# GitHub Actions
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy
env:
API_KEY: ${{ secrets.API_KEY }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
run: npm run deploy
Common mistakes
| Mistake | Problem | Fix |
|---|---|---|
Committing .env to git |
Secrets exposed forever in history | Add .env to .gitignore, rotate all exposed secrets |
process.env.PORT as number |
It's always a string | parseInt(process.env.PORT, 10) |
os.environ['KEY'] without handling missing |
Raises KeyError at runtime |
Use os.environ.get('KEY') or validate at startup |
| Secrets in Docker image layers | Image export leaks secrets | Pass secrets at runtime with --env-file, not ARG/ENV |
| No validation at startup | App crashes deep in code | Validate all required vars at boot with clear error messages |
| Logging env vars for debug | Secrets in log files | Log variable names, never values |
| Different var names per environment | Brittle deployment | Use same names everywhere, change values via platform |
FAQ
Should I commit .env.example?
Yes — commit .env.example with placeholder values (no real secrets). It documents what variables the app needs. Add .env to .gitignore.
What's the difference between os.environ['KEY'] and os.environ.get('KEY') in Python?os.environ['KEY'] raises KeyError if the variable is missing. os.environ.get('KEY') returns None (or a default) if missing. Use ['KEY'] for required vars so the error is immediate and obvious.
When should I use a secrets manager instead of env vars?
For team environments and production, use a dedicated secrets manager (AWS Secrets Manager, HashiCorp Vault, Doppler) when you need: secret rotation, audit logs, fine-grained access control, or when secrets change without redeployment.
Why does process.env.DEBUG === true always fail in Node.js?
All environment variables are strings. process.env.DEBUG is 'true', not true. Compare with process.env.DEBUG === 'true'.
Can I use env vars in client-side (browser) JavaScript?
No — browser JS has no access to process.env. Build tools like Vite and Next.js inject env vars at build time for public values. In Vite, use VITE_ prefix; in Next.js, use NEXT_PUBLIC_ prefix. Never put secrets in client-side vars.
What's the best way to handle different environments (dev/staging/prod)?
Use one .env file per environment (.env.development, .env.staging, .env.production), gitignored. Your deployment platform sets the real production values. The app code never needs to know which environment it's in — it just reads whatever is set.