Skip to main content

Woodpecker CI on GKE Autopilot

Woodpecker CI on GKE Autopilot

Woodpecker CI is a lightweight, container-native CI/CD engine — a simpler, self-hostable alternative to Drone. Pipelines are defined as YAML files, and each pipeline step runs in its own container. Woodpecker supports GitHub, Gitea, Forgejo, GitLab, and Bitbucket as "forges" — the term Woodpecker uses for the connected git host. This module deploys Woodpecker on GKE Autopilot on top of the App_GKE foundation, which provisions and manages the shared Google Cloud and Kubernetes infrastructure.

There is no Woodpecker_CloudRun, and there will not be one. Woodpecker's execution backend (WOODPECKER_BACKEND=kubernetes) needs real Kubernetes API access to dynamically create a pod for every pipeline step — Cloud Run has no privilege for docker-in-docker and no Kubernetes API to call. See Woodpecker_Common for the full writeup; 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).

This guide focuses on the cloud services Woodpecker uses and how to explore and operate them from the Google Cloud Console and the command line. For the mechanics common to every GKE application — Workload Identity, ingress, autoscaling, CI/CD, Cloud Armor, IAP, Binary Authorization, VPC Service Controls, and the deployment lifecycle — refer to the App_GKE foundation guide rather than repeating them here.


1. Overview

Woodpecker runs as a single pod that co-locates the server and the agent in one container, backed by Cloud SQL PostgreSQL:

CapabilityGoogle Cloud serviceNotes
ComputeGKE AutopilotOne pod runs both the server (HTTP API + web UI) and the agent (pipeline-step executor), 2 vCPU / 4Gi by default
DatabaseCloud SQL for PostgreSQL 15Server auto-migrates its own schema on boot; no separate migrate job
Pipeline executionGKE Autopilot, via a namespace-scoped RBAC RoleThe agent dynamically creates a Kubernetes Pod (plus PVCs/Services/Secrets as needed) for every pipeline step, in the SAME namespace the agent itself runs in
SecretsSecret ManagerOne secret: WOODPECKER_AGENT_SECRET, authenticating the co-located server↔agent gRPC connection
Forge (git host)none provisioned — externalPlaceholder Gitea/Forgejo values by default; point at a real instance post-deploy
IngressCloud Load BalancingLoadBalancer by default; the reference deployment used ClusterIP due to exhausted external-IP quota (see §6)

Sensible defaults worth knowing up front:

  • Server and agent are co-located in one pod, not two. Upstream Woodpecker ships the server and agent as two separate images (docker-compose runs them as two containers). This module grafts the agent binary onto the server image and starts it as a background process before exec-ing the server — necessary because the agent's Kubernetes backend must run under the SAME Kubernetes ServiceAccount that holds the elevated RBAC this module grants, and GKE's generic additional_services mechanism does not run sidecar Deployments under the main app's own ServiceAccount (only the primary Deployment does).
  • The agent needs elevated in-cluster RBAC. Woodpecker_GKE's woodpecker.tf provisions a namespace-scoped kubernetes_role_v1 + kubernetes_role_binding_v1 directly (no App_GKE foundation change was needed). See §3 for the full detail.
  • 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. application_version = "latest" resolves internally to a pinned v3.16.0.
  • Both upstream images are genuinely distroless. Confirmed via docker export: the entire rootfs is the single binary plus /etc/passwd,group, hosts — no shell at all. The custom image grafts a static busybox:musl binary (the default busybox:stable tag is dynamically linked and fails in this libc-less rootfs) so the cloud entrypoint can run.
  • The server hard-requires a forge to boot at all. Confirmed live: omitting forge configuration is a fatal exit ("forge not configured"), not a degraded empty page. This module defaults to placeholder Gitea/Forgejo values so it deploys cleanly; real pipeline triggers need a real forge registered post-deploy (§6).
  • Single instance, non-negotiable. max_instance_count is hard-capped at 1 by a plan-time validation — each pod runs a co-located server+agent, and Woodpecker's server has no documented/verified multi-instance coordination for its own database-backed state.
  • Health endpoint: GET /healthz, confirmed live to return 204 No Content, unauthenticated.

2. Google Cloud Services & How to Explore Them

All commands assume you have run gcloud container clusters get-credentials <cluster> --region <region> --project <project> and that PROJECT, REGION, and NAMESPACE are set. The namespace and other identifiers are reported in the deployment Outputs.

