Перенос сайта: домен и хостинг

Домен и хостинг

У каждого сайта две части, и они могут принадлежать разным компаниям:

Одна компания может продавать и то и другое, но не обязана. До переезда выясните по каждой части, у кого она, на чьё имя записана и как вы получаете доступ. Практические руководства по двум общим хостингам из примера: Beget и DreamHost. Инструменты, которые понадобятся: curl, SSH, git.

Сравните ещё до оплаты, сколько вы платите за сайт сейчас и сколько будете платить у нового хостера: разница бывает в несколько тысяч рублей в год. Домен может быть записан и на человека, и на ООО, а хостинг продаваться совсем другой компанией — проверьте, что домен записан на вас, а не на провайдера, который только перепродаёт чужие домены.

Перенос домена

Домен записан на человека или на компанию. Убедитесь, что он ваш. Регистратор и статус домена открыты на lookup.icann.org и в выводе whois:

whois example.com

[Querying whois.iana.org:43 'example.com'] [whois.iana.org] % IANA WHOIS server % for more information on IANA, visit http://www.iana.org % This query returned 1 object domain: EXAMPLE.COM organisation: Internet Assigned Numbers Authority created: 1992-01-01 source: IANA

example.com — зарезервированное имя, поэтому ответ останавливается на IANA и регистратора в нём нет. У настоящего домена ищите Registrar:, Registrar WHOIS Server:, Domain Status: и Registry Expiry Date:. Контактный email должен быть тем, который вы читаете: на него регистратор присылает коды для переноса.

Сервисов WHOIS много, и обновляют данные они с разной периодичностью, поэтому проверьте сразу несколько. Регистраторами называются организации, у которых есть право регистрировать домены, и не каждый хостинг-провайдер им является: в России один из крупнейших — reg.ru.

Три вопроса к DNS отвечают, где сейчас живут сайт и его почта. NS называет серверы, которые отвечают за домен, A — адрес сайта, MX — почтовые серверы:

dig +short NS example.com

ns2.dreamhost.com. ns1.dreamhost.com. ns3.dreamhost.com.

dig +short A example.com

203.0.113.10

dig +short MX example.com

Запрос MX ничего не печатает: этот домен не получает почту, поэтому почтовой записи, которую надо сохранить, у него нет.

Запишите ответы. После переезда NS и A должны указывать на новый хостинг, а MX — по-прежнему на работающий почтовый сервер. Что значат записи: DNS.

Чтобы перенести домен вроде .com к другому регистратору, нужен его код переноса (auth code, EPP code) от старого регистратора, а сам домен должен быть разблокирован. Обычно он не может переехать в первые 60 дней после регистрации или прошлого переноса. У страновых доменов свои правила; спросите регистратора. Учтите ещё, сколько стоит продление в год сейчас и у нового регистратора. Для переезда одного хостинга код переноса не нужен: домен остаётся у своего регистратора, меняются только его записи DNS.

Снизить TTL

Ответы DNS кешируются на время жизни записи, TTL. Запись с TTL в один час ещё час после переключения отправляет посетителей на старый сервер. Снизьте TTL за день до переезда у DNS-провайдера, который держит записи сейчас. Достаточно значения 300 секунд:

dig example.com A

example.com. 59 IN A 203.0.113.10

TTL — это число во втором столбце ответа. Поставьте 300 для записей A, AAAA и CNAME сайта. Подождите не меньше старого TTL, прежде чем переключать: так старое значение уйдёт из всех кешей. На Beget записи лежат в панели управления; панель DNS-провайдера делает то же самое, когда домен использует его серверы имён.

Копирование файлов

Для файлов нужен доступ к хостингу: панель управления, или FTP, или SFTP с адресом, именем пользователя и паролем, или SSH. Копируйте по SSH: так сохраняются отметки времени, а права можно выставить в том же проходе:

rsync -av --chmod=D755,F644 ~/site/ dh_user@<server>.dreamhost.com:~/example.com/

-a сохраняет время, ссылки и права. --chmod=D755,F644 затем перекрывает права в копии: каждая папка становится 755, каждый файл — 644. Без него права локальных файлов переезжают как есть. В рабочей копии часто папки 700, а файлы 600, и Apache, который работает под другим пользователем, не может их читать. Именно это случилось в проблеме 3 примера. После загрузки обе проверки должны напечатать 0:

find ~/example.com -type d ! -perm 755 | wc -l

find ~/example.com -type f ! -perm 644 | wc -l

Выкладка, которая копирует только отслеживаемые git файлы, пропускает картинки и другие игнорируемые файлы. Сначала перечислите игнорируемые файлы, затем скопируйте этот список:

cd ~/site

git ls-files --others --ignored --exclude-standard > /tmp/untracked.txt

wc -l /tmp/untracked.txt

rsync -av --chmod=D755,F644 --files-from=/tmp/untracked.txt ./ dh_user@<server>.dreamhost.com:~/example.com/

Проверьте, что каждая картинка на новом хостинге отвечает 200: пропавшая картинка — это сломанная страница, которую не увидит ни одна проверка HTML. Как хранить такой список в репозитории: Изображения вне git: список файлов в репозитории.

Перенести сайт может и сам новый хостер: часто достаточно сообщить ему данные доступа к панели или по FTP, и файлы с базой он скопирует сам. Спросите об этом до оплаты: у одного провайдера перенос входит в тариф, у другого это отдельная услуга. FTP-клиент вам, скорее всего, не понадобится.

Копирование базы данных

Сайту с MySQL или MariaDB нужны ещё и данные. Выгрузите их на старом хостинге одной транзакцией, вместе с процедурами и триггерами:

