Local development cluster running on Docker via KSail. Uses Talos with the Docker provider. A small, thin manual test-bed (see local development) — bring up a component, try it, then promote it to prod.
- 1 control-plane node + 1 worker node (Docker containers)
- Config:
ksail.yaml
Cloud cluster running on Hetzner Cloud via KSail's native Hetzner provider. Changes are verified through the production-deploying merge queue before landing on the default branch.
- Control planes, static workers, and autoscaling compute workers; sizing lives in the configuration.
- Config:
ksail.prod.yaml
A high-level inventory of what Flux reconciles onto the cluster. The manifests
live under k8s/bases/infrastructure/ and
k8s/bases/apps/, with provider-specific pieces (Hetzner
CCM/CSI, Longhorn, external-dns, …) under k8s/providers/.
The sections below describe the main building blocks, not every deployed
resource. The provider overlays select the exact set; the local Docker overlay
ships only core controllers by default and makes additional components opt-in.
- GitOps & config — Flux Operator, Reloader
- Networking — Cilium (CNI + Gateway API), CoreDNS, external-dns (Cloudflare), Hetzner CCM (prod)
- Certificates — cert-manager, trust-manager, Cloudflare Origin CA issuer
- Secrets — OpenBao + External Secrets Operator (runtime), SOPS + Age (at-rest seeds)
- Identity / SSO — Dex (OIDC) with oauth2-proxy / auth-proxy; see
docs/oidc-kubectl.md - Policy & runtime security — Kyverno, Kubescape, Tetragon; see
docs/runtime-security.md - Storage — Longhorn (replicated block/RWX, stored on each worker's own
disk), Hetzner CSI (separately billed block volumes, prod only),
CloudNativePG (PostgreSQL operator); see
docs/rwx-storage.md - Autoscaling — Cluster Autoscaler (nodes), SIG Descheduler (pod
rebalancing + node consolidation), Vertical Pod Autoscaler, KEDA request-rate
autoscaling (homepage/umami); see
docs/node-autoscaling.md - Progressive delivery — Flagger (Gateway API canary deployments with
SLO-gated automated rollback, metrics from Coroot); see
docs/progressive-delivery.md - Observability — Coroot (self-hosted, eBPF: metrics, logs, traces,
profiling, service map, SLO alerting, cost allocation); see
docs/dr/alerting.md - Backup / DR — Velero with CloudNativePG backups; see
docs/dr/ - Virtualization — KubeVirt + CDI (opt-in locally; disabled on the Hetzner/prod overlay)
Application manifests live in k8s/bases/apps/.
- Homepage (dashboard), Umami (analytics), Actual Budget (budgeting),
whoami(debug) - The apps assembly declares which optional applications are enabled.
- Tenants — apps deployed from their own repositories (
ascoachingogvaner,wedding-app); seedocs/TENANTS.md
The cluster uses Flux GitOps to reconcile the state of the cluster with the single source of truth stored in this repository and published as an OCI image. KSail is used for local development, CI/CD testing, and production deployments. For prod, nodes are provisioned on Hetzner Cloud by KSail's native Hetzner provider, which also installs the Hetzner CCM and CSI drivers.
All environments use the Talos Kubernetes distribution. Local development uses Talos with the Docker provider; prod uses Talos with the Hetzner provider. CI validates manifests without creating a local cluster; the merge queue also verifies changes against production.
The cluster configuration is stored in the k8s/* directories where the
structure is as follows:
clusters/: Contains the cluster-specific configuration for each environment.providers/: Contains the provider-specific configuration.bases/: Contains the different bases that are used for the different clusters and providers.infrastructure: Contains the different infrastructure components that are used for the different clusters and providers.apps: Contains the different apps that are used for the different clusters and providers.bootstrap: The foundational bootstrap layer. Holds the shared substitution variables (variables-baseConfigMap + SOPS-encrypted Secret) and cluster-scoped PriorityClasses (e.g.platform-critical), reconciled by thebootstrapFlux Kustomization before everything thatdependsOnit.
Two things stack up. First, each environment points at a provider, which patches the shared resources — so a change can be made for one cluster, for one provider, or for everything at once:
graph LR
subgraph "Cluster-specific"
local["clusters/local"]
prod["clusters/prod"]
end
subgraph "Provider-specific"
docker["providers/docker"]
hetzner["providers/hetzner"]
end
subgraph "Shared"
bases["bases/*"]
end
local --> docker
prod --> hetzner
docker --> bases
hetzner --> bases
Second, Flux applies those layers in order, each waiting for the one before it to come up:
graph TB
bootstrap["bootstrap"]
controllers["infrastructure-controllers"]
infra["infrastructure"]
apps["apps"]
controllers -- "depends on" --> bootstrap
infra -- "depends on" --> controllers
apps -- "depends on" --> infra
The controllers, infrastructure, and apps layers have matching folders under
providers/<provider-name>/ and bases/. The infrastructure layer, for
example, is backed by k8s/providers/<provider-name>/infrastructure/ and
k8s/bases/infrastructure/.
Bootstrap is cluster-specific: its Kustomization maps to
k8s/clusters/<cluster-name>/bootstrap/, not a provider folder.
The layer definitions themselves are written once in k8s/clusters/base/ with
placeholders where the cluster and provider names go; each cluster's overlay
fills those in. Only the per-cluster bootstrap/ directory holds manifests
unique to one cluster.
docs/TEMPLATING.md— the exact files a fork must edit, including how those placeholders get filled in.docs/TENANTS.md— adding a tenant: an app that runs on the platform from its own repository.