Manage configuration
Goal: get Drupal configuration from your machine onto every Cloud Platform environment reliably, without a deploy trampling what editors changed in production.
Drupal configuration is the site’s structure and settings: content types, field definitions, view modes, the module list, everything an admin form saves rather than an editor typing content. For which of your site’s settings count as configuration rather than content, see drupal.org’s configuration management guide.
What you’ll have when you’re done
Section titled “What you’ll have when you’re done”- A sync directory in the repository, so configuration travels the code path instead of being stranded in each environment’s database.
- A Cloud Hook that imports it on deploy, running the steps in an order that doesn’t fail.
- Production settings that editors own protected from being reverted by that import.
- Per-environment differences (a debugging module off production, say) expressed as configuration rather than as manual steps.
Prerequisites
Section titled “Prerequisites”- The deploy loop from the code-workflow quickstart, and the
hooks/directory from the code-workflow guide - A running site you can export from, with Drush available against it, whose configuration shares its lineage with the codebase. Set up a codebase arranges exactly that in its last step, by installing the site locally and pushing the database up
-
Know what a deploy moves, and what it doesn’t
Section titled “Know what a deploy moves, and what it doesn’t”Cloud Platform promotes code forward through environments and copies databases backward, never forward (the code workflow covers both directions). Drupal’s active configuration lives in the database. Put those two facts together and the problem states itself: the content type you added on your machine is a database row, so pushing your branch does not deliver it.
Configuration therefore takes a detour through the code path. You export it to files, commit them, and the target environment imports them back into its own database after the code lands:
Three commands do the whole round trip, and they are Drupal’s, not Acquia’s:
drush config:exportwrites the active configuration to files.drush config:statusreports how the files and the database differ.drush config:importmakes the database match the files.
What Cloud Platform adds is where those files live, where the import runs, and two ways the round trip goes wrong here.
-
Put the sync directory in the repository, outside the docroot
Section titled “Put the sync directory in the repository, outside the docroot”The sync directory is wherever
$settings['config_sync_directory']points. Left unset, Drupal defaults it to a randomly named directory inside the public files path, which is exactly wrong on Cloud Platform. Files directories are per-environment and copy backward, so configuration left there never reaches production, and it sits under the webroot in the meantime.Point it at the
config/directory the repository layout already reserves, as a sibling ofdocroot/:// settings.php, after the Acquia require line.$settings['config_sync_directory'] = '../config/sync';The path is relative to the docroot, so
../config/syncresolves toconfig/syncat the repository root: tracked by git, deployed with the code, and not served over HTTP. On the ordering rule (why this belongs after Acquia’s own include) see per-environment settings.Export and commit as part of the same change that made the configuration change:
Terminal window drush config:export --yesgit add config && git commit -m "Add teaser display to articles" -
Import on deploy with a Cloud Hook
Section titled “Import on deploy with a Cloud Hook”The import has to run on the environment, after the code arrives. That is what Cloud Hooks are for. For where they sit in the deploy loop, see the code-workflow guide; for the full event list and the six positional arguments the code events receive, see Supported hooks in the acquia/cloud-hooks repository.
What matters here is the order of the commands inside the hook. Database updates must run before the configuration import: an update hook can add the schema that new configuration depends on, and importing first can fail against a schema that hasn’t been updated yet. Drush bundles the whole sequence as one command,
drush deploy, which runsupdatedb, thenconfig:import, thencache:rebuild, thendeploy:hook, then (on Drupal 11.2 and later)cache:warm:hooks/common/post-code-update/deploy.sh #!/bin/shsite="$1"target_env="$2"drush @$site.$target_env deploy --yeschmod +xit before committing, and place the same script inpost-code-deploy/so tag switches on production get the identical treatment. If the alias invocation fails because thevendor/bin/drushshim lacks execute permission, calldrush.phpthrough PHP as the code-workflow guide shows. A non-zero exit marks the deploy task failed, which is what you want when an import didn’t apply.Two problems remain, and neither is solved by anything above. Both have a specific answer:
Problem Answer An editor changes a setting in the production UI, and the next deploy reverts it config_ignore(step 4)A module or setting should exist on some environments but not others config_split(step 5)A value differs per environment and shouldn’t be in git at all (an API endpoint, a credential) Not configuration: a platform environment variable -
Protect what editors own in production
Section titled “Protect what editors own in production”drush config:importmakes the database match the files, and that includes reverting anything the files don’t know about. Change the site name in the production Cloud Platform user interface, deploy anything at all, and the site name goes back to whateverconfig/sync/system.site.ymlsays. No warning: the import reports a successful update and the editor’s change is gone.Config Ignore is the answer, and it is one of the two contrib modules a Cloud Platform Drupal project realistically needs. Install it, then list the configuration the environment owns rather than the repository:
Terminal window composer require 'drupal/config_ignore:^3.4'drush pm:install config_ignore --yesRules go in
config_ignore.settings, one pattern per line, editable atadmin/config/development/configuration/ignore. Patterns can name a whole configuration object (system.site), a single key inside one (system.site:name), a wildcard set (webform.webform.*), or an exception to a wildcard (~webform.webform.contact). Key-level rules are usually what you want: they let the repository keep owning the rest of the object.config/sync/config_ignore.settings.yml mode: simpleignored_config_entities:- 'system.site:name'- 'system.site:slogan'With that rule in place, an import leaves the production site name alone and still applies every other change in
system.site. The ignored key also drops out ofdrush config:status, so a protected difference stops showing up as drift.One behavior to plan for: in
simplemode the same list applies in both directions, so an ignored key is skipped on export too. Your localdrush config:exportwill not push a new value for it into the repository. That is usually right (the value belongs to the environment), but it means active configuration and the files stay legitimately divergent whileconfig:statusreports no differences. If you want protection on import only, switchmodetointermediateand populate theimportlist.Do not add
core.extensionto the ignore list. It holds the module list, so ignoring it means a config import can never install or uninstall a module again, and module differences between environments are the next step’s job. That boundary is Config Ignore’s own documented guidance, not a preference. -
Split configuration that differs by environment
Section titled “Split configuration that differs by environment”A debugging module belongs on Dev and Stage and must not be enabled on production. Because
core.extensionis a single configuration object listing every installed module, one repository cannot express “enabled here, not there” on its own.Config Split adds that. A split is a named set of configuration that is exported to a separate directory and merged back in on import only where the split is active. Modules listed in a split are removed from
core.extensionin the main sync directory, so production imports a module list that never had them.Terminal window composer require 'drupal/config_split:^2.0'drush pm:install config_split --yesCreate a split at
admin/config/development/configuration/config-split, give it a folder that is a sibling of the sync directory (../config/dev, never insideconfig/sync), and add the modules and configuration it owns. Leave itsstatusunchecked, so the committed configuration says “off” and production gets the safe default.Then activate it per environment, which is where Cloud Platform does the work. The platform environment variables already tell
settings.phpwhich environment it is running on, so the same override that Config Split documents becomes environment-aware for free:// settings.php, after the Acquia require line.if (getenv('AH_SITE_ENVIRONMENT') !== 'prod') {$config['config_split.config_split.development']['status'] = TRUE;}Compare against the environment’s name, not the label on its environment card. Stage is named
test, so a split meant for that environment alone isgetenv('AH_SITE_ENVIRONMENT') === 'test'. Written as=== 'stage'the condition never fires, and nothing reports it: the deploy succeeds, the import succeeds, and that one environment quietly runs production’s configuration. The four names aredev,test,prod, andide.From then on the split is invisible in your workflow. Config Split 2.x hooks into Drupal core’s configuration transformation API (core 8.8 and later), so plain
drush config:exportanddrush config:importapply splits with no split-specific command and noconfig_filterdependency. Exporting on dev moves the split modules out of the main sync directory and into the split folder, and importing on production uninstalls them. Theconfig-split:exportandconfig-split:importcommands exist only for operating on one split in isolation.Where a value differs per environment but no module does, prefer the simpler tools first: a
settings.phpoverride guarded byAH_SITE_ENVIRONMENT, or a custom environment variable. Reach for a split when whole modules or configuration objects differ, which is the case it was built for.
When something goes wrong
Section titled “When something goes wrong”Site UUID in source storage does not match the target storage.
The environment’s site was installed independently of the repository, so its site UUID differs from the one in config/sync/system.site.yml, and Drupal refuses to import one site’s configuration into a different site. The import exits non-zero, so the deploy task fails rather than half-applying. Two ways out: reinstall the environment from your local database, which is what set up a codebase has you do precisely to avoid this, or set the environment’s active UUID to the repository’s value and import again.
acli remote:drush myapp.dev -- config:set system.site uuid <uuid-from-the-repo> --yesSetting the UUID is safe only when the two really are the same site with a divergent install history. Read the site UUID section of Drupal’s configuration guide before doing it on production.
A deploy reverted a setting an editor changed.
That is config:import working as designed, and step 4 is the fix. Restore the value in the UI, then add the key to config_ignore.settings in the same change, or the next deploy does it again.
config:import reports differences you didn’t make.
Someone changed configuration on the environment through the UI. Export on the environment first to see the drift (acli remote:drush myapp.dev -- config:export), decide per item whether the repository or the environment should own it, and route the environment-owned ones into config_ignore.
Config Split exports look wrong or a split seems to do nothing.
Two usual causes: the split folder is inside the sync directory (it must be a sibling, or the sync export sees the split’s own files), or the split is inactive on the machine you exported from. Only active splits take effect on export, so a split you activate through settings.php needs a cache rebuild before drush config:export will honor it.
The hook fails on the environment but the same command works locally.
Check the drush invocation before the configuration: environments that disable PHP process execution reject the drush launcher, and the drush.php-through-PHP form in the code-workflow guide is the workaround.
Next steps
Section titled “Next steps”- The code workflow: the deploy loop and Cloud Hook mechanics this page builds on.
- Per-environment settings: environment variables and
settings.php, for the values that shouldn’t be configuration at all. - Local development against Acquia: the refresh routine that keeps your local database close enough to export against.
- Automate with CI/CD: run the export check in a pipeline, so drift fails a build instead of a deploy.
Was this page helpful?
What went wrong?
Still stuck? Contact Acquia Support (opens in a new tab)