Kubernetes Storage, Ingress, And Exposure
Add Longhorn for persistent volumes, keep Traefik under control, advertise service IPs with MetalLB, and issue internal or public certificates with cert-manager.
Published January 20, 2025 · Updated January 31, 2025
Kubernetes Storage, Ingress, And Exposure
Once the nodes are Ready, the cluster still needs three things before it feels like infrastructure instead of a demo:
- storage that survives pod movement
- a way to hand real LAN IPs to services
- a certificate story that does not collapse into one-off YAML and wishful thinking
This page keeps those concerns together because they are tightly coupled in a small homelab cluster.
Storage - Longhorn
Longhorn replaces the built-in local-path storage class with a more serious replicated storage layer.
Why Longhorn?
| Feature | local-path (built-in) | Longhorn |
|---|---|---|
| Replication | ❌ Single node | ✅ Configurable replicas |
| Web GUI | ❌ None | ✅ Full dashboard |
| Snapshots | ❌ None | ✅ Scheduled snapshots |
| Backup | ❌ None | ✅ NFS/S3 backup targets |
| Live migration | ❌ Node-bound | ✅ Volumes move with pods |
Prerequisites Check
Verify open-iscsi and nfs-common are running on all nodes:
for NODE_IP in 192.168.50.60 192.168.50.61 192.168.50.62; do
echo "=== $NODE_IP ==="
ssh root@$NODE_IP "systemctl is-active iscsid && echo 'iscsi OK' || echo 'iscsi FAILED'"
doneInstall Longhorn Via Helm
Run from your workstation:
# Add the Longhorn Helm repository
helm repo add longhorn https://charts.longhorn.io
helm repo update
# Create the namespace
kubectl create namespace longhorn-system
# Install Longhorn
helm install longhorn longhorn/longhorn \
--namespace longhorn-system \
--set defaultSettings.defaultReplicaCount=2 \
--set defaultSettings.storageMinimalAvailablePercentage=15 \
--set service.ui.type=ClusterIPWait For Longhorn To Be Ready
kubectl -n longhorn-system rollout status deploy/longhorn-manager
kubectl -n longhorn-system get podsAll pods should be Running within 3-5 minutes.
Set Longhorn As The Default StorageClass
# Remove default flag from local-path
kubectl patch storageclass local-path \
-p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"false"}}}'
# Set Longhorn as default
kubectl patch storageclass longhorn \
-p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'
# Verify
kubectl get storageclassExpected output:
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
local-path rancher.io/local-path Delete WaitForFirstConsumer false 20m
longhorn (default) driver.longhorn.io Delete Immediate true 5mAccess Longhorn Dashboard
# Port-forward to access the Longhorn UI
kubectl -n longhorn-system port-forward svc/longhorn-frontend 8080:80Open http://localhost:8080 in your browser to view volumes and storage health.
Ingress - Traefik
k3s already bundles Traefik. The goal here is not to install another ingress controller. It is to verify the one already present and shape it into a more predictable HTTPS posture.1
Verify Traefik Is Running
kubectl -n kube-system get pods -l app.kubernetes.io/name=traefik
kubectl -n kube-system get svc traefikConfigure Traefik For HTTP To HTTPS Redirect
cat > /tmp/traefik-values.yaml << 'EOF'
globalArguments:
- "--global.sendanonymoususage=false"
additionalArguments:
- "--serversTransport.insecureSkipVerify=true"
- "--log.level=INFO"
ports:
web:
redirectTo:
port: websecure
websecure:
tls:
enabled: true
ingressRoute:
dashboard:
enabled: true
matchRule: Host(`traefik.local`)
entryPoints: ["websecure"]
EOF
helm upgrade traefik traefik/traefik \
--namespace kube-system \
--values /tmp/traefik-values.yamlExample Ingress Resource
# example-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-app
namespace: default
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: websecure
traefik.ingress.kubernetes.io/router.tls: "true"
spec:
ingressClassName: traefik
rules:
- host: myapp.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-app-svc
port:
number: 80
tls:
- hosts:
- myapp.example.com
secretName: myapp-tlsLoad Balancer - MetalLB
MetalLB is the piece that makes LoadBalancer services feel native on the LAN instead of cloud-only.
Reserve An IP Range
Choose a range of IPs on your LAN that is outside your DHCP range and not in use. Example: 192.168.50.200-192.168.50.220.
In the router, make sure that range is excluded from dynamic allocation.
Install MetalLB Via Helm
helm repo add metallb https://metallb.github.io/metallb
helm repo update
kubectl create namespace metallb-system
helm install metallb metallb/metallb \
--namespace metallb-system \
--waitConfigure The IP Address Pool
cat > /tmp/metallb-config.yaml << 'EOF'
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: homelab-pool
namespace: metallb-system
spec:
addresses:
- 192.168.50.200-192.168.50.220
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: homelab-l2
namespace: metallb-system
spec:
ipAddressPools:
- homelab-pool
EOF
kubectl apply -f /tmp/metallb-config.yamlVerify MetalLB
kubectl -n metallb-system get pods
kubectl -n metallb-system get ipaddresspoolsTest: Expose A Service With A Real IP
kubectl create deployment nginx-test --image=nginx:alpine
kubectl expose deployment nginx-test --port=80 --type=LoadBalancer
# Watch for an EXTERNAL-IP to be assigned
kubectl get svc nginx-test --watchExpected output (after 10-30 seconds):
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx-test LoadBalancer 10.43.12.34 192.168.50.200 80:31234/TCP 30sTest from your workstation:
curl http://192.168.50.200
# Should return the nginx welcome pageCleanup:
kubectl delete deploy nginx-test
kubectl delete svc nginx-testCertificate Management - cert-manager
cert-manager gives the cluster a certificate lifecycle instead of a pile of handwritten secrets.
Install cert-manager Via Helm
helm repo add jetstack https://charts.jetstack.io
helm repo update
kubectl create namespace cert-manager
helm install cert-manager jetstack/cert-manager \
--namespace cert-manager \
--set crds.enabled=true \
--waitVerify Installation
kubectl -n cert-manager get podsAll pods should be Running.
Option A: Self-Signed Certificates
Suitable for internal homelab services.
cat > /tmp/selfsigned-issuer.yaml << 'EOF'
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: selfsigned-issuer
spec:
selfSigned: {}
---
# Homelab CA — issue a CA cert, then sign with it for trusted certs across devices
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: homelab-ca
namespace: cert-manager
spec:
isCA: true
commonName: homelab-ca
secretName: homelab-ca-secret
duration: 87600h # 10 years
issuerRef:
name: selfsigned-issuer
kind: ClusterIssuer
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: homelab-ca-issuer
spec:
ca:
secretName: homelab-ca-secret
EOF
kubectl apply -f /tmp/selfsigned-issuer.yamlOption B: Let's Encrypt
# Replace email and ensure your domain points to your cluster's external IP
cat > /tmp/letsencrypt-issuer.yaml << 'EOF'
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: your-email@example.com # ← Replace
privateKeySecretRef:
name: letsencrypt-prod
solvers:
- http01:
ingress:
ingressClassName: traefik
EOF
kubectl apply -f /tmp/letsencrypt-issuer.yamlUsing A Certificate In An Ingress
Add the cert-manager.io/cluster-issuer annotation to your Ingress:
metadata:
annotations:
cert-manager.io/cluster-issuer: "homelab-ca-issuer" # or letsencrypt-prodcert-manager will automatically issue and renew the certificate referenced by tls.secretName.
How This Fits The Rest Of The Lab
If a service should stay private and only cross the internet through one controlled edge, pair the cluster with Cloudflare Tunnel On Proxmox instead of improvising raw port forwards. If Longhorn backups should land on the existing NAS tier, use TrueNAS Shares And Proxmox Integration as the storage-side target layout.
Continue with Kubernetes Verification, Upgrades, And HA for end-to-end tests, upgrade guardrails, troubleshooting, and the HA control-plane path.
Footnotes
-
K3s documents bundled defaults including Traefik and the local-path provisioner. Longhorn documents
open-iscsias a requirement on all nodes and requires an NFS client for backup or RWX-related features: What is K3s?, Longhorn installation requirements. ↩