Обновления без простоя и необслуживаемые патчи

Это седьмая статья серии «Эксплуатация VPS / DevOps-база». К концу прошлого шага (Firewall и SSH-харднинг) у вас закрытая машина: снаружи видно только {22, 80, 443}, боты ловятся fail2ban, лишние слушающие порты проверены через ss -tulpn. Но есть вещь, которую firewall закрыть не может: сам софт, который вы держите на машине, стареет. CVE (Common Vulnerabilities and Exposures — публичный идентификатор «известной дыры») выходят каждую неделю; боты, которые вы отбили на SSH, точно так же тычут в HTTP, в PHP и в ядро. Закрытый периметр без актуального ядра и PHP — это крепость с оторванными петлями на воротах: снаружи не залезут, а через то, что уже внутри и устарело — легко.

Обычные советы на эту тему разбиваются на два крайних полюса. Первый — «ставь автоматические обновления, они всё починят сами», в реальности оборачивающийся «сервер сам поставил новое ядро в 3 ночи и не перезагрузился, а libc перезапустил nginx посреди пиковой нагрузки». Второй — «я сам всё контролирую, ставлю раз в полгода всё разом», в реальности оборачивающийся «однажды после большого апгрейда не поднялся PHP и сайт лежит на две ночи, потому что не проверили на копии». Эта статья — про середину, где и обновления приходят вовремя, и сайт при этом стоит: как вообще живут обновления в apt (кандидат, hold, pin), что такое unattended-upgrades и для чего они реально, когда патч невозможно применить без перезагрузки (/var/run/reboot-required, needrestart, libc и ядро), как обновлять PHP и MariaDB на WordPress почти с нулевым простоем (reload вместо restart, параллельная версия PHP с переключением сокета в nginx), почему тест на копии — не бюрократия, а единственная защита от «я же обновлял, а оно сломалось», и как откатывать через apt-историю, если что-то пошло не так.

Термины, которые введём один раз: apt (Advanced Package Tool) — система пакетов Ubuntu/Debian; ставит, обновляет, удаляет программы из репозиториев. Пакет — «упаковка» программы (nginx, php8.2-fpm, mariadb-server); у каждого пакета в системе есть версия и состояние. Кандидат (candidate) — версия пакета, которую apt поставит при следующем обновлении (не всегда самая свежая, иногда сдержанная pin-ом). Hold — флаг «этот пакет не обновляй» (полезно на софте, который вы тестируете отдельно). Pin — «пришпилька» в apt: правило, какое происхождение (репозиторий/версия) предпочитать. unattended-upgrades — демон, сам ставящий (обычно только security-) патчи без вас. libc (glibc) — фундаментальная библиотека C, её использует каждый процесс на машине; её обновление почти всегда требует перезапуска сервисов. reload vs restart — из systemd: reload («перечитать конфиг/мягко пересоздать детей») не рвёт установленные соединения, restart («остановить и заново запустить») — рвёт. CVE (Common Vulnerabilities and Exposures) — публичный номер известной уязвимости; «security-patch» — пакет, который эту CVE закрывает. Все домены/IP/номера версий условные; подставляйте свои.


Часть I. Как живут обновления в apt: кандидат, hold, pin

Чтобы не ставить обновления «вслепую», сначала научимся читать, что вообще apt собирается сделать.

1. Разделение: «что я могу поставить» и «что стоит сейчас»

Bash
# 1) какие списки пакетов (индексы) я вижу — скачиваем актуальные с репозиториев
sudo apt update

# 2) что доступно для обновления, без установки
apt list --upgradable

# 3) про один пакет: что стоит, что кандидат, из какого репозитория
apt-cache policy php8.2-fpm

apt-cache policy покажет:

Installed: 8.2.10-1ubuntu0.3
Candidate: 8.2.12-1ubuntu0.4        ← вот это поставит apt upgrade
Version list:
  8.2.12-1ubuntu0.4 500 800 http://archive.ubuntu.com ... -updates  ← приор 800
  8.2.10-1ubuntu0.3 500 500  http://archive.ubuntu.com ...          ← приор 500

