Один 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
systemctl status php8.3-fpm— жив ли.ls -l /run/php/php8.3-fpm.sock— есть ли сокет.docker logs caddy --tail 50— что говорит Caddy.docker restart caddy— подхватить новый сокет.curl -I https://iniproject.ru/admin/— проверить ответ.- Если не помогло — смотреть
error.logадминки и логи PHP.
В 90% случаев помогает docker restart caddy. В оставшихся 10% — сокет, права или конфиг.
Почему не Kubernetes
Потому что один VPS дешевле, проще и предсказуемее. Мне не нужны автоскейлинг, service mesh и 20 YAML-файлов. Мне нужно, чтобы сайт открывался и бэкап разворачивался.
Kubernetes понадобится, когда:
- несколько серверов;
- горизонтальное масштабирование;
- сложные деплои с откатами;
- команда, которая не знает, где что лежит.
Пока этого нет — один VPS честнее.
Вместо вывода
Хороший деплой — скучный. Ты знаешь, где конфиг, где сокет, где база и где бэкап. Если что-то сломалось, у тебя есть пять команд, а не пять гипотез.
Самое важное, что я понял: инфраструктура — это набор граблей, которые ты уже прошёл. Inode, сокет, tar, кэш, .env — каждая из них когда-то стоила вечера. Зато теперь я знаю, куда смотреть.
Статья написана при поддержке:
