Set up env-specific config files

Set up environment-specific configuration files — CI/CD foundations tutorial. Learn why, how, and when to use them, with a hands-on exercise and troubleshooting tips.

Focus: set up environment-specific configuration files

Sponsored

You’ve just pushed a feature that passes all tests locally, but when it hits the staging server, the database URL points to production, and the API keys are wrong. Your CI/CD pipeline happily deployed it because it used the same configuration everywhere. This is the classic configuration chaos problem — and it’s exactly what environment-specific configuration files solve. By the end of this lesson, you’ll know how to set up separate config files for dev, staging, and prod, and wire them into your pipeline so the right values land in the right environment — every single time.

The problem this lesson solves

Configuration is the silent killer of reliable deployments. When your application reads a single configuration file, every environment shares the same secrets, endpoints, and feature flags. That means:

  • Developers test against production-like data — a recipe for disaster.
  • Staging doesn’t accurately mirror production — so bugs slip through.
  • A misconfigured variable can take down prod — with no warning.

Without environment-specific config files, your CI/CD pipeline becomes a liability. It deploys the same artifact to every environment, assuming the config inside is universal. It isn’t. Databases differ, third-party APIs have separate sandboxes, and feature flags should be toggled per environment.

The core problem: configuration is not code. It changes per deployment target, and you need a deliberate mechanism to separate it from your application binary. This lesson gives you that mechanism — a proven pattern called environment-specific configuration files.

Core concept / mental model

Think of your application as a universal adapter that plugs into different power sockets. The adapter itself is the same everywhere — your code and compiled artifact. But the plug — the configuration — must match the socket. Dev, staging, and prod each have their own socket shape, defined by their infrastructure, credentials, and networking rules.

Environment-specific configuration files are a set of files, one per environment, that hold the values that differ. They follow a naming convention like config.dev.yaml, config.staging.yaml, and config.prod.yaml. At runtime — or at build time — your application loads the file that matches the current environment, usually via an environment variable like APP_ENV or DEPLOY_ENV.

Here’s the mental model in words:

App code (static) --> + config.<env>.yaml (dynamic) --> working service

This separation gives you four key benefits:

  1. Isolation — prod secrets never appear in dev logs.
  2. Repeatability — the same artifact runs identically across environments.
  3. Auditability — you can see exactly what config was active during a deploy.
  4. Safety — a typo in a dev config can’t break production.

Pro tip: Always store secrets outside version control. Environment-specific files are for non-secret values (URLs, domain names, log levels). Use a secret manager (like AWS Secrets Manager or GitHub Actions secrets) for tokens and passwords.

How it works step by step

Setting up environment-specific config files follows a predictable sequence. Let’s walk through it logically — cause → effect at every step.

  1. Define your environments — Start by listing every deployment target. Typically: dev, staging, prod. You might also add test for automated tests.

  2. Create a base config template — This is a file with placeholder values and comments describing each variable. It serves as documentation and a starting point.

  3. Generate per-environment files — For each environment, create a copy with concrete values. Name them clearly, e.g., config.dev.yaml, config.prod.yaml.

  4. Add an environment selector — Your application must know which file to load. The standard pattern: read an environment variable (e.g., APP_ENV) and construct the filename from it.

  5. Wire it into CI/CD — In your pipeline, set the environment variable for each deploy job. GitHub Actions makes this easy with environment blocks and encrypted secrets.

  6. Test the switch — Run the application locally with different APP_ENV values and confirm the correct config is loaded.

Once this is in place, your pipeline knows exactly which config to apply at each stage — no more last-minute edits or manual overrides.

Key insight: The config file must be either baked into your artifact at build time or mounted at runtime. Don’t fetch it via a network call during deploy — that adds a failure point.

Hands-on walkthrough

Let’s build a minimal example in Python that reads environment-specific configuration files. We’ll create a simple app that displays the database host and log level based on the current environment.

1. Create the config files

