A homelab becomes substantially more useful when it stops being a collection of experiments and starts behaving like a platform.
That is the goal of my k3s-on-Proxmox stack. It is not intended to imitate a hyperscale cloud environment node for node. Instead, it gives me a compact, failure-aware environment where I can deploy services, test infrastructure changes, run personal applications, and practice the operational habits that matter in production: clear boundaries, repeatable recovery, observability, and controlled change.
The central design choice is simple: Proxmox provides the machines; k3s provides the platform. Each layer has a deliberately narrow responsibility, which makes the whole system easier to reason about when something inevitably breaks.
The foundation: Proxmox as the physical control plane
Proxmox sits directly on the homelab hardware and acts as the infrastructure layer. It owns the concerns closest to the machines: virtual machines, CPU and memory allocation, networking, local storage, snapshots, backups, and host maintenance.
I use virtual machines rather than placing Kubernetes directly on the Proxmox hosts. That decision adds a layer of virtualization, but the trade-off is worthwhile. VMs give every Kubernetes node a clean operating-system boundary and make cluster-level operations less coupled to the hypervisor. I can resize a node, replace it, clone it for testing, or restore it without turning a routine Kubernetes task into a change on the physical host.
The VM boundary also makes failures more legible. If an application is unavailable, I can work down the stack in a predictable order:
- Is the Kubernetes workload healthy?
- Is the k3s node reachable?
- Is the VM running and correctly networked?
- Is the Proxmox host healthy?
- Is the underlying network or storage available?
That sequence may sound obvious, but it prevents a common homelab problem: troubleshooting every layer at once.
Each VM is intentionally modest. A homelab rarely has unlimited CPU or memory, so capacity planning is a practical discipline rather than a theoretical exercise. Control-plane nodes receive enough stable resources to avoid starvation, while worker nodes are where I absorb more variable workloads. Keeping workloads in VMs also lets me test resource pressure realistically: Kubernetes requests and limits mean more when the underlying machines have finite capacity.
A small k3s cluster, not a miniature enterprise
k3s is the Kubernetes distribution at the center of the stack. It preserves the Kubernetes API and operating model while reducing operational overhead for a small environment. Its lightweight footprint is especially appropriate when hardware, power, and attention are all limited resources.
The cluster is organized around clear roles. Control-plane nodes run the services needed to manage cluster state. Worker nodes run the applications. In a smaller setup, those roles can overlap when necessary, but I prefer keeping the separation visible even if the physical distinction is not absolute. It reinforces an important operational fact: the cluster's ability to schedule workloads and its ability to make decisions about workloads are related but different concerns.
I treat Kubernetes manifests as the desired state of the platform, not as a set of one-time deployment instructions. Deployments describe application replicas. Services describe stable internal access. ConfigMaps and Secrets keep configuration out of container images. PersistentVolumeClaims describe storage requirements without forcing every application to understand the details of the underlying disks.
That declarative model is one of the reasons the stack is valuable. When a node disappears, the desired state still exists. Kubernetes can reschedule eligible workloads, and I can focus on restoring capacity rather than manually rebuilding every service that used to be running there.
Networking: making internal services feel intentional
Networking is where a homelab can quickly become confusing. It is easy to accumulate port forwards, ad hoc hostnames, and services that are only reachable from one machine on the network. My stack avoids that by treating ingress as a first-class part of the platform.
Traefik is the primary ingress controller. It receives HTTP and HTTPS traffic, matches hostnames and paths, and forwards requests to Kubernetes Services. Applications do not need to expose host ports or know about the external network directly. They only need to provide a service inside the cluster.
This creates a clean traffic path:
The value of this path is not just convenience. It creates a consistent location for TLS termination, routing rules, middleware, redirects, and access policies. When I deploy a new internal application, its network shape is predictable: a Deployment, a Service, and an Ingress or Traefik route.
For externally reachable services, Cloudflare can sit at the edge for DNS and additional protection. That separation is useful because public traffic and private cluster networking have different risks. Public endpoints should be narrowly exposed, authenticated where appropriate, and treated as services that may receive hostile traffic. Internal tools should not automatically become internet-facing merely because they have a convenient hostname.
Storage: where the lab becomes real
Stateless applications are easy to recreate. Databases, queues, and media services are not. Storage is therefore the most important place to be conservative.
Proxmox provides the underlying virtual disks, while Kubernetes consumes storage through persistent volumes and claims. The exact storage implementation can evolve, but the architectural principle remains stable: application data must have a documented location, a backup path, and a recovery procedure.
For services such as PostgreSQL or Redis, I avoid treating a persistent volume as a backup strategy. A volume protects data from a pod restart; it does not protect against accidental deletion, corruption, failed upgrades, or a broader infrastructure event. Database-aware backups matter. For PostgreSQL, that means logical dumps or physical backup tooling, tested independently from the running workload.
Snapshots are useful, particularly before maintenance, but they are not a substitute for backups either. A snapshot can be tightly coupled to the storage system that created it. A backup should be recoverable somewhere else and should be validated by actually restoring it.
The most useful homelab question is not, “Do I have backups?” It is, “Can I restore this service to a known-good state without improvising?”
Containers and application delivery
Docker remains part of the workflow even though Kubernetes runs the workloads. Containers are built from application source code, tested locally or in CI, published to a registry, and then referenced by Kubernetes manifests.
This separation gives the deployment process a useful shape:
- Source code produces a versioned container image.
- CI validates the image and application behavior.
- Kubernetes deploys that immutable image reference.
- The cluster reports whether the new workload becomes healthy.
GitHub Actions is a natural fit for this pipeline. A typical workflow can run tests, build a Docker image, tag it with a commit SHA or release version, publish it, and update the deployment configuration. For a homelab, this is not overengineering. It is a way to make deployment history visible and repeatable.
I prefer image tags that identify a specific build rather than mutable tags such as latest. A deployment should answer a simple question: what code is running right now? Immutable tags make rollback and diagnosis much easier, especially when multiple services are changing over time.
Operating the stack: boring is a feature
The best outcome for a homelab platform is not constant novelty. It is controlled boredom.
That means monitoring node health, watching disk consumption, reviewing Kubernetes events, and keeping an eye on certificate renewals before they become incidents. It means maintaining an inventory of VMs, IP addresses, DNS records, namespaces, and persistent workloads. It means documenting the steps required to rebuild a node and periodically proving that the documentation is correct.
Maintenance is also part of the design. Proxmox host updates, guest operating-system patching, k3s upgrades, container image updates, and database migrations should not be performed as one giant change. Each layer has its own blast radius. Updating deliberately, with backups and rollback points, is more valuable than updating quickly.
The stack is small, but it teaches large-system lessons. Kubernetes does not eliminate complexity; it organizes complexity around APIs and declared state. Proxmox does not eliminate hardware concerns; it makes infrastructure more manageable. Backups do not guarantee recovery; tested restores do.
Why this stack matters
My k3s-on-Proxmox homelab is a practical environment for building and operating software under realistic constraints. It gives applications a consistent route from source code to container image to deployed service. It gives infrastructure a stable foundation beneath Kubernetes. And it creates enough complexity to learn meaningful operational patterns without requiring a datacenter.
Most importantly, it is a system I can change with intent. I can add an application, simulate a node failure, test a backup restore, introduce a new ingress rule, or rebuild a worker node. Every one of those actions strengthens the platform when the result is documented and repeatable.
That is the value of the stack: not that it runs Kubernetes at home, but that it turns a home environment into a place where infrastructure practice becomes tangible.
No comments yet. Be the first to share your thoughts!