Après six mois à faire tourner mes services dans des LXC Docker sur Proxmox, je voulais comprendre concrètement comment fonctionne Kubernetes sans passer trois semaines à monter un cluster kubeadm complet. Un cluster full K8s avec etcd en haute disponibilité, c’est facilement 12 GB de RAM consommés avant même de déployer quoi que ce soit. Sur un MS-01 avec 32 GB, ça reste faisable, mais il ne reste plus grand-chose pour les workloads réels. K3s m’a semblé être le bon compromis : c’est du Kubernetes certifié CNCF, juste allégé des composants qui ne me servent pas.
Architecture cible et état avant
Avant K3s, mes services (Vaultwarden, Nextcloud, Gitea, Uptime Kuma) tournaient dans des LXC Docker gérés à la main. Chaque LXC avait ses propres docker-compose.yml et ses propres volumes. La mise à jour d’un service se faisait en SSH sur le bon LXC, docker compose pull && docker compose up -d, et on espérait que rien ne cassait. Pas de rollback automatique. Pas de health check centralisé. Le monitoring Prometheus + Loki sur hubble.arewel.com voyait les métriques, mais pas l’état de santé des déploiements au sens Kubernetes.
Le plan : trois VMs sur heighliner (Proxmox VE 8.3, MS-01 i9-13900H, 32 GB DDR5), toutes en Ubuntu 24.04 LTS.
| VM | ID Proxmox | Rôle | vCPU | RAM | IP |
|---|---|---|---|---|---|
| k3s-master | 204 | Control plane | 4 | 4 GB | 10.10.30.204 |
| k3s-worker-1 | 205 | Worker | 2 | 2 GB | 10.10.30.205 |
| k3s-worker-2 | 206 | Worker | 2 | 2 GB | 10.10.30.206 |
Les trois VMs sont sur le VLAN Serveurs (30), trunked sur le TL-SG108-M2. Le stockage des disques VM est sur le NVMe 1 TB local de heighliner. K3s en configuration SQLite (pas etcd) : c’est le défaut pour un nœud master unique, et c’est suffisant pour un homelab. etcd n’a de sens qu’à partir de 3 masters en HA.
Préparation des VMs
Même opération sur les trois nœuds. Je commence par le master.
Désactiver le swap et le rendre persistant :
sudo swapoff -a
sudo sed -i '/ swap / s/^/#/' /etc/fstab
Activer le forwarding IP et charger les modules kernel requis :
cat <
Fixer les IPs en statique dans Netplan (/etc/netplan/00-installer-config.yaml) et configurer /etc/hosts sur chaque nœud pour résoudre les autres membres du cluster :
10.10.30.204 k3s-master
10.10.30.205 k3s-worker-1
10.10.30.206 k3s-worker-2
Sans entrée /etc/hosts, les nœuds workers ont du mal à s'annoncer au master pendant les phases de join si le DNS homelab n'est pas encore en place.
Installation de K3s sur le master
Avant de lancer le script d'installation, je vérifie le checksum. Le site officiel expose un fichier sha256sum à côté du binaire :
curl -sfLO https://github.com/k3s-io/k3s/releases/download/v1.31.4+k3s1/k3s
curl -sfLO https://github.com/k3s-io/k3s/releases/download/v1.31.4+k3s1/sha256sum-amd64.txt
sha256sum -c sha256sum-amd64.txt --ignore-missing
Si la somme correspond, on lance l'installation via le script officiel. Le script télécharge la bonne version du binaire et configure le service systemd :
curl -sfL https://get.k3s.io | INSTALL_K3S_VERSION="v1.31.4+k3s1" sh -
Le script get.k3s.io télécharge et exécute du code depuis Internet. Vérifie toujours la somme du binaire final indépendamment du script d'installation, ou télécharge le binaire manuellement si ton niveau de risque l'exige.
K3s démarre en tant que service systemd k3s.service. On vérifie :
sudo systemctl status k3s
sudo k3s kubectl get nodes
Le master apparaît en Ready. Traefik (ingress controller) et CoreDNS démarrent automatiquement dans le namespace kube-system - c'est une des valeurs ajoutées de K3s par rapport à kubeadm pur.
Récupérer le token qui servira aux workers :
sudo cat /var/lib/rancher/k3s/server/node-token
Note ce token. Il sera nécessaire pour chaque worker.
Jointure des workers
Sur k3s-worker-1 (même chose sur k3s-worker-2, en changeant uniquement le hostname) :
curl -sfL https://get.k3s.io |
INSTALL_K3S_VERSION="v1.31.4+k3s1"
K3S_URL="https://10.10.30.204:6443"
K3S_TOKEN=""
sh -
Le service s'appelle k3s-agent.service sur les workers. Vérifier depuis le master :
sudo k3s kubectl get nodes -o wide
Les trois nœuds doivent apparaître en Ready. La jointure prend environ 30 secondes par worker.
Kubeconfig local
Pour piloter le cluster depuis ma machine de développement sans avoir à SSH sur le master à chaque fois, je copie le kubeconfig :
scp ubuntu@10.10.30.204:/etc/rancher/k3s/k3s.yaml ~/.kube/k3s-heighliner.yaml
Il faut ensuite modifier l'URL du serveur dans ce fichier, car K3s écrit 127.0.0.1 par défaut :
sed -i 's/127.0.0.1/10.10.30.204/' ~/.kube/k3s-heighliner.yaml
export KUBECONFIG=~/.kube/k3s-heighliner.yaml
kubectl get nodes
Tu peux merger plusieurs kubeconfig avec KUBECONFIG=~/.kube/config:~/.kube/k3s-heighliner.yaml kubectl config view --merge --flatten > ~/.kube/merged.yaml et basculer de contexte avec kubectl config use-context.
Premiers objets : Nginx, Service, Ingress
Un déploiement minimal pour valider que le scheduling fonctionne sur les workers :
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-test
namespace: default
spec:
replicas: 2
selector:
matchLabels:
app: nginx-test
template:
metadata:
labels:
app: nginx-test
spec:
containers:
- name: nginx
image: nginx:1.27-alpine
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: nginx-test
namespace: default
spec:
selector:
app: nginx-test
ports:
- port: 80
targetPort: 80
type: ClusterIP
kubectl apply -f nginx-test.yaml
kubectl get pods -o wide
Les deux réplicas doivent se répartir sur k3s-worker-1 et k3s-worker-2. Pour exposer nginx via l'Ingress Traefik (déjà installé) :
# nginx-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nginx-test
namespace: default
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: web
spec:
rules:
- host: nginx-test.homelab.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: nginx-test
port:
number: 80
kubectl apply -f nginx-ingress.yaml
Traefik écoute sur le port 80 de l'IP du master (10.10.30.204). En ajoutant nginx-test.homelab.local à Pi-hole qui pointe vers 10.10.30.204, la page nginx s'affiche correctement depuis n'importe quel client du réseau LAN.
Stockage persistant avec Longhorn
K3s vient avec un provisionner de stockage local (local-path), mais c'est du stockage node-local : si un pod est replanifié sur un autre worker, il perd l'accès à ses données. Pour du stockage distribué, Longhorn réplique les volumes entre les workers.
Installation de Helm 3
curl -sfL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
helm version
# version.BuildInfo{Version:"v3.16.4", ...}
Ajouter le repo Longhorn :
helm repo add longhorn https://charts.longhorn.io
helm repo update
Prérequis Longhorn
Longhorn a besoin de open-iscsi sur chaque nœud :
# Sur les trois nœuds (master + workers)
sudo apt-get install -y open-iscsi
sudo systemctl enable --now iscsid
Sans ça, Longhorn s'installe mais les volumes restent en Unknown. C'est la première chose à vérifier si un volume refuse de se monter.
Installation Longhorn via Helm
kubectl create namespace longhorn-system
helm install longhorn longhorn/longhorn
--namespace longhorn-system
--version 1.7.2
--set defaultSettings.defaultReplicaCount=2
Avec defaultReplicaCount=2, chaque volume est répliqué sur deux workers. Avec seulement deux workers, ça couvre la perte d'un nœud.
L'installation prend 3 à 5 minutes. On suit la progression :
kubectl -n longhorn-system get pods --watch
Une fois tous les pods en Running, l'interface web Longhorn est accessible via port-forward :
kubectl -n longhorn-system port-forward svc/longhorn-frontend 8080:80
L'UI montre l'espace disponible par nœud, les volumes créés, et l'état de santé des réplicas. Avec les deux workers à 2 GB RAM chacun et les disques VM sur le NVMe heighliner, Longhorn affiche environ 180 GB d'espace utilisable par nœud (les VMs ont des disques de 40 GB chacune, donc le total Longhorn est plafonné par le plus petit disque dans le pool).
Pour définir Longhorn comme StorageClass par défaut :
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"}}}'
Ce qui a coincé : le DNS CoreDNS
Après l'installation du cluster et le déploiement de Longhorn, j'ai voulu déployer Vaultwarden avec une référence à gitea.homelab.local (mon Gitea auto-hébergé sur LXC 104). Les pods démarraient mais les requêtes DNS vers les noms de domaine locaux échouaient avec NXDOMAIN.
CoreDNS, dans K3s, utilise par défaut les serveurs DNS du nœud master (lus dans /etc/resolv.conf au démarrage). Le problème : sur Ubuntu 24.04, systemd-resolved est actif et /etc/resolv.conf pointe vers 127.0.0.53 (le stub resolver local). CoreDNS voit 127.0.0.53 comme upstream, mais ce stub resolver n'est pas accessible depuis les pods (qui tournent dans un réseau overlay différent).
La solution : configurer CoreDNS pour forwarder vers Pi-hole directement. Pi-hole tourne sur LXC 105, IP 10.10.20.5.
kubectl -n kube-system edit configmap coredns
La section forward du Corefile par défaut ressemble à :
forward . /etc/resolv.conf
Je la remplace par :
forward . 10.10.20.5
Le ConfigMap complet après modification :
.:53 {
errors
health
ready
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
}
hosts /etc/coredns/NodeHosts {
ttl 60
reload 15s
fallthrough
}
prometheus :9153
forward . 10.10.20.5
cache 30
loop
reload
loadbalance
}
CoreDNS se recharge automatiquement dans les 30 secondes. Pour forcer :
kubectl -n kube-system rollout restart deployment coredns
Si tu as plusieurs serveurs DNS (Pi-hole primaire + secondaire), tu peux écrire forward . 10.10.20.5 10.10.20.6 - CoreDNS fait du round-robin entre les upstreams listés.
Après le redémarrage, les pods résolvent correctement les noms locaux. kubectl exec dans un pod de test pour vérifier :
kubectl run -it --rm dns-test --image=busybox:1.36 --restart=Never -- nslookup gitea.homelab.local
La réponse revient avec l'IP correcte du LXC Gitea.
Résultats mesurés
Consommation RAM en idle une heure après l'installation complète (Longhorn inclus), mesurée via kubectl top nodes avec metrics-server installé :
helm repo add metrics-server https://kubernetes-sigs.github.io/metrics-server/
helm install metrics-server metrics-server/metrics-server
--namespace kube-system
--set args={--kubelet-insecure-tls}
| Nœud | CPU (millicores) | RAM utilisée |
|---|---|---|
| k3s-master | ~120m | ~1 480 MB |
| k3s-worker-1 | ~45m | ~790 MB |
| k3s-worker-2 | ~42m | ~785 MB |
Le master consomme environ 1.5 GB : K3s server, CoreDNS, Traefik, Longhorn manager, et SQLite. Les workers tournent à ~800 MB en idle. Sur 32 GB dans heighliner, ces trois VMs (4 + 2 + 2 = 8 GB alloués) représentent 25% de la RAM totale. Il reste largement de la marge pour les workloads applicatifs et les autres LXC existants.
Grafana sur hubble.arewel.com collecte les métriques K3s via le job kubernetes-pods dans Prometheus. L'alerte de nœud en NotReady est configurée avec un seuil de 2 minutes.
Ce que je fais maintenant
Le cluster tourne depuis trois semaines sans redémarrage. Prochaine étape : migrer Vaultwarden et Uptime Kuma depuis leurs LXC Docker vers des déploiements K3s gérés par ArgoCD. L'article suivant couvre la mise en place du GitOps avec ArgoCD sur ce même cluster.
Ce que je n'ai pas fait dans cet article : la mise en HA du control plane (3 masters + etcd). Pour un homelab mono-nœud physique, ça n'a pas beaucoup de sens - si heighliner tombe, tout tombe de toute façon. La HA K3s multi-masters ne s'amortit que si on a plusieurs machines physiques.
slug: k3s-proxmox-kubernetes-homelab
meta: Monter un cluster K3s 3 nœuds sur Proxmox VE 8.3 avec Longhorn et la correction du DNS CoreDNS pour le réseau homelab. (~155 chars)
tags: k3s, kubernetes, proxmox, longhorn, helm, traefik, coredns, homelab