cheat-sheets

Woodpecker CI

Woodpecker is a lightweight, self-hosted, container-native CI system (a community fork of Drone, see Drone CI). Every step runs in a container, workflows live in .woodpecker/*.yaml (or a single .woodpecker.yaml) in the repo, and it connects to Gitea/Forgejo, GitHub, GitLab or Bitbucket. This sheet targets Woodpecker v3 (latest release seen: v3.20.1); v2 differences are in the last table. For overall concepts see CI/CD Overview, for the forge see Gitea (and Forgejo), and for image builds see CI Docker Builds.

Documentation: Woodpecker docs, Workflow syntax, CLI


Minimal Workflow #

yaml
# .woodpecker/test.yaml
when:
  - event: [push, pull_request]
    branch: main            # for pull_request this matches the target branch

steps:
  - name: test
    image: node:22
    commands:
      - npm ci
      - npm test

Several files in .woodpecker/ are separate workflows that run in parallel unless linked with depends_on. A pipeline is one run containing all of its workflows.

Step Keywords #

KEYWORDDESCRIPTION
nameStep name (optional, otherwise numbered); must be unique once depends_on is used
imageContainer image, pin a tag or digest
commandsShell commands, run as one script; the step fails on the first non-zero exit
environmentEnvironment variables (map only in v3)
settingsPlugin parameters; they become PLUGIN_<NAME> variables
whenConditions deciding if the step runs
depends_onMake a DAG; steps run in parallel except where dependencies exist
failurefail (default), ignore or cancel
detachRun in the background until the workflow ends
volumesExtra volume mounts (trusted repos only)
privilegedPrivileged container (trusted repos only)
pullAlways pull the image
backend_optionsBackend specific options (Docker user, Kubernetes service account)
directoryWorking directory for the step

Steps can be a list (shown above, the v3 form) or a map keyed by name; both are accepted.

Workflow Level Keywords #

KEYWORDDESCRIPTION
stepsThe ordered list of steps (required)
servicesSidecar containers such as databases; the name becomes the hostname
whenSkip the whole workflow unless the conditions hold
depends_onRun after other workflows (file name without path, dot and extension)
matrixRun the workflow once per combination (max 64 combinations, 10 values per axis)
labelsPick an agent, e.g. platform: linux/arm64
cloneCustomise the clone step (git: image: woodpeckerci/plugin-git)
skip_cloneDo not clone, for notification-only workflows
workspaceChange the workspace path
concurrencyconcurrency: 1 limits parallel runs of this workflow; extra runs queue instead of cancelling
variablesHome for YAML anchors that you reuse in the file

when Conditions #

A when list is OR-ed; the keys inside one list item are AND-ed.

yaml
steps:
  - name: deploy
    image: alpine
    commands: [./deploy.sh]
    when:
      - event: push
        branch: main
      - event: tag
        ref: refs/tags/v*
      - event: manual
      - evaluate: 'not (CI_COMMIT_MESSAGE contains "[skip deploy]")'
        event: push
  - name: notify-failure
    image: alpine
    commands: [./notify.sh]
    when:
      - status: [failure]
        event: [push, tag]
FILTERDESCRIPTION
eventpush, pull_request, pull_request_closed, pull_request_metadata, tag, release, deployment, cron, manual
branchBranch (glob). For pull requests it is the target branch
refFull ref, e.g. refs/tags/v*
repoowner/name
cronName of the cron job (also needs event: cron)
pathinclude/exclude globs on changed files (push and pull_request only)
statussuccess (default) or failure
platformAgent platform, e.g. linux/arm64
matrixOnly for one matrix permutation
instanceOnly on this Woodpecker server host
evaluateExpression on CI_* variables and custom variables

Always give every when item an event filter; the built-in linter warns otherwise, because new event types would trigger the step.

Services, Dependencies and Matrix #

yaml
matrix:
  GO_VERSION: ["1.23", "1.24"]
  DB: [postgres, mariadb]

services:
  - name: database
    image: postgres:17
    environment:
      POSTGRES_PASSWORD: example     # throw-away CI value

steps:
  - name: build
    image: golang:${GO_VERSION}
    commands: [go build ./...]
  - name: test
    image: golang:${GO_VERSION}
    depends_on: [build]
    environment:
      DATABASE_URL: postgres://postgres:example@database:5432/postgres
    commands: [go test ./...]

Matrix variables are substituted with ${VAR} before the YAML is parsed. Use include: to list explicit combinations instead of a full product.

Workflow dependencies across files:

yaml
# .woodpecker/deploy.yaml
depends_on: [lint, test]          # files .woodpecker/lint.yaml and test.yaml
when:
  - event: push
    branch: main

In a monorepo with when.path, mark dependencies optional: true (depends_on: [{name: lint, optional: true}]) so a skipped workflow does not block the deploy.

Secrets #

Add secrets under repo Settings > Secrets, or with the CLI. By default a secret is not available to pull_request events (enable that event explicitly and only for safe secrets). You can also restrict a secret to named plugin images.

yaml
steps:
  - name: publish
    image: woodpeckerci/plugin-docker-buildx
    settings:
      repo: ghcr.io/<OWNER>/<IMAGE>
      registry: ghcr.io
      username: <USER>
      password:
        from_secret: ghcr_token
      tags: latest
    when:
      - event: push
        branch: main

  - name: call-api
    image: curlimages/curl
    environment:
      API_TOKEN:
        from_secret: api_token
    commands:
      - 'curl -fsS -H "Authorization: Bearer $API_TOKEN" https://api.example.com/ping'

Write $VAR (not ${VAR}) for runtime shell variables in commands; ${...} expressions are pre-processed before the pipeline starts. Escape them as $${VAR} if you need a literal.

Built-in Variables #

VARIABLEMEANING
CI_REPOowner/name
CI_COMMIT_SHACommit SHA
CI_COMMIT_BRANCHBranch (target branch for pull requests)
CI_COMMIT_TAGTag name, empty unless the event is tag
CI_COMMIT_PULL_REQUESTPR number
CI_PIPELINE_EVENTThe event that triggered the pipeline
CI_PIPELINE_NUMBERPipeline number
CI_REPO_DEFAULT_BRANCHDefault branch of the repo
CI_WORKSPACEWorkspace path
CI_PIPELINE_DEPLOY_TARGETTarget of a deployment event

Cron and Manual Runs #

  • Cron: in the repo settings add a cron job (name + schedule), then give the steps event: cron (and optionally cron: <NAME>).
  • Manual: use the “Run pipeline” button or woodpecker-cli pipeline create; steps need event: manual to run. Variables passed with --var are available as environment variables.
  • Deployments: woodpecker-cli pipeline deploy <REPO> <PIPELINE> <ENVIRONMENT> creates a deployment event with CI_PIPELINE_DEPLOY_TARGET.

woodpecker-cli #

Point the CLI at the server and authenticate with your personal token (User settings > Personal access token):

bash
export WOODPECKER_SERVER=https://woodpecker.modem7.com
export WOODPECKER_TOKEN=<TOKEN>
COMMANDDESCRIPTION
woodpecker-cli repo listRepos you can see
woodpecker-cli repo add <FORGE-REMOTE-ID>Activate a repo
woodpecker-cli repo show <REPO>Repo settings
woodpecker-cli repo syncResync the repo list from the forge
woodpecker-cli repo repairRe-create webhooks
woodpecker-cli pipeline list <REPO>Recent pipelines
woodpecker-cli pipeline last <REPO>Most recent pipeline
woodpecker-cli pipeline show <REPO> <NUMBER>Details of one pipeline
woodpecker-cli pipeline ps <REPO> <NUMBER>Steps of a pipeline
woodpecker-cli pipeline log show <REPO> <NUMBER> <STEP>Step log
woodpecker-cli pipeline create --branch <BRANCH> <REPO>Trigger a manual run (--var KEY=VALUE, --workflow <NAME>)
woodpecker-cli pipeline start <REPO> <NUMBER>Restart a pipeline
woodpecker-cli pipeline stop <REPO> <NUMBER>Stop a running pipeline
woodpecker-cli pipeline kill <REPO> <NUMBER>Force kill
woodpecker-cli pipeline approve <REPO> <NUMBER>Approve a pipeline held for approval
woodpecker-cli pipeline decline <REPO> <NUMBER>Decline it
woodpecker-cli pipeline queuePending and running pipelines
woodpecker-cli repo secret add --name <NAME> --value <VALUE> <REPO>Add a secret (--event push --event tag limits events, --image limits plugins)
woodpecker-cli repo secret ls <REPO>List secrets (values hidden)
woodpecker-cli lint .woodpecker/Lint workflow files offline
woodpecker-cli exec .woodpecker/test.yamlRun a workflow locally (needs Docker)
woodpecker-cli exec --backend-engine docker --pipeline-event push --commit-branch main <FILE>Local run with metadata overrides
woodpecker-cli setupInteractive login/config

Run woodpecker-cli <COMMAND> --help to confirm flags on your version.

Server and Agent (Docker Compose) #

The server serves the UI/API and talks to the forge; one or more agents execute the workflows. See Docker-Compose.

yaml
services:
  woodpecker-server:
    image: woodpeckerci/woodpecker-server:v3
    restart: unless-stopped
    ports: ["8000:8000"]
    volumes:
      - woodpecker-data:/var/lib/woodpecker
    environment:
      WOODPECKER_HOST: https://woodpecker.example.com
      WOODPECKER_AGENT_SECRET: ${WOODPECKER_AGENT_SECRET}
      WOODPECKER_GITEA: "true"                      # or WOODPECKER_GITHUB / WOODPECKER_FORGEJO
      WOODPECKER_GITEA_URL: https://git.example.com
      WOODPECKER_GITEA_CLIENT: ${GITEA_OAUTH_CLIENT}
      WOODPECKER_GITEA_SECRET: ${GITEA_OAUTH_SECRET}
      WOODPECKER_ADMIN: <USER>

  woodpecker-agent:
    image: woodpeckerci/woodpecker-agent:v3
    restart: unless-stopped
    depends_on: [woodpecker-server]
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      WOODPECKER_SERVER: woodpecker-server:9000
      WOODPECKER_AGENT_SECRET: ${WOODPECKER_AGENT_SECRET}
      WOODPECKER_MAX_WORKFLOWS: 2

volumes:
  woodpecker-data:

Generate the shared secret with openssl rand -hex 32 (OpenSSL) and keep it in an .env file out of Git. The server needs a working webhook path from the forge and an OAuth app on the forge.

Docker Backend Notes #

  • Each step is a container on a per-workflow Docker network; services are reachable by their name.
  • The agent needs the Docker socket, which is root-equivalent on the host. Treat anyone who can edit a pipeline as able to run code on the agent host.
  • privileged, volumes and custom networks only work for repositories an admin marks Trusted (repo Settings). Docker-in-Docker (docker:dind service) needs privileged: true.
  • Extra agent settings: WOODPECKER_BACKEND_DOCKER_NETWORK, WOODPECKER_BACKEND_DOCKER_VOLUMES, WOODPECKER_MAX_WORKFLOWS, WOODPECKER_AGENT_LABELS.
  • Agent images accumulate; prune on a schedule (docker image prune -af --filter until=168h).
  • For image builds use woodpeckerci/plugin-docker-buildx (see CI Docker Builds) or a dind service.

v2 to v3 Differences #

TOPICv2v3
Secrets in stepssecrets: [name] listRemoved; use environment: NAME: from_secret: name or settings: key: from_secret: name
EnvironmentMap or listMap only; list form is rejected
Parallel stepsgroup:Removed; use depends_on:
Agent platform filterplatform:Removed; use labels: platform: linux/arm64
Event filtersincludes / excludes, environment filterRemoved; use when.evaluate
Failure stepsruns_on: [failure]runs_on is deprecated; use when: status: [failure]
Cron scheduleCron with secondsStandard five-field cron, no seconds
Image tagslatestlatest dropped; pin v3 or a full version
PR approval“gated” optionReplaced by “require approval” setting
CLI binary in DEB/RPMwoodpeckerwoodpecker-cli

Source: migration notes. Run woodpecker-cli lint after migrating.

Troubleshooting #

SYMPTOMCAUSE AND FIX
Pipeline never startsWebhook missing or failing: woodpecker-cli repo repair, check forge webhook delivery log and WOODPECKER_HOST
No workflow file foundFile must be .woodpecker/*.yaml or .woodpecker.yaml on the commit being built
Step skipped unexpectedlywhen item without matching event; for pull requests branch matches the target branch; check woodpecker-cli lint
Secret is emptySecret not enabled for that event (pull requests are off by default) or restricted to other plugin images
${VAR} is empty in commandsPre-processing consumed it; use $VAR, or $${VAR}
privileged ignored or rejectedRepo is not marked Trusted
Pipeline waiting foreverNo agent matches the workflow labels, or all agents are at WOODPECKER_MAX_WORKFLOWS
Agent cannot reach serverWOODPECKER_SERVER must be the gRPC address (port 9000), and WOODPECKER_AGENT_SECRET must match
Pull request from a fork needs approvalProject setting “Require approval for”: approve with the UI or pipeline approve
Cannot pull private imageAdd registry credentials in repo Settings > Registries, or WOODPECKER_DOCKER_CONFIG on the agent
Search all sheets

Tip: / or Ctrl/Cmd+K to open, Esc to close.