A. GKE Autopilot — the Woodpecker workload

A single pod runs both the server and the agent (Deployment by default).

  • Console: Kubernetes Engine → Workloads → select the Woodpecker workload to see the pod and events.
  • CLI:
    kubectl get deployment,pods,svc -n "$NAMESPACE"
    kubectl logs -n "$NAMESPACE" deployment/<service-name> --tail=100

See App_GKE for how Autopilot scheduling and Workload Identity work.

B. Cloud SQL — PostgreSQL 15

gcloud sql instances list --project "$PROJECT"
gcloud sql databases list --instance=<instance-name> --project "$PROJECT"

No separate migrate job runs — confirmed live: Woodpecker's server auto-migrates its own schema on boot ("Initializing Schema" appears automatically against an empty database). Only a db-init job (role + database creation) runs on first deploy.

C. Secret Manager — one secret

gcloud secrets list --project "$PROJECT" --filter="name~agent-secret"
gcloud secrets versions access latest --project "$PROJECT" --secret=<secret-name>

WOODPECKER_AGENT_SECRET authenticates the co-located server's and agent's internal gRPC connection to each other. See Woodpecker_Common §2 for detail.

D. RBAC — the agent's pipeline-execution permissions

kubectl get role,rolebinding -n "$NAMESPACE"
kubectl describe role <resource-prefix> -n "$NAMESPACE"

A namespace-scoped Role grants {persistentvolumeclaims, services, secrets: create, delete}, {pods: watch, create, delete, get, list}, and {pods/log: get} — sourced from Woodpecker's own official Helm chart RBAC template, not guessed. The RoleBinding subject is the pod's own Kubernetes ServiceAccount, bound by name. See Woodpecker_Common §6 and §3 below for the full mechanism.

E. Networking & ingress

kubectl get svc -n "$NAMESPACE"
gcloud compute addresses list --project "$PROJECT"

service_type defaults to LoadBalancer (external), matching the Foundation default. Forge webhooks need to reach this server from the internet for pipelines to trigger automatically — see §6 for the reference deployment's ClusterIP deviation and when to flip it back.

F. Cloud Logging & Monitoring

Pod stdout/stderr flows to Cloud Logging; GKE metrics flow to Cloud Monitoring.

gcloud logging read 'resource.type="k8s_container" AND resource.labels.namespace_name="'"$NAMESPACE"'"' \
--project "$PROJECT" --limit 50

An uptime_check_config is available, disabled by default, targeting /healthz.


3. Woodpecker Application Behaviour

  • Co-located server + agent, one container. entrypoint.sh starts the agent as a background process, then execs the server into the foreground:
    if [ "${1:-}" = "/bin/woodpecker-server" ]; then
    /bin/woodpecker-agent &
    fi
    exec "$@"
    This is necessary because GKE's generic additional_services mechanism runs a sidecar Deployment under the namespace's default ServiceAccount, not the main app's own Kubernetes ServiceAccount — and the agent's Kubernetes backend must run as the identity the RBAC Role/RoleBinding below actually grants permissions to.
  • RBAC, bound by ServiceAccount name only. Confirmed live via kubectl get deployment -o jsonpath='{.spec.template.spec.serviceAccountName}': App_GKE names the pod's KSA after the tenant-scoped resource prefix (e.g. gkee6a1e84d) even though the KSA lives inside the app-scoped namespace (e.g. woodpeckergkee6a1e84d). The RoleBinding subject uses that tenant-scoped name. This required its own provider "kubernetes" {} block (Woodpecker_GKE/provider-auth.tf) — App_GKE's own internal Kubernetes provider configuration is private to that module and isn't inherited by the calling Application module.
  • Pipeline pods run in the agent's own namespace. WOODPECKER_BACKEND_K8S_NAMESPACE is set to the pod's actual app-scoped namespace, so the RBAC grant is a namespaced Role, not a cluster-wide ClusterRole.
  • Forge configuration is required to boot, not just to log in. Confirmed live: the server exits fatally ("forge not configured") if no forge is set — this is unlike some other apps in this catalogue (e.g. Outline) that boot fine with zero auth providers and just show an empty login page. Placeholder Gitea/Forgejo values (WOODPECKER_GITEA_URL, WOODPECKER_GITEA_CLIENT, WOODPECKER_GITEA_SECRET — plain env vars, not Secret-Manager-backed) let the module deploy cleanly out of the box; an operator must swap them for a real registered OAuth application on an actual Gitea/Forgejo instance post-deploy.
  • Health path. Both probes target GET /healthz, confirmed live to return 204 No Content, unauthenticated.
  • Ports. 8000 is the real HTTP port (confirmed via docker inspect); 9000 is the agent's internal gRPC connection to the server, never exposed via the Kubernetes Service since both processes share one pod.
  • Single-writer, single-instance. max_instance_count is hard-capped at 1 at plan time — Woodpecker's server has no documented/verified multi-instance coordination for its own database-backed state.
  • Updates recreate the pod. A version bump rebuilds the custom image (server + agent + busybox, re-grafted) and recreates the single pod.

