🚀
Хостинг

Деплой без простоя: папки релизов и переключение через symlink

05.11.2026
← Все статьи

При обычном деплое — будь то FTP или git pull — обновление происходит прямо внутри папки работающего сайта. Файлы заменяются один за другим, библиотеки Composer перезаписываются, и несколько секунд или минут сайт находится в состоянии «наполовину старый, наполовину новый». Посетитель, зашедший в этот момент, может увидеть белую страницу, ошибку «Class not found» или сломанную вёрстку. Идея деплоя без простоя проста: новая версия полностью готовится в отдельной папке, проверяется, и только потом сайт мгновенно переключается на неё.

Структура папок

В основе метода три папки. В releases каждый деплой хранится под собственным именем, обычно в виде даты и времени. В shared лежит то, что не меняется от версии к версии: файл .env, загруженные пользователями изображения, журналы. А current — не настоящая папка, а символическая ссылка на последний релиз. Веб-сервер знает только о current, поэтому, перенаправив ссылку, вы переводите весь сайт на новую версию.

/var/www/example.uz/
├── current -> releases/20261104-1530
├── releases/
│   ├── 20261102-1010/
│   ├── 20261103-1745/
│   └── 20261104-1530/
└── shared/
    ├── .env
    └── storage/

Проще всего такая схема реализуется на VPS, где корневую папку веб-сервера вы задаёте сами. На shared-хостинге папка сайта может быть жёстко задана панелью; можно ли заменить её символической ссылкой, уточните в кабинете sayt.uz или через техническую поддержку.

Подготовка нового релиза

Каждый деплой начинается с создания новой папки. В неё забирается код из репозитория, устанавливаются зависимости и подключаются общие файлы через symlink. Важно, что ни один из этих шагов не затрагивает работающий сайт: если Composer выдаст ошибку или фронтенд не соберётся, скрипт остановится, а посетители ничего не заметят.

#!/bin/bash
set -e
APP=/var/www/example.uz
REL=$APP/releases/$(date +%Y%m%d-%H%M%S)

git clone --depth 1 --branch main git@github.com:misol/example-sayt.git "$REL"
cd "$REL"
ln -s $APP/shared/.env .env
rm -rf storage && ln -s $APP/shared/storage storage
composer install --no-dev --optimize-autoloader
php artisan config:cache
php artisan migrate --force

Отдельного внимания требуют миграции базы данных. Они выполняются до переключения current, то есть несколько секунд старый код работает с новой структурой таблиц. Поэтому миграции нужно писать обратно совместимыми: сначала добавляется новая колонка, а когда код начал её использовать, старая удаляется в следующем деплое. Переименование или удаление колонки в одном деплое приведёт к сбою.

Атомарная замена symlink

Многие используют ln -sfn, но эта команда фактически удаляет старую ссылку и создаёт новую, и между этими действиями ссылки на мгновение нет вовсе. Надёжнее создать временную ссылку и переместить её на место через mv -T: в Linux переименование — одна атомарная операция, поэтому запрос попадёт либо в старую, либо в новую версию.

ln -s "$REL" $APP/current_tmp
mv -T $APP/current_tmp $APP/current

PHP-FPM, OPcache и настройка Nginx

Даже после смены ссылки PHP иногда продолжает выполнять старые файлы. Причина в OPcache и realpath-кеше PHP: они запоминают файлы по реальному пути и не сразу замечают, что current теперь указывает на другую папку. Если в Nginx вместо $document_root использовать $realpath_root, PHP всегда получает реальный путь после раскрытия ссылки, и проблема в основном исчезает.

server {
    server_name example.uz;
    root /var/www/example.uz/current/public;

    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        fastcgi_param DOCUMENT_ROOT $realpath_root;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    }
}

Для дополнительной гарантии в конце деплоя можно выполнить reload PHP-FPM. В отличие от restart, reload не обрывает текущие запросы: старые процессы дорабатывают свои запросы, а новые запускаются уже с новым кодом. Путь к сокету и имя службы зависят от версии PHP, проверьте их на своём сервере.

sudo systemctl reload php8.3-fpm

Откат и очистка старых релизов

Главное преимущество метода — скорость отката. Если в новой версии нашлась ошибка, ничего не нужно загружать заново: достаточно вернуть current на предыдущую папку, и через секунду сайт снова в прежнем состоянии. Чтобы диск не переполнялся, хранят несколько последних релизов, а остальные удаляют.

# откат на предыдущий релиз
PREV=$(ls -1dt $APP/releases/* | sed -n 2p)
ln -s "$PREV" $APP/current_tmp && mv -T $APP/current_tmp $APP/current

# оставить только 5 последних релизов
ls -1dt $APP/releases/* | tail -n +6 | xargs -r rm -rf

Помните: база данных при откате не возвращается. Поэтому правило обратно совместимых миграций здесь тоже решающее.

Пример: интернет-магазин в Ташкенте

Ташкентский интернет-магазин бытовой техники обновляется несколько раз в день, и раньше при каждом деплое на минуту-две появлялась страница ошибки, а у покупателей пропадали товары из корзины. После того как разработчик внедрил описанную схему на VPS, новая версия стала собираться в фоне, а переключение сократилось до незаметного мгновения. Когда однажды на новой странице оплаты нашли ошибку, он одной командой вернулся на прошлый релиз, спокойно исправил баг и выкатил исправление через час.

Готовые инструменты

Собрать процесс вручную полезно для понимания, но в реальных проектах удобнее готовые инструменты. В мире PHP это Deployer, в Ruby — Capistrano: они автоматически создают ту же структуру releases, shared и current, сами берут блокировку и чистят старые релизы. В связке с GitHub Actions или GitLab CI деплой запускается сам при push в ветку main. Какой бы путь вы ни выбрали, принцип остаётся тем же: посетитель не должен видеть новую версию, пока она полностью не готова.

Похожие статьи

🔀 nvm: управление версиями Node.js, переключение и .nvmrc 📧 Как создать почтовый ящик в ISPmanager и настроить его ⚡ Redis на хостинге: сессии и кеш на практике ⚙️ PM2 для Node.js: запуск, логи, cluster-режим и автозапуск
🌐 Язык
🇺🇿 O'zbek 🇺🇿 Ўзбек 🇷🇺 Русский ✓ 🇬🇧 English