Infrastructure for the local development platform.
This repository owns the local Kubernetes platform used during development.
Current platform capabilities include:
- containerized development environment
- Kubernetes cluster
- local container registry
- GitOps engine
- HTTP and HTTPS routing through Gateway API
- TLS certificate management
- encrypted GitOps secret management
- observability
- direct local access to Gateway workloads
- local host integration for trusted HTTPS, hostname resolution, and required kernel limits
- task-based automation
Current implementations include:
- Dev Containers for the development environment
- Kind for Kubernetes
- Podman for container operations
- Argo CD for GitOps
- Envoy Gateway for Gateway API
- cert-manager for certificate management
- Sealed Secrets for encrypted GitOps secrets
- OpenTelemetry Collector for telemetry ingestion and processing
- Prometheus for metrics storage and querying
- Tempo for trace storage and querying
- Grafana for observability exploration and visualization
Future platform capabilities may include:
- CI pipelines
- more complete local LoadBalancer integration
- persistent observability storage
- a dedicated log backend
- richer dashboards and alerting
Alternative implementations may also be evaluated as the homelab evolves.
Examples include:
- Flux for GitOps
- Cilium for networking and Gateway API
- cloud-provider-kind for local LoadBalancer integration
- Ingress
- Traefik
- HAProxy
- Istio
- alternative OpenTelemetry-compatible observability backends
Specific tools are generally treated as implementations of platform capabilities rather than permanent architectural choices.
The developer host is intentionally kept lightweight.
Required on the host:
- Fedora Atomic
- Podman
- Dev Container CLI
- tmux
- Git
- terminal and editor of choice
Repository-specific tooling runs inside the Dev Container.
The rootless Podman and Kind platform also requires enough per-user inotify instances on the host as the number of platform components grows.
The recommended host value is:
fs.inotify.max_user_instances = 1024
The host-side inotify.sh helper can inspect and configure this limit.
This setting is kept outside task up because it modifies the developer
host.
Host
β
βΌ
dev.sh
β
βΌ
Dev Container
β
βΌ
Task
β
βΌ
scripts/
Inspect and configure the host inotify limit when needed:
./inotify.sh status
./inotify.sh installStart or reconnect to the development environment:
./dev.shInside the Dev Container, bring up the local platform:
task upThe platform currently consists of:
Local Container Registry
β
βΌ
Kind Cluster
β
βββββββββββββββββ¬βββββββββββββββββ¬βββββββββββββββββ
βΌ βΌ βΌ βΌ
Sealed Secrets Observability Argo CD Gateway/TLS
β β
βββ Prometheus βββ Envoy Gateway
βββ Tempo βββ cert-manager
βββ Grafana
βββ OTel Collector
β
βΌ
Telemetry
β
βΌ
Workloads
Inspect the running platform:
task statusOr inspect individual components:
task registry:status
task cluster:status
task argocd:status
task envoy:status
task cert-manager:status
task sealed-secrets:status
task observability:status
task prometheus:status
task tempo:status
task grafana:status
task otel:statusThe desired state of workloads is maintained separately in
local-environments.
After the platform lifecycle has installed Argo CD, register the desired-state applications from that repository:
task argocd:bootstrapFrom that point, workload changes are reconciled from Git.
Application repositories are responsible for building and publishing the
container images referenced by local-environments.
The local Gateway is reachable directly from the developer host through
fixed Kind port mappings. No long-running kubectl port-forward process
is required for normal workload access.
Configure the remaining host-side integration:
./trust-ca.sh install
./hosts.sh installAll three local hostnames resolve to 127.0.0.1. With the local CA trusted,
the standard HTTPS entry points are:
https://example.local:8443
https://grafana.local:8443
https://argocd.local:8443
The example backend can also be reached over HTTP:
curl http://example.local:8080/api/greetingOr over trusted HTTPS:
curl https://example.local:8443/api/greetingWhen finished, tear down the local platform:
task downHost-side inotify configuration, trust, and hostname configuration are intentionally separate from the platform lifecycle. They can be managed explicitly when needed:
./inotify.sh remove
./trust-ca.sh remove
./hosts.sh removeThe golden path is intentionally small:
host prerequisites
β
βΌ
./dev.sh
β
βΌ
task up
β
βΌ
local-environments
β
βΌ
Argo CD reconciliation
β
βΌ
Gateway API routing + observability
β
βΌ
direct host access
β
βΌ
workloads
β
βΌ
task down
dev.sh is the normal entrypoint for working with this repository.
Start a new repository-specific tmux session, or reconnect to an existing one:
./dev.shRebuild the Dev Container when its definition has changed:
./dev.sh rebuilddev.sh verifies host prerequisites, starts or reuses the
repository-specific tmux session, starts the Dev Container, and opens a
shell inside it.
Most repository operations are exposed through Task:
task --listThe Dev Container provides the tooling required to develop and operate the local platform.
The normal entrypoint is ./dev.sh, but the Dev Container can also be
operated manually.
Build the Dev Container:
devcontainer build \
--workspace-folder . \
--docker-path podmanStart it:
devcontainer up \
--workspace-folder . \
--docker-path podmanOpen a shell inside it:
devcontainer exec \
--workspace-folder . \
--docker-path podman \
bashPersonal developer configuration remains outside the repository where practical. Selected host configuration, such as Git and editor configuration, may be mounted into the Dev Container.
The complete local platform can normally be managed through:
task up
task status
task downtask up currently brings up the platform in dependency-aware order:
registry
β
βΌ
cluster
β
βΌ
registry integration
β
βΌ
Sealed Secrets key restore
β
βΌ
Sealed Secrets controller
β
βΌ
observability namespace
β
βββ Prometheus
βββ Tempo
βββ Grafana
βββ OpenTelemetry Collector
β
βΌ
Argo CD install -> configure -> status
β
βΌ
Envoy Gateway
β
βΌ
cert-manager
β
βΌ
Gateway configuration
Observability starts early so platform and workload telemetry can be
received as soon as producers become available. Its startup and status
order is observability -> Prometheus -> Tempo -> Grafana -> OpenTelemetry
Collector.
During shutdown the producer-facing OpenTelemetry Collector is removed
late, followed by Grafana, Tempo, Prometheus, and finally the shared
observability namespace.
The Kind cluster is created with fixed host-to-node port mappings for the local Gateway:
127.0.0.1:8080 -> Kind node :30080
127.0.0.1:8443 -> Kind node :30443
The Envoy data plane uses the corresponding fixed NodePorts, providing stable local access across clean platform bootstraps.
Sealed Secrets key material has a lifecycle independent of the disposable Kind cluster.
During task down, the complete Sealed Secrets key set is backed up before the
controller and cluster are deleted. During task up, any existing key backup is
restored before the Sealed Secrets controller is installed.
task down
β
βββ sealed-secrets:backup
βββ platform component deletion
βββ cluster:delete
task up
β
βββ cluster:create
βββ sealed-secrets:restore
βββ sealed-secrets:install
βββ remaining platform bootstrap
The restore operation is re-entrant. If no key backup exists, restore is skipped and the controller creates its initial sealing key. If a backup exists, the keys are restored before the controller starts so clean cluster recreation does not create unnecessary replacement keys.
Individual components also expose lifecycle tasks for development, troubleshooting, and experimentation.
This separation keeps the common workflow simple while still allowing each platform component to be operated independently.
The local platform currently uses Kind to provide Kubernetes.
The Kubernetes cluster lifecycle is owned by local-platform. Kind is
the current implementation and may be replaced or complemented by other
Kubernetes distributions as the homelab evolves.
Create the cluster:
task cluster:createShow cluster status:
task cluster:statusDelete the cluster:
task cluster:deleteUseful direct inspection commands include:
kind get clusters
kubectl get nodesThe Kind node exposes fixed container ports to the developer host so the Envoy Gateway can be reached without port forwarding.
The current mapping is:
Host :8080 -> Kind node :30080
Host :8443 -> Kind node :30443
These mappings are established when the Kind node is created and therefore depend on stable NodePorts on the Envoy Gateway service.
The platform provides a local OCI registry used by workloads running in the Kind cluster.
Application repositories are responsible for building and publishing
their container images. local-platform provides and operates the
registry.
Create the registry:
task registry:createShow registry status:
task registry:statusList repositories currently published to the registry:
task registry:imagesList available tags for an image:
task registry:tags IMAGE=example-backendFor example:
{"name":"example-backend","tags":["0.6.0"]}
Delete the registry:
task registry:deleteAn image can also be pushed manually:
podman push \
--tls-verify=false \
localhost:5001/example-backend:0.6.0The local platform includes Argo CD as the initial GitOps engine.
Argo CD is installed and operated by local-platform, while the desired
state of workloads is maintained separately in the local-environments
repository.
The responsibility boundary is intentionally explicit:
local-platform
β
βββ Kubernetes
βββ Container Registry
βββ Argo CD
βββ cert-manager
βββ Sealed Secrets controller
βββ Observability platform
βββ Gateway API
β
βΌ
local-environments
β
βββ desired workload state
βββ encrypted SealedSecret resources
β
βΌ
Workloads
The GitOps engine itself is considered an implementation detail. The architecture allows alternative GitOps tools, such as Flux, to be evaluated alongside or instead of Argo CD in the future.
Argo CD is available through the shared Gateway at
https://argocd.local:8443. Browser traffic is protected by TLS terminated at
Envoy. server.insecure=true applies only to the internal Envoy-to-
argocd-server hop.
The platform lifecycle configures Argo CD in order:
task argocd:install
task argocd:configure
task argocd:statusRefresh Argo CD applications:
task argocd:refreshForce a hard refresh, including cached manifests:
task argocd:refresh:hardDelete Argo CD:
task argocd:deleteApplication definitions and environment state are intentionally not maintained in this repository.
The initial Argo CD application definitions are owned by
local-environments and registered after the platform lifecycle has installed
Argo CD. After that registration, normal workload changes are performed through
Git and reconciled by Argo CD.
The local platform uses cert-manager for TLS certificate management.
The local certificate hierarchy consists of a self-signed bootstrap issuer and a locally generated root CA. The root CA is then used to issue certificates for the shared Gateway. This allows the host to trust one local CA instead of trusting individual leaf certificates.
Install cert-manager:
task cert-manager:installConfigure the local issuers and root CA:
task cert-manager:configureShow cert-manager status:
task cert-manager:statusExport the current local root CA from Kubernetes:
task cert-manager:export-caThe generated CA is local development infrastructure. Recreating the platform may generate a new root CA, in which case the host trust store must be updated to trust the newly exported CA.
Delete cert-manager:
task cert-manager:deleteThe local CA is intended for development only. A different certificate issuer can be introduced when environments require publicly or internally trusted certificates.
The local platform uses Sealed Secrets for GitOps-compatible secret management.
Plaintext secret values are not stored in Git. Workload repositories instead
store SealedSecret resources that contain encrypted secret data. The Sealed
Secrets controller runs inside the destination cluster and materializes normal
Kubernetes Secret resources there.
The responsibility boundary is:
local-platform
β
βββ Sealed Secrets controller
βββ sealing key lifecycle
β
βΌ
local-environments
β
βββ SealedSecret
β
βΌ
Kubernetes Secret
β
βΌ
Workload
Install the controller:
task sealed-secrets:installShow controller and sealing key status:
task sealed-secrets:statusBack up the current sealing keys:
task sealed-secrets:backupRestore backed-up sealing keys:
task sealed-secrets:restoreDelete the controller:
task sealed-secrets:deleteThe sealing keys are stored by the controller as Kubernetes TLS secrets.
Automatic key renewal may create additional keys over time. Old keys remain
important because existing SealedSecret resources may still depend on them.
The backup therefore contains the complete current key set, not only the newest key.
Local backups are stored under:
.local/sealed-secrets/keys.yaml
The .local/ directory is excluded from Git.
The backup contains private cryptographic key material and must be treated as sensitive persistent state. The local file is sufficient for development and recovery testing, but stronger external backup protection is expected for non-local environments.
The normal platform lifecycle automatically integrates key backup and restore:
clean shutdown
β
βΌ
backup complete sealing key set
β
βΌ
delete disposable cluster
create new cluster
β
βΌ
restore existing sealing keys
β
βΌ
install Sealed Secrets controller
β
βΌ
existing SealedSecret ciphertext remains decryptable
This keeps the Kubernetes cluster disposable while allowing encrypted GitOps state to remain stable across cluster recreation.
The explicit backup and restore tasks remain available for troubleshooting, disaster-recovery exercises, and manual key lifecycle operations.
The local platform uses an OpenTelemetry-first observability architecture.
Application workloads export telemetry using OpenTelemetry Protocol (OTLP). The OpenTelemetry Collector provides a stable ingestion and processing layer between workloads and observability backends.
The current architecture is:
Workload
β
β OTLP
βΌ
OpenTelemetry Collector
β
βββ logs βββββββββΊ debug exporter
β
βββ traces βββββββΊ debug exporter
β β
β ββββββββββΊ Tempo
β :4317 OTLP/gRPC
β :3200 query API
β β
β βΌ
β Grafana
β
βββ metrics
β
βββββββββββΊ debug exporter
β
βΌ
Prometheus exporter
:8889
β
β scrape
βΌ
Prometheus
β
β PromQL
βΌ
Grafana
The current implementation intentionally keeps the stack small. Metrics have a storage and query backend in Prometheus. Traces are exported to both the detailed Collector debug exporter and Tempo using OTLP/gRPC. Logs remain exported only through the debug exporter, and the metrics and logs pipelines are otherwise unchanged.
All observability components currently share the observability
namespace. The namespace has its own lifecycle and is not owned by any
individual observability component.
The OpenTelemetry Collector receives OTLP over:
gRPC: 4317
HTTP: 4318
Install the Collector:
task otel:installShow Collector status:
task otel:statusInspect recently received telemetry:
kubectl \
--context kind-local \
--namespace observability \
logs deployment/otel-collector \
--since=2mFollow telemetry live:
kubectl \
--context kind-local \
--namespace observability \
logs deployment/otel-collector \
--followDelete the Collector:
task otel:deleteThe metrics pipeline also exposes Prometheus-compatible metrics on port
8889.
For troubleshooting, the endpoint can be inspected directly using a temporary port forward:
kubectl \
--context kind-local \
--namespace observability \
port-forward service/otel-collector 8889:8889Then:
curl --silent http://localhost:8889/metricsThe verified application trace flow is:
browser frontend span
-> W3C trace context
-> Quarkus backend span
-> OpenTelemetry Collector
-> Tempo
-> Grafana Explore
Prometheus provides metrics storage and PromQL querying.
It scrapes the OpenTelemetry Collector's Prometheus exporter rather than scraping application workloads directly:
otel-collector.observability.svc:8889
Install Prometheus:
task prometheus:installShow Prometheus status:
task prometheus:statusDelete Prometheus:
task prometheus:deleteFor local exploration, expose the UI temporarily:
kubectl \
--context kind-local \
--namespace observability \
port-forward service/prometheus 9090:9090The UI is then available at:
http://localhost:9090
Example application metrics include:
http_server_requests_milliseconds_count
jvm_classes_loaded
Prometheus currently uses disposable local storage. Persistence and retention are intentionally deferred until they solve a concrete development need.
Tempo provides the local tracing backend. It runs in monolithic,
single-binary mode and receives OTLP/gRPC internally on port 4317.
Its query API is available only inside the cluster on port 3200; Tempo is
not exposed outside the cluster.
Tempo uses local ephemeral storage through an emptyDir mounted at
/var/tempo:
WAL: /var/tempo/wal
trace blocks: /var/tempo/blocks
Trace data is lost when the Tempo pod is recreated.
Install Tempo:
task tempo:installShow Tempo status:
task tempo:statusDelete Tempo:
task tempo:deletePersistent trace storage, object storage, the metrics generator, span metrics, service graphs, exemplars, and deeper Prometheus/Tempo correlation remain deferred until a concrete need is demonstrated.
Grafana provides the initial observability user interface.
Prometheus is provisioned declaratively as Grafana's default datasource:
http://prometheus.observability.svc:9090
Tempo is provisioned declaratively as Grafana's tracing datasource:
http://tempo.observability.svc:3200
No manual datasource setup should be required after a clean platform
bootstrap. Grafana is available through the shared Gateway at
https://grafana.local:8443 and has declaratively provisioned Prometheus and
Tempo datasources.
Install Grafana:
task grafana:installShow Grafana status:
task grafana:statusDelete Grafana:
task grafana:deleteThe current focus is manual exploration and learning through PromQL and Grafana Explore; traces can be inspected in Explore by trace ID. A temporary port-forward remains available for troubleshooting but is not the normal access path.
Dashboards will be added incrementally after useful queries and visualizations have first been understood and validated manually.
The local platform uses Kubernetes Gateway API for north-south HTTP and HTTPS routing.
Envoy Gateway is the initial Gateway API implementation.
local-platform owns:
- the Envoy Gateway controller
- the
GatewayClass - the shared
Gateway - the shared HTTP and HTTPS listeners
- local TLS certificate integration
- the local exposure of the Gateway data plane
Workload-specific routing is owned by local-environments and expressed
using standard Gateway API resources such as HTTPRoute.
The shared Envoy Gateway terminates TLS for all three local HTTPS hostnames. Route attachment is restricted per listener:
example.localaccepts routes from thedefaultnamespacegrafana.localaccepts routes from theobservabilitynamespaceargocd.localaccepts routes from theargocdnamespace
The Grafana and Argo CD routes are platform-owned routes. Workload routes,
including example.local, remain owned by local-environments.
The responsibility boundary is:
local-platform
β
βββ Envoy Gateway controller
βββ GatewayClass
βββ Gateway
βββ Grafana and Argo CD HTTPRoutes
βββ local Gateway exposure
β
βΌ
local-environments
β
βββ workload HTTPRoute
β
βΌ
Service
β
βΌ
Workload
Keeping workload routes based on the standard Gateway API makes it possible to experiment with alternative Gateway API implementations in the future without necessarily changing application routing manifests.
Install Envoy Gateway and the Gateway API CRDs:
task envoy:installConfigure the local GatewayClass and Gateway:
task envoy:configureShow Envoy Gateway status:
task envoy:statusThe Envoy data plane uses stable NodePorts for the shared Gateway:
HTTP: 30080
HTTPS: 30443
The Kind control-plane node maps these ports directly to the developer host:
127.0.0.1:8080 -> 30080
127.0.0.1:8443 -> 30443
This makes the Gateway directly reachable from the host without
requiring a long-running kubectl port-forward process.
With local hostname resolution configured, HTTP routes are available through:
curl http://example.local:8080/api/greetingWith the local root CA also trusted, HTTPS routes are available through:
curl https://example.local:8443/api/greetingThe NodePorts are intentionally fixed because Kind port mappings are configured when the cluster is created and therefore need stable destination ports.
The local Gateway terminates TLS using certificates managed by cert-manager.
For troubleshooting, the Gateway can still be exposed temporarily using:
task envoy:forwardPort forwarding is not required for normal local development.
Delete Envoy Gateway:
task envoy:deleteThe current host ports are deliberately non-privileged ports. More transparent local access on ports 80 and 443 can be evaluated separately without changing the Gateway API routing model.
The Envoy data plane uses inotify and may fail during startup when the host's per-user inotify instance limit is exhausted.
A typical symptom is an Envoy CrashLoopBackOff with an error similar
to:
assert failure: inotify_fd_ >= 0
Consider increasing value of fs.inotify.max_user_watches and/or
fs.inotify.max_user_instances via sysctl
For this local rootless Podman and Kind environment, configure the host through:
./inotify.sh installSee Local Host Integration for details.
The Kubernetes platform is intentionally isolated from permanent host configuration, but a small amount of optional or required host integration makes local development considerably more reliable and convenient.
There are three separate concerns:
- provide sufficient inotify resources for the rootless container platform
- trust the local root CA on the Fedora host
- resolve local Gateway hostnames to
127.0.0.1
These operations are explicit and reversible. They are not hidden inside
task up because they modify the developer host.
The local platform runs multiple services through rootless Podman and Kind. As the platform grows, the Fedora host's default limit for per-user inotify instances may be too low.
Envoy Gateway in particular requires an available inotify instance
during data-plane startup. When the limit is exhausted, Envoy may enter
CrashLoopBackOff.
The platform currently recommends:
fs.inotify.max_user_instances = 1024
Inspect the current value:
./inotify.sh statusConfigure the recommended value:
./inotify.sh installThe helper persists the host configuration under /etc/sysctl.d/ and
applies it immediately.
Remove the repository-managed setting and reload the remaining host sysctl configuration:
./inotify.sh removeOnly fs.inotify.max_user_instances is adjusted because that is the
limit currently demonstrated to be relevant to this platform. The
fs.inotify.max_user_watches limit is left unchanged until a concrete
need requires otherwise.
Export the root CA from the running platform inside the Dev Container:
task cert-manager:export-caOn the Fedora host, install the exported CA:
./trust-ca.sh installRemove it again with:
./trust-ca.sh removeBecause the root CA is generated by the local platform, a clean platform bootstrap may replace it. When that happens, export the new CA and reinstall the host trust entry.
This keeps certificate generation owned by Kubernetes while host trust remains an explicit developer-machine operation.
hosts.sh install and hosts.sh remove idempotently manage the local
/etc/hosts entries for all Gateway hostnames.
Install the mapping:
./hosts.sh installThis maps:
127.0.0.1 example.local
127.0.0.1 grafana.local
127.0.0.1 argocd.local
Remove it again with:
./hosts.sh removeThe helper keeps this host-specific operation outside the Dev Container and makes the change explicit and reversible.
With hostname resolution configured:
curl http://example.local:8080/api/greetingWith both hostname resolution and the local CA installed:
curl https://example.local:8443/api/greetingNo -k, --resolve, or long-running port-forward process is required
for normal Gateway access, including Grafana and Argo CD. A temporary
port-forward remains an acceptable troubleshooting fallback.
The resulting local request path is:
Developer Host
β
β example.local:8080 / :8443
βΌ
127.0.0.1
β
βΌ
Kind extraPortMappings
β
βββ :8080 -> node :30080
βββ :8443 -> node :30443
β
βΌ
Envoy Gateway
β
βΌ
HTTPRoute
β
βΌ
Service
β
βΌ
Workload
.
βββ .devcontainer/
βββ .local/
β βββ sealed-secrets/
β βββ keys.yaml
βββ manifests/
β βββ argocd/
β β βββ cmd-params-configmap.yaml
β β βββ httproute.yaml
β β βββ kustomization.yaml
β βββ cert-manager/
β β βββ selfsigned/
β β β βββ clusterissuer.yaml
β β β βββ kustomization.yaml
β β βββ ...
β βββ gateway/
β β βββ envoy/
β β βββ gatewayclass.yaml
β β βββ gateway.yaml
β β βββ kustomization.yaml
β β βββ ...
β βββ grafana/
β β βββ configmap-datasources.yaml
β β βββ deployment.yaml
β β βββ httproute.yaml
β β βββ kustomization.yaml
β β βββ service.yaml
β βββ kind/
β β βββ ...
β βββ observability/
β β βββ namespace.yaml
β βββ otel/
β β βββ configmap.yaml
β β βββ deployment.yaml
β β βββ kustomization.yaml
β β βββ service.yaml
β βββ prometheus/
β β βββ configmap.yaml
β β βββ deployment.yaml
β β βββ kustomization.yaml
β β βββ service.yaml
β βββ tempo/
β βββ configmap.yaml
β βββ deployment.yaml
β βββ kustomization.yaml
β βββ service.yaml
βββ scripts/
βββ Taskfile.yml
βββ dev.sh
βββ hosts.sh
βββ inotify.sh
βββ trust-ca.sh
βββ README.md
The main entrypoints are deliberately few:
dev.shmanages the developer environment.Taskfile.ymlexposes the operator-facing commands.scripts/contains reusable and non-trivial platform automation.manifests/contains declarative platform configuration owned by this repository.hosts.shmanages host-side local hostname resolution.inotify.shmanages the host-side inotify instance limit required by the rootless local platform.trust-ca.shmanages host-side trust of the local development CA..devcontainer/defines the repository development environment..local/contains generated local state, including sensitive Sealed Secrets key backups, and is excluded from Git.
Task provides the user-facing interface inside the Dev Container.
Reusable or non-trivial platform automation is implemented as shell
scripts under scripts/. Simple commands may be kept directly in the
Taskfile when doing so keeps the underlying operation visible and easy
to understand.
Host-specific operations that require privileges or modify the developer
machine are kept explicit rather than hidden inside task up.
The repository follows the broader design principles and architecture
decisions documented in the homelab repository.
In particular:
- keep the developer host lightweight
- prefer reproducible environments
- expose common operations through Task
- keep repository responsibilities explicit
- prefer open and portable technologies
- prefer standard APIs where practical
- use OpenTelemetry as the application-facing observability boundary
- keep observability backends replaceable behind that boundary
- never store plaintext workload secrets or private sealing keys in Git
- keep recovery-critical key material independent of disposable clusters
- evolve the platform incrementally
- validate small capabilities manually before automating them
- avoid abstractions before they solve a concrete problem
local-platform owns the platform itself.
Application source code belongs in application repositories, while
desired environment state and workload-specific routing belong in
local-environments.