CI/CD для VPS: автодеплой через GitHub Actions
Настраиваем минимальный пайплайн GitHub Actions: автоматический запуск тестов, сборка зависимостей и деплой на VPS по SSH при каждом пуше в основную ветку.
Что даёт CI/CD по сравнению с ручным git pull
Ручной git pull на сервере, разобранный в отдельной статье, отлично работает для небольших проектов, но по мере роста команды и частоты изменений начинает создавать проблемы: кто-то забывает запустить тесты перед пушем, кто-то деплоит прямо с личного компьютера в обход общего процесса, а история того, что и когда было выложено на продакшн, существует только в памяти участников команды.
CI/CD (Continuous Integration / Continuous Deployment) автоматизирует именно этот процесс: при каждом пуше в репозиторий автоматически запускаются тесты и проверки кода, и только при их успешном прохождении изменения могут быть автоматически (или по одобрению) выложены на сервер. GitHub Actions — встроенный в GitHub инструмент CI/CD, не требующий отдельной инфраструктуры и настраиваемый прямо в репозитории через YAML-файлы.
Три конкретных практических выгоды такого подхода: во-первых, ни один коммит с падающими тестами физически не попадёт на production, если пайплайн настроен блокировать деплой при неудачных тестах; во-вторых, процесс деплоя становится идентичным при каждом запуске — не зависящим от того, кто именно из команды выполняет его в этот раз и не забыл ли он какой-то шаг; в-третьих, GitHub сохраняет полный журнал каждого запуска пайплайна с точными логами каждой команды, что заметно упрощает разбор причин, если что-то пошло не так.
Устройство GitHub Actions: workflow, jobs, steps
Конфигурация GitHub Actions хранится в YAML-файлах внутри директории .github/workflows/ репозитория. Каждый файл описывает один workflow — последовательность автоматических действий, запускаемых по определённому триггеру (пуш в ветку, открытие pull request, расписание или ручной запуск).
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Внутри workflow описываются jobs (задачи) — каждая выполняется в отдельном изолированном виртуальном окружении (в примере — ubuntu-latest, свежий образ Ubuntu, поднимаемый заново при каждом запуске). Внутри job идут steps (шаги) — последовательные команды или готовые переиспользуемые действия из каталога GitHub Actions, отмеченные ключевым словом uses.
actions/checkout@v4 — стандартное первое действие практически любого workflow: оно клонирует репозиторий текущего коммита во временное окружение раннера, чтобы последующие шаги (установка зависимостей, тесты, деплой) имели доступ к актуальному коду. Без этого шага раннер работает с пустой директорией, не зная о содержимом репозитория вообще.
SSH-ключ как секрет: подключение к серверу без пароля
Для деплоя на VPS пайплайну нужен способ подключиться к серверу — использовать пароль для этого не следует ни при каких обстоятельствах, правильный способ — отдельная пара SSH-ключей, созданная специально для деплоя, приватная часть которой сохраняется как зашифрованный секрет GitHub Actions, а публичная — добавляется на сервер в authorized_keys нужного пользователя.
ssh-keygen -t ed25519 -f ./deploy_key -N ""
cat deploy_key.pub >> ~/.ssh/authorized_keys
Приватный ключ (файл deploy_key без расширения) добавляется в настройках репозитория на GitHub в разделе Settings → Secrets and variables → Actions как новый секрет, например DEPLOY_SSH_KEY — после сохранения его содержимое становится доступно только внутри выполнения workflow через специальный синтаксис ${{ secrets.DEPLOY_SSH_KEY }} и никогда не отображается в логах или интерфейсе повторно.
Критически важно использовать для деплоя отдельный ключ, а не личный SSH-ключ администратора, которым он же пользуется для ручного входа на сервер, — при компрометации репозитория или самого GitHub Actions (что случается на практике реже, но не исключено полностью) отдельный ограниченный ключ деплоя проще отозвать без влияния на личный доступ администратора и другие системы, где используется тот же личный ключ.
Полный пример workflow: тесты, сборка, деплой
Ниже — минимальный, но полностью рабочий пример workflow для PHP-проекта: он запускает тесты при каждом пуше в любую ветку, а деплой на сервер выполняет только при пуше именно в main и только если тесты прошли успешно.
name: Test and Deploy
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
- run: composer install --prefer-dist --no-progress
- run: vendor/bin/phpunit
deploy:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy over SSH
env:
SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
run: |
mkdir -p ~/.ssh
echo "$SSH_KEY" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
ssh-keyscan -H example.ru >> ~/.ssh/known_hosts
ssh -i ~/.ssh/deploy_key deploy@example.ru 'bash /var/www/example.ru/deploy.sh'
Ключевая связка — поле needs: test у задачи deploy: GitHub Actions не запустит эту задачу, пока задача test не завершится успешно, и вообще не запустит её, если тесты упали. Это и есть тот самый барьер, который физически не даёт непроверенному коду попасть на production через автоматический пайплайн.
Команда ssh-keyscan заранее добавляет реальный отпечаток ключа сервера в known_hosts раннера — это правильная альтернатива отключению проверки хоста через StrictHostKeyChecking=no, которое иногда встречается в упрощённых примерах в интернете, но по сути отключает защиту от подмены сервера и не должно использоваться в реальном production-пайплайне.
Сама команда деплоя на сервере остаётся тем же скриптом deploy.sh, что был разобран в статье про git-деплой — GitHub Actions в данном случае лишь автоматически инициирует его выполнение по SSH при выполнении всех условий, а не заменяет саму логику обновления кода на сервере.
Сборка фронтенда и зависимостей в пайплайне
Для проектов с фронтенд-сборкой (Node.js, Vite, Webpack) правильное место для npm install и npm run build — сам CI/CD-пайплайн, а не production-сервер: это переносит нагрузку сборки на отдельный раннер GitHub Actions вместо VPS, у которого может не хватать ресурсов на комфортную сборку, особенно параллельно с обслуживанием текущих запросов пользователей.
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm run build
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
Команда npm ci (в отличие от npm install) устанавливает зависимости строго по package-lock.json, ничего не пересчитывая заново, — для CI/CD-окружения это правильный выбор по тем же причинам, что composer install предпочтительнее composer update на production, разобранным в статье про Composer.
Готовый результат сборки затем можно либо передать напрямую на сервер через rsync или scp как часть шага деплоя, либо, если проект достаточно сложен, сохранить как артефакт GitHub Actions (actions/upload-artifact) и передать между разными задачами одного workflow, чтобы не пересобирать фронтенд заново на этапе деплоя.
Staging-окружение и постепенный рост уверенности в пайплайне
Даже хорошо написанные автоматические тесты не покрывают абсолютно все сценарии использования приложения — staging-окружение (отдельный сервер или поддомен, максимально похожий на production по конфигурации) даёт возможность вручную или автоматически проверить изменения в условиях, близких к боевым, прежде чем они попадут к реальным пользователям.
on:
push:
branches:
- develop # деплой на staging
- main # деплой на production
Практичная схема для команды, только начинающей выстраивать CI/CD: пуш в ветку develop автоматически деплоит на staging-сервер, где изменения проверяются вручную (или дополнительным набором end-to-end тестов), и только после этого через pull request изменения попадают в main, откуда уже идёт автоматический деплой на production.
Staging не обязан быть отдельным полноценным VPS с самого начала — для небольших проектов вполне достаточно отдельного поддомена на том же сервере со своей базой данных и .env, что уже даёт возможность отловить значительную часть проблем окружения до того, как они попадут на боевой сайт.
Безопасность автодеплоя: на что обратить внимание
Ограничение прав пользователя, от имени которого выполняется деплой на сервере, — базовая мера предосторожности: отдельный пользователь deploy с доступом только к директории проекта и с точечно разрешёнными через sudoers командами перезапуска нужных сервисов (без пароля только для этих конкретных команд) ограничивает ущерб в случае компрометации самого ключа деплоя по сравнению с полным root-доступом.
deploy ALL=(root) NOPASSWD: /usr/bin/systemctl restart php8.3-fpm
Никогда не стоит отключать проверку хоста SSH (StrictHostKeyChecking=no) ради упрощения настройки пайплайна — эта проверка защищает от атаки, при которой злоумышленник подменяет сервер, на который на самом деле подключается пайплайн, что особенно критично, учитывая, что в этот момент передаётся приватный ключ и выполняются команды с правами деплоя.
Секреты GitHub Actions стоит явно ограничивать по окружениям (GitHub Environments) там, где это доступно: например, секрет с ключом от production-сервера логически привязывается только к workflow, разворачивающимся именно на production, а не доступен любому job в репозитории — это дополнительный барьер, если в пайплайн случайно (или намеренно, при компрометации репозитория) добавится job, пытающийся получить доступ к секретам, для которых он не предназначен.
Каждый успешный автодеплой стоит логировать не только внутри GitHub Actions, но и на самой стороне сервера — например, добавлением строки в лог-файл с хешем задеплоенного коммита и временем — это упрощает разбор инцидентов позже, когда нужно быстро понять, какая именно версия кода работала на сервере в конкретный момент времени в прошлом.
План отката: что делать, если деплой прошёл, а приложение сломалось
Автоматические тесты снижают, но не исключают риск, что после деплоя приложение начнёт вести себя неправильно — конфигурация production может отличаться от staging, реальные данные пользователей могут выявить крайний случай, не покрытый тестами, или проблема может быть вовсе не в коде, а во внешнем сервисе, от которого приложение зависит.
План отката должен существовать до того, как он понадобится, а не придумываться в момент инцидента. Самый простой вариант для схемы деплоя через git — вручную (или отдельным workflow с ручным запуском workflow_dispatch) откатить сервер к хешу предыдущего успешно работавшего коммита.
ssh deploy@example.ru
cd /var/www/example.ru
git log --oneline -5
git checkout <хеш_предыдущего_коммита>
composer install --no-dev --optimize-autoloader
systemctl restart php8.3-fpm
Более надёжная архитектура деплоя (для проектов, где даже минутный простой критичен) — деплой в новую отдельную директорию с полностью готовым и проверенным кодом и последующее атомарное переключение символической ссылки на неё, вместо обновления файлов в директории, которую сервер обслуживает прямо в момент деплоя; в этой схеме откат — это переключение той же символической ссылки обратно на предыдущую версию, что происходит почти мгновенно и не оставляет промежуточных состояний.
Уведомления о результате деплоя
Даже с полностью автоматическим пайплайном кто-то из команды должен узнавать о результате деплоя, не заходя каждый раз в интерфейс GitHub Actions вручную, — особенно если деплой завершился неудачно и требует немедленного внимания. Простое решение — отправка уведомления в мессенджер как отдельный шаг workflow, выполняющийся после основного шага деплоя.
- name: Notify Telegram
if: always()
run: |
STATUS="${{ job.status }}"
curl -s -X POST "https://api.telegram.org/bot${{ secrets.TG_BOT_TOKEN }}/sendMessage" \
-d chat_id="${{ secrets.TG_CHAT_ID }}" \
-d text="Деплой example.ru: $STATUS, коммит $(git rev-parse --short HEAD)"
Условие if: always() гарантирует выполнение этого шага независимо от того, завершился ли предыдущий шаг деплоя успехом или ошибкой, — без него шаг уведомления по умолчанию выполнился бы только при успешном завершении всех предыдущих шагов, и команда узнавала бы только об успешных деплоях, оставаясь в неведении именно тогда, когда уведомление нужнее всего — при сбое.
Токен бота и идентификатор чата, как и SSH-ключ, хранятся в секретах репозитория, а не прямо в тексте workflow — это тот же принцип, что и с ключом деплоя: любое значение, дающее возможность действовать от имени какого-либо сервиса, должно передаваться через защищённый механизм секретов, а не находиться в открытом виде в файле, видимом всем, у кого есть доступ на чтение репозитория.
Похожим образом можно настроить уведомления и в Slack, и по электронной почте — принцип везде одинаковый: отдельный шаг с условием if: always() (или более узким if: failure() для уведомлений только о сбоях), который вызывает API нужного сервиса и передаёт статус выполнения задачи через встроенную переменную job.status.
Чек-лист настройки автодеплоя через GitHub Actions
Шаг 1. Создайте отдельную пару SSH-ключей для деплоя, добавьте публичный ключ на сервер, а приватный — как секрет в настройках репозитория GitHub.
Шаг 2. Опишите workflow с задачей тестов (composer install / npm ci и запуск тестов) и задачей деплоя, зависящей от успешного прохождения тестов через needs.
Шаг 3. Добавьте ssh-keyscan для сервера в шаг деплоя вместо отключения проверки хоста SSH.
Шаг 4. Заведите отдельного ограниченного пользователя deploy на сервере с точечными правами через sudoers вместо root-доступа.
Шаг 5. Настройте отдельную ветку и, желательно, отдельный сервер или поддомен для staging, прежде чем изменения попадают в main.
Шаг 6. Заранее определите и один раз проверьте на практике план отката к предыдущей версии на случай неудачного деплоя.
Итог: автоматизация как инвестиция, а не роскошь
Настройка CI/CD через GitHub Actions требует некоторого первоначального вложения времени по сравнению с простым git pull по SSH, но быстро окупается по мере роста частоты изменений и размера команды — автоматические тесты перед деплоем и единообразный, полностью логируемый процесс обновления кода снимают значительную часть человеческого фактора из повторяющихся операций.
Начинать не обязательно сразу со сложного многоступенчатого пайплайна со staging-окружениями и атомарным переключением симлинков — минимальный workflow с тестами и деплоем по SSH, разобранный в этой статье, уже даёт основную часть выгоды и может постепенно усложняться по мере реальной необходимости, а не заранее.
Если нужна помощь с настройкой CI/CD-пайплайна под конкретный стек проекта и особенности вашего VPS — оставьте заявку внизу страницы, поможем выстроить процесс автодеплоя от первого workflow до полноценной схемы со staging и планом отката.
Почему стоит выбрать нас
Быстрый запуск
Подберём конфигурацию и развернём проект в день обращения.
Надёжность
NVMe-диски, защита от DDoS и резервные копии по расписанию.
Поддержка 24/7
Дежурные инженеры на связи в любое время суток.
Настройка под ключ
Установим CMS, настроим веб-сервер, SSL и почту за вас.
Перенос без простоя
Аккуратно перенесём сайт и базу данных на новый сервер.
Прозрачные цены
Актуальные тарифы и честный подбор без навязывания.
Вопросы и ответы
Чем автодеплой через GitHub Actions лучше обычного git pull по SSH?
Он добавляет автоматическую проверку кода (тесты, линтеры) перед тем, как изменения попадут на сервер, убирает человеческий фактор из повторяющихся шагов деплоя и оставляет полный журнал каждого запуска — кто, когда и какой коммит задеплоил и с каким результатом.
Безопасно ли хранить приватный SSH-ключ в GitHub Actions Secrets?
Да, это стандартный и рекомендуемый способ — секреты в GitHub Actions хранятся в зашифрованном виде, недоступны через обычный просмотр репозитория и автоматически маскируются звёздочками в логах выполнения, если случайно попадают в вывод команды.
Нужен ли отдельный пользователь на сервере специально для деплоя из CI/CD?
Это хорошая практика — отдельный пользователь deploy с ограниченными правами (доступ только к директории проекта и к перезапуску нужных сервисов через sudo с ограниченным списком команд) снижает ущерб в случае компрометации самого CI/CD-ключа по сравнению с использованием root или личной учётной записи администратора.
Что делать, если задеплоенная версия оказалась нерабочей?
Быстрый откат — вернуть сервер к состоянию предыдущего успешного коммита (например, через git checkout на нужный хеш и повторный запуск шагов установки зависимостей) — именно поэтому важно заранее знать хеш последнего стабильно работавшего коммита, а не искать его в спешке во время инцидента.
Можно ли запускать деплой не при каждом пуше, а только вручную?
Да, GitHub Actions поддерживает триггер workflow_dispatch, запускающий пайплайн вручную по кнопке в интерфейсе GitHub — это часто комбинируют с автоматическим запуском тестов на каждый пуш и ручным подтверждением непосредственно деплоя на production.
Что означает StrictHostKeyChecking=no в примерах деплоя, и стоит ли его использовать?
Эта настройка отключает проверку, что SSH-ключ сервера совпадает с ожидаемым, и защищает от подмены сервера (атака «человек посередине») — отключать её ради удобства не стоит; правильное решение — заранее добавить реальный отпечаток ключа сервера в known_hosts пайплайна.
Нужно ли тестовое (staging) окружение, если проект небольшой?
Для совсем маленьких проектов можно обойтись без него, но по мере роста проекта staging-окружение, максимально похожее на production, заметно снижает риск, что ошибка, не покрытая автоматическими тестами, будет впервые обнаружена уже реальными пользователями на боевом сервере.
Что произойдёт, если во время деплоя упадёт сеть между GitHub Actions и сервером?
Шаг деплоя завершится с ошибкой, весь workflow будет помечен как неудачный, и (при корректно написанном скрипте с set -e) сервер, скорее всего, останется в состоянии частично применённых изменений — поэтому для критичных проектов стоит проектировать деплой так, чтобы такой сбой был безопасен, например через переключение симлинка только после полностью успешной подготовки новой версии.
Как оплачивать услуги?
Оплата помесячная, для облачных серверов доступна почасовая тарификация. Принимаются карты, банковские переводы и электронные кошельки.
Есть ли круглосуточная поддержка?
Да, дежурные инженеры на связи 24/7.
Выдаёте ли закрывающие документы?
Да, предоставим необходимые документы для оплаты.
Как оставить заявку?
Заполните форму на странице или нажмите «Заказать» — мы свяжемся и поможем с подбором.