Миграция рабочего WordPress-сайта — это процедура с высочайшей степенью риска. Для нагруженных интернет-магазинов на WooCommerce или контентных медиапорталов даже 10 минут простоя сайта или потеря одной транзакции означают прямой финансовый ущерб и падение поисковой выдачи Google/Яндекс.
При непрофессиональном подходе разработчики сталкиваются со следующими системными сбоями:
REPLACE() в SQL-файле или через Notepad++/Sublime. WordPress сохраняет сложные настройки тем и плагинов в виде PHP-сериализованных строк s:19:"http://oldsite.com". Замена строки на домен другой длины без пересчета байтов ломает десериализацию unserialize(), приводя к сбросу настроек и падению верстки.wp_posts: Изменение ссылок в колонке guid приводит к тому, что все RSS-читалки, новостные агрегаторы и внешние синдикаторы воспринимают все 50 000 постов сайта как абсолютно новые и повторно рассылают спам-уведомления подписчикам.utf8 вместо utf8mb4 приводит к превращению эмодзи и специфических иероглифов в знаки вопроса ???? без возможности восстановления.Инженерный стандарт AgenticLab 2.0 базируется на методике Zero Downtime Migration: предварительное понижение TTL, двухэтапная дифференциальная синхронизация медиафайлов через rsync, блокировка записи через SEO-безопасный 503 Service Unavailable, атомарный дельта-экспорт и рекурсивная замена URL через wp search-replace --precise --skip-columns=guid.
Надежная миграция продакшена никогда не делается за один шаг:
uploads) переносятся на новый сервер в фоновом режиме, пока сайт работает на 100% мощности.HTTP 503 Retry-After (что исключает санкции поисковых роботов), снимается быстрая дельта-копия БД (--single-transaction), синхронизируются последние измененные файлы через rsync, выполняется wp search-replace и переключается DNS.Процесс миграции объединяет операции на уровне файловой системы, сетевого уровня DNS и реляционной базы данных.
За 24–48 часов до миграции TTL A-записи домена уменьшается до 300 секунд (5 минут). Проверяется совпадение версий MySQL/MariaDB и кодировки таблиц (utf8mb4_unicode_ci).
Это гарантирует, что после переключения IP-адреса трафик переключится на новый сервер за считанные минуты.
Каталог wp-content/uploads (часто весящий 20–100 ГБ) копируется на целевой сервер в фоновом режиме без остановки старого сайта.
Применяются флаги сжатия трафика и исключения временных кэш-файлов.
Старый сайт переводится в режим обслуживания на уровне Nginx с отдачей статуса 503 и Retry-After: 300. Запись в базу блокируется, защищая от потери новых заказов.
Поисковые краулеры не понижают сайт в выдаче, понимая временный характер технических работ.
Снимается мгновенный дамп с флагами --single-transaction --quick. Финальный прогон rsync докачивает только те файлы, что появились во время фазы 1.
Время нахождения в maintenance-режиме составляет менее 2–3 минут даже для крупных проектов.
База импортируется на целевом сервере. WP-CLI безопасно перезаписывает URL с пересчетом длин сериализации. Сбрасываются правила ЧПУ и объектный кэш Redis.
Флаг --skip-columns=guid защищает RSS-ленты и глобальные идентификаторы от повреждения.
Первичная передача тяжелой папки uploads (95% данных) через rsync по SSH в фоновом режиме
Активация режима 503 на старом сервере; запрет POST-запросов и новых заказов
Экспорт базы через wp db export --single-transaction без блокировки чтения таблиц
Импорт в MySQL 8.4, выполнение wp search-replace --precise, сброс Redis (wp cache flush)
Обновление IP-адреса в DNS. Благодаря ранее сниженному TTL трафик мгновенно идет на новый хост
Проверка статус-кодов, SSL-сертификата Let's Encrypt и вывод старого сервера из эксплуатации
Почему выполнение прямого SQL-запроса `UPDATE wp_options SET option_value = REPLACE(option_value, 'https://old.com', 'https://new-long-domain.com')` приводит к фатальному сбою в работе тем и плагинов WordPress?
Зачем в команде `wp search-replace` обязательно указывать флаг `--skip-columns=guid` при переносе сайта на новый домен?
Ниже представлены четыре готовых производственных инструмента, реализующих промышленный стандарт миграции AgenticLab 2.0.
Скрипт выполняет проверку прав, исключает временные таблицы кэша, дампит базу с флагом --single-transaction (без блокировки чтения InnoDB) и производит десериализованную замену через wp search-replace.
#!/usr/bin/env bash
# ==============================================================================
# migrate-database.sh — Промышленный экспорт, перенос и search-replace базы данных
# ==============================================================================
set -euo pipefail
# Конфигурация серверов
SOURCE_SSH="[email protected]"
SOURCE_WP_PATH="/var/www/agenticlab"
SOURCE_URL="https://oldsite.agenticlab.dev"
TARGET_WP_PATH="/var/www/agenticlab"
TARGET_URL="https://newsite.agenticlab.dev"
REMOTE_DUMP="/tmp/wp_migration_source.sql"
LOCAL_DUMP="/tmp/wp_migration_target.sql"
# Таблицы, исключаемые из дампа (кэши, логи, сессии) для ускорения миграции
EXCLUDED_TABLES=(
"wp_wfhits"
"wp_wflogs"
"wp_wffilemods"
"wp_actionscheduler_logs"
)
echo "🔍 [1/5] Проверка доступности исходного сервера и WP-CLI..."
ssh "${SOURCE_SSH}" "wp core is-installed --path=${SOURCE_WP_PATH}"
echo "📦 [2/5] Снятие атомарного дампа БД (InnoDB Single-Transaction)..."
EXCLUDE_ARGS=""
for table in "${EXCLUDED_TABLES[@]}"; do
EXCLUDE_ARGS="${EXCLUDE_ARGS} --exclude_tables=${table}"
done
ssh "${SOURCE_SSH}" "wp db export ${REMOTE_DUMP} \
--path=${SOURCE_WP_PATH} \
--single-transaction \
--quick \
--default-character-set=utf8mb4 \
${EXCLUDE_ARGS}"
echo "📥 [3/5] Безопасная передача дампа на целевой сервер по SSH..."
scp "${SOURCE_SSH}:${REMOTE_DUMP}" "${LOCAL_DUMP}"
ssh "${SOURCE_SSH}" "rm -f ${REMOTE_DUMP}"
echo "💾 [4/5] Импорт базы данных на целевом сервере..."
wp db import "${LOCAL_DUMP}" --path="${TARGET_WP_PATH}"
rm -f "${LOCAL_DUMP}"
echo "🔄 [5/5] Рекурсивный search-replace с пересчетом сериализованных строк..."
wp search-replace "${SOURCE_URL}" "${TARGET_URL}" \
--path="${TARGET_WP_PATH}" \
--all-tables \
--precise \
--skip-columns=guid \
--report-changed-only
# Сброс объектного кэша и регенерация правил ЧПУ
echo "🧹 Сброс объектного кэша и ЧПУ..."
wp cache flush --path="${TARGET_WP_PATH}"
wp rewrite flush --path="${TARGET_WP_PATH}" --hard
echo "✅ База данных успешно перенесена и сконфигурирована под ${TARGET_URL}!"
Скрипт оптимизирован для передачи больших каталогов uploads: использует сжатие потока, контрольные суммы, исключает служебный мусор и поддерживает многопоточность.
#!/usr/bin/env bash
# ==============================================================================
# sync-media-rsync.sh — Высокопроизводительная синхронизация папки /uploads
# ==============================================================================
set -euo pipefail
SOURCE_SSH="[email protected]"
SOURCE_UPLOADS="/var/www/agenticlab/wp-content/uploads/"
TARGET_UPLOADS="/var/www/agenticlab/wp-content/uploads/"
echo "🚀 Запуск инкрементальной передачи медиатеки..."
# Параметры rsync:
# -a: архивный режим (сохранение прав, дат модификации, симлинков)
# -v: подробный вывод
# -z: сжатие данных на лету для экономии пропускной способности сети
# -P: отображение прогресса и возможность докачки при обрыве (partial + progress)
# --delete: удаление на приемнике файлов, которых больше нет на источнике (зеркалирование)
rsync -avzP --delete \
--exclude="*.tmp" \
--exclude="cache/" \
--exclude="et_temp/" \
--exclude="elementor/css/" \
--exclude=".DS_Store" \
-e "ssh -o Compression=yes -o StrictHostKeyChecking=accept-new" \
"${SOURCE_SSH}:${SOURCE_UPLOADS}" \
"${TARGET_UPLOADS}"
# Восстановление корректных прав доступа для Nginx и PHP-FPM
echo "🔒 Установка прав владения www-data:www-data..."
chown -R www-data:www-data "${TARGET_UPLOADS}"
find "${TARGET_UPLOADS}" -type d -exec chmod 755 {} +
find "${TARGET_UPLOADS}" -type f -exec chmod 644 {} +
echo "✅ Медиатека успешно синхронизирована!"
Конфигурация Nginx и легковесный шаблон ответа, предотвращающий падение позиций в поисковой выдаче во время финальной дельта-синхронизации.
# Nginx Maintenance Configuration с заголовком 503 Service Temporarily Unavailable
server {
listen 80;
listen 443 ssl;
server_name oldsite.agenticlab.dev;
# SSL конфигурация...
# Белый список IP-адресов инженеров, которым сайт доступен даже в режиме техработ
geo $maintenance_whitelist {
default 1;
192.168.1.100 0; # IP разработчика
203.0.113.50 0; # IP офиса
}
location / {
if ($maintenance_whitelist) {
return 503;
}
try_files $uri $uri/ /index.php?$args;
}
# Перехват ошибки 503 и отдача статического информационного экрана
error_page 503 @maintenance_screen;
location @maintenance_screen {
root /var/www/maintenance;
rewrite ^(.*)$ /maintenance.html break;
# КРИТИЧЕСКИ ВАЖНО: заголовок Retry-After сообщает поисковым роботам
# зайти повторно через 300 секунд, исключая удаление страниц из индекса
add_header Retry-After "300" always;
add_header Cache-Control "no-cache, no-store, must-revalidate" always;
}
}
Статический экран заглушки:
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Плановое обновление инфраструктуры | Fullstack Agentic Lab</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; background: #0f172a; color: #f8fafc; }
.card { max-width: 520px; padding: 40px; background: #1e293b; border-radius: 16px; border: 1px solid #334155; text-align: center; box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.5); }
h1 { font-size: 24px; margin-bottom: 16px; color: #38bdf8; }
p { font-size: 15px; line-height: 1.6; color: #94a3b8; }
.spinner { width: 40px; height: 40px; margin: 24px auto; border: 4px solid rgba(56, 189, 248, 0.2); border-top-color: #38bdf8; border-radius: 50%; animation: spin 1s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
</style>
</head>
<body>
<div class="card">
<div class="spinner"></div>
<h1>Плановое обновление платформы</h1>
<p>Мы переносим платформу на более производительные серверы для ускорения работы сайта. Время ожидания составит менее 3 минут.</p>
<p><small>Страница обновится автоматически по завершении работ.</small></p>
</div>
<script>
setTimeout(() => window.location.reload(), 15000);
</script>
</body>
</html>
Скрипт проверяет целевой хост перед финальным переключением DNS: статус-коды страниц, отсутствие старых ссылок в базе, целостность медиафайлов и доступность API.
#!/usr/bin/env bash
# ==============================================================================
# verify-migration.sh — Комплексная проверка целостности целевого сервера
# ==============================================================================
set -euo pipefail
TARGET_HOST="198.51.100.25" # IP-адрес нового сервера
DOMAIN="newsite.agenticlab.dev"
OLD_DOMAIN="oldsite.agenticlab.dev"
echo "🔎 [1/4] Проверка HTTP-статуса главной страницы через прямой IP..."
HTTP_STATUS=$(curl -k -s -o /dev/null -w "%{http_code}" --resolve "${DOMAIN}:443:${TARGET_HOST}" "https://${DOMAIN}/")
if [[ "${HTTP_STATUS}" -ne 200 ]]; then
echo "❌ ОШИБКА: Главная страница вернула HTTP ${HTTP_STATUS} вместо 200 OK!"
exit 1
fi
echo "✅ Главная страница отвечает 200 OK."
echo "🔎 [2/4] Проверка REST API эндпоинта ядра..."
REST_STATUS=$(curl -k -s -o /dev/null -w "%{http_code}" --resolve "${DOMAIN}:443:${TARGET_HOST}" "https://${DOMAIN}/wp-json/")
if [[ "${REST_STATUS}" -ne 200 ]]; then
echo "❌ ОШИБКА: WP REST API недоступен (HTTP ${REST_STATUS})!"
exit 1
fi
echo "✅ WP REST API доступен."
echo "🔎 [3/4] Проверка базы на наличие оставшихся упоминаний старого домена..."
REMAINING_OLD_URLS=$(wp search-replace "${OLD_DOMAIN}" "${DOMAIN}" --dry-run --report-changed-only | grep -E "Found [1-9]" || true)
if [[ -n "${REMAINING_OLD_URLS}" ]]; then
echo "⚠️ ВНИМАНИЕ: В базе обнаружены остаточные ссылки на старый домен:"
echo "${REMAINING_OLD_URLS}"
else
echo "✅ База данных полностью очищена от ссылок на старый домен."
fi
echo "🔎 [4/4] Проверка работы объектного кэша Redis..."
wp cache get dummy_test_key || true
wp cache set dummy_test_key "ok" 60
CACHED_VAL=$(wp cache get dummy_test_key)
if [[ "${CACHED_VAL}" == "ok" ]]; then
echo "✅ Объектный кэш функционирует штатно."
wp cache delete dummy_test_key
else
echo "❌ ОШИБКА: Сбой объектного кэша!"
exit 1
fi
echo "🎉 Все тесты пройдены! Целевой сервер готов к переключению DNS."
guid при выполнении search-replace#wp search-replace по умолчанию перезаписывает все текстовые колонки, включая guid таблицы wp_posts.--skip-columns=guid.ERR_TOO_MANY_REDIRECTS.wp-config.php:if ( isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) && 'https' === $_SERVER['HTTP_X_FORWARDED_PROTO'] ) {
$_SERVER['HTTPS'] = 'on';
}