K3s Homelab

A production-grade Kubernetes cluster on a Mac Mini M1. Provisioned, hardened, monitored, and operated by an AI infrastructure assistant. This page is served from the cluster it documents.

Noah Frost February 2026 Live Dashboards ↗

Architecture

Hardware

ComponentSpecification
MachineMac Mini (Macmini9,1)
ChipApple M1 — 8 cores (4 performance + 4 efficiency)
Memory8 GB
Storage228 GB SSD
OSmacOS 26.2 (Darwin 25.2.0, arm64)

Stack

LayerTechnologyVersion
Container RuntimeDocker Desktopv29.2.1
KubernetesK3s via k3dv1.33.6+k3s1
IngressTraefikv3.6.8
MonitoringPrometheus + Grafanakube-prometheus-stack v82.2.0
TunnelCloudflare Tunnel (QUIC)cloudflared 2026.2.0
DNS & TLSCloudflareUniversal SSL
OperatorJarvis — AI infrastructure assistant

Traffic Flow

Internet → Cloudflare Edge (TLS termination)
        → Cloudflare Tunnel (QUIC, outbound-only)
        → cloudflared pod (tunnel namespace)
        → ClusterIP Service
        → Application pod (workloads / monitoring namespace)

Zero inbound ports. No public IP exposure. All external traffic enters through an outbound-only tunnel to Cloudflare's edge network, routing directly to ClusterIP services. Traefik handles internal ingress routing.

Cluster Provisioning

K3s is a lightweight, CNCF-conformant Kubernetes distribution. It is Linux-only — k3d runs it inside Docker containers on macOS, transparent to all Kubernetes operations.

$ k3d cluster create homelab \
    --port "80:80@loadbalancer" \
    --port "443:443@loadbalancer" \
    --k3s-arg "--disable=traefik@server:0" \
    --wait

Default Traefik disabled at cluster creation. A manually configured instance is deployed later with HTTP-to-HTTPS redirect, TLS, and the API dashboard disabled.

$ kubectl get nodes -o wide
NAME                   STATUS   ROLES                  VERSION        INTERNAL-IP   OS-IMAGE
k3d-homelab-server-0   Ready    control-plane,master   v1.33.6+k3s1   172.18.0.2    K3s v1.33.6+k3s1
kubectl get nodes showing single node in Ready state
Single-node cluster — K3s v1.33.6+k3s1 on Apple Silicon via k3d

Security Hardening

Namespace Isolation

Four dedicated namespaces with Kubernetes Pod Security Standards (PSS) enforced at the namespace level via labels.

NamespacePSS LevelPurpose
workloadsRestrictedApplication pods — non-root, read-only rootfs, all capabilities dropped
monitoringPrivilegedPrometheus, Grafana, node-exporter — requires host access for metrics collection
tunnelBaselineCloudflare Tunnel connector — outbound-only traffic to Cloudflare edge
ingressBaselineTraefik ingress controller
kubectl get namespaces showing PSS enforcement levels per namespace
Eight namespaces — PSS enforcement levels: restricted, privileged, and baseline

Network Policies

Default deny ingress and egress on all workload and monitoring namespaces. Every permitted traffic flow is an explicit, scoped rule.

RuleNamespacePermits
default-deny-ingressworkloads, monitoringBlock all unlisted ingress
default-deny-egressworkloadsBlock all unlisted egress
allow-tunnel-to-buildlogworkloadsTunnel → build-log pod on port 8080
allow-tunnel-to-grafanamonitoringTunnel → Grafana pod on port 3000
allow-ingress-to-buildlogworkloadsTraefik → build-log pod on port 8080
allow-ingress-to-grafanamonitoringTraefik → Grafana pod on port 3000
allow-dns-egressworkloadsDNS resolution via kube-system
allow-tunnel-dnstunnelDNS resolution, egress to workloads:8080 and monitoring:3000, QUIC to Cloudflare edge
allow-monitoring-internalmonitoringInter-pod communication within monitoring
allow-monitoring-egress-kubesystemmonitoringMonitoring → kube-system for kubelet and API server metrics
allow-prometheus-scrapeworkloadsPrometheus → workload pods for metrics
Network policies across namespaces
12 network policies — default deny with explicit allow rules

