Долгое время я деплоил руками. SSH на сервер, git pull, npm run build, pm2 restart. Это работало. Это занимало пять минут. Это было предсказуемо.
Потом проектов стало больше. И каждый деплой — это пять минут концентрации, возможность ошибиться, и невозможность делать что-то другое пока идёт build. В какой-то момент я посчитал: трачу около часа в неделю на ручные деплои. Это мотивировало разобраться.
Что я хотел получить
Простую систему без overhead. Не Jenkins с его XML-конфигами. Не сложные Kubernetes-пайплайны. Просто: запушил в main — оно само задеплоилось.
Требования:
- Работает для Node.js / Python проектов
- Запускает тесты перед деплоем
- Откатывается если что-то пошло не так
- Не требует платного SaaS
GitHub Actions + простой деплой-скрипт
Для большинства проектов я сейчас использую GitHub Actions на стороне CI и простой bash-скрипт на стороне сервера.
Структура:
.github/
workflows/
deploy.yml
scripts/
deploy.sh
deploy.yml — триггер и запуск тестов:
name: Deploy
on:
push:
branches: [main]
jobs:
test-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Deploy
if: success()
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: /var/www/myapp/scripts/deploy.sh
deploy.sh на сервере:
#!/bin/bash
set -e
APP_DIR="/var/www/myapp"
cd $APP_DIR
echo "→ Pulling latest changes..."
git pull origin main
echo "→ Installing dependencies..."
npm ci --production
echo "→ Building..."
npm run build
echo "→ Restarting..."
pm2 restart myapp
echo "✓ Done"
set -e — скрипт остановится при любой ошибке. Это важно: без него build может упасть а pm2 restart всё равно выполнится со старым кодом.
Секреты и переменные
SSH-ключ и адрес сервера хранятся в Secrets репозитория (Settings → Secrets). Они шифруются GitHub'ом и не видны в логах.
Для переменных окружения приложения я использую отдельный .env на сервере который не трогается при деплое. Скрипт деплоя не перезаписывает его.
Откат если что-то сломалось
Простейший вариант — хранить предыдущий билд:
# В deploy.sh
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
cp -r $APP_DIR/current $APP_DIR/releases/$TIMESTAMP
# Если что-то пошло не так:
# ln -sfn $APP_DIR/releases/20260305_143022 $APP_DIR/current
Более правильный подход — symlink-стратегия как в Capistrano: новый релиз собирается в отдельной директории, и только если всё прошло успешно — переключается symlink. Откат это просто переключение symlink обратно.
Для большинства небольших проектов хватает более простого подхода: git позволяет откатиться командой git checkout <commit> и перезапустить.
Что это даёт на практике
Деплой теперь происходит сам. Я пишу код, делаю PR, мерджу — через несколько минут изменения на сервере. Тесты не прошли — деплой не случился, уведомление пришло в Telegram (настроил через Actions).
Время на «а давайте задеплоим» стало равно нулю. Это меняет ритм работы — можно делать маленькие изменения чаще вместо того чтобы накапливать большой релиз.
Эта схема не для всего. Большие команды, сложные инфраструктуры, требования к аудиту — там нужно что-то серьёзнее. Но для проекта с одним-двумя разработчиками — работает отлично.