Skip to main content
Version: v2

Provision

Provisioning turns an assembled codebase into a working Drupal site. By default it imports the database dump; switch the provision type to profile and it installs Drupal from a profile instead. Either way, it then runs the database updates, the configuration import, and the deployment hooks, so the site matches your code. A site that's already installed keeps its database unless you ask for a fresh one.

This page is for developers working day to day on a Vortex project, and for the lead who tunes provisioning for each environment. It explains what the provision script does and which variables steer it. It then shows how to fetch, refresh, and export the database, and how to add your own deploy steps and deploy hooks. How code reaches a hosting environment before provisioning runs there is a separate topic; see Deployment.

A single script does the whole job: vortex-provision, shipped in the drevops/vortex-tooling Composer package and installed at vendor/bin/vortex-provision. Your machine, CI, and every hosting environment run that same PHP script inside the CLI container. The steps stay the same everywhere, and only the environment variables and the detected environment type change what they do.

Rationale​

Drush has a command for each deployment step (drush updatedb, drush config:import, drush deploy:hook), but running them by hand assumes the site is already bootstrapped and stable. That assumption breaks during an initial setup, and in environments that come and go, such as CI pipelines, container builds, or multisite setups. There, something has to run the steps in the right order and decide which of them apply.

The provision script is that orchestration:

  • It bootstraps the environment by importing a database dump or installing a fresh Drupal instance from a profile.
  • It handles conditional logic for different runtime scenarios, such as skipping provisioning, enforcing fresh database imports, or using maintenance mode.
  • It runs the same provisioning logic across local, CI, staging, and production environments.
  • It runs your project's deploy steps with the deployment hooks, so you can add logic like enabling modules or running migrations.

Provisioning flow​

The following diagram shows the steps the script takes and the points where it branches. Environment variables steer most of the branches, and the circled numbers match the numbered variables in Customizing flow:

πŸš€ Start
β”‚
β‘  πŸ’‘ Skip provision? ──Yes──► 🏁 End
β”‚ No
β–Ό
β‘‘ πŸ’‘ Provision type = database
β”‚
β”œβ”€ πŸ’‘ Existing site found = YES
β”‚ β”œβ”€ Container image set? ──► preserve content (no import), β‘© sanitize
β”‚ β”œβ”€ β‘’ Overwrite DB? ──Yes──► πŸ›’οΈ Attempt import from dump
β”‚ β”‚ β”‚
β”‚ β”‚ β”œβ”€ Dump file exists? ──► πŸ—‘οΈ Drop DB ──► πŸ›’οΈ Import DB
β”‚ β”‚ └─ Dump file missing?
β”‚ β”‚ β”œβ”€ β‘£ Fallback? ──Yes──► πŸ“¦ Install from profile βœ“ (then β‘€ = Yes)
β”‚ β”‚ └─ β‘£ Fallback? ──No───► 🏁 EXIT 1 (fail) βœ—
β”‚ └─ else ──► preserve content, β‘© skip sanitization
β”‚
└─ πŸ’‘ Existing site found = NO
β”œβ”€ Container image set?
β”‚ β”œβ”€ β‘£ Fallback? ──Yes──► πŸ“¦ Install from profile βœ“ (then β‘€ = Yes)
β”‚ └─ β‘£ Fallback? ──No───► 🏁 EXIT 1 ("corrupted image") βœ—
└─ Container image not set? ──► πŸ›’οΈ Attempt import from dump
β”‚
β”œβ”€ Dump file exists? ──► πŸ—‘οΈ Drop DB ──► πŸ›’οΈ Import DB
└─ Dump file missing?
β”œβ”€ β‘£ Fallback? ──Yes──► πŸ“¦ Install from profile βœ“ (then β‘€ = Yes)
└─ β‘£ Fallback? ──No───► 🏁 EXIT 1 (fail) βœ—

── after provisioning (both DB and profile paths) ──

β‘€ πŸ’‘ Skip other operations? ──Yes──► 🏁 End
β”‚ No
β–Ό
β‘₯ 🚧 Enable maintenance mode
β–Ό
πŸ†” Set site UUID from configuration (if config files present)
β–Ό
πŸ”„ Run DB updates
β–Ό
⑦ πŸ’‘ Verify config unchanged? ──Config changed──► 🏁 EXIT 1 (fail) βœ—
β”‚ Config unchanged (or check disabled)
β–Ό
β‘§ 🧹 Rebuild caches after DB updates (skippable)
β–Ό
⬇️ Import configuration (if config files present)
β–Ό
⑨ πŸ” Repeat configuration import (opt-in)
β–Ό
⬇️ Import config_split configuration (if the module is enabled)
β–Ό
🧹 Rebuild caches
β–Ό
🧩 Enable deploy_steps and the *_base and *_migrate site modules
β–Ό
πŸ”„ Run deployment hooks and deploy steps
β–Ό
β‘© 😷 Run DB sanitization
β–Ό
β‘₯ 🚧 Disable maintenance mode
β–Ό
β‘ͺ πŸ’‘ Verify config unchanged? ──Config changed──► 🏁 EXIT 1 (fail) βœ—
β”‚ Config unchanged (or check disabled)
β–Ό
🏁 End

The diagram follows the default database provision type. With VORTEX_PROVISION_TYPE=profile, the branches at the top collapse into 1 decision. A site that isn't installed yet gets a fresh installation from the profile.

With the profile type, an installed site keeps its database and skips sanitization, unless VORTEX_PROVISION_OVERRIDE_DB is 1, which reinstalls it from the profile. Everything after the installation runs the same way for both types.

With a database container image, an installed site keeps the database the image provides, without an import. The script still treats that database as fresh: it sanitizes it and sets VORTEX_PROVISION_OVERRIDE_DB=1 for your deploy steps.

Right after the skip check (β‘ ), the script checks that the configuration directory in $settings['config_sync_directory'] exists, and stops with an error if it doesn't.

Before the deployment hooks, the script installs the deploy_steps module and every custom module whose directory name ends in _base or _migrate. Installing a module that's already installed does nothing, so this step is safe on every run.

Customizing flow​

These environment variables steer the flow:

  1. VORTEX_PROVISION_SKIP=1
    Skip provisioning entirely. The script exits as soon as it starts and leaves the site alone, which helps in an emergency when every kind of automation has to stop.

  2. VORTEX_PROVISION_TYPE=profile
    Install Drupal from the profile in DRUPAL_PROFILE (default standard) instead of importing a database dump. With exported configuration present, it runs drush site:install --existing-config, so the site installs from your configuration. That suits sites without a persistent database, and it tests that your configuration installs cleanly from scratch. Any value other than profile counts as database, the default.

  3. VORTEX_PROVISION_OVERRIDE_DB=1
    Replace the database of a site that's already installed. The script drops it and imports the dump again, or reinstalls from the profile. Without it, an installed site keeps its database and only the later steps run. .env ships with 0, the usual choice for hosted environments, and .env.local.example sets 1, so local provisioning starts from a fresh copy.

  4. VORTEX_PROVISION_FALLBACK_TO_PROFILE=1
    Fall back to a profile installation when the database dump file is missing, or when the container image holds no usable database. The script installs the profile in DRUPAL_PROFILE without your exported configuration and enables the Shield module to protect the environment. It then skips every post-provision operation, from database updates to deploy steps. The result is a minimal working Drupal site when no database is available. It applies to the database provision type only.

  5. VORTEX_PROVISION_POST_OPERATIONS_SKIP=1
    Stop right after the database import or profile installation, and skip every later step, from maintenance mode to the final configuration check. CI uses it to cache a database in the state it was fetched, before any updates run.

  6. VORTEX_PROVISION_USE_MAINTENANCE_MODE=1
    Put the site into maintenance mode right after the database import or installation, and take it out again at the end. Visitors then don't reach a site that's halfway through provisioning. It's on by default: .env sets 1, and so does the script when the variable isn't set.

  7. VORTEX_PROVISION_VERIFY_CONFIG_UNCHANGED_AFTER_UPDATE=1
    Fail the provisioning when drush updatedb changes active configuration. The script exports the configuration before and after the updates and compares the 2 copies, so drush config:import can't silently overwrite what an update hook changed. On failure, it lists the changed files and keeps both exports for you to review. The check needs exported configuration files and is off by default.

  8. VORTEX_PROVISION_CACHE_REBUILD_AFTER_DB_UPDATE_SKIP=1
    Skip the cache rebuild between the database updates and the configuration import. By default, the script runs drush updatedb --no-cache-clear and then rebuilds the caches itself, so the configuration import starts from a clean state.

  9. VORTEX_PROVISION_CONFIG_IMPORT_REPEAT=1
    Run the configuration import a second time, straight after the first. It helps when update hooks introduce configuration that changes what a later import does (for example, new config_split settings). It's off by default, and like the import itself it needs exported configuration.

  10. VORTEX_PROVISION_SANITIZE_DB_SKIP=1
    Skip database sanitization. Database sanitization explains when it runs otherwise.

  11. VORTEX_PROVISION_VERIFY_CONFIG_UNCHANGED_AFTER_PROVISION=1
    Fail the provisioning when the post-provision operations leave active configuration different from the exported files. The check runs last, after the configuration imports, deployment hooks, and deploy steps. It fails if drush config:status reports any item in the Different state. Items that exist only in the database or only in the sync directory don't count, because a site can legitimately carry either. Like the update check, it needs exported configuration files and is off by default.

