← All Articles
Last updated: 2026-03-30

How to Migrate a Website to a New Host Without Downtime

Zero-downtime website migration guide. File transfer, database export, DNS switchover, SSL on new server.

TL;DR

Migrating a website without downtime requires careful planning and a specific order of operations: lower your DNS TTL days in advance, replicate all files and databases to the new server, configure and test everything using a local hosts file override, then switch DNS. The old server keeps serving traffic until propagation completes. This guide walks through every step for Linux-based hosting with Apache/Nginx, MySQL/PostgreSQL, and Let's Encrypt SSL.

Prerequisites

Step 1: Pre-Migration Checklist

1.1 Inventory Your Current Setup

Before touching anything, document exactly what you have. Log into your old server and gather this information:

# Check web server and version
nginx -v 2>&1 || apache2 -v 2>&1 || httpd -v 2>&1

# Check PHP version (if applicable)
php -v

# Check database version
mysql --version 2>/dev/null || psql --version 2>/dev/null

# List all cron jobs
crontab -l
sudo ls /etc/cron.d/

# Check disk usage of your web root
du -sh /var/www/your-site/

# Check database size
mysql -u root -p -e "SELECT table_schema AS 'Database', \
  ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) AS 'Size (MB)' \
  FROM information_schema.tables GROUP BY table_schema;"

1.2 Prepare the New Server

Set up the new server with matching software versions. Mismatched versions are the number one cause of post-migration breakage.

# Example for Ubuntu/Debian - install the web stack
sudo apt update && sudo apt upgrade -y
sudo apt install nginx php8.2-fpm php8.2-mysql php8.2-xml \
  php8.2-curl php8.2-mbstring mariadb-server -y

# Verify versions match
nginx -v
php -v
mysql --version

1.3 Create a Full Backup

Never start a migration without a verified backup. This is your safety net.

# On the old server - create a full archive
tar czf /tmp/site-backup-$(date +%Y%m%d).tar.gz /var/www/your-site/

# Backup the database
mysqldump -u root -p --all-databases --single-transaction \
  --routines --triggers > /tmp/db-backup-$(date +%Y%m%d).sql

Step 2: Lower DNS TTL Values

This step must happen at least 24-48 hours before the actual migration. DNS records have a Time-To-Live (TTL) value that tells resolvers how long to cache the record. By default, this is often 3600 seconds (1 hour) or higher. Lowering it ensures that when you switch the IP, the change propagates quickly.

# Check your current TTL
dig +noall +answer your-domain.com

# Example output:
# your-domain.com.    3600    IN    A    203.0.113.10
#                     ^^^^ This is your TTL in seconds

Log into your DNS provider's control panel and lower the TTL for all relevant records (A, AAAA, CNAME, MX) to 60-300 seconds. Wait at least the duration of the old TTL before proceeding with the migration. If your old TTL was 3600 (1 hour), wait at least 1 hour after the change.

# Verify the TTL has propagated
dig +noall +answer your-domain.com
# Should now show: your-domain.com.    300    IN    A    203.0.113.10

Step 3: Copy Website Files to the New Server

3.1 Using rsync (Recommended)

rsync is the gold standard for file transfer during migrations. It is incremental, resumable, and preserves permissions.

# From the old server, push files to the new server
rsync -avz --progress --delete \
  -e "ssh -p 22" \
  /var/www/your-site/ \
  user@new-server-ip:/var/www/your-site/

# Flags explained:
# -a  Archive mode (preserves permissions, ownership, timestamps)
# -v  Verbose output
# -z  Compress data during transfer
# --progress  Show transfer progress
# --delete  Remove files on destination that don't exist on source

For very large sites, run rsync in a screen or tmux session so it survives SSH disconnections:

# Start a tmux session
tmux new -s migration

# Run rsync inside the session
rsync -avz --progress /var/www/your-site/ user@new-server-ip:/var/www/your-site/

# Detach: Ctrl+B, then D
# Reattach later: tmux attach -t migration

3.2 Using SFTP (Alternative)

If rsync is not available (some shared hosting environments), use SFTP:

# Connect to the new server via SFTP
sftp user@new-server-ip

# Navigate and upload
sftp> cd /var/www/your-site/
sftp> put -r /local/backup/your-site/*

# For automated transfers, use lftp for mirroring
lftp -u user,password sftp://new-server-ip -e "\
  mirror --reverse --delete --verbose \
  /var/www/your-site/ /var/www/your-site/; quit"

3.3 Set Correct Ownership and Permissions

# On the new server, fix ownership
sudo chown -R www-data:www-data /var/www/your-site/

# Fix directory and file permissions
find /var/www/your-site/ -type d -exec chmod 755 {} \;
find /var/www/your-site/ -type f -exec chmod 644 {} \;

Step 4: Database Export and Import

4.1 MySQL / MariaDB

# Export on the old server
mysqldump -u root -p --single-transaction --routines --triggers \
  your_database > /tmp/your_database.sql

# Transfer to the new server
rsync -avz /tmp/your_database.sql user@new-server-ip:/tmp/

# On the new server: create the database and user
mysql -u root -p -e "\
  CREATE DATABASE your_database CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;\
  CREATE USER 'your_user'@'localhost' IDENTIFIED BY 'strong_password_here';\
  GRANT ALL PRIVILEGES ON your_database.* TO 'your_user'@'localhost';\
  FLUSH PRIVILEGES;"

# Import the dump
mysql -u root -p your_database < /tmp/your_database.sql

# Verify table count
mysql -u root -p -e "SELECT COUNT(*) AS tables FROM information_schema.tables \
  WHERE table_schema = 'your_database';"

4.2 PostgreSQL

# Export on the old server
pg_dump -U postgres -Fc your_database > /tmp/your_database.dump

# Transfer to the new server
rsync -avz /tmp/your_database.dump user@new-server-ip:/tmp/

# On the new server: create the database and user
sudo -u postgres psql -c "CREATE USER your_user WITH PASSWORD 'strong_password_here';"
sudo -u postgres psql -c "CREATE DATABASE your_database OWNER your_user;"

# Import the dump
pg_restore -U postgres -d your_database /tmp/your_database.dump

# Verify
sudo -u postgres psql -d your_database -c "\dt" | tail -1

4.3 Handling Large Databases

For databases larger than a few hundred megabytes, compress the dump and pipe it directly:

# MySQL - stream directly without intermediate file
mysqldump -u root -p --single-transaction your_database \
  | gzip | ssh user@new-server-ip "gunzip | mysql -u root -p your_database"

# PostgreSQL - same approach
pg_dump -U postgres -Fc your_database \
  | ssh user@new-server-ip "pg_restore -U postgres -d your_database"

Step 5: Update Configuration Files

5.1 Web Server Configuration

Copy or recreate your virtual host configuration on the new server:

# Nginx - create site config
sudo nano /etc/nginx/sites-available/your-site.conf

# Minimal Nginx config template:
# server {
#     listen 80;
#     server_name your-domain.com www.your-domain.com;
#     root /var/www/your-site;
#     index index.php index.html;
#
#     location ~ \.php$ {
#         fastcgi_pass unix:/run/php/php8.2-fpm.sock;
#         fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
#         include fastcgi_params;
#     }
# }

# Enable the site
sudo ln -s /etc/nginx/sites-available/your-site.conf /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

5.2 Application Configuration

Update any hardcoded paths, database credentials, or server-specific settings:

# Find config files that reference the old server IP or hostname
grep -rl "203.0.113.10\|old-server-hostname" /var/www/your-site/ \
  --include="*.php" --include="*.conf" --include="*.env" --include="*.yml"

# Common files to check:
# WordPress: wp-config.php
# Laravel: .env
# Django: settings.py
# Generic: config.php, database.yml, .env

For WordPress specifically:

# Update wp-config.php with new database credentials
define('DB_NAME', 'your_database');
define('DB_USER', 'your_user');
define('DB_PASSWORD', 'strong_password_here');
define('DB_HOST', 'localhost');

5.3 Cron Jobs

# Export cron jobs from the old server
crontab -l > /tmp/crontab-backup.txt

# Transfer and import on the new server
rsync -avz /tmp/crontab-backup.txt user@new-server-ip:/tmp/
# On new server:
crontab /tmp/crontab-backup.txt
crontab -l  # Verify

Step 6: Set Up SSL on the New Server

You need SSL ready before the DNS switch. Use Let's Encrypt with a temporary verification method or copy existing certificates.

6.1 Copy Existing Certificates (Quick Method)

# On the old server, archive certificates
sudo tar czf /tmp/letsencrypt-backup.tar.gz /etc/letsencrypt/

# Transfer to new server
rsync -avz /tmp/letsencrypt-backup.tar.gz user@new-server-ip:/tmp/

# On the new server
sudo tar xzf /tmp/letsencrypt-backup.tar.gz -C /
sudo apt install certbot python3-certbot-nginx -y

6.2 Issue New Certificate via DNS Challenge

This method works even before DNS points to the new server:

# Install certbot on the new server
sudo apt install certbot -y

# Use DNS challenge - no need for the domain to point here yet
sudo certbot certonly --manual --preferred-challenges dns \
  -d your-domain.com -d www.your-domain.com

# Certbot will ask you to create a DNS TXT record
# Follow the on-screen instructions, then verify:
dig +short TXT _acme-challenge.your-domain.com

6.3 Update Web Server SSL Config

# Nginx SSL configuration
# server {
#     listen 443 ssl http2;
#     server_name your-domain.com www.your-domain.com;
#     ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
#     ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
#     ... rest of config ...
# }

sudo nginx -t && sudo systemctl reload nginx

Step 7: Test on the New Server Before DNS Switch

This is the most critical step. You must verify the site works on the new server before switching DNS. The hosts file trick lets you point your domain to the new server IP on your local machine only.

7.1 The Hosts File Trick

# On your local machine (macOS/Linux)
sudo nano /etc/hosts

# Add this line (use your NEW server's IP)
198.51.100.20    your-domain.com www.your-domain.com

# On Windows: edit C:\Windows\System32\drivers\etc\hosts

# Flush DNS cache
# macOS:
sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder
# Linux:
sudo systemd-resolve --flush-caches

7.2 Verification Checklist

Open your browser and test thoroughly:

# Command-line verification
curl -I https://your-domain.com  # Should return 200
curl -sL https://your-domain.com | grep -o ".*"  # Check title

# Test all important URLs
for url in "/" "/about" "/contact" "/blog" "/login"; do
  status=$(curl -o /dev/null -s -w "%{http_code}" https://your-domain.com$url)
  echo "$url -> $status"
done

7.3 Remove Hosts Entry After Testing

# IMPORTANT: Remove or comment out the hosts entry after testing
sudo sed -i '/your-domain.com/d' /etc/hosts

Step 8: DNS Switchover

8.1 Final Data Sync

Right before switching DNS, do one final sync to capture any changes made since the initial copy:

# Final file sync
rsync -avz --delete /var/www/your-site/ user@new-server-ip:/var/www/your-site/

# Final database sync
mysqldump -u root -p --single-transaction your_database \
  | ssh user@new-server-ip "mysql -u root -p your_database"

8.2 Switch DNS Records

Update your DNS A record (and AAAA if applicable) to point to the new server's IP address. Since you lowered the TTL in Step 2, propagation will be fast.

# After updating DNS, monitor propagation
watch -n 10 "dig +short your-domain.com"

# Check propagation from multiple locations
# Use online tools like dnschecker.org or:
for ns in 8.8.8.8 1.1.1.1 9.9.9.9 208.67.222.222; do
  echo "$ns: $(dig +short @$ns your-domain.com)"
done

8.3 Keep the Old Server Running

Do not shut down the old server immediately. Some DNS resolvers may ignore low TTL values. Keep the old server running for at least 48-72 hours after the switch. You can optionally set up a redirect on the old server:

# Optional: on the old server, redirect any remaining traffic
# Nginx configuration on old server:
# server {
#     listen 80;
#     listen 443 ssl;
#     server_name your-domain.com;
#     return 301 $scheme://your-domain.com$request_uri;
# }

Step 9: Post-Migration Verification

9.1 Automated Health Check

#!/bin/bash
# post-migration-check.sh
DOMAIN="your-domain.com"
EXPECTED_IP="198.51.100.20"  # New server IP

echo "=== Post-Migration Health Check ==="

# DNS check
CURRENT_IP=$(dig +short $DOMAIN)
if [ "$CURRENT_IP" = "$EXPECTED_IP" ]; then
  echo "[PASS] DNS points to new server ($CURRENT_IP)"
else
  echo "[FAIL] DNS still points to $CURRENT_IP (expected $EXPECTED_IP)"
fi

# HTTP status check
HTTP_STATUS=$(curl -o /dev/null -s -w "%{http_code}" https://$DOMAIN)
if [ "$HTTP_STATUS" = "200" ]; then
  echo "[PASS] HTTP status: $HTTP_STATUS"
else
  echo "[FAIL] HTTP status: $HTTP_STATUS"
fi

# SSL check
SSL_EXPIRY=$(echo | openssl s_client -connect $DOMAIN:443 2>/dev/null \
  | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
echo "[INFO] SSL expires: $SSL_EXPIRY"

# Response time
RESPONSE_TIME=$(curl -o /dev/null -s -w "%{time_total}" https://$DOMAIN)
echo "[INFO] Response time: ${RESPONSE_TIME}s"

# Check for mixed content / broken assets
BROKEN=$(curl -sL https://$DOMAIN | grep -c 'http://' || true)
echo "[INFO] Potential mixed content references: $BROKEN"

9.2 Monitor Logs

# Watch for errors on the new server
sudo tail -f /var/log/nginx/error.log
sudo tail -f /var/log/php8.2-fpm.log
sudo tail -f /var/log/mysql/error.log

9.3 Restore TTL

After 48-72 hours, increase your DNS TTL back to a normal value (3600 or higher) to reduce DNS query load:

# Verify final TTL
dig +noall +answer your-domain.com
# Should show your restored TTL value

Troubleshooting

Site Shows a Blank Page or 500 Error

# Check the web server error log
sudo tail -50 /var/log/nginx/error.log

# Common causes:
# - Missing PHP modules: sudo apt install php8.2-{module}
# - Permission issues: sudo chown -R www-data:www-data /var/www/your-site/
# - Wrong PHP version in config: check fastcgi_pass socket path

# Check PHP-FPM is running
sudo systemctl status php8.2-fpm

Database Connection Failed

# Verify MySQL is running
sudo systemctl status mariadb

# Test database credentials
mysql -u your_user -p your_database -e "SELECT 1;"

# Check socket path if using socket connection
ls -la /var/run/mysqld/mysqld.sock

# Check the database host in your config (localhost vs 127.0.0.1)
# Using localhost = socket, 127.0.0.1 = TCP

SSL Certificate Errors

# Check certificate status
sudo certbot certificates

# Test SSL configuration
openssl s_client -connect your-domain.com:443 -servername your-domain.com

# Force certificate renewal
sudo certbot renew --force-renewal

# Check Nginx SSL config syntax
sudo nginx -t

Emails Not Working After Migration

# Check MX records - they should NOT have changed
dig +short MX your-domain.com

# If you run your own mail server, check SPF and DKIM
dig +short TXT your-domain.com | grep spf

# Update SPF record if it includes old server IP
# Old: v=spf1 ip4:203.0.113.10 ...
# New: v=spf1 ip4:198.51.100.20 ...

rsync Transfer Interrupted

# rsync is resumable - simply run the same command again
rsync -avz --progress --partial /var/www/your-site/ user@new-server-ip:/var/www/your-site/

# --partial keeps partially transferred files for resume

Prevention & Best Practices

Before Every Migration

Infrastructure Practices

Post-Migration Hygiene

Need Expert Help?

Want zero-downtime migration handled by an expert? €99.

Book Now — €99

100% money-back guarantee

HR

Harald Roessler

Infrastructure Engineer with 20+ years experience. Founder of DSNCON GmbH.