mysqldump --single-transaction --routines --triggers -u user -p example > ~/backups/example-$(date +%F).sql

--single-transaction делает согласованный снимок, не блокируя таблицы нагруженного сайта. --routines и --triggers сохраняют хранимые процедуры и триггеры, которые обычный дамп выбрасывает. С -p команда спрашивает Enter password: и больше ничего не печатает при успехе. Скопируйте файл на новый хостинг:

scp ~/backups/example-2026-11-07.sql dh_user@<server>.dreamhost.com:~/backups/

Создайте базу и её пользователя в панели нового хостинга, затем загрузите в неё файл:

mysql -u user -p example < ~/backups/example-2026-11-07.sql

С -p команда спрашивает Enter password: и больше ничего не печатает при успехе.

Посчитайте таблицы с обеих сторон; числа должны совпасть:

mysql -u user -p -N -e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'example';"

Команды одинаковы для MySQL и MariaDB. База с другим именем или другой пользователь на новом хостинге требуют поправить дамп или загрузить его под новым именем; и то и другое видно в панели.

Проверка до DNS

Не меняйте DNS первым и не отлаживайте потом. Новый сервер отвечает за домен, как только настроен его веб-сервер, даже пока публичный DNS указывает на старый. Принудительно направьте соединение на новый адрес через --resolve:

curl -sS --resolve example.com:443:203.0.113.10 -o /dev/null -w "%{http_code} %{remote_ip}\n" https://example.com/

200 203.0.113.10

--resolve example.com:443:203.0.113.10 соединяется с 203.0.113.10, но отправляет example.com в заголовке Host и в рукопожатии TLS, поэтому сертификат и каждый виртуальный хост совпадают с настоящим именем. %{remote_ip} в выводе показывает, какой сервер ответил. Проверьте главную и несколько внутренних страниц, http и https, и один адрес, который должен отвечать 404.

Браузеру вместо этого нужна строка в /etc/hosts. Добавьте её редактором от root и удалите после проверки:

sudo vi /etc/hosts

203.0.113.10 example.com www.example.com

Теперь обычный адрес в браузере открывает новый сервер. Проверьте сертификат, редирект на https и форму, которая пишет в базу данных. Уберите строку, когда закончите, иначе браузер продолжит видеть новый сервер и после ошибки.

Смена DNS

Когда новый сайт отвечает правильно, смените серверы имён у регистратора или записи A у DNS-провайдера на новый хостинг. Если домен остаётся у того же регистратора и меняется только хостинг, обычная правка — одна запись A. Если переезжает весь DNS, регистратор получает новые серверы имён, например четыре у Beget или три у DreamHost.

Следите за изменением из публичного резолвера, а не с сервера, который его сделал:

dig +short NS example.com

ns2.dreamhost.com. ns1.dreamhost.com. ns3.dreamhost.com.

dig +short A example.com @8.8.8.8

203.0.113.10

curl -sS -o /dev/null -w "%{http_code} %{remote_ip}\n" https://example.com/

200 203.0.113.10

Адрес в %{remote_ip} должен быть адресом нового сервера. Держите старый хостинг оплаченным и работающим неделю: часть резолверов и браузеров кеширует старый адрес на старый TTL, и откат тогда — это одна правка DNS.

Чек-лист

ЗадачаКоманда или проверка
Сравнить цену за сайтсколько платите сейчас и сколько будете платить у нового хостера, в год
Посмотреть доменwhois example.com, lookup.icann.org
Записать регистратора, email домена и хостеракто где, на чьё имя записано и как вы входите
Записать текущий DNSdig +short NS example.com, dig +short A example.com, dig +short MX example.com
Снизить TTL за деньdig example.com A, затем поставить 300 у DNS-провайдера
Получить код переноса и разблокировать доментолько когда меняется регистратор
Скопировать файлы с безопасными правамиrsync -av --chmod=D755,F644 ~/site/ user@host:~/example.com/
Скопировать неотслеживаемые файлыgit ls-files --others --ignored --exclude-standard, затем rsync по списку
Выгрузить и загрузить базу данныхmysqldump --single-transaction --routines --triggers, затем mysql ... < dump.sql
Проверить новый сервер до DNScurl --resolve example.com:443:<new-ip> https://example.com/
Сменить DNSсерверы имён у регистратора или запись A у DNS-провайдера
Проверить, что новый адрес отвечаетdig +short A example.com @8.8.8.8, curl -w "%{remote_ip}"
Проверить картинки и внутренние страницыcurl -o /dev/null -w "%{http_code}" для каждой
Найти файлы, которые уводит редиректом правило для страниц301 на путях картинок и скриптов в журнале доступа: awk '$9 == 301 && $7 ~ /\.(jpe?g|png|webp|gif|svg|js|css)$/ {print $7}' ~/logs/example.com/https/access.log | sort | uniq -c (проблема 6)
Загружать в корень документов, а не в подпапкуисточник со слешем: rsync -av ~/site/ ..., затем пробный запуск -n (проблема 7)
Выключить списки файлов в папкахOptions -Indexes; папка без индексной страницы отвечает 403 (проблема 8)
Убрать с хостинга резервные копии и архивысравнить хостинг с git ls-files, просмотреть список, удалить (проблема 9)
Остановить рост журнала ошибокgrep -c 'Creation of dynamic property', затем объявить свойства (проблема 10)

Как выбрать новый: Какой хостинг выбрать. Правила, которые часто ломаются при переезде: Создание правил в .htaccess.

Пример: с Beget на DreamHost

