ZORL
Environment Variables vs Config Files: When to Use Each
5 min read

Environment Variables vs Config Files: When to Use Each

A practical guide to choosing between environment variables and configuration files for your application settings. Learn the trade-offs and best practices for each approach.

environment variablesconfig filesapplication configurationtwelve-factor appdevops

Every developer eventually faces this question: should I use environment variables or a config file? The answer is not always obvious, and getting it wrong can create maintenance headaches down the road.

After working on dozens of projects across different tech stacks, here is a practical framework for making this decision.

The Core Difference

Environment variables are key-value pairs set outside your application, typically by the operating system or container runtime. They are ephemeral and exist only for the lifetime of the process.

Config files are structured documents (JSON, YAML, TOML) stored alongside your code or in a separate location. They persist on disk and can contain complex nested structures.

# Environment variable
export DATABASE_URL="postgres://localhost/mydb"

# Config file (config.json)
{
  "database": {
    "host": "localhost",
    "port": 5432,
    "name": "mydb"
  }
}

Both accomplish the same goal: separating configuration from code. The right choice depends on what you are configuring and where your application runs.

When Environment Variables Win

Secrets and Credentials

Environment variables are the standard for secrets. Here is why:

  1. They are not committed to version control - Unlike config files, env vars never accidentally end up in your git history
  2. Platform support - Every hosting platform (AWS, Vercel, Heroku, Docker) has built-in env var management
  3. Runtime injection - Secrets can be injected at deploy time without touching your codebase
# These should ALWAYS be environment variables
DATABASE_URL=postgres://user:pass@host/db
API_SECRET_KEY=sk_live_xxxxx
STRIPE_WEBHOOK_SECRET=whsec_xxxxx

If you are storing credentials in a config file, stop. Move them to environment variables today. Tools like zorath-env can help you validate that required secrets exist before your app starts.

Per-Environment Settings

Values that change between development, staging, and production belong in environment variables:

# Development
NODE_ENV=development
LOG_LEVEL=debug
API_URL=http://localhost:3000

# Production
NODE_ENV=production
LOG_LEVEL=error
API_URL=https://api.example.com

This follows the Twelve-Factor App methodology: configuration that varies between deploys should be stored in the environment.

Simple Key-Value Settings

If your configuration is flat (no nesting) and consists of simple strings, numbers, or booleans, environment variables are usually simpler:

PORT=3000
MAX_UPLOAD_SIZE=10485760
ENABLE_CACHE=true
RATE_LIMIT=100

No parsing required. No file to read. Just process.env.PORT and you are done.

When Config Files Win

Complex Nested Structures

Environment variables are flat. If your configuration has hierarchy, a config file is more expressive:

# config.yaml
logging:
  level: info
  format: json
  outputs:
    - type: file
      path: /var/log/app.log
      rotation:
        max_size: 100MB
        max_age: 7d
    - type: stdout

cache:
  redis:
    cluster:
      - host: redis-1.example.com
        port: 6379
      - host: redis-2.example.com
        port: 6379
    ttl: 3600

Trying to express this in environment variables would be painful and error-prone.

Feature Flags and Business Logic

Configuration that represents application behavior (not infrastructure) often works better in files:

{
  "features": {
    "new_checkout_flow": {
      "enabled": true,
      "rollout_percentage": 25,
      "excluded_regions": ["EU", "UK"]
    },
    "dark_mode": {
      "enabled": true
    }
  }
}

These settings are often managed by product teams, not ops, and benefit from being in a reviewable, diffable format.

Default Values with Overrides

A common pattern: ship default config in a file, override specific values with environment variables.

// Load defaults from file
const defaults = require('./config.default.json');

// Override with environment variables
const config = {
  ...defaults,
  database: {
    ...defaults.database,
    host: process.env.DB_HOST || defaults.database.host,
    password: process.env.DB_PASSWORD // Always from env
  }
};

This gives you the best of both worlds: documented defaults in version control, with sensitive or environment-specific values injected at runtime.

The Hybrid Approach

Most production applications use both. Here is a sensible division:

| Use Environment Variables | Use Config Files | |--------------------------|------------------| | Secrets and API keys | Feature flags | | Database credentials | Default settings | | Per-environment URLs | Complex structures | | Feature toggles (simple) | Logging configuration | | Port numbers | Validation rules |

A Real-World Example

Consider a typical web application:

# .env (environment variables)
NODE_ENV=production
DATABASE_URL=postgres://...
REDIS_URL=redis://...
SESSION_SECRET=xxxxx
STRIPE_SECRET_KEY=sk_live_xxxxx
# config.yaml (config file, committed to repo)
app:
  name: "My Application"
  default_locale: "en"

rate_limiting:
  window_ms: 60000
  max_requests: 100

pagination:
  default_page_size: 20
  max_page_size: 100

The environment variables handle secrets and deployment-specific settings. The config file handles application behavior that is the same across all environments.

Validation Matters Either Way

Whether you choose environment variables, config files, or both, validation is critical. A missing or malformed configuration value should fail fast, not cause a cryptic error hours later.

For environment variables, define a schema that enforces types and requirements:

{
  "DATABASE_URL": {
    "type": "url",
    "required": true
  },
  "PORT": {
    "type": "int",
    "default": 3000,
    "validate": { "min": 1024, "max": 65535 }
  }
}

Run validation at startup, in CI, and before deploys. Catching a typo in your config before it reaches production is worth the 30 seconds of setup time.

Making the Decision

Here is a quick decision tree:

  1. Is it a secret? Use environment variables
  2. Does it change per environment? Use environment variables
  3. Is it complex or nested? Use a config file
  4. Is it managed by non-developers? Consider a config file
  5. Is it simple and flat? Either works; environment variables are often simpler

There is no universally right answer. The best choice depends on your team, your deployment process, and the nature of your application. What matters most is being consistent and validating everything.

Further Reading

Share this article

Z

ZORL Team

Building developer tools that make configuration easier. Creators of zorath-env.

Previous
How to Set Up .env Validation in Your CI/CD Pipeline
Next
5 Environment Variable Mistakes That Break Production

Related Articles

Never miss config bugs again

Use zorath-env to validate your environment variables before they cause production issues.