Skip to content

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.

  • 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.
  • 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
  1. 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:export writes the active configuration to files.
    • drush config:status reports how the files and the database differ.
    • drush config:import makes 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.

  2. 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 of docroot/:

    // settings.php, after the Acquia require line.
    $settings['config_sync_directory'] = '../config/sync';

    The path is relative to the docroot, so ../config/sync resolves to config/sync at 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 --yes
    git add config && git commit -m "Add teaser display to articles"
  3. 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 runs updatedb, then config:import, then cache:rebuild, then deploy:hook, then (on Drupal 11.2 and later) cache:warm:

    hooks/common/post-code-update/deploy.sh
    #!/bin/sh
    site="$1"
    target_env="$2"
    drush @$site.$target_env deploy --yes

    chmod +x it before committing, and place the same script in post-code-deploy/ so tag switches on production get the identical treatment. If the alias invocation fails because the vendor/bin/drush shim lacks execute permission, call drush.php through 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
  4. drush config:import makes 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 whatever config/sync/system.site.yml says. 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 --yes

    Rules go in config_ignore.settings, one pattern per line, editable at admin/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: simple
    ignored_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 of drush config:status, so a protected difference stops showing up as drift.

    One behavior to plan for: in simple mode the same list applies in both directions, so an ignored key is skipped on export too. Your local drush config:export will 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 while config:status reports no differences. If you want protection on import only, switch mode to intermediate and populate the import list.

    Do not add core.extension to 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.

  5. 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.extension is 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.extension in 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 --yes

    Create a split at admin/config/development/configuration/config-split, give it a folder that is a sibling of the sync directory (../config/dev, never inside config/sync), and add the modules and configuration it owns. Leave its status unchecked, 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.php which 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 is getenv('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 are dev, test, prod, and ide.

    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:export and drush config:import apply splits with no split-specific command and no config_filter dependency. Exporting on dev moves the split modules out of the main sync directory and into the split folder, and importing on production uninstalls them. The config-split:export and config-split:import commands 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.php override guarded by AH_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.

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.

Terminal window
acli remote:drush myapp.dev -- config:set system.site uuid <uuid-from-the-repo> --yes

Setting 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.

Was this page helpful?