K3s on Proxmox: Running a Production-Grade Kubernetes Cluster in Your Home Lab
Home Lab

K3s on Proxmox: Running a Production-Grade Kubernetes Cluster in Your Home Lab

Ricardo Gil
March 30, 2026
7 min read
#K3s #Kubernetes #Proxmox #Home Lab #Self-Hosting #Longhorn #Traefik

K3s on Proxmox: Running a Production-Grade Kubernetes Cluster in Your Home Lab

I've been running Kubernetes in production for years, but I always hesitated to bring it into my home lab. Full K8s felt like overkill for a few self-hosted services, and the maintenance overhead on a cluster I'd touch once a week seemed like a bad trade-off. Then I started using K3s on Proxmox VMs β€” and that calculation flipped completely.

K3s ships as a single 70MB binary. It passes all Kubernetes conformance tests. It runs comfortably on nodes with 1 CPU and 512MB RAM (though you'll want more for real workloads). It's maintained by Rancher/SUSE and actively developed. And when you pair it with Proxmox as your hypervisor, you get VM-level isolation, snapshots before upgrades, and easy node recreation β€” all on commodity hardware.

This guide walks through standing up a real multi-node K3s cluster on Proxmox: three VMs, persistent storage with Longhorn, ingress with Traefik, and remote access via Tailscale. No toy setup β€” this is how I actually run it.

---

Why K3s Over Full Kubernetes (and Why Not Just Docker Compose)

If you're running home lab services, Docker Compose works great right up until it doesn't. The breaking points I hit: no rolling updates, no built-in health-based rescheduling, manual SSL certificate management per service, no resource limits enforcement, and zero portability of workload definitions between machines.

K3s solves all of these. And unlike full Kubernetes, it:

  • Doesn't require etcd by default (uses SQLite for small clusters; you can add embedded etcd for HA)
  • Removes legacy and alpha APIs that add maintenance surface
  • Bundles Traefik, CoreDNS, Flannel, local-path provisioner β€” sensible defaults, zero extra installation
  • Upgrades with k3s-upgrade-controller β€” a single CRD application
  • The tradeoff vs. k8s: K3s makes some opinionated choices (Flannel CNI by default, Traefik ingress). You can swap these out, but if you need Calico/Cilium networking or you're chasing the absolute cutting edge of sig-network, full k8s may fit better. For a home lab running real workloads? K3s is the right call.

    ---

    Hardware Requirements and My Setup

    You don't need much. My cluster runs on a Beelink EQ12 Mini PC as the host, with three Proxmox VMs:

    | Node | Role | vCPU | RAM | Disk | |------|------|------|-----|------| | k3s-server-01 | Server (control plane) | 2 | 4GB | 32GB | | k3s-agent-01 | Agent (worker) | 2 | 4GB | 64GB | | k3s-agent-02 | Agent (worker) | 2 | 4GB | 64GB |

    The 64GB disks on the agents are what Longhorn will use for distributed storage. I'm using a Samsung 970 EVO Plus 500GB NVMe as the Proxmox host's primary drive, which gives enough IOPS for this to feel snappy.

    If you want to run this on dedicated hardware, the Minisforum MS-01 is an excellent 3-node-in-a-box option β€” dual 2.5GbE, an SFP+ port, and room for two NVMe drives per unit. The Beelink GTi14 Ultra is another solid choice if you want a single-host setup running multiple VMs.

    For RAM, if your mini PC supports it, upgrade to at least 32GB DDR5 so you have headroom for both VMs and the host OS.

    ---

    Step 1: Create the Proxmox VMs

    I use Ubuntu 24.04 LTS cloud images for K3s VMs. Download it once to Proxmox and create a template:

    bash
    # On the Proxmox host
    wget https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img

    Create VM template

    qm create 9000 --name ubuntu-2404-template --memory 2048 --net0 virtio,bridge=vmbr0 qm importdisk 9000 noble-server-cloudimg-amd64.img local-lvm qm set 9000 --scsihw virtio-scsi-pci --scsi0 local-lvm:vm-9000-disk-0 qm set 9000 --boot c --bootdisk scsi0 qm set 9000 --ide2 local-lvm:cloudinit qm set 9000 --serial0 socket --vga serial0 qm set 9000 --agent enabled=1 qm template 9000

    Then clone it three times:

    bash
    for i in 101 102 103; do
      qm clone 9000 $i --name k3s-node-$i --full
      qm set $i --memory 4096 --cores 2
      qm resize $i scsi0 +30G   # agents get +60G instead
    done

    Set cloud-init for each VM (SSH key, static IP, DNS):

    bash
    qm set 101 --ciuser ubuntu --sshkeys ~/.ssh/authorized_keys   --ipconfig0 ip=192.168.1.101/24,gw=192.168.1.1   --nameserver 192.168.1.1

    Start them up and make sure you can SSH in before proceeding.

    ---

    Step 2: Install K3s β€” Server Node First

    On k3s-server-01 (192.168.1.101):

    bash
    curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="server   --cluster-init   --tls-san 192.168.1.101   --disable traefik   --disable servicelb" sh -

    I disable the built-in Traefik and ServiceLB here because I'll install Traefik via Helm for more control, and I'll use MetalLB for LoadBalancer services on my LAN.

    Grab the node token for the agents:

    bash
    sudo cat /var/lib/rancher/k3s/server/node-token

    Verify the server is up:

    bash
    sudo k3s kubectl get nodes
    

    NAME STATUS ROLES AGE VERSION

    k3s-server-01 Ready control-plane,etcd,master 30s v1.32.x+k3s1

    ---

    Step 3: Join the Agent Nodes

    On each agent VM (replace and server IP):

    bash
    curl -sfL https://get.k3s.io | K3S_URL=https://192.168.1.101:6443   K3S_TOKEN=<TOKEN> sh -

    Back on the server, confirm both agents joined:

    bash
    sudo k3s kubectl get nodes
    

    NAME STATUS ROLES AGE VERSION

    k3s-server-01 Ready control-plane,etcd,master 5m v1.32.x+k3s1

    k3s-agent-01 Ready <none> 2m v1.32.x+k3s1

    k3s-agent-02 Ready <none> 1m v1.32.x+k3s1

    Copy the kubeconfig to your local machine:

    bash
    # On the Proxmox host or your workstation
    scp ubuntu@192.168.1.101:/etc/rancher/k3s/k3s.yaml ~/.kube/config-k3s
    

    Edit the server URL: change 127.0.0.1 to 192.168.1.101

    export KUBECONFIG=~/.kube/config-k3s kubectl get nodes # works from your laptop now

    ---

    Step 4: Persistent Storage with Longhorn

    The built-in local-path provisioner is fine for stateless workloads, but for anything with a database or file storage, you want Longhorn. It provides replicated block storage across your nodes β€” if an agent VM goes down, your data survives.

    Install via Helm:

    bash
    helm repo add longhorn https://charts.longhorn.io
    helm repo update
    helm install longhorn longhorn/longhorn   --namespace longhorn-system   --create-namespace   --set defaultSettings.defaultReplicaCount=2

    Wait for all pods to be Running:

    bash
    kubectl -n longhorn-system get pods --watch

    Set Longhorn as the default StorageClass:

    bash
    kubectl patch storageclass local-path   -p '{"metadata": {"annotations": {"storageclass.kubernetes.io/is-default-class": "false"}}}'
    kubectl patch storageclass longhorn   -p '{"metadata": {"annotations": {"storageclass.kubernetes.io/is-default-class": "true"}}}'

    For the storage to work well, make sure each agent node has that extra disk volume I mentioned. Longhorn auto-discovers /var/lib/longhorn by default. On my setup, I mount the extra Proxmox disk to that path:

    bash
    # On each agent, after formatting the extra disk as ext4
    sudo mkfs.ext4 /dev/sdb
    echo '/dev/sdb /var/lib/longhorn ext4 defaults 0 2' | sudo tee -a /etc/fstab
    sudo mount -a

    The Western Digital Blue 1TB SN580 NVMe is a cost-effective choice for Longhorn storage volumes β€” solid sequential throughput and competitive pricing. The Samsung 870 EVO 1TB SATA SSD works well too if your Proxmox setup is using SATA rather than NVMe passthrough.

    ---

    Step 5: Ingress with Traefik and cert-manager

    Install Traefik via Helm with a values file that suits a home lab:

    bash
    helm repo add traefik https://traefik.github.io/charts
    helm repo update

    cat << 'EOF' > traefik-values.yaml deployment: replicas: 1 service: type: LoadBalancer annotations: metallb.universe.tf/loadBalancerIPs: "192.168.1.200" additionalArguments: - "--certificatesresolvers.letsencrypt.acme.email=contact@gilricardo.com" - "--certificatesresolvers.letsencrypt.acme.storage=/data/acme.json" - "--certificatesresolvers.letsencrypt.acme.tlschallenge=true" EOF

    helm install traefik traefik/traefik --namespace traefik --create-namespace -f traefik-values.yaml

    For MetalLB (so LoadBalancer type services get real LAN IPs):

    bash
    helm repo add metallb https://metallb.github.io/metallb
    helm install metallb metallb/metallb -n metallb-system --create-namespace

    Configure IP pool

    kubectl apply -f - <<EOF apiVersion: metallb.io/v1beta1 kind: IPAddressPool metadata: name: homelab-pool namespace: metallb-system spec: addresses: - 192.168.1.200-192.168.1.220 --- apiVersion: metallb.io/v1beta1 kind: L2Advertisement metadata: name: homelab-l2 namespace: metallb-system EOF

    Now any LoadBalancer service gets a real IP in your 192.168.1.200-220 range β€” accessible from anywhere on your LAN.

    ---

    Step 6: Remote Access with Tailscale Operator

    The Tailscale Kubernetes Operator is a clean way to expose cluster services over Tailscale without a VPN client on every device. Install it:

    bash
    helm repo add tailscale https://pkgs.tailscale.com/helmcharts
    helm repo update
    helm upgrade --install tailscale-operator tailscale/tailscale-operator   --namespace=tailscale   --create-namespace   --set-string oauth.clientId=<TAILSCALE_CLIENT_ID>   --set-string oauth.clientSecret=<TAILSCALE_CLIENT_SECRET>   --wait

    Then annotate any service you want exposed over your Tailnet:

    yaml
    apiVersion: v1
    kind: Service
    metadata:
      name: my-app
      annotations:
        tailscale.com/expose: "true"
        tailscale.com/hostname: "my-app-k3s"
    spec:
      type: LoadBalancer
      ...

    The service gets a my-app-k3s hostname on your Tailnet β€” accessible from your phone, laptop, or any Tailscale-connected device. No port forwarding, no dynamic DNS, no certificate headaches for internal services.

    ---

    Gotchas I Hit (Save Yourself the Debugging)

    etcd disk I/O matters more than you'd think. If your control plane VM's disk is slow, etcd will be flaky. Use NVMe-backed storage for the server VM. I learned this the hard way when kubectl commands started hanging intermittently β€” the culprit was the server VM sitting on a slow SATA pool.

    Longhorn needs consistent node naming. If you recreate a VM and the hostname changes, Longhorn gets confused about which node owns which volume replicas. Set your hostnames in cloud-init and don't change them. I use k3s-agent-01 and k3s-agent-02 as static hostnames, not the default ubuntu cloud-init hostname.

    firewalld/ufw will break node communication. K3s nodes communicate on several ports (6443, 8472 UDP for Flannel VXLAN, 10250, etc.). Either configure firewall rules properly or (for a home lab) just disable ufw on the VMs: sudo ufw disable.

    Proxmox snapshots before any K3s upgrade. Run qm snapshot pre-k3s-upgrade before upgrading. K3s upgrades are usually smooth, but having a one-command rollback path costs you 30 seconds and has saved me twice.

    For a proper A/C-powered network switch to connect your mini PCs, the TP-Link TL-SG108E 8-Port is a cheap managed switch that handles VLAN segmentation if you want to isolate cluster traffic. The Netgear GS308E is another solid pick in the same tier.

    ---

    Deploying Your First Real Workload

    Here's a complete example deploying Gitea (self-hosted Git) on the cluster with a Longhorn PVC:

    yaml
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: gitea
      namespace: gitea
    spec:
      replicas: 1
      selector:
        matchLabels:
          app: gitea
      template:
        metadata:
          labels:
            app: gitea
        spec:
          containers:
          - name: gitea
            image: gitea/gitea:latest
            ports:
            - containerPort: 3000
            - containerPort: 22
            volumeMounts:
            - name: gitea-data
              mountPath: /data
          volumes:
          - name: gitea-data
            persistentVolumeClaim:
              claimName: gitea-pvc
    ---
    apiVersion: v1
    kind: PersistentVolumeClaim
    metadata:
      name: gitea-pvc
      namespace: gitea
    spec:
      accessModes:
        - ReadWriteOnce
      storageClassName: longhorn
      resources:
        requests:
          storage: 20Gi
    ---
    apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
      name: gitea
      namespace: gitea
      annotations:
        traefik.ingress.kubernetes.io/router.entrypoints: websecure
        traefik.ingress.kubernetes.io/router.tls.certresolver: letsencrypt
    spec:
      rules:
      - host: git.yourdomain.com
        http:
          paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: gitea
                port:
                  number: 3000

    Apply it and Traefik handles the TLS certificate automatically. Longhorn replicates the /data volume across your two agent nodes. If one agent goes down, K3s reschedules the pod to the surviving node and Longhorn serves the volume from the replica.

    That's the full production story in one YAML file.

    ---

    Is It Worth It?

    For someone already running Proxmox, yes β€” absolutely. The K3s setup takes about two hours from fresh VMs to a working cluster with storage and ingress. After that, deploying new services is kubectl apply -f and they get automatic TLS, resource limits, health monitoring, and log aggregation for free.

    The overhead is real: Longhorn alone runs about 10 pods per node, and the control plane eats ~1.5GB of RAM at rest. But on a machine with 32GB RAM split across VMs, that's a rounding error.

    The bigger win is that your home lab now runs the same primitives as work. When I'm debugging a Kubernetes issue at my day job, I can reproduce it locally in minutes β€” not on a shared staging environment, but on hardware I control completely.

    ---

    Disclosure: This post contains affiliate links. If you purchase through these links, I may earn a small commission at no extra cost to you.

    πŸ“¬Weekly Newsletter

    Get the best home lab & AI content

    No spam. One email per week. Unsubscribe anytime.

    Share this article