How to move a PHP website to a new server with almost no downtime

Copy everything while the old server keeps running, test the new one before anyone sees it, then do one short final copy and switch DNS. Visitors see a maintenance page for a few minutes at most.

Don't delete the old server when you switch. Keep it for a few days. As long as it exists, going back is one DNS change (step 11).

Before you start

  • Take a backup of the files and the database, and know how to restore it. See our backup guide.
  • The examples use a site called shop for example.com. Its files are in /var/www/shop, they belong to the user shop, and the MariaDB database is also called shop. The old server is 203.0.113.10, the new one 203.0.113.20. Use your own names.
  • Commands say which server they run on. Keep a terminal open on each.
  • MySQL instead of MariaDB? Use mysqldump, mysql and mysqlshow where the examples say mariadb-dump, mariadb and mariadb-show.

1. Lower the DNS TTL a day before

The TTL tells the rest of the internet how long to remember your server's address. It's often one hour or one day. If it's one day, some visitors keep going to the old server for a day after you switch.

See the current value (the number after the name, in seconds):

dig example.com A +noall +answer

At your DNS provider, set the TTL of the A record (and AAAA, if you have one) for example.com and www to 300 (5 minutes). Then wait at least as long as the old TTL, so the old, long value has run out everywhere. A day is a safe wait.

2. Set up the new server

Set up the new server the way you would any new server, and don't switch anything yet. Our guides cover each part:

Our tests moved a WordPress site from Ubuntu 24.04 (PHP 8.3, MariaDB 10.11) to Debian 13 (PHP 8.4, MariaDB 11.8).

3. Let the new server read from the old one

The new server fetches the files over SSH, as the site user on both sides. That way every file arrives owned by shop, with the same permissions, and you don't need root logins.

On the new server, make a key for the site user and show it:

sudo -u shop ssh-keygen -t ed25519 -N '' -f ~shop/.ssh/id_ed25519
sudo cat ~shop/.ssh/id_ed25519.pub

On the old server, let that key log in as the site user. Paste the line you just saw:

sudo -u shop mkdir -p -m 700 ~shop/.ssh
echo 'ssh-ed25519 AAAA... shop@new' | sudo -u shop tee -a ~shop/.ssh/authorized_keys

If the site user can't log in (its shell is /usr/sbin/nologin, as in our PHP-FPM pool guide), allow it for the move. You'll undo this in step 12:

sudo usermod -s /bin/bash shop

Also make a private folder for the database copy, outside the website, on both servers:

sudo install -d -o shop -g shop -m 700 /var/backups/move

On the new server, check that it works. It should print shop:

sudo -u shop ssh shop@203.0.113.10 whoami

4. Copy the files

On the new server:

sudo -u shop rsync -a --delete --exclude=/.ssh shop@203.0.113.10:/var/www/shop/ /var/www/shop/
  • -a keeps permissions, dates and links. The slash at the end of both folders matters: it copies what is inside the folder.
  • --exclude=/.ssh leaves out the SSH keys from step 3, in case the site folder is also the site user's home.
  • --delete removes files on the new server that are gone on the old one. Check the folder name twice before you run it.
  • The first run copies everything and can take a while. Later runs only copy what changed, so they take seconds.

This also copies wp-config.php or Laravel's .env, because they live in the site folder. If yours is kept somewhere else, copy it too. If the database password or address will be different on the new server, change it in that file.

Files owned by another user on the old server (for example root) will belong to shop on the new one. Check with sudo find /var/www/shop ! -user shop on the old server first.

5. Copy the database

On the new server, create the database and user with the same name and password the site uses. They're in wp-config.php (DB_NAME, DB_USER, DB_PASSWORD) or in .env:

sudo mariadb -e "CREATE DATABASE shop; CREATE USER 'shop'@'localhost' IDENTIFIED BY 'the-same-password'; GRANT ALL ON shop.* TO 'shop'@'localhost';"

On the old server, make a copy. --single-transaction takes a clean copy without stopping the site:

sudo sh -c 'umask 077; mariadb-dump --single-transaction --routines --triggers shop | gzip > /var/backups/move/shop.sql.gz'
sudo chown shop:shop /var/backups/move/shop.sql.gz

On the new server, fetch it and load it:

sudo -u shop rsync -a shop@203.0.113.10:/var/backups/move/ /var/backups/move/
gunzip < /var/backups/move/shop.sql.gz | sudo mariadb shop

Now compare the number of rows in each table. Run this on both servers; the numbers should match:

sudo mariadb-show --count shop

6. Note the cron jobs

Find the scheduled jobs on the old server:

sudo crontab -u shop -l
ls /etc/cron.d/ /etc/supervisor/conf.d/ 2>/dev/null

Save the site user's list into the move folder, so the new server can fetch it with the next copy:

sudo crontab -u shop -l | sudo -u shop tee /var/backups/move/shop.cron

Don't turn them on yet. If both servers run the same jobs, emails go out twice and the new database gets changes that the final copy throws away. You'll install them in step 10. The same goes for Laravel queue workers.

7. HTTPS on the new server

certbot proves that you own the domain by answering a request that Let's Encrypt sends to your domain. Until DNS points to the new server, that request goes to the old one, so a normal certbot --nginx fails. You have three ways:

  • Copy the certificates from the old server (best, and what we tested). The site works over HTTPS straight after the switch, and certbot on the new server renews them from then on.
  • Get a new certificate after the switch. Simple, but HTTPS shows an error until you run certbot. If you use HSTS, browsers won't open the site at all in that time.
  • Prove ownership through DNS (the "DNS challenge"). This works before the switch, but needs a certbot plugin for your DNS provider.

To copy them, on the old server pack the certificates and the site's web server file. The private keys are inside, so keep it private:

sudo sh -c 'umask 077; tar -czf /var/backups/move/server-files.tgz -C / etc/letsencrypt etc/nginx/sites-available/shop'
sudo chown shop:shop /var/backups/move/server-files.tgz

On the new server, install certbot as in our HTTPS guide, then fetch and unpack. Only do this on a fresh server: it replaces /etc/letsencrypt and the site file.

sudo apt install certbot python3-certbot-nginx
sudo -u shop rsync -a shop@203.0.113.10:/var/backups/move/ /var/backups/move/
sudo tar -xzf /var/backups/move/server-files.tgz -C /
sudo rm /var/backups/move/server-files.tgz
sudo ln -sf /etc/nginx/sites-available/shop /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

On Apache, use python3-certbot-apache, the file in /etc/apache2/sites-available/ (and its -le-ssl.conf twin), and a2ensite. In our test, a renewal on the new server worked with the copied files.

Then delete the package on the old server too: sudo rm /var/backups/move/server-files.tgz.

8. Test the new server before the switch

Make your own computer go to the new server, while everyone else still goes to the old one. Add a line to your hosts file: /etc/hosts on Linux and macOS, C:\Windows\System32\drivers\etc\hosts on Windows (edit it as administrator):

203.0.113.20  example.com www.example.com

Restart the browser, then click through the important pages: home, a few posts or products, images, login, forms, checkout and the admin area. To be sure which server answers, a quick check from any terminal that doesn't need the hosts file:

curl -I --resolve example.com:443:203.0.113.20 https://example.com/

--resolve sends this one request to the new server. Watch the new server's logs at the same time (step 10) to see your visits arrive.

Anything you add or change on the new server now will be overwritten in step 9. That's fine; it's only a test. Remove the hosts line when you're done.

9. The final copy

Pick a quiet time. Put the site on the old server into maintenance mode, so nobody adds orders or comments that would be lost. Run these in the site folder on the old server:

# WordPress (WP-CLI)
sudo -u shop wp maintenance-mode activate

# Laravel
sudo -u shop php artisan down

