Долгое время я деплоил руками. 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).

Время на «а давайте задеплоим» стало равно нулю. Это меняет ритм работы — можно делать маленькие изменения чаще вместо того чтобы накапливать большой релиз.

Эта схема не для всего. Большие команды, сложные инфраструктуры, требования к аудиту — там нужно что-то серьёзнее. Но для проекта с одним-двумя разработчиками — работает отлично.