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:
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.VORTEX_PROVISION_TYPE=profile
Install Drupal from the profile inDRUPAL_PROFILE(defaultstandard) instead of importing adatabasedump. With exported configuration present, it runsdrush 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 thanprofilecounts asdatabase, the default.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..envships with0, the usual choice for hosted environments, and.env.local.examplesets1, so local provisioning starts from a fresh copy.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 inDRUPAL_PROFILEwithout 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 thedatabaseprovision type only.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.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:.envsets1, and so does the script when the variable isn't set.VORTEX_PROVISION_VERIFY_CONFIG_UNCHANGED_AFTER_UPDATE=1
Fail the provisioning whendrush updatedbchanges active configuration. The script exports the configuration before and after the updates and compares the 2 copies, sodrush config:importcan'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.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 runsdrush updatedb --no-cache-clearand then rebuilds the caches itself, so the configuration import starts from a clean state.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, newconfig_splitsettings). It's off by default, and like the import itself it needs exported configuration.VORTEX_PROVISION_SANITIZE_DB_SKIP=1
Skip database sanitization. Database sanitization explains when it runs otherwise.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 ifdrush config:statusreports any item in theDifferentstate. 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_DIRandVORTEX_DB_FILElocate the dump (./.data/db.sqlby default).VORTEX_PROVISION_DB_DIRandVORTEX_PROVISION_DB_FILEoverride them for provisioning only, andVORTEX_PROVISION_DBtakes a full path to the dump.VORTEX_DB_IMAGE, orVORTEX_PROVISION_DB_IMAGEfor 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, andDRUPAL_ADMIN_EMAILfeed the profile installation. The administrator account is always namedadmin.VORTEX_PROVISION_LOG=1saves the output asprovision.loginVORTEX_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
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%uidwith the user ID. It also accepts%mail, the original address with@changed to_, and%name, the username with spaces changed to_..envships with this value; without it, the script usesuser+%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 shipsscripts/sanitize.sqlwith 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 atVORTEX_FETCH_DB_URLftp: a file on an FTP serveracquia: a backup of an Acquia environmentlagoon: a dump of a Lagoon environmentcontainer_registry: a database container image, pulled instead of a files3: a file in an S3 bucketnone: no source; you put the dump into.datayourself
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:
- Ahoy
- Host
# Fetch latest database dump
ahoy fetch-db
# Force a fresh fetch regardless of cache
ahoy fetch-db --fresh
# Fetch latest database dump
./vendor/bin/vortex-fetch-db
# Force a fresh fetch regardless of cache
VORTEX_FETCH_DB_FRESH=1 VORTEX_FETCH_DB_FORCE=1 \
./vendor/bin/vortex-fetch-db
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_IMAGEis 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
- Docker Compose
ahoy provision
docker compose exec cli ./vendor/bin/vortex-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:
- Ahoy
- Docker Compose
# 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
# Import database only from default dump in .data/db.sql
docker compose exec cli ./vendor/bin/vortex-import-db
# Import database only from a specific dump file
docker compose exec cli \
./vendor/bin/vortex-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:
- Ahoy
- Host
# Export current database to .data directory
ahoy export-db
# Export current database to .data directory
./vendor/bin/vortex-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:
phaseselectsPHASE_PRE(before thedeploy:hookbody) orPHASE_POST(after it). A pre-phase plugin that enables modules lets the body run their run-oncehook_deploy_NAME()in the same deploy.weightorders plugins within a phase (lower runs first).skip()returnsNULLto run, or a short reason to skip that's logged verbatim. ComposeEnvironmentTraitfor theenvironment()helper to gate by environment type.run()is the idempotent work. ComposeDrushTraitfor thedrush()helper to run a long sub-command such asmigrate:importin 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 step | Module | Phase and weight | Runs in | Present when |
|---|---|---|---|---|
CreateContentModelDeployStep | ys_base | Pre, -10 | Every environment except production | The Base custom module is selected |
EnableDevelopmentModulesDeployStep | ys_base | Pre, 0 | Every environment except production | The Base custom module is selected |
MigrateContentDeployStep | ys_migrate | Post, 0 | Every environment except production | The migration feature is selected |
RebuildSearchIndex | ys_search | Post, 10 | local, ci, development, and stage | The Search custom module and the Solr service are selected |
BuildStorybookDeployStep | ys_base | Post, 20 | local, ci, and development | The 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:
- Sets the site name
- Switches the administration interface to the core Navigation module and uninstalls Toolbar
- Installs the Storybook
module, on projects with Storybook enabled. In
the
localenvironment, it also turns on Twig development mode, so component template edits show up without a cache rebuild. - Installs the contributed and service modules the CLI kept, including SDC Devel, Devel, and Reroute Email, the email safety net
- Installs the
ys_searchandys_demosite modules;ys_demobrings Testmode as a dependency - 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 type | Use case | Execution | File location |
|---|---|---|---|
hook_update_N() | Database schema changes, data migrations | drushΒ updatedb | MODULE.install |
hook_post_update_NAME() | Entity updates and other module operations | drushΒ updatedb | MODULE.post_update.php |
hook_deploy_NAME() | Environment-specific deployment tasks | drushΒ deploy:hook | MODULE.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