Эта статья разворачивает в полноценный план один абзац из Бэкапов — сценарий «увести сайт на новый хост без простоя». Там он был списком из пяти шагов; здесь — разбор того же перехода по косточкам, с командами и ловушками, которые обычно и выдают «незапланированный» даунтайм посреди миграции. «Без простоя» (ноль downtime) здесь означает: переключая сайт с одной машины на другую, вы не сажаете его в «белый экран» — пользователи либо остаются на старой копии, либо уходят на новую, и обе отвечают одинаково, потому что собраны из одного слепка данных.
Часть I. Подготовка: TTL, дедлайн и «окно»
Миграция, у которой нет заранее сниженного TTL, превращается в лотерею. Разберём, почему — и что ещё подготовить до всякой команды переноса.
Что такое TTL и зачем его снижать
TTL (time-to-live, «сколько жить») у DNS-записи говорит всем, кто её читает (резолверы провайдеров, браузеры, кэши), как долго держать в памяти ответ без переспроса. Пока ваш домен указывал на старый хост с TTL, скажем, 3600 секунд (1 час), после переключения записи половина мира будет ещё час стучаться на старый IP, а половина — на новый. Это не авария, но это значит, что:
- вы не сможете чисто «проверить переключение» — часть трафика придёт на старый хост ещё с час;
- откат (вернули старую запись) тоже будет «вполсилы» до конца TTL.
За сутки до переезда выставляете TTL на A/AAAA-записях сайта в значение вроде 300 секунд (или минимальное, которое даёт панель провайдера). После успешного переезда и пары спокойных дней возвращаете TTL к нормальному (не держим вечно маленький — это лишние запросы к DNS). Снижать TTL надо заранее: у записей, которые читали с длинным TTL, старые адреса уже сидят в кэшах с тем большим TTL — пока он не истечёт, ваш новый короткий TTL на пришедшие запросы не влияет.
Окно и «план действий»
Перед началом фиксируете три вещи (одной строкой в тикете/заметке, как в Runbook «Авария»):
- Точка, с которой снимаем слепок (дата/время дампа и
rsync). Позже, при финальном переносе, вы будете сравнивать «что изменилось после слепка». - Когда переключаете DNS — договоритесь о времени с минимальной посещаемостью (для личного сайта — ночь; это снижает число обращений к обеим машинам в переходный момент).
- Что будет «сдано» как успех: главная открывается,
/wp-adminлогинится, один пост можно создать, один файл можно загрузить в медиа-библиотеку. Это же и ваш тест приёма.
Новый хост ещё пустой — это хорошо
Новая машина не должна быть «готовым WordPress», на который вы сверху накатываете данные. Сначала вы собираете окружение-двойник старого: та же версия PHP (или заранее решённая новая, см. Часть VI), MariaDB, nginx. Прогон настройки этой базы — у Анатомии VPS и Nginx. Дальше — что уже было у старого хоста, должно повториться у нового (иначе это тихая часть «на проде легло»): почта через SMTP/mail(), права/владелец файлов, PHP-лимиты (upload_max_filesize, memory_limit — из PHP-FPM).
Часть II. Слепок и перенос на новый хост
Здесь ровно то же, что и при бэкапе, но вместо «положить в архив» — «положить на новую машину и поднять там». Один снимок = файлы + база.
Файлы: rsync с сохранением атрибутов
# bash — тянем дерево сайта со СТАРОГО хоста на новый (выполнять НА НОВОМ хосте)
# -a = рекурсивно + сохранить права/владева/время/симлинки (критично для wp-content)
rsync -avh --progress \
-e "ssh -i ~/.ssh/migrate_key" \
deploy@old.example.test:/var/www/example.test/ \
/var/www/example.test/Не используйте scp -r: он не несёт -a-свойств, и вы получите сайт с поломанными правами (иногда «500» из-за того, что владелец root, а PHP-FPM живёт под www-data). Именно владелец и права — одна из самых частых «неожиданностей на проде»: после rsync -a владелец уехал цифрой, а на новом хосте uid другого пользователя. Исправьте владельца после копирования:
# bash — привести владельца дерева к тому, под кем живёт PHP-FPM
sudo chown -R www-data:www-data /var/www/example.test(Какой именно пользователь у пула — видно из www.conf/юнита: см. PHP-FPM, где разбирается user/group пула.)
База: дамп и импорт
# bash — на СТАРОМ хосте: согласованный снимок без блокировки живой базы (из Бэкапов, Часть II)
mysqldump --single-transaction --quick --routines --triggers --events \
--set-gtid-purged=OFF -u wpuser -p wpdatabase > /tmp/wp-migrate.sql
# bash — перенос дампа на новый хост
scp -i ~/.ssh/migrate_key /tmp/wp-migrate.sql deploy@new.example.test:/tmp/# bash — на НОВОМ хосте: создать пустую БД + пользователя и импортировать слепок
sudo mysql -e "CREATE DATABASE wpdatabase CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
# пользователя и права заведите тем же скриптом grant, что описан в [MariaDB] про «только под один сайт»
sudo mysql wpdatabase < /tmp/wp-migrate.sqlВажная деталь: импортируйте дамп в пустую базу, а не «поверх» уже накатанного туда WordPress. Иначе вы получите конфликт таблиц и «частично переехавший» контент.
wp-config.php и подключение
Файл wp-config.php приехал в дереве с rsync. Он несёт старые креды БД и старые уникальные ключи-соли (AUTH_KEY, SECURE_AUTH_KEY, …). Два решения:
- Креды БД (
DB_NAME/DB_USER/DB_PASSWORD) — подставьте те, что создали на новом хосте, иначе сайт будет стучаться в несуществующую базу. - Ключи-соли — можно оставить как были (проще), но это же ваши ключи шифрования куки-сессий. Если подозревали утечку на старом хосте, сгенерируйте новые (
https://api.wordpress.org/secret-key/1.1/salt/) — учтите, что это разлогинит всех текущих сессий. Обсуждение солей — в Шифрование данных.
Проверка PHP-версии
# bash — что за PHP реально работает на новом хосте
php -v
ls /etc/php/ # какая версия поставлена; пул должен совпасть с нейЕсли на новом хосте другая мажорная версия PHP, чем на старом, это не ошибка миграции, а потенциальный баг плагина/темы под новую версию — тестите это на этапе подъёма по IP (Часть IV), а не после переключения DNS.
Часть III. Замена адреса внутри базы (главная причина «переехал — не работает»)
WordPress хранит абсолютные URL внутри базы: option siteurl/home, а также сериализованные массивы (виджеты, настройки темы, мета-поля meta_value с a:2:{...}) и ссылки внутри текстов постов. Когда сайт переезжает со https://old.example.test на https://example.test, все эти вхождения надо заменить. Ключевое слово — заменить так, чтобы сериализованные данные остались валидными.
Почему «sed по дампу» — плохая идея
Сериализованный PHP хранит длину строки в тексте: s:19:"https://old.examp…". Если вы тупо замените подстроку другой по длине строкой, а метку длины s:19: не пересчитаете, PHP не сможет прочитать поле — виджет/настройка тихо отвалится, а дамп при импорте не выдаст ошибки. Вы получите «сайт переехал, а часть настроек/виджетов пустая», и debugging-сессия будет долгой.
Правильно: wp search-replace
WP-CLI (официальная утилита командной строки WordPress) меняет URL с учётом сериализации — он разбирает сериализованные поля, заменяет внутри них и пересобирает с корректной длиной.
# bash — безопасная замена URL по всей базе (выполнять НА НОВОМ хосте, в корне сайта)
wp search-replace 'https://old.example.test' 'https://example.test' --all-tables --skip-columns=guid--all-tables— пройтись не только по постам (wp_posts) и опциям (wp_options), но и по мета-таблицам, где тоже сидят абсолютные ссылки.--skip-columns=guid— колонкуguid(global unique identifier, «уникальный идентификатор» поста) не трогаем: это не «адрес картинки», а неизменяемый идентификатор, и его замена ломает RSS-ленту у подписчиков, которые его кэшировали.
Перед заменой — «на сухую прогнать», сколько будет вхождений, и посмотреть, что ни одно поле не стало битым:
# bash — предпросмотр без записи
wp search-replace 'https://old.example.test' 'https://example.test' --dry-run --report-changed-onlyЕсли WP-CLI на новом хосте ещё не поставлен — это одна команда (wp-cli.org), но он должен запускаться с версией PHP вашего сайта. Если CLI не ставится или вы предпочитаете не ставить его на прод, есть аккуратная альтернатива — плагин-заменщик (Better Search Replace / Search & Replace), который делает то же: сериализацию понимает. Прямой sed/replace в SQL — только для колонок, которые вы уверены что не сериализованы.
Отдельно: HTTPS после переезда
Если при жизни на старом хосте редирект на HTTPS делал nginx, на новом его надо поставить заново: сертификат Let’s Encrypt выпускается на новой машине (certbot), и конфиг отдаёт его (см. Nginx про TLS/certbot). Иначе сразу после переключения DNS получите либо «не доверенный сертификат» для пользователей, либо бесконечный редирект между http и https — когда WordPress считает себя HTTPS-сайтом, а nginx ещё не отдаёт HTTPS.
Часть IV. Подъём и проверка по IP — до переключения DNS
До того как трогать домен, новый хост обязан сам отдавать сайт, проверенный напрямую, без участия DNS. Только так «без простоя» перестаёт быть обещанием и становится фактом.
Открываем новый хост по IP
На новом хосте сайт уже лежит. На время проверки можно открыть его напрямую:
- у nginx-
serverблока — временное совпадение по IP илиdefault_server, чтобы запрос по IP попал в нужнуюlocation(конфигурацияlocation/server_name— из Nginx); - или локально у себя на машине в
/etc/hostsприписать боевой домен к новому IP:
# /etc/hosts на вашей тестовой машине (не на сервере!) — временная подмена DNS локально
NEW_SERVER_IP example.test
После этого браузер с вашей машины идёт на новый хост по боевому домену — и проверяет ровно то, что увидят пользователи: сериализованные URL поменялись, виджеты на месте, стили грузятся. (Если подменяете в /etc/hosts — отключите в браузере кэш DNS/secure-DNS и используйте приватное окно, иначе домен возьмётся из кэша.)
Тесты, которые доказывают, что переезд удался
Тот же «план действий» из Части I — прогоняем на новом хосте:
# bash — базовые проверки с машины-клиента
curl -sI https://example.test/ | head -1 # HTTP/2 200
curl -s https://example.test/ | grep -c 'example.test' # в HTML уже новый домен
curl -sI https://example.test/wp-login.php | head -1 # админка отвечает- Откройте главную, внутренний пост, страницу без комментария; зайдите в
/wp-adminпод своим пользователем. - Загрузите картинку в медиа-библиотеку: этот шаг проверяет сразу и права на
wp-content/uploads, и что PHP-FPM может писать (частый «на проде не грузится», еслиchownзабыли). - Зайдите в Настройки → Общие: поля
WordPress address (URL)иSite address (URL)должны показывать новый адрес. - Если есть форма (поиск, комментарий, контакт) — проверьте одну отправку: это косвенно проверяет, что nonce/сессии живут на новом хосте.
Пока эти тесты зелёные на новом хосте по IP, трогать DNS нельзя. Пока хоть один красный — исправляем на новом хосте, старый не трогаем вообще (и пользователи продолжают спокойно работать на старом — это и есть «без простоя»).
Финальная дельта перед свитчем
Между «сняли слепок» (Часть II) и «переключаем DNS» (Часть V) на старом хосте могли написать новые посты/комментарии и загрузить новые картинки. Прямо перед переключением снимаем дельту — инкрементальный rsync файлов (дособерёт новые uploads) и свежий дамп базы (включит новые записи):
# bash — финальный добор файлов (только что изменилось после слепка)
rsync -avh --delete -e "ssh -i ~/.ssh/migrate_key" \
deploy@old.example.test:/var/www/example.test/wp-content/uploads/ \
/var/www/example.test/wp-content/uploads/
# bash — финальный дамп базы со старого + импорт на новый (команды из Части II)
# (после импорта повторите wp search-replace URL на свежем дампе!)Здесь --delete уместен: за время подготовки uploads мог поменяться, и дельта синхронизирует новое состояние. Но помните предупреждение из Бэкапов: если на старом что-то случайно удалили, --delete сотрёт это и здесь — перед финальным добором убедитесь, что дамп и rsync совпадают по времени слепка и что вы не тащите «дыру».
Часть V. Переключение и быстрый откат
Свитч
DNS переведён на новый IP (после снижения TTL в Части I). Дальше — наблюдаем, а не чиним вслепую:
# bash — с машины-клиента убедиться, что резолвер уже отдаёт новый IP и сайт отвечает
dig +short example.test A # должен показать NEW_SERVER_IP
curl -sI https://example.test/ | head -1Пока старый TTL (для тех, кто его закэшировал раньше) не истёк, часть трафика ещё идёт на старый хост. Старый хост в это время остаётся живым и на записи — там работают формы и комменты, поэтому и делался финальный добор дельты в Части IV. Через несколько часов (по старому TTL) весь трафик уходит на новый; старый хост пока оставляем включённым.
Быстрый откат = «DNS обратно»
Если на новом хосте проявился сюрприз (плагин не работает на новой PHP, кривой .htaccess, не поднялся HTTPS), самый дешёвый откат — не трогая файлы и базу, вернуть A-запись на старый IP. Короткий TTL из Части I гарантирует, что откат сработает быстро, а не через час. Старый хост за время тестов не удалялся и не перезаписывался — значит он и есть рабочая копия «как было». Только после того, как откатились и разобрались с новым хостом по IP, делаете вторую попытку свитча.
Файловый/БД-откат (заменить состояние нового хоста на слепок) — это путь Бэкапов, Часть VI сценарий A. Но он дороже по времени, чем «DNS обратно», и обычно не нужен: держите старый хост живым и не удаляйте его раньше, чем новая машина прожила несколько дней без сюрпризов.
Когда гасить старый хост
Не в день переезда. Через несколько дней: убедитесь, что весь трафик уже на новом (по access-логам нового хоста — он отдаёт 100% запросов), что на старом больше нет живой почты/кронов, которые вы не перенесли, что вы не боитесь «а вдруг надо вернуться». И только после этого снимайте старый хост, предварительно сделав финальную копию всего дерева и базы — не выбрасывайте слепок, с которого начали: он и есть ваш резерв Бэкапов.
Инструменты под задачу
Если вы мигрируете с Apache на Nginx (типично для переезда «shared-хостинг/старый VPS на Apache → свой VPS на Nginx»): физически переносятся файлы и база, а виртуальный хост придётся переписать с синтаксиса Apache (VirtualHost, .htaccess, RewriteRule) на nginx-блоки (server/location). Это тот редкий случай, где «чуть поправить» не работает — ключи и логика разные. Держать в голове обе системы, чтобы не перепутать Directory и location, — как раз рутина для инструмента, который переводит между ними. В codestack.space есть AI Nginx ⇄ Apache Converter: он переносит конфиг в другую систему, не подставляя валидные ключи одной в другую.
Конвертер Nginx ⇄ .htaccess
Он снимает механику переноса конфига; понимание «какие заголовки и редиректы вы хотите получить после переезда» (из Части III про HTTPS) остаётся за вами.
Часть VI. Что «забыли» при переезде — и почему оно всплывает после свитча
Каждый пункт ниже на вид мелочь и почти всегда молчит, пока вы не переключили DNS. Пройдитесь по ним до Части V.
- SSL не продлевается на новом хосте. certbot ставит systemd-таймер/auto-renew на той машине, где он был запущен. Переехали на новый хост, но авто-продление осталось на старом — сертификат на новом через 90 дней не обновится, и вы об этом узнаете, когда он истечёт. Проверьте
systemctl list-timers | grep -i certbotна новом хосте (Nginx про автопродление). - Системный
cronсайта не уехал. У WordPress два «крона»: встроенныйwp-cron(живёт с сайтом и уехал вместе с файлами) и, если вы разводили тяжёлые задачи на настоящий cron (бэкап-скрипт,wp cron event run), то crontab/systemd-timerна новом хосте надо завести заново — они в/etc/cron*и у пользователя, не в дереве сайта (см. Бэкапы, Часть IV). - Права и владелец не совпали. После
rsyncс другого uid/гид, как описано в Части II, PHP-FPM не может писать. Симптомы: «не загружается картинка», «не обновляется плагин», «не создаётсяdebug.log». Лечитсяchown -R, но не стоит доверять, что «на старом же было так» — на новом uid может быть другим. - Разные PHP-лимиты и
php.ini.upload_max_filesize,post_max_size,memory_limit,max_execution_timeна новом хосте по умолчанию другие → «форма загрузки падает на файле, который на старом проходил». Сверяем и правим пул/ini по PHP-FPM. upload_max_filesizeв PHP ≠ лимиту nginx/apache. Даже подняв PHP-лимит, большой загрузочный файл может резать nginx (client_max_body_size) — при переезде на Nginx это отдельный параметр, и после переезда с Apache его «по-старому» нигде нет.- Разные версии PHP/MySQL. Тема или плагин молча завёлся на PHP 8.1 и упал на 8.3; или
ONLY_FULL_GROUP_BYв новой MariaDB сломал кастомный запрос плагина. Отсюда и тест на новом хосте по IP в Части IV — это про это. - Почта (
mail()/SMTP) не настроена. На новом хосте нет настроенногоsendmail/SMTP-релей, и письма WordPress (сброс пароля, уведомления) не уходят. Отдельная тема — настраивается заново, а не переезжает. - Смена IP в списки/ограничения. Если на старом хосте были whitelist по IP (панели, внешние API,
fail2ban-исключения, «только для доверенных сетей»), они надо обновить под новый IP (Firewall/SSH). robots.txt,.well-known/acme-challenge/, спец-пути. Проверьте, что они на месте на новом хосте:acme-challengeнужен для выпуска/продления сертификата; если редирект/locationего перехватывает — сертификат не перевыпустится.- Не выставлены
WP_HOME/WP_SITEURLпри «жёстком» адресе. Обычно адреса берутся из базы и их правитwp search-replace. Но если вwp-config.phpу вас на старом хосте стояли жёсткие константыWP_HOME/WP_SITEURL(частый способ задать адрес), они приехали вwp-config.phpсо старым доменом и перекроют то, что в базе — их надо править там же. - После переезда влезла старая кэш-плагиновая копия. Если на старом хосте был страничный кэш (файлы
wp-content/.../cache), он приехал как есть и может отдавать старый HTML со ссылками на старый домен. Перед проверкой по IP очистите кэш.
Чек-лист граблей (кратко)
- TTL DNS не снижен заранее → переключение и откат «висят» по старому кэшу.
- Переключили DNS до проверки по IP → сюрприз увидят пользователи, а не вы.
- Меняли URL через `sed`/ручной SQL → битые сериализованные виджеты/настройки без явной ошибки.
- Импорт дампа «поверх» готового WordPress → конфликт и «полупереезд».
- Забыли финальный добор дельты → новые записи/uploads «потерялись» между слепком и свитчем.
- `chown`/права не сверили → загрузки и обновления молча падают.
- `client_max_body_size` (nginx) и PHP-лимиты разошлись → «большой файл не грузится».
- Не перевели cron/timer и авто-продление сертификата на новый хост → узнали об этом через 90 дней.
- Жёсткие `WP_HOME`/`WP_SITEURL` в `wp-config.php` со старым доменом → не дают смене адреса из базы подействовать.
- Старый хост погасили в день переезда → лишили себя дешёвого отката «DNS обратно».
- Старая копия страничного кэша приехала → отдаёт старый домен в HTML.
👤 Зарегистрируйтесь, чтобы добавить материал в закладки.