Это настоящий переезд, имена изменены: домен — example.com, логин на Beget — user с домашней папкой /home/u/user, логин на DreamHost — dh_user, сервер — <server>.dreamhost.com. Сайт — написанный вручную сайт на PHP без базы данных. Большую часть времени заняли десять проблем; каждая ниже — это симптом, команда, которая его показывает, вывод и исправление.

Каждая проблема кончается блоком Проверьте, есть ли эта проблема у вас: команды для своего хостинга и что значит их вывод. Вывод в этих блоках взят с переехавшего сайта и из Docker-образов, похожих на Beget и DreamHost, имена заменены так же.

ПроблемаСимптомПричина
1500 на каждый запросphp_flag без mod_php
2каждый URL перенаправляется сам на себяправило https проверяет только заголовок прокси
3около 90 страниц отвечают 404 или 403папки 700 и файлы 600
4старые URL отвечают 404 на новом хостингеустаревшие копии на старом хостинге, а не в репозитории
5пропали картинкизагрузка по git пропускает игнорируемые файлы
6картинки и скрипты из одной папки отвечают 301правило для PATH_INFO подходит и к папке с именем .php
7новые страницы отвечают 404 после каждой загрузкизагрузка попала в подпапку
8папки без индексной страницы показывают список файловперечисление папок включено по умолчанию
9резервные копии и архивы из рабочей папки открытызагружалась вся рабочая папка
10журнал ошибок вырос до 230 МБ в деньпредупреждения о динамических свойствах на каждом запросе

Проблема 1: 500 на каждый запрос

Симптом. Первый же запрос, который записал в лог новый сервер, уже ответил 500, главная страница в том числе. Встроенная страница Apache сообщала, что сервер обнаружил внутреннюю ошибку. Заголовки ответа:

curl -sS -D - -o /dev/null https://example.com/

HTTP/2 500 content-length: 533 content-type: text/html; charset=iso-8859-1 server: Apache

Команда, которая это показывает. В логе ошибок названы файл и директива:

tail -n 1 ~/logs/example.com/https/error.log

[Fri Nov 06 23:29:54 2026] [example.com] [alert] [client 203.0.113.7:37118] [pid 3064457] config.c(2045): /home/user/example.com/.htaccess: Invalid command 'php_flag', perhaps misspelled or defined by a module not included in the server configuration

Почему. Beget запускает PHP как mod_php, где php_flag и php_value работают. DreamHost запускает PHP отдельным процессом FastCGI под управлением mod_fcgid и mod_php не загружает, поэтому Apache не знает этих слов: Почему php_flag отвечает 500. Полный разбор — в статье 500 на каждой странице после переезда.

Исправление. Сначала сделайте копию файла:

mkdir -p ~/backups

cp -p .htaccess ~/backups/example.com.htaccess.$(date +%Y%m%d-%H%M%S)

Оберните двенадцать строк в IfModule, чтобы хостинг без mod_php их пропускал:

vi .htaccess

<IfModule mod_php.c> php_flag display_errors off php_flag log_errors on # остальные десять строк остаются внутри этого блока </IfModule>

Затем положите те же настройки в файл, который читает DreamHost, и перезапустите PHP-процесс:

vi ~/.php/8.5/phprc

display_errors = Off log_errors = On error_reporting = -1

pkill -x php85.cgi

Защитные блоки в общем виде: IfModule. Проверка, которая отличает mod_php от FastCGI: Какой у вас сервер.

Та же проверка после переезда нашла такие строки ещё в шести файлах .htaccess во вложенных папках демонстраций PHP. 500 отвечали только эти папки; остальной сайт работал, а строка [alert] выше называет файл. Каждой папке нужен защитный блок IfModule и свой .user.ini; на хостинге user_ini.filename = .user.ini и user_ini.cache_ttl = 300, поэтому изменение видно через 5 минут. Не ~/.php/8.5/phprc: этот файл действует на весь аккаунт, а здесь настройки одной папки.

vi demo/.user.ini

; PHP FastCGI читает этот файл для каждой папки; mod_php читает .htaccess session.use_cookies = 1 session.use_only_cookies = 1 session.use_trans_sid = 0 session.use_strict_mode = 1 session.cache_limiter = nocache session.cookie_samesite = "Lax" session.name = AOSESSID session.save_path = "./tmp" session.cookie_path = "/" session.cookie_lifetime = 0 session.cookie_httponly = 1 session.cookie_secure = 1

Проверьте, есть ли эта проблема у вас

Найдите директивы PHP во всех файлах .htaccess сайта. Запустите это на сервере по SSH или в локальной копии корня документов:

grep -rn --include=.htaccess -E '^\s*php_(flag|value|admin_flag|admin_value)\s' ~/example.com

Затем спросите сервер, как он запускает PHP. Положите пробный файл в корень документов, запросите его и удалите:

printf '<?php echo php_sapi_name(), "\n";' > ~/example.com/sapi-check.php

curl -sS https://example.com/sapi-check.php

rm ~/example.com/sapi-check.php

Проблема есть, если grep выводит строки, а пробный файл выводит что угодно, кроме apache2handler, например cgi-fcgi (FastCGI, как на DreamHost):

/home/user/example.com/demo/.htaccess:3:php_flag session.use_cookies on /home/user/example.com/demo/.htaccess:4:php_flag session.use_only_cookies on

cgi-fcgi

Тогда папка каждого найденного файла и все папки ниже отвечают 500. Строка внутри блока <IfModule mod_php.c> безопасно пропускается, поэтому посмотрите каждый файл, который назвал grep. Проблемы нет, если grep ничего не выводит или пробный файл выводит apache2handler (mod_php, как на Beget).

Проблема 2: каждый URL перенаправлялся сам на себя

Симптом. Когда 500 не стало, каждый адрес отвечал 301, а целью был он сам. Один запрос:

