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.confis actually disabled andblog.confis enabled withapache2ctl -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:
- Go to Tools → Import, install the WordPress importer plugin when prompted.
- Upload the export XML file.
- 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.
- 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.