RBAC & Pod Security

ControlImplementation
Service accountsDedicated per workload — build-log-sa for the build log, scoped accounts for monitoring components
Cluster-adminNo workload runs with cluster-admin privileges
Monitoring accessRead-only ClusterRole — nodes, pods, services, metrics endpoints
Non-root executionEnforced by PSS restricted level (UID 101)
Read-only root filesystemEnforced. Writable paths via emptyDir volumes only.
CapabilitiesAll dropped. No privilege escalation.

Observability

Deployed via kube-prometheus-stack Helm chart — Prometheus, Grafana, node-exporter, kube-state-metrics, and the Prometheus Operator in a single deployment.

$ helm install monitoring prometheus-community/kube-prometheus-stack \
    --namespace monitoring \
    --values manifests/monitoring/kube-prometheus-values.yaml \
    --wait
ComponentPurpose
PrometheusMetrics collection and storage. 7-day retention, 5Gi persistent volume.
GrafanaDashboards. Anonymous read-only access enabled. 28 dashboards provisioned.
Node ExporterHost-level hardware and OS metrics
Kube State MetricsKubernetes object state as Prometheus metrics
Prometheus OperatorManages Prometheus configuration via CRDs
$ kubectl get pods -n monitoring
NAME                                                   READY   STATUS    RESTARTS   AGE
monitoring-grafana-67b8c4968d-zx775                    3/3     Running   0          14m
monitoring-kube-prometheus-operator-5888c6f4d6-z467s   1/1     Running   1          17h
monitoring-kube-state-metrics-66dddb6c8f-mxh9d         1/1     Running   1          17h
monitoring-prometheus-node-exporter-k766h              1/1     Running   1          16h
prometheus-monitoring-kube-prometheus-prometheus-0     2/2     Running   3          17h
All monitoring pods running
Five monitoring components — all healthy, stable through cluster lifecycle

Grafana is publicly accessible at dashboard.nfroze.co.uk with anonymous read-only access. 28 dashboards total — one custom Cluster Overview homepage, 27 provisioned by the Helm chart covering compute resources, networking, API server, kubelet, and Prometheus self-monitoring.

curl showing HTTP 200 response from dashboard.nfroze.co.uk via Cloudflare edge
Grafana live — HTTP 200 via Cloudflare edge, publicly accessible without authentication
Grafana Cluster Overview dashboard showing live metrics
Cluster Overview dashboard — 12 panels covering uptime, pod count, node status, restarts, scrape health, disk usage, CPU, memory, network I/O, and pod distribution by namespace

Build Log Deployment

This site runs as an Nginx container in the workloads namespace. The static files are baked into a custom Docker image — a single, versioned, immutable artefact.

FROM nginxinc/nginx-unprivileged:1.27-alpine

COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY index.html /usr/share/nginx/html/
COPY css/ /usr/share/nginx/html/css/
COPY images/ /usr/share/nginx/html/images/

EXPOSE 8080

The unprivileged Nginx variant runs as UID 101 on port 8080 — compliant with the restricted Pod Security Standard without modification. Security headers (X-Frame-Options, X-Content-Type-Options, CSP, Referrer-Policy) are set in the Nginx configuration.

Pod Security ControlValue
runAsNonRoottrue
runAsUser101
readOnlyRootFilesystemtrue
allowPrivilegeEscalationfalse
capabilitiesdrop: ALL
seccompProfileRuntimeDefault
$ kubectl get pods -n workloads
NAME                        READY   STATUS    RESTARTS   AGE
build-log-cc4948cdb-jcqwm   1/1     Running   0          18m

$ curl -s -o /dev/null -w "HTTP %{http_code}" https://k3s.nfroze.co.uk
HTTP 200

AI Operations

Jarvis is an AI infrastructure operator with full shell access to the host and the Kubernetes API. Cluster health queries are handled in real time via Discord — live kubectl output, not cached responses.

Discord conversation showing live cluster health query and response
Live health check — 11 pods across 5 namespaces, all running
This page is served from the cluster described above