Разбор 09

«Посмотрите, что-то не так» — сломалось после переноса

Перенесли сайт на новый сервер — «что-то перестало работать, всё сломалось». Разбираю типичную пост-миграционную лавину: ссылки, env, права, крон, пути.

миграция nginx Apache wp-cli env права файлов cron debug
Клиент
веб-студия (перенос сайта клиента)
Стек
WordPress + самописные модули, nginx, PHP-FPM, MySQL, cron
Срок
1 рабочий сеанс
Итог
сайт полностью работает на новом сервере

Суть проблемы

Клиент перенёс сайт (WordPress + самописная часть на PHP) с одного хостинга на другой сервер. Перенос делали вручную — скопировали файлы, дампили БД, залили. После переключения DNS позвонили с формулировкой «вроде перенесли, но что-то не так»: местами белый экран, местами кривые ссылки, картинки не грузятся, формы не шлют, крон-задачи не выполняются. Ни одного чёткого сообщения об ошибке — только расплывчатое «всё сломалось».

Отклик клиента До

«Вроде всё перенесли, но что-то не так. То белый экран, то 404, картинки битые, заявки не приходят. Половина админки не работает. Срочно нужно — релиз уже стоит.»

Состояние сайта После

Все страницы отдаются по корректным ЧПУ-маршрутам, ассеты и ссылки на новый домен, HTTPS без mixed content, формы доходят, медиа грузятся, cron выполняется по расписанию. Ни одного 404/500 по миграционным причинам.

Как диагностировал

Пост-миграционные «всё сломалось» почти всегда — это не одна ошибка, а лавина из шести типичных причин. Я прошелся по ним системно, от конфигов сервера до данных в БД, и на каждой получил конкретную корневую причину вместо абстрактного «что-то не так».

  1. Дифф конфигов старого и нового сервераСнял nginx -T на новом сервере и сравнил со старым .conf. Оказалось: server_name, root, try_files и rewrite rules для ЧПУ не перенесены. Старый сервер был Apache+nginx, новый — чистый nginx без fallback на .htaccess, поэтому пермалинки и ЧПУ сломались.
  2. Поиск зашитых абсолютных URLПроверил wp_options (siteurl, home), wp_posts.guid и сериализованные массивы в wp_postmeta. Везде торчал старый домен http://old-domain.ru. Обычный SQL-REPLACE не подходит — ломает длину сериализованных строк, нужен корректный search-replace с пересчётом.
  3. Аудит конфигов и envОткрыл wp-config.php и .env: DB_HOST, DB_PASSWORD, API-ключи и пути к /tmp/storage остались от старого сервера. Абсолютные пути /var/www/olduser/... на новом другие (/var/www/newuser/...), поэтому самописные модули не находили свои директории.
  4. Проверка прав и владельца файловПрогнал ls -la wp-content/uploads storage: владелец root:root после копирования через scp под root. php-fpm работает от www-data и не может писать → 500 при загрузке медиа и записи кэша.
  5. Анализ mixed contentОткрыл DevTools → Network и Console. Сайт на HTTPS, но <img>, <link> и ссылки в контенте тянутся по http:// — браузер блокирует. Корень тот же, что в шаге 2: в БД зашит http://, а не https://.
  6. Инспекция 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 для людей).

bash — ручной SQL (сломанный)было
# Нельзя: ломает сериализованные массивы в 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');
# Результат: сериализованные строки битые, виджеты и опции падают
bash — wp-cli search-replaceстало
# Корректный 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: данные БД, ключи и абсолютные пути под новый сервер.

/etc/nginx/sites-available/new-domain.confбыло
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
}
/etc/nginx/sites-available/new-domain.confстало
# 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";
    }
}
wp-config.phpбыло
# Осталось от старого сервера — подключения и пути битые
define( 'DB_HOST', 'localhost:3306' );   // новый хост другой
define( 'DB_PASSWORD', 'old_pass' );   // не подходит
define( 'WP_DEBUG', false );            // ошибки скрыты — белый экран без подсказок
define( 'UPLOADS', 'wp-content/uploads' );
// Самописный модуль искал /var/www/olduser/storage
wp-config.phpстало
# Актуальные данные нового сервера
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.

bashбыло
# Владелец 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
bashстало
# Владелец — пользователь 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://. Это страховка на будущее.

nginx — заголовок для upgrade-insecure-requestsстало
# В 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 на новом сервере.

crontab -eбыло
# Пути старого сервера — не существуют на новом, задания падают
*/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 не отключён — двойной запуск и гонки
crontab -eстало
# 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. Всё встало на место за один сеанс с чеклистом.

множество → 0
0
404 и битых маршрутов
сотни → 0
0
ссылок на старый домен
регулярно → 0
0
ошибок 500 от прав и путей
все молчали → по расписанию
100%
cron-задач выполняется

Сайт полностью работает на новом сервере: ЧПУ-маршруты отдаются корректно, ассеты и ссылки на новый домен, 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-проверка.
Есть похожая проблема?

Сначала найдём причину. Потом решим, что действительно нужно исправить.

Опишите симптом и что из-за него перестало работать. Я посмотрю, где искать причину и насколько задача похожа на точечный фикс.

Описать проблему ↗