Skip to content
Linux Running · 7 min read

Server Migration: Build a Full Test Copy Before You Touch DNS

Moving a forum, WordPress, Nextcloud and mail off a dead Plesk box: pull with ssh -A, test via hosts file, doveadm imapc for mail, then the delta cutover.

I had two projects left on an old Plesk box with a dead license. One is a community forum I run on XenForo. The other is a small client with WordPress, a Nextcloud and three mailboxes. Both had to move to my new server with KeyHelp.

I didn't want a "move it and pray" weekend. So I built a complete copy on the new box first, tested it from my laptop with a hosts file, and wrote down the cutover steps. The copy is done and tested. The DNS switch is still waiting on a domain transfer, so this post ends with the checklist, not with "it went fine".

TL;DR: Pull everything from the new server with ssh -A, so nothing gets written on the old box. Files with rsync, databases with a mysqldump | mysql pipe, mail with Dovecot's doveadm and the imapc backend. Import the old Let's Encrypt certs so HTTPS works in the test. Test with hosts file entries. Nextcloud's occ won't start with a broken DB config, so fix config.php with a small PHP script. For the cutover: maintenance mode, final delta, DNS, cron, stop Postfix on the old box.

The layout on the new box

One KeyHelp client account per project: one for the forum, one for the client. Inside each account one database per app: WordPress, Nextcloud, XenForo. Separate accounts mean separate system users, so the forum can't read the client's files and the other way round. More about the panel in KeyHelp as a free Plesk alternative.

The old box still serves everything. The Plesk CLI refuses to work without a license, but SSH, the web server, MariaDB and mail keep running. That's enough to get data out. What still works in that state is in Plesk license expired: what still works.

Pull, don't push

# on my laptop
ssh -A root@203.0.113.20          # new box, agent forwarding on

# on the new box, pulling from the old one
rsync -aH --info=progress2 \
  root@203.0.113.10:/var/www/vhosts/forum.example.com/httpdocs/ \
  /home/users/forum/www/forum.example.com/

-A forwards my SSH agent. The new box can log in to the old box with the key on my laptop, and no private key ever lands on either server. Everything runs from the new side, so the old box only gets read. If the copy goes wrong, I delete it on the new box and start over. The old box doesn't care.

After rsync, fix the owner to the KeyHelp account's system user. Files copied as root and owned by root give you 403s and broken uploads.

Databases with a pipe

ssh root@203.0.113.10 \
  'mysqldump -uadmin -p"$(cat /etc/psa/.psa.shadow)" --single-transaction forumdb' \
  | mysql -u forum_xf -p forum_xf

The dump streams straight into the new database. No dump file on the old disk, no copy step. --single-transaction gives a consistent snapshot for InnoDB tables without locking the site.

Then point each app at its new database. For XenForo that's src/config.php, for WordPress wp-config.php. Nextcloud is where it got annoying.

Gotcha: occ can't fix the config that stops occ

The normal way to change Nextcloud settings is occ config:system:set. But occ boots Nextcloud first, and booting means connecting to the database from config.php. The copied config still had the old DB host, name, user and password. So occ crashed before it could do anything. You can't use the tool to fix the thing that stops the tool from starting.

The fix: edit config.php with a tiny PHP script. It loads the file, changes the values and writes it back.

<?php
// fix-nc-config.php, run with: php fix-nc-config.php
$file = '/home/users/client/www/cloud.client.example.com/config/config.php';
require $file;   // defines $CONFIG

$CONFIG['dbhost']        = 'localhost';
$CONFIG['dbname']        = 'client_nc';
$CONFIG['dbuser']        = 'client_nc';
$CONFIG['dbpassword']    = 'new-db-password';
$CONFIG['datadirectory'] = '/home/users/client/nextcloud-data';
$CONFIG['trusted_domains'] = ['cloud.client.example.com'];

file_put_contents($file, "<?php\n\$CONFIG = " . var_export($CONFIG, true) . ";\n");

Run it as the account's user, or fix the owner afterwards. Keep a copy of the original config.php next to it. datadirectory matters as much as the DB values. If it still points to the old path, Nextcloud starts but can't find any files. After that occ worked again.

I keep this script. It has to run again after the final sync, because the final rsync brings the old config.php back.

Mail with doveadm and imapc

My last mail move used imapsync in Docker, see moving mailboxes with imapsync. There's no imapsync package in Ubuntu 24.04. This time I skipped Docker. The new box already runs Dovecot, and doveadm can pull from any IMAP server with its imapc backend.

General shape, per mailbox:

doveadm -o imapc_host=203.0.113.10 \
        -o imapc_port=993 \
        -o imapc_ssl=imaps \
        -o imapc_ssl_verify=no \
        -o imapc_user=office@client.example.com \
        -o imapc_password='old-password' \
        -o mail_prefetch_count=20 \
        sync -1 -R -u office@client.example.com imapc:

-u is the local mailbox on the new server. imapc: is the remote side, configured by the -o options. -R reverses the direction, so it pulls from the remote into the local mailbox. The exact option names changed between Dovecot versions, so check doveadm --version and the docs for your version. I use sync -1, a one-way sync that only adds what's missing and doesn't delete anything on the target. That's why I can run it again at cutover for the delta. backup would make the target an exact copy and wipe anything extra. imapc_ssl_verify=no is only there because the old box's cert didn't match the IP I connect to. If you connect by hostname with a valid cert, leave it on.

If the old server's cert doesn't match the name or IP you connect to, the SSL check fails. Connect by the hostname the cert was issued for.

Afterwards I compared message counts per folder on both sides. They matched exactly, plus one on the new side. That one is the welcome mail KeyHelp drops into every new mailbox. Good to know before you panic about a mystery message.

doveadm mailbox status -u office@client.example.com messages '*'

Bring the old certificates along

Testing via hosts file only works over HTTPS if the new server has a valid cert for the name. Let's Encrypt can't issue one yet, because the domain still points to the old box. So I copied the existing certs and keys from the old server and imported them into KeyHelp as custom certificates.

Now the browser sees a valid cert for forum.example.com on the new IP. No warnings, no "accept risk" clicking, and you test the real thing including redirects and HSTS. After the DNS switch I replace them with KeyHelp's own Let's Encrypt certs. The old ones expire anyway.

Test through the hosts file

On my Windows laptop, C:\Windows\System32\drivers\etc\hosts, edited as admin:

203.0.113.20  forum.example.com
203.0.113.20  client.example.com
203.0.113.20  www.client.example.com
203.0.113.20  cloud.client.example.com
203.0.113.20  mail.client.example.com

Then ipconfig /flushdns and close the browser fully, because browsers keep their own DNS cache. Only my laptop sees the new server. Everyone else still uses the old one.

What I tested:

  • forum: browse, log in, open threads, check attachments and avatars load
  • WordPress: front page, login, admin
  • Nextcloud: login, files there, open a few
  • mail: log in to a mailbox on the new server, check folders, send a test mail

When you're done, comment the lines out. Forgetting a hosts entry is a fun way to lose an hour after the real switch.

The cutover checklist

This part hasn't run yet. It's what I'll do once the domain transfer is through.

Days before

  1. Lower the TTL to 300 for every record that moves. Wait for the old TTL to run out.
  2. Write down every cron job on the old box for these accounts. That list becomes the new box's crontab later.

Cutover

  1. Forum: switch "Board active" off in the XenForo admin, so nobody posts into a database that's about to be copied.
  2. Nextcloud on the old box into maintenance mode:
cd /path/to/nextcloud
sudo -u www-data php occ maintenance:mode --on

Use the user that owns the Nextcloud files. On a plain Ubuntu install that's www-data. On Plesk and KeyHelp it's the account's own system user.

  1. Final database dumps with the same pipe as above, into the existing new databases.
  2. Final rsync. Same command, it only copies what changed, so it's fast.
  3. Run the Nextcloud config script again. The rsync just brought the old config.php back.
  4. Final mail sync with doveadm for all three mailboxes.
  5. Switch the A and AAAA records to the new server.
  6. Enable the cron jobs on the new box from the prepared list.
  7. Stop Postfix on the old box. Once MX points to the new server, there must be no second place that accepts mail for the domain. Otherwise some sender with a stale cache delivers to the old box and nobody ever reads it.
  8. Turn the board back on, maintenance mode off on the new Nextcloud, test again without the hosts file.
  9. Replace the imported certs with KeyHelp's Let's Encrypt.

Keep the old box running for a while after. If something's missing, it's still there to pull from.

Why I do it this way

The test copy is most of the work. Once it runs, the cutover is a list of steps I already did once: dump, rsync, config fix, mail sync. Nothing on the night is new, which is the whole point. And if the test copy shows a problem, like the Nextcloud config thing, I find it on a quiet afternoon and not with the forum offline.

If the old server is on its way out anyway, back up the new one before you switch. I use plain rsync backups for that. And if half your data is in one backup and half in another, this restore story is what happens when you skip it.


Related posts