Что нового

Как я переезжал на php 8.5

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

Содержание

С чего всё началось​


У меня на сервере с форумом стоит 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-файла меня поджидала главная засада вечера.

💡 На заметку написал:
Сокет в /tmp — вообще говоря, не лучшее место именно из-за PrivateTmp. Если бы он лежал в /run/php/ (где его и держит пакет по умолчанию), всей этой пляски с приватным tmp не было бы. Но раз исторически сложилось так — приходится учитывать.

Переключаем 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

⚠️ Мораль написал:
ls -ld на подозрительном пути — первое, что нужно делать при E212, а не последнее. Первый символ в выводе ls -l говорит тип объекта: d — каталог, - — файл, l — симлинк. Я потерял минут двадцать, прежде чем посмотрел на эту единственную букву.

И ещё один приём на будущее, чисто 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.
1781252242657.png

Выводы​


Что я вынес из этого переезда:

  • 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 доступным для записи. Перенос докрута — та самая структурная профилактика, до которой у меня всё никак не дойдут руки. Но это уже тема для отдельной статьи.
Об авторе
Guru
Василий, cистемный админ /gnu/linux/windows/macos/mikrotik/troubleshooter, создатель сайта
Интересуюсь всем что делает инфраструктуру быстрой и надёжной
Открыт к общению и проектам, написать мне можно через форму или в личном сообщении

❗ Если есть пожелания по обзору какого-либо вопроса не представленного на сайте - пиши в комментариях

Комментарии

Нет комментариев для отображения.

Информация о статье

Автор
Guru
Время чтения статьи
8 мин чтения
Просмотры
22
Посл. обновление

Ещё в Linux

Ещё от Guru

Поделиться этой статьёй

Назад
Верх