Суть проблемы
Клиент перенёс сайт (WordPress + самописная часть на PHP) с одного хостинга на другой сервер. Перенос делали вручную — скопировали файлы, дампили БД, залили. После переключения DNS позвонили с формулировкой «вроде перенесли, но что-то не так»: местами белый экран, местами кривые ссылки, картинки не грузятся, формы не шлют, крон-задачи не выполняются. Ни одного чёткого сообщения об ошибке — только расплывчатое «всё сломалось».
- !Часть страниц отдаёт 404Маршруты битые: главная открывается, а ЧПУ-ссылки на записи и кастомные типы падают в 404 — rewrite rules не перенесены.
- !Картинки и ассеты не грузятсяБитые иконки вместо фото, стили подцепляются через http://old-domain.ru — браузер блокирует, ссылки в контенте ведут на старый домен.
- !Mixed content и молчащие интеграцииСайт на HTTPS, а ресурсы тянутся по http:// — браузер режет. Формы заявок и вебхуки во «молчании»: ни ошибки, ни доставки.
- !Ошибки 500 и неотправляемые файлыНа некоторых действиях (загрузка медиа через админку, запись в кэш) — 500. php-fpm не может писать в wp-content/uploads и storage.
- !Регулярные задачи не выполняютсяРассылки, бэкапы и интеграции по расписанию молчат: cron-задачи ссылаются на абсолютные пути старого сервера и тихо падают.
«Вроде всё перенесли, но что-то не так. То белый экран, то 404, картинки битые, заявки не приходят. Половина админки не работает. Срочно нужно — релиз уже стоит.»
Все страницы отдаются по корректным ЧПУ-маршрутам, ассеты и ссылки на новый домен, HTTPS без mixed content, формы доходят, медиа грузятся, cron выполняется по расписанию. Ни одного 404/500 по миграционным причинам.
Как диагностировал
Пост-миграционные «всё сломалось» почти всегда — это не одна ошибка, а лавина из шести типичных причин. Я прошелся по ним системно, от конфигов сервера до данных в БД, и на каждой получил конкретную корневую причину вместо абстрактного «что-то не так».
- Дифф конфигов старого и нового сервераСнял
nginx -Tна новом сервере и сравнил со старым.conf. Оказалось:server_name,root,try_filesи rewrite rules для ЧПУ не перенесены. Старый сервер был Apache+nginx, новый — чистый nginx без fallback на.htaccess, поэтому пермалинки и ЧПУ сломались. - Поиск зашитых абсолютных URLПроверил
wp_options(siteurl,home),wp_posts.guidи сериализованные массивы вwp_postmeta. Везде торчал старый доменhttp://old-domain.ru. Обычный SQL-REPLACEне подходит — ломает длину сериализованных строк, нужен корректный search-replace с пересчётом. - Аудит конфигов и envОткрыл
wp-config.phpи.env:DB_HOST,DB_PASSWORD, API-ключи и пути к/tmp/storageостались от старого сервера. Абсолютные пути/var/www/olduser/...на новом другие (/var/www/newuser/...), поэтому самописные модули не находили свои директории. - Проверка прав и владельца файловПрогнал
ls -la wp-content/uploads storage: владелецroot:rootпосле копирования черезscpпод root. php-fpm работает отwww-dataи не может писать → 500 при загрузке медиа и записи кэша. - Анализ mixed contentОткрыл DevTools → Network и Console. Сайт на HTTPS, но
<img>,<link>и ссылки в контенте тянутся поhttp://— браузер блокирует. Корень тот же, что в шаге 2: в БД зашитhttp://, а неhttps://. - Инспекция cron-задачВыполнил
crontab -lиgrep -r WP_CRON wp-config.php. Задания ссылались на/usr/bin/php /old/path/wp-cron.php— путь не существует. ПлюсDISABLE_WP_CRONне выставлен, но системный cron не настроен корректно, поэтому планировщик WordPress не запускался вовсе.
Что исправил
Каждый фикс бил в корневую причину, а не в симптом. Разбил работу на пять блоков: домен и ссылки, конфиг сервера и приложения, права файлов, mixed content и cron. Ниже — код «было/стало» по каждому блоку.
1. Домен и ссылки в БД
Старый домен http://old-domain.ru был зашит в siteurl, home, guid и сериализованных массивах wp_postmeta. Обычный SQL-REPLACE ломает сериализацию, поэтому использовал wp-cli с корректной обработкой — он пересчитывает длины строк в сериализованных данных. Колонку guid исключил намеренно: WordPress не должен менять guid при смене домена (это идентификатор фида, не URL для людей).
# Нельзя: ломает сериализованные массивы в wp_postmeta/wp_options
UPDATE wp_options SET option_value = REPLACE(option_value, 'http://old-domain.ru', 'https://new-domain.ru');
UPDATE wp_posts SET post_content = REPLACE(post_content, 'http://old-domain.ru', 'https://new-domain.ru');
UPDATE wp_posts SET guid = REPLACE(guid, 'http://old-domain.ru', 'https://new-domain.ru');
# Результат: сериализованные строки битые, виджеты и опции падают
# Корректный search-replace с пересчётом сериализованных данных
# --skip-columns=guid: guid не трогаем по правилам WP
wp search-replace 'http://old-domain.ru' 'https://new-domain.ru' \
--all-tables --skip-columns=guid --precise
# Обновляем siteurl/home явно на новый домен
wp option update siteurl 'https://new-domain.ru'
wp option update home 'https://new-domain.ru'
# Проверяем, что остатков старого домена нет
wp search-replace 'old-domain.ru' 'new-domain.ru' --dry-run --all-tables
2. Конфиг nginx и приложения
Новый сервер на чистом nginx, без Apache-fallback — .htaccess из корня WordPress молча игнорируется. Поэтому ЧПУ-маршруты (/blog/post-name/, кастомные типы записей) падали в 404. Переписал server-блок: правильный root, try_files с fallback на index.php, rewrite rules для WordPress и HTTPS-редирект. Параллельно поправил wp-config.php: данные БД, ключи и абсолютные пути под новый сервер.
server {
listen 80;
server_name new-domain.ru;
root /var/www/newuser/public_html;
index index.php;
location / {
# try_files нет — ЧПУ не работает, всё падает в 404
try_files $uri $uri/ =404;
}
# Нет HTTPS, нет редиректа, нет location для PHP
}
# HTTP → HTTPS редирект
server {
listen 80;
server_name new-domain.ru www.new-domain.ru;
return 301 https://new-domain.ru$request_uri;
}
server {
listen 443 ssl http2;
server_name new-domain.ru;
ssl_certificate /etc/letsencrypt/live/new-domain.ru/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/new-domain.ru/privkey.pem;
root /var/www/newuser/public_html;
index index.php;
client_max_body_size 64m;
location / {
# Fallback на index.php — основа ЧПУ WordPress
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass unix:/run/php/php8.1-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
location ~* \.(js|css|png|jpg|jpeg|gif|svg|ico|woff2?)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
}
# Осталось от старого сервера — подключения и пути битые
define( 'DB_HOST', 'localhost:3306' ); // новый хост другой
define( 'DB_PASSWORD', 'old_pass' ); // не подходит
define( 'WP_DEBUG', false ); // ошибки скрыты — белый экран без подсказок
define( 'UPLOADS', 'wp-content/uploads' );
// Самописный модуль искал /var/www/olduser/storage
# Актуальные данные нового сервера
define( 'DB_HOST', '127.0.0.1' );
define( 'DB_PASSWORD', 'new_secure_pass' );
# Включаем debug на время отладки (после фиксa выключить)
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', false );
# Абсолютные пути под новый сервер
define( 'STORAGE_PATH', '/var/www/newuser/storage' );
define( 'TMP_DIR', '/var/www/newuser/tmp' );
# Принудительно HTTPS в админке
define( 'FORCE_SSL_ADMIN', true );
# Отключаем HTTP- wp-cron — переведём на системный cron
define( 'DISABLE_WP_CRON', true );
3. Права файлов и владельцы
После scp под root владелец файлов стал root:root, а php-fpm работает от www-data. Отсюда 500 на записи в wp-content/uploads и storage. Вернул корректного владельца и привёл права к стандарту: директории 755, файлы 644, а на каталоги записи — 775 с группой www-data.
# Владелец root после копирования — php-fpm не может писать
$ ls -la wp-content/uploads
drwxr-xr-x root root uploads/
$ ls -la storage
drwxr-xr-x root root storage/
# Загрузка медиа → 500, запись кэша → 500
# Владелец — пользователь php-fpm
chown -R www-data:www-data /var/www/newuser/public_html
# Директории 755, файлы 644
find /var/www/newuser/public_html -type d -exec chmod 755 {} +
find /var/www/newuser/public_html -type f -exec chmod 644 {}
# Каталоги записи — 775 для группы www-data
chmod -R 775 /var/www/newuser/public_html/wp-content/uploads
chmod -R 775 /var/www/newuser/storage
chmod -R 775 /var/www/newuser/tmp
# wp-config.php закрыт от чужих глаз
chmod 640 /var/www/newuser/public_html/wp-config.php
4. Mixed content и HTTPS
Основную массу http:// закрыл wp search-replace из блока 1. Дополнительно добавил заголовок Content-Security-Policy: upgrade-insecure-requests — он заставляет браузер подтягивать все вложения по HTTPS, даже если где-то в контенте остался http://. Это страховка на будущее.
# В server-блок HTTPS: апгрейд всех http://-ресурсов до https://
add_header Content-Security-Policy "upgrade-insecure-requests" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
5. Cron и планировщик
Задачи в crontab ссылались на абсолютный путь старого сервера /var/www/olduser/..., которого на новом не существует — задания падали молча. Плюс WordPress-планировщик по умолчанию запускается по HTTP на каждом хите (wp-cron.php), что ненадёжно и тормозит фронт. Я отключил HTTP-wp-cron через DISABLE_WP_CRON и перевёл запуск на системный cron с реальным путём к php и wp-cron.php на новом сервере.
# Пути старого сервера — не существуют на новом, задания падают
*/5 * * * * /usr/bin/php /var/www/olduser/public_html/wp-cron.php
0 2 * * * /usr/bin/php /var/www/olduser/scripts/backup.php
*/15 * * * * /usr/bin/php /var/www/olduser/scripts/sync.php
# Тихо падает: No such file or directory, никто не замечает
# Плюс WP_CRON не отключён — двойной запуск и гонки
# WP_CRON отключён в wp-config.php — запускаем системным cron
# Пути актуальные для нового сервера
*/5 * * * * /usr/bin/php8.1 /var/www/newuser/public_html/wp-cron.php >/dev/null 2>&1
# Самописные задачи — через wp-cli с явным путём
0 2 * * * cd /var/www/newuser/public_html && /usr/bin/php8.1 scripts/backup.php >>/var/log/site/backup.log 2>&1
*/15 * * * * cd /var/www/newuser/public_html && /usr/bin/php8.1 scripts/sync.php >>/var/log/site/sync.log 2>&1
# Проверка, что cron вообще дёргается
# $ grep CRON /var/log/syslog | grep newuser
Результат
После применения всех фиксов прогнал проверку по каждому симптому: ЧПУ-маршруты, ассеты, формы, медиа, cron, HTTPS. Всё встало на место за один сеанс с чеклистом.
Сайт полностью работает на новом сервере: ЧПУ-маршруты отдаются корректно, ассеты и ссылки на новый домен, HTTPS без mixed content, формы доходят, медиа грузятся, cron выполняется по расписанию. Время восстановления — один рабочий сеанс. Составил клиенту чеклист переноса, чтобы больше не было «что-то не так».
- DNS переключён, A/AAAA-записи на новый сервер, TTL снижен заранее.
- SSL-сертификат выпущен и подключён (Let's Encrypt / коммерческий), редирект HTTP → HTTPS работает.
- Конфиг nginx: server_name, root, try_files, rewrite rules, location для PHP, client_max_body_size.
- wp-config.php / .env: DB_HOST, DB_PASSWORD, API-ключи, абсолютные пути под новый сервер.
- wp-cli search-replace старого домена на новый с --skip-columns=guid, siteurl/home обновлены.
- Права файлов: chown www-data, директории 755, файлы 644, каталоги записи 775.
- cron: пути переписаны, DISABLE_WP_CRON + системный cron с реальным путём к php.
- Редиректы со старого домена на новый (301) на период индексации.
- Мониторинг: логи nginx/php-fpm, проверка рассылок и интеграций, uptime-проверка.