A few more variables shape what gets provisioned:

  • VORTEX_DB_DIR and VORTEX_DB_FILE locate the dump (./.data/db.sql by default). VORTEX_PROVISION_DB_DIR and VORTEX_PROVISION_DB_FILE override them for provisioning only, and VORTEX_PROVISION_DB takes a full path to the dump.
  • VORTEX_DB_IMAGE, or VORTEX_PROVISION_DB_IMAGE for provisioning only, names the container image when the database ships inside one. Setting it sends the flow down the "Container image set?" branches of the diagram.
  • DRUPAL_PROFILE, DRUPAL_SITE_NAME, DRUPAL_SITE_EMAIL, and DRUPAL_ADMIN_EMAIL feed the profile installation. The administrator account is always named admin.
  • VORTEX_PROVISION_LOG=1 saves the output as provision.log in VORTEX_NOTIFY_LOG_DIR, where deployment notifications collect it.

The Variables reference lists the variables that .env sets, with their defaults, and the top of vendor/bin/vortex-provision declares every variable the script reads. Set them in .env to apply them everywhere, or in your CI or hosting provider's environment variables to change a single environment.

Maintenance mode​

Maintenance mode keeps visitors out while the database updates and configuration imports run, so nobody browses a site that's halfway between 2 versions. It's on by default: .env sets VORTEX_PROVISION_USE_MAINTENANCE_MODE=1, and the script falls back to 1 when the variable isn't set at all. To keep the site online during provisioning, set it to 0 in .env or in your hosting provider's environment variables.

If a step fails partway through, the script exits with the site still in maintenance mode. Fix the problem and provision again, or turn maintenance mode off by hand with Drush in the affected environment:

drush maint:set 0

Maintenance mode runs in PHP and needs the database. To keep every request away from PHP, for example during a migration on Lagoon, serve the static maintenance page.

Database sanitization​

Sanitization replaces real user data with safe values, so a copy of production can't email real people from a test run or show their details to the team. The script runs drush sql:sanitize, which does the following:

  • Replaces account email addresses and passwords
  • Scrubs the values of user fields
  • Removes comment author names and email addresses
  • Empties the sessions table
warning

Sanitization changes the imported database, not the dump file, so anyone with access to the dump can still read the sensitive data in it.

If your database holds highly sensitive data, sanitize the dump before it can be downloaded (sanitize on export). Tools such as Drush GDPR Dumper or MTK do this, and they fit into Vortex-based projects without changes to the provisioning process.

Sanitization runs by default whenever provisioning brings in a fresh database: a dump import, a profile installation, or a database container image. It runs after the deployment hooks and deploy steps, so the data they create is sanitized too. A fallback installation is the exception, because it skips every step after the install.

Sanitization is also skipped whenever the script keeps an existing database, and that includes a database the hosting platform copied in, for example from production. Production keeps its own database between deployments, so it isn't sanitized, and .lagoon.yml also sets VORTEX_PROVISION_SANITIZE_DB_SKIP=1 in production explicitly.

To turn sanitization off, set VORTEX_PROVISION_SANITIZE_DB_SKIP=1 in the .env file or in your hosting provider's environment variables.

Customizing database sanitization​

