VPS Migration Checklist: Move Your Server Without Downtime or Data Loss
Migrating a VPS — whether to a different provider, a larger plan, or a different region — involves more than copying files. Databases must be transferred consistently, configurations must be replicated, DNS must be cut over with minimal downtime, and the old server must remain available as a fallback until the new one is verified. This checklist guides you through a safe, systematic migration.
Migration Overview
<code">Phase 1: Prepare new VPS (install software, copy config) Phase 2: Transfer data (files, databases) Phase 3: Test on new VPS (verify everything works) Phase 4: DNS cutover (point domain to new server) Phase 5: Monitor (watch for issues) Phase 6: Decommission old server (after confirmation period)
Phase 1: Inventory the Old Server
<code"># Document everything installed and running echo "=== OS ===" && cat /etc/os-release echo "=== Running services ===" && systemctl list-units --type=service --state=running echo "=== Open ports ===" && sudo ss -tlnp echo "=== Installed packages ===" && dpkg -l | grep "^ii" > /tmp/packages.txt echo "=== Nginx sites ===" && ls /etc/nginx/sites-enabled/ echo "=== Cron jobs ===" && crontab -l && sudo crontab -l echo "=== Docker containers ===" && docker ps -a echo "=== Docker volumes ===" && docker volume ls echo "=== Disk usage ===" && df -h && du -sh /var/www/* /opt/* 2>/dev/null echo "=== Environment files ===" && find / -name ".env" 2>/dev/null | grep -v proc
Phase 2: Prepare the New VPS
<code"># On NEW VPS — install same software stack
sudo apt update && sudo apt upgrade -y
# Install same packages (from list exported above)
# Do NOT install from the package list directly — use it as reference
sudo apt install -y nginx certbot python3-certbot-nginx \
docker.io docker-compose-plugin \
nodejs npm \
postgresql mariadb-server redis-server \
fail2ban ufw
# Copy SSH authorized keys
mkdir -p ~/.ssh
# Paste your public key:
echo "ssh-ed25519 AAAA... you@yourmachine" >> ~/.ssh/authorized_keys
chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys
# Apply same sysctl tuning
# Copy /etc/sysctl.d/99-vps-performance.conf from old server
# sudo sysctl -p /etc/sysctl.d/99-vps-performance.conf
Phase 3: Transfer Files
<code"># From your LOCAL machine (or from the new VPS using rsync over SSH):
# Transfer web files
rsync -avz --progress \
deploy@OLD_VPS_IP:/var/www/ \
deploy@NEW_VPS_IP:/var/www/
# Transfer Nginx configuration
rsync -avz \
deploy@OLD_VPS_IP:/etc/nginx/ \
/tmp/nginx-backup/
# Review, then copy to new VPS:
scp -r /tmp/nginx-backup/ deploy@NEW_VPS_IP:/etc/nginx/
# Transfer application directories
rsync -avz deploy@OLD_VPS_IP:/opt/ deploy@NEW_VPS_IP:/opt/
# Transfer environment files
scp deploy@OLD_VPS_IP:/opt/myapp/.env /tmp/myapp.env
scp /tmp/myapp.env deploy@NEW_VPS_IP:/opt/myapp/.env
rm /tmp/myapp.env # Clean up local copy
Phase 4: Transfer Databases
PostgreSQL
<code"># On OLD VPS — dump all databases sudo -u postgres pg_dumpall | gzip > /tmp/postgres-all.sql.gz # Transfer to new VPS scp deploy@OLD_VPS_IP:/tmp/postgres-all.sql.gz /tmp/ # On NEW VPS — restore gunzip -c /tmp/postgres-all.sql.gz | sudo -u postgres psql rm /tmp/postgres-all.sql.gz # Verify databases restored sudo -u postgres psql -c "\l"
MariaDB/MySQL
<code"># On OLD VPS
sudo mysqldump --all-databases --single-transaction \
--routines --triggers \
| gzip > /tmp/mysql-all.sql.gz
scp deploy@OLD_VPS_IP:/tmp/mysql-all.sql.gz /tmp/
# On NEW VPS
gunzip -c /tmp/mysql-all.sql.gz | sudo mysql
rm /tmp/mysql-all.sql.gz
# Verify
sudo mysql -e "SHOW DATABASES;"
Docker Volumes
<code"># For each important Docker volume on OLD VPS:
docker run --rm \
-v myapp_data:/source \
-v /tmp:/backup \
alpine tar czf /backup/myapp_data.tar.gz -C /source .
scp deploy@OLD_VPS_IP:/tmp/myapp_data.tar.gz /tmp/
# On NEW VPS — restore into volume:
docker volume create myapp_data
docker run --rm \
-v myapp_data:/target \
-v /tmp:/backup \
alpine tar xzf /backup/myapp_data.tar.gz -C /target
Phase 5: Issue SSL Certificates on New VPS
<code"># Method A: Get new certificates (requires DNS to point to new VPS first — do after DNS cutover) sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com # Method B: Transfer existing certificates (extend testing time before DNS cutover) # On OLD VPS: sudo tar czf /tmp/letsencrypt.tar.gz /etc/letsencrypt/ scp deploy@OLD_VPS_IP:/tmp/letsencrypt.tar.gz /tmp/ # On NEW VPS: sudo tar xzf /tmp/letsencrypt.tar.gz -C / sudo nginx -t && sudo systemctl reload nginx # Certificates will remain valid until expiry — Certbot auto-renews
Phase 6: Test on New VPS (Before DNS Cutover)
<code"># Test by modifying your LOCAL /etc/hosts temporarily echo "NEW_VPS_IP yourdomain.com www.yourdomain.com" | sudo tee -a /etc/hosts # Now test in browser — you're hitting new VPS while DNS still points to old # Verify: # ✅ Homepage loads correctly # ✅ Admin login works # ✅ Database reads work (existing content visible) # ✅ File uploads work # ✅ Email sending works (test contact form) # ✅ SSL certificate valid # ✅ All critical pages load # Remove test /etc/hosts entry when done: sudo sed -i '/yourdomain.com/d' /etc/hosts
Phase 7: DNS Cutover
<code"># 1. Lower TTL 24 hours before cutover (to reduce propagation time) # yourdomain.com A record: change TTL from 3600 to 300 (5 minutes) # www.yourdomain.com A record: change TTL from 3600 to 300 # 2. Take a final database snapshot on old VPS just before cutover sudo -u postgres pg_dumpall | gzip > /tmp/final-backup.sql.gz # 3. Change DNS A records to NEW VPS IP # In your DNS provider: # yourdomain.com A NEW_VPS_IP # www.yourdomain.com A NEW_VPS_IP # 4. Verify propagation dig yourdomain.com A +short # Should return NEW_VPS_IP # Or check: https://dnschecker.org # 5. Monitor new VPS during propagation tail -f /var/log/nginx/access.log
Phase 8: Post-Migration Verification
<code"># Checklist — verify on new VPS: curl -I https://yourdomain.com # SSL valid, 200 OK sudo systemctl status nginx fail2ban # Services running sudo fail2ban-client status # Jails active crontab -l # Cron jobs present docker ps # All containers running sudo -u postgres psql -c "\l" # Databases present # Check Certbot auto-renewal on new VPS: sudo certbot renew --dry-run
Phase 9: Decommission Old VPS
<code"># Keep old VPS running for 7–14 days as fallback # Monitor: check if any traffic still hitting old VPS tail -f /var/log/nginx/access.log # On OLD VPS — should quieten # After 7–14 days with no issues: # 1. Take a final backup from old VPS for archival # 2. Cancel the old VPS plan in provider's control panel # 3. Delete DNS records pointing to old IP (if any) # Store final backup somewhere safe: restic backup /etc /var/www /opt # Or download to local machine: rsync -avz deploy@OLD_VPS_IP:/var/www/ /backup/old-vps/
Common Migration Mistakes
- Forgetting environment files: Database passwords, API keys, .env files are not in git — find and transfer every one
- Forgetting cron jobs: Run
crontab -landsudo crontab -lon old server — don’t forget root’s crontab - Not lowering TTL early: Default 24-hour TTL means 24 hours of mixed traffic after DNS change
- Cutting over without testing: Always verify on new server before changing DNS
- Forgetting Docker volumes: Containers copy fine; volumes (persistent data) need explicit migration
- Missing PHP/Nginx config includes: Custom php.ini settings, included Nginx snippets, upstream configs
Getting Started
Whether migrating to a larger plan, a better provider, or a different geographic location, VPS.DO’s Ubuntu VPS plans support the same software stack as any Ubuntu host — making migration straightforward. New VPS provisioning takes under 60 seconds; the migration process itself typically takes 2–6 hours depending on data volume. Hong Kong VPS plans serve APAC users; USA VPS plans serve North American audiences.
Conclusion
A systematic VPS migration checklist prevents the most common failure modes: missing environment files, high-TTL DNS delays, untested configuration, and forgotten cron jobs. The key practices are: inventory everything on the old server, test on the new server using local /etc/hosts before changing DNS, lower DNS TTL 24 hours before the cutover, and keep the old server running for 7–14 days as a fallback. A migration executed this way is low-risk and fully reversible until the final decommission step.