Toolmingo
Guides8 min read

How to Use Environment Variables: The Complete Guide

Learn how to use environment variables in Node.js, Python, Go, and PHP. Covers .env files, dotenv, Docker, best practices, and common security mistakes to avoid.

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:

  1. Add .env to .gitignore — never commit it
  2. Commit .env.example with dummy values as documentation
  3. Never put .env contents 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.

Related tools

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools