← All projects

Cloud-Native Homelab

I built a Proxmox homelab with Docker, k3s, private service access, and live portfolio telemetry, then used it to complete real development and real troubleshooting

techCloud EngineeringHomelabTailscaleZeroTrustProjects
Cloud-Native Homelab

I wanted my homelab to support the projects I was building: somewhere to develop an application, somewhere to deploy it, and a way to understand what the infrastructure was doing. Getting those pieces to work together gave the lab a purpose beyond installing tools to try them out.

I built the environment around Proxmox, a Docker Compose services stack, and a three-node k3s cluster. The work extends beyond getting containers to start. It includes private access, DNS, HTTPS routing, monitoring, and a live telemetry feed on this portfolio.

Stack: Proxmox VE, Ubuntu, Docker Compose, k3s, Tailscale, Traefik, Pi-hole, Unbound, Prometheus, Grafana, Loki, and Grafana Alloy.

One host, several jobs

The lab runs on a Dell OptiPlex with an Intel i5, 32 GB of RAM, and a 1 TB SSD. Proxmox lets me divide that hardware into environments with different jobs: a services VM for the Docker stack, development and practice VMs, LXC containers for Jellyfin and Samba, and three VMs for Kubernetes.

The k3s cluster has one control-plane node and two workers. Google's Online Boutique gives it a multi-service workload to run while I work on cluster operations and telemetry. Ops-Status is a separate application project I plan to deploy here as its delivery workflow develops.

That separation gives me room to experiment, but it has a limit: every VM still depends on one physical machine. Three Kubernetes nodes let me practice scheduling and node operations; they do not protect the lab from losing the Proxmox host.

Making the lab usable day to day

Tailscale provides authenticated private connectivity for remote administration. Traefik handles hostname-based routing and TLS for service URLs, while Pi-hole and Unbound handle local DNS, filtering, and recursive resolution. Each has a different job: reaching a device, resolving a name, and routing an HTTPS request are separate parts of getting to a service.

Dashy brings the daily operations view together. I organized it into a Command Deck for service links, Mission Control for operational status, and Incoming Transmissions for collapsible technical feeds.

image

The captured dashboard shows Pi-hole activity, ten Uptime Kuma checks reporting Up, and seven VMs reporting running status. Those are observations from that moment; VM state, service checks, and application health answer different questions.

Getting this view working included resolving a Pi-hole widget data-fetch error and restoring Proxmox access through its friendly HTTPS URL without a browser certificate warning. The useful result is a single place to open services and check their status while working on the lab.

Bringing live evidence onto the portfolio

I wanted visitors to see something coming from the running environment. That meant connecting local monitoring to a public website while keeping the management interfaces private.

Prometheus collects host, container, proxy, DNS, and Kubernetes state metrics. Recording rules turn selected measurements into aggregate portfolio:* metrics. Grafana Alloy collects those approved series through federation and sends them outbound to Grafana Cloud. The portfolio uses that data for its own telemetry display.

The card shows services-host CPU, memory, and disk utilization, running containers, Prometheus scrape health, and Kubernetes counts for ready nodes, running pods, and namespaces.

(add screenshot of the live portfolio telemetry card showing host gauges and Kubernetes counts)

The host gauges describe the services VM, not total Proxmox utilization. Scrape health describes metrics collection, not the availability of every application.

The public feed is deliberately narrower than the local monitoring view. The design keeps raw hostnames, addresses, and workload identifiers out of the published measurements. A metric name alone does not provide that protection; the aggregation and labels need to be reviewed as part of the configuration.

When the monitoring found a problem

The telemetry card showed five of six scrape targets healthy. The failing target was the Pi-hole exporter, which was receiving HTTP 401 responses after an authentication change.

For an investigation like this, a small PromQL query helps narrow the search:

Plain Text
up == 0

It returns targets whose latest scrape failed. It does not identify the cause or prove that the underlying application is down; the exporter response supplies the next piece of evidence.

A dedicated Pi-hole application password restored exporter access. Prometheus returned the target to healthy state, and the public card returned to six of six. That checked the repair through the collection and publishing path, rather than stopping at a container reporting that it was running.

(add screenshot of the Pi-hole exporter target healthy in Prometheus, with credentials and identifying labels hidden)

What I would improve next

The infrastructure is running, and the dashboard and public telemetry are useful now. The next step is preserving the configuration and the reasoning behind it in a sanitized repository: architecture, service inventory, monitoring rules, and practical runbooks.

There are also operational gaps to close. I still need to document and test backup and recovery procedures, capture the exact DNS and TLS paths, review access boundaries, and add configuration validation and tested image versions. Infrastructure as Code and configuration management are later improvements.

The lab gives me a connected environment to operate. A change to authentication can surface in an exporter, a failed scrape can appear on the portfolio, and a repair can be checked across that whole path. Those connections are what make the environment useful for the work I want to do.

View the live telemetry

The GitHub repository link will be added when the sanitized documentation repository is ready.