To adjust what sanitization does, set these variables in the .env file or in your hosting provider's environment variables:

  • VORTEX_PROVISION_SANITIZE_DB_EMAIL=user_%uid@your-site-domain.example
    Replace every account email address with this pattern. Drush replaces %uid with the user ID. It also accepts %mail, the original address with @ changed to _, and %name, the username with spaces changed to _. .env ships with this value; without it, the script uses user+%uid@localhost.

  • VORTEX_PROVISION_SANITIZE_DB_PASSWORD=<random or exact>
    Replace account passwords with this exact value. By default the script generates a random number on each run, so no production password works on the sanitized copy.

  • VORTEX_PROVISION_SANITIZE_DB_REPLACE_USERNAME_WITH_EMAIL=1
    Replace each username with the user's sanitized email address, so the usernames don't give real names away either. It's off (0) by default.

  • VORTEX_PROVISION_SANITIZE_DB_ADDITIONAL_FILE=./scripts/sanitize.sql
    Run the SQL queries in this file after the Drush sanitization. This path is the default, and Vortex ships scripts/sanitize.sql with an example query that blocks user 1. The script skips this step when the file doesn't exist, so deleting the file turns it off.

Whatever you configure, the script then empties the name and email of the anonymous user (user 0). When DRUPAL_ADMIN_EMAIL is set, it also sets user 1's email back to that address.

Database​

The running site's database lives inside the database container. The .data/db.sql file on your machine is a dump: the import commands read it, and the export command writes new dumps next to it. Remove the container (ahoy reset does, for example) and the live database goes with it; the next build re-creates it from the dump.

Fetching and exporting start on the host, because they move dump files around. Refreshing runs inside the containers, because it imports into the running database. The command tabs in the following sections follow that split.

Fetching database​

ahoy fetch-db downloads a database dump into the .data directory. VORTEX_FETCH_DB_SOURCE sets where the dump comes from, and accepts the following values:

  • url, the default: the file at VORTEX_FETCH_DB_URL
  • ftp: a file on an FTP server
  • acquia: a backup of an Acquia environment
  • lagoon: a dump of a Lagoon environment
  • container_registry: a database container image, pulled instead of a file
  • s3: a file in an S3 bucket
  • none: no source; you put the dump into .data yourself

Any other value stops the command with an error that lists these values.

Each source reads its own settings, which the Variables reference lists. The hosting sources copy the environment named in VORTEX_FETCH_DB_ENVIRONMENT: .env ships with prod, and the CLI sets main for lagoon, because Lagoon names environments after their branches. Projects that install from a profile have no ahoy fetch-db, because the CLI removes it.

To fetch the dump, run the following command in the project root:

# Fetch latest database dump
ahoy fetch-db
# Force a fresh fetch regardless of cache
ahoy fetch-db --fresh

Confirm that the output ends with Finished database download. and that .data holds the dump.

When .data already holds a dump, the fetch keeps it and stops, whatever its age, and the output says Download will not proceed. instead. VORTEX_FETCH_DB_FORCE=1 downloads a new copy anyway, and .env.local.example sets it, so a .env.local copied from that file always downloads. With the none source, the command only lists what .data holds.

--fresh sets VORTEX_FETCH_DB_FORCE=1 too, along with VORTEX_FETCH_DB_FRESH=1. That second variable makes Acquia create a new backup instead of downloading the latest one. On Lagoon, it creates a new dump instead of reusing the one made earlier that day.

The dump stays in .data instead of going straight into the database, so you can reuse it: refreshing the local environment doesn't need a new fetch. To use a dump you downloaded yourself, name it db.sql and put it in the .data directory.

Refreshing database​

You can load the dump from .data into the running database container in 2 ways, depending on what you need. Use ahoy import-db for the raw data without any configuration changes or updates, and ahoy provision for the full setup, including configuration and updates.

Import and run updates (ahoy provision)​

ahoy provision runs the whole provisioning flow inside the CLI container:

  • Database import from dump or profile installation
  • Database updates with drush updatedb
  • Configuration import with drush config:import, when configuration files exist
  • Cache rebuilds
  • Deployment hooks and deploy steps
  • Database sanitization

To bring the local environment back to a fully updated state, with a configured site that's ready for development or deployment, use ahoy provision. Besides the flow itself, the command does the following:

  • Asks for confirmation before it starts
  • Reloads the database container from its image first, when VORTEX_DB_IMAGE is set
  • Unblocks user 1 and prints a one-time login link at the end

The dump replaces the current database only when no site is installed yet or VORTEX_PROVISION_OVERRIDE_DB=1 is set, which .env.local.example does after you copy it to .env.local. Otherwise the site keeps its database and only the later steps run.

To provision the site, run the following command:

ahoy provision

Confirm that the output ends with Finished site provisioning, followed by the time it took. With ahoy provision, the one-time login link comes after it.

Import only (ahoy import-db)​

ahoy import-db imports the raw dump and runs none of the other provisioning steps:

  • Imports the database dump
  • Skips database updates
  • Skips configuration imports
  • Skips cache rebuilds
  • Skips deployment hooks and deploy steps
  • Skips database sanitization

To reset the database contents to the state captured in the dump, use ahoy import-db. It asks for confirmation, then drops the current database and loads the dump. A missing or unreadable dump file stops it with Unable to import database from file. before anything is dropped.

To import a dump, run the following command:

# Import database only from default dump in .data/db.sql
ahoy import-db
# Import database only from a specific dump file
ahoy import-db path/to/dump.sql

Confirm that the output ends with Finished database import.

Exporting database​

ahoy export-db saves the current local database as a timestamped dump in .data, with a name such as export_db_20260101_120000.sql. To export the database, run the following command:

# Export current database to .data directory
ahoy export-db

Confirm that the output includes Exported database dump saved, followed by the path of the new file.

These dumps let you return the local environment to a known state later: rename one to .data/db.sql and run ahoy import-db, or pass its path to ahoy import-db directly. To pick the filename yourself, pass it to the export: ahoy export-db db.sql writes .data/db.sql. With VORTEX_DB_IMAGE set, the command exports a database container image instead of a file.

Cache tables are exported with their structure but without their data, because Drupal rebuilds them on demand and their contents would only inflate the dump. To choose which tables are exported this way, set VORTEX_EXPORT_DB_FILE_STRUCTURE_TABLES to a comma-separated list of table names. Each name can use the * wildcard, and the default is cache*. To export the data of every table, set it to an empty value.

Running custom scripts​

Logic that must run on every deploy, such as enabling modules conditionally, running migrations, or rebuilding derived data, goes in a DeployStep plugin in a custom module. The deploy_steps module discovers these plugins and runs them around drush deploy:hook in every environment, ordered by phase and then weight.

To add a deploy step, declare a plugin in the src/Plugin/DeployStep/ namespace of any enabled module:

namespace Drupal\my_module\Plugin\DeployStep;

use DrevOps\EnvironmentDetector\Environment;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\deploy_steps\Attribute\DeployStep;
use Drupal\deploy_steps\DeployStepBase;
use Drupal\deploy_steps\DeployStepInterface;
use Drupal\deploy_steps\DrushTrait;
use Drupal\deploy_steps\EnvironmentTrait;

#[DeployStep(
id: 'rebuild_search_index',
label: new TranslatableMarkup('Rebuild the search index'),
weight: 10,
phase: DeployStepInterface::PHASE_POST,
)]
final class RebuildSearchIndex extends DeployStepBase {

use DrushTrait;
use EnvironmentTrait;

public function skip(): ?string {
return $this->environment() === Environment::PRODUCTION ? 'production environment' : NULL;
}

public function run(): void {
$this->drush('search-api:index');
}

}

The plugin's parts do the following:

  • phase selects PHASE_PRE (before the deploy:hook body) or PHASE_POST (after it). A pre-phase plugin that enables modules lets the body run their run-once hook_deploy_NAME() in the same deploy.
  • weight orders plugins within a phase (lower runs first).
  • skip() returns NULL to run, or a short reason to skip that's logged verbatim. Compose EnvironmentTrait for the environment() helper to gate by environment type.
  • run() is the idempotent work. Compose DrushTrait for the drush() helper to run a long sub-command such as migrate:import in its own memory-bounded, resumable subprocess.

The provision script sets VORTEX_PROVISION_OVERRIDE_DB before it runs drush deploy:hook, so a deploy step can check whether the database is fresh. The value is 1 whenever the database is new: imported from a dump, installed from a profile, or started from a container image.

The following complete deploy step makes a starting point to copy. It enables custom modules before the deploy:hook body, checks the environment type in skip(), and branches on VORTEX_PROVISION_OVERRIDE_DB:

Example of a custom deploy step
<?php

declare(strict_types=1);

namespace Drupal\my_module\Plugin\DeployStep;

