MHOSTMHOST

Деплой из 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 под похожими названиями.
Совет: команды ниже даны с sudo. Если вы уже подключены как root и sudo в системе нет — выполняйте те же команды без префикса sudo.
Совет: если базовое окружение (например, LEMP или Django) вы разворачивали через готовый рецепт VMmanager 6 (см. статью «Готовые рецепты VMmanager 6»), эта статья — про следующий шаг: как заводить и обновлять в этом окружении именно ваш код из Git.

Шаг 1. Установите Git и подготовьте доступ к репозиторию

  1. Установите Git, если его ещё нет:
bash
sudo apt install -y git      # Ubuntu/Debian
sudo dnf install -y git      # AlmaLinux/Rocky
bash
git --version
  1. Если репозиторий публичный, ключ не нужен — доступ по HTTPS работает сразу, переходите к шагу 2.
  1. Если репозиторий приватный, создайте на сервере отдельный SSH-ключ только для доступа к нему (deploy key) — так сервер получит доступ на чтение только к этому репозиторию, а не ко всем вашим репозиториям сразу:
bash
ssh-keygen -t ed25519 -C "deploy@myapp" -f ~/.ssh/id_ed25519_deploy -N ""
cat ~/.ssh/id_ed25519_deploy.pub
  1. Скопируйте вывод последней команды и добавьте его в репозиторий на GitHub: Settings → Deploy keys → Add deploy key. Вставьте ключ в поле Key и оставьте галочку Allow write access выключенной — доступа на чтение достаточно для деплоя.
Совет: на GitLab аналогичная настройка называется Deploy Keys и находится в Settings → Repository → Deploy keys проекта.
  1. Поскольку ключ сохранён не под стандартным именем, укажите Git, для какого хоста его использовать, — добавьте в ~/.ssh/config:
bash
nano ~/.ssh/config
Host github.com-myapp
    HostName github.com
    User git
    IdentityFile ~/.ssh/id_ed25519_deploy
    IdentitiesOnly yes
Совет: если на сервере это единственный SSH-ключ и он нужен только для одного репозитория, можно сохранить его сразу под стандартным именем ~/.ssh/id_ed25519 — тогда правка ~/.ssh/config не понадобится.
  1. Проверьте соединение:
bash
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 или домашний каталог отдельного пользователя, если разворачивали приложение по другой статье цикла).

  1. Создайте каталог приложения:
bash
sudo mkdir -p /var/www/myapp
sudo chown "$USER":"$USER" /var/www/myapp
  1. Склонируйте репозиторий — по SSH (алиас хоста из шага 1, для приватного репозитория):
bash
git clone git@github.com-myapp:owner/repo.git /var/www/myapp

или по HTTPS (для публичного репозитория):

bash
git clone https://github.com/owner/repo.git /var/www/myapp
  1. Проверьте:
bash
cd /var/www/myapp
git remote -v
git log -1 --oneline
Важно: в этой статье веткой по умолчанию считается main — замените на master во всех командах ниже, если в вашем репозитории используется старое имя ветки.

Шаг 3. Запустите приложение под systemd или PM2

Скрипту деплоя из шага 4 нужно что-то перезапускать — то есть приложение уже должно быть поднято как управляемый процесс: через systemd (подходит для любого стека) или через PM2 (обычно для Node.js). Если вы уже прошли отдельную статью по своему стеку — этот шаг, скорее всего, уже сделан, переходите к шагу 4.

Вариант А: systemd-сервис

Создайте юнит /etc/systemd/system/myapp.service:

bash
sudo nano /etc/systemd/system/myapp.service
ini
[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.

Включите и запустите сервис:

bash
sudo systemctl daemon-reload
sudo systemctl enable --now myapp
sudo systemctl status myapp

Логи — через journalctl:

bash
sudo journalctl -u myapp -f

Вариант Б: PM2 (для Node.js)

bash
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».

Важно: PM2 хранит список процессов отдельно для каждого пользователя системы. Команда pm2 restart myapp из скрипта деплоя (шаг 4) сработает только от имени того же пользователя, под которым вы выполнили pm2 start/pm2 save, — учтите это при настройке автоматизации в шаге 5: деплой должен подключаться именно под этим пользователем.

Шаг 4. Напишите скрипт деплоя: git pull + restart

Создайте deploy.sh в каталоге приложения:

bash
nano /var/www/myapp/deploy.sh

Для варианта А (systemd):

bash
#!/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):

bash
#!/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)"

Сделайте скрипт исполняемым и проверьте вручную:

bash
chmod +x /var/www/myapp/deploy.sh
/var/www/myapp/deploy.sh

Флаг --ff-only останавливает деплой, если ветку нельзя быстро перемотать вперёд (fast-forward), — например, если на сервере остались локальные правки или история разошлась. Это осознанное ограничение: лучше явно увидеть ошибку в выводе скрипта, чем получить на проде неожиданный merge-коммит.

Важно: если деплой будет запускаться автоматически не под root (вебхуком или из GitHub Actions — шаг 5), команда sudo systemctl restart внутри скрипта потребует пароль и зависнет без интерактивного терминала. Разрешите этому пользователю выполнять именно эту команду без пароля:
bash
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 именно для этой задачи.

  1. Скачайте бинарник (готового пакета webhook нет одинаково во всех дистрибутивах, поэтому надёжнее поставить так — одинаково для Ubuntu/Debian и AlmaLinux/Rocky):
bash
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
Проверьте: версия 2.8.3 актуальна на момент написания статьи — свежий номер и имя архива для вашей архитектуры уточните на странице релизов adnanh/webhook.
  1. Опишите хук в /etc/webhook/hooks.json:
bash
sudo mkdir -p /etc/webhook
sudo nano /etc/webhook/hooks.json
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.

  1. Запустите webhook как systemd-сервис на localhost — наружу его отдаст reverse proxy:
bash
sudo nano /etc/systemd/system/webhook.service
ini
[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.target
bash
sudo systemctl daemon-reload
sudo systemctl enable --now webhook
sudo systemctl status webhook
  1. Опубликуйте /hooks/deploy наружу через reverse proxy с HTTPS — например, добавьте в Caddyfile (см. статью «Caddy: автоматический HTTPS»):
caddyfile
example.com {
    reverse_proxy /hooks/* 127.0.0.1:9000
    reverse_proxy 127.0.0.1:3000
}
Важно: порт 9000 наружу открывать не нужно — он слушает только 127.0.0.1, снаружи к нему обращается только reverse proxy на этом же сервере. А вот порты 80 и 443 должны быть открыты в файрволе — подробнее в статье «Настройка файрвола UFW».
  1. Добавьте вебхук в репозиторий на GitHub: Settings → Webhooks → Add webhook. Заполните: Payload URLhttps://example.com/hooks/deploy, Content typeapplication/json, Secret — тот же секрет, что в hooks.json, в списке событий выберите Just the push event. Сохраните.

GitHub сразу отправит тестовое событие ping — на вкладке Recent Deliveries этого вебхука появится его результат (должен быть ответ 200).

Совет: на GitLab аналогичная настройка — в Settings → Webhooks проекта; секрет передаётся напрямую в заголовке X-Gitlab-Token (без HMAC), поэтому правило в hooks.json в этом случае строится на типе value, а не payload-hmac-sha256.

Вариант Б: GitHub Actions — деплой по SSH

Здесь всё наоборот: не сервер слушает входящий запрос, а GitHub сам подключается к нему по SSH и выполняет deploy.sh.

  1. Сгенерируйте отдельную пару SSH-ключей для CI (не переиспользуйте ключ из шага 1 — тот открывает доступ к репозиторию, а этот должен открывать доступ на сервер):
bash
ssh-keygen -t ed25519 -C "github-actions-deploy" -f ./gh-deploy-key -N ""
  1. Добавьте публичный ключ в authorized_keys пользователя на сервере, под которым будет заходить CI:
bash
ssh-copy-id -i ./gh-deploy-key.pub deploy@your-server-ip
Совет: если ssh-copy-id недоступен (например, ключ создавался не на Linux/macOS), добавьте содержимое gh-deploy-key.pub в ~/.ssh/authorized_keys этого пользователя на сервере вручную.
  1. Добавьте секреты репозитория: 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...-----.
  1. Создайте в репозитории файл .github/workflows/deploy.yml:
yaml
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.

Важно: у GitHub Actions нет постоянного IP-адреса — раннеры используют динамические адреса из большого диапазона. Если SSH на сервере ограничен по конкретным IP (файрвол — см. статью «Настройка файрвола UFW»), для этого ключа такое ограничение работать не будет: либо разрешайте порт SSH шире, либо используйте self-hosted runner с известным IP.

Шаг 6. Проверьте, что деплой работает

  1. Внесите небольшое изменение в проект локально, закоммитьте и запушьте в main.
  2. В зависимости от выбранного варианта автоматизации: для вебхука — посмотрите sudo journalctl -u webhook -f на сервере и вкладку Recent Deliveries вебхука на GitHub, должен появиться новый успешный (200) вызов; для GitHub Actions — откройте вкладку Actions репозитория и убедитесь, что job deploy завершился успешно.
  3. На сервере проверьте, что код обновился и процесс перезапустился:
bash
cd /var/www/myapp && git log -1 --oneline
sudo systemctl status myapp      # for option A
pm2 status                       # for option B
Готово: если git log показывает ваш последний коммит, а сервис или процесс — недавний перезапуск, деплой из Git настроен и работает.

Что дальше

  • Полный разбор установки Node.js, PM2 и Nginx — в статье «Node.js приложение с PM2 и Nginx».
  • Если вместо systemd/PM2 удобнее упаковать приложение в контейнер — см. «Docker и Docker Compose».
  • HTTPS для сайта и для самого вебхука — «Caddy: автоматический HTTPS».
  • Открывайте в файрволе только те порты, которые действительно нужны снаружи, — «Настройка файрвола UFW».
  • Если добавляли SSH-ключ для GitHub Actions — заведите под него отдельного пользователя без sudo и с ограниченными правами, а не используйте root.