Ой, ничего не найдено!

К сожалению, по вашему запросу пока ничего нет (но это только пока!), зато вы можете подписаться на нашу замечательную email-рассылку, чтобы не пропустить самое интересное в будущем.

  • 13

Деплой без FTP в 2026-м: git pull, хуки и простой CI/CD на своём VPS

  • 3 минуты на чтение

Часть 13 из 14 · Серия «Сервер в бою»

Признавайтесь: у скольких из вас прямо сейчас в FileZilla сохранено подключение к боевому серверу? Деплой перетаскиванием файлов дожил до 2026-го в прекрасной форме — и каждый, кто им пользуется, знает этот букет ощущений: «а какие файлы я менял?», «а точно все залил?», посетители, поймавшие сайт в момент, когда половина файлов новая, а половина старая, и главный ужас — «сломалось, срочно вернуть как было!» — а «как было» не существует нигде, кроме вашей памяти.

Смешное в том, что инструмент, решающий все эти проблемы, у вас уже есть. Код ведь и так лежит в Git (а если нет — начните с этого, серьёзно): полная история, атомарные переключения, откат на любую точку одной командой. Осталось заметить очевидное: Git — это не только контроль версий, это ещё и транспорт. Сегодня пройдём путь от git pull вместо FTP до выкатки одной командой с автоматическим откатом — четырьмя уровнями, каждый из которых самодостаточен: остановиться можно на любом, и это уже будет лучше FileZilla на порядок.

Уровень 1: git pull вместо FTP — минимальный переход

Схема: код живёт на GitHub/GitLab (или вашем Gitea), сервер имеет к репозиторию доступ только на чтение, деплой — это «сервер забрал свежий код». Настройка на десять минут.

На сервере, от пользователя сайта (не от root — восьмая статья объясняла, почему владельцы файлов не должны плыть):

sudo -u site1 -i
ssh-keygen -t ed25519 -f ~/.ssh/deploy_key -N ""
cat ~/.ssh/deploy_key.pub

Публичный ключ добавляем в репозиторий как deploy key с правами read-only — это важная деталь: даже полный взлом сервера (от которого мы застраховались изоляцией) не даст злоумышленнику права писать в ваш репозиторий. Дальше:

git clone [email protected]:you/site1.git /var/www/site1
SSD/NVMe в комплекте
Пока вы это читаете, чей-то сайт уже летает
NVMe-диски, свежее железо и честные ресурсы без «соседей», которые съедают всю мощность. Разница чувствуется с первого запроса.
Разогнать проект

И весь деплой отныне:

cd /var/www/site1 && git pull

Что мы получили одним махом: файлы всегда в согласованном состоянии («залил не всё» больше не существует как явление); полная история выкаток (git log); возможность посмотреть, чем прод отличается от репозитория (git status — и любая «правка по-быстрому прямо на сервере» становится видимой, что дисциплинирует лучше регламентов); и примитивный, но работающий откат — git checkout на предыдущий коммит. Уже на этом уровне FTP проигрывает всухую.

Два обязательных штриха безопасности. Первый: каталог .git не должен быть доступен из веба. У Laravel и большинства фреймворков корень сайта — public/, и проблема не возникает; если же root смотрит в корень репозитория — закройте явно:

location ~ /\.(git|env) { deny all; }

Открытый .git — это скачиваемая копия всех ваших исходников вместе с историей; сканеры проверяют его на каждом встречном домене. Второй штрих: .env и прочие секреты в репозитории не живут — они лежат на сервере рядом и в .gitignore. К этому ещё вернёмся на уровне 3.

Уровень 2: pull — это не деплой. Скрипт

Быстро выяснится, что забрать код — полдела: надо поставить зависимости, прогнать миграции и — помните обещания? — сбросить кэши, которые мы так старательно настраивали. Пропущенный шаг = «я же исправил, почему не работает?!» (opcache, девятая статья) или «на сайте старая страница» (FastCGI cache). Деплой — это последовательность, а последовательности мы уже договорились оформлять скриптами со всей гигиеной. deploy.sh для типичного Laravel-сайта:

#!/bin/bash
set -euo pipefail
cd /var/www/site1

php artisan down --retry=15          # аккуратная заглушка на время работ

