Zero-downtime деплой на обычном VPS через rsync и симлинки
2026-08-20 10:00:00Zero-downtime деплой ассоциируется с Kubernetes и блю-грин на десяти серверах. На практике его можно собрать и на одном обычном VPS буквально за вечер.
Схема каталогов
/var/www/app/
releases/
20260810-1200/
20260815-0930/
shared/
.env
storage/
current -> releases/20260815-0930
Nginx и php-fpm смотрят не на конкретный релиз, а на symlink current.
Переключение релиза — это атомарная смена симлинка, а не подмена файлов на лету.
Сам деплой
#!/usr/bin/env bash
set -euo pipefail
RELEASE=$(date +%Y%m%d-%H%M)
TARGET="/var/www/app/releases/$RELEASE"
rsync -az --delete \
--exclude='.git' \
--exclude='storage' \
./ deploy@server:"$TARGET"/
ssh deploy@server "
ln -sfn /var/www/app/shared/.env $TARGET/.env
ln -sfn /var/www/app/shared/storage $TARGET/storage
cd $TARGET && composer install --no-dev -o
ln -sfn $TARGET /var/www/app/current
sudo systemctl reload php8.3-fpm
"
Почему это zero-downtime
Смена symlink — атомарная операция на уровне файловой системы: nginx либо видит старый путь,
либо новый, промежуточного «наполовину выкачанного» состояния не существует.
reload у php-fpm (не restart) поднимает новые воркеры, не убивая старые,
пока они не доработают текущие запросы.
Откат
ssh deploy@server "ln -sfn /var/www/app/releases/20260810-1200 /var/www/app/current"
Откат — тоже атомарная смена symlink на предыдущий релиз. Никаких git revert под нагрузкой в 3 часа ночи.
Что не забыть
- Хранить не больше 3-5 последних релизов — иначе диск закончится незаметно
.envи загруженные файлы держать вshared, а не в каждом релизе- Прогонять миграции БД отдельным шагом, до переключения симлинка