MAATRIX / Блог / Drone CI на сервере: частые ошибки и решения

Drone CI на сервере: частые ошибки и решения

MAATRIX

Drone CI подкупает простотой: один бинарник сервера, один раннер, весь пайплайн описан в .drone.yml, а каждый шаг выполняется в отдельном одноразовом Docker-контейнере. Но именно контейнерная изоляция и порождает большинство проблем — от «висит на статусе pending» до «шаг не видит файлы предыдущего шага». Ниже — конкретные причины и рабочие решения, которые встречаются на реальных VPS с Gitea, GitLab и GitHub в качестве источника репозиториев.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →

Сервер не стартует или падает после первого запуска

Drone Server поднимается через Docker Compose, и первая ошибка обычно связана с обязательными переменными окружения. Без DRONE_RPC_SECRET и DRONE_SERVER_HOST контейнер либо не стартует, либо стартует, но раннер не может к нему подключиться.

Минимальный рабочий docker-compose.yml:

services:
  drone-server:
    image: drone/drone:2
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - drone-data:/data
    environment:
      - DRONE_GITEA_SERVER=https://git.example.com
      - DRONE_GITEA_CLIENT_ID=xxxxx
      - DRONE_GITEA_CLIENT_SECRET=xxxxx
      - DRONE_RPC_SECRET=замените_на_случайную_строку
      - DRONE_SERVER_HOST=ci.example.com
      - DRONE_SERVER_PROTO=https
      - DRONE_USER_CREATE=username:admin,admin:true

volumes:
  drone-data:

DRONE_RPC_SECRET должен быть одинаковым на сервере и на раннере — это общий ключ, которым раннер аутентифицируется при получении заданий. Сгенерировать его проще всего командой openssl rand -hex 16. Если после docker compose up -d контейнер сразу перезапускается — смотрите логи:

docker compose logs -f drone-server

Частая находка в логах — context deadline exceeded при обращении к Gitea/GitLab: значит DRONE_GITEA_SERVER указывает на адрес, недоступный из контейнера (например, localhost, который внутри контейнера — не то же самое, что на хосте). Указывайте реальный домен или IP, доступный изнутри Docker-сети, а не 127.0.0.1.

OAuth-авторизация не проходит: redirect_uri mismatch

Вторая по частоте проблема — не удаётся залогиниться через Gitea/GitHub/GitLab. Drone в качестве OAuth-callback использует DRONE_SERVER_HOST + /login, и если это значение не совпадает с тем, что зарегистрировано у провайдера, авторизация падает с redirect_uri_mismatch или зависает на белом экране.

Проверьте три вещи:

  1. В настройках OAuth-приложения (Settings → Applications в Gitea/GitHub) Redirect URI должен быть ровно https://ci.example.com/login, без слэша на конце лишнего или отсутствующего протокола.
  2. DRONE_SERVER_PROTO=https обязателен, если сервер за реверс-прокси с TLS — иначе Drone сгенерирует callback на http://, и провайдер его отклонит.
  3. Если Gitea/GitLab самохостинг с самоподписанным сертификатом, добавьте DRONE_GITEA_SKIP_VERIFY=true (или аналог для GitLab/GitHub Enterprise) — иначе сервер не сможет проверить TLS при обмене токеном.

Для reverse-proxy (например, Traefik или Caddy) важно пробросить заголовки X-Forwarded-Proto и Host — без них Drone за прокси иногда «думает», что работает по HTTP, даже если снаружи HTTPS.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Раннер не подключается или не видит docker.sock

Drone Runner для Docker-раннера должен иметь доступ к сокету Docker демона — именно через него он создаёт контейнер под каждый шаг пайплайна. Это самая частая причина зависших сборок: статус pending держится бесконечно, а в логах раннера — permission denied или cannot connect to the Docker daemon.

Compose для раннера:

services:
  drone-runner:
    image: drone/drone-runner-docker:1
    restart: unless-stopped
    ports:
      - "3000:3000"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      - DRONE_RPC_HOST=ci.example.com
      - DRONE_RPC_PROTO=https
      - DRONE_RPC_SECRET=тот_же_секрет_что_и_на_сервере
      - DRONE_RUNNER_CAPACITY=2
      - DRONE_RUNNER_NAME=runner-01