Две ключевые вещи: Installed — версия в системе, Candidate — версия, которую apt upgrade поставит. Приоритеты (Pin priority) решают, кто побеждает между репозиториями. Если Candidate «какая-то странная» — вы ещё не поставили, уже можно понять, почему apt выбрал её.

2. hold: «эту версию не трогай»

Если вы держите PHP/MariaDB на конкретной версии и обновляете её вручную, поставьте флаг удержания:

Bash
sudo apt-mark hold php8.2-fpm php8.2-common php8.2-cli
sudo apt-mark showhold          # список удерживаемых пакетов

# снять
sudo apt-mark unhold php8.2-fpm

hold не «выключает обновления» целиком — он не даёт apt upgrade трогать конкретный пакет. Security-пакеты того же PHP при hold не применяются автоматически, и вы обязаны сами закрыть эту CVE — осознанно.

Практическое правило: hold ставим на то, что обновляем вручную (версия PHP, на которую настроены пулы/описания в nginx), а unattended-upgrades не должен на этих пакетах ничего пытаться. Это не «мы не хотим обновлять» — это «мы хотим обновлять вот так и вот тогда». Не держите hold дольше, чем плановый цикл проверок, иначе однажды увидите, что на сервере полгода живёт неподдерживаемая версия.

3. pin: приоритет «не брать отсюда» или «брать отсюда»

pin — более тонкий, чем hold: «когда есть несколько источников, бери вот из этого». Например, вы поставили PPA ondrej/php (сторонний репозиторий с PHP-версиями), и хотите, чтобы из него apt брал только PHP, а не всё подряд:

# /etc/apt/preferences.d/ondrej-php.pref
Package: php*
Pin: release o=LP-PPA-ondrej-php
Pin-Priority: 500

Package: *
Pin: release o=LP-PPA-ondrej-php
Pin-Priority: 100

Строки Pin-Priority: 100 — «бери в последнюю очередь», 500 — обычный, 1000 — «форсировать». Про apt update не сломается; но с 100 вы не рискуете получить из этого PPA libssl вместо репозиторного.

4. Обновление и restart — два разных события

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

  • Пакет «одноразовый» (утилита, скрипт) — обновился, и всё сразу работает (curl, git).
  • Пакет-демон (nginx, php-fpm, mariadb) — на диске новое; живой процесс исполняет старый код, пока его не перезапустить. Часть демонов сама делает reload/restart после обновления (postinst-скрипт); часть — нет. Это надо знать по факту, а не предполагать.
  • Пакет-библиотека (glibc, openssl) — файлы обновились, но каждый процесс, который уже загрузил старую библиотеку в память, продолжает работать со старой. Это отдельный класс «неприменённых обновлений», см. Часть II.

Поэтому после любого apt upgrade — всегда два вопроса: «что просит перезапуск?» и «что просит перезагрузку?». Не «всё же ок, пошло-поехало».

Чек-лист части

  • Держат `hold` на ядре/PHP «навсегда» — забывают, что security-патч теперь не применяется сам и CVE живёт на сервере.
  • Пишут pin-файлы в `/etc/apt/preferences.d/` с опечаткой в имени пакета → `apt` тихо игнорирует правило, приоритеты «не те».
  • Считают, что `apt upgrade` «применил изменения» — а PHP-FPM/nginx/mariadb живут на старом коде, пока их не перезапустить.
  • Обновляют всё разом «по любви к новизне» — на самом деле каждый обновлённый демон = «потенциально рванёт», и это надо тестировать (см. Часть IV).

Часть II. Что ставит unattended-upgrades и когда неизбежна перезагрузка

Главный вопрос на живом VPS: как получать security-патчи своевременно, но без сюрпризов («сервер сам перезагрузился», «PHP-процесс держит старую libc месяцами»). Здесь два отдельных механизма:

1. unattended-upgrades: автоматические security-патчи

Bash
sudo apt install -y unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades   # ставит базовый конфиг

Настройки (/etc/apt/apt.conf.d/50unattended-upgrades):

