Woodpecker CI в Docker Compose: готовый файл
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) это делается так:
- В Gitea зайдите в Settings → Applications → Manage OAuth2 Applications и создайте новое приложение.
- В поле Redirect URI укажите
https://ci.example.com/authorize— именно/authorize, а не корень домена, это частая ошибка. - Скопируйте Client ID и Client Secret в переменные
WOODPECKER_GITEA_CLIENTиWOODPECKER_GITEA_SECRETв compose-файле. - Перезапустите сервер:
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 ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →