Skip to content
Web Development

How to Continue Dev Config Across Environments Without Overwrite

Bitkadan4 min read

Keeping your development configuration consistent across staging, testing, and production can be a nightmare, especially when automated pipelines overwrite settings. This guide shows how to continue dev config safely across environments without losing custom tweaks.

The Overwrite Dilemma: Why Dev Config Gets Erased in Multi‑Env Deploys

Common pitfalls in CI/CD pipelines

When you first wire up a CI/CD pipeline you tend to think of it as a glorified copy‑and‑paste operation: fetch the source, run the build, push the artifact, and the job is done. The pipeline quickly becomes the single authority on what ends up on the target environment, and that authority can silently discard the little‑but‑crucial tweaks you made while developing locally.

  • Hard‑coded configuration steps – A step that runs cp config/default.yml config/production.yml before each deploy. The command is harmless when you only have a default.yml, but as soon as a developer adds a dev.local.yml with feature flags, the copy overwrites it every time.
  • One‑size‑fits‑all build artefacts – Building a Docker image with --build-arg ENV=prod forces the Dockerfile to copy the same application.properties into every container, regardless of the environment it will run in. The container that lands in a test cluster now carries production defaults.
  • Configuration drift in secret stores – Many teams store environment variables in a vault and then inject them with a script like envsubst < config/template.env > .env. If the template is missing a key that was added locally, the resulting .env file simply drops that key, leaving the service in a half‑configured state.
  • Unconditional clean‑up phases – A “clean workspace” step that runs git clean -fdx before the build is great for reproducibility, but it also removes any untracked files that a developer might have placed in the repository for local testing. Those files never make it into the artifact.
  • Skipping merge‑conflict markers – When a merge conflict is resolved automatically by the CI server (e.g., git merge --strategy=ours), the resulting file can carry only the base version, wiping out any environment‑specific overrides that were added in the feature branch.

Notice the pattern: each of those steps assumes a static, immutable configuration. The pipeline treats config.yml as a monolith and never asks “what if the developer already has a richer file?”. The result is an environment that looks exactly like the one defined in the pipeline, not the one tweaked during development.

pipeline {
    agent any
    stages {
        stage('Prepare') {
            steps {
                // Pull in the base config that ships with the repo
                sh 'cp src/main/resources/application.yml target/'
                // Overwrite with a CI‑generated version
                sh 'envsubst < ci/config/template.yml > target/application.yml'
            }
        }
        // other stages…
    }
}

If a developer added feature.toggle=true to src/main/resources/application.yml for a local experiment, the cp command copies it into target/ only to have it instantly replaced by the templated version. The developer’s intent never reaches the next environment, and the next time the pipeline runs the missing flag isn’t even on the radar.

These pitfalls are insidious because the pipeline doesn’t throw an error. It simply produces a clean, predictable artifact—just the way the pipeline designer intended. The cost is paid later when a QA engineer encounters a missing feature flag, or when a performance test fails because a logging level was forced to INFO instead of the developer’s DEBUG setting.

How default config files dominate custom settings

  1. Default config bundled with the application (application.yml, settings.py, .env).
  2. Environment‑specific file (application-prod.yml, settings/production.rb, .env.production).
  3. System environment variables.

When a CI/CD step unconditionally rewrites the default file, it collapses the entire hierarchy into a single layer. The custom file you added for development is never consulted because the framework still believes it is dealing with the “default” layer.

# src/main/resources/application.yml
spring:
  datasource:
    url: jdbc:h2:mem:testdb
    username: sa
    password:
# src/main/resources/application-dev.yml
spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/devdb
    username: dev_user
    password: secret
# CI step (pseudo‑code)
cat > src/main/resources/application.yml <<EOF
spring:
  datasource:
    url: jdbc:postgresql://prod-db.example.com:5432/proddb
    username: prod_user
    password: ${prod_db_password}
EOF

When the same artifact lands in the staging environment, the default file already contains the production URL. Even if the dev profile is still active, Spring Boot will load application-dev.yml on top of the new defaults, but the production URL wins for any property that isn’t overridden. If application-dev.yml only defines url and leaves username blank, the default’s username (the production one) remains in effect.

#Continue #Config #Web Development