Если раннер и сервер — разные контейнеры на одной машине, DRONE_RPC_HOST может смотреть на внутреннее DNS-имя сервиса из compose-сети (например, drone-server), а не на публичный домен — это и быстрее, и не зависит от внешнего DNS. DRONE_RUNNER_CAPACITY ограничивает число параллельных заданий: на VPS с 2 vCPU и 4 ГБ RAM больше 2 одновременных сборок обычно приводит к OOM, если проекты собирают что-то тяжелее статического сайта.

Отдельная грабля — SELinux/AppArmor на некоторых дистрибутивах блокирует запись через смонтированный docker.sock, даже если пользователь формально в группе docker. Проверяется командой docker exec -it <runner-container> docker ps — если падает с permission denied, добавьте раннеру privileged: true для диагностики, а в проде решайте точечно через :z/:Z метки тома.

Ошибки в `.drone.yml`: синтаксис и структура пайплайна

Drone строг к формату YAML-пайплайна, и часть ошибок обнаруживается только в момент запуска, а не при коммите файла. Базовая структура для Drone 2.x:

kind: pipeline
type: docker
name: default

steps:
  - name: build
    image: golang:1.22
    commands:
      - go build -o app .

  - name: test
    image: golang:1.22
    commands:
      - go test ./...

trigger:
  branch:
    - main
  event:
    - push

Частые причины, по которым пайплайн вообще не запускается (не появляется даже в списке сборок):

  • Файл называется не так. Drone 2.x ищет .drone.yml в корне репозитория; если у вас старый .drone.star (Starlark) или .drone.jsonnet, нужно явно включить соответствующий конвертер в настройках репозитория (Settings → вкладка с конвертерами), иначе файл просто игнорируется.
  • Секция trigger отсекает событие. Если trigger.event не содержит push, а вы пушите в ветку — сборка не стартует, и это не ошибка, а тихий пропуск. Проверяйте вкладку Activity в UI Drone — там видно, распознал ли Drone вебхук вообще.
  • Неверный отступ или дублирующийся ключ name на уровне step. Drone выдаст yaml: line X: mapping values are not allowed in this context — ищите лишний двоеточие или неэкранированную строку со спецсимволами (: внутри команды без кавычек).

Полезно держать под рукой CLI: drone lint .drone.yml (после drone login с токеном) проверяет синтаксис локально до пуша.

Секреты не доступны шагу пайплайна

Секреты в Drone хранятся отдельно от .drone.yml (Settings → Secrets в UI репозитория) и подключаются к шагу явно — это осознанное ограничение безопасности, но именно поэтому новички получают пустую переменную вместо значения.

steps:
  - name: deploy
    image: alpine
    environment:
      SSH_KEY:
        from_secret: deploy_ssh_key
    commands:
      - echo "$SSH_KEY" > /tmp/key

Если секрет не подтягивается — проверьте три момента:

  1. Имя в from_secret должно точно совпадать с именем, заданным в UI (регистр важен).
  2. По умолчанию секреты не доступны в пайплайнах, запущенных из pull request — это защита от кражи секретов через форк с вредоносным PR. Если вам нужны секреты и в PR-сборках, добавляйте environment.SSH_KEY.from_secret вместе с явным trigger.event: [push, pull_request] и осознавайте риск, либо разделяйте пайплайн на «build без секретов на PR» и «deploy с секретами на push в main».
  3. Секрет, добавленный на уровне организации/сервера (DRONE_ORG_SECRETS через плагин или отдельный source-manager), может быть переопределён одноимённым секретом уровня репозитория — проверяйте, откуда реально приходит значение, через временный echo-шаг с маскировкой в логах (Drone сам скрывает значения секретов в выводе, заменяя на ***).

Кэш и данные между шагами: что теряется и почему

Каждый шаг пайплайна — это отдельный, изолированный Docker-контейнер, и по умолчанию между шагами общими остаются только файлы в рабочей директории репозитория (Drone монтирует один и тот же workspace-volume во все шаги). А вот что не сохраняется автоматически:

  • содержимое /root/.cache, ~/.m2, node_modules — если шаг качает зависимости, следующий шаг в новом контейнере начнёт с нуля;
  • Docker-образы, собранные на шаге build, недоступны шагу test, если это разные контейнеры без общего Docker daemon (проблема Docker-in-Docker);
  • переменные окружения, экспортированные командой export внутри одного шага, не переживают переход к следующему шагу.

Для кэширования зависимостей между запусками пайплайна (не между шагами одного запуска, а между разными сборками) используют плагин drone-cache с бэкендом S3/MinIO:

steps:
  - name: restore-cache
    image: meltwater/drone-cache
    settings:
      restore: true
      backend: s3
      cache_key: "{{ checksum \"go.sum\" }}"
      mount:
        - /go/pkg/mod
    environment:
      AWS_ACCESS_KEY_ID:
        from_secret: s3_access_key
      AWS_SECRET_ACCESS_KEY:
        from_secret: s3_secret_key

Если поднимаете MinIO для этой цели рядом на том же сервере — см. MinIO в Docker Compose. Для задач, где нужен полноценный Docker-in-Docker (например, шаг сам собирает и пушит образ), используйте плагин plugins/docker вместо ручного вызова docker build — он инкапсулирует DinD-сложность и не требует пробрасывать хостовый docker.sock внутрь пользовательского шага, что заметно безопаснее.

Вебхук от Gitea/GitHub/GitLab не доходит до Drone

Пайплайн исправен, секреты на месте, а сборка просто не запускается при пуше — почти всегда это оборванный вебхук. Проверка по шагам:

# на сервере Git-провайдера: смотрим историю доставки вебхука
# Gitea: Settings репозитория → Webhooks → History
# видно код ответа и тело — обычно 200 при успехе

# на сервере Drone: смотрим, доходит ли запрос вообще
docker compose logs -f drone-server | grep webhook

Типичные причины разрыва:

  • Реверс-прокси режет заголовки. Если перед Drone стоит Nginx/Caddy, убедитесь, что проксируются оригинальные заголовки (X-Gitea-Event, X-Hub-Signature для GitHub) и не обрезается тело запроса ограничением client_max_body_size — большие payload с длинным списком коммитов иногда превышают дефолтный лимит в 1 МБ.
  • URL вебхука указывает на внутренний адрес. Если Gitea и Drone в одной Docker-сети, вебхук должен идти на внешний домен ci.example.com, а не на внутреннее имя контейнера — Git-провайдер обычно недоступен изнутри той же приватной сети, что видит только Drone.
  • HMAC-подпись не совпадает. Для GitHub/GitLab секрет вебхука должен совпадать с тем, что Drone ожидает — при самостоятельном пересоздании интеграции (например, после миграции сервера) старый секрет вебхука в настройках репозитория Git-провайдера остаётся прежним и рассинхронизируется.

Код 502/504 в истории доставки означает, что проблема не в Drone: сервер не успевает ответить (перегружен параллельными сборками) или прокси таймаутит раньше, чем Drone обработает запрос.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Чем Drone CI отличается от GitLab CI Runner по ресурсам?

Drone Server сам по себе лёгкий (около 100-150 МБ RAM в простое), но реальный аппетит определяет раннер и число параллельных job — как и с GitLab CI Runner, закладывайте отдельно ресурсы под сами сборки, а не только под управляющий сервис.

Можно ли запустить Drone без Docker-раннера, если проекту не нужна изоляция контейнерами?

Да, есть drone-runner-exec, который выполняет шаги напрямую на хосте без создания контейнеров — но тогда теряется главное преимущество Drone (чистое окружение на каждый шаг), и это скорее исключение для специфичных legacy-сборок.

Нужен ли отдельный сервер под Drone или можно на том же, где крутится продакшен?

Технически можно, но CI-нагрузка (параллельные сборки, докер-образы) непредсказуема по CPU и диску, поэтому CI лучше держать на отдельной VPS — так сборка не заберёт ресурсы у продакшен-сервисов в пиковый момент.

Как ограничить, кто может запускать пайплайны с доступом к секретам?

Через trigger.event/trigger.branch в .drone.yml плюс права доступа к репозиторию на уровне Git-провайдера — сам Drone не имеет отдельной ролевой модели поверх этого, он доверяет правам, выданным в Gitea/GitHub/GitLab.

Что делать, если после обновления Drone с 1.x на 2.x пайплайны перестали запускаться?

Проверьте kind: pipeline и type: docker в начале файла — в версии 2.x они обязательны, в 1.x синтаксис был немного другим (без явного kind/type), и старые файлы нужно привести к новому формату.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →