A Kubernetes cluster is easy to describe as a collection of nodes, manifests, and containers. That description is technically correct, but it misses the interesting part: the cluster only becomes a platform when useful workloads begin to depend on it.
My homelab runs k3s on Proxmox, but the virtual machines and Kubernetes control plane are only the foundation. The real stack begins after the cluster is healthy: networking, ingress, certificates, application delivery, persistent data, observability, and the small operational decisions that make self-hosting feel less like a weekend experiment and more like a dependable internal platform.
This is not a cloud-scale architecture, nor is it intended to imitate every managed service available from a major provider. It is a deliberately constrained environment designed to teach the same lessons: systems fail at boundaries, state deserves special care, and a deployment pipeline is only useful when rollback and diagnosis are practical.
The physical layer is virtual, but the constraints are real
Proxmox is the substrate for the lab. It gives me a place to define virtual machines with explicit CPU, memory, disk, and network boundaries before Kubernetes enters the picture. That separation matters. A workload requesting resources in Kubernetes still eventually consumes real capacity on a physical host, and no YAML manifest changes that fact.
I treat the Proxmox layer as the machine room of the stack. It is responsible for:
- Hosting the virtual machines that form the k3s cluster
- Providing snapshots and backup integration for infrastructure recovery
- Making CPU, memory, and storage allocation visible
- Keeping Kubernetes concerns separate from the underlying host lifecycle
This division has a useful operational consequence: a Kubernetes issue and a hypervisor issue are not automatically the same incident. If an application pod is failing, I can investigate the workload, its node, and its dependencies in order. If several nodes fail together, the Proxmox layer becomes a more likely starting point.
A homelab has limited capacity, which turns resource planning into an everyday design discipline. Overcommitting memory may work until a burst of activity creates contention. Giving every service persistent storage may sound prudent until backup size and recovery time become inconvenient. Constraints make trade-offs visible early, which is one reason a smaller environment is such a good platform for learning.
k3s is the platform contract
k3s provides the Kubernetes API and operational model that workloads target. Its lightweight distribution is well suited to a homelab because it reduces installation and maintenance overhead without removing the core Kubernetes concepts that matter: scheduling, Services, Deployments, Ingress, Secrets, ConfigMaps, namespaces, and persistent volumes.
The cluster is not merely a place where containers run. It is the contract between applications and infrastructure.
An application should be able to declare:
- Which container image it needs
- How many replicas should run
- Which configuration is environment-specific
- What network endpoint it exposes
- How it receives credentials
- What storage it requires
- How Kubernetes can determine whether it is healthy
That contract is valuable even when the cluster has only a few nodes. It encourages applications to be explicit about their dependencies instead of relying on undocumented host configuration or manually started containers.
Namespaces provide an important second boundary. I use them to separate concerns such as platform services, development tools, and application workloads. They are not a complete security model by themselves, but they make the cluster easier to navigate and reduce the tendency for every manifest to accumulate in one undifferentiated default namespace.
Traefik is where internal services become usable
A cluster can host a dozen services and still feel inaccessible if every application requires a different port number, local hostname, or ad hoc network workaround. Ingress is the layer that turns deployments into coherent web services.
Traefik acts as the front door for HTTP workloads. It receives requests, matches hostnames and paths, and forwards traffic to Kubernetes Services. That means application containers can focus on application behavior rather than binding directly to externally exposed ports.
The routing model also gives the stack a consistent shape. A service becomes available through a predictable hostname, protected by TLS, and represented by an Ingress resource stored alongside the rest of its deployment configuration.
I prefer this declarative approach over manually editing reverse-proxy configuration on a separate machine. When a service is removed, its route disappears with the deployment. When a new environment is introduced, its hostname is part of the same reviewable configuration as the application itself.
For internet-facing services, Cloudflare can provide an additional edge layer for DNS, certificate handling patterns, and protection from exposing the home network more broadly than necessary. The important principle is not that every service must be public. Most should not be. The important principle is that public exposure is intentional, narrow, and represented as configuration rather than a forgotten router rule.
Storage is the part that refuses to be abstract
Stateless services are forgiving. If a pod disappears, Kubernetes can create another one. Stateful services require a more careful definition of recovery.
Databases, queues, caches, and file-backed applications are where a homelab becomes operationally honest. A PersistentVolume claim may make storage convenient for a workload, but it does not answer the harder questions:
- Where does the data physically live?
- What happens if a node is lost?
- How is the data backed up?
- Can it be restored into a clean environment?
- Has that restore path ever been tested?
PostgreSQL is a common example. It is an excellent fit for many application workloads, but it should not be treated as just another disposable container. Its deployment needs durable storage, controlled credentials, regular backups, and a documented restore procedure. The same thinking applies to Redis when it is used for anything more consequential than an easily rebuilt cache.
In a homelab, simplicity often wins. A modest architecture with understandable backup and restore behavior is safer than a highly distributed design that is difficult to operate with limited time and hardware. The goal is not to reproduce enterprise complexity; it is to learn where complexity earns its keep.
Delivery begins in Git and ends with a running revision
A platform becomes significantly more useful when deployments stop depending on a person remembering a sequence of commands. GitHub Actions provides the automation boundary between source control and a deployable artifact.
A typical path through the stack looks like this:
- A change is pushed or merged into Git.
- GitHub Actions runs tests and builds a container image.
- The image is tagged with an immutable identifier, commonly a commit SHA or release version.
- The image is published to a registry.
- Kubernetes manifests are updated or applied to reference that exact image.
- The rollout is observed until the new revision is healthy.
The immutable image tag is more important than it first appears. A floating tag such as latest makes it difficult to answer a basic operational question: what code is actually running? A commit-derived version gives deployments, logs, pull requests, and container images a shared identity.
For smaller services, a simple pipeline is usually enough. Build, test, publish, deploy, and verify. The pipeline should be boring in the best sense: repeatable, inspectable, and easy to rerun. Complexity can be added later when the system has a demonstrated need for it.
Observability is a habit, not a dashboard collection
The most useful monitoring setup is one that helps answer questions during failure:
- Is the application running?
- Is Kubernetes scheduling it successfully?
- Is the node healthy?
- Can the service reach its database or dependency?
- Did a deployment change introduce the problem?
Logs are the first layer. Containerized applications should write useful structured output to standard output and standard error, allowing Kubernetes-native tooling to retrieve it consistently. Health probes are the second layer. Readiness probes prevent traffic from reaching an application before it can serve requests, while liveness probes provide a recovery signal when a process is stuck rather than simply slow.
Metrics and dashboards are valuable, but they should follow questions rather than precede them. In a resource-constrained homelab, collecting every possible metric can create more storage and maintenance work than insight. I prefer a focused view of node capacity, pod restarts, request errors, latency, and the health of stateful dependencies.
The stack is valuable because it has edges
The most educational parts of my k3s-on-Proxmox homelab are not the happy-path deployments. They are the edges between layers.
A certificate renewal problem crosses DNS, ingress, and application routing. A failed database connection crosses Kubernetes networking, Secret management, and stateful workload health. A pod that cannot schedule exposes the gap between requested resources and actual Proxmox capacity. A deployment that succeeds in CI but fails in the cluster reveals a missing assumption about configuration or runtime dependencies.
Those boundaries are where the stack earns its place. Each service is small enough to understand, but the system is connected enough to require real operational thinking.
That is the purpose of the lab: not to accumulate tools, but to build confidence operating a platform with clear layers, explicit dependencies, and recoverable failure modes. Proxmox provides the ground. k3s provides the platform contract. Traefik provides the front door. GitHub Actions provides delivery. Persistent storage provides the necessary caution. Together, they form a small environment that behaves like a serious one.
No comments yet. Be the first to share your thoughts!