</>StackKit
</>StackKit

Developer tutorials & guides

Environment Variables: The Complete Guide to Managing Config Safely

Learn how to use environment variables properly — in Node.js, Docker, CI/CD, and production — without leaking secrets or breaking deployments.

N

Nitheesh DR

Founder & Full-Stack Engineer

7 min read739 words
#environment-variables#security#nodejs#devops#dotenv

What Are Environment Variables?

Environment variables are key-value pairs stored in the operating system's environment, outside your application code. They let you configure an application differently across environments (development, staging, production) without changing the code.

DATABASE_URL=postgresql://localhost:5432/mydb
SECRET_KEY=abc123
NODE_ENV=production
PORT=3000

The key rule: secrets and config should never be hardcoded in source code. Environment variables are how you keep them out.


The .env File

For local development, the .env file is the standard way to set environment variables. The dotenv library loads it automatically.

# .env
DATABASE_URL=postgresql://localhost:5432/devdb
SECRET_KEY=dev-secret-not-real
STRIPE_SECRET_KEY=sk_test_your_stripe_key_here
PORT=3000
NODE_ENV=development

Node.js with dotenv:

npm install dotenv
// At the very top of your entry file
import "dotenv/config";
// or
require("dotenv").config();

const dbUrl = process.env.DATABASE_URL;

Node.js 20.6+ has built-in .env support — no package needed:

node --env-file=.env server.js

CRITICAL: Add .env to .gitignore

Never commit .env to version control. This is the most common way secrets get leaked.

# .gitignore
.env
.env.local
.env.production

Instead, commit a .env.example file with placeholder values:

# .env.example — safe to commit
DATABASE_URL=postgresql://localhost:5432/yourdb
SECRET_KEY=your-secret-key-here
STRIPE_SECRET_KEY=sk_test_your-key
PORT=3000
NODE_ENV=development

This documents what variables are needed without exposing real values.


Naming Conventions

Use SCREAMING_SNAKE_CASE. Be descriptive:

# Good
DATABASE_URL=...
STRIPE_SECRET_KEY=...
SENDGRID_API_KEY=...
NEXT_PUBLIC_SITE_URL=...   # NEXT_PUBLIC_ prefix = exposed to browser in Next.js

# Bad
DB=...
KEY=...
SECRET=...

Group related variables with a prefix:

POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_USER=admin
POSTGRES_PASSWORD=secret
POSTGRES_DB=myapp

REDIS_HOST=localhost
REDIS_PORT=6379

Validation at Startup

Never let your app start with missing required variables. Catch it immediately:

// env.js — validate on startup
const required = [
  "DATABASE_URL",
  "SECRET_KEY",
  "STRIPE_SECRET_KEY",
];

for (const key of required) {
  if (!process.env[key]) {
    throw new Error(`Missing required environment variable: ${key}`);
  }
}

export const env = {
  databaseUrl: process.env.DATABASE_URL,
  secretKey: process.env.SECRET_KEY,
  stripeSecretKey: process.env.STRIPE_SECRET_KEY,
  port: parseInt(process.env.PORT || "3000", 10),
  nodeEnv: process.env.NODE_ENV || "development",
};

For more robust validation, use zod:

npm install zod
import { z } from "zod";

const envSchema = z.object({
  DATABASE_URL: z.string().url(),
  SECRET_KEY: z.string().min(32),
  PORT: z.coerce.number().default(3000),
  NODE_ENV: z.enum(["development", "staging", "production"]).default("development"),
});

export const env = envSchema.parse(process.env);

If any variable is missing or invalid, your app crashes on startup with a clear error — far better than a cryptic runtime failure hours later.


Environment Variables in CI/CD

Never put secrets in your docker-compose.yml, Dockerfile, or CI config files.

GitHub Actions:

# .github/workflows/deploy.yml
steps:
  - name: Deploy
    env:
      DATABASE_URL: ${{ secrets.DATABASE_URL }}
      SECRET_KEY: ${{ secrets.SECRET_KEY }}
    run: npm run deploy

Add secrets under Repository Settings → Secrets and Variables → Actions.

Docker:

# Pass at runtime, not in Dockerfile
docker run -e DATABASE_URL=postgresql://... myapp

# Or use --env-file
docker run --env-file .env.production myapp

Never do this:

# WRONG — baked into the image layer, visible in docker history
ENV DATABASE_URL=postgresql://real-host/prod

Multiple Environments

Use separate .env files per environment:

.env.development    # local dev
.env.test           # test runner
.env.staging        # staging server
.env.production     # production (NEVER commit this)

Load the right one based on NODE_ENV:

dotenv -e .env.${NODE_ENV} node server.js

Or in Next.js — it handles this automatically: .env.local overrides .env.


Secrets Management at Scale

For production applications, a .env file on a server is a security risk. Use a secrets manager instead:

  • AWS Secrets Manager — retrieve secrets programmatically
  • HashiCorp Vault — enterprise secrets management
  • Doppler — developer-friendly secrets manager, syncs to any platform
  • Vercel / Netlify / Railway — built-in environment variable management

Example with Doppler:

doppler run -- node server.js
# Doppler injects env vars at runtime — no .env file needed on the server

Security Checklist

  • .env is in .gitignore
  • .env.example is committed with placeholder values
  • All required variables are validated at startup
  • Secrets are not logged (especially in error handlers)
  • Different secrets per environment (dev secrets ≠ prod secrets)
  • Rotate secrets if they're ever exposed
  • Use a secrets manager in production

Conclusion

Environment variables are the simplest and most universal way to configure applications. The rules are straightforward: never hardcode secrets, never commit .env, always validate on startup, and use a proper secrets manager for production. Following these practices prevents the most common class of security incidents — accidentally exposed credentials.

Tagged

#environment-variables#security#nodejs#devops#dotenv
N

Written by

Nitheesh DR

Founder & Full-Stack Engineer

Nitheesh is a full-stack software engineer based in Tamil Nadu, India, with hands-on experience building production SaaS applications using Next.js, TypeScript, React, Node.js, and cloud infrastructure. He founded StackKit to share the practical knowledge he uses every day — not just theory, but the real-world techniques that help developers ship better software faster.