curl -sS -o /dev/null -w "%{http_code} %{redirect_url}\n" https://example.com/

301 https://example.com/

Команда, которая это показывает. Если пойти по редиректу, виден цикл:

curl -sS -L -o /dev/null https://example.com/

curl: (47) Maximum (50) redirects followed

Почему. Старое правило https проверяло только заголовок, который присылает фронтенд Beget:

RewriteCond %{HTTP:X-Forwarded-Proto} !https RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]

DreamHost завершает TLS в самом Apache и такого заголовка не присылает, поэтому «не https» было верно для каждого запроса, включая https.

Исправление. Добавьте условие на состояние TLS, которое видит сам Apache:

vi .htaccess

RewriteCond %{HTTPS} off RewriteCond %{HTTP:X-Forwarded-Proto} !https RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]

На DreamHost %{HTTPS} равно on для https, и первое условие останавливает правило. За старым прокси %{HTTPS} всегда off, и решает заголовок. Браузеры кешируют 301: после такого цикла проверяйте в приватном окне. Вся история — в статье Переезд на DreamHost: цикл; как написать правило для обоих видов хостинга: Redirect 301 или пара.

Проверьте, есть ли эта проблема у вас

Запросите главную страницу по https, не следуя редиректам:

curl -sS -o /dev/null -w "%{http_code} %{redirect_url}\n" https://example.com/

Проблема есть, если ответ — 301 на тот же адрес:

301 https://example.com/

Если пройти по редиректам, цикл подтвердится:

curl -sS -L --max-redirs 5 -o /dev/null -w "%{http_code}\n" https://example.com/

curl: (47) Maximum (5) redirects followed 301

Проблемы нет, если первая команда выводит 200 или 301 на другой адрес, который затем отвечает 200.

Проблема 3: около 90 страниц отвечали 404 или 403

Симптом. Страницы, которые работали на Beget, на DreamHost отвечали 403 Forbidden или 404 Not Found. Файлы были на месте, пути тоже.

Команда, которая это показывает. Сравните права папки и её дочерней папки:

stat -c '%a %U:%G %n' dev/python dev/python/files

755 dh_user:pg000000 dev/python 700 dh_user:pg000000 dev/python/files

Почему. У 69 папок в локальной копии были права 700, а у примерно 450 файлов — 600 или 700. rsync скопировал эти права, как и zip-архив, распакованный на сервере. На новом хостинге Apache, который работает под другим пользователем, не мог войти в папки и прочитать файлы.

Исправление. Сохраните старые права, затем откройте папки и файлы на сервере:

find ~/example.com -printf '%m %p\n' > ~/backups/example.com.modes.$(date +%Y%m%d-%H%M%S)

find ~/example.com -type d -exec chmod 755 {} +

find ~/example.com -type f -exec chmod 644 {} +

Исправьте локальную копию так же, иначе следующая загрузка вернёт права обратно. С этого момента копируйте с rsync --chmod=D755,F644, как в разделе Копирование файлов. Сторона DreamHost: SSH и права файлов; контейнер, который воспроизводит 403: проверка 5.

Проверьте, есть ли эта проблема у вас

На сервере выведите папки, в которые другие пользователи не могут войти, и файлы, которые они не могут прочитать. Apache обычно работает от другого пользователя, чем владелец файлов. Файлы сессий PHP (sess_*) и журналы закрыты нарочно, поэтому вторая команда их пропускает:

find ~/example.com -type d ! -perm -o=x

find ~/example.com -type f ! -perm -o=r ! -name 'sess_*' ! -name '*.log'

Проблема есть, если хоть одна команда выводит путь. Каждая такая папка и каждый такой файл отвечают 403:

/home/user/example.com/closed /home/user/example.com/notes.html

Проблемы нет, если обе команды ничего не выводят.

Проблема 4: старые URL отвечали 200 на Beget и 404 на DreamHost

Симптом. Часть адресов, которые ещё работали на старом хостинге, на новом отвечала 404. Этих старых адресов в репозитории не было вовсе.

Команда, которая это показывает. У старого сайта были папки верхнего уровня /centos/, /rocky/, /ubuntu/ и /debian/. Их страницы в репозитории переехали в /linux/..., но старый хостинг по-прежнему отвечал 200 на старые пути, потому что загрузка никогда не удаляет файлы на сервере. На новом хостинге был только репозиторий. Его лог доступа показывал 404:

grep '"GET /centos/network/firewall.php ' ~/logs/example.com/https/access.log

198.51.100.23 - - [07/Nov/2026:00:34:30 -0800] "GET /centos/network/firewall.php HTTP/2.0" 404 5665 "https://www.example.com/" "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1"

Почему. Это были устаревшие копии на старом хостинге, а не страницы репозитория. На новом хостинге по старым путям не было ничего.

Исправление. До переезда перечислите, что старый хостинг отдаёт, а репозиторий не содержит. Карта сайта — хорошее начало:

curl -sS https://example.com/sitemap.xml | grep -o "<loc>[^<]*" | cut -c 6- > /tmp/old-urls.txt

wc -l /tmp/old-urls.txt

5523

Проверьте список на новом хостинге после загрузки. Пары правил добавьте после всех точных правил для этих папок, чтобы папка и её страницы уходили в одно новое дерево:

vi .htaccess

RewriteCond %{REQUEST_URI} ^/(centos|rocky|ubuntu|debian)/?$ RewriteRule ^ %{ENV:SCHEME}://%{HTTP_HOST}/linux/%1/ [R=301,L] RewriteCond %{REQUEST_URI} ^/(centos|rocky|ubuntu|debian)/(.*)$ RewriteRule ^ %{ENV:SCHEME}://%{HTTP_HOST}/linux/%1/%2 [R=301,L]

