Authoritative reference for the on-disk layout of a lok8s project. For
concepts (atoms/molecules, bootstrap vs workloads, build/deploy
pipeline) see docs/guide/concepts.md. For
spec field semantics see docs/reference/specs.md.
For writing addons see docs/guide/addons.md.
- Production first. Production is the reference setup; local dev is a nerfed overlay of the same structure.
- Same tree everywhere. Lo (kind), Capi, and future drivers all use
the same
.lok8s/layout. - Domain keyed. Every cluster and every deployment is keyed by an
FQDN. One cluster = one folder under
clusters/. - Kustomize native. Targets are kustomize builds. Helm charts are
inflated via khelm at
kustomize buildtime — no helm CLI dependency. - Artifacts gitignored. Rendered output under
artifacts/is ephemeral and rebuilt on demand. If you want GitOps-committed artifacts, opt in per-project. - Framework-owned
.lok8s/. Everything under.lok8s/is shipped/managed by the framework (b syncowns it) and stays a flat, override-free tree. User content lives underclusters/and at the repo root. - No framework-level workload ordering. The only ordering primitive
lok8s exposes is
spec.bootstrapfor cluster-infrastructure addons applied at provision time. Workload ordering (if needed) is expressed by the workload layer itself — kubectl's in-manifest order, Tilt'sresource_deps, or GitOps engine primitives.
| Layer | What | lok8s scope |
|---|---|---|
| 0 | Hardware provider (hcloud, AWS, GCP, bare metal) | Abstracted by CAPI |
| 1 | System / kernel | Abstracted by CAPI |
| 2 | Kubernetes (control plane, nodes) | Created by driver (Lo / Capi) |
| 3 | Cluster infrastructure (CNI, CSI, MetalLB, CRDs) | spec.bootstrap → framework addons |
| 4 | Third-party software (monitoring, operators) | targets/<name>/ (referencing addons) |
| 5 | User applications | targets/<name>/ + services.yaml |
Layers 3–5 are the cluster content. Layer 3 runs at provision time
before anything else lands. Layers 4–5 run after the cluster is
healthy, via Tilt (dev) or lo deploy (headless/CI).
How the cluster comes into existence. Handled by the driver
(.lok8s/drivers/<kind>/main), not by kustomize.
| Kind | Driver | Runtime |
|---|---|---|
Lo |
drivers/lo/main |
kind (Docker-in-Docker) |
Capi |
drivers/capi/main |
Cluster API (any provider — Hetzner, AWS, ...) |
KubeOne |
drivers/kubeone/main |
KubeOne (imperative worker management) |
Kkp |
drivers/kkp/main |
Kubermatic Kubernetes Platform |
The driver consumes cluster.lok8s.yaml and provisions the cluster.
When run locally, the lo script invokes the driver directly. In the
operator mode (future), the operator reads the same CRD from
Kubernetes.
What runs on the cluster. Split into two planes:
- Plane A — cluster infrastructure (
spec.bootstrap). Framework addons applied during provisioning (by.lok8s/libs/bootstrap, identically for every driver) as a dependency DAG:dependsOn:declares ordering edges,wait:gates readiness, and independent entries apply in parallel (CNI first, then e.g. cert-manager and the operators concurrently). The cluster is not considered ready until the whole DAG converges. - Plane B — workloads (
targets/). User-named kustomize directories, each built independently intoartifacts/<target>/artifacts.yaml. Applied by Tilt (dev) orlo deploy(headless). No framework-level ordering.
See concepts.md for the full model.
my-project/
├── services.yaml # service definitions + registry config (committed)
├── services.<config>.yaml # personal overrides (gitignored)
├── Tiltfile # loads .lok8s/tilt/Tiltfile
├── .bin/ # b-managed tool binaries (argsh, kustomize, ...)
├── .kustomize/ # b-managed kustomize plugin binaries
│ ├── khelm.mgoltzsche.github.com/v2/chartrenderer/ChartRenderer
│ └── secrets.lok8s.dev/v1/secret/Secret
├── clusters/ # user cluster definitions, one dir per FQDN
└── .lok8s/ # framework tree (synced, don't edit)
cluster.lok8s.yaml never lives at the repo root. It lives inside its
domain directory, under clusters/<domain>/.
.lok8s/ # framework — flat, synced, override-free
├── lo # CLI entrypoint
├── libs/ # shared bash libraries
│ ├── bootstrap # applies spec.bootstrap (framework-level)
│ ├── addons # lo addons command
│ ├── build, deploy, env
│ ├── lint, status
│ ├── gitops, kubehz/
│ └── ...
├── utils/ # shared helpers (verbose, ip, types, http, ...)
├── addons/ # framework-shipped bootstrap addons
│ ├── cilium/ # Cilium CNI (khelm ChartRenderer + layered values)
│ └── metallb/ # MetalLB L2 LB (khelm ChartRenderer)
├── drivers/
│ ├── lo/
│ │ ├── main # Lo driver (kind)
│ │ ├── cluster/ # kind runtime templates
│ │ │ ├── config.yaml # base kind config
│ │ │ ├── registry/ # registry container templates
│ │ │ └── coredns/ # CoreDNS overlay
│ │ └── utils/ # driver helpers
│ ├── capi/ # CAPI driver + envsubst templates
│ ├── kubeone/ # KubeOne driver
│ └── kkp/ # KKP driver
├── providers/ # physical infra providers
│ └── hetzner/ # hcloud + Robot (cloud-init, installimage)
└── tilt/
└── Tiltfile # the lok8s() extension function
clusters/ # user content — one dir per FQDN
├── .active # runtime state: current domain (gitignored)
├── lok8s.dev/ # default cluster domain (ships with lok8s)
│ ├── cluster.lok8s.yaml
│ ├── targets/ # workload plane (Plane B)
│ │ └── <name>/
│ │ └── kustomization.yaml
│ ├── artifacts/ # built output (gitignored)
│ │ ├── kustomization.yaml # auto-generated by lo env kustomization
│ │ └── <target>/
│ │ └── artifacts.yaml
│ └── .secrets/ # secret cache (gitignored; encrypted opt-in)
└── example.com/ # additional cluster domain
└── cluster.lok8s.yaml
Framework-shipped bootstrap addons. Each subdirectory is a
kustomize-buildable addon — typically a khelm ChartRenderer + layered
values files, but any kustomization works. These are the atoms of
the lok8s addon model, shared across every driver.
Referenced by name from cluster.lok8s.yaml:
spec:
bootstrap:
- cilium # → .lok8s/addons/cilium/
- metallb # → .lok8s/addons/metallb/Or as kustomize bases from workload targets:
# clusters/<domain>/targets/platform/kustomization.yaml
resources:
- ../../../addons/cert-manager/
- ./ingress-routes.yamlValues files stack as base < driver < provider < inline — see
docs/guide/addons.md for the full addon
authoring guide and precedence rationale.
The workload plane. Each subdirectory is an independent kustomize target — the molecules of the model. Targets compose addons and in-repo services into a single kustomization.
targets/
├── platform/
│ └── kustomization.yaml # e.g. cert-manager + ingress routes
├── monitoring/
│ └── kustomization.yaml # e.g. prometheus stack
└── apps/
└── kustomization.yaml # your applications
Targets are independently built by lo build, producing one
artifacts/<target>/artifacts.yaml per target. There is no top-level
composition and no inter-target ordering.
Generated by lo build and lo env kustomization. Gitignored.
artifacts/
├── kustomization.yaml # auto-generated: resources: per-target + images swap
├── .cache-queue # Tilt's build:false pre-pull queue (TSV)
├── platform/
│ └── artifacts.yaml # per-target rendered output
├── monitoring/
│ └── artifacts.yaml
└── apps/
└── artifacts.yaml
The top-level kustomization.yaml is what Tilt reads: one
kustomize build over this directory yields a unified pool that Tilt
then partitions via filter_yaml() (by lok8s.dev/type: system for
infrastructure, by service-name labels for per-service workloads, rest
as uncategorized).
Everything the driver needs to create and prepare a cluster lives in a
single file under clusters/<domain>/cluster.lok8s.yaml. The full field
reference is in
docs/reference/specs.md; a minimal Lo
(kind) example:
apiVersion: cluster.lok8s.dev/v1beta1
kind: Lo
metadata:
name: local
spec:
cluster:
domain: lok8s.dev
bootstrap:
- cilium
- metallbEvery other field is defaulted — slot-derived from the *.lok8s.dev
domain or applied as a domain-independent default. See the
Specs reference for the
full defaulting table.
The folder name under clusters/ MUST match spec.cluster.domain.
spec.cluster.domain should be the k8s API endpoint hostname, not
the user-facing brand domain. This lets you have multiple clusters
serving the same brand from different environments without folder
collisions:
clusters/example.com/— local kind cluster claimingexample.comvia/etc/hostsclusters/cluster.example.in.net/— production, API atcluster.example.in.net, serving public traffic onapp.example.comvia Envoy routes
Deployment domains use deploy.lok8s.yaml instead of cluster.lok8s.yaml.
They reference an existing cluster via spec.clusterRef.domain to
deploy content to someone else's cluster. Workload selection for
Deploy specs is being reworked alongside the services.yaml targets-map
redesign — currently a Deploy spec carries only clusterRef + namespace.
One committed file at the repo root. Defines which services exist,
their build config, and a shared registry config. Personal overrides
land in services.<config>.yaml (gitignored).
# services.yaml
registry:
endpoint: "${DOCKER_REGISTRY}"
branch: "${DOCKER_PROJECT}"
tag: "${DOCKER_TAG}"
prefix: lok8s.local
defaults:
build: true
services:
my-api:
build: true
my-frontend:
build: true
redis:
build: false # use prebuilt image# services.local.yaml — personal override, gitignored
services:
my-frontend:
enabled: false # not working on this todaySee docs/guide/services.md for the full
schema and image-swap pipeline.
Services with build: true can carry a lok8s.yaml alongside their
source describing how Tilt should build and run them:
# my-api/lok8s.yaml
build:
dockerfile: service
context: .
ports:
- "3000:3000"
live_update:
sync:
- src/
run:
- npm run build
tilt:
resource_deps: [redis]The Tiltfile iterates active services, reads each lok8s.yaml, and
wires docker_build + k8s_resource accordingly.
Three standard labels on kustomize output, applied via labels: blocks
in target/addon kustomizations:
| Label | Values | Purpose |
|---|---|---|
lok8s.dev/name |
addon or service name | Identity — Tilt groups by this |
lok8s.dev/type |
system, service, job |
Partition (system → infra pool) |
lok8s.dev/node |
all, core, database, none |
Scheduling hints (node affinity) |
The Tilt extension reads the compiled pool and partitions by
lok8s.dev/type: system first (applied as uncategorized
infrastructure), then by lok8s.dev/name per active service, and
whatever's left goes in as uncategorized.
lo up <domain>
├─ provision driver creates the cluster (kind/CAPI/...)
├─ bootstrap framework applies spec.bootstrap addons
│ (.lok8s/libs/bootstrap) in order, waits
│ healthy between stages
└─ tilt up Tilt reads services.yaml, builds targets,
applies with service-enable filters, wires
docker_build + live_update
The headless primitives:
lo build [target...]— per-target kustomize build. No args → build every target alphabetically. Args → build only those.lo deploy [target...]— per-target apply loop. For each target, extract CRDs and apply first (waits for establishment), then apply the rest, then wait for Deployments to become Available before moving on. Not an ordering primitive — per-target health waits are just good UX.lo env kustomization— builds all targets and writes the top-levelartifacts/kustomization.yamlthat references each<target>/artifacts.yamlas a resource, with image swaps generated fromservices.yaml.lo env services— prints the mergedservices.yaml+services.<config>.yamlas YAML (used by the Tiltfile).lo addons— lists framework bootstrap addons. Seedocs/guide/addons.md.lo lint— validates specs, bootstrap entries, target kustomization references, labels, secrets.lo status— cluster health + per-target build state.
The following are intentionally incomplete while their designs settle:
- CAPI bootstrap path —
drivers/capi/mainprovisions the work cluster and hands off to framework bootstrap, but the addon set for CAPI clusters (CCM, CSI, cert-manager) is still being defined. Currently only Cilium is applied by default. lo gitops flux|argo— stubbed with a deferred-error message. Will be rebuilt from the post-services.yaml-targets-map model to emit per-target FluxKustomization/ ArgoApplicationresources with native ordering primitives (dependsOn,sync-waveannotations).- Deploy CRD workload selection —
deploy.lok8s.yamlcurrently carries onlyclusterRef+namespace. Target selection will land with theservices.yamltargets-map design. - Provision lifecycle hooks — pluggable PreProvision/PostProvision events so integrations can attach registration/teardown steps without patching the core provision path. Needs a design for the hook mechanism first.
| Key | Value |
|---|---|
| Last rewrite | 2026-06-11 (consolidated from STRUCTURE.md / STRUCTURE-PLAN.md) |
| Related docs | concepts.md, specs.md, addons.md |