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
- SSH access to both old and new servers
- Root or sudo privileges on the new server
- Access to your DNS management panel (registrar or DNS provider)
- Same or compatible software stack on the new server (web server, PHP/Python/Node version, database engine)
rsync,mysqldumporpg_dumpinstalled- A working backup of your website (always have one before you start)
- Sufficient disk space on the new server
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:
- Homepage loads correctly
- All internal links work
- Images and assets load (check browser DevTools for 404s)
- Forms submit correctly
- Login/authentication works
- SSL certificate is valid (check the padlock icon)
- Database-driven pages display correct data
- Search functionality works
- File uploads work (if applicable)
# 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
- Document everything - Create a migration runbook with all server details, credentials, and steps specific to your setup
- Test the restore process - A backup is worthless if you have never tested restoring it
- Schedule during low-traffic hours - Even with zero-downtime migration, do the final sync and DNS switch during off-peak hours
- Communicate with stakeholders - Let your team know a migration is happening and provide a timeline
Infrastructure Practices
- Use infrastructure as code - If your server setup is scripted (Ansible, Terraform, Docker), migrations become reproducible and less error-prone
- Keep TTL low if you migrate often - A TTL of 300 seconds is reasonable for most sites and makes future migrations easier
- Automate database backups - Daily automated backups with retention policy, stored off-server
- Version control your configs - Web server configs, application configs, and cron jobs should be in Git
Post-Migration Hygiene
- Decommission the old server after 72 hours minimum and only after confirming zero traffic in access logs
- Update monitoring - Point your uptime checks and alerting to the new server IP
- Update external services - Webhook URLs, API whitelists, CDN origin settings, firewall rules
- Run a security audit - Fresh server is a good time to audit open ports, firewall rules, and software updates
- Document the new setup - Update your internal documentation with the new server details
Need Expert Help?
Want zero-downtime migration handled by an expert? €99.
Book Now — €99100% money-back guarantee