Create a directory called config/ with three files:

# config/config.dev.yaml
app_name: demo-app
log_level: DEBUG
database:
  host: localhost
  port: 5432
# config/config.staging.yaml
app_name: demo-app
log_level: INFO
database:
  host: staging-db.example.com
  port: 5432
# config/config.prod.yaml
app_name: demo-app
log_level: WARNING
database:
  host: prod-db.internal
  port: 5432

2. Write the loader

Now write a Python script that picks the right file based on the APP_ENV environment variable (default to dev):

import os
import yaml

def load_config(env=None):
    """Load config for the current environment.

    Args:
        env: Override APP_ENV, mainly for tests.
    """
    if env is None:
        env = os.getenv('APP_ENV', 'dev')

    config_path = f"config/config.{env}.yaml"

    if not os.path.exists(config_path):
        raise FileNotFoundError(f"No config for environment '{env}' at {config_path}")

    with open(config_path, 'r') as f:
        return yaml.safe_load(f)

if __name__ == '__main__':
    config = load_config()
    print(f"Loaded config for APP_ENV={os.getenv('APP_ENV', 'dev')}")
    print(f"Log level: {config['log_level']}")
    print(f"DB host: {config['database']['host']}")

Run it with different environments:

# Defaults to dev
python app.py
# Output: Loaded config for APP_ENV=dev
# Log level: DEBUG
# DB host: localhost

# Switch to staging
APP_ENV=staging python app.py
# Output: Log level: INFO
# DB host: staging-db.example.com

# Switch to prod
APP_ENV=prod python app.py
# Output: Log level: WARNING
# DB host: prod-db.internal

3. Wire it into CI/CD (GitHub Actions example)

Now let’s see how this fits into a pipeline. Here’s a GitHub Actions workflow that deploys to staging and production, injecting the APP_ENV variable and mounting the config as a secret-friendly artifact:

name: Deploy with environment config
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        environment: [staging, production]
    steps:
      - uses: actions/checkout@v4
      - name: Set APP_ENV
        run: echo "APP_ENV=${{ matrix.environment }}" >> $GITHUB_ENV
      - name: Deploy
        run: |
          echo "Deploying to $APP_ENV"
          # Example: run ansible or your deploy script
          # The app will read config.$APP_ENV.yaml
          ./deploy.sh $APP_ENV

Note: The config files are committed to the repo (non-secret values only). For secrets, use GitHub Actions environment-specific secrets, which are injected as environment variables at runtime.

Compare options / when to choose what

Environment-specific config files aren’t the only way, and sometimes they aren’t the best. Here’s a comparison:

Approach Pros Cons Best for
Environment-specific files (per-env YAML/JSON) Simple, human-readable, easy to audit Files can drift if not maintained; secrets must be excluded Most projects; great when you have 2–5 environments
Environment variables only No config files to manage; built into 12-factor apps Hard to audit; no structure for complex config Small apps or serverless functions
Central config service (e.g., Consul, Vault) Centralized; dynamic updates; secret management Extra infrastructure; more moving parts Large microservices platforms
Build-time templating (e.g., envsubst) Values injected at build time Requires extra tooling; leaks into image if not careful Container images that are built per environment

When to choose what:

  • Start small — Use environment-specific files as soon as you have more than one environment. They’re the lowest-friction option.
  • Move to env vars — For 12-factor cloud-native apps, env vars are the standard. But they can get messy when you have 50+ variables.
  • Adopt a config service — Only when you need dynamic updates or SECRETS management at scale. It’s overkill for a startup.

Troubleshooting & edge cases

Even with this pattern, things can go sideways. Here are the most common issues and how to fix them:

1. “Config file not found” error

Symptom: Your app crashes with FileNotFoundError: No config for environment 'qa'. Cause: You set APP_ENV=qa but only created dev, staging, and prod files. Fix: Always create config files for every environment you reference. Or add a fallback to dev in your loader.

