CI/CD на Linux: GitLab CI с нуля за час
Свой раннер, первый пайплайн из трёх стадий, секреты в переменных и деплой на сервер по SSH.

Коротко: чтобы завести GitLab CI с нуля, нужны три вещи: раннер на Linux-машине (пакет gitlab-runner, executor Docker), файл .gitlab-ci.yml в корне репозитория со стадиями test, build и deploy, и переменные CI/CD для секретов. Первый рабочий пайплайн с деплоем по SSH собирается примерно за час, без единого клика по кнопке «выложить вручную».
Ручной деплой держится, пока проект маленький и выкладывает его один человек. Потом появляется второй разработчик, кто-то забывает прогнать тесты, кто-то копирует на сервер не ту ветку, и выкладка превращается в ритуал с чек-листом в заметках. Конвейер CI/CD убирает человеческий фактор: каждый пуш проверяется одинаково, а на прод попадает только то, что прошло проверки. Поэтому команды, которые берут на себя разработку сервиса под ключ, обычно поднимают такой конвейер в первую же неделю проекта, ещё до первой строчки бизнес-логики: дальше вся работа идёт уже через него.
В этой статье соберём минимальный, но честный конвейер на собственном сервере: поставим раннер, напишем пайплайн, спрячем ключи и настроим деплой. Примеры проверены на Debian 12 и Ubuntu 24.04, но подойдут для любого дистрибутива с systemd.
Что такое CI/CD и как устроен GitLab CI?
CI (continuous integration, непрерывная интеграция) — автоматическая сборка и проверка кода при каждом изменении. CD (continuous delivery или deployment) — автоматическая доставка проверенного кода на стенд или в прод. В GitLab всё это описывается одним файлом в репозитории, а выполняется отдельной программой.
В схеме четыре сущности:
- Пайплайн — один прогон конвейера, который запускается на пуш, мерж-реквест, тег или по расписанию.
- Стадии (stages) — этапы, идущие строго по порядку: например, test → build → deploy. Если стадия упала, следующие не стартуют.
- Задачи (jobs) — конкретные команды внутри стадии. Задачи одной стадии выполняются параллельно.
- Раннер (runner) — агент на какой-то машине, который забирает задачи у GitLab и выполняет их.
Сам GitLab ничего не собирает: он только хранит код, читает .gitlab-ci.yml, раскладывает задачи и показывает логи. Вся работа происходит на раннерах, поэтому их можно ставить где угодно — на VPS, на домашний сервер, в Kubernetes.
Какой раннер и executor выбрать?
На GitLab.com есть общие раннеры с лимитом минут, но свой раннер полезен почти всегда: нет очереди, есть доступ во внутреннюю сеть, а окружение сборки под вашим контролем. Важнее выбрать executor — способ, которым раннер запускает задачу.
| Executor | Как работает | Когда брать | Минусы |
|---|---|---|---|
| shell | Команды выполняются прямо в системе от пользователя gitlab-runner | Деплой на этот же сервер, нет возможности поставить Docker | Нет изоляции, сборки засоряют хост и мешают друг другу |
| docker | Каждая задача — новый контейнер из указанного образа | Стандартный выбор для первого раннера | Нужен Docker и место под образы |
| docker-autoscaler | Раннер сам поднимает и гасит облачные машины под задачи | Много параллельных сборок, нестабильная нагрузка | Сложнее в настройке, нужен облачный аккаунт |
| kubernetes | Задача запускается подом в кластере | Команда уже живёт в Kubernetes | Избыточен, если кластера нет |
Дальше используем docker: он даёт чистое окружение на каждую задачу и не требует ничего, кроме самого Docker. Как работают контейнеры и образы, мы разбирали в статье Docker на Linux простыми словами. Полный список вариантов есть в документации по executor-ам GitLab Runner.
Шаг 1. Как установить GitLab Runner на Linux?
Нужен сервер с 2 vCPU, 4 ГБ памяти и хотя бы 40 ГБ диска. Сначала ставим Docker из официального репозитория (пакет docker.io из стандартных репозиториев тоже подойдёт для начала), затем сам раннер из репозитория GitLab:
curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | sudo bash
sudo apt install gitlab-runner
sudo systemctl status gitlab-runnerДля RPM-дистрибутивов скрипт называется script.rpm.sh, а пакет ставится через dnf. Подробности по версиям и архитектурам — в официальной инструкции по установке. Раннер работает как обычная служба systemd, поэтому его логи читаются привычно: journalctl -u gitlab-runner -f. Если systemd пока вызывает вопросы, загляните в наш разбор systemd без боли.
Шаг 2. Как зарегистрировать раннер в проекте?
С версии GitLab 16 старые регистрационные токены устарели, раннер создаётся из интерфейса:
- Откройте проект: Settings → CI/CD → Runners.
- Нажмите New project runner, укажите теги (например, docker, linux) и отметьте Run untagged jobs, если хотите, чтобы раннер брал задачи без тегов.
- GitLab покажет токен вида glrt-… — он показывается один раз, скопируйте его.
- На сервере выполните регистрацию:
sudo gitlab-runner register \
--url https://gitlab.com \
--token glrt-XXXXXXXXXXXXXXXX \
--executor docker \
--docker-image alpine:3.20 \
--non-interactive
sudo gitlab-runner verify
sudo gitlab-runner listКонфигурация сохраняется в /etc/gitlab-runner/config.toml. Там стоит поправить одну строку сразу: concurrent — сколько задач раннер выполняет одновременно. По умолчанию 1; на машине с 4 ГБ памяти разумно поставить 2. После правки файла раннер перечитывает его сам, перезапуск не нужен. В интерфейсе GitLab раннер должен загореться зелёным статусом online.
Шаг 3. Как написать первый .gitlab-ci.yml?
Файл кладётся строго в корень репозитория и строго под именем .gitlab-ci.yml — в других местах GitLab его не увидит. Начнём с проекта на Node.js, но структура одинакова для Python, Go или PHP: меняются только образ и команды.
stages:
- test
- build
- deploy
default:
image: node:22-alpine
interruptible: true
variables:
npm_config_cache: "$CI_PROJECT_DIR/.npm"
cache:
key:
files:
- package-lock.json
paths:
- .npm/
lint_and_test:
stage: test
script:
- npm ci
- npm run lint
- npm test
build:
stage: build
script:
- npm ci
- npm run build
artifacts:
paths:
- dist/
expire_in: 7 daysЧто здесь происходит:
- default.image — образ, в котором запускаются все задачи, если у задачи не указан свой.
- interruptible: true — если вы запушили новый коммит, старый незаконченный пайплайн можно отменить автоматически (включается в настройках проекта опцией Auto-cancel redundant pipelines).
- cache — каталог с зависимостями переживает между запусками. Ключ привязан к package-lock.json, поэтому кэш сбрасывается ровно тогда, когда меняются зависимости.
- artifacts — результат сборки (dist/) передаётся в следующие стадии и доступен для скачивания из интерфейса.
Важная разница: кэш — это ускоритель, на него нельзя полагаться, он может не прийти. Артефакты — это гарантированная передача файлов между задачами. Собранный код передавайте только артефактами. Все ключевые слова описаны в справочнике по синтаксису .gitlab-ci.yml, а тонкости кэширования — в разделе о кэше.
Закоммитьте файл и запушьте: в разделе Build → Pipelines появится первый пайплайн. Перед пушем полезно проверить синтаксис в Build → Pipeline editor — встроенный линтер сразу покажет ошибку отступа или неизвестный ключ.
Шаг 4. Как хранить секреты в GitLab CI?
Пароли, токены и ключи никогда не попадают в репозиторий, даже закрытый: история git хранит всё, и однажды закоммиченный ключ придётся отзывать. Для секретов есть переменные CI/CD: Settings → CI/CD → Variables.
| Флаг | Что делает | Когда включать |
|---|---|---|
| Protected | Переменная доступна только в защищённых ветках и тегах (обычно main) | Для всего, что ведёт на прод |
| Masked | Значение заменяется звёздочками в логах задач | Для любых паролей и токенов |
| Type: File | Значение пишется во временный файл, в переменной — путь к нему | Для SSH-ключей, kubeconfig, сертификатов |
Для деплоя заведите три переменные: SSH_PRIVATE_KEY (тип File, Protected), SSH_KNOWN_HOSTS (тип File, вывод ssh-keyscan вашего сервера) и DEPLOY_HOST. Ключ генерируйте отдельный, только для CI, а на сервере кладите его публичную часть в authorized_keys отдельного пользователя deploy без sudo. Как работают SSH-ключи и почему пароль лучше отключить, мы подробно писали в статье о входе по ключам. Все типы и ограничения переменных описаны в документации по переменным CI/CD.
Шаг 5. Как настроить деплой на сервер по SSH?
Добавляем в .gitlab-ci.yml задачу деплоя. Она забирает артефакт dist/ из стадии build и копирует его на сервер через rsync:
deploy_prod:
stage: deploy
image: alpine:3.20
needs: [build]
resource_group: production
environment:
name: production
url: https://example.org
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
when: manual
before_script:
- apk add --no-cache openssh-client rsync
- mkdir -p ~/.ssh && chmod 700 ~/.ssh
- cp "$SSH_KNOWN_HOSTS" ~/.ssh/known_hosts
- chmod 600 "$SSH_PRIVATE_KEY"
script:
- rsync -az --delete -e "ssh -i $SSH_PRIVATE_KEY" dist/ deploy@$DEPLOY_HOST:/var/www/app/releases/$CI_COMMIT_SHORT_SHA/
- ssh -i "$SSH_PRIVATE_KEY" deploy@$DEPLOY_HOST "ln -sfn /var/www/app/releases/$CI_COMMIT_SHORT_SHA /var/www/app/current"Несколько деталей, которые отличают рабочий деплой от учебного:
- rules — задача появляется только в ветке по умолчанию, а when: manual делает её кнопкой: прод выкатывается осознанно. Когда процесс устоится, manual можно убрать.
- resource_group: production — два деплоя никогда не пойдут одновременно, даже если два пайплайна дошли до этой стадии разом.
- environment — GitLab запоминает, какой коммит сейчас на проде, и показывает историю выкладок в Operate → Environments.
- Релизы в отдельных каталогах и симлинк current — переключение мгновенное, а откат сводится к перенаправлению симлинка на предыдущий каталог или к повторному запуску deploy_prod из старого пайплайна.
- known_hosts из переменной вместо StrictHostKeyChecking=no — иначе конвейер без вопросов отдаст ключ любому, кто подменит сервер.
Старые каталоги релизов стоит чистить: хватит небольшого bash-скрипта, который оставляет пять последних. Шаблоны таких скриптов есть в нашей подборке bash-скриптов для автоматизации. Подробнее про окружения и ручные шаги — в документации по environments.
Какие ошибки встречаются чаще всего?
| Симптом | Причина | Что сделать |
|---|---|---|
| Задача висит в статусе pending | У задачи есть теги, которых нет у раннера, или раннер offline | Сверить теги в Settings → CI/CD → Runners, проверить gitlab-runner verify |
| Пайплайн не создаётся | Ошибка YAML или файл не в корне | Проверить в Pipeline editor |
| Переменная пустая в задаче | Переменная Protected, а ветка не защищена | Защитить ветку или снять флаг для тестового окружения |
| Permission denied (publickey) | Права на файл ключа или ключ не в authorized_keys | chmod 600 на файл, проверить ~deploy/.ssh/authorized_keys |
| No space left on device на раннере | Накопились образы и тома Docker | Таймер с docker system prune -af --filter "until=168h" |
| Сборка каждый раз качает зависимости заново | Неверный путь кэша или кэш вне $CI_PROJECT_DIR | Держать кэш внутри каталога проекта, как в примере |
Почти любую проблему с пайплайном решают две вещи: внимательно прочитанный лог задачи целиком и Pipeline editor. Не начинайте с переустановки раннера.
Как развивать конвейер дальше?
Когда базовая схема работает, добавляйте по одному улучшению за раз, чтобы видеть, что именно сломалось, если что-то сломается:
- Ревью до мержа. Запускайте test на мерж-реквестах через правило if: $CI_PIPELINE_SOURCE == "merge_request_event" и запретите мерж без зелёного пайплайна в настройках проекта.
- Стенд перед продом. Отдельная задача deploy_staging без when: manual на каждый пуш в main, а прод — по кнопке после проверки стенда.
- Сборка образа вместо файлов. Если приложение живёт в контейнере, собирайте образ, пушьте его во встроенный Container Registry, а на сервере делайте docker compose pull и up -d.
- Проверки безопасности. Сканирование зависимостей и секретов подключается шаблонами через include — это пара строк в файле.
- Hardening раннера. Раннер исполняет код из репозитория, поэтому держите его на отдельной машине, не давайте ему доступ к проду шире, чем нужно для деплоя, и следуйте базовым правилам из статьи о защите Linux-сервера.
Главная мысль: конвейер — такой же код, как приложение. Он лежит в git, проходит ревью и меняется маленькими шагами. Если вы только начинаете работать с git, сначала пригодится наш гид по git для начинающих.
Частые вопросы
Нужен ли свой раннер, если проект лежит на GitLab.com?
Не обязательно: GitLab.com даёт общие раннеры с ограниченным числом минут в месяц. Свой раннер нужен, когда минут не хватает, нужен доступ во внутреннюю сеть, особое железо или полный контроль над окружением сборки.
Какой executor выбрать для первого раннера?
Docker. Каждая задача запускается в чистом контейнере с нужным образом, сборки не мешают друг другу и не засоряют хост. Shell оправдан только для деплоя на тот же сервер или если Docker поставить нельзя.
Почему пайплайн не запускается после пуша?
Чаще всего причин три: файл называется не .gitlab-ci.yml или лежит не в корне репозитория, в YAML синтаксическая ошибка, либо у задач есть теги, которых нет ни у одного активного раннера. Проверьте файл в редакторе пайплайнов и статус раннера в Settings → CI/CD → Runners.
Как хранить пароли и ключи для деплоя?
В переменных CI/CD проекта с флагами Protected и Masked, а SSH-ключ — переменной типа File. В репозиторий секреты не коммитят никогда, даже в закрытый.
Чем GitLab CI отличается от GitHub Actions?
Логика одна и та же: YAML-файл в репозитории, задачи, стадии, раннеры. GitLab CI проще развернуть целиком у себя вместе с самим GitLab, а свой раннер регистрируется одной командой. GitHub Actions сильнее экосистемой готовых действий из маркетплейса.
Сколько ресурсов нужно серверу под раннер?
Для небольших проектов хватает 2 vCPU и 4 ГБ памяти. Главное ограничение обычно — диск: образы и кэш Docker быстро занимают десятки гигабайт, поэтому настройте периодическую очистку через docker system prune.