Deployment workflow
This page gives you a repeatable sequence for promoting a Drupal 11 site from your repository to a running environment. Code, database structure, and configuration change on different schedules, and the deploy steps that reconcile them have a fixed order. Run them out of order and you corrupt state; run them in order every time and a deploy is boring.
Take the exact code that passed review and apply it to a target environment so the running site matches the repository: the same module versions, the same database schema, and the same configuration. The sequence is build, update the database, import configuration, rebuild the cache, run deploy hooks. The same sequence runs in CI, in staging, and in production.
Prerequisites
Section titled “Prerequisites”- A site whose code, including
composer.jsonandcomposer.lock, lives in version control, set up per Anatomy of a Drupal project. - Configuration exported to files and committed, per the configuration management workflow.
- Drush available on the target environment (
vendor/bin/drush), per Essential tools. - A database backup taken immediately before the deploy. Database updates are not transactional across the whole run; a backup is your rollback.
Two commands are the deploy: composer install builds the code, and drush deploy reconciles the database, configuration, and caches with it, in a fixed order. Everything else (pulling code, taking the backup) brackets them. They run on the target environment, from the project root, with the project’s own vendor/bin/drush; to rehearse a deploy locally, run them as ddev composer ... and ddev drush ....
1. Get the reviewed code onto the target
Section titled “1. Get the reviewed code onto the target”Check out the commit or tag you are deploying. The deployed artifact is the contents of the repository at that ref, including composer.lock.
git fetch --tagsgit checkout v2.14.0Many teams build a deployment artifact in CI instead of running Composer on the production host. Either way, the next step produces the vendor/ and web/core/ trees that match composer.lock.
2. Install dependencies with Composer
Section titled “2. Install dependencies with Composer”Install the exact versions recorded in composer.lock, with development-only packages excluded and the autoloader optimized.
composer install --no-dev --optimize-autoloader --no-interaction--no-dev keeps test frameworks, coding-standards tools, and other development dependencies out of the production tree. Installing from composer.lock (the default for composer install, as opposed to composer update) guarantees the deployed environment has byte-for-byte the dependency versions you tested. Never run composer update as part of a deploy: it resolves new versions and is a code change, not a deployment.
3. Run drush deploy
Section titled “3. Run drush deploy”vendor/bin/drush deploy --no-interactiondrush deploy runs four Drush commands in order, stopping at the first failure: updatedb, config:import, cache:rebuild, and deploy:hook. On Drupal 11.2 and later it then runs cache:warm. The sections below explain what each one does and why the order is fixed.
Database updates
Section titled “Database updates”updatedb applies the pending schema and data updates that the new code requires: the hook_update_N() and hook_post_update_NAME() implementations that ship in .install and .post_update.php files. Update hooks change the shape of stored data: they add database tables and columns, migrate field values, and rewrite stored configuration that the new code can no longer read in its old form. They must run before configuration import, because import assumes the database can hold the configuration the new code expects.
Updates run in dependency order, tracked by the schema version core records per module. hook_update_N functions run first and may not rely on the full service container or on APIs from other modules; hook_post_update_NAME functions run afterward, when the container and entity system are fully available. Both stay procedural functions in .install / .post_update.php files, not attribute classes.
Configuration import
Section titled “Configuration import”config:import reads the YAML files in your configuration sync directory and writes the differences into active storage: new views, changed field settings, enabled or uninstalled modules, permission changes. It runs after database updates so the schema is already correct, and after composer install so any module a configuration change enables is actually present on disk.
The import is one operation over a computed diff, ordered so dependencies are satisfied (a field’s storage is imported before the field, a module is installed before its configuration). If the active configuration’s system.site UUID does not match the files, the import refuses to run: that mismatch means the files came from a different site, and importing them would not be a synchronization. Configuration management workflow covers exporting, the sync directory, and resolving UUID mismatches.
Cache rebuild
Section titled “Cache rebuild”New code changes plugin definitions, routes, service definitions, and theme registries, all of which are discovered and cached. cache:rebuild runs after the database and configuration are settled, so the rebuilt caches reflect the final state. Rebuilding earlier would cache definitions that a later step then changes.
Deploy hooks
Section titled “Deploy hooks”deploy:hook runs hook_deploy_NAME() implementations from MODULE.deploy.php files, once each. They run last, against a fully updated and configured site, which makes them the place for one-off content changes that depend on imported configuration.
The deploy steps run in a fixed order because each depends on the prior one. composer install puts the code on disk; drush updatedb corrects the database schema, which assumes that code; drush config:import writes configuration, which assumes the corrected schema; drush cache:rebuild runs so the rebuilt caches reflect the final state; drush deploy:hook runs one-off deploy functions against the finished site. drush deploy runs the last four. The steps are not independent.
Verify
Section titled “Verify”After the sequence completes, confirm the running site matches the repository:
vendor/bin/drush updatedb --no-interactionreportsNo pending updates.on a second run.vendor/bin/drush config:statusreports no differences between active configuration and the sync files. Any line other than the header means active and exported configuration disagree.vendor/bin/drush core:requirements --severity=2(aliasstatus-report) lists no errors.- The site responds.
curl -sI https://example.com/ | grep -i x-drupal-cachereturns a header, confirming the page cache is serving.
If config:status shows differences right after a deploy, configuration was changed in the running environment outside the workflow, or an export was missed. Re-export from the source of truth and redeploy; do not hand-edit active configuration to match.
Variations
Section titled “Variations”Maintenance mode for deploys with update hooks
Section titled “Maintenance mode for deploys with update hooks”When a deploy ships update hooks, put the site into maintenance mode around it so no request hits a half-updated database.
vendor/bin/drush maint:set 1# composer install, drush deployvendor/bin/drush maint:set 0Maintenance mode trades availability for safety. Deploys that only add code and configuration do not need it; for zero downtime with update hooks, use the atomic release pattern below.
Zero-downtime considerations
Section titled “Zero-downtime considerations”With an in-place composer install, the new code serves live traffic against the old schema and configuration from the moment the files change until updatedb and config:import finish. On a site with real update hooks that window lasts minutes, not milliseconds, and it is where deploy-time fatal errors come from. These remove it:
- Atomic symlink switch. Build the new release in a fresh directory, run
composer installanddrush deployagainst it, then switch acurrentsymlink to the new release and reload the web server. The switch is atomic; no request sees a mixed tree. - Backward-compatible updates. Write update hooks so the old code keeps working against the new schema for the length of the deploy: add columns before you read them, keep old columns until the next release removes them. This is what lets the symlink switch be safe without maintenance mode.
- Opcache reset. PHP caches compiled bytecode. After swapping code, reset the opcode cache (a web server reload, or
cachetool opcache:reset) so the new files are actually executed; otherwise the old bytecode runs until the cache expires. - Shared cache backend. When several web nodes share a Redis or Memcache cache backend (see Performance and caching),
drush cache:rebuildon one node clears the shared bins for all of them. Rundrush deployonce per deploy, not once per node.
Related
Section titled “Related”- Configuration management workflow for exporting configuration and managing the sync directory.
- Performance and caching for the cache layers
cache:rebuildclears and shared cache backends. - Custom database tables and update paths for writing the
hook_update_Nandhook_post_update_NAMEfunctions this sequence runs. - The
Update API topicon api.drupal.org.
Was this page helpful?
Drupal is a registered trademark of Dries Buytaert.