Skip to content

Powered by Grav

Migrating a Drupal 7 site to Drupal CMS

Migrating a Drupal 7 site to Drupal CMS

Your BOA server is the ideal migration rig, because it hosts both ends at once: the Drupal 7 site that keeps serving your visitors, and — right next to it, in the same Octopus instance — a fresh Drupal CMS site that Drupal's own migration engine fills with the Drupal 7 site's content, files and users. You rehearse the run as many times as you like on disposable copies, and when the result is right you swap the domain over. The live site is never in the write path.

Two different "migrates" on this page. Ægir's Migrate Site task moves a site between platforms or renames its domain — it never changes content. Drupal's Migrate API — the engine behind core's /upgrade tool — reads a Drupal 7 database and rebuilds that content inside a modern Drupal site. You use the Migrate API for the crossing, and the Migrate Site task at the very end, purely as a rename.

What crosses, what doesn't. Content, files, taxonomy and users cross. Your Drupal 7 theme, custom modules and views don't — core ships no Drupal 7 Views migration, so listings built in Views are rebuilt on the target — and the front end is a fresh start on Drupal CMS's toolset, a project of its own. What this page gives you is the safe, repeatable machinery around the crossing.

Step 1 — Make sure the Drupal CMS platform is there

Drupal CMS ships in BOA's distribution catalogue, but platforms only install when asked for, and the instance ships with an empty platform list. So first look for the version-numbered Drupal CMS entry in the Platform picker of the create-site form.

If it isn't there, tell the instance which platform symbols it should carry. That lives in the instance's own control folder rather than in the config file. List every symbol the instance should carry, upper-case and separated by spaces or newlines — the file replaces the default list rather than adding to it, so naming only CMS would leave the Drupal 7 platforms your source site sits on out of future upgrade passes:

TXT
echo "DL7 CMS" > /data/disk/<user>/static/control/platforms.info

The keyword ALL is the never-skip option if you would rather not maintain the list by hand. Then run your normal upgrade pair so the platform installs:

TXT
barracuda up-lts system
octopus up-lts o1

(up-pro on PRO instances — whichever tree the server already follows.)

The cheat sheet in ~/static/control/README.txt on every instance lists the full symbol set.

The platform is rebuilt from the upstream Composer template as BOA releases ship, so it tracks the version the Drupal Association currently publishes.

Both ends must live in the same Octopus instance. Each instance is a deliberate isolation boundary — a site's PHP can only read inside its own instance's tree — so a Drupal CMS target on o1 cannot import from a Drupal 7 site on o2. If your Drupal 7 site lives in another instance (or on another server), bring a copy into the target's instance first: restore it there from a backup, or register its codebase and import the site the usual Ægir way.

Step 2 — Install the target site

In your Ægir panel, create a new site on the Drupal CMS platform:

  • Domain: use a dev.-prefixed rehearsal name — dev.newsite.example.com. Self-identifying crawlers and bots get a 404 on dev. names, so search engines never see your half-migrated rehearsals — and unlike deeper test labels, a first-label dev. name can still get a real HTTPS certificate if you enable HTTPS on it. The production name comes later, by rename.
  • Install profile: pick the Drupal CMS profile, drupal_cms_installer — the distribution installs itself around it.

The install runs unattended and lands on a deliberately plain front page — on BOA the panel installs the complete Drupal CMS package headlessly, so the interactive setup wizard you may have seen elsewhere never runs. A plain front page is a healthy install; log in and the full toolset is there.

Step 3 — Switch on the core migration modules

Drupal's upgrade tool ships in core, disabled. On the target site, log in as the site admin (the Log in button on the site's panel page gets you there), open Extend (/admin/modules) and under the Migration group enable:

  • Migrate
  • Migrate Drupal
  • Migrate Drupal UI

Two things you'll see are normal. The last two are labelled (Deprecated) — upstream's label, meaning they're headed for contrib in some future major release; today they are the supported Drupal 7 upgrade path, and core asks you to confirm installing deprecated modules — continue. The confirmation also adds Password Compatibility for you: it lets imported users keep logging in with their existing Drupal 7 passwords.

One more decision on this page: any content feature the Drupal 7 site uses must be enabled here before you migrate, or its data is skipped. The headline example is Comment — Drupal CMS deliberately ships without it, so a Drupal 7 site with comments needs Comment enabled now. The upgrade's review screen (Step 5) shows exactly which of the old site's modules have nowhere to land, so you don't need the list perfect on the first pass.