// какие репозитории брать
Unattended-Upgrade::Allowed-Origins {
    "${distro_id}:${distro_codename}-security";   // только security — это база
    // "${distro_id}:${distro_codename}-updates"; // ← раскомментируют «для удобства» и потом жалеют
};

// что не трогать (наши ручные обновления)
Unattended-Upgrade::Package-Blacklist {
    "php8.2-";     // мы держим на hold, но подстрахуемся
    "mariadb-";
};

// авто-ребут — по умолчанию выключен, включайте только со временным окном
Unattended-Upgrade::Automatic-Reboot "false";
// Unattended-Upgrade::Automatic-Reboot-Time "03:30";   // только если выше "true"

// почта/лог при проблемах
Unattended-Upgrade::Mail "you@example.test";
Unattended-Upgrade::Verbose "true";

Что реально делать:

  • Разрешить автоматическое обновление только security-репозитория. Это патчи к CVE, у них обычно высокая «совместимость внутри минорной версии» и почти никогда не ломается API. Полные -updates (мажорные/фичевые) держим на ручном тестировании.
  • Не включать Automatic-Reboot "true" без явно настроенного окна и без мониторинга. На машине с 1–2 ГБ RAM без бэкап-подтверждения авто-ребут = однажды в 03:37 сайт лежит из-за не закрывшегося nginx.
  • Видеть лог: /var/log/unattended-upgrades/unattended-upgrades.log. Раз в день/неделю смотреть, что он реально сделал: grep -i 'error\|fail' /var/log/unattended-upgrades/*.log.

Проверка, что демон работает:

Bash
systemctl status unattended-upgrades --no-pager | head -n 6
sudo unattended-upgrade --dry-run --debug 2>&1 | tail -n 40   # что сделал бы без установки

2. Когда патч невозможно «применить, не перезагружая»

Есть класс обновлений, которые физически не применяются к живому процессу на машине без рестарта. Это:

  • новое ядро (kernel) — старое ядро работает в памяти; новая версия в /boot, но загрузится только при ребуте.
  • libc (glibc) — фундаментальная библиотека C, её уже загрузил в память каждый демон; перезагрузить её «на лету» для уже запущенного процесса нельзя.
  • libssl/libcrypto, systemd, dbus и др. — аналогично: если процесс держит в памяти «старую» копию библиотеки, он не переедет на новую без рестарта.

Как узнать, что такое было и не применено:

Bash
# 1) явный флаг «нужна перезагрузка» — ставится postinst-скриптами после обновления ядра/libc
ls -l /var/run/reboot-required
cat /var/run/reboot-required   # текстовое «почему»
cat /var/run/reboot-required.pkgs  # список пакетов-виновников

# 2) кто реально держит УДАЛЁННЫЕ файлы на диске (старая версия библиотеки)
#    ставим needrestart (есть в репозитории)
sudo apt install -y needrestart
sudo needrestart -r l          # level: «что требует перезапуска демонов»
sudo needrestart -k            # требует ли ядро перезагрузки

needrestart — это самый честный ответ «применились ли мои патчи». Он сканирует /proc/*/maps, находит процессы, у которых открыты файлы с пометкой deleted (старая версия библиотеки заменена новой на диске, но процесс держит старую), и предлагает аккуратный список «кого перезапустить». Перезапуск sshd/cron/rsyslog часто безопасен (текущая SSH-сессия не рвётся), но php-fpm/nginx/mariadb требуют внимания (см. Часть III — как reload, а не restart).

3. Практика перезагрузки

  • Перезагружайтесь только в окно тишины, когда можно себе это позволить. На одном VPS под личным блогом это обычно ночной слот.
  • Перед ребутом проверьте: (а) что вы можете зайти в консоль провайдера (KVM/VNC через панель) — это ваш запасной вход, если сетевой вход не поднимется; (б) что после ребута firewall (ufw) поднимется сам (это у вас есть, см. Часть II); (в) что после ребута сервер вообще загрузился — если панель VPS не даёт «не загружается» — сразу через неё же.
  • Не откладывайте ребут бесконечно: если флаг /var/run/reboot-required висит неделями, вы «обновлены только наполовину». Это не «мелочь», это «CVE уже закрыты, но ядро ещё то же, что было».
  • Краткая формула: авто-патчи безопасности ставим, авто-ребут отключаем, reboot-required смотрим вручную, ребут планируем, а не ловим «как получится».

Чек-лист части

  • Включили `Automatic-Reboot «true»` без `Automatic-Reboot-Time` — сервер ребутится сразу, как патч встал, иногда в пик трафика.
  • Открыли `-updates` в `Allowed-Origins` «чтобы свежее» — получаете мажорные версии без теста; однажды PHP-минор с изменённым поведением и «почему-то не работает».
  • Флаг `/var/run/reboot-required` висит месяцами → «у меня всё обновлено» не правдиво: libc/ядро старые.
  • Не установили `needrestart`/`checkrestart` → не знаете, что реально требует рестарта сервисов после `apt upgrade`.

Часть III. PHP и MariaDB почти без простоя: reload, параллельная версия, переключение сокета

Обещание «почти без простоя» требует честного разговора: нулевой даунтайм на одной машине для одной БД — это миф. Но «простой в секунды, а не в минуты, и без риска сломать сайт» — это реальность, если знать механику.

1. PHP: минорные обновления почти всегда «незаметны»

Минорное обновление внутри одной ветки (php8.2.10 → php8.2.12) — это apt upgrade php8.2-fpm и мягкий reload:

Bash
# ставим только php-пакеты, не всё подряд (чтобы держать контроль над списком)
sudo apt install --only-upgrade php8.2-fpm php8.2-cli php8.2-common
# пул сам обычно делает «reload» (systemd-юнит сохранный). Если — нет, делаем сами:
sudo systemctl reload php8.2-fpm

Механику reload-против-restart разбирали в systemd; PHP-FPM- specifics — в Части II: reload перечитывает конфиг пула и мягко пересоздаёт воркеров (идущие запросы доживаются, новые обслуживаются). Для WordPress это «почти незаметно»: воркеры под нагрузкой пересоздаются за секунды.

⛔ Перед любым шагом, который рвёт воркеры — убедитесь, что вы не на середине wp-cron-импорта или массового экспорта. reload не рвёт, restart — рвёт; если restart неизбежен (например, libc требует рестарта), ставьте в тихое время.

2. PHP: мажорная версия (8.2 → 8.3) — параллельная установка

Мажорную версию PHP нельзя «обновить на месте без риска»: плагины/тема могут не любить новый движок, поведение функций меняется, php.ini с другой версии не всегда мигрирует. Правильный путь на одном VPS — поставить рядом и переключить:

Bash
# (обычно) нужен PPA на Ubuntu: sudo add-apt-repository ppa:ondrej/php
sudo apt update
sudo apt install -y php8.3-fpm php8.3-cli php8.3-mysql php8.3-curl php8.3-mbstring ...

# новый пул уже в /etc/php/8.3/fpm/pool.d/; сокет будет свой:
sudo systemctl status php8.3-fpm --no-pager | head -n 6
ls -l /run/php/php8.3-fpm.sock

Теперь у вас живут параллельно два PHP-FPM (8.2 и 8.3), каждый со своим сокетом. WordPress на них не подействовал — переключение делаем по доменам, в nginx:

# /etc/nginx/snippets/php-version.conf  (сделайте include, чтобы правка в одном месте)
set $phppool /run/php/php8.2-fpm.sock;   # ← переключаем одной строкой
# /etc/nginx/snippets/fastcgi-php.conf  (в include, не дублируя в каждом vhost)
fastcgi_pass unix:$phppool;
fastcgi_param PHP_ADMIN_VALUE "php_version=on";

И для сайта, который хотите протестировать на 8.3, — в server-блоке:

server {
    listen 443 ssl http2;
    server_name example.test;
    ...
    # локальный override для одного vhost:
    location ~ \.php$ {
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
        include snippets/fastcgi-php.conf;
        ...
    }
}
Bash
sudo nginx -t && sudo systemctl reload nginx   # reload — текущие соединения доживут

Дальше — drill на копии (Часть IV) или, если вы уже переключили, беглая проверка. Если что-то пошло, откат = вернуть include/override на 8.2-сокет и reload nginx — секунды. Когда всё хорошо, правим snippets/php-version.conf на 8.3 глобально (для всех доменов), и потом можно снять 8.2-пакет (apt remove php8.2-fpm), а перед снятием — hold снимаем.

3. MariaDB: честный ответ «restart нужен»

Мажорная ветка MariaDB (10.5 → 10.11) — это всегда миграция системных таблиц, рестарт сервиса, иногда «после апгрейда нельзя понизить». На одном VPS без репликации честный путь: (а) тест на копии; (б) окно обслуживания; (в) дамп базы перед стартом (мы умеем из Части IV); (г) systemctl restart mariadb и проверка.

Минорные (10.11.4 → 10.11.6) часто рестарта не требуют или требуют «осторожный». Проверка:

Bash
apt list --upgradable | grep mariadb
apt-cache policy mariadb-server
# посмотрим, что пакет просит после апгрейда:
sudo needrestart -r l | grep -i mariadb

reload у MariaDB нет (сервер базы данных — не PHP-FPM с воркерами), но restart в минорном обновлении обычно = секунды на холодном старте с InnoDB-пулом (пул прогреется; WordPress «на первой сотне хитов» будет медленнее, см. warm-up в Части IV — та же логика для кэшей).

Если нужно «совсем без простоя» для базы — это репликация (master–master/master–slave на второй машине), тема следующей серии, здесь её не раскрываем, а фиксируем: на одной машине «нулевой даунтайм БД» — это миф, «секунды» — достижимо.

4. Нginx: самый аккуратный, почти всегда reload

Обычное обновление nginx (apt upgrade nginx) пост-скрипт делает systemctl reload (не restart). Ваш WordPress этого не заметит. Исключение — когда nginx требует рестарта (редко; например, после обновления модулей или glibc, если nginx держит старую libc в памяти — тогда needrestart подскажет). Порядок безопасный:

Bash
sudo apt install --only-upgrade nginx
sudo nginx -t                        # синтаксис конфига — перед reload
sudo systemctl reload nginx          # текущие соединения не рвутся

5. Связка: что реально рвёт трафик и что нет

ДействиеЧто происходитПростой для пользователя
reload nginxmaster перечитывает конфиг, старые воркеры доживают свои запросы, новые на новом конфигенет (или ~мс)
restart nginxвсе воркеры убиты, сервер поднимается заново1–5 секунд
reload php-fpmmaster «мягко» пересоздаёт детейнет (или ~мс)
restart php-fpmдети убиты; запросы на них — 502 до подъёма1–5 секунд
restart mariadbсервер базы останавливается и поднимается (прогрев пула)от секунд до десятков секунд (в зависимости от размера пула и «прогретости» ОС-кэша)

Именно из этой таблицы вытекает, что «обновления без простоя» — это умение выбрать reload, а не restart, а не магия. Если же вам объективно нужен restart (libc, ядро, БД-мажор) — это окно обслуживания, и в окне нужно уметь откатиться (Часть V) и быстро восстановиться после (Часть III + warm-up из PHP-FPM).

Чек-лист части

  • Ставят мажорную PHP «обновились пакетом», не проверяя, что плагины на ней живут → однажды не поднимается админка. Правильно — параллельная установка + переключение сокета.
  • Держат только один PHP-сокет и «меняют его», а не ставят рядом → откат = `restart` с риском.
  • На всякий случай сделали `restart php8.3-fpm` вместо `reload` → рванули живые запросы «на ровном месте».
  • Не проверили `nginx -t` перед reload/после переключения сокета → nginx reload провалился, и на 8.2-сокет запросы не идут, а на 8.3 nginx не слушает (белый экран/`502`).
  • Не сняли `hold` с PHP после того, как перешли на 8.3 глобально →security-патчи к 8.2 продолжают не ставиться, а 8.3 на него не распространяется.

Часть IV. Тест на копии: staging на одной машине без второй машины

