DEV Community

Alex Spinov
Alex Spinov

Posted on

Stop Using Environment Variables for Configuration (Here's What I Do Instead)

I know this is going to be controversial

Environment variables are the default for app configuration. Every tutorial uses them. Every framework expects them. .env files are everywhere.

But after managing 77 production services and debugging configuration issues at 2 AM, I've stopped using env vars as my primary config mechanism.

Here's why — and what I use instead.


The problems with env vars

1. No type safety

DATABASE_PORT=5432  # Is this a string or an integer?
ENABLE_CACHE=true    # Is this a boolean or the string "true"?
MAX_RETRIES=three    # This will silently break your app
Enter fullscreen mode Exit fullscreen mode

Every env var is a string. Your app has to parse them, and if it does it wrong, you won't know until production.

2. No validation at startup

Missing DATABASE_URL? You'll find out when the first query runs — not when the app starts. I've seen apps run for hours before hitting a code path that needs a missing env var.

3. No documentation

# .env.example
API_KEY=
SECRET=
DATABASE_URL=
REDIS_URL=
STRIPE_KEY=
SENDGRID_KEY=
AWS_ACCESS_KEY=
AWS_SECRET_KEY=
Enter fullscreen mode Exit fullscreen mode

What format should API_KEY be in? What's the default for REDIS_URL? Is STRIPE_KEY the test key or production key? Nobody knows without reading the code.

4. They leak everywhere

# Every child process inherits ALL env vars
ps eww  # Shows env vars on some systems
cat /proc/self/environ  # Full env dump on Linux
docker inspect container_name  # Shows all env vars
Enter fullscreen mode Exit fullscreen mode

Env vars are not secrets management. They're visible in process listings, Docker inspect, CI logs, and error reporting tools.


What I use instead: typed config files with validation

# config.py — my actual config pattern
from pydantic_settings import BaseSettings
from pydantic import Field, field_validator

class Config(BaseSettings):
    database_url: str = Field(..., description="PostgreSQL connection string")
    database_port: int = Field(default=5432, ge=1, le=65535)
    enable_cache: bool = Field(default=True)
    max_retries: int = Field(default=3, ge=0, le=10)
    api_key: str = Field(..., min_length=20)

    @field_validator('database_url')
    @classmethod
    def validate_db_url(cls, v):
        if not v.startswith('postgresql://'):
            raise ValueError('Must be a PostgreSQL URL')
        return v

    class Config:
        env_file = '.env'

# This runs at startup — if anything is wrong, the app won't start
config = Config()
Enter fullscreen mode Exit fullscreen mode

What this gives you:

  • Type safety (int, bool, str are enforced)
  • Validation at startup (app crashes immediately if config is wrong)
  • Documentation (Field descriptions + type hints)
  • Defaults (sensible fallbacks built in)
  • Still reads from env vars (backwards compatible)

For Node.js: zod + dotenv

import { z } from 'zod';
import 'dotenv/config';

const configSchema = z.object({
  DATABASE_URL: z.string().url(),
  PORT: z.coerce.number().int().min(1).max(65535).default(3000),
  NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
  API_KEY: z.string().min(20),
});

export const config = configSchema.parse(process.env);
// ^ Throws immediately if validation fails
Enter fullscreen mode Exit fullscreen mode

The pattern in one sentence

Read from env vars. Validate and type-cast at the boundary. Never use process.env or os.environ directly in business logic.

This way you get the deployment flexibility of env vars with the safety of a typed config.


"But env vars are the 12-Factor App standard!"

Yes, and the 12-Factor App was written in 2011. It's great advice for deployment (store config in the environment, not in code). But it says nothing about how your app should consume that config.

My approach is 100% compatible with 12-Factor. I still deploy with env vars. I just don't use them raw.


I build automation tools and data pipelines — config management is something I deal with daily across 77 services. Follow me for more hot takes on developer tooling.

Do you validate your env vars at startup? Or are you living dangerously? Let me know in the comments.


Need help with production infrastructure? Email me.


More from me: 10 Dev Tools I Use Daily | 77 Scrapers on a Schedule | 150+ Free APIs

Top comments (0)