| .. | ||
| secrets | ||
| ingress-hosts.patch.yaml | ||
| issuer-email.patch.yaml | ||
| kustomization.yaml | ||
| README.md | ||
| storage-class.patch.yaml | ||
Hetzner single-node overlay
This overlay turns the shared platform and unrip project bases into a concrete first-node bootstrap target for the Terraform-provisioned k3s VM.
The checked-in overlay is the declarative template. For first-cluster bootstrap, scripts/hetzner/bootstrap.sh renders a generated overlay under .state/hetzner/generated-overlay/ and applies that generated copy as the source of truth for the run.
Two ways to use this overlay
1. Recommended: scripts/hetzner/bootstrap.sh
This is the intended operator workflow for a fresh Hetzner cluster. The bootstrap script renders secret and patch inputs from local env and pass, creates imperative registry secrets, and applies a generated Kustomize overlay.
That generated overlay now imports the platform resources from deploy/k8s/platform/base/kustomization.yaml, so new checked-in platform components such as observability manifests are included automatically during bootstrap instead of being silently skipped by a hard-coded file list.
Bootstrap overwrites these operator-worktree files on each run:
deploy/k8s/overlays/hetzner-single-node/secrets/unrip.envdeploy/k8s/overlays/hetzner-single-node/secrets/forgejo.envdeploy/k8s/overlays/hetzner-single-node/secrets/observability.env
Bootstrap also renders and applies generated copies of these patch files under .state/hetzner/generated-overlay/ instead of modifying the checked-in overlay files directly:
ingress-hosts.patch.yamlissuer-email.patch.yamlstorage-class.patch.yaml
Secret/config sources when using bootstrap:
- from
passor direct env overrides viascripts/hetzner/bootstrap-secrets.env:HCLOUD_TOKENTAILSCALE_AUTH_KEYCLOUDFLARE_API_TOKENCLOUDFLARE_ZONE_IDPORKBUN_API_KEYPORKBUN_SECRET_API_KEYREGISTRY_PASSWORDNEAR_INTENTS_API_KEYFORGEJO_ADMIN_PASSWORD- optional
GRAFANA_ADMIN_PASSWORD(bootstrap generates one if omitted)
- from plain env/non-secret config in
scripts/hetzner/bootstrap-secrets.env:PUBLIC_DOMAIN,BASE_DOMAIN,FORGEJO_DOMAIN,FORGEJO_ROOT_URL,REGISTRY_DOMAIN,GRAFANA_DOMAIN,GRAFANA_ROOT_URL,HEADLAMP_DOMAIN- default hostname model under
PUBLIC_DOMAIN:git.${PUBLIC_DOMAIN},registry.${PUBLIC_DOMAIN},grafana.${PUBLIC_DOMAIN},headlamp.${PUBLIC_DOMAIN} LETSENCRYPT_EMAILREGISTRY_USERNAMEFORGEJO_ADMIN_USERNAME,FORGEJO_ADMIN_EMAIL- optional
GRAFANA_ADMIN_USERNAME(defaults toadmin) - optional project overrides such as
PROJECT_NAME,PROJECT_NAMESPACE, andPROJECT_SECRET_ENV_BASENAME
Bootstrap materializes Kubernetes inputs like this:
secrets/unrip.envgetsNEAR_INTENTS_API_KEYsecrets/forgejo.envgets onlyroot_urlanddomainsecrets/observability.envgetsgrafana_admin_user,grafana_admin_password, andgrafana_root_url- generated overlay Kustomize secret generators create
observability-secretsin namespaceobservabilityalongside the project and Forgejo secrets registry-secretsin namespaceregistryis created imperatively fromREGISTRY_USERNAMEandREGISTRY_PASSWORD<project>-registry-credsimage pull secret is created imperatively in the project namespace from the same registry credentials
Note: the Forgejo runner no longer reads runner_registration_token from forgejo-secrets. scripts/hetzner/bootstrap.sh generates a one-time runner token in-cluster, registers the runner, and writes /data/forgejo-runner/.runner on the shared Forgejo PVC before restarting the runner deployment.
2. Manual: kubectl apply -k
Use this only if you intentionally want to manage the checked-in overlay inputs yourself. In manual mode, the checked-in overlay remains the source of truth; in bootstrap mode, the generated overlay is the source of truth for what gets applied.
Before apply, create or edit real local input files:
cp deploy/k8s/overlays/hetzner-single-node/secrets/unrip.env.example deploy/k8s/overlays/hetzner-single-node/secrets/unrip.env
cp deploy/k8s/overlays/hetzner-single-node/secrets/forgejo.env.example deploy/k8s/overlays/hetzner-single-node/secrets/forgejo.env
cp deploy/k8s/overlays/hetzner-single-node/secrets/observability.env.example deploy/k8s/overlays/hetzner-single-node/secrets/observability.env
cp deploy/k8s/overlays/hetzner-single-node/secrets/registry.htpasswd.example deploy/k8s/overlays/hetzner-single-node/secrets/registry.htpasswd
Then update:
- ingress hosts in
ingress-hosts.patch.yamlfor Forgejo, Registry, Grafana, and Headlamp - ACME email in
issuer-email.patch.yaml - project secret values in
secrets/unrip.env - Forgejo secret values in
secrets/forgejo.env(root_urlanddomainonly) - observability secret values in
secrets/observability.env(grafana_admin_user,grafana_admin_password,grafana_root_url)
Important manual-mode caveat:
kubectl apply -k deploy/k8s/overlays/hetzner-single-nodecreates only the Kustomize-managed secrets from the checked-in files (unrip-secrets,forgejo-secrets,observability-secrets, andregistry-secretswhensecrets/registry.htpasswdexists)- it does not create the project docker-registry pull secret
- if you skip
scripts/hetzner/bootstrap.sh, you must create that pull secret separately before expecting image pulls or CI builds to work
Apply
Bootstrap path:
bash scripts/hetzner/bootstrap.sh
Manual path:
kubectl apply -k deploy/k8s/overlays/hetzner-single-node
What gets installed
- shared platform namespaces for registry, ingress, cert-manager, Forgejo, and observability
- project namespace
unrip - Redpanda plus a topic bootstrap job inside
unrip - app worker deployments referencing
unrip-secrets - Forgejo and Forgejo runner referencing
forgejo-secrets - private registry workload, which still requires the imperative
registry-secretsauth secret to be created separately unless you usedscripts/hetzner/bootstrap.sh - nginx ingress and ACME issuers for TLS
- observability ingress for Grafana and Headlamp, plus local-path PVC overrides for Grafana and Loki
Observability UI exposure policy
- Grafana and Headlamp are both wired into the Hetzner ingress/domain model.
- Use
grafana.${PUBLIC_DOMAIN}/headlamp.${PUBLIC_DOMAIN}or explicitGRAFANA_DOMAIN/HEADLAMP_DOMAINvalues. - Grafana is the historical log search UI backed by Loki.
- Headlamp is the Kubernetes cluster UI for workloads, events, and pod logs.
- Grafana is authenticated through
observability-secrets; Headlamp is authenticated with the generated Kubernetes service-account token that bootstrap stores inpasswhenHEADLAMP_ADMIN_TOKEN_PASSis configured.
For future projects, do not reuse unrip; create a new project namespace and matching <project>-config, <project>-secrets, and <project>-registry-creds resources.