Автодеплой на VPS через GitHub Actions
«Зайти по SSH, стянуть код, перезапустить контейнер» — нормальный процесс, пока не начинает повторяться по три раза в день. Автоматизируем его через git push.
Как устроен пайплайн
Магии нет: на пуш в ветку main GitHub Actions подключается к VPS по SSH и выполняет там те же команды, что и вы руками — git pull, пересборку контейнера, проверку статуса. Разница только в поводе запуска — событие в репозитории вместо вашего желания в конце дня. Сервер уже должен быть настроен, а приложение запущено через Docker Compose (см. гайд по Docker и базовый деплой на VPS) — автодеплой автоматизирует только шаг обновления.
Отдельный SSH-ключ для CI
Личный ключ разработчика для CI — плохая идея: он даёт доступ ко всему, что доступно человеку. Генерируем отдельную пару только для деплоя (-N "" — без парольной фразы, CI не введёт её интерактивно):
Публичную часть добавляем на сервере в authorized_keys пользователя для деплоя:
Приватный ключ никуда не выкладывается — он уходит только в секреты репозитория.
Секреты в GitHub
Settings → Secrets and variables → Actions → New repository secret, четыре значения:
HOST— IP или домен VPS;USERNAME— пользователь для деплоя, не обязательно root (см. безопасность SSH);SSH_PRIVATE_KEY— приватный ключ целиком, включая строкиBEGIN/END;SSH_PORT— если порт нестандартный.
GitHub маскирует значения секретов в логах job, но это не повод писать их текстом в самом workflow-файле — он лежит в репозитории открыто.
${{ secrets.ИМЯ }}. Если ключ уже попал в историю коммитов, считайте его скомпрометированным и перевыпускайте.Workflow-файл
Создаём .github/workflows/deploy.yml: триггер и команды для VPS по SSH. Код на runner GitHub тянуть не нужно — сборка идёт на сервере.
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy via SSH
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
port: ${{ secrets.SSH_PORT }}
script: |
cd /var/www/myapp
git pull origin main
docker compose up -d --build
appleboy/ssh-action избавляет от ручного вызова ssh-agent внутри job; альтернатива — «сырой» ssh -i, но это лишний шаг ради того же результата.
Что происходит на сервере при деплое
Блок script — тот же ручной процесс: git pull подтягивает код, docker compose up -d --build (см. Docker Compose) пересобирает образ и перезапускает контейнер. Стоит добавить и docker compose ps — статус попадёт в лог job, и если контейнер не поднялся, это видно сразу в Actions, а не при следующем заходе на сервер.
Откат при ошибке
Автодеплой ускоряет доставку не только удачных изменений, но и багов — откатываться нужно так же быстро:
git checkout <commit-hash>
docker compose up -d --build
Надёжнее — перед пересборкой тегировать рабочий образ (docker tag myapp:latest myapp:previous), тогда откат — это запуск готового myapp:previous без ожидания новой сборки.
Проверка после деплоя
Контейнер может подняться (статус Up) и при этом отдавать 500-ю ошибку — процесс жив, а приложение нет. Добавьте в конец workflow проверку health-эндпоинта:
run: |
sleep 5
curl -f https://your-domain.ru/health || exit 1
-f у curl превращает HTTP-ошибку в код возврата, который завалит job — о падении узнаёте от GitHub, а не от пользователя в поддержке. Эту же проверку стоит вынести и в постоянный мониторинг.
Типовые ошибки
- Неверные права на
authorized_keysили.ssh— SSH молча отклоняет ключ; нужноchmod 600 ~/.ssh/authorized_keysиchmod 700 ~/.ssh. - Пользователь для деплоя не в группе
docker—docker composeпадает с ошибкой доступа к сокету при живом SSH-подключении. - Кеш слоёв Docker подсовывает старый код после
--build— помогаетdocker compose build --no-cacheили проверка.dockerignore. - Опечатка в имени секрета (
SSH_PRIVATE_KEYvsSSH_PRIVATEKEY) — GitHub подставит пустую строку, и workflow упадёт без внятной причины.
Что дальше
Деплой теперь запускается сам, но узнавать о падении сервиса по звонку от клиента всё ещё не хочется: убедимся, что после автодеплоя сервис жив — настроим мониторинг →