Skip to main content

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

AreaProvided by Woodpecker_CommonWhere it surfaces
Container imageCustom 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 ARGcontainer_image output of the platform deployment
Version resolutionapplication_version = "latest" resolves internally to a pinned v3.16.0 — Woodpecker publishes no latest tag upstreamImage tag actually deployed
DatastoreFixes database_type = "POSTGRES_15"§3 in the platform guide
Database bootstrapThe first-deploy db-init job — role, database, grants. No separate migrate job: Woodpecker auto-migrates its own schema on server bootinitialization_jobs output
Co-located agentThe agent binary runs as a background process inside the same container/pod as the server§Application Behaviour in the platform guide
Forge placeholdersMerges placeholder Gitea/Forgejo values into environment_variables so the server can boot at all§Application Behaviour in the platform guide
SecretsGenerates WOODPECKER_AGENT_SECRET in Secret Manager — the co-located server↔agent internal gRPC credentialInjected automatically; retrieve via Secret Manager
Health checksDefault startup/liveness probes targeting HTTP GET /healthz (unauthenticated, confirmed 204 No Content)§Observability in the platform guide
Object storageNonestorage_buckets = []. Woodpecker CI has no object-storage dependencyNo 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:

SecretEnv varContentRotation
secret-<prefix>-<app>-agent-secretWOODPECKER_AGENT_SECRET40-character random password, no special charactersA 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 varSourcePurpose
WOODPECKER_GITEAfixed "true"Enables the Gitea/Forgejo forge driver
WOODPECKER_GITEA_URLvar.forge_url (default http://forgejo.example.internal)Base URL of the Gitea/Forgejo instance
WOODPECKER_GITEA_CLIENTvar.forge_client_id (default placeholder-client-id)OAuth application client ID
WOODPECKER_GITEA_SECRETvar.forge_client_secret (default placeholder-client-secret)OAuth application client secret
WOODPECKER_ADMINvar.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 probeinitial_delay = 30s, timeout = 10s, period = 10s, failure_threshold = 30.
  • Liveness probeinitial_delay = 30s, timeout = 10s, period = 30s, failure_threshold = 3.

8. Ports

  • 8000 — HTTP. The real listen port of the Woodpecker server, confirmed via docker inspect of the real v3-tagged image. This is what container_port and the Kubernetes Service target.
  • 9000 — gRPC. The agent's internal connection to the co-located server, authenticated with WOODPECKER_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.