// статьи / статья
#статья #vps#caddy#php#деплой

> Один VPS: деплой, Caddy, PHP-FPM и откат

Почему после обновления PHP отваливается 502, при чём тут inode, как устроены бэкапы и откат.

SkilinPur 25.10.2026 5 мин чтения серия: как это устроено 👁
Один VPS: деплой, Caddy, PHP-FPM и откат

Один VPS, Caddy в контейнере, PHP-FPM на хосте и SQLite. Не Kubernetes. Пока работает — это самый скучный и самый понятный деплой из всех, что я делал.

Стек без облака

Сайт живёт на одном сервере с Ubuntu 24.04. Внутри:

  • Caddy — контейнер, reverse proxy и статика.
  • PHP 8.3-FPM — на хосте, сокет проброшен в контейнер.
  • SQLite — файл в admin/data/admin.db.
  • Статика — HTML, CSS, JS, JSON, картинки.

Нет Docker Compose для сайта, нет оркестратора, нет отдельной базы. Caddy запускается вручную через docker run. Это не модно, но я знаю, где что лежит.

Главное — не потерять монтирования. Контейнер видит только то, что ему пробросили:

  • /opt/caddy/Caddyfile;
  • /var/www/InIProject;
  • /var/www/devsite;
  • /var/www/chzog;
  • сокет PHP-FPM;
  • volume caddy_data.

Если пересоздать контейнер и забыть одно монтирование — отвалится поддомен или PHP. Поэтому запуск Caddy — это не «docker run», а ритуал.

Caddy: inode — главный враг

Caddy читает конфиг из файла, который примонтирован в контейнер. Если править файл через sed -i или распаковать поверх через tar, создаётся новый inode. Bind-mount внутри контейнера продолжает смотреть на старый. Внешне файл новый, внутри — старый.

Симптом: правишь Caddyfile, делаешь caddy reload, а ничего не меняется. Или ещё хуже — меняется не то.

Лечение:

docker restart caddy

Именно restart, а не reload. Reload перечитывает файл по тому же пути, но если inode уже другой — контейнер видит старую версию.

Урок: если правишь примонтированный конфиг не «на месте», а заменой файла — перезапускай контейнер.

PHP-FPM: сокет, который протухает

PHP-FPM на хосте слушает Unix-сокет. Caddy подключён к нему через mount. Когда PHP-FPM перезапускается, сокет создаётся заново — с новым inode. Caddy держит старый и получает ошибку.

Симптом: статика работает, а /admin/, /project/, /roadmap/ отдают 502. При этом PHP-FPM формально запущен.

Лечение:

systemctl restart php8.3-fpm
docker restart caddy

Именно в таком порядке: сначала PHP, потом Caddy, чтобы он подхватил новый сокет.

Автообновление PHP

23.10.2026 unattended-upgrades обновил php8.3-fpm и перезапустил его. Сайт отдал 502 на всех PHP-страницах. Помог docker restart caddy.

Чтобы это не повторялось, появился drop-in:

# /etc/systemd/system/php8.3-fpm.service.d/restart-caddy.conf
[Service]
ExecStartPost=-/bin/sh -c "sleep 2; /usr/bin/docker restart caddy >/dev/null 2>&1"

Теперь после старта PHP-FPM Caddy перезапускается сам.

Урок: обновления — это не только «новая версия». Это ещё и перезапуск сервисов, о котором ты не просил.

Деплой: dev и prod

Есть две копии:

  • dev — /var/www/devsite, техническая площадка.
  • prod — /var/www/InIProject, боевой сайт.

Dev закрыт снаружи заглушкой; открыты только /admin/ и /assets/, а для тестов нужен заголовок X-Devsite-Key. Это не идеальная изоляция, но достаточно, чтобы случайный посетитель не видел полуготовый сайт.

Перенос — скриптом migrate-to-prod.sh:

bash /root/migrate-to-prod.sh                  # только код
bash /root/migrate-to-prod.sh --no-switch      # без переключения Caddy
bash /root/migrate-to-prod.sh --with-db        # сравнить базы и спросить
bash /root/migrate-to-prod.sh --with-content   # + data/, uploads/, music/

По умолчанию — только код. Контент на проде не перезаписывается. Это прямое следствие инцидента 21.10.2026, когда старый скрипт затёр базу.

Перед миграцией базы печатается сравнение: счётчики, id, которых нет в одной из баз. Решение принимает человек, а не скрипт.

Урок: «синхронизировать всё» — опасная команда. Лучше пять флагов, чем один сюрприз.

Бэкапы и откат

Бэкап — это tar по нужным каталогам:

cd / && tar czf /tmp/iniproject-backup-$(date +%Y%m%d-%H%M) \
  --exclude="var/www/devsite/music" \
  --exclude="var/www/InIProject/music" \
  --exclude="var/www/devsite/.env" \
  --exclude="var/www/InIProject/.env" \
  var/www opt/caddy etc/php

Музыка исключена: она тяжёлая и не меняется. .env исключён: секреты не должны лежать в архивах. Caddy и PHP-конфиги включены: без них сайт не поднимется.

Откат — rollback-prod.sh, который разворачивает последний архив. Но tar не удаляет файлы, появившиеся после бэкапа. Если после архива появился новый js/pjax.js, он останется и может сломать сайт.

Урок: откат — это не только «вернуть старое», но и «убрать новое». Иногда руками.

Caddy: порядок правил

Caddy выполняет rewrite раньше respond. Если нужно «сначала 404 для секретов, потом rewrite», это надо оборачивать в route { }, где порядок — как написано.

Ещё есть no_html_cache для страниц и no-store для статей. Это не прихоть: HTML с пользовательскими данными нельзя кэшировать надолго.

Урок: конфиг прокси — это код. У него есть порядок, побочные эффекты и грабли.

Чек-лист, когда 502

  1. systemctl status php8.3-fpm — жив ли.
  2. ls -l /run/php/php8.3-fpm.sock — есть ли сокет.
  3. docker logs caddy --tail 50 — что говорит Caddy.
  4. docker restart caddy — подхватить новый сокет.
  5. curl -I https://iniproject.ru/admin/ — проверить ответ.
  6. Если не помогло — смотреть error.log админки и логи PHP.

В 90% случаев помогает docker restart caddy. В оставшихся 10% — сокет, права или конфиг.

Почему не Kubernetes

Потому что один VPS дешевле, проще и предсказуемее. Мне не нужны автоскейлинг, service mesh и 20 YAML-файлов. Мне нужно, чтобы сайт открывался и бэкап разворачивался.

Kubernetes понадобится, когда:

  • несколько серверов;
  • горизонтальное масштабирование;
  • сложные деплои с откатами;
  • команда, которая не знает, где что лежит.

Пока этого нет — один VPS честнее.

Вместо вывода

Хороший деплой — скучный. Ты знаешь, где конфиг, где сокет, где база и где бэкап. Если что-то сломалось, у тебя есть пять команд, а не пять гипотез.

Самое важное, что я понял: инфраструктура — это набор граблей, которые ты уже прошёл. Inode, сокет, tar, кэш, .env — каждая из них когда-то стоила вечера. Зато теперь я знаю, куда смотреть.

Статья написана при поддержке:

// участник команды
Glachi
Developer · middle

Гений, миллиордер, плейбой, филантроп.

// реакции
SkilinPurучастник InIProject