The Pre-Deploy Checklist
Five checks worth doing before the maintenance window: schema, patches, indexer modes, cron, and a rollback you have actually run.
bin/magento setup:db:status && bin/magento indexer:show-modeReleases fail in the same few places. Check them on staging with the exact artifact you are about to ship, not during the window on production.
The artifact
Schema validated. Every module whose etc/db_schema.xml changed needs its etc/db_schema_whitelist.json regenerated in the same commit; the whitelist is the record of what declarative schema is allowed to touch. After applying the release to a copy of production data, bin/magento setup:db:status should print "All modules are up to date." and exit 0. Patches reviewed. A data patch runs once, is recorded in the patch_list table, and never runs again. Read each one against real row counts, and check whether revert() can genuinely undo it.
The server
Indexers in schedule mode. bin/magento indexer:show-mode should report Update by Schedule, so a product save writes a changelog row instead of reindexing inside the request. Cron running. Update by Schedule is a promise cron has to keep; a dead cron means stale prices, unprocessed changelogs and no queue consumers. Check the crontab of the file system owner, and check that jobs are completing rather than that the line exists.
The way back
setup:upgrade also validates the configured search engine, so an unreachable OpenSearch fails the release mid-window. Nothing it applies rolls itself back: a rollback is the previous release directory plus a database dump taken inside the window, and it only counts if you have restored one and timed it.
Example
# staging, with the exact artifact that is going to production
bin/magento setup:db:status
# All modules are up to date.
bin/magento indexer:show-mode
# Design Config Grid: Update by Schedule
# Customer Grid: Update on Save
# Product Price: Update by Schedule
# Category Products: Update by Schedule
bin/magento queue:consumers:list | head -3
# the release itself, timed so you know how long the window has to be
time bin/magento setup:upgrade --keep-generated
# ...
# Upgrade completed successfully.When to use it
- Agreeing a maintenance window length with a client from a measured number.
- Catching a db_schema.xml change that was committed without its whitelist file.
- Confirming a production box is not quietly reindexing inside customer requests.
- Rehearsing the rollback before the release rather than during it.
More examples
Indexer modes and a cron that is really alive
indexer:set-mode schedule with no argument moves every indexer to Update by Schedule, which is what a busy store wants. The customer grid indexer is the documented exception: it has no changelog of its own and should stay on Update on Save, otherwise the admin customer grid stops reflecting new customers. A crontab line proves nothing by itself, so read the log as well; schedule mode with no cron means data that silently never updates.
bin/magento indexer:set-mode schedule
bin/magento indexer:set-mode realtime customer_grid
# the crontab belongs to the file system owner, not to root
crontab -l -u www-data | grep 'bin/magento cron:run'
# and jobs must be finishing, not just scheduled
tail -n 20 var/log/magento.cron.logProve the whitelist was regenerated
db_schema_whitelist.json lists the tables, columns and keys that declarative schema owns for that module, and it is what allows a later release to drop them. Regenerate it in the same commit as the db_schema.xml change, or part of your change is ignored at deploy time with no error. Passing --module-name=all regenerates every module, which produces a noisy diff; name the module instead.
# which modules changed their schema in this release?
git diff --name-only origin/main...HEAD | grep 'etc/db_schema.xml'
# app/code/Vendor/Module/etc/db_schema.xml
bin/magento setup:db-declaration:generate-whitelist --module-name=Vendor_Module
git status --short app/code/Vendor/Module/etc/db_schema_whitelist.jsonA data patch you can review in thirty seconds
Reviewing a patch means answering two questions: how many rows does apply() touch, and what does revert() actually restore. This one writes a single UPDATE, so it is safe on a large table, but its revert is honest about restoring nothing, which tells you the rollback plan has to be the database dump. The constructor here uses promoted properties, which is shorthand for declaring 'private readonly ModuleDataSetupInterface $moduleDataSetup;' in the class body and assigning it in the constructor; both forms compile to the same class.
<?php
// app/code/Vendor/Module/Setup/Patch/Data/BackfillThingStatus.php
declare(strict_types=1);
namespace Vendor\Module\Setup\Patch\Data;
use Magento\Framework\Setup\ModuleDataSetupInterface;
use Magento\Framework\Setup\Patch\DataPatchInterface;
use Magento\Framework\Setup\Patch\PatchRevertableInterface;
class BackfillThingStatus implements DataPatchInterface, PatchRevertableInterface
{
public function __construct(
private readonly ModuleDataSetupInterface $moduleDataSetup
) {
}
public function apply()
{
$connection = $this->moduleDataSetup->getConnection();
$table = $this->moduleDataSetup->getTable('vendor_module_thing');
$connection->update($table, ['status' => 1], ['status IS NULL']);
return $this;
}
public function revert(): void
{
// nothing to undo: the previous value was NULL and is not recoverable
}
public static function getDependencies(): array
{
return [];
}
public function getAliases(): array
{
return [];
}
}
Discussion