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 storageNone — storage_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 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 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.

Need RAD to do something it does not do yet? Request it on the roadmap, or vote on what is already there.