How to Migrate a WordPress Site With a Custom Theme Safely

How to Migrate a WordPress Site With a Custom Theme Safely

To migrate a WordPress site with a custom theme safely, you move the database and the files as two separate objects, then make the new host serve the custom theme from a location that your deployment process cannot overwrite. The danger is never the copy itself. The danger is that a managed platform, a one click installer, or a theme update routine decides your custom theme is an outdated copy of something and replaces it.

So the whole job reduces to three questions. Does the code live in version control or only on the server? Does anything on the destination run a theme update that could clobber it? And does the database still point at the old domain? Answer those before you touch a file and the migration becomes boring, which is what you want.

Understand what a custom theme actually is on disk

A theme is a directory under wp-content/themes/ containing at minimum a style.css with a header comment. WordPress parses that header to register the theme. The header is not decoration, it is the identity record, and it is what the updater matches against a remote repository.

/*
Theme Name: Acme Child
Template:   twentytwentyfour
Version:    1.4.2
*/

The Template: line is the important one. If it is present, this is a child theme and WordPress loads the parent as a fallback for any file the child does not provide. If it is absent, this is a standalone theme and it must contain every template it needs. Get this wrong and you get a broken layout with no error message, because WordPress will happily activate a theme whose parent is missing and then render almost nothing.

A theme that was customised in place, by editing a copy of a stock theme, is the worst case. It has the stock theme's name and version in its header, so a theme update check can decide it is out of date and replace the directory. Before you migrate, edit that header. Change the theme name and give it a version number that no remote repository will ever match. That single edit is what stops an automatic update from deleting your work.

Move the files without breaking the theme

Copy the whole wp-content tree, not just the theme directory. Uploads, must use plugins, and any custom mu plugin live alongside it. A theme that calls a function defined in wp-content/mu-plugins/ will fatal on the new host if you left that directory behind.

Archive locally, transfer, and unpack on the destination. Keeping the archive intact means permissions and line endings survive the trip, which matters if any file was edited on Windows.

tar czf site-files.tgz wp-content
scp site-files.tgz user@newhost:/var/www/example/
ssh user@newhost 'cd /var/www/example && tar xzf site-files.tgz'

Then verify the theme is present and readable before you go near the database. The command below prints the resolved path of the active theme, which is the fastest way to confirm the destination sees the same directory the source did.

wp theme list --status=active --fields=name,status,version

If that output names a theme you do not recognise, stop. Something on the destination installed a theme of its own and may have set it active. Fix that before importing the database, otherwise the import will simply reactivate whatever the old site had and you will be back to debugging a blank page.

Set the theme directory read only

Once the files are in place, make the theme directory unwritable by the web user. This is the mechanical guarantee that no update, no plugin, and no installer can modify your custom code. It costs you the ability to edit themes from the admin dashboard, which you should not be doing on a production site anyway.

chown -R deploy:www-data /var/www/example/wp-content/themes/acme-child
find /var/www/example/wp-content/themes/acme-child -type d -exec chmod 755 {} \;
find /var/www/example/wp-content/themes/acme-child -type f -exec chmod 644 {} \;

Ownership by a deploy user and group read access for the web server is the arrangement that works. If the web user owns the files, PHP can rewrite them, and an exploited plugin can then persist code in your theme. If the web user cannot read them at all, the site breaks. Test by loading the front page immediately after changing permissions, not an hour later.

Bring the database across and fix the URLs

Dump the source database and import it on the destination. Use a single transaction so a half finished import does not leave you with a site that is partly old and partly new.

mysqldump --single-transaction --default-character-set=utf8mb4 \
  -u user -p dbname > site-db.sql

WordPress stores absolute URLs in many places, not just the siteurl and home options. Widget settings, custom fields, serialised arrays inside wp_options, and any page builder content all carry the old hostname. A naive search and replace on the SQL file will corrupt serialised data, because PHP serialisation records string lengths and changing a URL without updating those lengths produces an array that will not unserialise.

Use a tool that understands serialisation, or run the replacement after import with WP CLI, which handles it correctly. Set the two core options first, then search the rest.

wp option update home 'https://example.com'
wp option update siteurl 'https://example.com'
wp search-replace 'old.example.com' 'example.com' --all-tables --precise

The --precise flag is what makes the serialised data safe. Without it you are doing a raw string swap and you will find out about the damage later, usually as a widget that renders nothing.

Check the theme mods survived

Customiser settings for a custom theme live in the theme_mods_ option, keyed by the theme's directory name. If you renamed the directory during the migration, that option no longer matches and every customiser setting silently resets to default. The fix is to rename the option to match the new directory slug, or to keep the directory name identical between hosts. Keeping it identical is simpler and is the reason you should not tidy up directory names as part of a migration.

If the theme stores its own settings in a separate option, check that too. Anything registered with a prefix tied to the old theme slug will need the same treatment.

Verify before you cut over DNS

Test the destination using a hosts file entry rather than by changing public DNS. That lets you browse the migrated site on the real hostname while the rest of the world still sees the old one, and it lets you roll back by deleting one line.

curl -sI https://example.com/ | head -n 5

Check the response headers for a redirect loop, and load a page that exercises the theme's templates rather than just the home page. A custom theme often has a page template, a custom post type archive, and a single template that each fail independently. Visit one of each. Then check the browser console for assets still being requested from the old hostname, which is the usual sign that a hardcoded path survived the search and replace.

When the destination behaves identically to the source, lower the TTL on the DNS record well in advance, switch it, and keep the old host running read only for a while so you can compare if something looks wrong.

Next, put the theme in version control if it is not already there, and deploy it as part of your release process rather than by editing files on a server. A migration is a good moment to fix that, because you have just proved to yourself that the files only exist in one place. Treat the database as the mutable state and the theme as immutable code, and the next move will take minutes instead of an afternoon.

Related articles

Subscribe to our newsletter

Get the latest hosting tips, performance insights, and industry news.