Drupal Configuration Management: Environment-Specific Overrides with Config Split and settings.php
Use Drupal Configuration Split with settings.php to layer environment-specific modules on a shared base config. Worked example: dev split for devel and stage_file_proxy, activation per environment, avoiding UUID mismatches and secrets in YAML.
16 Apr 2026, 14:28 UTC

The Problem: Shared Config, Different Environments
Drupal's Configuration Management Initiative (CMI) exports all site configuration—content types, views, fields, modules—as YAML files into a single sync directory. This works well until you need different modules or settings per environment: devel and stage_file_proxy on local and staging, but never on production; debug logging enabled in dev, suppressed in prod. Committing everything to one sync directory forces you to either enable dev modules everywhere or maintain divergent branches that drift.
The reliable pattern: keep a single base configuration in Git, then layer environment-specific splits via the Configuration Split module (core since Drupal 10.1) and activate them through settings.php. This gives you version-controlled, repeatable deployments without polluting shared config.
How It Works: Base Config + Active Splits
Configuration Split defines named splits (e.g., dev, stage, prod). Each split specifies a filter—module list, config prefix, or regex—and a separate storage directory. When a split is active on an environment, its configuration is exported to and imported from that directory in addition to the base sync directory. Activation is controlled entirely in settings.php (or an included settings.local.php), so the same codebase behaves differently per environment without code changes.
Typical deployment sequence on target environment:
drush cim -y— imports base configuration fromconfig/syncdrush config-split:import -y— imports configuration for all active splitsdrush updb -y— runs database updatesdrush cr— clears caches
Because splits are additive, the base config remains clean. Environment-specific modules and settings live only in their split directories and are applied only where the split is active.
Worked Example: Dev Split for Devel and Stage File Proxy
1. Enable Configuration Split
# Run on any environment where you'll create splits
# Requires drush and an enabled Drupal site
drush en config_split -y
Run this once per site (or include in your install profile). The module provides the UI at /admin/config/development/configuration/split and the Drush commands config-split:export, config-split:import, config-split:list.
2. Create the Split Definition (Committed to Git)
Create the split via the UI or by placing a YAML file in your base sync directory (config/sync). The split definition itself is configuration, so it must be exported and committed.
# File: config/split/dev.yml
uuid: 1a2b3c4d-5e6f-7g8h-9i0j-klmnopqrstuv
langcode: en
status: true
dependencies:
module:
- config_split
id: dev
label: 'Development'
folder: '../config/sync-dev'
module:
- devel
- stage_file_proxy
- webprofiler
theme: { }
blacklist: { }
graylist: { }
graylist_dependents: true
graylist_skip_equal: true
weight: 0
Key fields:
- id: machine name (
dev) used insettings.phpactivation. - folder: relative path from the Drupal root to the split's storage directory. Use
../config/sync-devto keep it outside the docroot alongsideconfig/sync. - module: explicit list of modules this split manages. When the split is active, these modules are enabled; when inactive, they are disabled. Prefer explicit module lists over regex for clarity.
After creating the split, export configuration so the definition lands in config/sync:
drush cex -y
# Commit config/split/dev.yml to Git
3. Activate the Split Per Environment
In settings.local.php (included from settings.php and excluded from Git via .gitignore), set the split status for the current environment:
<?php
// settings.local.php on dev/staging
$settings['config_split_config_split.dev']['status'] = TRUE;
// On production, either omit the line or set FALSE
// $settings['config_split_config_split.dev']['status'] = FALSE;
The variable name follows the pattern config_split_config_split.<split_id>. Because settings.local.php is environment-specific, the same codebase activates the dev split only where the file sets it to TRUE.
4. Export Dev-Only Configuration
On a development environment where the split is active, export the split's configuration to its dedicated directory:
# Run on dev/staging where split is active
drush cex -y --destination=../config/sync-dev
This writes YAML for the modules listed in the split (devel, stage_file_proxy, webprofiler and their configuration) into config/sync-dev. Commit that directory to Git. On production, where the split is inactive, drush cex (without --destination) exports only base config; the dev modules remain absent.
5. Verify Split Status
drush config-split:list
Expected output shows the split and its active status:
+------+------------+--------+--------+
| Name | Machine ID | Status | Path |
+------+------------+--------+--------+
| Dev | dev | Active | ../config/sync-dev |
+------+------------+--------+--------+
If Status shows Inactive on an environment where you expect it active, check settings.local.php inclusion and variable spelling.
Handling Secrets and Per-Environment Values
Config splits manage which configuration objects exist (modules, views, content types). They cannot override individual values inside a config object—e.g., different API keys per environment. For that, use $config overrides in settings.php:
<?php
// settings.local.php on production
$config['system.mail']['interface']['default'] = 'smtp';
$config['system.mail']['interface']['smtp']['host'] = 'smtp.sendgrid.net';
$config['system.mail']['interface']['smtp']['port'] = 587;
$config['system.mail']['interface']['smtp']['username'] = 'apikey';
$config['system.mail']['interface']['smtp']['password'] = getenv('SENDGRID_API_KEY');
These overrides take precedence over both base config and split config at runtime. They are not exported to YAML, so secrets stay out of Git. Use environment variables (via getenv() or $_ENV) for credentials.
Limits and Gotchas
Config Entities Only
Splits handle configuration entities (modules, views, fields, block placement). They do not manage content entities (nodes, users, taxonomy terms) or file assets (uploaded images, generated CSS/JS). Content and files require separate migration or sync strategies.
Chicken-and-Egg on First Setup
The split definition itself lives in base config. You must create the split on a site, export config (drush cex), and commit config/split/dev.yml before other environments can use it. On a fresh clone, import base config first (drush cim), then the split becomes available for subsequent config-split:import.
Split Deactivation Does Not Delete Config
When a split is inactive, its configuration is not removed from the active configuration store. If you enable a split, import, then disable it, the modules and settings remain enabled. Cleanup requires manual drush config:delete or a dedicated uninstall split. Plan split lifecycles accordingly.
UUID Mismatch Blocks Import
Every Drupal site has a system.site UUID. Config import fails if the target site's UUID differs from the source. Before first import on a new environment, sync the UUID:
# Get UUID from source site
drush config:get system.site uuid
# Set on target site (run before drush cim)
drush config:set system.site uuid ""
Schema Validation Applies
Split configuration undergoes the same schema validation as base config. Invalid YAML (missing required keys, type mismatches) halts import. Test imports on a clone before production.
Database Updates Run After Config Import
hook_update_N() implementations that modify configuration run after drush cim and config-split:import. If an update alters config managed by a split, the split's next import may overwrite the update's changes. Coordinate update logic with split ownership, or use drush config:import --partial with care.
Common Mistakes Checklist
- Split definition not committed: Creating a split via UI without
drush cexand Git commit means other environments never see the split. - Wrong folder path:
folderin split YAML is relative to Drupal root.../config/sync-devworks when docroot isweb/; adjust for your layout. - Secrets in split YAML: API keys, passwords, or tokens placed in split config files end up in Git. Use
$configoverrides insettings.phpinstead. - Forgetting
drush crafter split changes: Split activation state is cached. Clear caches after togglingsettings.phpvariables. - Overcomplicated filters: Regex filters (
config_prefix: 'system.') are brittle. Explicit module lists are readable and predictable. - Too many splits: More than 3–4 splits (dev, stage, prod, maybe test) increases cognitive load. Consolidate where possible.
Verification Steps Before Production Deploy
- On a clean database clone, run the full import sequence:
drush cim -y && drush config-split:import -y && drush updb -y && drush cr - Confirm environment-specific modules are enabled/disabled as expected:
drush pml --status=enabled --type=module | grep -E 'devel|stage_file_proxy|webprofiler' # Should appear on dev/staging, absent on prod - Verify no secrets appear in committed YAML:
grep -r 'password\|api_key\|secret' config/sync config/sync-dev --include="*.yml" # Should return nothing - Run
drush config-split:liston each environment to confirm active splits match intent.
Summary
Use a single base configuration directory (config/sync) for everything shared across environments. Define one Configuration Split per environment variant (dev, stage) with an explicit module list and a dedicated storage directory (config/sync-dev). Activate splits via $settings['config_split_config_split.<id>']['status'] = TRUE in environment-specific settings.local.php. Export split config with drush cex --destination=../config/sync-dev on environments where the split is active. Override individual values (API keys, SMTP credentials) with $config in settings.php, never in YAML. This pattern keeps deployments deterministic, secrets out of Git, and environment differences explicit and version-controlled.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.