The same rule covers languages: a multilingual Drupal 7 site needs Language and Content Translation enabled here first — without them the upgrade derives no language migrations at all, and the language setup is silently skipped rather than reported.

One last piece of target preparation: clear out the starter content. Open Content (/admin/content) and delete the content items a fresh Drupal CMS ships with (there's a draft Privacy policy page). Imported Drupal 7 content keeps its original ID numbers, and an item whose ID is already taken by an existing content item fails to import rather than replacing it — silently costing you your oldest content. An empty content list means every ID is free. (The Home page is unaffected — it isn't a content item.)

If you enable more modules after clearing the list (Comment included), open Content once more right before running the upgrade — installing modules on Drupal CMS can re-create starter items, and a resurrected item's ID collides exactly like the originals did.

The upgrade tool is now live at /upgrade on the target site.

Step 4 — Prepare the source and collect its coordinates

The upgrade form needs the Drupal 7 site's database credentials and its platform's document root. As root, both are one look away — the credentials live in the site's drushrc.php, not in settings.php, which only names them (see Database credentials):

SH
grep "db_" \
  /data/disk/o1/distro/002/drupal-7.105.2-prod/sites/www.example.com/drushrc.php

Adjust the instance (o1) and platform directory to yours — the site's Drupal root as shown by drush @www.example.com status run as the instance user. Note the root path exactly: the upgrade form wants this real absolute path, because it's the target site's PHP that reads the source files during the import.

Then one preparation command, and it's the step everyone would otherwise lose an afternoon to. Ægir keeps a site's files location in settings.php rather than in the Drupal 7 database — but the upgrade tool reads the database — so without help it assumes the stock sites/default/files and quietly imports no files at all. Mirror the real value into the database (as the instance user):

SH
drush @www.example.com vset file_public_path sites/www.example.com/files

This is safe on the live site: settings.php keeps overriding the setting at runtime, so behaviour doesn't change — the value just becomes visible to the migration. If the site uses private files, mirror file_private_path the same way.

Step 5 — Run the upgrade

