Migrating from DigitalOcean to Hetzner
Key point
Migrated 30 MySQL and 34 Nginx services to Hetzner with 0 minutes of downtime
Details
They moved a $1,432-per-month DigitalOcean production infrastructure to a $233 Hetzner dedicated server, also switching the OS from CentOS 7 → AlmaLinux 9.7. As a result, they saved $1,199 per month, $14,388 per year, and completed the cutover with 0 minutes of downtime.
The setup was actual production, not a simple test environment. 30 MySQL databases (248GB), 34 Nginx virtual hosts, GitLab EE, Neo4J, Supervisor, and Gearman all ran together, serving a mobile app with hundreds of thousands of users.
The migration proceeded in 6 stages.
- First, they installed the same stack on the new server, matching Nginx/PHP/MySQL 8.0/Neo4J/GitLab/Node.js/Supervisor/Gearman to the existing setup.
- They replicated about 65GB, 1.5 million files in
/var/www/htmlusingrsync. - For MySQL, instead of
mysqldump, they used mydumper + myloader to move data in parallel, then maintained real-time sync via replication. - For DNS, they reduced the A/AAAA TTL from 3600 seconds to 300 seconds to shorten propagation time.
- They changed the existing server's Nginx to proxy to the new server, so that incoming traffic was passed to the new server even while DNS was still propagating.
- Finally, they switched all A records to the new IP, kept the old server as cold standby for 1 week, and then shut it down.
The trickiest part was MySQL. They used mydumper's --threads 32, --compress, and --trx-consistency-only for fast dumping, and started replication from the binlog position (mysql-bin.000004:21834307) recorded in the metadata. Afterward, the upgrade failed once due to a mysql.user schema mismatch and a sys schema issue, but they resolved it by running DROP DATABASE sys; and re-running.
During replication, a duplicate key (1062) error occurred, caused by an overlap between the dump and binlog replay. They kept syncing by working around it with SET GLOBAL slave_exec_mode = 'IDEMPOTENT';, and within a few minutes Seconds_Behind_Master became 0.
Another pitfall was the SUPER privilege. Even with the new server set to read_only = 1, application accounts could bypass writes because of the SUPER privilege. Only after removing SUPER from 24 application users did read-only mode work correctly.
Right before the cutover, they first verified the new server locally via /etc/hosts, and also updated GitLab webhooks in bulk afterward. The Python scripts used for the migration were published on GitHub, automating everything from TTL lookup, bulk DNS changes, Nginx proxy conversion, and MySQL comparison to GitLab webhook fixes.
There are three key takeaways.
- MySQL replication is the linchpin of zero-downtime migration.
- For large-scale migration, mydumper + myloader is far more efficient than
mysqldump. - Tasks like DNS, Nginx, and webhooks must be scripted.
Even when moving to a cheap dedicated server, with sufficient preparation and automation, you can improve both cost and performance while maintaining service continuity.
This summary was generated automatically by AI. Check the original for the author's claims and context. Copyright belongs to the original author.
Our guide explains how the AI works. Report summary errors, attribution issues, or removal requests via Contact.