GitLab CI/CD превращает ручной деплой "зашёл по SSH и обновил файлы" в предсказуемый конвейер: каждый push в ветку запускает сборку, тесты и выкладку на сервер без участия человека. Разберём, как настроить рабочий пайплайн деплоя на обычный VDS с нуля, включая безопасную передачу SSH-ключа и откат при ошибке.
Что такое CI/CD и зачем он нужен при деплое на VPS
CI/CD расшифровывается как continuous integration и continuous delivery — непрерывная интеграция и непрерывная доставка. На практике это файл .gitlab-ci.yml в корне репозитория, который описывает стадии: собрать проект, прогнать тесты, выложить код на сервер. GitLab Runner выполняет эти стадии на каждый push или merge request автоматически.
Без CI/CD деплой зависит от человека: забыл шаг — сломал прод. С пайплайном последовательность шагов одна и та же каждый раз, а лог выполнения виден в интерфейсе GitLab.
Минимальный .gitlab-ci.yml для деплоя по SSH
Базовый пайплайн из двух стадий — test и deploy — выглядит так:
stages:
- test
- deploy
run_tests:
stage: test
image: php:8.3-cli
script:
- php -l public/index.php
deploy_production:
stage: deploy
image: alpine:latest
only:
- main
before_script:
- apk add --no-cache openssh-client rsync
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | ssh-add -
- mkdir -p ~/.ssh
- ssh-keyscan -H $DEPLOY_HOST >> ~/.ssh/known_hosts
script:
- rsync -avz --delete ./app/ deploy@$DEPLOY_HOST:/var/www/app/
Как безопасно передать SSH-ключ в GitLab CI
Приватный ключ никогда не хранят в репозитории. Его добавляют в разделе Settings → CI/CD → Variables как переменную SSH_PRIVATE_KEY с флагами Protected и Masked. Protected означает, что переменная доступна только на защищённых ветках, Masked — что значение не попадёт в открытом виде в лог job.
- Ключ генерируют отдельно для CI, а не переиспользуют личный ключ разработчика
- На сервере для этого ключа создают отдельного пользователя
deployс ограниченными правами - В
authorized_keysна сервере ключ привязывают командой с ограничениемcommand=, если нужен доступ только к одному скрипту
Стадии пайплайна: build, test, deploy
Стадия build собирает артефакт — например, архив с зависимостями Composer или собранный фронтенд. Стадия test прогоняет линтеры и юнит-тесты и останавливает пайплайн при ошибке, не давая сломанному коду попасть на сервер. Стадия deploy копирует уже проверенный артефакт на VDS.
| Стадия | Что делает | Что происходит при ошибке |
|---|---|---|
| build | Собирает зависимости и артефакт деплоя | Пайплайн останавливается, деплоя не будет |
| test | Запускает линтеры, юнит- и интеграционные тесты | Job помечается failed, деплой блокируется |
| deploy | Копирует артефакт на сервер, перезапускает службы | Виден лог job, можно вручную откатить релиз |
Деплой через rsync и симлинк на релиз
Надёжная схема — деплой в отдельную папку с датой и переключение симлинка current только после успешной выкладки. Так сайт ни секунды не смотрит на частично скопированные файлы.
ssh deploy@$DEPLOY_HOST "mkdir -p /var/www/releases/$CI_COMMIT_SHORT_SHA"
rsync -avz ./app/ deploy@$DEPLOY_HOST:/var/www/releases/$CI_COMMIT_SHORT_SHA/
ssh deploy@$DEPLOY_HOST "ln -sfn /var/www/releases/$CI_COMMIT_SHORT_SHA /var/www/current"
ssh deploy@$DEPLOY_HOST "systemctl reload php8.3-fpm"
Откат при неудачном деплое
Если релиз сломал прод, откат — это переключение симлинка current на предыдущую папку в /var/www/releases/, а не повторный деплой старого коммита. Такой откат занимает секунды и не требует нового прогона пайплайна.
- Хранить на сервере последние 5–10 релизов, а не только текущий
- Держать отдельный job
rollbackв.gitlab-ci.yml, запускаемый вручную кнопкой в GitLab - Логировать каждый деплой с меткой времени и хэшем коммита в отдельный файл на сервере
Частые ошибки в конфигурации пайплайна
Первая ошибка — деплой без стадии тестов: пайплайн выкладывает на прод код, который даже не проверен линтером. Вторая — общий SSH-ключ на всех окружениях сразу, из-за чего компрометация одного ключа даёт доступ ко всем серверам. Третья — забытый only или rules в job деплоя, из-за чего выкладка запускается с любой ветки, а не только с main.
Если после деплоя код нужно ещё и настроить на уровне ОС — пакеты, cron, конфиги, — этот шаг логично вынести в отдельный Ansible-плейбук, который пайплайн запускает следующим job. Для сравнения с альтернативной платформой CI смотрите статью про деплой через GitHub Actions. Если сервером назначения выступает кластер из нескольких машин, деплой удобнее нацелить на Docker Swarm вместо одиночного VDS.
Итог: чек-лист рабочего пайплайна
Работающий деплой на VPS через GitLab CI строится из четырёх обязательных элементов, без которых пайплайн рано или поздно подведёт в проде.
- Отдельный SSH-ключ для CI с флагами Protected и Masked в переменных
- Стадия тестов перед стадией деплоя, а не деплой напрямую
- Релизы в отдельных папках с симлинком current для мгновенного отката
- Ограничение деплоя правилом
onlyилиrulesна нужную ветку