С $1432 до $233 в месяц без простоя: как мигрировали с DigitalOcean на Hetzner — перевод гайда
С $1432/мес до $233/мес без простоя: перевод гайда о миграции с DigitalOcean на Hetzner с mydumper, MySQL-репликацией и reverse proxy на Nginx.
Если вы держите продакшн в DigitalOcean и платите за облако больше тысячи долларов в месяц — этот перевод показывает, как за одну ночь переехать на Hetzner в 6 раз дешевле, без секунды простоя, с 248 ГБ MySQL и живыми мобильными приложениями. Оригинал написал Ибрагим Саетер — основатель турецкой софтверной компании с 8-летним стажем в DigitalOcean. 18 апреля 2026 года статья вышла в топ Hacker News и запустила очередную волну разговоров о цене облака.
Что в гайде
Главное
Что забрать из гайда
- С $1432 до $233/месяц (экономия $14 388/год) — при этом сервер объективно мощнее
- Стек в продакшене: 30 MySQL БД на 248 ГБ, 34 Nginx-сайта, GitLab EE, Neo4J, живые мобильные приложения
- 6 фаз миграции: установка стека → rsync файлов → MySQL master-slave → DNS TTL → reverse proxy → cutover
- mydumper/myloader вместо mysqldump — часы вместо дней на 248 ГБ
- Все скрипты (DNS, Nginx reverse proxy, GitLab webhooks) на GitHub с режимом DRY_RUN
Почему мы переехали
Ведение софтверной компании в Турции стало за последние годы очень дорогим. Инфляция и резкое ослабление лиры к доллару превратили долларовые счета за инфраструктуру в ощутимую нагрузку: счёт, который пару лет назад казался нормальным, при выросшем в несколько раз курсе бьёт совсем иначе.
Каждый месяц мы платили DigitalOcean $1432 за droplet со 192 ГБ RAM, 32 vCPU, 600 ГБ SSD, двумя Block Storage по 1 ТБ и включёнными бэкапами. Сервер был хороший, но отношение цены и производительности перестало иметь смысл.
Потом мы увидели Hetzner AX162-R:
- CPU: DigitalOcean — 32 vCPU, Hetzner — AMD EPYC 9454P (48 ядер / 96 потоков)
- RAM: 192 ГБ vs 256 ГБ DDR5
- Диск: 600 ГБ SSD + 2×1 ТБ volumes vs 1,92 ТБ NVMe Gen4 RAID1
- Цена: $1432/мес vs $233/мес
- Экономия: $1199/мес = $14 388/год
И это для сервера, который объективно мощнее по всем параметрам. Решение было простым.
Я клиент DigitalOcean почти 8 лет. У них отличный продукт, вопросов к надёжности и developer experience нет. Но когда смотришь на эти цифры, становится немного грустно от того, сколько лишних денег я оставил на столе за все эти годы. Если у вас стабильная нагрузка и вы не пользуетесь экосистемными фичами DO — сравните цены на выделенные серверы перед следующим продлением.
Что у нас крутилось
Это не пет-проект. В стеке было:
- 30 MySQL-баз данных (248 ГБ данных)
- 34 виртуальных хоста Nginx на нескольких доменах
- GitLab EE (бэкап 42 ГБ)
- Neo4J Graph DB (граф на 30 ГБ)
- Supervisor с десятками фоновых воркеров
- Gearman — очередь заданий
- Несколько боевых мобильных приложений на сотни тысяч пользователей
Старый сервер: CentOS 7 — давно EOL, но всё ещё в продакшене. Новый сервер: AlmaLinux 9.7 — совместимая с RHEL 9 сборка и естественный преемник CentOS. Миграция заодно дала повод уйти с ОС, которая несколько лет не получала security-апдейтов.
Стратегия: нулевой простой
Наивный подход — сменить DNS, перезапустить всё и надеяться на лучшее — не годился. Вместо этого мы спроектировали миграцию в шести фазах:
Фаза 1 — установка всего стека на новый сервер. Nginx (собранный из исходников с теми же флагами), PHP (через Remi-репозиторий с теми же .ini-конфигами со старого сервера), MySQL 8.0, Neo4J, GitLab EE, Node.js, Supervisor, Gearman. Каждый сервис должен был повторять поведение старого сервера до того, как мы тронем хоть одну DNS-запись.
SSL-сертификаты перенесли rsync-ом каталога /etc/letsencrypt/ со старого сервера. Уже после финального cutover, когда весь трафик шёл через новый сервер, принудительно обновили все сертификаты одной командой:
Фаза 2 — веб-файлы через rsync. Весь каталог /var/www/html (~65 ГБ, 1,5 млн файлов) скопировали на новый сервер rsync-ом по SSH с флагом --checksum для проверки целостности. Финальную инкрементальную синхронизацию запустили прямо перед cutover — чтобы забрать файлы, которые изменились после начальной копии.
Фаза 3 — MySQL master-slave. Вместо остановки базы на dump-and-restore мы настроили живую репликацию. Старый сервер — master, новый — read-only slave. Для первичной загрузки использовали mydumper, а затем запустили репликацию с той позиции бинлога, которая зафиксирована в метаданных дампа. Обе базы оставались синхронны в реальном времени до самого cutover.
Фаза 4 — снижение DNS TTL. Скриптом через DigitalOcean DNS API опустили TTL всех A- и AAAA-записей с 3600 до 300 секунд. MX- и TXT-записи не трогали (изменение TTL почтовых записей может ударить по доставляемости). Подождали час, пока старые TTL протухнут глобально, — и получили окно cutover меньше пяти минут.
Фаза 5 — Nginx на старом сервере превращаем в reverse proxy. Написали Python-скрипт, который распарсил все server {} блоки в 34 конфигах, сделал бэкапы оригиналов и заменил их на прокси-конфиги, указывающие на новый сервер. Это значило, что во время DNS-пропагации любой запрос, всё ещё долетавший на старый IP, молча форвардился. Пользователи ничего не заметили.
Фаза 6 — DNS cutover и вывод из эксплуатации. Один Python-скрипт дёрнул DigitalOcean API и за секунды поменял все A-записи на IP нового сервера. Старый сервер ещё неделю стоял как холодный standby, потом мы его выключили.
Главный принцип: в любой момент времени сервис отвечал — напрямую или через прокси. Окна недоступности не было вообще.
Миграция MySQL
Самая сложная часть всей операции.
Снимаем дамп
Мы использовали mydumper вместо стандартного mysqldump — и это сыграло огромную роль. Благодаря 48 ядрам нового сервера параллельный экспорт и импорт заняли часы вместо дней, которые ушли бы на однопоточный mysqldump. Если у вас большая MySQL-база, а вы всё ещё не на mydumper/myloader — вы делаете это трудным путём.
Файл метаданных дампа зафиксировал позицию бинлога на момент снимка — это станет точкой старта репликации:
Переносим дамп на новый сервер
После снятия дампа мы перенесли его rsync-ом по SSH. На 248 ГБ сжатых чанков это сильно быстрее любого другого способа:
Загружаем данные
Подводный камень MySQL 5.7 → 8.0
Застряв на CentOS 7, мы застряли и на MySQL 5.7 — устаревшей версии, которая годами крутилась в продакшене. Перед миграцией мы запустили mysqlcheck --check-upgrade, чтобы убедиться, что данные совместимы с 8.0. Проверка прошла чисто, поэтому на новый сервер поставили свежую MySQL 8.0 Community. Прирост производительности по всем проектам заметили сразу — запросы выполняются ощутимо быстрее благодаря улучшениям оптимизатора и InnoDB в 8.0.
Но апгрейд всё же подкинул одну занятную проблему.
После импорта у таблицы mysql.user оказалась не та структура — 45 колонок вместо ожидаемых 51. Из-за этого пропала mysql.infoschema, и аутентификация сломалась.
Фикс:
Но первый запуск упал с ошибкой:
Схема sys импортировалась как обычные таблицы вместо view. Решение:
И повторный запуск upgrade. На этот раз успех.
Настройка репликации MySQL
Когда оба дампа были импортированы, мы настроили новый сервер как реплику старого:
Почти сразу репликация встала с ошибкой 1062 (Duplicate Key). Причина: дамп снимался в два прохода, в промежутке между ними в некоторые таблицы писались строки, и теперь импортированный дамп и replay бинлога пытались вставить одни и те же записи.
Фикс:
Режим IDEMPOTENT молча пропускает дубликаты ключей и ошибки отсутствующих строк. Все критичные базы синхронизировались без единой ошибки. Через несколько минут Seconds_Behind_Master упал до нуля.
Тестируем до cutover
Перед тем как трогать DNS, нужно было убедиться, что все сервисы на новом сервере работают. Приём: временно прописать домены на IP нового сервера в /etc/hosts локальной машины.
С такой настройкой браузеры и Postman попадают на новый сервер, а весь остальной мир — ещё на старый. Мы прошли по API-эндпоинтам, проверили админки и убедились, что каждый сервис отвечает корректно. Только после этого пошли в cutover.
Хитрая проблема с привилегией SUPER
Когда master-slave был полностью синхронизирован, мы заметили, что INSERT-запросы на новом сервере проходят, хотя стоит read_only = 1. Запись шла.
Причина: у всех PHP-пользователей базы была привилегия SUPER. В MySQL SUPER обходит read_only.
Мы отозвали SUPER у всех 24 пользователей:
После этого read_only = 1 корректно блокировал запись от приложений, оставив репликации зелёный свет.
Подготовка DNS
Все домены управлялись через DigitalOcean DNS (nameservers при этом указаны через GoDaddy). TTL мы опускали скриптом через DO API, трогая только A и AAAA, но не MX и TXT (изменение TTL почтовых записей может ударить по доставке в Google Workspace).
Подождали час, пока старые TTL протухнут, — и готовы.
Старый Nginx превращаем в reverse proxy
Вместо ручной правки 34 конфигов мы написали Python-скрипт, который парсил каждый server {} блок во всех конфигах, определял основные content-блоки и заменял их на прокси-конфиг, сохраняя оригиналы как .backup.
Ключевой момент — proxy_ssl_verify off: SSL-сертификат нового сервера валиден для домена, а не для IP. Выключить проверку здесь безопасно, потому что мы контролируем оба конца.
Cutover
При Seconds_Behind_Master: 0 и готовом reverse proxy cutover был в таком порядке:
Скрипт DNS cutover дёрнул DigitalOcean API и поменял каждую A-запись на IP нового сервера примерно за 10 секунд.
Ещё одна деталь после cutover
Уже после миграции выяснилось, что многие вебхуки GitLab-проектов по-прежнему указывали на IP старого сервера. Написали скрипт, который через GitLab API сканирует все проекты и массово обновляет вебхуки.
Итоговые цифры
Мы ушли с $1432 в месяц на $233 — сэкономив $14 388 в год. И получили сервер мощнее:
- CPU: 32 vCPU → 96 логических ядер (AMD EPYC 9454P, 48 ядер × 2 потока)
- RAM: 192 ГБ → 256 ГБ DDR5
- Диск: ~2,6 ТБ смешанного → 1,92 ТБ NVMe RAID1
- Downtime: 0 минут
- Время миграции: около 24 часов
Пользователей миграция не коснулась.
Что унести с собой
MySQL-репликация — ваш лучший друг для zero-downtime миграций. Поднимайте её заранее, дайте догнать master, потом спокойно переключайтесь.
Проверяйте привилегии MySQL-пользователей до миграции. SUPER обходит read_only — если у ваших app-пользователей она есть, ваш slave на самом деле не read-only.
Автоматизируйте всё. DNS-апдейты, правки Nginx, обновления вебхуков — вручную на 34+ сайтах это часы работы и гарантированные ошибки.
mydumper + myloader сильно обгоняет mysqldump на больших данных. Параллельный дамп и восстановление с 32 потоками превратили дни работы в часы.
Облака дороги для стабильных нагрузок. Если вы не используете автоскейлинг или эфемерную инфраструктуру, выделенный сервер часто даёт лучшую производительность за долю цены.
Что это значит для российских команд
Hetzner для российских разработчиков — де-факто стандарт, если продакшн живёт в Европе. Принимает европейские карты и SEPA, берёт евро, выделенные серверы заметно дешевле AWS/GCP. DigitalOcean же отказывает в приёме платежей с российских карт, и обходные пути через посредников работают всё хуже.
Практические нюансы, которых в оригинальной статье нет:
- DigitalOcean не принимает российские карты — даже через посредников, платежи отклоняются.
- Hetzner принимает европейские карты и SEPA, счета в евро. У новых аккаунтов из стран с высоким риск-скорингом запрашивает верификацию — паспорт или удостоверение личности. Для ИП на Кипре или в Казахстане обычно проходит без проблем.
- Выделенные серверы AX-линейки иногда выдаются через очередь — от нескольких минут до суток, зависит от модели и загруженности ДЦ. Cloud VPS у Hetzner выдаются сразу.
- Для персональных данных граждан РФ держать сервер в Hetzner — прямое нарушение 152-ФЗ о локализации. Санкционные риски тоже реальны: Hetzner периодически блокирует клиентов с российскими реквизитами.
- Российские альтернативы с сопоставимым железом: Selectel, Serverspace, Timeweb Cloud Dedicated, Aeza. Оплата рублями, договор с юрлицом, хранение в ЦОДах на территории РФ.
Частые вопросы
Почему выделенный сервер Hetzner дешевле облачного DigitalOcean в 6 раз?
Облако продаёт гибкость — автоскейлинг, снапшоты, сетевые volumes, интеграции. Выделенный сервер продаёт железо как есть. Если ваши нагрузки стабильны и гибкость облака не используется, переплата идёт за функции, которые вам не нужны. На 32 vCPU без автоскейла выделенный AX162-R с 96 потоками и NVMe RAID даёт больше мощности за в разы меньшие деньги.
Можно ли реально сделать миграцию без простоя?
Да. Ключ — репликация MySQL и reverse proxy на старом сервере во время DNS-пропагации. Запросы всегда попадают либо на новый сервер напрямую, либо на старый и форвардятся на новый. Автор подтверждает: в его кейсе 0 минут простоя на 248 ГБ MySQL и живом трафике.
Что делать с MySQL 5.7 → 8.0?
Запустить mysqlcheck --check-upgrade на старой базе до миграции. После импорта на 8.0 почти гарантированно потребуется mysqld --upgrade=FORCE. Если процесс падает на схеме sys — дропните её (DROP DATABASE sys) и повторите. Это штатное поведение при переходе между поколениями.
Почему mydumper лучше mysqldump?
Параллельный экспорт и импорт по количеству ядер CPU. На 48-ядерном сервере 32 потока mydumper/myloader обрабатывают 248 ГБ за часы — вместо дней, которые ушли бы на однопоточный mysqldump. Плюс --compress и --chunk-filesize облегчают передачу дампа по сети.
Подходит ли этот подход для Kubernetes-команд?
В целом — да, но тогда логика другая: не переносите данные руками, а разворачиваете кластер на Hetzner Cloud или выделенных нодах и переключаете ингресс-контроллер через DNS. Репликация баз и reverse proxy остаются в силе как принципы, сами инструменты меняются.
Если у вас стабильная нагрузка и вы не пользуетесь экосистемными фичами DigitalOcean — сравните цены на выделенные серверы перед следующим продлением. Я оставил на столе слишком много денег за восемь лет.
Выводы
Гайд Саетера — редкий случай, когда миграция задокументирована до уровня конкретных команд и конфигов, а не в жанре «мы смигрировали, всё работает». Чек-лист на ближайший понедельник: посмотрите на счёт за облако, поделите его на реальную загрузку CPU/RAM/диска, сравните с ценой выделенного сервера соответствующего размера. Если разница в разы — у вас есть план на следующий квартал. А все Python-скрипты из гайда выложены в открытый доступ с режимом DRY_RUN — можно прогнать миграцию сначала вхолостую, а потом уже всерьёз.