Короткий ответ

404 после переноса почти никогда не «пропали файлы целиком». Обычно новый nginx/Apache смотрит не в /var/www/site.example, try_files не отдаёт фронтконтроллер, .htaccess не читается без AllowOverride, либо в БД остался старый siteurl. IP нового сервера — 10.0.20.40. Проверяйте через /etc/hosts или --resolve, не через публичный DNS: он ещё может бить в старый узел — DNS.

Не удаляйте старый webroot, пока новый стенд не отдаёт те же URL. План переноса: безопасный cutover.

Симптомы и как отличить

  • / открывается, /about/ — 404 nginx (короткая страница, не тема WordPress).
  • Все URL 404, даже index.php.
  • HTML есть, но CSS/картинки 404 из‑за абсолютных http://old.example.
  • 403 вместо 404 — права, не rewrite: 403.
ЧтоКуда смотреть
404 с Server: nginx и телом nginxroot/try_files
404 тема WordPress «страница не найдена»permalinks/БД, приложение живо
500PHP
Открывается другой сайтdefault_server / Host

Возможные причины

  1. root / DocumentRoot указывает на /var/www/html дефолта, архив лежит в /var/www/site.example.
  2. Файлы залиты в подкаталог public/ / web/, а vhost смотрит на уровень выше (или наоборот).
  3. Nginx без try_files $uri $uri/ /index.php?$args — ЧПУ отдают 404.
  4. Apache: AllowOverride None, RewriteEngine в .htaccess мёртв.
  5. WordPress permalink_structure, нет mod_rewrite / нет правил nginx.
  6. siteurl/home в wp_options со старым доменом или http://.
  7. Регистр: с Windows Index.PHP, на Ubuntu регистр важен.
  8. Симлинки, не попавшие в rsync без -l.
  9. Только часть файлов: забыли wp-content/uploads.

Диагностика

1. Куда реально ходит запрос

curl -sI --resolve site.example:80:10.0.20.40 http://site.example/ | head
curl -sI --resolve site.example:80:10.0.20.40 http://site.example/index.php | head
curl -sI --resolve site.example:80:10.0.20.40 http://site.example/not-a-file | head
sudo nginx -T 2>/dev/null | grep -A20 'server_name site.example'
sudo apache2ctl -S 2>/dev/null

Сравните root с ls /var/www/site.example.

2. Есть ли фронтконтроллер

ls -la /var/www/site.example/index.php /var/www/site.example/public/index.php /var/www/site.example/web/index.php 2>/dev/null
find /var/www/site.example -maxdepth 3 -name index.php

Laravel/Symfony часто требуют root .../public.

3. Rewrite

Nginx:

sudo grep -n try_files /etc/nginx/sites-enabled/*

Apache:

sudo apache2ctl -M | grep rewrite
sudo grep -n AllowOverride /etc/apache2/sites-enabled/*
sudo tail -n 20 /var/www/site.example/.htaccess

4. URL в приложении

WordPress (WP-CLI, если стоит):

sudo -u www-data wp --path=/var/www/site.example option get siteurl
sudo -u www-data wp --path=/var/www/site.example option get home
sudo -u www-data wp --path=/var/www/site.example option get permalink_structure

Без WP-CLI — SQL к таблице опций, не правьте сериализованные строки руками вслепую.

5. Регистр и пропущенные файлы

find /var/www/site.example -name '*.PHP' -o -name '*.HTM' | head
du -sh /var/www/site.example

Сравните размер с исходным сервером.

Решение

sudo tar -C /var/www -czf /root/site.example-before-fix-$(date +%F).tar.gz site.example

Сценарий A. Неверный root

server {
    listen 80;
    server_name site.example www.site.example;
    root /var/www/site.example;
    index index.php index.html;
    location / {
        try_files $uri $uri/ /index.php?$args;
    }
    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    }
}

Для приложения с public/:

root /var/www/site.example/public;

nginx -t и reload. Apache: DocumentRoot /var/www/site.example/public плюс <Directory> с AllowOverride All или явные RewriteRule в vhost (предпочтительнее, чем слепой All).

Сценарий B. ЧПУ WordPress на nginx

Нужен try_files на index.php. permalinks в админке «Простые» работают без rewrite, «Название записи» — нет. После фикса откройте «Настройки → Постоянные ссылки» и сохраните (пересоберёт правила Apache).

Сценарий C. siteurl в БД

Через WP-CLI:

sudo -u www-data wp --path=/var/www/site.example search-replace 'http://old.internal' 'https://site.example' --all-tables

Делайте на копии БД. Для сериализованных данных — только WP-CLI/wp search-replace, не голый sed по дампу.

Сценарий D. Регистр

Приведите имена к фактическим URL. Не включайте mod_speling как постоянное решение.

Сценарий E. Забыли uploads

Догоните rsync:

rsync -aH --numeric-ids src:/var/www/site.example/wp-content/uploads/ /var/www/site.example/wp-content/uploads/

Права www-data, не 777.

Сценарий F. Мультисайт и COOKIE_DOMAIN

После переноса поддомены shop.site.example 404, apex жив: в wp-config.php DOMAIN_CURRENT_SITE / PATH_CURRENT_SITE остались со старого стенда, nginx server_name без wildcard. Добавьте server_name site.example *.site.example только если сертификат SAN это покрывает, иначе отдельный vhost. DNS A для поддомена тоже должен смотреть на 10.0.20.40.

Регистр файлов: с Windows-архива Assets/Logo.PNG, URL в CSS /assets/logo.png — на ext4 404. Найдите find /var/www/site.example -iname logo.png. Не включайте mod_speling в проде.

Как проверить, что проблема устранена

Список URL из старого access.log / карты сайта:

for u in / /index.php /about/ /wp-login.php; do
  echo -n "$u "
  curl -s -o /dev/null -w '%{http_code}\n' --resolve site.example:443:10.0.20.40 "https://site.example$u"
done

Ожидание: 200/302 на живые страницы, 404 только на заведомо несуществующие. Главная не должна быть «чужим» default vhost.

Проверьте CSS: не mixed content из старых http-ссылок.

Если не помогло

  • 404 только с телефона: кэш CDN, Purge, Host.
  • index.php 200, ЧПУ 404 на Apache: AllowOverride и rewrite.
  • Мультисайт WordPress: DOMAIN_CURRENT_SITE в wp-config.php.
  • Старый сервер всё ещё в DNS — вы тестируете не тот узел.

Профилактика

  • Чеклист миграции: root, PHP socket, rewrite, search-replace, hosts-проверка, затем DNS.
  • Хранить эталон vhost в git.
  • Не полагаться на .htaccess как на единственный rewrite на nginx (он его не читает).

FAQ

Почему /index.php/about работает, а /about нет?

Rewrite не доводит ЧПУ до фронтконтроллера. Это классика после переноса на nginx.

Нужно ли копировать /var/www/html?

Нет, если сайт жил в /var/www/site.example. Дефолтный html Ubuntu — ловушка.

404 на .php, html открывается

PHP location не совпал (\.php$ vs try_files отдал файл как статику 404, если файла нет). Сверьте fastcgi_split_path_info.

Можно ли Alias /blog → другой каталог?

Да, но try_files и SCRIPT_FILENAME должны знать этот Alias. Иначе 404/500.

После смены домена 404 на превью Elementor/WP

Кэш плагина и старые URL в CSS. Сброс кэша после search-replace, не rm -rf wp-content.