После этого один старый адрес делает один переход в новое дерево:

curl -sS -o /dev/null -w "%{http_code} %{redirect_url}\n" https://example.com/centos/network/firewall.php

301 https://example.com/linux/centos/network/firewall.php

Порядок правил и правило одного перехода: Порядок правил. SCHEME задаётся один раз в начале файла правилом E=SCHEME:https; полный блок — в статье пара. Редирект для статического файла, который ещё лежит на диске, на Beget не срабатывает: nginx отдаёт файл первым, как объясняет nginx перед Apache. Когда 301 оставляет страницу, которая должна исчезнуть, используйте [R=404,L]: Безопасные правила, защитные блоки и откат.

Проверьте, есть ли эта проблема у вас

Пока старый хостинг не закрыт, сравните страницы, которые он отдаёт, со страницами в репозитории. Запускайте на своём компьютере, в папке рабочей копии, где лежит корень документов:

ssh user@old-host 'cd ~/example.com && find . -type f -name "*.php" -printf "%P\n"' | LC_ALL=C sort > /tmp/host-pages.txt

git ls-files '*.php' | LC_ALL=C sort > /tmp/repo-pages.txt

LC_ALL=C comm -23 /tmp/host-pages.txt /tmp/repo-pages.txt

Проблема есть, если comm выводит файлы. Старый хостинг их отдаёт, а после переезда каждый ответит 404, если его не скопировать и не перенаправить:

dev/php/htaccess.php edu/nuclear_power/nuclear_power.php web/hosting.php

Обоим спискам нужен LC_ALL=C: comm ждёт, что они отсортированы одинаково, а две локали сортируют по-разному. После переезда самые частые 404 в журнале доступа показывают, чего ещё не хватает:

awk '$9 == 404 {print $7}' ~/logs/example.com/https/access.log | sort | uniq -c | sort -rn | head

10 /linux/rocky/network/netstat.php 7 /.well-known/traffic-advice 6 /linux/rocky/network/hostname.php

Проблемы нет, если comm ничего не выводит.

Проблема 5: пропали картинки

Симптом. Страницы загружались, но картинки отвечали 404. В локальной копии картинки работали.

Команда, которая это показывает. Посчитайте картинки, которые git отслеживает, и картинки, которые он игнорирует:

git ls-files | grep -c -i -E '\.(jpe?g|png|webp|gif)$'

242

git ls-files --others --ignored --exclude-standard | grep -c -i -E '\.(jpe?g|png|webp|gif)$'

19475

Почему. Файлы картинок игнорируются git. Выкладка, которая копирует только отслеживаемые файлы, их пропускала, а на старом хостинге ещё лежали копии, загруженные годами раньше по FTP.

Исправление. Скопируйте игнорируемые файлы отдельно с тем же перекрытием прав, затем проверьте каждую на новом хостинге:

rsync -av --chmod=D755,F644 ~/site/img/ dh_user@<server>.dreamhost.com:~/example.com/img/

curl -sS -o /dev/null -w "%{http_code} %{size_download}\n" https://example.com/img/cover.jpg

200 180023

Список игнорируемых файлов, который хранится в репозитории, делает следующий переезд повторяемым: Изображения вне git: список файлов в репозитории.

Проверьте, есть ли эта проблема у вас

В папке рабочей копии, где лежит корень документов, запросите с живого сайта каждую картинку, которую игнорирует git, и выведите те, что не отвечают 200. head -n 200 делает первый запуск коротким; уберите его, чтобы проверить все:

git ls-files --others --ignored --exclude-standard | grep -i -E '\.(jpe?g|png|webp|gif|svg)$' | head -n 200 | while IFS= read -r f; do printf '%s %s\n' "$(curl -s -o /dev/null -w '%{http_code}' "https://example.com/$f")" "$f"; done | grep -v '^200'

Проблема есть, если появляются строки с 404. Эти картинки есть в рабочей копии, но их нет на хостинге:

404 aa_img/120_600_MINI-black.gif 404 aa_img/about-finland.jpg 404 aa_img/admitad/admitad_book24_600x300.jpg

Проблемы нет, если ничего не выводится. 000 вместо кода значит, что ответа не было: общий хостинг может на несколько минут отказывать в частых запросах. Подождите и запустите снова с меньшим head -n.

Проблема 6: картинки и скрипты из одной папки отвечали 301

Симптом. На нескольких страницах карточки показывали сломанные картинки, а их скрипты не работали. Файлы лежали на сервере с правильными правами, а журнал ошибок о них молчал.

Команда, которая это показывает. Запросите один из файлов:

curl -sS -o /dev/null -D - https://example.com/.php/img/cards/airport.jpg | grep -i -E '^(HTTP|location|server)'

HTTP/2 301 location: https://example.com/.php server: Apache

Затем выведите все 301 ниже этой папки из журнала доступа:

awk '$9 == 301 && $7 ~ /\/\.php\// {print $7}' ~/logs/example.com/https/access.log | sort | uniq -c

3 /ru/.php/img/cards/actress-800x600.jpg 1 /.php/scripts/top10_script3.js 1 /.php/scripts/top10_script2.js 1 /ao/.php/scripts/top10_script2.js

Причина. Сайт хранит свои PHP-классы, а с ними картинки и скрипты, которые они используют, в папках с именем .php. Правило, которое уводит страницу с PATH_INFO на чистый адрес, использовало шаблон ^(.+\.php)/.+$, а он подходит и к этим папкам. На Beget файлы отдавал nginx раньше, чем Apache читал правило, поэтому ошибка не проявлялась. На DreamHost их отдаёт Apache.