Now repeat steps 4 and 5. They're quick this time:

  1. New server: the rsync of the files from step 4.
  2. Old server: the mariadb-dump and chown lines from step 5.
  3. New server: fetch it and load it again. The copy replaces each table, so you don't need to empty the database first.
  4. Both: sudo mariadb-show --count shop and compare.

The maintenance setting was copied too (WordPress's .maintenance file, Laravel's storage/framework/down). So on the new server, turn it off, and check the site with curl --resolve or your hosts file once more:

sudo -u shop wp maintenance-mode deactivate   # WordPress
sudo -u shop php artisan up                     # Laravel

In our test, this whole step took under a minute for a small site.

10. Switch DNS and watch

At your DNS provider, change the A records for example.com and www to 203.0.113.20. Don't forget AAAA: if you have one, change it to the new server's IPv6 address or remove it, or IPv6 visitors keep going to the old server. Leave the old site in maintenance mode.

Then, on the new server, turn on the cron jobs and workers from step 6:

sudo crontab -u shop /var/backups/move/shop.cron
sudo crontab -u shop -l

Check what the world sees. A public resolver answers with the new address within a few minutes:

dig +short example.com @1.1.1.1

Watch the logs. Requests should arrive on the new server and stop on the old one:

sudo tail -f /var/log/nginx/access.log /var/log/nginx/error.log

Then check that renewal works on the new server, now that DNS points to it:

sudo certbot renew --dry-run

And turn renewal off on the old server, where it would now fail: sudo systemctl disable --now certbot.timer (if you installed certbot with snap: snap.certbot.renew.timer).

Does the site send email itself? If your SPF record lists the old server's IP address, add the new one, or mail may land in spam.

11. Keep the old server for a few days

Leave it running in maintenance mode for a few days to a week. If something serious breaks on the new server, you can go back:

  • Point the DNS records back to 203.0.113.10.
  • Turn off maintenance mode on the old server (wp maintenance-mode deactivate or php artisan up).
  • Orders or comments made on the new server in the meantime are only in the new database. Copy them back by hand, or move the new database to the old server with step 5 the other way round.

12. Clean up

When the new server has run well for a few days:

  • On both servers, delete the move folder. It holds a full copy of the database: sudo rm -r /var/backups/move.
  • On the old server, remove the key from ~shop/.ssh/authorized_keys, and if you changed the shell in step 3, set it back: sudo usermod -s /usr/sbin/nologin shop.
  • On the new server, remove the key you made: sudo rm ~shop/.ssh/id_ed25519 ~shop/.ssh/id_ed25519.pub.
  • Set the DNS TTL back to a normal value, such as 3600.
  • Set up backups on the new server, and test a restore. Then take a last backup of the old one and delete it.

If something goes wrong

  • "Permission denied (publickey)" in step 3: the key line in authorized_keys is wrong or broken over two lines, or the old server only lets some users log in (AllowUsers or AllowGroups in /etc/ssh/sshd_config).
  • "This account is currently not available": the site user's shell is nologin. See step 3.
  • "Error establishing a database connection" or "Access denied for user": the database name, user or password on the new server don't match wp-config.php or .env.
  • "Unknown collation" when loading the copy: the old server runs MySQL 8 and the new one MariaDB (or an older version). Install the same database on the new server.
  • Row counts differ: the site changed between the copy and the check. That's normal in step 5; in step 9 the old site should be in maintenance mode, so they must match.
  • The new site shows the maintenance page: the setting was copied. Turn it off on the new server (step 9).
  • Some visitors still see the old server: the old TTL hasn't run out for them, or an AAAA record still points to the old server.
  • Images or uploads are missing: they live outside the site folder, or the first rsync was stopped. Run it again; it only copies what's missing.

Official sources

Next steps: go through the security checklist for a new PHP server on the new machine, and set up backups with a tested restore.

ServerNest is being built to do this for you: copy an app's files and database to another of your servers, and let you test the copy before you switch. It's planned for after launch. Join early access to try it first.

← Back to the security blog