# Fallback to dev if env file missing
config_path = f"config/config.{env}.yaml"
if not os.path.exists(config_path):
    env = 'dev'
    config_path = f"config/config.{env}.yaml"

2. Staging config accidentally uses prod database

Symptom: Staging tests are modifying real user data. Cause: Someone copied config.prod.yaml to config.staging.yaml and forgot to change the DB host. Fix: Enforce a key check in CI. For example, have a test that fails if the staging config points to a host with prod in its name.

# In your test suite
def test_staging_config_does_not_use_prod_db():
    staging_config = load_config('staging')
    prod_config = load_config('prod')
    assert staging_config['database']['host'] != prod_config['database']['host']

3. Config values are out of date

Symptom: Dev works, staging fails with outdated API keys. Cause: Multiple config files are maintained manually, and someone changed the API key in one place only. Fix: Use a base template and a script to merge overrides, or use a diff tool in CI to ensure parity.

4. Secrets accidentally committed

Symptom: You see a password in the repo history. Cause: Someone added a secret to config.prod.yaml. Fix: Add .gitignore entries for any config file that may contain secrets, or use a pre-commit hook to scan for patterns.

Pro tip: Use a tool like gitleaks in CI to scan for secrets before every push.

What you learned & what's next

You now know how to set up environment-specific configuration files — a foundational skill for any CI/CD engineer. Let’s recap what you accomplished:

  • Explained the core idea — Config files separate that change per environment from static application code.
  • Completed a hands-on exercise — You created config files for dev, staging, and prod, wrote a Python loader, and wired it into a GitHub Actions flow.
  • Connected to the next lesson — In the next lesson (Step 38), you’ll tackle secrets management in CI/CD. You’ll learn how to inject API keys and passwords safely into your builds without exposing them in config files or logs. The pattern you just built will be the perfect foundation — you’ll replace hardcoded secrets with references to environment variables and secret managers.

You’re one step closer to mastering configuration management. Keep going!

Remember: Configuration is a deployment concern, not a code concern. Keep it separate, keep it environment-aware, and your pipeline will never surprise you again.

Practice recap

Create a small project with config files for dev, staging, and prod. Write a loader that selects based on an APP_ENV variable, then run it with each environment to confirm the correct values appear. Next, add a unit test that asserts staging does not use production database hosts — this will lock in the safety you just learned.

Common mistakes

  • Hardcoding the environment-specific values directly into the application code, which defeats the purpose of separation.
  • Forgetting to set the APP_ENV (or equivalent) variable in the deployment job, causing the app to default to dev config in production.
  • Committing secrets (passwords, API keys) into config files that are tracked by version control.
  • Creating config files for only a few environments, then referencing an undefined one in the pipeline, leading to runtime crashes.

Variations

  1. Use environment variables directly instead of separate files — simpler but less structured.
  2. Leverage a central config service like HashiCorp Vault or AWS AppConfig for dynamic, encrypted configuration.
  3. Adopt a build-time templating tool (e.g., envsubst, Confd) to inject values into config files during the CI process.

Real-world use cases

  • Staging environment for a web app uses a separate database and test API keys to avoid polluting production data.
  • Mobile app backend deploys different feature flags to a beta channel vs. the public release.
  • Multi-region infrastructure uses different endpoint URLs and log levels in each region's config to meet compliance requirements.

Key takeaways

  • Environment-specific configuration files separate environment-dependent values from static code, promoting safety and reproducibility across dev, staging, and prod.
  • The standard pattern is to name files like config.<env>.yaml and load them based on an environment variable (e.g., APP_ENV).
  • Always keep secrets out of these config files — use environment variables or a secret manager instead.
  • Wire the environment selector into your CI/CD pipeline by setting the APP_ENV variable in each deployment job.
  • Choose between per-env files, env vars, or a central config service based on the size and complexity of your project.
  • Test your config loading logic with unit tests to catch mistakes like a staging config pointing to a prod database.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.