MAATRIX / Блог / Woodpecker CI в Docker Compose: готовый файл

Woodpecker CI в Docker Compose: готовый файл

MAATRIX

Drone CI после смены лицензии на Polyform Small Business перестал быть по-настоящему открытым для коммерческого использования, и часть сообщества форкнула последнюю свободную версию под именем Woodpecker CI. Если вам нужен CI/CD без вендор-лока и без вопросов «а можно ли нам это на проде», Woodpecker закрывает эту потребность полностью бесплатно и без ограничений лицензии. Ниже — рабочий docker-compose.yml, который поднимает сервер и агента, подключает Gitea или GitHub и разворачивается на чистом VPS за 15 минут.

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

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

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

Почему Woodpecker, а не Drone или Jenkins

Woodpecker — это не косметический форк, а живой проект с активной разработкой: сообщество продолжило развивать кодовую базу Drone версии до смены лицензии, вычистило проприетарные куски и держит весь код под Apache 2.0. Практическое следствие — вы можете использовать его в закрытом коммерческом продукте, встраивать в инфраструктуру клиентов, форкать под себя, и никто не пришлёт письмо от юристов.

По сравнению с Jenkins разница в философии, а не только в лицензии:

  • Конфиг пайплайна лежит в репозитории (.woodpecker.yml), а не размазан по кликам в вебморде — это версионируется вместе с кодом и проходит код-ревью.
  • Каждый шаг пайплайна — отдельный Docker-контейнер, изоляция из коробки, никаких «на агенте случайно осталась старая версия node».
  • Сервер и агент — это два маленьких статических бинарника, а не Java-приложение с плагинами, которые то и дело ломают друг друга после обновления.

Из минусов — экосистема плагинов у Woodpecker заметно скромнее, чем у Jenkins, и часть задач (сложные approval-гейты, matrix-билды с наследованием) придётся собирать руками через YAML-хитрости или отдельные шаги. Для типового пайплайна «собрать образ → прогнать тесты → задеплоить» этого более чем достаточно, а вот дженкинсовский зоопарк из сотен плагинов Woodpecker не заменит один в один — если у вас уже есть завязанный на Jenkins-плагины процесс, миграция потребует переосмысления, а не просто переноса YAML.

Как устроена система: сервер, агент, форджа

Woodpecker состоит из двух ролей, которые в проде почти всегда живут раздельно:

  • Server — веб-интерфейс, API, хранилище состояния пайплайнов (SQLite или Postgres/MySQL), общается с вашей Git-форджей (Gitea, GitHub, GitLab, Bitbucket, Forgejo) через OAuth и принимает вебхуки о пушах и PR.
  • Agent — воркер, который забирает задания у сервера по gRPC и реально запускает шаги пайплайна как Docker-контейнеры. Агенту нужен доступ к Docker (обычно через /var/run/docker.sock), а не наоборот — сервер докера не касается вообще.

Связь агент-сервер держится на общем секрете WOODPECKER_AGENT_SECRET — сгенерируйте его один раз и используйте одинаковый в обоих контейнерах. Агентов может быть сколько угодно, в том числе на разных серверах — это стандартный способ горизонтально масштабировать сборочные мощности, не трогая сам Woodpecker-сервер.

Отдельная деталь: сервер сам ничего не собирает и не хранит артефакты пайплайнов дольше их жизни — если вам нужен персистентный кэш зависимостей между сборками, об этом придётся озаботиться отдельно (том с кэшем на агенте или внешнее S3-хранилище).

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

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

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

Готовый docker-compose.yml

Ниже — конфиг с Postgres вместо SQLite (переживает пересоздание контейнера сервера без плясок с volume) и Traefik в качестве reverse proxy с автоматическим HTTPS. Если Traefik у вас уже поднят на сервере для других сервисов, уберите блок traefik и оставьте только labels на woodpecker-server.

version: "3.8"

networks:
  woodpecker:
  web:

volumes:
  woodpecker-server-data:
  woodpecker-db-data:
  traefik-certs:

services:
  traefik:
    image: traefik:v3.1
    restart: unless-stopped
    command:
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "--certificatesresolvers.le.acme.httpchallenge=true"
      - "--certificatesresolvers.le.acme.httpchallenge.entrypoint=web"
      - "--certificatesresolvers.le.acme.email=admin@example.com"
      - "--certificatesresolvers.le.acme.storage=/certs/acme.json"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - traefik-certs:/certs
    networks:
      - web

  woodpecker-db:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      - POSTGRES_USER=woodpecker
      - POSTGRES_PASSWORD=change-me-strong-password
      - POSTGRES_DB=woodpecker
    volumes:
      - woodpecker-db-data:/var/lib/postgresql/data
    networks:
      - woodpecker

  woodpecker-server:
    image: woodpeckerci/woodpecker-server:latest
    restart: unless-stopped
    depends_on:
      - woodpecker-db
    volumes:
      - woodpecker-server-data:/var/lib/woodpecker
    environment:
      - WOODPECKER_OPEN=false
      - WOODPECKER_HOST=https://ci.example.com
      - WOODPECKER_DATABASE_DRIVER=postgres
      - WOODPECKER_DATABASE_DATASOURCE=postgres://woodpecker:change-me-strong-password@woodpecker-db:5432/woodpecker?sslmode=disable
      - WOODPECKER_AGENT_SECRET=замените-на-случайную-строку-32-символа
      - WOODPECKER_GITEA=true
      - WOODPECKER_GITEA_URL=https://git.example.com
      - WOODPECKER_GITEA_CLIENT=ваш-oauth-client-id
      - WOODPECKER_GITEA_SECRET=ваш-oauth-client-secret
      - WOODPECKER_ADMIN=ваш-username-в-gitea
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.woodpecker.rule=Host(`ci.example.com`)"
      - "traefik.http.routers.woodpecker.entrypoints=websecure"
      - "traefik.http.routers.woodpecker.tls.certresolver=le"
      - "traefik.http.services.woodpecker.loadbalancer.server.port=8000"
    networks:
      - woodpecker
      - web

  woodpecker-agent:
    image: woodpeckerci/woodpecker-agent:latest
    restart: unless-stopped
    command: agent
    depends_on:
      - woodpecker-server
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      - WOODPECKER_SERVER=woodpecker-server:9000
      - WOODPECKER_AGENT_SECRET=замените-на-случайную-строку-32-символа
      - WOODPECKER_MAX_WORKFLOWS=2
    networks:
      - woodpecker

Секрет проще всего сгенерировать так:

openssl rand -hex 32

Полученную строку подставьте в оба места, где встречается WOODPECKER_AGENT_SECRET — сервер и агент должны совпадать, иначе агент просто не сможет авторизоваться и будет молча висеть в статусе disconnected.

Подключение Gitea или GitHub как форджи

Woodpecker не хранит логины-пароли сам — авторизация целиком идёт через OAuth-приложение вашей Git-платформы. Для self-hosted Gitea (о её установке есть отдельный гайд по установке Gitea на VPS) это делается так:

  1. В Gitea зайдите в Settings → Applications → Manage OAuth2 Applications и создайте новое приложение.
  2. В поле Redirect URI укажите https://ci.example.com/authorize — именно /authorize, а не корень домена, это частая ошибка.
  3. Скопируйте Client ID и Client Secret в переменные WOODPECKER_GITEA_CLIENT и WOODPECKER_GITEA_SECRET в compose-файле.
  4. Перезапустите сервер: docker compose up -d woodpecker-server.

Для GitHub логика идентичная, только переменные называются WOODPECKER_GITHUB=true, WOODPECKER_GITHUB_CLIENT, WOODPECKER_GITHUB_SECRET, а OAuth-приложение создаётся в Settings → Developer settings → OAuth Apps вашего GitHub-аккаунта или организации. Callback URL там тот же паттерн: https://ваш-домен/authorize.

Важный момент безопасности — пока вы не проверили, что всё работает, держите WOODPECKER_OPEN=false. При true любой человек с аккаунтом в вашей фордже сможет залогиниться в Woodpecker и активировать свои репозитории на ваших вычислительных ресурсах. Список тех, кому можно быть админом, задаётся через WOODPECKER_ADMIN — это username в фордже, через запятую, если админов несколько.

Пример `.woodpecker.yml` в репозитории

После первого логина зайдите в веб-интерфейс Woodpecker, найдите нужный репозиторий во вкладке Repositories и включите его (Activate). С этого момента любой пуш будет триггерить пайплайн, если в корне репозитория лежит .woodpecker.yml:

when:
  - event: push
    branch: main
  - event: pull_request

steps:
  - name: test
    image: node:20-alpine
    commands:
      - npm ci
      - npm test

  - name: build
    image: docker:26-cli
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    commands:
      - docker build -t registry.example.com/myapp:${CI_COMMIT_SHA:0:8} .
      - docker push registry.example.com/myapp:${CI_COMMIT_SHA:0:8}
    when:
      branch: main

  - name: deploy
    image: alpine:3.20
    commands:
      - apk add --no-cache openssh-client
      - ssh -o StrictHostKeyChecking=no deploy@prod.example.com "docker compose pull && docker compose up -d"
    when:
      branch: main

