CI/CD на своём VPS
Свой сервер для сборок окупается, когда облачных минут не хватает, сборки медленные или нужен доступ к внутренним ресурсам. Но раннер выполняет любой код из репозитория, поэтому ошибка в его настройке открывает доступ к рабочему серверу и всем секретам. Разберём, как устроены раннеры GitLab и GitHub, сколько ресурсов им нужно, как деплоить по SSH и где хранить ключи.
CI/CD — это автоматическая сборка, проверка и выкладка кода после каждого изменения. Описание конвейера лежит в репозитории (.gitlab-ci.yml или файлы в .github/workflows/), а выполняет его раннер — программа, которая забирает задания у GitLab или GitHub и запускает их. Раннер можно взять у сервиса или поставить на свой VPS.
Свои раннеры: GitLab и GitHub
GitLab Runner устанавливается из официального репозитория (инструкция по установке) и регистрируется в проекте или группе токеном из настроек CI/CD. Главный выбор при регистрации — исполнитель (executor). shell запускает команды прямо в системе сервера, docker — в новом контейнере для каждого задания. По документации GitLab у shell нет чистого окружения для каждой сборки и задания видят всю файловую систему, включая токен раннера и чужой кэш. Для большинства проектов подходит исполнитель docker.
Свой раннер GitHub Actions добавляется в настройках репозитория или организации (документация GitHub). Пользоваться им бесплатно, вы платите только за сервер. Раннер сам подключается к GitHub по HTTPS на порт 443, входящие порты на сервере открывать не нужно. Если раннер не обновлялся 30 дней, GitHub перестаёт давать ему задания (справочник по своим раннерам).
Важное ограничение для открытых проектов. GitHub пишет, что свои раннеры почти никогда не стоит использовать с публичными репозиториями: любой может открыть pull request и выполнить на сервере свой код. Для публичного кода оставьте облачные раннеры, а свой сервер подключайте к закрытым репозиториям.
Почему сборка и рабочий сервер должны быть разными
- Нагрузка. Сборка фронтенда или компиляция занимает все ядра на минуты. Если раннер стоит на сервере сайта, сайт в это время отвечает медленнее.
- Безопасность. Раннер выполняет код из любой ветки, в том числе из ветки, которую прислал подрядчик. По документации GitLab исполнитель shell несёт высокий риск для сервера и сети, а контейнер в режиме privileged даёт заданию полный root на сервере. Если на этом же сервере работает сайт с базой клиентов, она доступна любому заданию.
- Место на диске. Образы Docker, кэш зависимостей и артефакты занимают десятки гигабайт. Когда диск заполнится, остановится не только сборка, но и сайт.
Схема для небольшой команды: один VPS под раннер, отдельный под рабочий сайт, если нужно, третий — под тестовый стенд (о нём статья VPS для разработчика: тестовые стенды). Раннер доставляет на рабочий сервер только готовый результат.
Сколько ресурсов нужно для сборки
Ориентир — облачные раннеры GitHub. По его справочнику стандартный Linux-раннер для закрытых репозиториев — 2 ядра, 8 ГБ памяти и 14 ГБ SSD, для публичных — 4 ядра и 16 ГБ. Если ваши сборки укладываются в эти ресурсы, такой же VPS справится не хуже, а с быстрым диском и сохранённым кэшем обычно быстрее.
- Ядра. Сборка с параллельной компиляцией и тестами использует все ядра. Если сборки идут одна за другой, 4 ядра заметно сокращают очередь по сравнению с двумя.
- Память. Сборка фронтенда, компиляция Java или Go и тесты с базой данных в контейнере требуют 4–8 ГБ на одно задание. Число одновременных заданий задаётся параметром
concurrentвconfig.tomlGitLab Runner (описание параметров); умножьте память одного задания на это число. - Диск. Установка зависимостей — это тысячи мелких файлов, на NVMe она идёт быстрее. Берите от 50 ГБ и чистите кэш Docker по расписанию:
docker builder pruneиdocker image prune. Почему диск важен, разобрано в статье NVMe или SSD. - Виртуализация. Для исполнителя docker нужен KVM: на OpenVZ Docker работает с ограничениями или не работает вовсе (KVM или OpenVZ).
По каталогу accsly.io на ноябрь 2026 года тариф VPS с 2 ядрами и 4 ГБ памяти в середине рынка стоит 1 710 ₽ в месяц, четверть таких тарифов дешевле 1 027 ₽. С 4 ядрами и 8 ГБ — 3 286 ₽, четверть дешевле 1 844 ₽. С 8 ядрами и 16 ГБ — 6 567 ₽. Подходящие тарифы собраны в подборках VPS для CI/CD-раннеров и VPS для GitLab, если вы ставите и сам GitLab.
Сервер для сборки по часам
Раннер нужен, пока идут сборки, обычно несколько часов в рабочий день. По каталогу accsly.io на ноябрь 2026 года почасовую оплату предлагают 173 из 2 248 хостеров с тарифами VPS, API для создания и удаления серверов — 298, и то и другое — 96. Тариф с 4 ядрами и 8 ГБ у хостеров с почасовой оплатой стоит в середине рынка 4 157 ₽ в месяц, около 5,7 ₽ в час. Сервер, который работает 3 часа в каждый из 22 рабочих дней, обойдётся примерно в 380 ₽ вместо месячной цены.
Чтобы это работало без ручных действий, сервер создаётся через API перед сборкой и удаляется после. GitLab Runner умеет так масштабироваться с плагинами для облаков, GitHub рекомендует одноразовые (ephemeral) раннеры, которые берут одно задание и удаляются. Бонус для безопасности: следующее задание не увидит следов предыдущего. Когда почасовая оплата выгоднее помесячной, разобрано в статье Почасовая оплата VPS: когда выгодна; тарифы — в подборках VPS с почасовой оплатой и VPS с API.
Деплой по SSH с ограниченным ключом
Самый простой способ доставить сборку на рабочий сервер — SSH: rsync файлов или команда, которая забирает новую версию и перезапускает службу. Чтобы ключ из CI не стал ключом от всего сервера:
- Отдельный пользователь. Создайте пользователя
deployбез sudo с правами только на каталог сайта. Если нужно перезапустить службу, разрешите в sudoers одну конкретную команду. - Отдельный ключ. Сгенерируйте пару ключей только для CI и не используйте её больше нигде.
- Ограничения в authorized_keys. Перед ключом допишите
restrict— запрет переадресации портов и терминала — иfrom="адрес раннера", чтобы ключ работал только с сервера сборки. Параметрcommand="…"разрешает ключу запускать только один скрипт деплоя (man authorized_keys). - Проверка сервера. Сохраните отпечаток ключа рабочего сервера в переменной CI и записывайте его в
known_hostsперед подключением. Отключать проверку параметромStrictHostKeyChecking=noнельзя: так раннер подключится к любому серверу, который выдаст себя за ваш.
Чтобы выкладка не прерывала работу сайта, новую версию запускают рядом со старой и переключают трафик, когда она ответила на проверку; прежняя версия остаётся для отката. Как это устроить на одном сервере, рассказано в статье Обновление сервера без простоя.
Базовая защита самого рабочего сервера — вход только по ключу, файрвол, fail2ban — описана в статье Первые 10 шагов после покупки VPS. Записи о входах ключа деплоя помогут разобраться, если что-то пошло не так: где их искать, рассказано в статье Журналы и аудит входов на сервере.
Где хранить секреты
- Не в репозитории. Пароли базы данных, токены API и ключи деплоя хранятся в переменных CI. В GitLab переменную можно сделать защищённой — она доступна только конвейерам защищённых веток и тегов — и маскированной, тогда её значение заменяется на
[MASKED]в журнале задания. Маскированное значение должно быть одной строкой без пробелов длиной от 8 символов (документация GitLab). - По одному значению на секрет. GitHub предупреждает, что секрет в виде JSON или YAML хуже скрывается в журналах; каждое значение храните отдельно и не выводите секреты в журнал сборки.
- Разные секреты для стенда и рабочего сервера. Ключ деплоя на рабочий сервер должен быть доступен только из защищённой ветки, а не из любой ветки разработчика.
- Короткоживущие токены вместо постоянных. Для облачных сервисов GitHub рекомендует OpenID Connect: задание получает токен на время сборки, и хранить постоянный ключ не нужно.
- Ротация. Меняйте ключи, когда из команды уходит человек с доступом к настройкам CI, и после любой утечки журнала сборки.
Файл .env с настройками рабочего сервера создают на нём один раз, с правами 600, и деплой его не перезаписывает. Тогда даже полная копия репозитория не содержит паролей от рабочей базы.