Автодеплой на VPS через GitHub Actions и SSH
Практика3 минРедакция Innovialabs
Коротко
Создайте отдельного пользователя и SSH-ключ для деплоя, положите приватный ключ в секреты GitHub, а workflow по push в main подключится к серверу, обновит код, соберёт проект и перезапустит сервис; окружение production защитите правилами.
Как это работает
Схема простая: при push в main GitHub Actions запускает job, который по SSH подключается к вашему VPS и выполняет там скрипт деплоя — обновить код, поставить зависимости, собрать и перезапустить приложение. Вам нужны три вещи: ключи доступа, секреты в репозитории и сам workflow.
Шаг 1. Пользователь и ключи на сервере
Не деплойте от root. Создайте отдельного пользователя, например deploy, с доступом только к каталогу приложения.
Ключей понадобится два, и их легко перепутать:
| Ключ | Откуда куда | Где хранится |
|---|---|---|
| Ключ для CI | GitHub Actions → ваш сервер | Приватный — в секретах GitHub, публичный — в ~/.ssh/authorized_keys пользователя deploy |
| Deploy key | Ваш сервер → GitHub | Приватный — на сервере, публичный — в Settings → Deploy keys репозитория (только чтение) |
Deploy key нужен, если репозиторий приватный и сервер сам делает git pull. Создать ключ можно так:
ssh-keygen -t ed25519 -C "github-actions-deploy" -f ./deploy_ci -N ""
Используйте для каждой задачи свой ключ: так его легко отозвать, не затрагивая остальной доступ.
Шаг 2. Секреты репозитория
В Settings → Secrets and variables → Actions добавьте:
SSH_PRIVATE_KEY— содержимое приватного ключа для CI;SSH_HOSTиSSH_USER— адрес сервера и имя пользователя;SSH_KNOWN_HOSTS— выводssh-keyscan your-server.com, лучше сверенный с отпечатком ключа, который вы видите на самом сервере.
Последний пункт часто пропускают и отключают проверку хоста. Так делать не стоит: без неё вы не заметите подмену сервера.
Шаг 3. Скрипт деплоя на сервере
Держите логику деплоя в скрипте на сервере, например /srv/app/deploy.sh:
#!/usr/bin/env bash
set -euo pipefail
cd /srv/app
git pull --ff-only origin main
npm ci
npm run build
sudo systemctl restart app
set -euo pipefail остановит скрипт на первой ошибке, чтобы не перезапускать сервис с недособранным кодом. Чтобы deploy мог перезапускать сервис без пароля, разрешите ему только эту команду через visudo:
deploy ALL=(root) NOPASSWD: /usr/bin/systemctl restart app
Путь к systemctl проверьте командой which systemctl.
Шаг 4. Workflow деплоя
Файл .github/workflows/deploy.yml:
name: Deploy
on:
push:
branches: [main]
workflow_dispatch:
concurrency:
group: deploy-production
cancel-in-progress: false
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps:
- name: Configure SSH
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
echo "${{ secrets.SSH_KNOWN_HOSTS }}" > ~/.ssh/known_hosts
- name: Run deploy script
run: ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} "/srv/app/deploy.sh"
- concurrency не даёт двум деплоям идти одновременно;
cancel-in-progress: falseпозволяет текущему деплою завершиться. - workflow_dispatch добавляет ручной запуск — пригодится для повторного деплоя.
- Если у вас есть CI-проверки, вынесите их в отдельный job и добавьте
needs, чтобы деплой шёл только после зелёных тестов.
Шаг 5. Защита окружения production
Строка environment: production связывает job с окружением. В Settings → Environments для него можно настроить:
- Required reviewers — деплой ждёт ручного подтверждения ответственного;
- Wait timer — задержка перед стартом;
- Deployment branches — деплоить можно только из
main; - Environment secrets — храните SSH-ключи на уровне окружения, тогда их получит только job с этим окружением.
Доступность части правил для приватных репозиториев зависит от тарифа GitHub.
Частые ошибки
- Деплой от
rootили с ключом, у которого есть доступ ко всему серверу. StrictHostKeyChecking=noвместо known_hosts.- Нет
set -e: сборка упала, а сервис всё равно перезапустился. - Локальные правки прямо на сервере, из-за которых
git pull --ff-onlyотказывается работать.