use DrevOps\EnvironmentDetector\Environment;
use Drupal\Core\Extension\ModuleInstallerInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\deploy_steps\Attribute\DeployStep;
use Drupal\deploy_steps\DeployStepBase;
use Drupal\deploy_steps\DeployStepInterface;
use Drupal\deploy_steps\EnvTrait;
use Drupal\deploy_steps\EnvironmentTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
* Example deploy step: prepares a non-production environment on every deploy.
*
* The deploy_steps equivalent of the legacy `provision-10-example.sh` custom
* provision script. Copy it into any enabled module's `src/Plugin/DeployStep/`
* namespace and adapt it - or remove it. The deploy_steps runner discovers it
* and runs it around every `drush deploy:hook`.
*
* It runs in the PRE phase so the modules it enables have their run-once
* `hook_deploy_NAME()` fired by the `deploy:hook` body that follows.
*/
#[DeployStep(
id: 'example',
label: new TranslatableMarkup('Example environment setup'),
weight: 0,
phase: DeployStepInterface::PHASE_PRE,
)]
final class ExampleDeployStep extends DeployStepBase {

use EnvironmentTrait;
use EnvTrait;

/**
* The module installer.
*/
protected ModuleInstallerInterface $moduleInstaller;

/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
$instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
$instance->moduleInstaller = $container->get('module_installer');

return $instance;
}

/**
* {@inheritdoc}
*/
public function skip(): ?string {
// Run only in non-production environments. The value of
// $settings['environment'] is set in the Drupal settings file.
return $this->environment() === Environment::PRODUCTION ? 'production environment' : NULL;
}

/**
* {@inheritdoc}
*/
public function run(): void {
// Enable custom site modules. Idempotent: already-enabled modules are
// skipped. Their run-once deploy hooks fire in the deploy:hook body because
// this step runs in the PRE phase.
$this->moduleInstaller->install(['my_module_search', 'my_module_demo']);

// Conditionally act on a freshly imported database. The provision script
// exports VORTEX_PROVISION_OVERRIDE_DB before `drush deploy:hook`, so the
// step reads it here via getenv().
if ($this->env('VORTEX_PROVISION_OVERRIDE_DB', '0') === '1') {
// Fresh database detected - place one-off setup here.
}
}

}

Vortex ships the following deploy steps:

Deploy stepModulePhase and weightRuns inPresent when
CreateContentModelDeployStepys_basePre, -10Every environment except productionThe Base custom module is selected
EnableDevelopmentModulesDeployStepys_basePre, 0Every environment except productionThe Base custom module is selected
MigrateContentDeployStepys_migratePost, 0Every environment except productionThe migration feature is selected
RebuildSearchIndexys_searchPost, 10local, ci, development, and stageThe Search custom module and the Solr service are selected
BuildStorybookDeployStepys_basePost, 20local, ci, and developmentThe Base custom module and Storybook are selected

The CLI removes each step whose module or feature isn't selected. The following sections describe each one.

Development modules​

EnableDevelopmentModulesDeployStep sets up the development and demo site in every environment except production. It runs after CreateContentModelDeployStep, which applies the recipes/page recipe to create the page content type. The site modules attach behavior to that content type, and their deployment hooks need it.

The step does the following, in order:

  1. Sets the site name
  2. Switches the administration interface to the core Navigation module and uninstalls Toolbar
  3. Installs the Storybook module, on projects with Storybook enabled. In the local environment, it also turns on Twig development mode, so component template edits show up without a cache rebuild.
  4. Installs the contributed and service modules the CLI kept, including SDC Devel, Devel, and Reroute Email, the email safety net
  5. Installs the ys_search and ys_demo site modules; ys_demo brings Testmode as a dependency
  6. Installs Generated Content last

Because the step runs in the pre phase, the run-once deploy hooks of the modules it installs run in the same deploy.

Several of these modules add themselves to $settings['config_exclude_modules'] in their settings files, which keeps them out of the exported configuration. A configuration import never installs them, so this step is the only place that does.

The step gates on production alone, so it also runs in preview and in any custom environment type. That matters for email rerouting in particular: Reroute Email's settings turn rerouting on in every environment outside local, ci, and production. To opt out of the development modules, remove the step's plugin class from ys_base.

Generated Content​

Projects with the Generated Content module install it from the same development modules deploy step, after the site modules that provide its content plugins.

The module creates content from its own hook_modules_installed() when GENERATED_CONTENT_CREATE=1 is set. The step sets that variable for the installation, so the content appears as part of the install.

