3.9 KiB
3.9 KiB
Hetzner + k3s + self-hosted Git/CI bootstrap
Goal: provision and deploy everything from this repo to a single Hetzner machine with no manual server login.
Stack
- Terraform provisions the Hetzner Cloud VM, private network, and firewall
- cloud-init installs Tailscale first when configured, then installs k3s automatically
- Kubernetes manifests deploy:
- Redpanda
- trading system services
- private registry
- Forgejo
- ingress-nginx
- cert-manager
- ACME issuers
- local bootstrap script:
- runs Terraform
- optionally creates DNS records via Cloudflare or Porkbun
- writes overlay secrets/host patches from local env
- applies the Hetzner single-node k8s overlay
- builds the current app image locally
- fetches the real kubeconfig from the node
- imports the bootstrap image into k3s for the first rollout
Files
infra/terraform/hetzner/deploy/k8s/base/deploy/k8s/overlays/hetzner-single-node/scripts/hetzner/bootstrap.shscripts/hetzner/configure-cloudflare-dns.shscripts/hetzner/destroy.shscripts/k8s/logs.sh.forgejo/workflows/deploy.yml
Required local tools
terraformkubectldockercurlpython3
Required local env
Start from:
cp scripts/hetzner/bootstrap-secrets.env.example scripts/hetzner/bootstrap-secrets.env
source scripts/hetzner/bootstrap-secrets.env
Required values:
HCLOUD_TOKENSSH_PUBLIC_KEY_PATHPUBLIC_DOMAINBASE_DOMAIN- recommended Tailscale values:
TAILSCALE_AUTH_KEYTAILSCALE_CONTROL_PLANE_HOSTNAME
FORGEJO_DOMAINFORGEJO_ROOT_URLREGISTRY_DOMAINLETSENCRYPT_EMAILREGISTRY_USERNAMEREGISTRY_PASSWORDNEAR_INTENTS_API_KEYFORGEJO_RUNNER_REGISTRATION_TOKEN
Optional for automatic DNS:
- Cloudflare:
CLOUDFLARE_API_TOKENCLOUDFLARE_ZONE_ID
- Porkbun:
PORKBUN_API_KEYPORKBUN_SECRET_API_KEY
Bootstrap
bash scripts/hetzner/bootstrap.sh
Outputs:
- Hetzner VM created
- Tailscale joined if configured
- k3s installed
- kubeconfig written to
.state/hetzner/kubeconfig.yaml - overlay secrets and ingress host patches rendered from local env
- namespaces, Redpanda, app deployments, Forgejo, registry, ingress, cert-manager, and issuers applied
- bootstrap image built and first rollout triggered
Tailscale-first admin access
Recommended mode:
- public firewall exposes only
80/443 - admin access uses Tailscale
- Kubernetes API uses the Tailscale hostname when
TAILSCALE_CONTROL_PLANE_HOSTNAMEis set
TF_ADMIN_CIDR_BLOCKS remains only as a fallback if you intentionally want public admin/API exposure.
DNS and TLS
If DNS provider credentials are present, bootstrap updates:
${BASE_DOMAIN}git.${BASE_DOMAIN}registry.${BASE_DOMAIN}
Supported scripted providers:
- Cloudflare
- Porkbun
TLS is handled in-cluster by cert-manager using Let's Encrypt issuers and the rendered ingress hosts.
Observe the cluster
KUBECONFIG=.state/hetzner/kubeconfig.yaml kubectl get pods -A
bash scripts/k8s/logs.sh
Self-hosted CI/CD handoff
After bootstrap:
- open Forgejo at
https://${FORGEJO_DOMAIN} - seed or mirror this repo into Forgejo
- add Forgejo Actions secrets:
KUBECONFIG_B64REGISTRY_USERNAMEREGISTRY_PASSWORD
- add Forgejo Actions variable:
REGISTRY_HOST=${REGISTRY_DOMAIN}
- push to
main
The workflow then:
- builds the image
- pushes it to
https://${REGISTRY_DOMAIN} - updates the app deployments in
unrip - waits for rollout
Destroy everything
bash scripts/hetzner/destroy.sh
Current limitations
- Forgejo admin bootstrap and repo seeding are still operator-driven after the first cluster bootstrap.
- bootstrap and CI authentication paths should still be hardened before production use.
- routine deploys are intended to be registry-native through Forgejo Actions, but that still needs a real-world verification pass.