Server Migration Without Downtime: TTL, rsync, Proxy

Updated: 5 min read Editorial team vserverhost
Short answer

For a server migration without downtime, lower the TTL of the A and AAAA records to 300 seconds at least one old TTL in advance, build the new server alongside the old one and sync data with rsync. After testing with curl --resolve, switch DNS and turn the old server into a reverse proxy to the new one until traffic stops arriving.

Most migrations that cause outages don’t fail on the new server. They fail because DNS caches keep sending visitors to the old machine for hours after it was switched off. Or because orders placed on the old server during that window never make it to the new one. Both are avoidable with a bit of planning.

The idea behind a zero-downtime migration

Two servers run side by side for a while. The new one takes over once DNS is switched. The old one accepts whatever stale caches still send its way and hands it to the new one. At no point is there a server that doesn’t answer, and all writes end up in one place.

PhaseOld serverNew serverDNS
T minus 24 hLiveBeing builtTTL lowered to 300
T minus 1 hLiveReady, testedUnchanged
TBrief write pause, final syncTakes overNew IP
T plus 5 min to 2 daysProxies to newLiveNew IP
T plus 2 daysShut downLiveTTL back to 3600

Step 1: lower the TTL in time

Check the current TTL on the authoritative server:

dig NS example.com +short
dig @ns1.example-dns.net example.com A +noall +answer
example.com.		86400	IN	A	198.51.100.7

At 86400, lower it to 300 at least 24 hours before the move. Do this for every record that holds an IP of the old server: the apex, www unless it’s a CNAME, subdomains, mail. Why the old value is what counts is covered in the DNS TTL guide. At betadomain you change TTLs per record in the customer area.

Steps 2 and 3: build the new server and copy data

Install the same versions of web server, runtime and database on the new machine. Then the files:

rsync -aHAX --numeric-ids --info=progress2 \
  /var/www/ [email protected]:/var/www/
rsync -aHAX --numeric-ids \
  /etc/nginx/ [email protected]:/etc/nginx/
rsync -aHAX --numeric-ids \
  /etc/letsencrypt/ [email protected]:/etc/letsencrypt/

-H keeps hard links, -A and -X keep ACLs and extended attributes, and --numeric-ids stops files getting the wrong owner when user names map to different IDs. Copy /etc/letsencrypt in full so the symlinks under live/ and the renewal configs survive. Renewal on the new server only works once DNS points there. Until then it may fail harmlessly on either side, and a 90-day certificate leaves plenty of margin.

A first database copy lets you test:

mysqldump --single-transaction --routines example_db | ssh [email protected] mysql example_db
pg_dump -Fc example_db | ssh [email protected] pg_restore -d example_db --clean --if-exists

For large databases, replication from old to new is worth setting up. The write pause at cutover then shrinks to seconds.

Step 4: test without touching DNS

curl -sI --resolve example.com:443:192.0.2.20 https://example.com
curl -s --resolve example.com:443:192.0.2.20 https://example.com/health

This reaches the new server under the real name with a valid certificate. Click through in a browser as well, using a temporary /etc/hosts entry. Check logins, uploads, forms and scheduled jobs.

Step 5: final sync and switch

  1. Put the app on the old server into maintenance or read-only mode.
  2. Run a last rsync with --delete and a final database dump.
  3. Point the A and AAAA records at the new IP.
  4. Go straight on to step 6.

For small projects the pause lasts a few minutes. dig @1.1.1.1 example.com A +short shows when the big resolvers return the new IP. Cache behaviour is explained in DNS propagation time.

Step 6: the old server becomes a proxy

This is the step that makes the difference. On the old server, replace the location block with a forward to the new IP:

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name example.com www.example.com;
    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    location / {
        proxy_pass https://192.0.2.20;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
        proxy_ssl_server_name on;
        proxy_ssl_name $host;
    }
}

proxy_ssl_server_name on makes nginx send the right SNI name to the new server. Anyone still routed to the old machine by a stale cache is served transparently, and every write lands in the new database. The trade-off: the app sees these requests coming from the old server’s IP unless it reads X-Forwarded-For. More on proxy setups in reverse proxy for multiple domains.

Step 7: watch, then switch off

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

Once a day or two passes with nothing but bots hitting the bare IP, shut the old server down and raise the TTL to 3600 again.

Things that get forgotten

The old IP lives in more places than the A record. Search before you switch off:

grep -rn "198.51.100.7" /etc /var/www /opt 2>/dev/null

Usual suspects:

  • SPF records with ip4:198.51.100.7 instead of mx. Mail from the new server then lands in spam.
  • Third-party allowlists such as payment webhooks, external databases or APIs that accept only certain source IPs.
  • Cron jobs and systemd timers still running on the old server, sending duplicate emails or processing data twice. Disable them after the switch.
  • Backups whose source or target is configured by IP.
  • Monitoring still pointed at the old address, happily reporting green while the new server is down.

Mail servers in a migration

If Postfix moves too, two more things apply. The PTR record for the new IP must be set at your provider before the switch. And the old server should keep accepting mail until the MX TTL has expired, after which you copy what arrived with imapsync. Senders that get a temporary failure in between retry. Record details are in MX record setup.

Write down the TTLs of every record that points at your server today, before the next move forces the question. If the migration comes with a new name, you can register the new domain at betadomain, for example a .cloud domain at €25.99 a year with the same price on renewal. More background is in the betadomain domain guides.

Step by step

  1. Lower the TTLAt least one old TTL before the move, set the TTL of all affected A, AAAA and, if relevant, MX records to 300 seconds.
  2. Build the new serverInstall and configure packages, web server, application and firewall on the new machine.
  3. Sync data and certificatesCopy the web root, configuration and /etc/letsencrypt with rsync -aHAX, and the database via dump or replication.
  4. Test with curl --resolveCheck the new server with curl --resolve example.com:443:new-ip https://example.com without changing DNS.
  5. Final sync and DNS switchPause writes briefly, run a final rsync and database sync, then point the A and AAAA records at the new IP.
  6. Turn the old server into a proxyReconfigure nginx on the old server to forward every request to the new IP.
  7. Clean upAfter a day or two without real traffic on the old server, raise the TTL back to 3600 and shut the old server down.

Found your domain? You can check and register it directly at domain registration without teaser pricing – yearly price from €2.99.

FAQ

How far in advance should I lower the TTL?

At least as long as the old TTL. If the A record has 86400 seconds, lower it to 300 no later than 24 hours before the move. Lowering it just before the switch does nothing, because resolvers cached the old value with the old TTL.

How do I move Let's Encrypt certificates to the new server?

Copy the whole /etc/letsencrypt directory with rsync, including the symlinks under live and archive. Renewals then continue with the same configuration. Alternatively, issue new certificates via DNS-01 before DNS is switched.

What about database writes during the migration?

That's the tricky part. Either put the app into read-only or maintenance mode for a few minutes, do a final sync and switch, or set up replication. With the old server proxying to the new one, all writes after the switch land on the new database.

Do I need to migrate email too?

Only if the mail server runs on the same machine. Then the MX hostname and its A record are part of the move, and the PTR for the new IP must be set at your provider beforehand or outgoing mail gets rejected.