Единственная надёжная защита от «я обновил — а сайт не поднимается» — это прежде, чем на боевой, поставить кандидатную версию в песочницу и проверить. Отдельная staging-машина идеальна, но на самописном VPS её часто нет. Поэтому учимся делать песочницу на той же машине, не трогая боевой сайт.

1. Два способа «копии рядом»

  • Вариант А (проще): другая БД + другой wp-content + другой домен/порт на том же стеке. Разворачиваем копию боевого сайта как «второй сайт», обновляем пакет, проверяем, боевой не тронут.
  • Вариант Б (чище): временный контейнер или chroot с той же версией PHP. Но это уже тема Контейнеры и оркестрация; для одной машины без Docker вариант А реальнее.

Дальше — вариант А.

2. Клон базы в песочную (механика из Части V)

Bash
# снимаем дамп боевой базы «на лету», без блокировки
mysqldump --single-transaction --routines --triggers -u wp_example wp_example \
  | gzip > /tmp/staging_dump_$(date +%F).sql.gz

# создаём песочную БД и пользователя (не трогаем боевую!)
sudo mariadb -e "CREATE DATABASE wp_staging CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;\
                 CREATE USER 'wp_staging'@'localhost' IDENTIFIED BY 'песочный_пароль';\
                 GRANT ALL ON wp_staging.* TO 'wp_staging'@'localhost';"
gunzip -c /tmp/staging_dump_$(date +%F).sql.gz | sudo mariadb wp_staging

3. Клон файлов и wp-config песочницы

Bash
sudo -u site_example cp -a /var/www/example.test /var/www/staging.test
# правим wp-config песочницы на staging-БД и staging-пользователя

В nginx подвешиваем staging.test на другой порт (listen 8081;) или на поддомен staging.example.test, и держим его не открытым наружу в firewall: из Части II — либо bind на 127.0.0.1, либо ufw открывает 8081 только под ваш IP (allow from 203.0.113.7 to any port 8081). Иначе вы только что выложили копию боевого сайта в интернет — включая тестовые данные.

⛔ Песочница обязана быть закрыта наружу. Это та же конвенция, что и везде в ветке: тестовый стенд с боевым дампом (в нём — пользователи, хэши паролей, настройки плагинов) снаружи недопустим. Проверьте ss -tulpn и ufw status после настройки staging.

4. Ставим кандидатную версию на песочницу и проверяем

# на staging-пуле ставим новую PHP (или ту, что тестируем), НЕ на боевом
# после параллельной установки (см. Часть III) переключаем STAGING-vhost на новый сокет
# боевой vhost остаётся на php8.2-fpm.sock

Минимальный набор проверок (не «открыть и посмотреть», а пройтись руками по критичному):

  • Главная открывается (200, контент на месте, кириллица не «кракозябрами»).
  • Админка заходит («/wp-admin»), видны посты, настройки, пользователи.
  • Один сабмит формы работает (комментарий/форма плагином) — ловит поломку `wp-cron`/nonce.
  • Загрузка медиа (upload) проходит — ловит поломку `upload_max_filesize`/прав на `wp-content/uploads`.
  • `php -m` на песочнице содержит нужные расширения, что стояли на боевом (`mysqli`, `curl`, `mbstring`, `gd/imagick`, `intl`) — мажорная PHP часто «теряет» расширение, которое надо ставить отдельно (`php8.3-gd`).
  • Логи не сыпят фаталами: `tail /var/log/php8.3-fpm.log`, `journalctl -u php8.3-fpm -p err —since -5m`.

5. Что не заменяет тест на копии (и почему)

Тест на копии ловит «не поднимется/сломается», но не ловит нагрузочное поведение новой версии (другой опкод, другой план запроса БД). Для «обновил и поедет ли под пиком» это Нагрузочное тестирование из серии про архитектуру, плюс php-fpm slowlog (см. Часть VI). В рамках одной машины и одной статьи фиксируем главное: никакой релиз версии PHP/БД/nginx не идёт на боевой без предварительной проверки на копии — даже «минорный security», если он меняет major-библиотеку.