git pull origin main
composer install --no-dev --optimize-autoloader --no-interaction
php artisan migrate --force
php artisan config:cache && php artisan route:cache && php artisan view:cache

sudo /usr/bin/systemctl reload php8.3-fpm      # сброс opcache
sudo /usr/local/bin/purge-fastcgi-cache.sh     # rm -rf кэша nginx, статья 11

php artisan up
curl -fsS https://site1.ru > /dev/null         # смок-тест: сайт жив?
echo "Deployed: $(git rev-parse --short HEAD)"

(Точечные sudo для пользователя сайта разрешаются через sudoers на конкретные команды — не общий доступ.) Благодаря set -e скрипт останавливается на первой ошибке: упал composer — миграции не поедут, кэш не сбросится, и вы разбираетесь с проблемой, а не с её последствиями. Выкатка превратилась в одну команду с предсказуемым содержимым — собственно, заголовок статьи выполнен. Но у этой схемы остаются два врождённых дефекта: пока крутится composer, сайт стоит на заглушке (или, без заглушки, работает на полусобранной версии), а откат по-прежнему требует соображать под давлением. Лечится оба — сменой структуры.

от 0 ₽ на старте
Свой сервер по цене чашки кофе в неделю
Прозрачные тарифы без сюрпризов в чеке. Платите за то, что реально используете, и масштабируйтесь, когда вырастете.
Смотреть тарифы

Идея старая как Capistrano и по-прежнему лучшая в классе: новую версию собираем полностью в сторонке и переключаемся на неё мгновенно.

/var/www/site1/
├── releases/
   ├── 20260612-101502/     # прошлые релизы, целиком
   ├── 20260703-183012/
   └── 20260704-094455/     # только что собранный
├── shared/
   ├── .env                 # секреты — вне релизов
   └── storage/             # загрузки пользователей, логи
└── current -> releases/20260704-094455/   # nginx смотрит СЮДА

Nginx в конфиге указывает root на current/public — и никогда не меняется. Деплой теперь выглядит так: клонируем код в новый каталог releases/<время>, симлинками подключаем shared/.env и shared/storage (то, что живёт между релизами: секреты и пользовательские данные), собираем всё — composer, кэши фреймворка, миграции — пока старая версия спокойно обслуживает посетителей, и в финале:

ln -sfn /var/www/site1/releases/20260704-094455 /var/www/site1/current
sudo systemctl reload php8.3-fpm

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

ln -sfn /var/www/site1/releases/20260703-183012 /var/www/site1/current
sudo systemctl reload php8.3-fpm

Три секунды, ноль размышлений, работает в три ночи и в состоянии паники — прошлый релиз лежит на диске целиком собранный, его не надо восстанавливать, в него надо просто вернуться. Оформите оба действия скриптами (deploy.sh / rollback.sh), старые релизы чистите, оставляя последние пять (ls -1 releases | head -n -5 | xargs rm -rf в конце деплоя — привет, пятая статья).

Одна честная звёздочка — миграции. Код откатывается симлинком, база — нет: обратные миграции на проде опасны (down-методы редко тестируют, а DROP COLUMN уносит данные). Взрослое правило: миграции пишутся обратно-совместимыми — новая колонка появляется на релиз раньше, чем код перестаёт работать без неё; старая удаляется на релиз позже, чем код перестал её трогать. Тогда откат кода на шаг назад безопасен всегда. И железное правило прямо из вашей статьи про бэкапы: дамп базы — перед каждым деплоем с миграциями, в скрипт, автоматически.

Уровень 4: убираем человека — хуки и простой CI/CD

Осталось перестать заходить на сервер вообще. Слово «CI/CD» звучит как отдельная профессия, но для одного VPS сводится к вопросу: «кто и когда запускает deploy.sh?» Три ответа по возрастанию цивилизованности:

Запуск за 2 минуты
Идея есть — пусть будет где её запустить
VPN, Telegram-бот, своё облако или пет-проект — арендуйте сервер за пару минут и проверьте идею в деле уже сегодня.
Создать сервер

Вариант А: git push прямо на сервер. На VPS заводится bare-репозиторий с хуком post-receive, который запускает деплой; у вас локально — второй remote (git remote add prod ...), и выкатка — это git push prod main. Красиво, минималистично, никаких третьих сторон. Минус: сборка (composer, а тем более npm build) происходит на боевом VPS и ест его память и CPU.

