Your First Woodpecker Pipeline - Anatomy of .woodpecker.yml
Status: Active
Last Updated: 2026-08-26
Category: CI/CD - Phase 3: Woodpecker CI
Prerequisites: woodpecker-installation
Time: 1-2 hours
Tags: woodpecker, pipeline, yaml, docker-buildx, secrets, matrix, debugging, ci
Summary
Every Woodpecker pipeline is a single .woodpecker.yml file living in your repository โ conditions (when), steps, container images, commands, and secrets in one versioned YAML file. This guide dissects the syntax, builds a realistic test + Docker-image-publish pipeline, introduces secrets management and matrix builds, and shows how to debug failures from the logs.
๐ฏ What You'll Learn
By the end of this article, you'll be able to:
- โ
Write
.woodpecker.ymlwithwhen,steps,image, andcommands - โ Filter pipelines by branch, event, and tag
- โ Run automated tests in the right language image
- โ Build and push a Docker image from a pipeline using the buildx plugin
- โ
Store and consume secrets safely with
from_secret - โ Fan out test runs with matrix builds (and know the 27-axis limit)
- โ Read pipeline logs to find the exact failing command fast
Table of Contents
- Pipeline Basics
- Anatomy of .woodpecker.yml
- When: Controlling When Pipelines Run
- A Realistic Pipeline: Test Then Publish
- Building and Pushing Docker Images
- Secrets Management
- Matrix Builds
- Debugging Failed Pipelines
- Troubleshooting
- Key Takeaways
- Next Steps
Pipeline Basics
From the moment you activated a repository (woodpecker-installation), every push, tag, or PR triggers a webhook to Woodpecker. The server then looks for one thing: a pipeline definition at the repo root.
my-app/
โโโ src/
โโโ Dockerfile
โโโ .woodpecker.yml โ this is all it takes
Two facts that shape everything else:
- The clone step is implicit. Woodpecker clones your repo into a shared workspace before your first step runs; every step sees the same files.
- Each step runs in its own container, defined by
image. Steps share the workspace directory, but not processes or installed packages โ anything you need must come from the image, be committed to disk in the workspace, or be reinstalled per step.
Anatomy of .woodpecker.yml
The minimal official example:
when:
- event: push
branch: main
steps:
- name: build
image: debian
commands:
- echo "This is the build step"
- echo "some-data" > some-file.txt
- name: a-test-step
image: alpine
commands:
- echo "Testing ..."
- cat some-file.txt
Piece by piece:
| Key | Meaning |
|---|---|
when (top-level) |
Conditions for running this workflow at all |
steps: |
Ordered list of work units; run sequentially top-to-bottom |
name |
Identifier shown in the UI and used for depends_on |
image |
Any OCI image from any registry you can pull โ debian, node:22-alpine, your own |
commands |
Shell commands executed inside the step's container, in the workspace |
You can also put multiple workflows in .woodpecker/ as separate files (e.g. .woodpecker/test.yml, .woodpecker/publish.yml) โ handy once pipelines grow.
When: Controlling When Pipelines Run
when accepts a list of condition blocks; the workflow runs if any block matches:
when:
- event: push
branch: main # pushes to main
- event: tag # any tag push
- event: pull_request # PRs from anyone
Common filters:
when:
- event: push
branch:
include: [main, "release/*"]
path: # only when relevant files changed
include: ["src/**", ".woodpecker/**"]
- event: cron
cron: nightly-audit
Step-level when works the same way and toggles individual steps:
- name: deploy
image: alpine
commands: [...]
when:
- event: push
branch: main
๐ก Without any
when, the workflow runs on every event including pull requests โ usually not what you want for deploy steps.
A Realistic Pipeline: Test Then Publish
A Node app: install โ lint โ test on every push/PR; build and publish the image only on main.
# .woodpecker.yml
when:
- event: push
branch: main
- event: pull_request
steps:
- name: install
image: node:22-alpine
commands:
- corepack enable
- npm ci
- name: lint
image: node:22-alpine
commands:
- corepack enable && npm run lint
- name: test
image: node:22-alpine
environment:
CI: "true"
commands:
- npm test -- --run
- name: publish
image: woodpeckerci/plugin-docker-buildx:5
settings:
repo: registry.example.com/team/my-app
tags: "${CI_COMMIT_SHA},latest"
registry: registry.example.com
username:
from_secret: registry_user
password:
from_secret: registry_password
when:
- event: push
branch: main
Notes:
- Steps 1โ3 share nothing except the workspace โ each reinstalls what it needs.
npm cioutput isn't cached between steps unless you add cache volumes. publishonly executes for pushes tomain; PRs stop after tests.- Built-in environment variables like
CI_COMMIT_SHAlet you stamp artifacts with commit identity.
Building and Pushing Docker Images
The idiomatic way is the buildx plugin โ a plugin is just another container whose behavior is configured via settings. Plugins never run arbitrary shell, which makes them safe to receive secrets.
- name: publish
image: woodpeckerci/plugin-docker-buildx:5
settings:
# build context & Dockerfile default to the workspace root
repo: registry.example.com/team/my-app
tags:
- latest
- "${CI_COMMIT_TAG}" # when building from a tag event
registry: registry.example.com
username:
from_secret: registry_user
# plugin settings become PLUGIN_* env vars inside the container
password:
from_secret: registry_password
platforms: linux/amd64,linux/arm64
The plugin needs access to a Docker daemon to build. On our agent setup (Docker backend), that's already satisfied through the mounted socket โ no extra configuration required for standard builds. Multi-arch via QEMU may need platforms support enabled on the runner.
Prefer a private registry? Create registry credentials once under Repository Settings โ Registries instead of baking them into steps; Woodpecker injects them automatically when pulling private base images.
๐ Need a better Dockerfile first? See containers/dockerfile-guide.
Secrets Management
Woodpecker stores named variables in a central secret store, consumed in YAML with from_secret. Three levels, most-specific wins:
| Level | Scope | Set by |
|---|---|---|
| Repository | All pipelines of one repo | Repo admins |
| Organization | All repos in an org | Org owners |
| Global | Entire instance | Instance admins only |
Create one via UI (Repo โ Settings โ Secrets) or CLI:
woodpecker-cli repo secret add \
--repository team/my-app \
--image woodpeckerci/plugin-docker-buildx \
--event push \
--event tag \
--name registry_password \
--value "$(cat ~/.registry-pass)"
Safety rules baked into the design:
- Secrets are masked in logs by default.
- Secrets are not exposed to pull_request events unless you explicitly opt in โ think twice before enabling that on public repos.
- Restrict secrets to specific plugins/images so a rogue step can't exfiltrate them.
- Inside
commands, escape interpolation as$${VAR}so Woodpecker's preprocessor doesn't eat it:
- name: use-secret-in-shell
image: alpine
environment:
TOKEN_ENV:
from_secret: secret_token
commands:
- echo $${TOKEN_ENV} # $$ escapes Woodpecker's ${} preprocessing
Matrix Builds
Run the same workflow across combinations of versions/platforms. Woodpecker fans out one full workflow per combination (max 27 axes):
matrix:
NODE_VERSION:
- 20
- 22
include:
- NODE_VERSION: 22
EXPERIMENTAL: "true"
steps:
- name: test
image: node:${NODE_VERSION}-alpine
commands:
- npm test
${NODE_VERSION} is interpolated before YAML parsing โ even into image:. The multi-platform pattern is especially useful for agents on different architectures:
matrix:
platform:
- linux/amd64
- linux/arm64
labels:
platform: ${platform}
steps:
- name: test
image: alpine
commands:
- echo "running on ${platform}"
Debugging Failed Pipelines
When a pipeline goes red, the UI gives you a waterfall: each step with duration and exit code. Click the failed step and read the log bottom-up:
+ npm test -- --run
...
FAIL src/api.test.ts
โ GET /health returns 200
expected 200, received 500
exit code 1
Reading rules:
- Last lines first โ the failing command is almost always the final block before
exit code N. - Compare with local execution. You can reproduce without pushing:
woodpecker-cli exec .woodpecker.ymlruns the workflow locally against a supported backend โ same images, same commands. - Check which
whenmatched. A pipeline that "didn't run" is usually a filter mismatch; the server log shows skipped events, or temporarily relaxwhen. - Secret problems look like empty values. If a step prints blank where a secret should be, check the secret's allowed events/images โ restrictions silently exclude mismatched steps.
- Clone issues are webhook issues. Nothing appears at all? Return to woodpecker-installation and verify webhook deliveries return HTTP 200.
Troubleshooting
| Symptom | Likely cause / fix |
|---|---|
pipeline has no workflows / nothing triggers |
File must be named .woodpecker.yml (leading dot) at repo root, or valid YAML under .woodpecker/. Check indentation โ tabs are invalid YAML. |
image: pull access denied |
Private image without registry credentials โ add them under Repo Settings โ Registries. |
Step fails with command not found |
Each step is a fresh container; the tool lives in a different image. Use an image containing the toolchain, or install it in commands first. |
| Files written in step 1 missing in step 2 | Only the workspace persists. Write outputs relative to the workspace, not /tmp or $HOME. |
Secret is empty / masked value leaks as literal ${SECRET} |
Forgot from_secret, wrong secret name, or secret restricted to other events/images. Also remember $${VAR} escaping inside commands. |
| publish step fails auth to registry | from_secret names don't match created secrets, or secret not enabled for push/tag events. |
| Matrix job count explodes | Axes multiply: 3ร3ร3 = 27 jobs. Cap combinations with include:. |
| Works locally, fails in CI | Different working directory, missing env vars, or architecture mismatch (arm vs amd). Print env and pwd in a debug step. |
Key Takeaways
- One YAML file per repo drives everything: conditions, steps, images, secrets.
- Steps = containers sharing a workspace; tools come from the image, data from the workspace.
- Gate deploys with step-level
when(branch/event filters). - Publish images with the buildx plugin; feed it credentials via
from_secret. - Secrets have three scopes, are masked, hidden from PRs by default, and restrictable per image/event.
- Debug bottom-up from step logs, and reproduce locally with
woodpecker-cli exec.
Next Steps
- Wire deployments to real infrastructure: gitops/gitops and manual-vs-automated
- Improve the image you're publishing: containers/dockerfile-guide
- Where CI fits in the bigger picture: cicd-concepts
Sources & Related
Researched via headless-browser session (DuckDuckGo results were bot-challenged; official documentation fetched directly):
- https://woodpecker-ci.org/docs/usage/intro (Your first pipeline, v3.18.x โ clone step, plugins, workspace)
- https://woodpecker-ci.org/docs/usage/secrets (secret levels, from_secret, $$ escaping, event/image filters)
- https://woodpecker-ci.org/docs/usage/matrix-workflows (matrix axes, 27-axis limit, platform labels)
- https://woodpecker-ci.org/docs/intro (Welcome to Woodpecker โ container-native design)
- Related KB: woodpecker-installation, forgejo-introduction, containers/dockerfile-guide, gitops/gitops
Change Log
- 2026-08-26: Initial draft created via headless-browser web research session.