Woodpecker CI — self-hosted CI/CD¶
AKKO ships a self-hosted Woodpecker server (Apache-2.0, OSS) that replaces GitHub Actions. Pipelines run as pods on the same k3s cluster that hosts the rest of the platform — no external runners, no billing limits, no "job not started" failures when the GitHub free tier exhausts.
| Woodpecker (current) | GitHub Actions (legacy) | |
|---|---|---|
| Cost | 0 € | $0.008 / Linux minute past free tier |
| Concurrency | k3s scheduler | GitHub-imposed |
| Storage | local-path PVC | GHA cache |
| Visibility | ci.<domain> (oauth2-proxy gated) |
github.com |
| Sovereignty | 100 % on-prem | externally hosted |
Architecture¶
GitHub repo ──webhook──► Woodpecker server (akko-woodpecker-server, port 8000)
│
│ gRPC :9000 (shared secret)
▼
Woodpecker agent (2 replicas)
│
│ k8s API (namespaced Role)
▼
Pipeline pods (one per step)
│
│ PVC /woodpecker (per-build)
▼
Logs streamed back to the server UI
- Server: web UI, REST API, GitHub OAuth, scheduler. Backed by the
shared
akko-postgresqlcluster (databasewoodpecker). - Agent: pulls work over gRPC, creates pipeline pods in the same namespace via a namespaced Role (no cluster-wide privilege).
- Pipeline pods: ephemeral, one per step. Mount a per-build PVC
(
workspace.pvcSize: 10Gidefault) so all steps in a pipeline share the same/woodpecker/<repo>/<branch>workspace.
File layout in this repo¶
.woodpecker/ # auto-discovered by the server
├── 00-validate.yml # ACTIVE — helm lint + template --strict + grep changeme
├── 01-helm-lint.yml # ACTIVE — helm lint (dev + rendered)
├── 02-helm-template.yml # ACTIVE — helm template + kubeconform
├── 03-no-hardcoding.yml # ACTIVE — no hardcoded IPs/domains (R01)
├── 04-secrets.yml # ACTIVE — gitleaks + no plaintext passwords
├── 05-doc-consistency.yml # ACTIVE — mkdocs nav, bilingual, service coverage
├── 06-trivy.yml # ACTIVE — Trivy CRITICAL CVE scan (14 images)
├── 07-trino-ai-plugin.yml # ACTIVE — Maven build + unit tests
├── 08-deploy-netcup.yml.disabled # DISABLED — deploy to Netcup (enable manually)
├── 09-tests.yml # ACTIVE — pytest + Playwright smoke
├── 10-aden-tests.yml # ACTIVE — ADEN unit tests + coverage gate
└── 12-post-deploy-tests.yml.disabled # DISABLED — post-deploy E2E (depends on 08)
helm/akko/charts/akko-woodpecker/ # Helm sub-chart that runs server + agent
The numeric prefix gives a stable order in the Woodpecker dashboard; all pipelines run in parallel by default — the prefix is purely cosmetic.
Activating a disabled pipeline¶
Rename the file to remove .disabled:
Commit and push. Woodpecker auto-discovers all .yml files in .woodpecker/.
Pipeline matrix¶
| Pipeline | Trigger | Gate | Runs on PR? |
|---|---|---|---|
00-validate |
helm/** changes |
lint + template + changeme | Yes |
01-helm-lint |
helm/** changes |
lint dev + rendered | Yes |
02-helm-template |
helm/** changes |
template + kubeconform | Yes |
03-no-hardcoding |
helm/** changes |
grep forbidden patterns | Yes |
04-secrets |
any push / PR | gitleaks + plaintext scan | Yes |
05-doc-consistency |
any push / PR | mkdocs build, bilingual | Yes |
06-trivy |
docker/** changes |
CRITICAL CVE scan | Yes |
07-trino-ai-plugin |
docker/trino-ai-functions/** |
Maven test + package | Yes |
09-tests |
tests/** changes |
pytest + Playwright | Yes |
10-aden-tests |
docker/aden/** |
unit tests + 70% coverage | Yes |
08-deploy-netcup |
push to main only | helm upgrade (needs secrets) | No (disabled) |
12-post-deploy-tests |
after 08 succeeds | E2E health + UI | No (disabled) |
Enable on a cluster¶
1. Create a GitHub OAuth App¶
GitHub → Settings → Developer settings → OAuth Apps → New OAuth App
| Field | Value |
|---|---|
| Application name | AKKO Woodpecker (<env>) |
| Homepage URL | https://ci.<domain>/ |
| Authorization callback URL | https://ci.<domain>/authorize |
Note the Client ID + Client Secret.
2. Generate the agent shared secret¶
3. Add to helm/examples/values-dev-secrets.yaml (gitignored)¶
akko-woodpecker:
agent:
secret: <agent-shared-secret>
github:
client_id: <oauth-app-client-id>
client_secret: <oauth-app-client-secret>
4. Enable the sub-chart¶
5. Helm upgrade¶
helm upgrade akko helm/akko/ -n akko \
-f helm/examples/values-dev.yaml \
-f helm/examples/values-domain.yaml \
-f helm/examples/values-dev-secrets.yaml \
--set-file akko-keycloak.realm.data=helm/examples/realm-domain.json
6. Add the GitHub webhook¶
After the first OAuth login, Woodpecker prints the webhook URL — paste it
into your GitHub repo (Settings → Webhooks → Add webhook). Content type
application/json, secret = the agent shared secret.
7. Add deploy secrets in the Woodpecker UI¶
The 08-deploy-netcup.yml pipeline reads two secrets from the Woodpecker
secret store (Repo Settings → Secrets):
| Secret | Value |
|---|---|
kubeconfig |
content of /etc/rancher/k3s/k3s.yaml (the in-cluster kubeconfig the agent will use) |
akko_domain |
bare hostname, e.g. 159.195.77.208.nip.io |
Mark them available to push events only (not pull requests) so a malicious PR cannot deploy to prod.
Migrating a workflow from GitHub Actions¶
The Woodpecker schema is close enough to GHA that a 1:1 port is usually trivial. The two main differences:
| GitHub Actions | Woodpecker |
|---|---|
jobs.<id>.steps.[].uses: actions/checkout@v4 |
implicit — every pipeline gets a fresh git clone |
runs-on: ubuntu-latest |
N/A — every step picks its own image: (Docker) |
env: (job-level) |
environment: (step-level) |
secrets.MY_SECRET |
secrets: [my_secret] + from_secret: env wiring |
if: github.ref == 'refs/heads/main' |
when: block at the top of the pipeline file |
Troubleshooting¶
- Pipeline stuck "Pending" — agent has reached
WOODPECKER_MAX_WORKFLOWS=4. Either wait, or bump replicas inhelm/akko/charts/akko-woodpecker/values.yaml. agent secret mismatch— the agent and server have differentWOODPECKER_AGENT_SECRET. Re-roll: edit the Helm secret + restart both deployments.github oauth: invalid client— double-check the callback URL in the GitHub OAuth App matcheshttps://ci.<domain>/authorizeexactly.postgres connection refused— the server is starting beforeakko-postgresqlis ready. Bump readinessProbe initialDelaySeconds, or rely on the post-install hook to wait.
Roadmap¶
- [ ] OPA-style approval gates for
08-deploy-netcup(manual click from akko-admin in the UI before helm upgrade runs). - [ ] Slack notifications on failure (Woodpecker has a built-in
notifyplugin). - [ ] Metrics endpoint — Woodpecker exposes Prometheus metrics on
/metrics; add a ServiceMonitor.
See also¶
- CI GitHub Actions (legacy) — what we migrated from
- Backup & DR — the Woodpecker DB lives in
akko-postgresql, covered by the dailypg_dumpCronJob (see Observability & SLO) - Upstream docs : https://woodpecker-ci.org/