Skip to main content

Why a workload lands where it lands

The homelab runs workloads on three active runtimes — the on-prem Talos production cluster, the Hetzner edge cluster, and a plain Docker host on the Synology NAS — plus one retired runtime, the K3s mini-server cluster, that still shapes the docs. Every placement decision follows from a small set of questions, and the answer is encoded in the directory layout of k8s/ rather than in anyone's head.

This page explains both halves: why a workload goes where it goes, and how the base/talos/edge overlay pattern writes that decision down.

The runtimes involved:

RuntimeWhere it lives in docs
Talos production cluster (3× NUC, Proxmox VMs)foundation/cluster-talos · hardware/talos
Edge cluster (1× Hetzner cx33)foundation/cluster-edge · foundation/hetzner
Docker host maresa (Synology DS723+)foundation/docker · hardware/hot_storage
K3s cluster (retired)foundation/k3s · hardware/k3s
Flux, reconciling both clustersfoundation/flux
Ansible, configuring the non-cluster hostsfoundation/ansible

The topology, top-down

The Ansible inventory confirms the cast: proxmox-1/2/3 at 192.168.100.51–53 carry the Talos VMs, maresa at 192.168.0.5 is the Synology, and home-assist at 192.168.2.20 is a standalone appliance. The edge node is not in the inventory at all — it's Talos, there is no SSH.

The placement questions

Four questions decide where a workload runs, asked in order:

QuestionIf yes →
Must it keep working when both clusters are down? (DNS, dynamic DNS, mesh connectivity, backup replication)Docker on maresa
Is it the public entry point, or does it need internet ingress with its own blast radius?Edge cluster
Does it have persistent state, a database, a GPU need, or bulk NFS media?Talos cluster
Is it a throwaway experiment?k3d on a workstation, never a real cluster

The defaults fall out of the answers: talos is where workloads go unless there's a reason not to — it has Longhorn for replicated block storage, CNPG with anti-affinity across nodes, the Intel iGPU plugin for transcoding, and NFS into the TrueNAS box. The edge cluster is deliberately starved of those capabilities so nothing stateful can accidentally settle there. And maresa runs exactly the services whose failure mode "the cluster is down" must not affect: the NAS is the Syncthing backup target, AdGuard is the LAN's DNS, the NetBird agent is the site's mesh presence.

K3s answers none of these questions anymore — the cluster was retired and its role absorbed by talos. The foundation/k3s page documents why (immutability, API-driven config, no base OS, one-step upgrades).

What actually runs where

Talos (k8s/apps/talos/, fifty overlays): everything with state or hardware needs. Media — Jellyfin, Immich, Navidrome, Tube Archivist, the arr bundle (Sonarr, Radarr, Lidarr, Prowlarr, Whisparr, Seerr, Flaresolverr in one namespace). Productivity — Nextcloud, Outline, Paperless, Vaultwarden, Vikunja. DevOps — Gitea + gitea-runner, Keycloak, n8n, monitoring. Plus 18 platform controllers (k8s/platform/talos/controllers/), including the talos-only ones: Longhorn, k8up, External Secrets, Kyverno, Crossplane, Spegel, Tetragon, NetBird operator, the GPU plugin.

Edge (k8s/apps/edge/, three overlays): netbird-client (the mesh leg back to production), openbeehive, and seitenraum — small public-facing apps whose entire state fits in a single-node CNPG on local-path storage. The platform layer (k8s/platform/edge/controllers/) is the minimum needed to terminate the internet: Cilium, Envoy Gateway, cert-manager + the all-inkl DNS-01 webhook, CNPG, local-path-provisioner, metrics-server, kubelet-serving-cert-approver. Eight controllers versus talos's eighteen — no Longhorn, no k8up, no Kyverno. The asymmetry is the point: losing the cx33 loses ingress, nothing else, and recovery is minutes.

Maresa (docker/maresa/): seven Compose services — Traefik (internal *.maresa.int.kueber.eu TLS), AdGuard Home (LAN DNS on port 53), ddclient (dynamic DNS), the NetBird agent, Syncthing (the hot-backup mover — see Backups end-to-end), Portainer (seeded via the root docker-compose.yaml, then managing the rest), and a family dashboard. Digest-pinned images, host volumes under /volume1/docker/<app>, no orchestrator.

How the directory layout encodes placement

The k8s/ tree has four load-bearing directories, each split the same way:

k8s/
├── apps/ base/ talos/ edge/ ← workload manifests
├── platform/ base/ talos/ edge/ ← controllers + CRDs
├── components/ base/ talos/ edge/ ← reusable Kustomize Components
├── resources/ base/ talos/ ← shared routes, NFS storage
└── clusters/ talos/ edge/ ← the Flux entry points