Вариант Б: webhook. Код живёт на GitHub/GitLab, на сервере маленький слушатель (готовый webhook из репозиториев Ubuntu), GitHub дёргает URL при пуше в main — слушатель запускает deploy.sh. Плюс: рабочий процесс не меняется, деплоит сам факт пуша.

Вариант В: GitHub Actions / GitLab CI — и вот это уже настоящий CI/CD. Пайплайн на стороне Git-платформы прогоняет тесты, собирает фронтенд у себя (npm build на VPS с гигабайтом памяти — известный способ устроить OOM в самый неподходящий момент; пусть этим давится бесплатный раннер), и лишь потом по SSH запускает деплой на сервере. Прод получает готовый артефакт и не тратит ни мегабайта на сборку. Для команды больше одного человека — единственный по-настоящему правильный вариант, и это по-прежнему один YAML-файл на полстраницы, а не «отдельная профессия».

И финальный штрих дисциплины, мостик к нашим статьям про Git: выкатывайте теги, а не «что было в main». git tag v1.4.2 && git push --tags — и деплой-скрипт собирает именно тег. Релизы получают имена, changelog пишется сам, а фраза «откатись на v1.4.1» однозначна для всех участников — включая вас самих через полгода.

Грабли деплоя: сводная таблица

Грабля Симптом Лечение
Открытый .git в вебруте Исходники сайта утекают сканерам root в public/ или deny all на /\.git
.env в репозитории Пароли от базы в истории Git навсегда Секреты — в shared/, в .gitignore; утекли — менять
Деплой от root Чехарда владельцев, «Permission denied» через раз Всё от пользователя сайта + точечные sudoers
Забытый сброс кэшей «Я же исправил!» / старые страницы reload php-fpm + purge FastCGI — шаги скрипта, не памяти
npm build на слабом VPS OOM ровно в момент выкатки Собирать в CI, на сервер — артефакт
Правки прямо на проде git pull конфликтует, изменения теряются git status перед деплоем; правка на проде = коммит немедленно
Обратные миграции при откате Потерянные колонки и данные Обратно-совместимые миграции + дамп перед деплоем
Деплой в час пик «на минутку» Минутка, как обычно, не минутка Симлинк-схема — и деплой в любое время безопасен

Что в итоге

Проверим по заголовку. Без FTP — да: транспортом стал Git, и «залил не всё» вымерло как класс. Одной командой — да: ./deploy.sh (или просто git push prod main), внутри которой зависимости, миграции, сброс всех кэшей серии и смок-тест. Откат без слёз — да: предыдущий релиз лежит собранным, возврат — это перекинуть симлинк за три секунды. И бонусом — дисциплина, которая приходит бесплатно: история выкаток, именованные теги, видимость любых «правок по-быстрому». Вечер настройки — против многих лет перетаскивания файлов с замиранием сердца.

Серия подходит к финалу, и остался последний большой страх владельца VPS — переезд. Новый провайдер дешевле, старый тариф тесен, или (вспомните шестую статью) вы выбрали «путь Б» вместо release-upgrade — а сайт должен переехать так, чтобы посетители ничего не заметили. В заключительной статье соберём весь чек-лист миграции без даунтайма: снижение TTL заранее, rsync с репетицией, генеральный прогон переключения DNS — и красиво закольцуем серию с того, с чего всё начиналось: с бэкапов, которые реально спасают.

от 0 ₽ на старте
Свой сервер по цене чашки кофе в неделю
Прозрачные тарифы без сюрпризов в чеке. Платите за то, что реально используете, и масштабируйтесь, когда вырастете.
Смотреть тарифы
Хостинг Siteko

VDS с первого дня

Свой сервер Siteko под полным контролем

NVMe-диски, root-доступ и честные ресурсы без «соседей». Поднимите VPN, бота или прод-проект и масштабируйтесь без тесных рамок.

  • Root-доступ полная свобода настройки под ваш стек.
  • NVMe и свежее железо скорость заметна с первого запроса.
  • Перенос бесплатно поможем переехать без простоя.
Выбрать VDS