NC Logo UseToolSuite
Developer Tools

Environment Variables and Config Management: A Developer's Guide

Learn how to manage environment variables and configuration files securely. Covers .env files, secrets management, 12-factor app principles, and common mistakes that leak credentials.

Necmeddin Cunedioglu Necmeddin Cunedioglu

Practice what you learn

Base64 Encoder / Decoder

Try it free →

Two years ago, a colleague accidentally pushed an .env file to a public GitHub repository. It contained the production database URL, an AWS access key, and a Stripe secret key. Within 6 hours, someone had spun up 14 EC2 instances for cryptocurrency mining. The AWS bill hit $2,300 before we noticed.

Configuration management — especially secrets — is one of those things that seems simple until you get it wrong. Here’s everything I’ve learned about doing it right.

The 12-Factor App Approach

The 12-Factor methodology defines a clear rule: store configuration in environment variables, not in code. Configuration is anything that varies between environments (development, staging, production):

  • Database connection strings
  • API keys and secrets
  • Feature flags
  • Service URLs
  • Port numbers
// Bad: hardcoded config
const db = new Database('postgres://admin:pass@prod-db:5432/myapp');

// Good: environment variable
const db = new Database(process.env.DATABASE_URL);

Environment variables separate config from code. Your codebase can be open-source without exposing any credentials.

.env Files: Local Development

Basic Structure

# .env
DATABASE_URL=postgres://localhost:5432/myapp_dev
API_KEY=sk_test_abc123
REDIS_URL=redis://localhost:6379
DEBUG=true
PORT=3000

Loading .env Files

Node.js (dotenv):

require('dotenv').config();
// or in ES modules:
import 'dotenv/config';

console.log(process.env.DATABASE_URL);

Python (python-dotenv):

from dotenv import load_dotenv
import os

load_dotenv()
db_url = os.getenv('DATABASE_URL')

.env File Hierarchy

Most frameworks support multiple .env files with a priority order:

.env.local          ← highest priority (git-ignored)
.env.development    ← environment-specific
.env                ← shared defaults (can be committed)

What goes where:

  • .env — non-sensitive defaults shared across the team (PORT=3000, LOG_LEVEL=info)
  • .env.local — personal overrides and secrets (always gitignored)
  • .env.production — production-specific non-secret values

Never Store Secrets in Code

The .gitignore Rule

# Always gitignore these
.env.local
.env.*.local
.env.production
.env.staging

# Keep shared defaults
# !.env  (commit this if it only contains non-sensitive values)

Scanning for Leaked Secrets

Even with .gitignore, secrets slip through. Tools like git-secrets, truffleHog, and gitleaks scan your repository for accidentally committed credentials:

# Install gitleaks
brew install gitleaks

# Scan your repo
gitleaks detect --source . --verbose

Add this to your CI pipeline — it takes seconds and catches mistakes before they reach your remote repository.

What to Do If You Leak a Secret

  1. Revoke the key immediately — don’t just remove it from code
  2. Rotate all exposed credentials — generate new keys
  3. Scrub git historygit filter-branch or BFG Repo Cleaner
  4. Audit access logs — check if the key was used maliciously
  5. Add .gitignore rules — prevent recurrence

The key is already in your git history even if you delete the file in a new commit. Step 3 is essential.

Environment Variable Patterns

Validation at Startup

Don’t wait until a function needs a variable to discover it’s missing. Validate everything at application startup:

const required = [
  'DATABASE_URL',
  'API_KEY',
  'JWT_SECRET',
  'REDIS_URL',
];

const missing = required.filter(key => !process.env[key]);

if (missing.length > 0) {
  console.error(`Missing required env vars: ${missing.join(', ')}`);
  process.exit(1);
}

With Zod (my preferred approach):

import { z } from 'zod';

const envSchema = z.object({
  DATABASE_URL: z.string().url(),
  API_KEY: z.string().min(10),
  JWT_SECRET: z.string().min(32),
  PORT: z.coerce.number().default(3000),
  NODE_ENV: z.enum(['development', 'staging', 'production']).default('development'),
  DEBUG: z.coerce.boolean().default(false),
});

export const env = envSchema.parse(process.env);
// Now env.PORT is a number, env.DEBUG is a boolean
// Missing or invalid values throw descriptive errors at startup

Type Conversion

Environment variables are always strings. Forgetting this causes subtle bugs:

// Bug: this is the STRING "3000", not the number 3000
const port = process.env.PORT;
console.log(port + 1); // "30001" not 3001

// Fix: explicit conversion
const port = parseInt(process.env.PORT, 10) || 3000;

// Bug: this is the STRING "false", which is truthy!
if (process.env.DEBUG) {
  enableDebugMode(); // Always runs!
}

// Fix: explicit boolean check
const debug = process.env.DEBUG === 'true';

Encoding secrets? If you need to store binary data or certificates in environment variables, Base64 encoding is the standard approach. Use our Base64 Encoder to encode values, and decode them in your application at startup.

Secrets Management in Production

Platform-Native Secret Stores

For production, don’t use .env files. Use your platform’s secret management:

PlatformSecret Store
AWSSecrets Manager, SSM Parameter Store
Google CloudSecret Manager
AzureKey Vault
KubernetesSecrets (with encryption at rest)
VercelEnvironment Variables (encrypted)
CloudflareWorkers Secrets
DockerDocker Secrets (Swarm mode)

Kubernetes Secrets

# secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: app-secrets
type: Opaque
data:
  DATABASE_URL: cG9zdGdyZXM6Ly9hZG1pbjpwYXNzQGRiOjU0MzIvYXBw  # base64
  API_KEY: c2tfdGVzdF9hYmMxMjM=  # base64
# deployment.yaml
spec:
  containers:
    - name: app
      envFrom:
        - secretRef:
            name: app-secrets

Note that Kubernetes secrets are Base64-encoded, not encrypted. Enable encryption at rest in your cluster configuration.

Config Files: YAML vs JSON vs TOML

For non-secret configuration, structured config files are often better than environment variables:

FormatBest ForCommentsDrawback
YAMLKubernetes, Docker, CI/CDYesIndentation-sensitive
JSONAPIs, package.json, tsconfigNoVerbose, no comments
TOMLRust (Cargo.toml), Python (pyproject.toml)YesLess tooling support
.envLocal development secretsYes (some parsers)Flat key-value only

For converting between formats, our YAML to JSON Converter handles bidirectional conversion when you need to switch between config formats.

Common Mistakes

Mistake 1: Different Variable Names Across Environments

# Development
DB_HOST=localhost

# Production
DATABASE_HOST=prod-db.example.com  # Different name!

Use identical variable names across all environments. Only the values should change.

Mistake 2: Storing Secrets in Docker Images

# NEVER do this — secret is baked into the image layer
ENV API_KEY=sk_live_abc123

# Instead, pass at runtime
# docker run -e API_KEY=sk_live_abc123 myapp

Docker image layers are permanent. Even if you delete the ENV line in a later layer, the secret exists in previous layers.

Mistake 3: Logging Environment Variables

// Accidentally logs all secrets
console.log('Config:', process.env);

// Safe: log only non-sensitive values
console.log('Config:', {
  port: process.env.PORT,
  env: process.env.NODE_ENV,
  dbHost: process.env.DB_HOST,
  // Never log: API_KEY, JWT_SECRET, DATABASE_URL
});

Mistake 4: Using Production Secrets in Development

Your local .env should use test/development credentials, never production. If your dev database URL points to production, one bad migration wipes real user data.

Quick Reference

PracticeDoDon’t
SecretsPlatform secret storeHardcode in source
Local dev.env.local (gitignored)Share real credentials
ValidationValidate at startupFail silently at runtime
TypesExplicit conversionTrust string values
DockerRuntime --env or --env-fileENV in Dockerfile
Git.gitignore + secret scanningCommit .env.local

Further Reading


Managing configuration? Use our Base64 Encoder for encoding secrets, Hash Generator for integrity checks, and YAML to JSON Converter for config format conversion.

Necmeddin Cunedioglu
Necmeddin Cunedioglu Author

Software developer and the creator of UseToolSuite. I write about the tools and techniques I use daily as a developer — practical guides based on real experience, not theory.