The rule is uniform: base/ holds the cluster-agnostic definition, talos/ and edge/ hold the placement decision. A workload runs on a cluster if — and only if — that cluster's overlay directory contains it and k8s/clusters/<cluster>/ has a Flux Kustomization wiring it in. Placement is therefore greppable: ls k8s/apps/edge/ is the authoritative list of edge workloads.

Apps: base defines, overlay places

k8s/apps/base/<app>/ contains the Deployment and nothing environment-specific. The cluster overlay adds everything that depends on where it runs. Vaultwarden's talos overlay (k8s/apps/talos/vaultwarden/kustomization.yaml) is the canonical shape:

namespace: vaultwarden
components:
- network-policies # → components/talos/app-network-policy
- workspace # → components/talos/workspace
- cnpg # → components/talos/cnpg
- backups # → components/talos/backups
resources:
- ../../base/vaultwarden
- http-route.yaml # hostname on this cluster's gateway
- pvc.yaml # this cluster's storage class
- namespace.yaml
- secret-longhorn.yaml
- secret-vaultwarden.yaml
patches:
- path: vaultwarden-patch.yaml

The same base can be placed on both clusters with different dressing — openbeehive and seitenraum exist in apps/base/ and are instantiated only from apps/edge/; Vaultwarden only from apps/talos/. Some talos overlays have no base at all when they're pure aggregations, like apps/talos/arr/, which composes seven apps/base/ services into one namespace.

Components: same name, per-cluster meaning

The components chain is where placement gets teeth. An app overlay references cnpg, which chains to components/<cluster>/cnpg, which layers a cluster patch over components/base/cnpg. The two patches diverge exactly where the clusters do:

Concern (cnpg component)components/talos/components/edge/
Storage classlonghorn-db-single-encryptedlocal-path
Instances2, required anti-affinity across nodes, switchover updates1 (base default)

Same component name in the app's kustomization.yaml, different guarantees underneath — the app doesn't know which cluster it's on. The component catalog also differs: edge offers only app-network-policy, backups, cnpg, and valkey; talos additionally has minio, mongodb, nats, workspace, email, and backups-cnpg. An app that needs MongoDB or NATS cannot be placed on edge — the overlay wouldn't build. Capability gaps are compile-time errors, not runtime surprises.

Platform: the same pattern, one level down

platform/base/controllers/ holds seventeen controller definitions; platform/talos/ and platform/edge/ pick from them and add per-cluster Helm values (cert-manager-values.yaml etc.). Edge picks eight, talos picks eighteen. The overlay that isn't there is the decision: there is no platform/edge/controllers/longhorn, so no PVC on edge can ever be Longhorn-backed.

Clusters: where Flux reads the answer

k8s/clusters/talos/ and k8s/clusters/edge/ are the two Flux roots — one file per workload. Platform controllers come straight from Git (sourceRef: GitRepository, path: ./k8s/platform/<cluster>/controllers/<name>). Apps come from per-app OCI artifacts (oci://gitea.web.kueber.eu/johnny/oci-talos-app-<name> or oci-edge-app-<name>), cosign-verified and SOPS-decrypted in-cluster. Every app Kustomization carries dependsOn: [infra-configs], and infra-configs in turn dependsOn every controller with wait: true — so on either cluster, apps only reconcile once that cluster's full platform layer is healthy. The mechanics of that loop are the subject of GitOps flow.

Maresa sits outside all of this: docker/maresa/ is plain Compose, deployed by hand/Portainer, with the Docker engine itself managed by the docker-host Ansible role. That's deliberate — the host that must survive a GitOps outage should not depend on GitOps.

What can break, and where to look

SymptomMost likely causeWhere to look first
App builds locally but Flux won't place it on edgeOverlay references a component edge doesn't have (minio, nats, …)ls k8s/components/edge/ vs the app's components: list
Public URL down, app healthy in-clusterEdge → production forwarding, not placementReal client IPs across the chain · fabric/netbird
App deployed to edge starts losing data on node replacementIt's on local-path storage — edge is disposable by designMove it to talos; see cluster-edge
DNS/ad-blocking dead during a cluster incidentShouldn't happen — AdGuard is on maresa, not in a clusterapps/adguard-home · the Synology itself
New app reconciles before its database existsMissing dependsOn: infra-configs in k8s/clusters/<cluster>/<app>.yamlThe cluster-root Kustomization file
Whole cluster goneDifferent recovery per cluster: edge is minutes, talos is a drillDisaster recovery

Why this is a topic, not a foundation page

Each runtime already has its own single-source-of-truth page — cluster-talos, cluster-edge, docker, k3s. What none of them owns is the seam: the decision procedure between them and the overlay convention that makes the decision durable. When you add workload number fifty-one, this page tells you which directory it belongs in; the foundation pages tell you what it will find when it gets there.