Skip to content

Your own Gardener

paasbox garden up turns one Hetzner Cloud project into a working Gardener landscape in about 40 minutes: a k3s runtime cluster (Gardener calls it the soil), Gardener’s virtual garden, a seed on the same cluster, and the Gardener Dashboard. From there you create clusters, which Gardener calls shoots, from the dashboard, with kubectl or with the CLI, and Gardener runs them: control planes as pods on the seed, workers as Hetzner servers, upgrades, backups, certificates, DNS.

It is not a wrapper around a hosted service. Everything runs in your project, on your token, and paasbox garden teardown removes it all again. The source is on GitHub under Apache-2.0.

  1. A Hetzner Cloud project and a read/write API token. This is the one precondition nothing can remove. Everything else the CLI generates or does without.
  2. A few tools on your machine: kubectl, helm, hcloud, flux, git, ssh and Python 3.11 or newer. paasbox doctor names each missing one with the install line for macOS and Debian or Ubuntu.
  3. Optionally a DNS zone, an S3 bucket and an OIDC issuer. None of them on day one.
Terminal window
curl -fsSL https://raw.githubusercontent.com/paasbox-cloud/paasbox-cli/main/install.sh | sh
paasbox doctor # what is missing, and how to install it
paasbox garden init my-garden # writes my-garden/values.yaml
$EDITOR my-garden/values.yaml # name, location, DNS mode, backup switch
export HCLOUD_TOKEN=# the only secret the simplest setup needs
paasbox garden up my-garden # check → up → flux → garden → images → status

values.yaml is the whole configuration:

profile: solo # solo | ha
name: my-garden
location: nbg1 # nbg1 | fsn1 | hel1
os: debian-13 # debian-13 | ubuntu-26.04
dns:
mode: my-paasbox # my-paasbox | zone
backup:
enabled: false
dashboard: true
oidc:
mode: "off"
versions: {} # empty = the release's pins

Every command writes its stage log to my-garden/out/<stage>.log and prints one line per stage. When up finishes you have a kubeconfig for the garden and an example shoot to apply:

Terminal window
export KUBECONFIG=my-garden/out/my-garden.virtual-garden.kubeconfig
kubectl apply -f my-garden/examples/first-shoot.yaml

Or use the CLI for the same thing:

Terminal window
paasbox garden shoot create my-garden first --machine cpx32 --k8s 1.36
paasbox garden shoot list my-garden
paasbox garden shoot kubeconfig my-garden first --out first.kubeconfig
paasbox garden shoot delete my-garden first

A shoot’s admin credential is not an object you can read. The garden mints it on request and it is short-lived by design, so ask again when it expires. The first shoot is ready about nine minutes after you create it.

Gardener needs names: for the garden API, for the dashboard, for the seed’s ingress, and for every shoot’s API server. There are three ways to get them.

dns.modeWhat it costs youWhat you getWhen
sslipnothingthe garden, the dashboard and the seed ingress work, but shoots get no external DNS because there is no zone to write records intotrying it out (designed, not built yet; up refuses it today)
my-paasboxa paasbox account and one sign-in in your browser<team>.paasbox.app, free, with TXT records so Let’s Encrypt worksyou have no domain and want a real landscape
zonea domain, about €10 a year, and a Hetzner DNS zone with its token in $DNS_TOKENyour own names, shared with nobodyanything that outlives a trial

Switching later is two lines in values.yaml and another paasbox garden up.

dns.mode: my-paasbox needs a token for your team’s names. There is nothing to copy out of a web page: the CLI asks for it itself, and paasbox garden up starts the sign-in the first time it needs one.

$ paasbox garden up my-garden
dns.mode my-paasbox and no $DNS_TOKEN — signing in to console.paasbox.com
open https://console.paasbox.com/device (opening your browser…)
code FTQK-2X7M (expires in 10 minutes)
waiting… ✓ signed in — team Acme GmbH → acme.paasbox.app
### check — preflight …

It is the OAuth 2.0 device grant, the flow gh auth login and docker login use, so it works over SSH and on a machine with no browser: --no-browser prints the URL and you approve it on your phone. The portal’s address is compiled in and never taken from a reply; an answer that points anywhere else aborts the sign-in.

Terminal window
paasbox auth login # get a team token (--team acme, --no-browser)
paasbox auth status # who you are, checked against the portal
paasbox auth token # just the token, for a script
paasbox auth logout # forget it here; revoking is done in the portal

The credential lives in ~/.config/paasbox/credentials.json with mode 0600 and is never written into a landscape directory. A token you export yourself, or keep in the landscape’s .env, always wins. sslip and zone never talk to the portal at all.

The team token can write records under your team’s name and nothing else. The zone apex and every other team’s names are refused, and reads are filtered so the rest of the zone is invisible to you.

ProfileServersFitsList price, net
solo1 × cpx62 (16 vCPU, 32 GB)garden, seed and a few shootsabout €130 a month
ha3 × cpx42 (8 vCPU, 16 GB)garden, seed and about twelve shoots; survives one nodeabout €208 a month

