Migrating a WordPress Site to a New Domain: A Step-by-Step Guide

If you’ve ever needed to spin up a fresh WordPress install, retire an old site, and move your content across to a new domain — while keeping everything reachable during the transition — this walkthrough covers the whole process from a real migration, start to finish.

The scenario: an existing WordPress install (from a Proxmox VE community-scripts helper) needed to be decommissioned, replaced with a clean install for a new domain, and the old content imported across — all while testing everything by IP address first, before the domain was pointed at the server.

Step 1: Remove the Old WordPress Install and Database

Start by disabling the old site in Apache and clearing out its files:

a2dissite wordpress.conf
systemctl reload apache2
rm -rf /var/www/html/wordpress

Then drop the old database and its dedicated user:

mysql -u root -p
DROP DATABASE wordpress_db;
DROP USER 'wordpress'@'localhost';
FLUSH PRIVILEGES;
EXIT;

Step 2: Create a Fresh Database for the New Site

mysql -u root -p
CREATE DATABASE blog_ntg_db;
CREATE USER 'blog_ntg'@'localhost' IDENTIFIED BY 'STRONG_PASSWORD_HERE';
GRANT ALL PRIVILEGES ON blog_ntg_db.* TO 'blog_ntg'@'localhost';
FLUSH PRIVILEGES;
EXIT;

Step 3: Download and Lay Out a Fresh WordPress Install

cd /tmp
wget https://wordpress.org/latest.zip
unzip latest.zip
mv wordpress /var/www/html/blog
rm latest.zip
chown -R www-data:www-data /var/www/html/blog
find /var/www/html/blog -type d -exec chmod 755 {} \;
find /var/www/html/blog -type f -exec chmod 644 {} \;

Step 4: Configure wp-config.php

cd /var/www/html/blog
mv wp-config-sample.php wp-config.php
sed -i -e "s|^define( 'DB_NAME', '.*' );|define( 'DB_NAME', 'blog_ntg_db' );|" \
  -e "s|^define( 'DB_USER', '.*' );|define( 'DB_USER', 'blog_ntg' );|" \
  -e "s|^define( 'DB_PASSWORD', '.*' );|define( 'DB_PASSWORD', 'STRONG_PASSWORD_HERE' );|" \
  wp-config.php

Grab fresh security keys and salts and drop them into the same file:

curl -s https://api.wordpress.org/secret-key/1.1/salt/ > /tmp/salts.txt

Then replace the placeholder AUTH_KEY block in wp-config.php with the contents of /tmp/salts.txt.

Step 5: Create the New Apache Virtual Host – Reachable by IP First

The goal here is to make the new site reachable by the server’s bare IP address, so it can be set up and content imported before DNS for the real domain is pointed at it.

Apache uses the first vhost defined for a given IP:port as its fallback for any request that doesn’t match a ServerName — including plain IP requests. So as long as this is the only site enabled, browsing to the container’s IP will land here automatically.

cat <<EOF >/etc/apache2/sites-available/blog.conf
<VirtualHost *:80>
    ServerName blog.example.com
    ServerAlias www.blog.example.com
    DocumentRoot /var/www/html/blog
    <Directory /var/www/html/blog>
        AllowOverride All
        Options -Indexes
    </Directory>
    ErrorLog \${APACHE_LOG_DIR}/blog-error.log
    CustomLog \${APACHE_LOG_DIR}/blog-access.log combined
</VirtualHost>
EOF

a2ensite blog.conf
a2dissite 000-default.conf
a2enmod rewrite
apache2ctl configtest
systemctl reload apache2

Browsing to the server’s IP should now load the WordPress installer instead of Apache’s default “It works!” placeholder page.

If you still see the Apache default page: confirm 000-default.conf is actually disabled and blog.conf is enabled with apache2ctl -S, which lists every active vhost and where its config came from.

Step 6: Run the WordPress Installer

Browse to http://<server-ip>/ and complete the standard five-minute install. WordPress will record siteurl/home as the IP address for now — that’s expected, and gets corrected later once the domain is live.

Step 7: Import Content from the Old Site

Export your content from the old site (Tools → Export) if you haven’t already, then on the new site:

  1. Go to Tools → Import, install the WordPress importer plugin when prompted.
  2. Upload the export XML file.
  3. On the import options screen:
  • Import author — leave this mapped to your admin account, so imported content is editable right away.
  • Import Attachments — check this only if you want WordPress to fetch media files itself over HTTP from the old site. If you’re planning to copy the uploads folder manually instead, leave this unchecked.
  • Content Options (“Change all imported URLs…”) — leave this checked. It rewrites internal URLs in the imported content from the old site to the new one, which is essential since the old site is being retired.
  1. Click Submit and let it run — larger exports can take a while.

Step 8: Copy Media Files Manually

If attachments weren’t downloaded during import, the files still need to end up in the right place. Copy the entire uploads folder from the old server, preserving its year/month folder structure:

scp -r root@<old-server-ip>:/var/www/html/wordpress/wp-content/uploads/ /var/www/html/blog/wp-content/
chown -R www-data:www-data /var/www/html/blog/wp-content/uploads
find /var/www/html/blog/wp-content/uploads -type d -exec chmod 755 {} \;
find /var/www/html/blog/wp-content/uploads -type f -exec chmod 644 {} \;

As long as the folder structure matches what’s referenced in the imported post content, images will render on the site immediately — even before they show up in the Media Library.

Step 9: Populate the Media Library

Copying files into uploads/ makes them render on the front end, but doesn’t create Media Library entries on its own. To populate the Media Library grid, install WP-CLI and bulk-import the existing files:

curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
chmod +x wp-cli.phar
mv wp-cli.phar /usr/local/bin/wp
cd /var/www/html/blog
find wp-content/uploads -type f \( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" -o -iname "*.gif" -o -iname "*.webp" \) -exec wp media import {} --skip-copy --allow-root \;

The --skip-copy flag tells WP-CLI the files are already in the right place, so it just creates database records pointing at them rather than copying them again.

Step 10: Cut Over to the Domain

Once DNS for the new domain resolves to the server, confirm it with:

dig +short blog.example.com

Then update every reference to the IP address across the database. wp search-replace is the right tool here rather than a raw SQL update, because it correctly handles PHP-serialized data — a plain UPDATE can silently corrupt serialized strings if their byte-length changes.

cd /var/www/html/blog
wp search-replace 'http://<server-ip>' 'https://blog.example.com' --allow-root
wp option update siteurl 'https://blog.example.com' --allow-root
wp option update home 'https://blog.example.com' --allow-root

The two wp option update commands act as a backstop for those specific settings, in case anything didn’t get swept up by the search-replace.

Step 11: Optional Cleanup – Restore the Default Vhost

Once the domain is live and working, you can re-enable a proper default Apache vhost so that bare IP requests no longer serve the blog directly:

a2ensite 000-default.conf
systemctl reload apache2

And that’s the whole migration — from decommissioning the old install through to a fresh site running on its own domain with HTTPS. The main things worth double-checking along the way are the uploads folder structure (it has to match what’s referenced in post content) and running search-replace rather than manual SQL edits when swapping URLs, since it’s the only safe way to handle WordPress’s serialized data.

Leave a Reply

Your email address will not be published. Required fields are marked *