Woodpecker Common — Shared Application Configuration
Woodpecker_Common is the shared application layer for Woodpecker CI. It
is not deployed on its own; instead it supplies the Woodpecker-specific
configuration that Woodpecker_GKE builds on. End users
never configure this layer directly — it has no deployment UI inputs of its
own — but understanding what it provides explains the defaults you see in
the platform docs.
GKE-only, permanently. Woodpecker's execution backend
(WOODPECKER_BACKEND=kubernetes) runs each pipeline step in its own
dynamically created Kubernetes Pod, which requires real in-cluster
Kubernetes API access — Cloud Run has no Kubernetes API to call and no
privilege for docker-in-docker. There is no Woodpecker_CloudRun and there
will not be one; this is the same architectural class of gap as this
catalogue's other Common + GKE only modules (Kopia, RocketChat, Immich,
Temporal, Prowlarr, VictoriaMetrics, Plausible, LobeChat, Supabase).
Woodpecker_Common follows the same shape every Common module in this
catalogue uses purely for consistency, not because a second platform variant
exists or is planned.
For the infrastructure that actually provisions and runs Woodpecker, see the Woodpecker_GKE platform guide and the foundation guides (App_GKE, App_Common).
1. What this layer provides
| Area | Provided by Woodpecker_Common | Where it surfaces |
|---|---|---|
| Container image | Custom build: the official server image plus the agent binary grafted on, plus a static busybox:musl shell, plus a cloud entrypoint; built via Cloud Build with the app-specific WOODPECKER_VERSION build ARG | container_image output of the platform deployment |
| Version resolution | application_version = "latest" resolves internally to a pinned v3.16.0 — Woodpecker publishes no latest tag upstream | Image tag actually deployed |
| Datastore | Fixes database_type = "POSTGRES_15" | §3 in the platform guide |
| Database bootstrap | The first-deploy db-init job — role, database, grants. No separate migrate job: Woodpecker auto-migrates its own schema on server boot | initialization_jobs output |
| Co-located agent | The agent binary runs as a background process inside the same container/pod as the server | §Application Behaviour in the platform guide |
| Forge placeholders | Merges placeholder Gitea/Forgejo values into environment_variables so the server can boot at all | §Application Behaviour in the platform guide |
| Secrets | Generates WOODPECKER_AGENT_SECRET in Secret Manager — the co-located server↔agent internal gRPC credential | Injected automatically; retrieve via Secret Manager |
| Health checks | Default startup/liveness probes targeting HTTP GET /healthz (unauthenticated, confirmed 204 No Content) | §Observability in the platform guide |
| Object storage | None — storage_buckets = []. Woodpecker CI has no object-storage dependency | No module-managed buckets |
2. WOODPECKER_AGENT_SECRET — the one generated secret
Woodpecker ships with no built-in credentials of any kind that this layer needs to seed. The one thing it does generate is the shared secret that authenticates the co-located server's and agent's internal gRPC connection to each other — confirmed against the upstream docker-compose reference configuration, both processes read this exact variable name:
| Secret | Env var | Content | Rotation |
|---|---|---|---|
secret-<prefix>-<app>-agent-secret | WOODPECKER_AGENT_SECRET | 40-character random password, no special characters | A stable value is required — rotating it independently would break the co-located agent's connection to the server on every subsequent pod restart |
gcloud secrets list --project "$PROJECT" --filter="name~agent-secret"
gcloud secrets versions access latest --secret=<secret-name> --project "$PROJECT"
There is no other application-level secret generated by this layer. Woodpecker's own JWT/session material is managed entirely by the server itself against its Cloud SQL database.
3. Container image: co-located server + agent
Woodpecker ships the server and agent as two separate images — the
upstream docker-compose.yml reference runs two containers. This layer
builds ONE custom image instead by grafting the agent binary onto the server
image:
ARG WOODPECKER_VERSION=v3.16.0
FROM woodpeckerci/woodpecker-agent:${WOODPECKER_VERSION} AS agent
FROM busybox:musl AS busybox
FROM woodpeckerci/woodpecker-server:${WOODPECKER_VERSION}
COPY --from=agent /bin/woodpecker-agent /bin/woodpecker-agent
COPY --from=busybox /bin/busybox /busybox/busybox
COPY entrypoint.sh /cloud-entrypoint.sh
RUN ["/busybox/busybox", "--install", "-s", "/busybox"] # as root
ENV PATH="/busybox:$PATH"
ENTRYPOINT ["/busybox/busybox", "sh", "/cloud-entrypoint.sh"]
CMD ["/bin/woodpecker-server"]
Why co-located, not a separate additional_services sidecar. GKE's
generic additional_services mechanism (used elsewhere in this catalogue
for genuinely independent sidecar containers) deploys a sidecar as its own
Deployment under the namespace's default ServiceAccount — not the main
app's own Kubernetes ServiceAccount. The agent's Kubernetes execution
backend must run under the SAME identity that Woodpecker_GKE's namespaced
RBAC Role/RoleBinding actually grants pod/PVC/Service/Secret
create+delete permissions to — that identity is the main app's own KSA, only
reachable by running the agent inside the primary Deployment's own pod.
Why the grafted busybox:musl, specifically. Confirmed via docker export of both the real server and agent images: the entire rootfs is just
the single binary plus /etc/passwd,group,hosts — genuinely distroless, no
shell at all. A shell-script entrypoint needs a grafted static shell. The
default busybox:stable tag is dynamically linked and fails with "no
such file or directory" in this libc-less rootfs; busybox:musl is
genuinely static (the same fix pattern already established for
Loki/Headscale elsewhere in this catalogue). Copying the raw busybox
binary is not sufficient on its own — its multi-call applet dispatch (sh,
awk, ...) only works when invoked under one of those applet names, so the
build also runs busybox --install -s /busybox (as root, before the image
drops to its non-root USER woodpecker) to create the symlinks
entrypoint.sh actually calls.
No :latest tag exists upstream, by design. Confirmed live: docker run woodpeckerci/woodpecker-server:latest just prints a tag-schema notice and
exits — a deliberate anti-accidental-major-upgrade measure, not a bug. The
Dockerfile's WOODPECKER_VERSION build ARG is deliberately not the
generic APP_VERSION the Foundation injects into every custom build's
build_args (which wins the merge and would force the tag to a nonexistent
latest) — Woodpecker_Common/main.tf's local.resolved_version maps
application_version = "latest" to a pinned v3.16.0 before it reaches the
build.
entrypoint.sh starts the agent as a background process, then execs the
server into the foreground so it remains PID 1 for signal handling:
if [ "${1:-}" = "/bin/woodpecker-server" ] || [ "${1:-}" = "woodpecker-server" ]; then
/bin/woodpecker-agent &
AGENT_PID=$!
trap 'kill "$AGENT_PID" 2>/dev/null || true' TERM INT
fi
exec "$@"
4. Database bootstrap — one job, no separate migration
Woodpecker requires PostgreSQL; this layer fixes database_type = "POSTGRES_15". On the first deployment a one-shot job (db-init,
postgres:15-alpine, 600s timeout, 3 retries) idempotently creates the
application role and database, grants privileges on the database and the
public schema, and pre-creates the uuid-ossp extension.
No separate migrate job runs. Confirmed live: Woodpecker's server auto-migrates its own schema on boot — "Initializing Schema" appears in the logs automatically the first time it starts against an empty database. This differs from some other apps in this catalogue (e.g. Saleor/Payload) that need a dedicated migration job to run before the server process can start successfully.
5. Forge configuration — required just to boot
Woodpecker's server hard-requires at least one git forge configured to boot at all. Confirmed live: omitting forge configuration produces a fatal exit ("forge not configured"), not a degraded empty page — unlike some other apps in this catalogue (e.g. Outline) that boot fine with zero auth providers configured and just show an empty login page.
Woodpecker_Common merges placeholder Gitea/Forgejo values into
environment_variables so the module deploys cleanly out of the box:
| Env var | Source | Purpose |
|---|---|---|
WOODPECKER_GITEA | fixed "true" | Enables the Gitea/Forgejo forge driver |
WOODPECKER_GITEA_URL | var.forge_url (default http://forgejo.example.internal) | Base URL of the Gitea/Forgejo instance |
WOODPECKER_GITEA_CLIENT | var.forge_client_id (default placeholder-client-id) | OAuth application client ID |
WOODPECKER_GITEA_SECRET | var.forge_client_secret (default placeholder-client-secret) | OAuth application client secret |
WOODPECKER_ADMIN | var.admin_username (default admin) | Forge username(s) granted Woodpecker admin rights on first login |
These are plain env vars, not Secret-Manager-backed — the same
deferred-setup pattern already established for this catalogue's Outline OIDC
placeholders. An operator must point forge_url at a real Gitea/Forgejo
instance (this catalogue's own Forgejo_GKE module works) and register a
real OAuth application there post-deploy. Until that's done, the server
runs healthy but pipeline triggers and forge login do not work.
6. Kubernetes execution backend
WOODPECKER_BACKEND = "kubernetes"
WOODPECKER_BACKEND_K8S_NAMESPACE = <the pod's own app-scoped namespace>
The Kubernetes backend is the only viable option in a GKE pod — confirmed
live: the agent's default "auto-detect" backend fails outright without an
explicit backend set, since there is no docker.sock and no privilege for
docker-in-docker. Pipeline pods run in the same namespace as the agent.
Woodpecker_Common itself receives only the tenant-scoped resource_prefix
as an input; Woodpecker_GKE's woodpecker.tf overrides
WOODPECKER_BACKEND_K8S_NAMESPACE to the pod's actual app-scoped namespace
before the config reaches the Foundation, matching the namespace the RBAC
Role it provisions actually grants permissions in. The RBAC Role and
RoleBinding resources themselves live in Woodpecker_GKE, not here — they
need their own provider "kubernetes" {} configuration
(Woodpecker_GKE/provider-auth.tf) that a Common module has no way to
supply, since App_GKE's own internal Kubernetes provider is private to
that module. See Woodpecker_GKE §3
for the full RBAC writeup, including the exact rule set (sourced from
Woodpecker's own official Helm chart, not guessed) and the
tenant-vs-app-scoped naming subtlety that determines the RoleBinding
subject.
7. Health probe behaviour
Both the startup and liveness probes issue an HTTP GET /healthz,
confirmed live to return 204 No Content, unauthenticated:
- Startup probe —
initial_delay = 30s,timeout = 10s,period = 10s,failure_threshold = 30. - Liveness probe —
initial_delay = 30s,timeout = 10s,period = 30s,failure_threshold = 3.
8. Ports
8000— HTTP. The real listen port of the Woodpecker server, confirmed viadocker inspectof the realv3-tagged image. This is whatcontainer_portand the Kubernetes Service target.9000— gRPC. The agent's internal connection to the co-located server, authenticated withWOODPECKER_AGENT_SECRET. Never exposed via the Kubernetes Service — both processes share one pod's network namespace.
For the Woodpecker-specific, user-facing configuration (variables by group,
outputs, and how to explore each service from the Console and CLI), see the
platform guide: Woodpecker_GKE. There is no
Woodpecker_CloudRun — see the note at the top of this guide for why the
Kubernetes execution backend makes that permanently impossible.