On the target site, open /upgrade and follow the wizard:

  1. Version — pick 7.
  2. Source database — the credentials from Step 4: database name, username, password, host and port exactly as drushrc.php shows them.
  3. Files — as Document root for public files, give the source platform's absolute root path. With the file_public_path mirror from Step 4 in place, the importer resolves each upload's real location under that root and copies it across.
  4. The conflict warning. Before the report, the tool warns that content may be overwritten and asks you to press "I acknowledge I may lose data. Continue anyway." It fires because the target isn't a bare Drupal — Drupal CMS ships starter menu links and files, and the import replaces the ones whose ID numbers collide with incoming items. On a rehearsal target that's exactly what you want — you cleared the content items in Step 3, and the remaining starter menu links and files are placeholders. Acknowledge and continue. (This is also why you migrate into a fresh site, never one you've started building by hand.)
  5. Review the "What will be upgraded?" report. Two tables: the old site's modules that will be upgraded, and those that won't — modules with no counterpart installed on the new site, whose configuration and content would be skipped. If something you care about is in the wrong table (remember Comment from Step 3), go back to Extend, enable the counterpart, and re-enter the wizard — the report updates to match. When the tables look right, press Perform upgrade.
  6. Watch it run. Configuration imports first, then content. Minutes for a modest site; large media libraries take longer.

Step 6 — Review what landed

The batch drops you back on the target, and the first impression is delightfully strange: the site now believes it's your Drupal 7 site — its name, slogan and settings crossed over — while wearing Drupal CMS's theme. Walk it as an editor and as a visitor: content in place, images attached, menus sane, users present (old passwords keep working). Classic artifacts to expect and fix:

  • The front page may 404. Drupal 7's default front page is the /node listing, and that setting migrates — but the target has no /node page. Point the front page somewhere real under Configuration → Basic site settings.
  • Basic pages may arrive unpublished. A content type that already exists on the target — Basic page being the everyday case — receives imported items through Drupal CMS's editorial workflow, and they can land as drafts; types recreated from the source (a Drupal 7 Article) keep their published state. Review Content and publish what should be live.
  • A few broken block placements. Block positions migrate from a theme the target doesn't have, so odd corners may show "This block is broken or missing" — remove or re-place them under Structure → Block layout.
  • Views listings are absent. Drupal 7 views don't migrate — core ships no Views migration — so any page or block a view provided isn't there and its path answers 404. Rebuild the listings you keep with Drupal CMS's own tools, and note the changed paths: they're the short redirect list for cutover.
  • Roles and permissions cross, with one ordering rule. Users arrive with their roles, and a role's permissions carry for every module enabled on the target when the migration runs — a counterpart enabled later (the Comment loop-back) does not retro-fill already-imported roles. A fresh rehearsal target fixes this by itself; mid-iteration, the command-line lane refreshes roles (below).
  • Translation sets merge into one item. A Drupal 7 translation pair arrives as one content item with a translation per language (the item's Translate tab), keeping published state; language-neutral content lands with its language Not specified.
  • Files stay files. Uploads cross as file and image fields, references and alt text intact — nothing becomes a Drupal CMS Media entity by itself; adopting Media for old content is optional, later editorial work.
  • The review tables told the truth. Anything the report listed as not upgraded really isn't there — your worklist for the next rehearsal.

A few more checks worth the minutes. Compare the Content and People counts on both sites (the command-line lane below prints per-migration row counts); check your forms and roles — Contact and Webform are separate modules, so open the target's own contact-form and webform screens and look; a form that did not cross is rebuilt — and re-point anything that talks to an outside system deliberately, so a rehearsal never talks to a production endpoint before you mean it to. Hand the copy to your editors for real tasks; anything they type into a rehearsal copy is thrown away by the final run.

Step 7 — Iterate on disposable targets

One rule shapes the rehearsal loop, and the tool states it itself if you revisit /upgrade: an upgrade runs once per site. There's no re-running it on the same target to top up or retry — the UI refuses, and rollbacks aren't supported through it. That's what the panel's cheap site tasks are for:

  • Iterate by replacing the target. Adjust whatever the last run taught you, run Delete Site on the target, repeat Steps 2–5 on a fresh one, and compare. Every rehearsal is as clean as the first, and the Drupal 7 site serves visitors untouched throughout.
  • Freeze the source if you want runs comparable. The importer reads the source's live database. For a fixed reference, Clone the Drupal 7 site to a dev. name and point the upgrade form at the clone's credentials (its own drushrc.php — a clone gets its own database name and user; same platform root — and run the Step 4 vset against the clone). Clones land with cron off by design, so the copy holds perfectly still between runs.

When are you done rehearsing? When the last run needed no fix you have not written down, the review tables list nothing you still want to keep, the walk found only the known artifacts, every integration is re-pointed, and the short redirect list is written. Nothing here prescribes how many rehearsals that takes.

Step 8 — Cut over by rename

Because the migration runs once per target, the final run is the cutover build: when a rehearsal has proven the process end to end, do one last fresh run at a quiet moment — content editing on the Drupal 7 site paused briefly — and that target becomes the site. The swap is two renames with Ægir's Migrate Site task — a domain can only be held by one site at a time, so free the name first:

  1. Rename the Drupal 7 site out of the way: Migrate Site, change only the Domain nameold.example.com. It stays installed and serving there as your fallback and archive.
  2. Rename the target into place: Migrate Site on the Drupal CMS site, change only the Domain name, to the production domain.
  3. Re-enable HTTPS for the renamed site — a rename always switches HTTPS off, because the certificate belonged to the old name. Turn it back on and a fresh certificate is issued for the production domain.
  4. Enable the new site's scheduled cron, and disable it on the retired Drupal 7 site.

Keep the retired Drupal 7 site at its old. name until you're certain, then take a final backup and delete it at your leisure.

The clock on cutover day. The final run takes minutes on a modest site; each rename takes the site briefly offline — a few seconds to a few minutes depending on how big the site is — and the domain stops serving from the moment the first rename starts until the second one finishes, so run them back to back. There is no freeze switch: tell your editors the hour, or pick one nobody works — an edit made after the final run started is not in the new site, but the retired site still holds it. Afterwards, the old site's sitemap or a crawl of it is the URL list to test against the new site; watch the search consoles and your traffic pages for a few weeks, and retire the old site when you would not roll back anyway.

The SEO once-over after the swap. Because the cutover is a rename, the domain is unchanged and the migrated aliases and pathauto patterns keep the URL space intact — no every-URL redirect map is needed. What's left: set up redirects for the few paths that genuinely changed (views pages, routes of modules that didn't cross, Drupal 7's /node front page), publish and resubmit an XML sitemap if the old site had one, and spot-check titles and meta descriptions on the most-visited pages against the old site. The renamed site is served by the same stack and caching that served the Drupal 7 site — no performance re-plan needed.

The command-line lane (advanced)

The wizard is the everyday path, but the Drupal CMS platform also ships Drupal's full migration pipeline as code — migrate_plus, migrate_tools and migrate_upgrade sit in the platform's codebase, switched off — so the whole crossing can be driven from the shell instead: scriptable, repeatable, and re-runnable per migration. The commands run through the site's own bundled Drush: run the Unlock Local Drush task on the Drupal CMS platform first (Ægir keeps vendor/drush locked read-only between uses), connect as the instance's .ftp user, cd to the platform root (the directory holding vendor/) as its own command — the limited shell refuses cd ... && vdrush ... one-liners — and re-lock with Lock Local Drush when done.

This lane is also where the Migration source task earns its keep: run it on the target site, pick the Drupal 7 source, and the source database arrives as a named migrate connection — read-only, enforced by the database server, with no credentials collected or pasted anywhere. The task is switched on automatically by every Octopus install and upgrade: Migration source wiring covers exactly what it grants, what bounds it, and the manual enabling pair for an instance that predates the automatic path. It wires the database, not the files, so the document root and the Step 4 file_public_path preparation stay yours:

SH
vdrush @dev.newsite.example.com pm:install migrate migrate_drupal migrate_plus migrate_tools migrate_upgrade
vdrush @dev.newsite.example.com migrate:upgrade --legacy-db-key=migrate --legacy-root=/data/disk/o1/distro/002/drupal-7.105.2-prod --configure-only
vdrush @dev.newsite.example.com migrate:status --group=migrate_drupal_7
vdrush @dev.newsite.example.com migrate:import --group=migrate_drupal_7 --continue-on-failure

Without the task, the same command takes the Step 4 credentials directly, with the host and port exactly as drushrc.php shows them: --legacy-db-url="mysql://DBUSER:DBPASS@DBHOST:DBPORT/DBNAME" in place of --legacy-db-key=migrate.

Migrate Drupal UI isn't needed in this lane; the Step 4 file_public_path preparation applies unchanged; and for a site created moments ago whose alias drush11 aliases doesn't list yet, replace the alias with --root=<platform root>/web --uri=dev.newsite.example.com. --configure-only is this lane's review screen — it derives one migration per piece of the old site without importing, and migrate:status shows the set with row counts read straight from the Drupal 7 database. Always import with --continue-on-failure: without it, one failed row halts the whole group.

Two behaviours look like breakage and have routine fixes. A missing counterpart can stop the run mid-group — the classic case is a Drupal 7 site with comments crossing into comment-less Drupal CMS: the import aborts ("Plugin ID 'd7_comment_type' was not found") and the interrupted migration is left showing Importing, which blocks the next run. Enable the counterpart (pm:install comment), re-run the migrate:upgrade --configure-only command to re-derive the set, reset the stuck migration (migrate:reset-status upgrade_d7_field_instance, or whichever the status table shows stuck), and import again. And partial counts are the report, not a failure — Drupal 7 actions, blocks and theme settings whose destination doesn't exist on Drupal CMS fail or are skipped row by row; read migrate:messages upgrade_<name> for anything surprising. It's the content migrations — nodes, users, terms, files, aliases — that should reach their totals.

The late-counterpart rule from Step 6 has a lane-specific fix here: if a module was enabled only after the roles imported, refresh them with migrate:rollback upgrade_d7_user_role followed by migrate:import upgrade_d7_user_role — the re-import brings in the permissions that now have a provider.

Unlike the wizard, this lane isn't once-per-site: re-running migrate:import picks up unprocessed rows and leaves imported migrations alone, so an interrupted run simply continues. Step 6's review list applies to this lane unchanged.

The other exit from Drupal 7

The Migrate-API road rebuilds the site around your content. If what you want instead is continuity — same structure, same theme lineage, no rebuild — BOA's panel also ships the Drupal 7 → Backdrop upgrade, which converts a copy of the site wholesale. Both exits stay open; nothing on this page commits you to either.

© 2026 BOA Documentation. All rights reserved.