Migrate Your App to a New Host Without Downtime
Step-by-step guide to zero-downtime app migration. Parallel-server strategy, database migration, DNS cutover, and rollback procedures to migrate your app safely.
- Zero-downtime migration uses a parallel-server strategy: keep the old server live while building the new one, sync databases incrementally, then flip DNS.
- Lower DNS TTL to 300 seconds 48 hours before cutover so resolvers refresh quickly when you change the A record.
- Database migration is the highest-risk step; use mysqldump --single-transaction for small databases or native replication (MySQL binlog, PostgreSQL WAL) for large active databases.
- Test the new server end-to-end using the hosts file technique before switching traffic, and keep the old server running for 24 hours after cutover as a rollback failsafe.
- Blue-green deployment eliminates DNS propagation delays but requires two full servers; managed platforms like Ship automate this complexity entirely.
Migrating an app to a new host without downtime is possible when you plan backward from DNS propagation timing and keep both servers live during the cutover. The key: a parallel-server strategy combined with low DNS TTL windows. This guide covers the exact sequence, database-specific procedures, and rollback protocols so your app keeps serving requests through every step.
Why app migrations fail--and how to avoid it
Migrations under stress often go badly. Not because zero-downtime tech doesn't exist; it does. Usually because planning assumes a perfect cutover window, then something breaks: a forgotten cron job, SSL certificate path mismatch, or hardcoded IP address somewhere in your config.
The anxiety is real. Your app is live, paying customers depend on it, and you are about to move the whole thing. Most failures happen not during the technical cutover but in the prep phase: incomplete inventory, untested configurations, and no rollback plan.
This guide assumes you have a working app on a current host (maybe a platform like Northflank, maybe a traditional VPS) and doubt, not ignorance, is holding you back. You know how to deploy; you know how to manage a database. Your fear is: "What if traffic stops while we switch?"
The honest answer: with proper sequencing, traffic does not stop. You keep the old server live while building on the new one, test everything in parallel, then flip DNS. If it fails, you flip it back. The whole thing takes 30 to 60 minutes of active work. The rest is preparation.
Pre-migration: What you need to document
Before touching anything new, write down what you have. This is boring work. Do it anyway.
Make a full inventory. Document every app (web server, API, backend workers, scheduled tasks). List every database (MySQL, PostgreSQL, MongoDB, Redis, even caches). Find SSL certificates and note where they live: Let's Encrypt renewal scripts, private keys, paths in your config. Write down environment variables and secrets: database credentials, API keys, auth tokens. Identify any files stored outside the database: user uploads, cache directories, session files. Locate cron jobs and background tasks: email workers, backups, analytics processors. Record DNS records: A records, CNAME aliases, MX records if relevant. Check for IP whitelists or firewall rules that depend on your current server's IP.
This is not optional. Every missing item becomes a broken feature after migration.
Next, measure data size. Run du -sh on your database, file storage, and code directories. If your database is terabytes, your migration strategy changes. If it is gigabytes, standard dump-and-restore works fine.
Then test your new host with a staging copy. Deploy your app, restore a database backup, run your test suite. Fix any environment issues now, not during cutover.
Finally, document your cutover plan on paper: exact commands, expected times, and who does what. Share it with your team. Have someone read it back to you and ask "what if this fails?" The answers to those questions are your rollback procedures.
The parallel-server strategy: Zero downtime in three moves
The core technique: never take the old server offline until you know the new one works.
Step 1: Provision and deploy to the new host.
Rent a new server with the same or better specs. Install your runtime environment (Node.js, Python, Go, whatever). Deploy your app code using your normal process. Do not connect it to the production database yet.
If you use containers (Docker), this is straightforward: build an image, push to your registry, pull and run on the new host. If you deploy via Git, clone and install dependencies the same way. The code should be identical or newer.
For traditional VPS deploys, use your deploy script (Capistrano, Fabric, a shell script). Same discipline: code from Git, dependencies installed, environment ready.
At this point, your new server is running your app, but it is not serving traffic. It is not connected to the production database. This matters.
Step 2: Lower DNS TTL 48 hours before cutover.
TTL (Time To Live) is how long DNS resolvers cache your domain's IP address. The default is usually 3600 seconds (one hour). When you change your domain's A record to point to the new server, old resolvers hold the old IP for up to TTL seconds.
48 hours before your planned cutover, reduce TTL to 300 seconds (five minutes). This tells the world: DNS changes coming soon, refresh your cache often. After the cutover, resolvers will refresh within five minutes instead of waiting an hour.
Change this at your DNS provider (Cloudflare, Route53, whatever you use). Just edit the A record's TTL setting; do not change the IP yet.
Example, using AWS Route53 or similar:
- Current: example.com A record pointing to 203.0.113.45, TTL 3600
- 48 hours before cutover: Change TTL to 300 (IP still 203.0.113.45)
This is a no-op for existing traffic. The IP does not change yet; only the cache duration changes.
Step 3: Sync your database.
This is where it gets real. Your database lives on the old server. You need a copy on the new server, and it must be up to date as close to cutover as possible.
For MySQL, do an initial full backup 24 hours before cutover using mysqldump --single-transaction --all-databases > backup.sql. Restore on new server: mysql < backup.sql. Set up replication (optional but recommended if database is large or active).
For PostgreSQL, full backup: pg_dump --verbose --full -Fc db_name > db_name.backup. Restore on new server: pg_restore -d db_name db_name.backup. Or use pg_basebackup for streaming replication setup.
Why --single-transaction for MySQL? It takes a consistent snapshot without locking the table. Your app keeps running.
If your database is huge (50+ GB) or highly active, set up replication beforehand. Both MySQL (CHANGE MASTER) and PostgreSQL (pg_basebackup) support streaming replication: the new server follows the old one's write log, staying in sync until you flip the switch.
Most teams with smaller databases (less than 10 GB) just do a big dump, restore, then a final incremental sync (dump again with --where "created_at > NOW() - INTERVAL 1 HOUR") right before cutover to catch new rows.
Step 4: DNS cutover.
This is the actual switch. Your downtime window is measured in seconds.
Update your domain's A record to point to the new server's IP.
Example:
- Old: example.com A record 203.0.113.45 (old server)
- New: example.com A record 198.51.100.50 (new server)
With TTL set to 300, most users' DNS resolvers will update within five minutes. Some older resolvers or ISPs ignore TTL and cache longer, but they are rare.
During those five minutes, some users hit the old server, some hit the new one. Both serve requests because both have the same codebase and the new one now has the same database.
Key: make sure your new server can access the database. If you moved the database too, make sure connection strings are updated in your app config.
Step 5: Monitor.
Watch access logs on both servers for 30 minutes. You should see traffic migrate from old to new as DNS propagates.
On the old server (nginx, Apache, whatever): tail -f /var/log/nginx/access.log | grep -c "HTTP"
On the new server: Same command.
If traffic does not show up on the new server after 15 minutes, something is wrong. See the rollback section below.
If both servers show traffic for 30 minutes, congrats: your migration is complete. After 24 hours, when you are confident, you can turn off the old server.
Database migration: The highest-risk step
Databases are where most migrations fail. Wrong approach, and you lose data or face hours of downtime.
The safest approach depends on your database size and tolerance for a brief sync window.
MySQL smaller than 10 GB (dump-and-restore).
mysqldump with --single-transaction is your friend:
# On old server, 1 hour before cutover
mysqldump --single-transaction --all-databases --result-file=backup.sql
# Copy to new server (SSH, SCP, S3, whatever)
scp backup.sql newserver:/tmp/
# On new server, stop writes briefly (send maintenance page)
mysql -u root < /tmp/backup.sql
This takes minutes for a 10 GB database. Yes, you stop writes for those minutes, but it is 5 to 10 minutes of "creating connection" message, not an hour.
PostgreSQL smaller than 10 GB (pg_dump).
# On old server
pg_dump --verbose --full -Fc dbname > db.backup
# Copy to new server
scp db.backup newserver:/tmp/
# On new server
pg_restore -d dbname db.backup
Similar timeline: 5 to 10 minutes for 10 GB.
Large databases (50+ GB, high write rate).
Dump-and-restore will be too slow. Use replication:
MySQL replication:
On old server, enable binary logging (if not already):
[mysqld]
log-bin = mysql-bin
server-id = 1
On new server, set up as a replica:
CHANGE MASTER TO
MASTER_HOST = 'old-server-ip',
MASTER_USER = 'replication-user',
MASTER_PASSWORD = 'password',
MASTER_LOG_FILE = 'mysql-bin.000123',
MASTER_LOG_POS = 12345;
START SLAVE;
SHOW SLAVE STATUS\G
The new server follows the old one's write log. When you are ready to switch, run STOP SLAVE on the new server, then update your app's database connection. Zero downtime.
PostgreSQL streaming replication:
On old server, enable WAL (Write-Ahead Logging):
wal_level = replica
max_wal_senders = 10
On new server, use pg_basebackup:
pg_basebackup -h old-server-ip -D /var/lib/postgresql/main -v -P
Then set up recovery.conf to follow the old server. The standby is always in sync.
Backward-compatible schema changes.
If your migration timing is tight, push schema changes to the old server first, even if you do not use the new columns or tables yet. Add new columns as nullable (old code ignores them). Let replication catch up. Switch apps. Old code keeps working because it only touches old columns. This expand-contract pattern gives you more time and less risk.
Testing before cutover: The hosts file technique
Do not trust that your app will work on the new server until you prove it.
On your local machine (or a jump host), edit /etc/hosts:
198.51.100.50 example.com
Now when you visit https://example.com in your browser, your computer routes to the new server's IP instead of the old one. The certificate still matches (assuming you copied or reissued it), and you hit your app running on the new hardware.
Go through your app end-to-end: sign in, create something, upload a file, run a cron job by hand, check email notifications (if applicable), verify API responses.
On Windows, edit C:\Windows\System32\drivers\etc\hosts. On macOS or Linux, /etc/hosts. Restart your browser's DNS cache (or restart the browser) after editing.
If anything breaks (500 errors, database timeouts, missing environment variables), you caught it before production traffic arrived. Fix it, test again, then proceed.
Remove the hosts file entry after cutover so your machine goes back to normal DNS resolution.
The DNS cutover: When to flip the switch
This is the moment. You have a new server running the app, database in sync, TTL lowered to 300, and everything tested.
Update your A record. Different providers have different UIs, but the action is the same: change the IP address from old to new.
Example: Cloudflare
- Log in to Cloudflare
- Go to DNS settings
- Find the A record for example.com
- Change IP from 203.0.113.45 to 198.51.100.50
- Click Save
The change is live immediately, but DNS resolvers take time to refresh. Older resolvers might take up to 300 seconds (your new TTL). Newer ones refresh in seconds.
What happens during propagation:
- Second 0 to 60: Most modern clients (Chrome, Firefox, mobile apps) refresh. Traffic shifts to new server.
- Minute 5: ISP-level resolvers refresh. More traffic shifts.
- Minute 5 to 300: Stubborn clients or cached entries still hit old server. Old server is still live, so it serves them.
The old server keeps running for 24 hours after the flip. If you notice a problem after cutover, a quick DNS revert (flip the A record back) will bring everything home immediately.
Blue-green deployment: An alternative for high-stakes apps
The parallel-server strategy is simple: one old, one new, flip DNS. But it has a weakness: DNS propagation is not instant. Some users hit the old server, some the new one. If there is a subtle bug, you have split-brain.
Blue-green deployment eliminates this by using a load balancer instead of DNS.
Blue: current production server (e.g., 198.51.100.1). Green: new server (e.g., 198.51.100.2). Load balancer: routes 100% of traffic to blue initially.
When ready, the load balancer switches 100% of traffic to green instantly. No DNS propagation. No split-brain. One request goes to blue, the next to green, all within seconds.
The tradeoff: you pay for two full servers continuously (or pay for the load balancer). For a bootstrapped startup, this might not make sense. For a SaaS with high uptime expectations, it is worth it.
How to set it up: deploy app to both blue and green servers. Place load balancer in front (nginx, HAProxy, cloud load balancer). Point your domain to the load balancer's IP (one A record change). Load balancer config routes traffic to blue initially. To switch: change load balancer to route to green. If green breaks, change it back to blue in seconds.
Example load balancer configuration (HAProxy or cloud load balancer): Define an upstream group with the blue server IP (e.g., 198.51.100.1). Route all traffic to that upstream. When you need to switch, update the upstream to point to the green server IP (e.g., 198.51.100.2), then reload the load balancer. Traffic switches instantly, with no DNS propagation delays.
To revert if green breaks, simply point the upstream back to blue. This is faster than DNS TTL refresh and avoids split-brain scenarios.
The blue-green market is substantial: $1.42 billion in 2024, expected to reach $6.89 billion by 2033, a 19.2% annual growth rate. But this growth reflects demand from enterprise deployments and microservices architectures. For a small team, simple DNS switching is often enough. Managed platforms like Opsily's Ship handle blue-green as a built-in feature, eliminating manual orchestration entirely.
Only use blue-green if your app is stateless (or state is shared in a central cache), you have the infrastructure budget, and downtime costs outweigh the complexity. For those requiring on-premise or data residency controls, Ship's self-hosted PaaS option provides the same capability.
Rollback procedures: When it goes wrong
It will not, but prepare anyway.
If the new server fails after cutover (a silent bug, a misconfigured database, a missing file): flip the DNS A record back to the old server's IP. Wait five minutes for resolvers to refresh. Traffic returns to the old server. This is the entire rollback. DNS revert is your nuclear button.
But DNS does not revert everything. If you already changed your app config and removed the old database, that data is gone. Do not do this. If you restarted the old server in the meantime, it might be out of sync. Do not restart it.
The safe approach: leave the old server running for 24 hours after cutover. Do not delete databases, do not turn off the service. Just let it sit there, fully restored, ready to serve traffic again if you flip DNS back.
After 24 hours, if everything is stable on the new server, then you can stop the old server, delete the old database copy (if space-constrained), and cancel the old host subscription.
Why managed hosting is the off-ramp
This whole guide assumes you are managing the cutover yourself. And you can. Millions of apps migrate this way every day.
But there is a cost to thinking about TTLs, testing via hosts files, and maintaining two servers in parallel. It is not just DevOps time; it is mental load. Every time you scale again, you think: "Do I have to do this dance again?"
Managed platforms like Opsily's Ship take this off your plate entirely. Ship handles database migrations, zero-downtime deployments, and auto-scaling as standard features. No parallel servers. No DNS tricks. You push code, Ship deploys it, and traffic keeps flowing.
The pricing is higher than raw VPS cost (Hetzner is cheaper), but Ship includes managed backups, automated deployments, and scaling without manual intervention. For a growing team, that trade--predictable flat cost plus no DevOps headaches--often wins. You are buying back time.
If you find yourself running through this guide and thinking "I have to manage this every time we grow," that is the signal to look at managed hosting.
Frequently Asked Questions
How do you migrate data without downtime?
Use replication (MySQL CHANGE MASTER or PostgreSQL streaming) to keep the new database in sync with the old one as writes happen. When the new database is caught up, switch your app to use it. No downtime because writes pause only during the cutover window, not during replication.
What is zero downtime migration?
Zero downtime migration means moving an app between servers without interrupting user traffic. Typically done by keeping both servers live, syncing data continuously, then switching traffic (via DNS or load balancer) once verification is complete.
How do you do deployment without downtime?
Deploy the app code to the new server first, test it, then switch traffic (DNS cutover or load balancer switch). The old app keeps serving requests until the switch is confirmed, so users never see an error page.
What are the main migration strategies?
Common strategies include: big bang (switch all at once), parallel run (old and new live side-by-side until transition), pilot (migrate one subset first), phased (migrate features incrementally), modular (migrate components independently), rehost (lift-and-shift to new infrastructure), and refactor (rearchitect during migration).
Which tools work best for data migration?
Depends on your database: mysqldump + mysql for MySQL, pg_dump + pg_restore for PostgreSQL, and rsync for file or code sync. For large databases, native replication (MySQL binlog or PostgreSQL WAL streaming) minimizes downtime.
Can you test your app on the new server before switching traffic?
Yes. Use the hosts file technique: edit your local /etc/hosts to point your domain to the new server's IP, then browse and test your app. This way you hit the new server's IP without DNS changes, letting you verify everything works first.
How long does a typical zero-downtime migration take?
Planning and prep: one to two weeks. Actual cutover (DNS change and propagation): 30 to 60 minutes of active work. DNS resolvers propagate within five to 300 seconds depending on TTL and resolver behavior.
What happens if the new server breaks right after cutover?
Flip the DNS A record back to the old server's IP. Traffic reverts within minutes. Keep the old server running for 24 hours after cutover to enable this rollback.
The Bottom Line
Migrating an app to a new host without downtime is a sequence, not magic. Provision the new host, lower DNS TTL, sync your database, test end-to-end, flip DNS, and monitor. Most failures happen in prep, not in the cutover itself.
The parallel-server strategy (keep both live, flip DNS) works for most teams. Blue-green deployment is more complex but eliminates propagation delays if you have the infrastructure. Either way, the principle is the same: never take the old system offline until you know the new one works. If managing migrations feels like a recurring tax on your growth, that is exactly when managed hosting like Opsily's Ship becomes worth the cost.