Git-деплой на сервер: bare-репозиторий и хуки
Деплой по git push — самый простой способ выкатывать код на один-два сервера без CI-платформ. Пушите в bare-репозиторий на VPS, хук post-receive раскладывает файлы в каталог сайта и перезапускает сервис. Всё нативным git, без сторонних сервисов.
Содержание
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →Как это устроено
На сервере создаётся bare-репозиторий (без рабочего дерева — только история). Локально вы добавляете его как remote и пушите. При получении пуша срабатывает хук post-receive, который делает git checkout рабочего дерева в отдельный каталог и запускает нужные команды — установку зависимостей, миграции, рестарт.
Схема идеальна для VPS: никаких раннеров и агентов, только SSH и git, которые уже есть. На root-доступе MAATRIX всё это настраивается за пять минут и не тянет за собой облачную инфраструктуру — весь процесс живёт на одном сервере и полностью под вашим контролем.
Стоит понимать разницу между bare и обычным репозиторием. Обычный репозиторий содержит и историю (папку .git), и рабочие файлы, которые вы редактируете. Bare-репозиторий (флаг --bare) хранит только историю, без рабочего дерева — принимать пуши в него безопасно, потому что здесь нет открытых файлов, которые пуш мог бы затереть. Именно поэтому bare-репозиторий выступает точкой приёма, а рабочие файлы хук выкладывает отдельно в каталог сайта.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США и РФ. Оплата картой РФ и по СБП.
Арендовать VPS для деплояШаг 1. Пользователь и bare-репозиторий
Заводим отдельного пользователя для деплоя и bare-репозиторий в его домашней директории:
sudo adduser --disabled-password --gecos '' deploy
sudo -u deploy git init --bare /home/deploy/myapp.git
Скопируйте свой SSH-ключ пользователю deploy, чтобы пушить без пароля:
ssh-copy-id -i ~/.ssh/id_ed25519.pub deploy@SERVER_IP
Создаём каталог, куда будет выкладываться сайт:
sudo mkdir -p /var/www/myapp
sudo chown deploy:deploy /var/www/myapp
Шаг 2. Хук post-receive
В bare-репозитории создаём исполняемый хук. Он раскладывает содержимое ветки main в каталог сайта:
cat > /home/deploy/myapp.git/hooks/post-receive <<'EOF'
#!/bin/bash
set -e
TARGET="/var/www/myapp"
BRANCH="main"
while read oldrev newrev ref; do
if [[ "$ref" = "refs/heads/$BRANCH" ]]; then
echo "Deploying $BRANCH -> $TARGET"
git --work-tree="$TARGET" --git-dir=/home/deploy/myapp.git checkout -f "$BRANCH"
cd "$TARGET"
# пример: зависимости и рестарт
# /opt/myapp/venv/bin/pip install -r requirements.txt
sudo systemctl restart myapp
fi
done
EOF
chmod +x /home/deploy/myapp.git/hooks/post-receive
Разберём логику хука. Переменные oldrev, newrev и ref git передаёт в stdin для каждой обновлённой ссылки. Мы реагируем только на ветку main: остальные ветки (feature-, dev-) можно пушить на сервер, не выкатывая их в продакшен. Флаг -f у checkout принудительно приводит рабочее дерево к состоянию ветки, перезаписывая локальные изменения в каталоге сайта — это ожидаемо, каталог сайта не место для ручных правок.
После checkout идёт блок деплой-команд. Здесь уместно поставить зависимости, применить миграции БД, собрать фронтенд и в конце перезапустить сервис. Держите этот блок идемпотентным: он выполняется на каждый пуш, поэтому команды должны корректно отрабатывать и на чистом, и на уже развёрнутом приложении.
Чтобы хук мог рестартовать сервис без пароля, разрешите deploy запускать конкретную команду через sudoers:
echo 'deploy ALL=(root) NOPASSWD: /bin/systemctl restart myapp' | sudo tee /etc/sudoers.d/deploy-myapp
sudo chmod 440 /etc/sudoers.d/deploy-myapp
Шаг 3. Пуш с локальной машины
Добавляем сервер как remote и пушим:
git remote add production deploy@SERVER_IP:/home/deploy/myapp.git
git push production main
Каждый следующий git push production main выкатывает свежий код. Вывод хука (echo Deploying...) вы видите прямо в терминале во время пуша — это ваш живой лог деплоя. Если хук упадёт (set -e прерывает выполнение на первой ошибке), git покажет ненулевой код возврата, и вы сразу поймёте, что выкатка не прошла, ещё не отходя от терминала.
Для команды удобно, что remote можно назвать осмысленно: production, staging, backup. Тогда выкатка на разные окружения — это разные bare-репозитории на сервере и разные remote локально, а команда пуша остаётся такой же простой.
Откат и частые ошибки
Откат — это просто пуш нужного коммита в main:
git push production <commit-hash>:main --force
- hook not executed — забыли chmod +x на post-receive.
- checkout: невозможно — не создан TARGET-каталог или у deploy нет прав на запись в него.
- sudo: a password is required в хуке — неверная строка в sudoers, команда в ней должна совпадать посимвольно с вызовом.
- Пушите не в ту ветку — хук реагирует только на main.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США и РФ. Оплата картой РФ и по СБП.
Арендовать VPS для деплояОбсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →Частые вопросы
Чем это лучше rsync или scp?
Git хранит историю: видно, что и когда выкатили, и откат делается пушем. rsync копирует состояние без версий.
Нужен ли отдельный пользователь deploy?
Не обязательно, но так безопаснее: деплой изолирован от вашего основного аккаунта и имеет только нужные права.
Как деплоить сразу зависимости и миграции?
Впишите команды в тело хука post-receive после checkout — pip install, npm ci, миграции БД, сборку фронтенда.