Drupal 7 reached end of life in January 2025, so this move is overdue but very doable. Get the framing right first: this is not an upgrade, it’s a new site that inherits your old site’s content. Content, users, and files ride the migration pipeline; the theme, Views, and layouts get rebuilt by hand. One honest deadline: the Drupal 7 migration tools live in Drupal 11 core but are deprecated and gone in Drupal 12, so migrate to 11 now rather than waiting for the shiny new major. You’ll need the D7 site copied down locally and a few working sessions — 240 minutes is the pipeline, not the rebuild.
Audit what’s worth bringing
Before you migrate a single node, run a content audit on the old site. Every content type, user role, and file you delete now is one you never debug later. A typical fifteen-year-old D7 site migrates cleaner at half its size, and nobody misses the other half.
Build the empty destination site
Fresh Drupal 11 in its own DDEV project:
mkdir mysite-d11 && cd mysite-d11
ddev config --project-type=drupal11 --docroot=web
ddev start
ddev composer create-project drupal/recommended-project
ddev drush site:install -y
Resist the urge to configure anything yet. The migration wants a blank canvas; it will create content types, fields, and vocabularies itself.
Point the migrate tools at the old database
Enable core’s migrate modules, add the contrib helpers, and load the D7 database into a second database alongside the new one:
ddev composer require drupal/migrate_upgrade drupal/migrate_plus drupal/migrate_tools
ddev drush en migrate migrate_drupal migrate_upgrade migrate_plus migrate_tools -y
ddev import-db --database=d7 --file=/path/to/d7-backup.sql.gz
Then tell the new site where that database lives, in
web/sites/default/settings.php:
$databases['migrate']['default'] = [
'database' => 'd7',
'username' => 'db',
'password' => 'db',
'host' => 'db',
'driver' => 'mysql',
];
Run the migration in passes
Generate the migrations, then run them in two passes — structure first, content second:
ddev drush migrate:upgrade --legacy-db-key=migrate --legacy-root=/path/to/old-d7-docroot --configure-only
ddev drush migrate:import --tag=Configuration
ddev drush migrate:import --tag=Content
The --legacy-root path is where the old site’s files live, so image and
attachment migrations can copy them in. Passes matter because content
migrations depend on the fields the configuration pass creates. Anything
that goes sideways can be rolled back with drush migrate:rollback and
run again — that’s the pipeline’s best feature.
Review what didn’t make it
Perfection on the first run would be a first. Check the score:
ddev drush migrate:status
ddev drush migrate:messages MIGRATION_ID
migrate:status shows imported-versus-total counts per migration;
migrate:messages explains each skipped row. Common stragglers: nodes
referencing a deleted user, files missing on disk, and fields from D7
modules with no modern equivalent. Fix the source data or accept the loss
— deliberately, in writing.
Rebuild the parts that don’t migrate
The theme, Views, Panels pages, and custom modules don’t come across — they’re rebuilt against modern Drupal. This is genuinely good news: those were the crustiest parts of the old site, and rebuilding them is where the new site earns its keep. Budget this as its own project phase, not a migration footnote.
Verify counts against the source
The finish line is arithmetic. Count on the old database and the new site:
ddev mysql d7 -e "SELECT COUNT(*) FROM node WHERE status = 1"
ddev drush sql:query "SELECT COUNT(*) FROM node_field_data WHERE status = 1"
Do the same for users and files, then spot-check ten real pages side by side against the live D7 site — body text, images, attached files, author. When the counts match your audit’s keep-list and the spot-checks read clean, the content is across and the rebuild can start on solid ground.