To opt out of the content, set DRUPAL_GENERATED_CONTENT_SKIP=1 in the .env file or in your hosting provider's environment variables. The module is still installed, so you can generate the content later from the administration interface at /admin/config/development/generated-content or with Drush, for example drush generated-content:create-content node page 20.

Installing a module that's already installed does nothing, so provisioning a site that already holds the module leaves its content untouched. The content appears only on the provision that first installs the module. A site provisioned once with DRUPAL_GENERATED_CONTENT_SKIP=1 doesn't gain the content when you unset the variable and provision again.

Migration​

Projects with the migration feature ship MigrateContentDeployStep in the ys_migrate module. It runs the content migration in every environment except production, and skips when DRUPAL_MIGRATION_SKIP=1 is set.

The step loads the migration source database dump into the migrate database connection when the database is fresh or the source database fails its probe. It then runs drush migrate:import --all.

On projects with the Solr service, the step disables the search server while the migrations run, so migrated entities aren't indexed 1 at a time. The search indexing step rebuilds the index straight after.

➑️ See Migrations for the step's configuration variables and how to add your own migrations.

Search indexing​

Projects with the Solr service ship RebuildSearchIndex in the ys_search module. It runs after the migration step, resets the Search API index tracker, and re-indexes the site content.

The step runs only in the local, ci, development, and stage environments. They receive a database whose index tracker state doesn't match their own (empty) search backend, while every other environment keeps its existing index.

To opt out of search indexing, set DRUPAL_SEARCH_INDEX_SKIP=1 in the .env file or in your hosting provider's environment variables.

Storybook build​

Projects with Storybook enabled ship BuildStorybookDeployStep in the ys_base module. It runs vendor/bin/vortex-storybook build, which generates the stories and builds the component library served at /storybook.

The step runs only in the local, ci, and development environments, where the Storybook settings open the story render route. It also skips when the theme's Storybook dependencies aren't installed.

To skip the build, set DRUPAL_STORYBOOK_SKIP=1 in the .env file or in your hosting provider's environment variables.

Deploy hooks​

Update hooks automate database schema changes, data migrations, and environment-specific deployment tasks, so a change ships together with the code that needs it instead of being applied by hand.

Hook types overview​

Drupal core provides the first 2 hook types in the following table, and Drush adds the third:

Hook typeUse caseExecutionFile location
hook_update_N()Database schema changes, data migrationsdrushΒ updatedbMODULE.install
hook_post_update_NAME()Entity updates and other module operationsdrushΒ updatedbMODULE.post_update.php
hook_deploy_NAME()Environment-specific deployment tasksdrushΒ deploy:hookMODULE.deploy.php

All these hooks run automatically during the provisioning process: drush updatedb runs the update and post-update hooks, and drush deploy:hook runs the deploy hooks.

For 1-time changes tied to a deployment, use deploy hooks. Each one runs only once: after it runs, Drush records its name in Drupal's key-value store and skips it from then on. For logic that runs on every deploy, use a deploy step instead.

Example deploy hook​

The following deploy hook, placed in ys_base.deploy.php, creates an About Us page in every environment except production:

function ys_base_deploy_create_about_page(): string {
$environment = \Drupal\Core\Site\Settings::get('environment');

// Conditional execution based on environment.
if ($environment === \DrevOps\EnvironmentDetector\Environment::PRODUCTION) {
return 'Skipped in production environment';
}

// Check if the About Us page already exists.
$node = \Drupal::entityTypeManager()
->getStorage('node')
->loadByProperties(['type' => 'page', 'title' => 'About Us']);
if (!empty($node)) {
return 'About Us page already exists';
}

$node = \Drupal\node\Entity\Node::create([
'type' => 'page',
'title' => 'About Us',
'body' => [
'value' => 'This is the About Us page content.',
'format' => 'basic_html',
],
]);

$node->save();

return 'Created About Us page';
}

The shipped Drupal helpers module provides static facade helpers for the common deploy hook operations. With them, a hook stays a few lines long instead of repeating the entity API boilerplate every time:

use Drupal\drupal_helpers\Helper;

function ys_base_deploy_create_default_tags(): string {
Helper::term()->createTree('tags', ['News', 'Events', 'Blog']);

return 'Created the default tags.';
}

Debugging commands​

To list the pending deploy hooks or run them by hand, use the following Drush commands in the CLI container:

# Show pending deploy hooks.
drush deploy:hook-status

# Run deploy hooks manually (for testing).
drush deploy:hook