Comprehensive guide for Drupal configuration management including imports, exports, config splits, and environment syncing.
Remote CLI — host-neutral
Examples below use Pantheon's Terminus (terminus drush <site>.<env> -- <cmd>). The commands are identical on any host — substitute your platform's remote-drush form. If your platform provides Drush site aliases, the generic drush @<alias> <cmd> works everywhere.
| Task | Acquia (acli) |
Pantheon (Terminus) | Platform.sh / Upsun | Lagoon (amazee.io) | Generic (Drush aliases) |
|---|---|---|---|---|---|
| Remote drush | acli remote:drush -- <cmd> |
terminus drush <site>.<env> -- <cmd> |
platform drush -e <env> -- <cmd> (Upsun: upsun drush …) |
lagoon ssh -p <project> -e <env> -C "drush <cmd>" |
drush @<alias> <cmd> |
| Get/inspect config | acli remote:drush -- config:get <name> |
terminus drush <site>.<env> -- config:get <name> |
platform drush -e <env> -- config:get <name> |
lagoon ssh -p <project> -e <env> -C "drush config:get <name>" |
drush @<alias> config:get <name> |
The auto-confirm warning below applies to every host — append
--notocim/config:importto preview instead of apply. Drush defaults to--yeswhen invoked non-interactively (which all remote-CLI wrappers do).
Problem: Avoid Accidental Config Imports
CRITICAL: Terminus drush commands default to --yes unless explicitly told --no. This means commands like config:import or cim will AUTO-CONFIRM and import configuration even when you only want to inspect differences.
Dangerous vs Safe Patterns
❌ DANGEROUS - Auto-imports without confirmation:
terminus drush {site}.{env} -- cim --diff # DON'T DO THIS!
✅ SAFE - Shows diff without importing:
terminus drush {site}.{env} -- cim --no --diff
✅ SAFEST - Read-only commands:
terminus drush {site}.{env} -- config:get config.name
terminus drush {site}.{env} -- config:status
Table of Contents
- Preferred Prod Config Merge Workflow
- Configuration Import & Export Basics
- Config Splits Overview
- Complete vs Partial Splits
- Config Split Commands
- Safe Inspection Workflow
- Syncing Config from Upstream Environments
Configuration Import & Export Basics
Default to one named object at a time. A blanket
cexwrites every active-vs-sync difference to disk and a blanketcimimports (and deletes) every difference, so in a tree with drift you did not create they sweep that drift into your commit or your database. An agent changes config withconfig:get/config:set/config:delete/ a partial import of one file, and leaves the full import to the deploy (the tail is in thedrupal-deploy-safetyskill). Run a blanket export only from a clean, committed tree when a full export is the point (a prod config merge, a split change), and reviewgit status config/afterwards. Full rules: surgical-config.md.
Exporting Configuration
Export ALL configuration (from active config to YAML files; clean tree only, see above):
# Local
ddev drush config:export
ddev drush cex
# Remote
terminus drush {site}.{env} -- config:export
Export a SINGLE config object:
# Get config and save to file
ddev drush config:get config.name --format=yaml > config/default/config.name.yml
# Example: Export a specific view
ddev drush config:get views.view.content --format=yaml > config/default/views.view.content.yml
Importing Configuration
Import ALL configuration (from YAML files to active config; this is what a deploy does, so preview locally and leave remote imports to the deploy tail):
# Local
ddev drush config:import
ddev drush cim
# Remote (DANGEROUS - auto-confirms with terminus!)
terminus drush {site}.{env} -- config:import --no # Use --no to preview only
Import a SINGLE config object (partial import from a directory holding only that file; under DDEV the directory must be inside the project, see surgical-config.md):
mkdir -p .config-one && cp config/default/config.name.yml .config-one/
ddev drush config:import --partial --source=/var/www/html/.config-one -y
rm -rf .config-one
# Or use config:set for specific values
ddev drush config:set config.name key.subkey value
Best Practice: Always preview changes first:
ddev drush config:import --no --diff # Show what would change
ddev drush cim --no --diff # Alias
Config Split Commands
CRITICAL: Active vs Exported Configuration
⚠️ IMPORTANT: When updating config split definitions, changes must be in ACTIVE configuration (database), not just exported files!
Workflow:
- Edit
config/default/config_split.config_split.{name}.yml - Import to make active: a single-file partial import (see above) OR use PHP (see below)
- Export:
ddev drush cex. This is the one routine case that needs a full export, because config_split writes the split directory and its patch files duringcex. Start from a clean tree and revert anything ingit status config/you did not intend.
Quick method - Set active config via PHP:
ddev drush php:eval "\$config = \Drupal::configFactory()->getEditable('config_split.config_split.local'); \$config->set('partial_list', ['config.name']); \$config->save();"
See examples.md for detailed workflow.
Export Config with Splits
Export ALL config including active splits:
ddev drush config:export
This exports:
- Base config to
config/default/ - Active split config to
config/{split-name}/
Export a specific split:
ddev drush config-split:export {split-name}
ddev drush csex {split-name}
Import Config with Splits
Import ALL config including active splits:
ddev drush config:import
ddev drush cim
This imports:
- Base config from
config/default/ - Active split config from
config/{split-name}/
Import a specific split:
ddev drush config-split:import {split-name}
ddev drush csim {split-name}
Import only base config (ignore splits): Drush 13's config:import has no option for this (--skip-modules was Drush 8). drush config-split:deactivate <split> itself imports the config without the split, writing status: false into active config; a later full config:import re-activates the split if its synced YAML has status: true. To keep it off across imports, set a status override: drush config-split:status-override <split> inactive (stored in state; values active|inactive|default, alias csso), or in settings.php:
ddev drush config-split:deactivate {split-name} # one-off
ddev drush config-split:status-override {split-name} inactive # sticks across imports
$config['config_split.config_split.{split-name}']['status'] = FALSE; // settings.php
A --partial import skips config transformation, so no split is applied to it at all.
Activate/Deactivate Splits
Activate a split:
ddev drush config-split:activate {split-name}
Deactivate a split:
ddev drush config-split:deactivate {split-name}
Check Split Status
List all splits and their status:
ddev drush config-split:status
ddev drush css
Example output:
Split Active Configuration directory
local Yes ../config/local
dev No ../config/dev
test No ../config/test
Safe Inspection Workflow
Use config:get and config:status for read-only inspection, or use --no flag with cim/cex to prevent auto-confirmation.
Get Config Values
# Get full config object
terminus drush {site}.{env} -- config:get config.name
# Get as YAML
terminus drush {site}.{env} -- config:get config.name --format=yaml
# Extract specific values
terminus drush {site}.{env} -- config:get config.name 2>&1 | grep "setting_name"
Compare Local vs Remote
# View diffs without importing (SAFE with --no)
terminus drush {site}.{env} -- cim --no --diff
# Get remote and compare manually
terminus drush {site}.{env} -- config:get config.name --format=yaml > /tmp/remote.yml
diff -u config/default/config.name.yml /tmp/remote.yml
CRITICAL: Always use --no flag with terminus! Without it, commands auto-confirm.
Apply Changes
Preferred: never hand-author the YAML. Either make the change in the site (UI, config:set for simple config, or the entity API for config entities) and export that one object, or, to take an environment's value, write that environment's config:get output for the one object (see surgical-config.md):
ddev drush config:get config.name --format=yaml > config/default/config.name.yml
# or: terminus drush {site}.{env} -- config:get config.name --format=yaml > config/default/config.name.yml
git diff config/default/config.name.yml
git add config/default/config.name.yml
git commit -m "Update config from {env}"
Syncing Config from Upstream Environments
Quick Methods
Single config object:
terminus drush {site}.{env} -- config:get config.name --format=yaml > config/default/config.name.yml
git add config/default/config.name.yml && git commit -m "Update from {env}"
# Then apply that one file locally with a single-file partial import (see above)
Full config sync via rsync:
Caution: a remote
cexwrites every drifted config object on that environment, and a localcimimports (and deletes) everything that differs. Never run a blanket export/import against a shared or remote environment as a casual step — prefer the single-config method above, and review the full diff before anything is imported. See surgical-config.md.
terminus drush {site}.{env} -- cex
terminus rsync {site}.{env}:code/config/default /tmp/remote
diff -r config/default /tmp/remote # Review
cp /tmp/remote/*.yml config/default/
git add config/default/ && git commit -m "Sync from {env}"
ddev drush cim
Via database pull (DDEV + Pantheon). This is a deliberate full export: commit your own work first and restore any of your files the export deletes, as in prod-config-merge.md:
ddev pull pantheon --environment={env} # Warning: Overwrites local DB!
ddev drush cex
git diff config/ && git add config/ && git commit -m "Config from {env}"
See examples.md for detailed workflows.
Best practices: Review diffs, commit separately, test locally, document source, avoid syncing environment-specific config.
Deep Dive References
For comprehensive technical documentation, see:
- config-split-deep-dive.md - Complete technical reference on Config Split 2.0, patch files, export/import process, and dependency handling
- surgical-config.md - One-config-at-a-time export/set/delete for agents, the
core.extension.ymlexception, raw config writes that dropdependencies, baked (PHP-computed) config, and verifying imports withconfig:status - examples.md - Practical examples and workflows
References
| File | Read it when |
|---|---|
| references/surgical-config.md | Before any config change an agent makes: exporting, setting or deleting ONE named config object, the core.extension.yml exception, raw config writes that drop dependencies, baked (PHP-computed) config, verifying an import with config:status, shipping a config-only change |
| references/prod-config-merge.md | Merging production config changes into a branch that carries local feature config (pull prod DB, export, restore your deleted/overwritten files) |
| references/config-splits.md | Deciding whether to use a split, or choosing between a Complete and a Partial split |
| references/config-split-deep-dive.md | You need the Config Split 2.0 internals: patch files, file naming, the export/import process, dependency handling, and worked local-vs-remote Solr examples (including a complete-split server with auto-generated index patches) |
| references/examples.md | You want worked examples: syncing one config from dev, syncing Search API config, comparing environments, updating split definitions |
Config Status Check
Check what config would be imported (read-only):
# Local environment
ddev drush config:status
# Remote environment
terminus drush {site}.{env} -- config:status
Best Practices
- Always inspect before importing - Use
config:getand--no --diffflags - Change config in the site, export one object - UI /
config:set/ entity API, thenconfig:get --format=yaml; hand-editing exported YAML is the exception (it skips dependency calculation), and when you do it, import that one file and confirm withconfig:status - One config type per commit - Separate concerns for clean history
- Clear commit messages - Reference source environment
- Clean up temp files - Remove temporary YAML files
- Verify before committing - Always review
git diffoutput - Test locally first - Import and test before deploying
- Use config splits - Keep environment-specific config separate
Troubleshooting
Config files deleted from working directory
If files are marked as deleted in git status:
git checkout HEAD -- config/default/*.yml
This can happen if a drush command runs unexpectedly.
Split not activating
Check split status:
ddev drush config-split:status
Manually activate:
ddev drush config-split:activate {split-name}
# Save the activation state: export only the split definition
ddev drush config:get config_split.config_split.{split-name} --format=yaml > config/default/config_split.config_split.{split-name}.yml
Config deleted from config/default on export
COMMON ISSUE: Config (like search_api.server.my_search_server) gets removed from config/default/ when you run drush cex.
Root cause (99% of cases): Config is in complete_list instead of partial_list!
Complete split = Config is REMOVED from config/default/ and moved to split directory entirely
Partial split = Config STAYS in config/default/, only differences are patched
Diagnosis:
# Check if config is in complete_list (will be deleted from config/default)
grep -A10 "complete_list:" config/default/config_split.config_split.local.yml
# Check if config is in partial_list (will stay in config/default)
grep -A10 "partial_list:" config/default/config_split.config_split.local.yml
Solution: Move from complete_list to partial_list
# Edit the split definition
# Move: search_api.server.my_search_server
# FROM: complete_list
# TO: partial_list
ddev drush cex # Re-export
# Check that config/default/search_api.server.my_search_server.yml exists
# Check that config/local/config_split.patch.search_api.server.my_search_server.yml exists
See config-split-deep-dive.md for complete technical explanation.
Config won't import
Common issues:
- Dependencies missing: Install required modules first
- Site UUID mismatch ("Site UUID in source storage does not match the target storage"): the database was installed separately from the exported config. Install from config with
drush site:install --existing-config, or setsystem.site:uuidto the exported value withdrush config:set system.site uuid <uuid> - Config entity UUID differs: not an error, but the import deletes and recreates that entity (for a field storage, its data goes with it); see the
drupal-config-reconcileskill
Drush 13's config:import has no --skip-config option. To keep one object out of an import, use the config_ignore module or a single-file partial import (above).
Related Commands
Read-only: config:get, config:status
Exports: config:export (alias: cex)
Imports: config:import (alias: cim) - Use with --no --diff to preview
Splits: config-split:status, csex, csim, config-split:activate