Drupal's configuration management system is one of its most powerful features for maintaining consistency across development, staging, and production environments. When it works well, deploying a change is as simple as running drush cim -y. When it goes wrong, you are staring at UUID mismatches and hash errors. This article covers the full workflow, from basic export/import to environment-specific config splits and safe deployment practices.
The Core Workflow
Drupal stores configuration in the database at runtime, but for deployment purposes it is exported to YAML files in a sync directory. The basic cycle is:
- Make configuration changes on your development site (via the UI or programmatically)
- Export the configuration to YAML files:
drush config:export - Commit the YAML files to version control
- On the target environment (staging, production), pull the code and run
drush config:import
# Export current configuration to the sync directory
drush config:export
# Shorthand:
drush cex
# Import configuration from the sync directory to the database
drush config:import
# Shorthand:
drush cim
# Preview what will change before importing
drush config:import --preview
# Or use:
drush config:status
Configuring the Sync Directory
Drupal 10 and 11: $settings['config_sync_directory']
In Drupal 10+, the sync directory is configured in settings.php via the $settings array:
// settings.php (Drupal 10/11)
$settings['config_sync_directory'] = '../config/sync';
The path is relative to the Drupal web root (the web/ directory). Placing the sync directory outside the web root (../config/sync) ensures that config YAML files are not publicly accessible via HTTP.
Drupal 9 Style (now deprecated): $config_directories
Older Drupal 9 sites and documentation used a different variable:
// settings.php (Drupal 9 — deprecated style, still works but avoid for new projects)
$config_directories['sync'] = '../config/sync';
This still works in Drupal 10 for backwards compatibility, but all new projects should use $settings['config_sync_directory'].
Typical Project Structure
myproject/
composer.json
composer.lock
config/
sync/ ← YAML files exported by drush cex
core.extension.yml
system.site.yml
node.type.article.yml
... (hundreds of files)
web/ ← Drupal web root
core/
modules/
sites/
default/
settings.php
The Config Split Module
The Config Split module solves a common problem: some configuration should only exist in certain environments. Dev modules like devel, kint, and webprofiler should be enabled on development but never on production. Stage file proxy settings differ per environment.
composer require drupal/config_split
drush en config_split -y
After installing, go to /admin/config/config-split to create split configurations. A typical setup has a "Development" split:
- Folder:
../config/dev - Active: controlled by
settings.php - Modules in split: devel, devel_generate, webprofiler, stage_file_proxy
- Config in split: any dev-only configuration items
Activate the split per-environment in settings.php:
// settings.local.php (development only, not committed)
$config['config_split.config_split.development']['status'] = TRUE;
// settings.php (production — development split is inactive by default)
// No override needed; the split's saved status (inactive) takes effect
When you run drush cex with the development split active, dev-only config is written to ../config/dev/ while production config goes to ../config/sync/. When you run drush cim on production (where the split is inactive), the dev-only config is ignored.
The Config Ignore Module
Some configuration values are environment-specific and should never be overwritten by drush cim. Examples include:
- API keys and secrets in config (ideally, don't store these in config at all — use
settings.phpoverrides) - Email addresses (different per environment)
- Google Analytics IDs
- Performance-related settings (aggregation, caching) that are managed per environment
composer require drupal/config_ignore
drush en config_ignore -y
Configure which config keys to ignore at /admin/config/development/configuration/ignore. You can specify full config names or use wildcards:
system.site:mail
google_analytics.settings:account
mailchimp.settings:api_key
smtp.settings:smtp_username
smtp.settings:smtp_password
With these entries, drush cim will import all other configuration but leave these specific values untouched in the database.
Storing Config in Git
The config sync directory should be committed to your repository. Every change to Drupal's configuration should flow through a Git commit. This gives you:
- A full history of every configuration change
- The ability to roll back config to any previous state
- Peer review of configuration changes via pull requests
- Reproducible environments from a clean checkout
A typical .gitignore for the config directory only excludes environment-specific overrides:
# .gitignore
/web/sites/default/settings.local.php
/web/sites/default/files/
# Do NOT ignore config/sync — it must be committed
Partial Config Imports
Sometimes you need to import only specific configuration items, not the entire sync directory. Use the --partial flag with caution:
# Import only — does not delete config that exists in DB but not in sync dir
drush cim --partial
# Import a single config file
drush config:import --source=/tmp/single-config/
# Set a single config value directly
drush config:set system.site name "My Site"
# Get a config value
drush config:get system.site name
--partial is useful when onboarding a new module that adds config — you import its config without it treating "this config doesn't exist in the sync directory yet" as a deletion to perform. But use it sparingly; the full drush cim provides the guarantee that your running configuration matches your YAML files.
Deployment Scripts
A standard Drupal deployment script should run in this order:
#!/bin/bash
set -e
DRUPAL_ROOT="/var/www/drupal/web"
DRUSH="$DRUPAL_ROOT/../vendor/bin/drush"
echo "=== Deploying ==="
# 1. Pull latest code
git pull origin main
# 2. Install new Composer dependencies
composer install --no-dev --optimize-autoloader
# 3. Put site in maintenance mode
$DRUSH state:set system.maintenance_mode 1 --input-format=integer
$DRUSH cache:rebuild
# 4. Run database updates
$DRUSH updatedb -y
# 5. Import configuration
$DRUSH config:import -y
# 6. Rebuild caches
$DRUSH cache:rebuild
# 7. Take site out of maintenance mode
$DRUSH state:set system.maintenance_mode 0 --input-format=integer
echo "=== Deployment complete ==="
The order matters: database updates (drush updatedb) must run before config import (drush cim), because a module update may add new configuration schema that needs to exist before config import can succeed.
Common Pitfalls
UUID Mismatches
Every Drupal site has a unique UUID, stored in system.site.yml. If you export config from one site and try to import it into a different site (e.g., a fresh install), the UUIDs will not match and the import will fail:
Site UUID in source storage does not match the target storage.
Fix this by setting the target site's UUID to match the source:
# Get the UUID from your config file
grep uuid config/sync/system.site.yml
# Set it on the target site
drush config:set system.site uuid YOUR-UUID-HERE
This situation arises when setting up a new environment from a database dump that came from a different source than your config YAML files. Always use matching database dumps and config exports from the same point in time.
Config Hash Issues on Fresh Installs
When installing Drupal from scratch and then importing config, the installer sets up default configuration that conflicts with your exported config. The recommended approach is to install Drupal with a minimal profile and immediately import your config:
# Install Drupal with the minimal profile (no default content)
drush site:install minimal --existing-config -y
The --existing-config flag tells the installer to use the config in the sync directory as the configuration for the new install, rather than the profile's defaults. This is the cleanest way to spin up a new environment.
Uncommitted Config Changes in Production
A common problem is an admin making configuration changes directly in production through the UI, without exporting them. The next deployment runs drush cim and overwrites those changes. Always treat production config changes as pull requests: make the change in development, export, commit, deploy.
You can detect uncommitted changes with:
drush config:status
This shows which config items differ between the database and the sync directory. Running it before a deployment gives you a chance to identify and handle any out-of-band changes in production.
Module Enable/Disable via Config Import
Config import does not enable or disable modules. If core.extension.yml in your sync directory lists a module that is not installed, drush cim will fail. Always run drush updatedb first (which enables newly required modules) or ensure module installation is part of your deployment script before drush cim.
The full config management workflow — sync directory in Git, config split for environment differences, config ignore for environment-specific values, and a disciplined deployment script — gives you reproducible Drupal environments and a clear audit trail of every configuration change.