Skip to content

Technology Stack

This is the complete inventory of what runs the lab, from bare hardware up to the applications on top. Versions are pinned in code (mostly the Dockerfile and the Helm chart references) and kept current by Renovate, so they are not repeated here.

The lab is built in five layers, each covered by a tutorial.

LayerTechnologyRole
0. HardwareLow-power x86_64 mini PCsThree or more nodes. Quiet, cheap to run.
1. Operating systemDebianInstalled unattended from a preseed.cfg.
2. VirtualisationProxmox VEType-1 hypervisor, configured with Ansible.
3. StorageTrueNASRuns as a Proxmox VM, serves storage over NFS/SMB network shares.
4. KubernetesTalos LinuxImmutable, API-driven nodes provisioned as VMs.

Proxmox earns its place by letting one box run both Kubernetes nodes and a storage VM, and by making clusters cheap to rebuild while iterating. Talos is the opposite of a general-purpose OS: no SSH, no shell, no package manager, just an API. That trade buys a small attack surface and fully declarative nodes.

There is no toolchain to install locally. A single container image (built by the Dockerfile) carries everything the layers need:

ToolUsed for
OpenTofuTerraform-compatible IaC engine.
TerragruntDRY wrapper around OpenTofu, wires up remote state.
AnsibleProxmox cluster setup and Kubernetes bootstrap.
TaskTask runner that drives the workflows.
talosctlTalos node and cluster management.
kubectlKubernetes CLI.
HelmRenders charts (through Kustomize).
KustomizeManifest assembly, with --enable-helm.
jqJSON wrangling in scripts.

A second image variant adds task-ui for a browser-based view of the tasks. There is deliberately no argocd or bws CLI: Argo CD is driven through kubectl, and Bitwarden access happens in-cluster through External Secrets.

OpenTofu provisions the VMs and Terragrunt keeps the configuration DRY, with remote state in a Cloudflare R2 bucket. The OpenTofu providers in use are:

  • bpg/proxmox for Proxmox VMs
  • siderolabs/talos for Talos machine config and bootstrap
  • maxlaverse/bitwarden for reading secrets during bootstrap
  • hashicorp/kubernetes, hashicorp/helm, hashicorp/local, hashicorp/time, hashicorp/null

Layer 2 uses the lae.proxmox Ansible role for the core cluster build, alongside custom roles for the rest: tailscale-bootstrap, btrfs-subvolume, proxmox-permissions, pve-resource-mappings (PCI passthrough), cert, and a handful of package and host hygiene roles. The same playbook also creates the Proxmox API user and token that Terragrunt later uses.

Everything inside the cluster is GitOps-managed. Argo CD watches this repository and applies it, using ApplicationSets to generate applications from the directory tree. Components live under kubernetes/cluster/active, with retired ones kept under inactive for reference.

Ingress has been through three generations. It started on ingress-nginx, then moved to the Gateway API fronted by Istio (written up on the Tower of Kubes blog). It now runs Envoy Gateway, adopted for its built-in OIDC support. Istio still lives under kubernetes/cluster/inactive for reference to the previous setup.

ComponentRole
CiliumeBPF-based CNI: pod networking, network policy, and Hubble observability.
Envoy GatewayCurrent ingress, a Gateway API implementation. Runs a Coraza WAF policy.
CoreDNSIn-cluster DNS.
external-dnsPublishes DNS records to Cloudflare, including a dynamic-DNS target.
Tailscale operatorExposes services and node access over the Tailnet.

cert-manager issues TLS certificates from Let’s Encrypt using the ACME DNS-01 challenge through Cloudflare. The letsencrypt-prod and letsencrypt-staging ClusterIssuers and the certificates themselves live in cert-resources.

The External Secrets Operator syncs secrets from Bitwarden Secrets Manager into the cluster. The single bootstrap token (BWS_ACCESS_TOKEN) is all that is needed to unlock the rest, so no application secrets are ever committed to Git.

OperatorProvides
CloudNativePGPostgreSQL clusters.
mariadb-operatorMariaDB databases.
OperatorProvides
kaniopKanidm, the cluster’s identity provider.

Longhorn provides the default storage class, with replicated block volumes for application state. The csi-driver-smb and csi-driver-nfs drivers mount shares from the TrueNAS VM for bulk data and media.

Add-onRole
metrics-serverResource metrics for autoscaling and kubectl top.
node-feature-discoveryLabels nodes by hardware capability.
intel-gpu-resource-driverExposes Intel iGPUs for hardware transcoding.
talos-cloud-controller-managerNode lifecycle integration for Talos.
spegelPeer-to-peer OCI registry mirror to speed up image pulls.
reloaderRestarts workloads when their config or secrets change.
reflectorMirrors secrets and config maps across namespaces.
tupprAutomates Talos and Kubernetes upgrades.
monitoringVictoriaMetrics K8s Stack.

The self-hosted apps live under kubernetes/cluster/active/apps; that directory is the source of truth as the set changes. As of writing it includes:

  • Media: Immich (photos), a Jellyfin-based media server, the *arr stack (servarr), Seerr (requests), qBittorrent, MeTube, and Pinepods (podcasts).
  • Knowledge and productivity: Outline (wiki), Mealie (recipes), Miniflux (RSS), Karakeep (bookmarks), and a books server.
  • Developer tooling: Forgejo (Git forge), OpenGist (snippets), Actions Runner Controller (CI runners), and a Renovate operator.
  • Utilities: Homepage (dashboard), IT-Tools, CyberChef, OmniTools, BentoPDF, and Transmute.

A few rules shaped most of these choices, and the design decisions page goes deeper on the decisions:

  • Open source wherever possible
  • Everything declarative, with Git as the single source of truth.