4. Configuration Variables

Variables are grouped exactly as they appear on the deployment platform. Only settings specific to or notable for Woodpecker are listed; every other input is inherited from App_GKE with its standard behaviour and defaults. See modules/Woodpecker_GKE/README.md for the exhaustive, group-by-group input reference.

Group 1 — Project & Identity

VariableDefaultDescription
project_id(required)Target Google Cloud project.
regionus-central1Region for the workload and regional resources.

Group 2 — Deployment Environment

VariableDefaultDescription
tenant_deployment_iddemoShort suffix that makes resource names unique per environment.
support_users[]Emails granted project access and monitoring alerts.

Group 3 — Application Identity

VariableDefaultDescription
application_namewoodpeckerBase name for resources. Do not change after first deploy.
application_versionlatestResolves internally to a pinned v3.16.0 — Woodpecker publishes no latest tag.
deploy_applicationtrueSet false to provision infrastructure only.
display_name / description(stale, see §7)Left over from this module's Immich clone source — do not trust the shipped text; the values describe photo/video management, not Woodpecker CI.

Group 4 — Runtime & Scaling

VariableDefaultDescription
cpu_limit2000mCPU limit for the co-located server+agent container.
memory_limit4GiMemory limit for the co-located server+agent container.
container_port8000Confirmed via docker inspect of the real v3-tagged image.
min_instance_count1
max_instance_count1Hard-capped by plan-time validation — see §3.
enable_cloudsql_volumetrueCloud SQL Auth Proxy Unix socket sidecar.
enable_image_mirroringtrueMirror the built image into Artifact Registry.
forge_urlhttp://forgejo.example.internalPlaceholder. Point at a real Gitea/Forgejo instance post-deploy — see §6.
forge_client_id / forge_client_secretplaceholder-client-id / placeholder-client-secretPlaceholder OAuth application credentials.
admin_usernameadminForge username(s) granted Woodpecker admin rights on first login.

Group 5 — Environment Variables & Secrets

VariableDefaultDescription
environment_variables{}Extra plain-text settings, merged over the module's forge/backend defaults.
secret_environment_variables{}Additional Secret Manager references injected as env vars.

Group 6 — GKE Backend & Cluster

VariableDefaultDescription
service_typeLoadBalancerThe reference deployment used ClusterIP due to exhausted quota — see §6.
workload_typenullAlways resolves to Deployment for this module (no stateful_pvc_enabled use case).
termination_grace_period_seconds60Seconds after SIGTERM before SIGKILL.

Group 7 — StatefulSet

Not used by this module — Woodpecker's state lives entirely in Cloud SQL, and there is no per-pod filesystem state to persist via a PVC.

Group 10 — Observability & Health

VariableDefaultDescription
startup_probe / liveness_probeHTTP /healthzConfirmed live: 204 No Content, unauthenticated.
uptime_check_configdisabledIf enabled, targets /healthz.

Group 11 — Jobs & Scheduled Tasks

VariableDefaultDescription
initialization_jobs[]Empty default resolves to Woodpecker_Common's single db-init job — Woodpecker migrates its own schema on boot, so no separate migrate job exists.
cron_jobs[]Kubernetes CronJobs.

Group 13 — Filesystem (NFS)

VariableDefaultDescription
enable_nfstrueStale default and description, left over from this module's Immich clone source — Woodpecker CI has no media library and no functional use for NFS. See §7.

Group 14 — Cloud Storage & Artifact Registry

VariableDefaultDescription
create_cloud_storagetrueCreates the default data bucket — unused by Woodpecker.

Group 16 — Database

VariableDefaultDescription
db_namewoodpecker_db
db_userwoodpecker_user
database_typefixed POSTGRES_15 by Woodpecker_CommonInert mirror on this module's own database_type variable.

