Drupal Configuration Management: Syncing Config Between Environments

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:

  1. Make configuration changes on your development site (via the UI or programmatically)
  2. Export the configuration to YAML files: drush config:export
  3. Commit the YAML files to version control
  4. 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.php overrides)
  • 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.

Add new comment

Restricted HTML

  • Allowed HTML tags: <a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>
  • Lines and paragraphs break automatically.
  • Web page addresses and email addresses turn into links automatically.
Please share this article on your favorite website or platform.