Check Hetzner’s current price list; these are list prices from the day this was written, and every shoot adds its own workers. A 16 GB server cannot run a landscape at all: Gardener’s own monitoring requests about 8 GiB, and on a 16 GB node six pods stay pending for ever. That was measured, not estimated, and it is why solo is a 32 GB server.

Each shoot’s control plane needs about 1.5 to 2 GiB and half a vCPU on the seed while it idles; that is what “fits a few shoots” means. A cloud landscape also runs its ingress on two Hetzner load balancers, because Gardener runs two istio ingress gateways, the virtual garden’s and the seed’s. Budget about €12 net a month for them.

A server: block in values.yaml puts the landscape on a Hetzner dedicated server you already rent instead of a cloud server the CLI creates. The price argument in one sentence: a solo landscape needs 32 GB, which costs about €130 a month as a cloud server and starts near €60 a month as a 64 GB box from Hetzner’s Serverbörse.

server:
kind: robot # a Hetzner dedicated server (Robot); the only kind today
number: 1234567 # your Robot server number; the address is discovered, never configured
login: admin # the per-server admin login (Robot → the server → Rescue)
reimage: true # install the OS through the rescue system first. THE DISK IS WIPED, once

up then resolves the server number to an address, enters the rescue system and, with reimage: true, installs Debian 13 and waits for the machine to come back. A second up on a machine this landscape already installed reimages nothing and only verifies. A dedicated box has no cloud load balancer, so one haproxy on the node splits port 443 by server name between the two istio gateways and publishes both under one name. No Hetzner network, firewall, volume or load balancer is created at all.

Three things to know before you use it:

  • Which login to give it. The per-server admin login reaches this one server and nothing else, and is the one to prefer. It needs sshpass for a single first connection. A Robot webservice user is the alternative; it reaches every server on the account.
  • The box has no cloud firewall. Filter it yourself, on the machine or with Hetzner’s Robot firewall: 22 and 6443 from your own addresses, 80 and 443 from anywhere, 8443 and 8132 for the shoots’ connection back to the seed.
  • Nothing in paasbox can cancel a server. There is no such call anywhere in it. teardown removes the DNS records and leaves the machine untouched; teardown --wipe --yes reinstalls it. Ending the contract is something you do by hand in Robot.

You still need the Hetzner Cloud project and token: your shoots’ workers are cloud servers.

Ad-hoc is the default: paasbox garden init my-garden, and Flux on the cluster syncs the public kit read-only at the release tag. Your values travel to the cluster as a ConfigMap and a Secret. There is no repository to keep.

Durable is paasbox garden init my-garden --gitops: the same landscape plus a small GitOps repository of your own, with a README, values.yaml, and a Flux GitRepository and Kustomization that point at the public kit at a tag. You push it, Flux syncs it, and you add your own manifests next to it. When a new release lands, paasbox garden update re-renders what the template owns and leaves your edits alone. This replaces forking the kit.

Terminal window
paasbox garden status my-garden # the garden, the seed, the shoots, the backups
paasbox garden dashboard-token my-garden # a login token for the dashboard
paasbox garden images my-garden # the worker-image snapshots in your project
paasbox garden upgrade my-garden # the k3s version step, one server at a time
paasbox garden restore my-garden # in place, from the newest etcd snapshot in S3
paasbox garden update my-garden # a new release: re-derive, re-render
paasbox garden teardown my-garden --yes # servers, volumes, network, firewall, records

backup.enabled: true writes an etcd snapshot to S3 every ten minutes and keeps the material an in-place restore needs. Off, you lose restore and the ten-minute recovery point. It needs a bucket in the same project, which Hetzner Object Storage creates in one click, and $S3_ACCESS_KEY and $S3_SECRET_KEY.

--stub on any command runs the whole sequence against an in-memory fake: no Hetzner, no SSH, no money. It is how CI rehearses a bring-up in a second, and a fair way to see what the commands do before you spend anything.

oidc.mode: "off" is token login. dex-github runs Dex on the soil with the GitHub connector: you create one GitHub OAuth app, paste its client id and secret into the environment, and list the GitHub logins or the organisation and team that become garden admins. external points the garden at your own issuer, such as Zitadel, Keycloak, Entra or Google.

  • Not a managed service. Nobody watches your landscape but you. If you want clusters without a landscape, I can run them for you, on a best-effort basis.
  • Not multi-tenant. One project, one garden, one seed. No billing, no portal, no quotas.
  • Not a fork-me kit. The kit is pulled by tag; you keep values, not copies.
  • Not supported. It is free and best effort: an open-source project that helps a team run its own Gardener. Issues are read; nothing is promised. If you want to learn it properly with me in the room, there is a course.

The CLI is developed inside the gardener-stack monorepo, next to the deployment it drives, and published to GitHub and PyPI on each release. The public repository is a read-only snapshot with issues open; contributions come back as patches against the monorepo. Licensed under Apache-2.0.