Деплой из Git
В этой статье — как обновлять приложение на VPS mHost прямо из Git: один раз разворачиваете репозиторий, а дальше каждое обновление — это git pull и перезапуск процесса через systemd или PM2. По желанию весь процесс можно автоматизировать полностью: пуш в репозиторий сам вызовет деплой — через вебхук на сервере или через GitHub Actions по SSH.
Что понадобится
- VPS mHost на Linux: Ubuntu 22.04/24.04, Debian 11/12 или AlmaLinux/Rocky Linux 8/9.
- Доступ по SSH — под
rootили пользователем сsudo. - Приложение, которое уже умеет запускаться на этом сервере, — сборка и первый запуск зависят от стека и в этой статье не разбираются; см., например, «Node.js приложение с PM2 и Nginx», «Python и FastAPI: продакшн-деплой» или «Django на VPS». Здесь фокус — на самом обновлении кода из Git и его автоматизации.
- Репозиторий с кодом на GitHub, GitLab или другом Git-хостинге. Примеры ниже — на GitHub; у GitLab и большинства других хостингов есть свои аналоги deploy key, вебхуков и секретов CI под похожими названиями.
Шаг 1. Установите Git и подготовьте доступ к репозиторию
- Установите Git, если его ещё нет:
sudo apt install -y git # Ubuntu/Debian
sudo dnf install -y git # AlmaLinux/Rockygit --version- Если репозиторий публичный, ключ не нужен — доступ по HTTPS работает сразу, переходите к шагу 2.
- Если репозиторий приватный, создайте на сервере отдельный SSH-ключ только для доступа к нему (deploy key) — так сервер получит доступ на чтение только к этому репозиторию, а не ко всем вашим репозиториям сразу:
ssh-keygen -t ed25519 -C "deploy@myapp" -f ~/.ssh/id_ed25519_deploy -N ""
cat ~/.ssh/id_ed25519_deploy.pub- Скопируйте вывод последней команды и добавьте его в репозиторий на GitHub: Settings → Deploy keys → Add deploy key. Вставьте ключ в поле Key и оставьте галочку Allow write access выключенной — доступа на чтение достаточно для деплоя.
- Поскольку ключ сохранён не под стандартным именем, укажите Git, для какого хоста его использовать, — добавьте в
~/.ssh/config:
nano ~/.ssh/configHost github.com-myapp
HostName github.com
User git
IdentityFile ~/.ssh/id_ed25519_deploy
IdentitiesOnly yes- Проверьте соединение:
ssh -T github.com-myappОжидаемый ответ — что-то вроде Hi OWNER/REPO! You've successfully authenticated, but GitHub does not provide shell access. Шелл-доступ GitHub и не должен давать — само сообщение подтверждает, что ключ принят.
Шаг 2. Склонируйте репозиторий на сервер
В примерах ниже используется путь /var/www/myapp — замените на свой (например, /opt/myapp или домашний каталог отдельного пользователя, если разворачивали приложение по другой статье цикла).
- Создайте каталог приложения:
sudo mkdir -p /var/www/myapp
sudo chown "$USER":"$USER" /var/www/myapp- Склонируйте репозиторий — по SSH (алиас хоста из шага 1, для приватного репозитория):
git clone git@github.com-myapp:owner/repo.git /var/www/myappили по HTTPS (для публичного репозитория):
git clone https://github.com/owner/repo.git /var/www/myapp- Проверьте:
cd /var/www/myapp
git remote -v
git log -1 --onelineШаг 3. Запустите приложение под systemd или PM2
Скрипту деплоя из шага 4 нужно что-то перезапускать — то есть приложение уже должно быть поднято как управляемый процесс: через systemd (подходит для любого стека) или через PM2 (обычно для Node.js). Если вы уже прошли отдельную статью по своему стеку — этот шаг, скорее всего, уже сделан, переходите к шагу 4.
Вариант А: systemd-сервис
Создайте юнит /etc/systemd/system/myapp.service:
sudo nano /etc/systemd/system/myapp.service[Unit]
Description=myapp
After=network.target
[Service]
Type=simple
User=deploy
WorkingDirectory=/var/www/myapp
ExecStart=/usr/bin/node /var/www/myapp/app.js
Restart=on-failure
RestartSec=5
Environment=NODE_ENV=production
[Install]
WantedBy=multi-user.targetЗамените ExecStart на реальную команду запуска вашего приложения — например, для Python это может быть /var/www/myapp/venv/bin/gunicorn -b 127.0.0.1:8000 wsgi:app, для скомпилированного бинарника — просто путь к нему; Node.js в примере выше — лишь иллюстрация. User замените на пользователя, от имени которого должно работать приложение (например, того же, под которым клонировали репозиторий в шаге 2); если строку убрать, сервис будет работать от root.
Включите и запустите сервис:
sudo systemctl daemon-reload
sudo systemctl enable --now myapp
sudo systemctl status myappЛоги — через journalctl:
sudo journalctl -u myapp -fВариант Б: PM2 (для Node.js)
sudo npm install -g pm2
cd /var/www/myapp
pm2 start app.js --name myapp
pm2 save
pm2 startupКоманда pm2 startup выведет ещё одну команду для копирования и выполнения — она подключает PM2 к systemd, чтобы процессы поднимались и после перезагрузки сервера. Подробный разбор установки Node.js, PM2 и автозапуска — в статье «Node.js приложение с PM2 и Nginx».
Шаг 4. Напишите скрипт деплоя: git pull + restart
Создайте deploy.sh в каталоге приложения:
nano /var/www/myapp/deploy.shДля варианта А (systemd):
#!/usr/bin/env bash
set -euo pipefail
APP_DIR="/var/www/myapp"
SERVICE_NAME="myapp"
BRANCH="main"
cd "$APP_DIR"
git pull --ff-only origin "$BRANCH"
# add a build/install step here if your stack needs one, e.g.:
# npm ci --omit=dev
# pip install -r requirements.txt
sudo systemctl restart "$SERVICE_NAME"
echo "Deployed $(git rev-parse --short HEAD) at $(date)"Для варианта Б (PM2):
#!/usr/bin/env bash
set -euo pipefail
APP_DIR="/var/www/myapp"
BRANCH="main"
cd "$APP_DIR"
git pull --ff-only origin "$BRANCH"
# add a build/install step here if needed, e.g.: npm ci --omit=dev
pm2 restart myapp
echo "Deployed $(git rev-parse --short HEAD) at $(date)"Сделайте скрипт исполняемым и проверьте вручную:
chmod +x /var/www/myapp/deploy.sh
/var/www/myapp/deploy.shФлаг --ff-only останавливает деплой, если ветку нельзя быстро перемотать вперёд (fast-forward), — например, если на сервере остались локальные правки или история разошлась. Это осознанное ограничение: лучше явно увидеть ошибку в выводе скрипта, чем получить на проде неожиданный merge-коммит.
sudo visudoДобавьте строку (замените deploy на реального пользователя, а путь — на вывод command -v systemctl, если он отличается):
deploy ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart myappШаг 5. Автоматизация (опционально): вебхук или GitHub Actions по SSH
Оба варианта решают одну задачу — запускать deploy.sh при пуше в репозиторий, — но по-разному: вебхук слушает входящий HTTP-запрос на самом сервере, а GitHub Actions сам подключается к серверу по SSH снаружи. Достаточно настроить один из них.
Вариант А: вебхук на сервере
GitHub (или GitLab) при пуше шлёт HTTP POST на ваш сервер; лёгкий приёмник вебхуков проверяет подпись запроса и запускает deploy.sh. Ниже — на примере `webhook` (adnanh/webhook), небольшого демона на Go именно для этой задачи.
- Скачайте бинарник (готового пакета
webhookнет одинаково во всех дистрибутивах, поэтому надёжнее поставить так — одинаково для Ubuntu/Debian и AlmaLinux/Rocky):
curl -fsSL -o /tmp/webhook.tar.gz https://github.com/adnanh/webhook/releases/download/2.8.3/webhook-linux-amd64.tar.gz
tar -xzf /tmp/webhook.tar.gz -C /tmp
sudo install -m 0755 /tmp/webhook-linux-amd64/webhook /usr/local/bin/webhook
webhook -version- Опишите хук в
/etc/webhook/hooks.json:
sudo mkdir -p /etc/webhook
sudo nano /etc/webhook/hooks.json[
{
"id": "deploy",
"execute-command": "/var/www/myapp/deploy.sh",
"command-working-directory": "/var/www/myapp",
"trigger-rule": {
"and": [
{
"match": {
"type": "payload-hmac-sha256",
"secret": "CHANGE_ME_SECRET",
"parameter": {
"source": "header",
"name": "X-Hub-Signature-256"
}
}
},
{
"match": {
"type": "value",
"value": "refs/heads/main",
"parameter": {
"source": "payload",
"name": "ref"
}
}
}
]
}
}
]Замените CHANGE_ME_SECRET на длинную случайную строку — тот же секрет вы укажете в настройках вебхука на GitHub (шаг 5 ниже). Первое правило проверяет подпись запроса из заголовка X-Hub-Signature-256, второе — пропускает только пуши в ветку main; без них вызвать deploy.sh сможет кто угодно, кто узнает URL.
- Запустите
webhookкак systemd-сервис на localhost — наружу его отдаст reverse proxy:
sudo nano /etc/systemd/system/webhook.service[Unit]
Description=webhook
After=network.target
[Service]
Type=simple
ExecStart=/usr/local/bin/webhook -hooks /etc/webhook/hooks.json -ip 127.0.0.1 -port 9000 -verbose
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.targetsudo systemctl daemon-reload
sudo systemctl enable --now webhook
sudo systemctl status webhook- Опубликуйте
/hooks/deployнаружу через reverse proxy с HTTPS — например, добавьте в Caddyfile (см. статью «Caddy: автоматический HTTPS»):
example.com {
reverse_proxy /hooks/* 127.0.0.1:9000
reverse_proxy 127.0.0.1:3000
}- Добавьте вебхук в репозиторий на GitHub: Settings → Webhooks → Add webhook. Заполните: Payload URL —
https://example.com/hooks/deploy, Content type —application/json, Secret — тот же секрет, что вhooks.json, в списке событий выберите Just the push event. Сохраните.
GitHub сразу отправит тестовое событие ping — на вкладке Recent Deliveries этого вебхука появится его результат (должен быть ответ 200).
Вариант Б: GitHub Actions — деплой по SSH
Здесь всё наоборот: не сервер слушает входящий запрос, а GitHub сам подключается к нему по SSH и выполняет deploy.sh.
- Сгенерируйте отдельную пару SSH-ключей для CI (не переиспользуйте ключ из шага 1 — тот открывает доступ к репозиторию, а этот должен открывать доступ на сервер):
ssh-keygen -t ed25519 -C "github-actions-deploy" -f ./gh-deploy-key -N ""- Добавьте публичный ключ в
authorized_keysпользователя на сервере, под которым будет заходить CI:
ssh-copy-id -i ./gh-deploy-key.pub deploy@your-server-ip- Добавьте секреты репозитория: Settings → Secrets and variables → Actions → New repository secret. Понадобятся:
SSH_HOST— IP-адрес или домен сервера;SSH_USER— пользователь, для которого добавили ключ (например,deploy);SSH_PORT— порт SSH (обычно22);SSH_PRIVATE_KEY— содержимое приватного ключаgh-deploy-keyцеликом, включая строки-----BEGIN...-----и-----END...-----.
- Создайте в репозитории файл
.github/workflows/deploy.yml:
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy over SSH
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
port: ${{ secrets.SSH_PORT }}
script: /var/www/myapp/deploy.shЗакоммитьте и запушьте этот файл — начиная со следующего пуша в main GitHub Actions сам подключится к серверу и выполнит deploy.sh.
Шаг 6. Проверьте, что деплой работает
- Внесите небольшое изменение в проект локально, закоммитьте и запушьте в
main. - В зависимости от выбранного варианта автоматизации: для вебхука — посмотрите
sudo journalctl -u webhook -fна сервере и вкладку Recent Deliveries вебхука на GitHub, должен появиться новый успешный (200) вызов; для GitHub Actions — откройте вкладку Actions репозитория и убедитесь, что jobdeployзавершился успешно. - На сервере проверьте, что код обновился и процесс перезапустился:
cd /var/www/myapp && git log -1 --oneline
sudo systemctl status myapp # for option A
pm2 status # for option BЧто дальше
- Полный разбор установки Node.js, PM2 и Nginx — в статье «Node.js приложение с PM2 и Nginx».
- Если вместо systemd/PM2 удобнее упаковать приложение в контейнер — см. «Docker и Docker Compose».
- HTTPS для сайта и для самого вебхука — «Caddy: автоматический HTTPS».
- Открывайте в файрволе только те порты, которые действительно нужны снаружи, — «Настройка файрвола UFW».
- Если добавляли SSH-ключ для GitHub Actions — заведите под него отдельного пользователя без
sudoи с ограниченными правами, а не используйте root.