Чек-лист части

  • Песочница (`staging.test`, порт 8081) открыта наружу в firewall с боевым дампом внутри → утечка пользователей/хэшей.
  • Тестируют новую PHP «переключив весь сервер» → если не зашло, лежит весь боевой сайт. Правильно: боевой на старом сокете, staging на новом.
  • Проверяют «главная открылась» → а админка/загрузка медиа/формы — это другие коды, и они чаще всего и ломаются.
  • Не проверили `php -m` → `gd`/`imagick`/`intl` новой мажорной версии не установлены, и «почему-то не генерятся миниатюры».

Часть V. Откат: apt-история, понижение версии и ядро через GRUB

Если после обновления что-то сломалось и копия «не предупредила» (или вы обновили без копии — не надо так, но случается), нужен быстрый путь назад. Разберём откат пакета и отдельно — ядро.

1. Что вообще ставилось и когда: apt-история

# последние транзакции: дата, какие пакеты, кто инициировал
less /var/log/apt/history.log
# быстрый взгляд на последние обновления
grep -A3 'Install\|Upgrade' /var/log/apt/history.log | tail -n 40
# полная история (ротация history.log держит прошлые месяцы в .gz)
zcat /var/log/apt/history.log.*.gz | less

Здесь вы видите, например, что «в 03:11 unattended-upgrades поставил php8.2-fpm 8.2.12», и понимаете, что сравнивать/откатывать.

2. Понизить версию пакета назад

Если нужная версия ещё лежит в кэше/репозитории:

Bash
# посмотреть доступные версии пакета
apt-cache madison php8.2-fpm | head
# поставить конкретную (понижение) — --allow-downgrades обязателен при downgrade
sudo apt-get install php8.2-fpm=8.2.10-1ubuntu0.3 --allow-downgrades
sudo systemctl reload php8.2-fpm

Нюансы честного понижения:

  • не всякая версия «откатывается»: у PHP/БД дамп базы после миграции на новую версию может быть несовместим со старой (MariaDB «понизить» нельзя после миграции системных таблиц). Поэтому дамп перед мажорным апгрейдом (Часть III) — не «на всякий», а реальный путь отката базы.
  • зависимости: понижение одного пакета может требовать понижения связанных (apt покажет список «будут понижены»). Читайте вывод перед Y.
  • hold снова: после успешного отката «заморозьте» (apt-mark hold) пакет, чтобы unattended-upgrades завтра не поставил ту же битую версию назад.

3. Ядро — только через GRUB и с планировкой

Пакеты ядра не «понижаются» той же командой безопасно: ядро грузится с /boot через загрузчик GRUB. Откат ядра = загрузить старую версию:

# посмотреть, какие ядра стоят
dpkg -l | grep -i 'linux-image' | awk '$2 ~ /linux-image-[0-9]/'
ls -l /boot/vmlinuz-*

В GRUB-меню при загрузке выбирается старое работающее ядро. Держать старое ядро установленным (просто не удаляйте его пакет) — страховка. После успешной загрузки на новом ядре — можно снять страховку.

Для панели VPS без консоли «выбрать ядро при загрузке» (а многие облачные так и дают) — единственный путь, если новое ядро грузится в kernel panic. Поэтому, прежде чем обновлять ядро: узнайте, что есть консоль провайдера (KVM/VNC), и что вы можете через неё выбрать ядро в GRUB-меню. Нет консоли — не обновляйте ядро, не застраховав через snapshot/бэкап диска.

4. autoremove, «осиротевшие» пакеты и мусор

После откатов/переездов между версиями PHP остаются старые .conf-пулы и старые ядра в /boot (закончившееся место на /boot — отдельная частая причина «apt не может поставить обновление»: см. финальную статью про диск). Аккуратно чистим:

Bash
sudo apt-get -s autoremove --purge   # СНАЧАЛА симуляция: что он собирается удалить
sudo apt autoremove --purge          # потом уже реально
sudo apt autoclean                   # старый кэш пакетов, чтобы /var/cache/apt не раздувался

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

5. Порядок «сломалось после обновления» — быстрый

# 1) что стоит сейчас и что ставилось (apt history)
grep -i 'Upgrade' /var/log/apt/history.log | tail
# 2) что из живых процессов держит удалённую библиотеку (неприменённое)
sudo needrestart -r l
# 3) какой юнит упал / перезапускается в цикле
systemctl --failed --no-pager ; sudo journalctl -u php8.3-fpm -p err --since -15m
# 4) откатить пакет, который подозревается (apt-get install pkg=ver --allow-downgrades)
# 5) reload затронутого демона, проверить сайт
# 6) apt-mark hold на откатанную версию, чтобы авто не вернуло битую

Чек-лист части

  • Понижают версию MariaDB «apt-downgrade» после мажорного апгрейда → БД может не подняться на старой версии из-за нового формата таблиц. Откат базы = восстановление дампа, не apt.
  • Удаляют старое ядро `autoremove` «для места» → нечем откатиться, если новое ядро не грузится, а консоли провайдера нет.
  • Не читают вывод `apt` перед `Y` при downgrade → понижают зависимости, которые тянут за собой «свал» полстека.
  • Забыли `hold` после отката → `unattended-upgrades` ночью снова ставит ту же битую версию.

Инструмент под задачу

После автопатча главный вопрос — «что именно произошло ночью»: разбор apt-истории и логов обновлений по временной метке. Попробуйте инструмент прямо здесь, на своих данных:

AI Log Analyzer
Гость
Токены: 0 / 50 000
Гостевой лимит — 50 000 токенов. Зарегистрируйтесь — для пользователей лимит + 200 000 токенов.
0 / 20 000 символов
ИИ анализирует логи...

Результат анализа:

Чек-лист граблей всей статьи

  • Считают `apt upgrade` = «изменения применены» — живые процессы на старом коде, пока не перезапустишь (libc/демоны).
  • `unattended-upgrades` на `-updates` «чтобы всё свежее» → мажорные версии без теста.
  • `Automatic-Reboot «true»` без окна и мониторинга → «сервер сам перезагрузился в пик».
  • Флаг `/var/run/reboot-required` висит неделями → «обновлён только наполовину», старое ядро живёт.
  • `hold` на ядре/PHP навсегда, без плана вручную закрыть CVE.
  • Мажорную PHP ставят «вместо», а не рядом → нет пути отката за секунды (нет второго сокета).
  • `restart` там, где достаточно `reload` (PHP-FPM, nginx) → рвут запросы «на ровном месте».
  • Тестируют новую PHP на боевом, переключая весь сервер → не зашло, лежит весь сайт.
  • Песочница (`staging.test`) открыта наружу с боевым дампом → утечка.
  • `autoremove` без симуляции (`apt-get -s autoremove`) → удалили нужное старое ядро/зависимость.
  • Не сделали дамп базы перед мажорным апгрейдом БД → «откатить» базу нечем.

Материал написан простым языком; любой англоязычный термин (apt, пакет, candidate, hold, pin, unattended-upgrades, libc/glibc, reload/restart, CVE, needrestart, reboot-required, blue-green, staging, GRUB, autoremove, logrotate) объясняется при первом употреблении. Конфиги — для Ubuntu/Debian; имена пакетов и юнитов (php8.2-fpm/php8.3-fpm, ssh/sshd, mariadb/mysql), поведение пост-скриптов apt («сам делает reload» или нет) и версия PPA ondrej/php сознательно условны — проверяйте фактические через apt-cache policy, systemctl status, /var/log/apt/history.log. Домены/IP/пароли/версии условные (example.test, staging.test, 203.0.113.x — TEST-NET как в №1); подставляйте свои. Правило «не остаться без входа» и «боевой не трогать, пока не проверил на копии» действует во всех частях: песочница не открыта наружу, боевой сокет не переключён, дамп базы снят перед мажорным шагом. Тонкие механизмы (live-patch ядра типа kpatch/livepatch, полностью zero-downtime через репликацию БД, конфигурационное управление через Ansible на флот машин) упомянуты лишь где нужно — это следующий уровень или отдельная разовая тема, когда базовый цикл «патч вовремя + reload где можно + тест на копии + откат» уже выстроен.

👤 Зарегистрируйтесь, чтобы добавить материал в закладки.