Архив рассылок сообщества снова открыт. Читать письма 2003-2009
Главная/Блог/Разработка
Разработка13 мин чтенияНиколай Ковалёв

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 старые регистрационные токены устарели, раннер создаётся из интерфейса:

  1. Откройте проект: Settings → CI/CD → Runners.
  2. Нажмите New project runner, укажите теги (например, docker, linux) и отметьте Run untagged jobs, если хотите, чтобы раннер брал задачи без тегов.
  3. GitLab покажет токен вида glrt-… — он показывается один раз, скопируйте его.
  4. На сервере выполните регистрацию:
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_keyschmod 600 на файл, проверить ~deploy/.ssh/authorized_keys
No space left on device на раннереНакопились образы и тома DockerТаймер с docker system prune -af --filter "until=168h"
Сборка каждый раз качает зависимости зановоНеверный путь кэша или кэш вне $CI_PROJECT_DIRДержать кэш внутри каталога проекта, как в примере
Почти любую проблему с пайплайном решают две вещи: внимательно прочитанный лог задачи целиком и Pipeline editor. Не начинайте с переустановки раннера.

Как развивать конвейер дальше?

Когда базовая схема работает, добавляйте по одному улучшению за раз, чтобы видеть, что именно сломалось, если что-то сломается:

  1. Ревью до мержа. Запускайте test на мерж-реквестах через правило if: $CI_PIPELINE_SOURCE == "merge_request_event" и запретите мерж без зелёного пайплайна в настройках проекта.
  2. Стенд перед продом. Отдельная задача deploy_staging без when: manual на каждый пуш в main, а прод — по кнопке после проверки стенда.
  3. Сборка образа вместо файлов. Если приложение живёт в контейнере, собирайте образ, пушьте его во встроенный Container Registry, а на сервере делайте docker compose pull и up -d.
  4. Проверки безопасности. Сканирование зависимостей и секретов подключается шаблонами через include — это пара строк в файле.
  5. 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.

Вернуться в блог