DEV Community

Cover image for How to Properly Set Up K3s on Your Homelab or Server (2026 Edition)
Precious Okpor
Precious Okpor

Posted on

How to Properly Set Up K3s on Your Homelab or Server (2026 Edition)

I recently migrated my homelab from Docker + Jenkins to K3S because it was my production infrastructure that runs K3S on AWS, and my homelab should mirror that same flow.

This guide documents exactly how I set up K3S, MetalLB, Traefik, cert-manager with Let's Encrypt, and ArgoCD. All on a single Ubuntu 26.04 VM.

One important thing most 2025 tutorials won't tell you is that ingress-nginx was archived on March 24, 2026, which means no more releases or security patches. The good news is K3S ships Traefik v3 as its default ingress controller, so you get a maintained, production-grade ingress out of the box.

What We're Building

Component Role
K3s v1.33 Lightweight Kubernetes runtime
MetalLB v0.16.1 LoadBalancer IP assignment for bare metal
Traefik v3 Ingress controller (bundled with K3s)
cert-manager Automated TLS certificate management
Let's Encrypt (HTTP-01) Free, trusted certificates
ArgoCD GitOps continuous delivery
Helm 3 Package manager

Prerequisites

  • A VM or server running Ubuntu 24.04 and above
  • A public domain with an A record pointing to your server's public IP (Optional)
  • Port 80 and 443 open on your server/firewall (required for Let's Encrypt HTTP-01 challenge)
  • A local IP subnet for MetalLB (e.g. 192.168.1.0/24) — adjust to match your network

Step 1: System Prep

Before installing anything, get the system into the right state.

# Update system
sudo apt update && sudo apt upgrade -y

# Install dependencies
sudo apt install -y curl wget git open-iscsi nfs-common

# Disable swap (Kubernetes requires this)
sudo swapoff -a
sudo sed -i '/ swap / s/^\(.*\)$/#\1/g' /etc/fstab

# Load required kernel modules
sudo modprobe overlay
sudo modprobe br_netfilter

# Persist kernel modules across reboots
cat <<EOF | sudo tee /etc/modules-load.d/k3s.conf
overlay
br_netfilter
EOF

# Set required sysctl parameters
cat <<EOF | sudo tee /etc/sysctl.d/k3s.conf
net.bridge.bridge-nf-call-iptables  = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward                 = 1
EOF

sudo sysctl --system
Enter fullscreen mode Exit fullscreen mode

Step 2: Install K3s

K3s ships with its own built-in ServiceLB (Klipper). We disable it because MetalLB will handle LoadBalancer IP assignment instead. Traefik stays on.

curl -sfL https://get.k3s.io | INSTALL_K3S_CHANNEL=stable sh -s - \
  --disable servicelb \
  --write-kubeconfig-mode 644

# Wait for node to be ready
sudo kubectl wait --for=condition=ready node --all --timeout=120s

# Verify
sudo kubectl get nodes
sudo kubectl get pods -A
Enter fullscreen mode Exit fullscreen mode

Set up kubeconfig for your non-root user:

mkdir -p ~/.kube
sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config
sudo chown $USER:$USER ~/.kube/config
chmod 600 ~/.kube/config

# Persist in shell
echo 'export KUBECONFIG=~/.kube/config' >> ~/.bashrc
source ~/.bashrc

# Confirm it works
kubectl get nodes
Enter fullscreen mode Exit fullscreen mode

You should see your node in Ready state.

Step 3: Install Helm

curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash

# Verify
helm version
Enter fullscreen mode Exit fullscreen mode

Step 4: Install MetalLB

On bare metal and VMs, LoadBalancer type services stay stuck in <pending> forever without a load balancer controller. MetalLB fixes this by assigning real IPs from a pool you define.

# Add the MetalLB Helm repo
helm repo add metallb https://metallb.github.io/metallb
helm repo update

# Install
helm install metallb metallb/metallb \
  --namespace metallb-system \
  --create-namespace \
  --wait

# Confirm pods are running
kubectl get pods -n metallb-system
Enter fullscreen mode Exit fullscreen mode

Now configure the IP address pool. Pick a range from your local subnet that is outside your router's DHCP range to avoid conflicts. Adjust 192.168.1.200-192.168.1.220 to match your network:

cat <<EOF | kubectl apply -f -
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
spec:
  ipAddressPools:
  - homelab-pool
EOF

# Verify
kubectl get ipaddresspool -n metallb-system
kubectl get l2advertisement -n metallb-system
Enter fullscreen mode Exit fullscreen mode

Step 5: Verify Traefik

K3S already installed Traefik v3 during setup. You don't need to install it separately. Just confirm it's running and that MetalLB has assigned it an IP:

# Traefik should be running in kube-system
kubectl get pods -n kube-system | grep traefik

# The service should now have an EXTERNAL-IP from your MetalLB pool
kubectl get svc traefik -n kube-system
Enter fullscreen mode Exit fullscreen mode

You should see an IP in the 192.168.1.200-220 range (or whichever range you set) in the EXTERNAL-IP column. That's MetalLB doing its job.

Step 6: Install cert-manager

cert-manager automates the full lifecycle of TLS certificates — requesting, renewing, and storing them as Kubernetes secrets. We use the HTTP-01 challenge, which means Let's Encrypt will verify your domain by making an HTTP request to your server.

# Add the Jetstack Helm repo
helm repo add jetstack https://charts.jetstack.io
helm repo update

# Install cert-manager with CRDs
helm install cert-manager jetstack/cert-manager \
  --namespace cert-manager \
  --create-namespace \
  --set crds.enabled=true \
  --wait

# Verify all 3 pods are running: controller, cainjector, webhook
kubectl get pods -n cert-manager
Enter fullscreen mode Exit fullscreen mode

Now create a ClusterIssuer to tell cert-manager how to obtain certificates:

cat <<EOF | kubectl apply -f -
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: your@email.com        # ← replace with your email
    privateKeySecretRef:
      name: letsencrypt-prod-key
    solvers:
    - http01:
        ingress:
          class: traefik
EOF

# Verify the issuer is ready
kubectl get clusterissuer letsencrypt-prod
Enter fullscreen mode Exit fullscreen mode

The STATUS should show True under READY.

Step 7: Install ArgoCD

ArgoCD gives you GitOps-based deployments — your cluster state is driven by Git, not manual kubectl apply commands. It's the right pattern for a homelab that mirrors how production should work.

# Add the Argo Helm repo
helm repo add argo https://argoproj.github.io/argo-helm
helm repo update

# Install ArgoCD
helm install argocd argo/argo-cd \
  --namespace argocd \
  --create-namespace \
  --set server.service.type=LoadBalancer \
  --wait

# Verify pods are running
kubectl get pods -n argocd

# Get the external IP (assigned by MetalLB)
kubectl get svc argocd-server -n argocd

# Get the initial admin password
kubectl -n argocd get secret argocd-initial-admin-secret \
  -o jsonpath="{.data.password}" | base64 -d && echo
Enter fullscreen mode Exit fullscreen mode

Access the ArgoCD UI at https://<EXTERNAL-IP> with username admin and the password above. Change the password after the first login.

Step 8: Smoke Test

Deploy a quick test app to confirm the full stack is working end to end:

# Deploy a test nginx pod
kubectl create deployment test-app --image=nginx:alpine

# Expose it as a LoadBalancer service
kubectl expose deployment test-app \
  --port=80 \
  --type=LoadBalancer \
  --name=test-app-svc

# Watch for MetalLB to assign an external IP
kubectl get svc test-app-svc --watch

# Once an IP appears, curl it
curl http://<EXTERNAL-IP>

# Clean up
kubectl delete deployment test-app
kubectl delete svc test-app-svc
Enter fullscreen mode Exit fullscreen mode

If you get an nginx welcome page, everything is wired up correctly.

How to Deploy an App with TLS

Once you deploy a real app, here's what an Ingress resource looks like using Traefik + cert-manager. Just add the annotation, and cert-manager handles the certificate request automatically:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-app
  namespace: default
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
  ingressClassName: traefik
  rules:
  - host: myapp.yourdomain.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: my-app-svc
            port:
              number: 80
  tls:
  - hosts:
    - myapp.yourdomain.com
    secretName: myapp-tls
Enter fullscreen mode Exit fullscreen mode

Apply it, and cert-manager will automatically:

  1. Detect the cert-manager.io/cluster-issuer annotation
  2. Create a Certificate resource
  3. Complete the HTTP-01 challenge with Let's Encrypt
  4. Store the issued cert in the myapp-tls secret
  5. Renew it automatically before expiry

What's Next

With this foundation in place, here's what I'd layer on next:

  • Prometheus + Grafana: observability for the cluster (you already have prom-client experience, this maps directly)
  • Gitea + Woodpecker CI: lightweight self-hosted CI that replaces Jenkins
  • Harbor: private container registry running inside the cluster
  • Longhorn: persistent storage for stateful workloads

The goal is a homelab that serves as a direct rehearsal environment for production.

Summary

System prep → K3s → Helm → MetalLB → Traefik (built-in) → cert-manager → ArgoCD
Enter fullscreen mode Exit fullscreen mode

Every piece here is actively maintained in 2026. No ingress-nginx, no Klipper ServiceLB conflicts, no Jenkins.

If this helped you, drop a comment with what you're running on your homelab. Always curious what other engineers are self-hosting.

Top comments (0)