Исправление. Требовать перед .php имя файла:

RewriteCond %{REQUEST_URI} ^(.*[^/]\.php)/.+$

Правило целиком — DreamHost: статические файлы проходят через .htaccess, а тот же запрос к серверам обоих видов, до исправления и после, — в проверке 6 плана проверок Docker.

Проверьте, есть ли эта проблема у вас

На новом хостинге выведите статические файлы, которые ответили 301:

awk '$9 == 301 && $7 ~ /\.(jpe?g|png|webp|gif|svg|js|css)$/ {print $7}' ~/logs/example.com/https/access.log | sort | uniq -c | sort -rn | head

5 /python/img/00114.webp 3 /.php/img/cards/actress-800x600.jpg 3 /databases/db_img/mysql_00100.jpg

Перенесённая картинка может уходить редиректом нарочно. Посмотрите, куда ведёт каждый редирект:

curl -sS -o /dev/null -w "%{http_code} %{content_type} %{redirect_url}\n" https://example.com/python/img/00114.webp

301 text/html; charset=iso-8859-1 https://example.com/dev/python/img/00114.webp

curl -sS -o /dev/null -w "%{http_code} %{content_type} %{redirect_url}\n" https://example.com/.php/img/cards/actress-800x600.jpg

301 text/html; charset=iso-8859-1 https://example.com/.php

Проблема есть, если цель — папка или страница, а не файл того же типа, как во втором ответе. Первый ответ — перенесённая картинка, с ним всё в порядке.

Журнал показывает только файлы, которые кто-то запросил. Чтобы проверить все файлы папки раньше посетителей, запросите их напрямую из папки рабочей копии, где лежит корень документов. Поменяйте условие -path на папки, к которым могут подойти ваши правила:

find . -type f -path '*.php/*' \( -iname '*.jpg' -o -iname '*.png' -o -iname '*.webp' -o -iname '*.js' -o -iname '*.css' \) | head -n 50 | while IFS= read -r f; do printf '%s %s\n' "$(curl -s -o /dev/null -w '%{http_code}' "https://example.com/${f#./}")" "${f#./}"; done | grep -v '^200'

301 .php/php_includes/topmenu.js 301 .php/img/ads/ads_banner_fin2.PNG 301 .php/php_includes/scripts/rb_script.js

Проблемы нет, если ничего не выводится. 000 вместо кода значит, что ответа не было: общий хостинг может на несколько минут отказывать в частых запросах. Подождите и запустите снова с меньшим head -n.

Проблема 7: новые страницы отвечали 404 после каждой загрузки

Симптом. Новые страницы после каждой загрузки отвечали 404, хотя файлы были в локальной копии. Страницы первой загрузки работали.

Команда, которая это показывает. В команде загрузки не было слеша в конце источника:

rsync -av ~/site dh_user@<server>.dreamhost.com:~/example.com/

sending incremental file list site/ site/!!!_notes.txt site/.htaccess site/3108a3f8a0abf63e94ac5c71960b0c741b3e71d2.txt

Без слеша rsync создаёт ~/example.com/site/ и копирует файлы туда. Каждая загрузка после первой попадала в эту папку. Корень документов не получил ни одной новой страницы: 77 из них отвечали 404. Корневой .htaccess на хостинге тоже остался версии первой загрузки, поэтому поздние исправления правил не доходили до сайта. А https://example.com/site/ отдавал публичную вторую копию сайта:

curl -s -o /dev/null -w '%{http_code}\n' https://example.com/site/

200

Почему. rsync считает источник без слеша папкой, которую надо скопировать, а не её содержимым. В месте назначения появляется папка site со всем сайтом внутри.

Исправление. Добавьте слеш к источнику и копируйте содержимое папки:

rsync -av ~/site/ dh_user@<server>.dreamhost.com:~/example.com/

Сначала запустите с -n и прочитайте первые пути списка. Они должны начинаться с ./, а не с site/:

rsync -avn ~/site/ dh_user@<server>.dreamhost.com:~/example.com/ | head -n 5

sending incremental file list ./ PHP_errors copy(1).log PHP_errors copy(2).log PHP_errors copy.log

Первый путь — ./, а не site/; перечисленные файлы — тоже лишние, они уйдут на хостинг, см. проблему 9.

Затем посмотрите на лишнюю папку на сервере и удалите её:

ssh dh_user@<server>.dreamhost.com 'ls ~/example.com/site | head'

ssh dh_user@<server>.dreamhost.com 'rm -r ~/example.com/site'

Скрипт загрузки должен отказываться от цели, в которой нет index.php. Слеш в общем виде разобран в статье Перенос сайта: копирование файлов; тот же раздел на этой странице — Копирование файлов.

Проверьте, есть ли эта проблема у вас

Запросите папку, которую создала бы ваша загрузка:

curl -s -o /dev/null -w '%{http_code}\n' https://example.com/site/

404

Проблема есть, если ответ 200: папка открыта. Проблемы нет, если ответ 404, как сегодня.

По SSH проверьте, есть ли в корне документов папка с именем вашей локальной папки-источника и файл index.php в ней:

ssh dh_user@<server>.dreamhost.com 'ls -d ~/example.com/site ~/example.com/site/index.php'

ls: cannot access '/home/dh_user/example.com/site': No such file or directory ls: cannot access '/home/dh_user/example.com/site/index.php': No such file or directory

Строка No such file для обоих путей значит, что проблемы нет. Путь и файл значат, что загрузка попала в подпапку.

Сравните корневой .htaccess на хостинге со своим:

ssh dh_user@<server>.dreamhost.com 'cat ~/example.com/.htaccess' | diff -q - ~/site/.htaccess && echo same

same

Files - and ~/site/.htaccess differ значит, что загрузки не доходят до корня документов или файл ещё не загружен.

Последняя проверка — пробный запуск с первыми путями:

rsync -avn ~/site/ dh_user@<server>.dreamhost.com:~/example.com/ | head -n 5

sending incremental file list ./ PHP_errors copy(1).log PHP_errors copy(2).log PHP_errors copy.log

Если список начинается с site/, у источника нет слеша и загрузка снова уйдёт в подпапку.

Проблема 8: папки без индексной страницы показывали список файлов

Симптом. Адрес папки вроде https://example.com/img/ показывал список всех файлов в ней.

Команда, которая это показывает. Запросите папку, в которой нет index.php или index.html:

curl -s https://example.com/img/ | grep -c 'Index of'

2

Почему. На DreamHost Apache показывает файлы папки, в которой нет индексной страницы: перечисление папок включено по умолчанию.

Исправление. Выключите список в корневом .htaccess:

vi .htaccess

# No file list for a folder without an index page. Options -Indexes

После неё те же папки отвечают 403, а папка с index.php по-прежнему загружается:

curl -s -o /dev/null -w '%{http_code}\n' https://example.com/img/

403

Проверьте, есть ли эта проблема у вас

Найдите папки без индексной страницы на сервере или в рабочей копии:

find . -type d ! -path '*/.git*' | while IFS= read -r d; do [ -e "$d/index.php" ] || [ -e "$d/index.html" ] || echo "$d"; done | head -n 20

./code ./gcc ./repository ./images ./.php

Запросите несколько из них:

curl -s https://example.com/img/ | grep -c 'Index of'

0

curl -s -o /dev/null -w '%{http_code}\n' https://example.com/img/

403

curl -s -o /dev/null -w '%{http_code}\n' https://example.com/.php/img/

403

Проблема есть, если grep выводит число больше 0 или код ответа 200. Проблемы нет, если grep выводит 0, а код — 403: сегодня /img/ и /.php/img/ отвечают 403.

000 вместо кода значит, что ответа не было: общий хостинг может на несколько минут отказывать в частых запросах. Подождите и запустите снова.

Проблема 9: резервные копии и архивы из рабочей папки были открыты

Симптом. Резервная копия или архив отвечали 200 по публичному адресу. Старые загрузки копировали всю рабочую папку, поэтому в корне документов лежали файлы, которые не относятся к сайту.

Команда, которая это показывает. Запросите копию главной страницы:

curl -s -o /dev/null -w '%{http_code}\n' https://example.com/index.php.bak

200

Почему. Копию PHP-файла с расширением .bak PHP не выполняет: Apache отдаёт её как текст, поэтому index.php.bak показывает исходный код главной страницы. На хостинге строгий список скрипта загрузки не включал 1 496 файлов: 3 архива (один — zip папки PHP, 87 МБ, в корне сайта), 89 резервных копий (index.php.bak, 27 копий карты сайта, копии), 328 старых журналов, 467 файлов в папках инструментов (отчёты проверок, кеши), 18 черновиков страниц, а ещё файлы данных и базы, которые сайт пишет сам и которые должны остаться. После очистки тот же запрос отвечает 404.

Исправление. Загружайте список, а не папку. Начните с файлов, которые отслеживает git, добавьте игнорируемые картинки по расширению и отфильтруйте черновики и мусорные папки:

cd ~/site

git ls-files > /tmp/deploy.txt

git ls-files --others --ignored --exclude-standard | grep -i -E '\.(jpe?g|png|webp|gif|svg|ico)$' >> /tmp/deploy.txt

grep -v -E '^(drafts/|tools/|tmp/|.*\.(bak|log|zip|orig|sql)$)' /tmp/deploy.txt | LC_ALL=C sort > /tmp/deploy.clean.txt

rsync -av --chmod=D755,F644 --files-from=/tmp/deploy.clean.txt ./ dh_user@<server>.dreamhost.com:~/example.com/

С --files-from исключение --exclude для папки не убирает перечисленные файлы ниже неё, поэтому фильтруется сам список. Другой способ — исключить каждую папку дважды, X/ и X/**. Затем перечислите файлы хостинга, которых нет в списке выкладки, просмотрите их и удалите то, что не относится к сайту:

ssh dh_user@<server>.dreamhost.com 'cd ~/example.com && find . -type f | sed "s|^\./||" | LC_ALL=C sort' > /tmp/host.txt

LC_ALL=C comm -23 /tmp/host.txt /tmp/deploy.clean.txt > /tmp/extra.txt

wc -l /tmp/extra.txt

939 /tmp/extra.txt

Число зависит от фильтра. Этот короткий фильтр оставляет в списке больше файлов, поэтому остаются 939; строгий список скрипта загрузки оставлял 1 496.

Прочитайте список перед удалением и никогда не удаляйте файлы данных и базы, которые пишет сайт. Сравнение — та же идея с comm, что и в проблеме 4.

Проверьте, есть ли эта проблема у вас

Запросите резервную копию по публичному адресу:

curl -s -o /dev/null -w '%{http_code}\n' https://example.com/index.php.bak

200

200 значит, что файл есть; 404 — что его нет.

По SSH выведите мусор по именам:

ssh dh_user@<server>.dreamhost.com "cd ~/example.com && find . -type f \( -name '*.bak' -o -name '*.zip' -o -name '*.orig' -o -name '*~' -o -name '*.sql' -o -name '*.log' \) | head -n 20"

./git/tortoisegit/img/kdiff3/case_change.log ./git/tortoisegit/img/kdiff3/image_proc.log ./git/install/windows/img/image_proc.log ./aa_img/image_proc.log ./backup.zip

Сравните файлы хостинга с git, не считая картинок. Это та же идея с comm, что и в проблеме 4:

ssh dh_user@<server>.dreamhost.com 'cd ~/example.com && find . -type f | sed "s|^\./||" | LC_ALL=C sort' > host.txt

git ls-files | LC_ALL=C sort > git.txt

LC_ALL=C comm -23 host.txt git.txt | grep -v -i -E '\.(png|jpe?g|gif|webp|svg|ico)$' | head -n 30

.php/_modules/_heihei/img/cards/Finland/life/image_proc.log .php/_modules/_heihei/img/cards/Finland/news/image_proc.log .php/img/cards/it/image_proc.log .php/php_includes/_cards/heihei/Finland/fests/_lux_helsinki_inc copy.php

Проблема есть, если первая команда отвечает 200 или списки выводят файлы. Проблемы нет, если копия отвечает 404, а списки ничего не выводят.

Проблема 10: журнал ошибок вырос до 230 МБ в день

Симптом. Журнал ошибок рос на сотни мегабайт в день. Почти каждая строка — одно и то же предупреждение об устаревшем коде, повторённое на каждом запросе.

Команда, которая это показывает. Посчитайте строки о динамических свойствах в копии за 2026-11-07:

grep -c 'Creation of dynamic property' ~/logs/example.com/https/error.log.2026-11-07

795171

DreamHost хранит копию за каждый день, а error.log.0 ссылается на вчерашнюю.

Почему. С PHP 8.2 присваивание свойства, которое класс не объявил, считается устаревшим. Почти каждая строка журнала была PHP Deprecated: Creation of dynamic property Site\\Php\\Related::$urls is deprecated in ..., её писал mod_fcgid: stderr (строка есть в статье DreamHost: журналы ошибок и доступа). Журнал ошибок за 2026-11-07 был 231 МБ и 804 127 строк, 795 171 из них о динамическом свойстве; за 2026-11-08 — 130 МБ и 454 263 строки, 449 025 из них.

Исправление. Объявите свойства в классах. В движках страниц 11 таких классов; каждое свойство получает объявление вроде public $urls;:

vi .php/Related.php

class Related { public $urls; // ... }

Быстрый способ, который сохраняет старое поведение, — атрибут #[\AllowDynamicProperties] у класса:

#[\AllowDynamicProperties] class Related { // ... }

Выложено 2026-11-09 в 06:43 по времени сервера; последняя такая строка была в 06:50. С 07:00 до 11:37 в журнал попало 321 строка, ни одной Deprecated.

Проверьте, есть ли эта проблема у вас

Посчитайте предупреждения во вчерашней копии:

grep -c 'Creation of dynamic property' ~/logs/example.com/https/error.log.0

449025

Тысячи строк значат, что проблема есть; 0 — что её нет.

После исправления найдите время последней такой строки в текущем журнале:

grep 'Creation of dynamic property' ~/logs/example.com/https/error.log | tail -n 1 | cut -c1-26

[Mon Nov 09 06:50:16 2026]

Время до выкладки значит, что исправление работает; время минуту назад значит, что предупреждения продолжаются; пустой вывод значит, что сегодня их не было.

Посмотрите, какие классы и свойства повторяются:

grep -o 'dynamic property [^ ]*' ~/logs/example.com/https/error.log.0 | sort | uniq -c | sort -rn | head

7744 dynamic property Site\\Php\\SxGeo\\SxGeo::$pack 7744 dynamic property Site\\Php\\SxGeo\\SxGeo::$max_region 5994 dynamic property Site\\Php\\Related::$urls 4948 dynamic property Site\\Php\\Tcont::$urls_length

Размер по дням:

ls -lh ~/logs/example.com/https/ | grep error

-rw-r--r-- 1 root root 19M Nov 9 12:01 error.log lrwxrwxrwx 1 root root 20 Nov 9 00:37 error.log.0 -> error.log.2026-11-08 -rw-r--r-- 1 root root 23K Nov 6 00:37 error.log.2026-11-05 -rw-r--r-- 1 root root 17M Nov 7 00:13 error.log.2026-11-06 -rw-r--r-- 1 root root 221M Nov 8 00:57 error.log.2026-11-07 -rw-r--r-- 1 root root 125M Nov 9 00:37 error.log.2026-11-08

ls -h считает в МиБ: 221M — это 231 МБ из истории.

Перед загрузкой запустите ту же проверку в рабочей копии. Перейдите в корень документов рабочей копии, здесь ~/site, и запустите PHP одной строкой, которая задаёт DOCUMENT_ROOT:

cd ~/site

php -d error_reporting=E_ALL -d display_errors=stderr -r "\$_SERVER[\"DOCUMENT_ROOT\"] = getcwd(); include \"index.php\";" 2>&1 >/dev/null | grep -c "dynamic property"

14

На коде до исправления она выводит 14, на исправленном — 0. 14 значит, что проблема есть, 0 — что её нет.

После всех десяти исправлений главная и внутренняя страница отвечали 200, а несуществующий адрес — 404:

for u in http://example.com/ https://example.com/ https://example.com/about/ https://example.com/no-such-page-xyz/; do curl -sS -o /dev/null -w "%{http_code} %{redirect_url} <- $u\n" "$u"; done

301 https://example.com/ <- http://example.com/ 200 <- https://example.com/ 200 <- https://example.com/about/ 404 <- https://example.com/no-such-page-xyz/

Те же проверки, автоматизированные против локальной копии каждого хостинга, есть в плане проверок для образов Docker.

Авторы статьи: Андрей Олегович, Arthur Isaev