История о том, как обновление одной цифры в номере версии превратилось в вечер с тремя ложными следами, читаемым только из браузера сайтом и файлом, который притворялся каталогом
.
Содержание
У меня на сервере с форумом стоит PPA
Сразу оговорюсь про важную деталь, которая всплывёт ниже: мой сайт живёт в
Переезд я по привычке начал с того, что попробовал прогнать консольную команду XenForo — и тут же получил в лоб:
Функция
Разгадка проста и неприятна: CLI-бинарь
Проверяю гипотезу тремя командами:
Картина прояснилась мгновенно:
Вот и весь диагноз: CLI уже уехал на 8.5 (свежую, без модулей), а nginx всё ещё общается с FPM версии 8.4 через сокет в
Первым делом — поставить под 8.5 ровно тот же набор расширений, что стоит под 8.4. Не «какие вспомню», а именно зеркало, иначе потом будешь по одному отлавливать недостающие. Это красиво решается одной связкой:
Логика простая: берём список всех установленных пакетов
Если какого-то пакета под 8.5 в PPA ещё нет (так иногда бывает с экзотикой вроде imagick), apt честно об этом скажет — тогда ставишь всё остальное без него и разбираешься с отстающим отдельно.
Тонкий момент, на котором легко обжечься: установка пакетов 8.5 не копирует твои правки из 8.4. Свежие php.ini и пул придут со стоковыми значениями. Поэтому сверяем и переносим вручную:
Из
И туда же — те же
А вот тут начинается самое интересное. У юнитов php-fpm в Ubuntu по умолчанию включён
Но 8.4 же как-то работает с сокетом в
То, что нашлось, нужно продублировать для 8.5 — но к этому я вернусь чуть ниже, потому что именно на создании этого override-файла меня поджидала главная засада вечера.
Когда модули поставлены, конфиги перенесены, а сокет настроен — запускаем 8.5 и переводим на него nginx. Ключевой принцип: 8.4 не трогаем, пока не убедимся, что 8.5 работает. Параллельная работа двух версий — это и есть страховка.
Прелесть этого подхода в том, что откат — это тот же
Запускаю
В штатном юните php-fpm от Debian/Ubuntu стоит
На 8.4 это в своё время уже было пробито override-файлом (потому 8.4 и работал без нареканий), а новенький юнит 8.5 приехал со стоковой бронёй. Лечится добавлением
Обрати внимание — здесь сразу обе строки: и отключение приватного tmp (та самая проблема с сокетом из раздела выше), и разрешение на запись. Одним файлом закрываем два вопроса.
И вот тут вечер заиграл новыми красками. Override-файлы systemd живут в каталоге вида
…написал нужные строки, нажал сохранить — и получил:
Затем при попытке записи — ещё хлеще:
Я честно сначала пошёл по ложному пути: подумал на права, на то, что сессия не под root, на readonly-режим самого vi. Перепробовал
Разгадка оказалась обиднее некуда. Я проверил, что вообще происходит:
Вот оно. Смотрим на самый первый символ вывода
Что произошло: в одной из самых первых попыток vi сохранил конфиг прямо по пути, который должен был быть каталогом. То есть вместо каталога
Лечится в четыре строки:
И ещё один приём на будущее, чисто vim'овский — для ситуации «открыл файл без sudo, уже всё наредактировал, а сохранить не можешь»:
Эта команда пишет содержимое буфера через sudo, не теряя правок. Куда удобнее, чем выходить без сохранения и начинать заново.
Самый же канонический способ создавать override для systemd — вообще не трогать vi руками, а довериться штатному механизму:
Он сам создаёт именно каталог нужного вида, кладёт туда
После рестарта проверяю, что хардеринг настроен как надо и запись прошла:
Первая команда должна показать
Дальше — CLI и хвосты:
Отдельно стоит заглянуть в crontab: если задачи XenForo ходят через системный cron с голым
Ну и финальный аккорд — открываю форум в браузере, убеждаюсь, что страницы отдаются, а в
Что я вынес из этого переезда:
И сквозная мысль через все мои последние заметки: если бы сайт жил в
Содержание
С чего всё началось
У меня на сервере с форумом стоит PPA
ondrej/php — это значит, что в системе спокойно сосуществуют несколько версий PHP сразу. На тот момент сайт крутился на 8.4, и меня это устраивало ровно до того момента, пока я не решил перебраться на свежую 8.5. Казалось бы — пара команд: поставить пакеты, переключить nginx, перезапустить. На деле вечер растянулся на несколько ложных следов, и каждый из них стоит того, чтобы о нём рассказать — потому что любой, кто держит сайт на PPA с несколькими версиями PHP, рано или поздно наступит на те же грабли.Сразу оговорюсь про важную деталь, которая всплывёт ниже: мой сайт живёт в
/usr/www/sysadmin/httpdocs/. Это нестандартное расположение, и именно оно дважды за вечер аукнулось. Но обо всём по порядку.Первый сюрприз: CLI и FPM живут разными версиями
Переезд я по привычке начал с того, что попробовал прогнать консольную команду XenForo — и тут же получил в лоб:
Код:
PHP Fatal error: Uncaught Error: Call to undefined function mb_internal_encoding() in /usr/www/sysadmin/httpdocs/src/XF.php:140
Функция
mb_internal_encoding() — это mbstring. Её отсутствие означает только одно: у того PHP, которым я сейчас запускаю команду, не подключено расширение mbstring. Но сайт-то работает! Страницы отдаются, форум живёт. Значит, у той версии, что крутит сайт, mbstring точно есть.Разгадка проста и неприятна: CLI-бинарь
php и PHP-FPM, который обслуживает сайт, — это разные версии. С PPA ondrej это классика жанра: после очередного apt update механизм update-alternatives переключил симлинк php на самую новую установленную версию, для которой я ещё не доставил модули. А FPM продолжал слушать на своей, укомплектованной.Проверяю гипотезу тремя командами:
Bash:
php -v # какая версия у CLI
ls /etc/php/ # какие версии вообще стоят
grep -rh fastcgi_pass /etc/nginx/ | sort -u # какой fpm-сокет использует сайт
Картина прояснилась мгновенно:
Код:
root@sysadmin:~# php -v
PHP 8.5.7 (cli) (built: Jun 6 2026 05:36:56) (NTS)
root@sysadmin:~# ls /etc/php/
8.4 8.5
root@sysadmin:~# grep -rh fastcgi_pass /etc/nginx/ | sort -u
fastcgi_pass unix:/tmp/php8.4-fpm.sock;
Вот и весь диагноз: CLI уже уехал на 8.5 (свежую, без модулей), а nginx всё ещё общается с FPM версии 8.4 через сокет в
/tmp. Раз уж я всё равно затеял переезд на 8.5 — значит, нужно не латать CLI, а перевести на 8.5 всё: и консоль, и сайт. Зеркально.Переезд целиком: зеркалим набор модулей
Первым делом — поставить под 8.5 ровно тот же набор расширений, что стоит под 8.4. Не «какие вспомню», а именно зеркало, иначе потом будешь по одному отлавливать недостающие. Это красиво решается одной связкой:
Bash:
# сначала посмотреть глазами, что получится
dpkg -l | awk '/^ii php8\.4/{print $2}' | sed 's/8\.4/8.5/'
# а теперь поставить
dpkg -l | awk '/^ii php8\.4/{print $2}' | sed 's/8\.4/8.5/' | xargs apt install -y
Логика простая: берём список всех установленных пакетов
php8.4-*, меняем в названии версию на 8.5 и скармливаем в apt install. Именно здесь, кстати, и закрывается тот самый фатал с mbstring — пакет php8.5-mbstring приедет вместе с остальными.Если какого-то пакета под 8.5 в PPA ещё нет (так иногда бывает с экзотикой вроде imagick), apt честно об этом скажет — тогда ставишь всё остальное без него и разбираешься с отстающим отдельно.
Конфиги не переедут сами
Тонкий момент, на котором легко обжечься: установка пакетов 8.5 не копирует твои правки из 8.4. Свежие php.ini и пул придут со стоковыми значениями. Поэтому сверяем и переносим вручную:
Bash:
diff /etc/php/8.4/fpm/php.ini /etc/php/8.5/fpm/php.ini
diff /etc/php/8.4/fpm/pool.d/www.conf /etc/php/8.5/fpm/pool.d/www.conf
diff /etc/php/8.4/cli/php.ini /etc/php/8.5/cli/php.ini
Из
php.ini для форума обычно критичны memory_limit, upload_max_filesize, post_max_size, max_execution_time, блок opcache.* и date.timezone. В пуле 8.5 первым делом прописываю отдельный сокет — отдельный, чтобы 8.4 продолжал жить параллельно и был запасным аэродромом на случай отката:
Код:
listen = /tmp/php8.5-fpm.sock
И туда же — те же
user, group и настройки pm.*, что были в 8.4.Подводный камень: сокет в /tmp и PrivateTmp
А вот тут начинается самое интересное. У юнитов php-fpm в Ubuntu по умолчанию включён
PrivateTmp=true — systemd выдаёт сервису приватный /tmp, изолированный от остальной системы. Это значит, что сокет, который FPM создаёт в /tmp, для nginx попросту невидим — они смотрят в разные /tmp.Но 8.4 же как-то работает с сокетом в
/tmp? Значит, для него где-то лежит drop-in, который это отключает. И вот он привязан к юниту именно 8.4 и на 8.5 не подействует. Проверяю:
Bash:
systemctl cat php8.4-fpm | grep -iA2 -B2 PrivateTmp
ls /etc/systemd/system/php8.4-fpm.service.d/ 2>/dev/null
То, что нашлось, нужно продублировать для 8.5 — но к этому я вернусь чуть ниже, потому что именно на создании этого override-файла меня поджидала главная засада вечера.
💡 На заметку написал:
Переключаем nginx
Когда модули поставлены, конфиги перенесены, а сокет настроен — запускаем 8.5 и переводим на него nginx. Ключевой принцип: 8.4 не трогаем, пока не убедимся, что 8.5 работает. Параллельная работа двух версий — это и есть страховка.
Bash:
systemctl enable --now php8.5-fpm
ls -la /tmp/php8.5-fpm.sock # сокет на месте, владелец верный
# меняем сокет в конфигах nginx
grep -rl 'php8.4-fpm.sock' /etc/nginx/ | xargs sed -i 's#/tmp/php8\.4-fpm\.sock#/tmp/php8.5-fpm.sock#g'
nginx -t && systemctl reload nginx
Прелесть этого подхода в том, что откат — это тот же
sed в обратную сторону плюс reload. 8.4-fpm всё это время работает рядом и ждёт своего часа. Гасить его (systemctl disable --now php8.4-fpm) можно только тогда, когда всё устаканится.Read-only file system: старый знакомый
Запускаю
php8.5-fpm — и сайт начинает ругаться, что не может ничего записать. Read-only file system. Тем, кто читал мою предыдущую статью про загрузку вложений, симптом знаком до боли: это снова systemd-хардеринг.В штатном юните php-fpm от Debian/Ubuntu стоит
ProtectSystem=full — systemd монтирует /usr, /boot и /etc внутри неймспейса сервиса в read-only. А мой сайт, напомню, живёт в /usr/www. Поэтому 8.5-fpm физически не может писать в internal_data, data и вообще никуда в докруте.На 8.4 это в своё время уже было пробито override-файлом (потому 8.4 и работал без нареканий), а новенький юнит 8.5 приехал со стоковой бронёй. Лечится добавлением
ReadWritePaths, который перемонтирует именно нужное поддерево обратно в rw, не снимая защиту с остального /usr:
Код:
[Service]
PrivateTmp=false
ReadWritePaths=/usr/www
Обрати внимание — здесь сразу обе строки: и отключение приватного tmp (та самая проблема с сокетом из раздела выше), и разрешение на запись. Одним файлом закрываем два вопроса.
Главная ловушка: файл, который притворялся каталогом
И вот тут вечер заиграл новыми красками. Override-файлы systemd живут в каталоге вида
<имя-юнита>.service.d/. Я открыл vi:
Bash:
vi /etc/systemd/system/php8.5-fpm.service.d/override.conf
…написал нужные строки, нажал сохранить — и получил:
Код:
-- INSERT -- W10: Warning: Changing a readonly file
Затем при попытке записи — ещё хлеще:
Код:
E212: Can't open file for writing
Я честно сначала пошёл по ложному пути: подумал на права, на то, что сессия не под root, на readonly-режим самого vi. Перепробовал
:w!, попробовал положить файл через tee, через printf — ничего. Везде отказ.Разгадка оказалась обиднее некуда. Я проверил, что вообще происходит:
Код:
root@sysadmin:~# whoami
root
root@sysadmin:~# ls -ld /etc/systemd/system/php8.5-fpm.service.d
-rw-r--r-- 1 root root 34 Jun 12 09:42 /etc/systemd/system/php8.5-fpm.service.d
Вот оно. Смотрим на самый первый символ вывода
ls: там -, а не d. То, что должно было быть каталогом, оказалось обычным файлом. И размер красноречивый — 34 байта, это ровно мои строки [Service] и ReadWritePaths=/usr/www с переводами строк.Что произошло: в одной из самых первых попыток vi сохранил конфиг прямо по пути, который должен был быть каталогом. То есть вместо каталога
php8.5-fpm.service.d с файлом override.conf внутри у меня появился файл с именем php8.5-fpm.service.d. А внутрь файла, понятное дело, второй файл не положишь — отсюда и E212. И mkdir -p молча отказывался, потому что не может создать каталог поверх существующего файла (эту ошибку я в потоке проглядел).Лечится в четыре строки:
Bash:
rm /etc/systemd/system/php8.5-fpm.service.d
mkdir -p /etc/systemd/system/php8.5-fpm.service.d
printf '[Service]\nPrivateTmp=false\nReadWritePaths=/usr/www\n' > /etc/systemd/system/php8.5-fpm.service.d/override.conf
systemctl daemon-reload && systemctl restart php8.5-fpm
⚠️ Мораль написал:
И ещё один приём на будущее, чисто vim'овский — для ситуации «открыл файл без sudo, уже всё наредактировал, а сохранить не можешь»:
Код:
:w !sudo tee %
Эта команда пишет содержимое буфера через sudo, не теряя правок. Куда удобнее, чем выходить без сохранения и начинать заново.
Самый же канонический способ создавать override для systemd — вообще не трогать vi руками, а довериться штатному механизму:
Bash:
systemctl edit php8.5-fpm
Он сам создаёт именно каталог нужного вида, кладёт туда
override.conf, открывает редактор с шаблоном и после выхода сам делает daemon-reload. Промахнуться невозможно в принципе. Если бы я сразу пошёл этим путём — статья была бы вдвое короче.Финальная проверка
После рестарта проверяю, что хардеринг настроен как надо и запись прошла:
Bash:
systemctl show php8.5-fpm -p PrivateTmp -p ReadWritePaths
ls -la /tmp/php8.5-fpm.sock
sudo -u www-data touch /usr/www/sysadmin/httpdocs/internal_data/.rwtest && rm /usr/www/sysadmin/httpdocs/internal_data/.rwtest && echo OK
Первая команда должна показать
PrivateTmp=no и /usr/www в списке путей, сокет — быть на месте с верным владельцем, а тест записи — выдать OK.Дальше — CLI и хвосты:
Bash:
update-alternatives --set php /usr/bin/php8.5
php -m | grep -E 'mbstring|intl|mysqli|sqlite3|gd|curl'
crontab -l -u www-data # если там был голый php — теперь он 8.5, всё консистентно
Отдельно стоит заглянуть в crontab: если задачи XenForo ходят через системный cron с голым
php, они теперь поедут на 8.5 — что в нашем случае как раз и нужно, всё стало единообразным. Но если бы я хотел оставить их на старой версии — там пришлось бы прописать версионированный бинарь явно.Ну и финальный аккорд — открываю форум в браузере, убеждаюсь, что страницы отдаются, а в
admin.php → Сервер и производительность версия PHP честно показывает 8.5.7. Заодно заглядываю в журнал ошибок сервера: на новой мажорной версии старые аддоны иногда начинают сыпать deprecated-предупреждениями — это не катастрофа, но повод либо обновить аддон, либо, если что-то критично, откатиться тем же sed'ом на 8.4.Выводы
Что я вынес из этого переезда:
- CLI и FPM — это разные PHP. На PPA с несколькими версиями
update-alternativesлегко уводит консольныйphpв сторону от того, что крутит сайт. Фатал сmb_internal_encoding()— типичный симптом именно этого. - Переезжай зеркально и параллельно. Ставь под новую версию ровно тот же набор модулей, держи отдельный сокет, переключай nginx одним
sed'ом и не гаси старую версию, пока новая не докажет работоспособность. Откат должен быть в одну команду. - Конфиги руками. Установка пакетов новой версии не тащит за собой твои правки php.ini и пула — переноси через
diff. - systemd-хардеринг бьёт дважды.
ProtectSystem=full(read-only/usr) иPrivateTmp=true(изолированный/tmp) — обе настройки привязаны к конкретному юниту, и при смене версии их нужно переносить. Один override-файл сReadWritePathsиPrivateTmp=falseзакрывает оба вопроса. - Читай первый символ
ls -l.dпротив-— это разница между каталогом и файлом, и именно она стоила мне самой долгой части вечера. При E212 первым делом делайls -ldна путь. - Для override используй
systemctl edit. Он не даёт промахнуться с типом объекта и сам перечитывает юниты.
И сквозная мысль через все мои последние заметки: если бы сайт жил в
/var/www, а не в /usr/www, половины этих приключений просто не случилось бы — ProtectSystem=full намеренно оставляет /var доступным для записи. Перенос докрута — та самая структурная профилактика, до которой у меня всё никак не дойдут руки. Но это уже тема для отдельной статьи.