Group 19 — Custom Domain, Static IP & Networking

VariableDefaultDescription
enable_custom_domaintrueProvision Gateway for a custom hostname.
reserve_static_iptrueThe reference deployment used false due to exhausted quota — see §6.

Group 20 — Identity-Aware Proxy (IAP)

VariableDefaultDescription
enable_iapfalseRequires custom domain and both OAuth credentials, validated at plan time.

Group 21 — Redis & Cloud Armor

VariableDefaultDescription
enable_redistrueNot confirmed to be functionally requiredWoodpecker_Common's entrypoint never reads REDIS_HOST/REDIS_PORT. See §7.

Groups 8, 9, 12, 17, 18, 22

Standard App_GKE behaviour — Resource Quota, Reliability Policies, CI/CD & Binary Authorization, Backup & Maintenance, Custom SQL (not applicable), VPC Service Controls. See App_GKE.


5. Outputs

These values are returned on a successful deployment and are the quickest way to locate and explore the running resources.

OutputDescription
service_nameKubernetes Service name.
namespaceNamespace the workload runs in.
service_cluster_ip / service_external_ipClusterIP / external LoadBalancer IP (when reserved).
web_urlReads additional_service_urls["web"]resolves null on a stock deployment, since no "web" additional service is wired by default.
database_instance_name / database_name / database_user / database_password_secret / database_host / database_portCloud SQL identity and connection details.
storage_bucketsCreated Cloud Storage buckets (the default data bucket — unused).
network_name / network_exists / regionsVPC network, presence, available regions.
container_image / container_registryDeployed image and Artifact Registry repo.
initialization_jobsCreated initialization job names (the default db-init).
deployment_id / tenant_id / resource_prefixNaming identifiers.
project_id / project_numberProject identifiers.
kubernetes_readyWhether the cluster/workload is ready.
vpc_sc_enabled / vpc_sc_perimeter_name / vpc_sc_dry_run_modeVPC-SC status.
audit_logging_enabled / artifact_registry_cmek_enabledAudit logging and CMEK status.

6. Configuration Pitfalls & Sensible Defaults

Risk: Critical (data loss / outage / security) — High (service degraded) — Medium (cost or partial degradation) — Low (minor).

Inherited plan-time validation. This module passes its configuration through the App_GKE foundation engine, which validates values and combinations at plan time. Woodpecker_GKE's own validation.tf additionally rejects max_instance_count > 1 and enable_iap = true without both OAuth credentials.

SettingSensible valueRiskConsequence if wrong
Forge configuration (forge_url/forge_client_id/forge_client_secret)Replace all three with a real registered OAuth application on a real Gitea/Forgejo instance post-deployHighThe server boots and reports healthy with the placeholder values, but pipelines never trigger and forge login never works — a working-looking deployment that is not actually usable for CI.
service_type / reserve_static_ipLoadBalancer / true once external IP quota allowsHighThe reference deployment used ClusterIP / false purely because the test project's IN_USE_ADDRESSES quota was exhausted. Forge webhooks (push/PR events) need to reach this server from the internet — a ClusterIP deployment cannot receive them and pipelines will not auto-trigger.
max_instance_count1 (enforced at plan time)CriticalEach pod runs a co-located server+agent; more than one replica would run multiple servers against the same database with no verified coordination.
Health probesLeave as HTTP /healthz (module default)Medium/healthz is confirmed unauthenticated and returns 204; pointing a probe at an authenticated endpoint would wedge the rollout.
enable_nfsLeave default or verify against actual need — description text is misleadingLowDescription references a "photo/video library" that doesn't exist in Woodpecker CI; if true, an unused NFS mount is provisioned into the pod at an Immich-derived path with no functional effect, but it is not free — see Notes in modules/Woodpecker_GKE/README.md.
enable_redisLeave default; not confirmed requiredLowDescription claims Redis is "REQUIRED", but Woodpecker_Common's entrypoint never reads REDIS_HOST/REDIS_PORT — this appears to be inert leftover from the module's clone source, not verified Woodpecker CI behaviour.

For the foundation behaviour referenced throughout — IAM and Workload Identity, autoscaling, ingress and certificates, CI/CD, Cloud Armor, IAP, Binary Authorization, VPC-SC, and image mirroring — see App_GKE. Woodpecker-specific application configuration is described in Woodpecker_Common.