Каждый шаг — отдельный контейнер из указанного образа, окружение между шагами не разделяется автоматически (кроме файлов в рабочей директории, которая монтируется общим томом на время пайплайна). Шаг build, который сам вызывает Docker, требует доступа к docker.sock хоста — это классическая схема Docker-out-of-Docker, у неё есть известный недостаток: контейнер шага получает фактический доступ к докер-демону хоста, то есть может влиять и на другие контейнеры на этой машине. Для чужого недоверенного кода (например, публичных PR) это не самый безопасный вариант, и в таких случаях стоит присмотреться к rootless-подходам или отдельному изолированному агенту под эту нагрузку.

Если у вас уже настроен собственный registry, у нас есть отдельная статья про приватный Docker registry на VPS — связка с Woodpecker в шаге build выше собрана именно под такой сценарий.

Секреты, кэш и несколько агентов

Хардкодить пароли и токены в .woodpecker.yml — плохая идея хотя бы потому, что файл лежит в git-истории навсегда. Секреты добавляются через веб-интерфейс: Repository → Settings → Secrets, либо глобально на уровне организации, если нужно расшарить один секрет на несколько репозиториев. В пайплайне секрет становится переменной окружения:

steps:
  - name: deploy
    image: alpine:3.20
    secrets: [ deploy_ssh_key ]
    commands:
      - echo "$DEPLOY_SSH_KEY" > /tmp/key

Секреты можно ограничить конкретными событиями (push, pull_request, tag) и конкретными ветками — это стоит сделать хотя бы для продовых деплой-токенов, чтобы форк-PR от постороннего не смог их выудить, изменив пайплайн.

Масштабирование по нагрузке решается количеством агентов, а не размером одного. Добавьте в compose ещё один сервис woodpecker-agent-2 с теми же переменными окружения (или поднимите агент на втором сервере — ему нужен только сетевой доступ до порта 9000 сервера) — Woodpecker сам распределит очередь заданий между всеми подключёнными агентами. Параметр WOODPECKER_MAX_WORKFLOWS на агенте ограничивает, сколько пайплайнов он тянет параллельно — на VPS с 2 vCPU разумно оставить значение 1-2, иначе сборки начнут конкурировать за CPU и всё будет собираться медленнее, чем по одной.

Для persistent-кэша между сборками (например, кэш npm или Maven) готового решения из коробки нет — практический вариант: смонтировать на агент отдельный volume и обращаться к нему из шагов через volumes: в .woodpecker.yml, либо вынести кэш во внешнее S3-совместимое хранилище через отдельный шаг синхронизации.

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

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

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

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

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

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

Woodpecker CI полностью бесплатен?

Да, весь код под Apache 2.0 без коммерческих ограничений — в отличие от текущей лицензии Drone CI, которая для бизнеса требует платной подписки при определённых сценариях использования.

Можно ли использовать SQLite вместо Postgres?

Можно, для небольших команд и тестового стенда SQLite подойдёт — просто не задавайте WOODPECKER_DATABASE_DRIVER и WOODPECKER_DATABASE_DATASOURCE, тогда сервер создаст SQLite-файл в /var/lib/woodpecker. Для прода с несколькими агентами и историей сборок за месяцы Postgres надёжнее в плане резервного копирования и параллельного доступа.

Агент показывает статус offline, хотя контейнер запущен — в чём дело?

Почти всегда это несовпадающий WOODPECKER_AGENT_SECRET между сервером и агентом, либо агент не может достучаться до сервера по адресу из WOODPECKER_SERVER — проверьте, что оба контейнера в одной Docker-сети и порт 9000 у сервера ничем не блокируется.

Чем Woodpecker отличается от GitLab CI/CD?

GitLab CI/CD идёт в комплекте с GitLab и заточен именно под него, а Woodpecker — независимый инструмент, который одинаково подключается к Gitea, GitHub, GitLab или Bitbucket. Если у вас уже self-hosted GitLab, штатный GitLab CI/CD на VPS может оказаться проще, поскольку не требует отдельного сервиса.

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

Для небольшой команды сервер и агент спокойно уживаются на одном VPS с Gitea — сервер Woodpecker лёгкий, основную нагрузку дают именно сборочные контейнеры агента. Если сборки тяжёлые (компиляция, тесты с браузером), агент лучше вынести на отдельный сервер, чтобы не забивать CPU и не мешать самой Gitea отвечать на запросы.

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

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

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