Le kubelet ne supprime les images inutilisées qu'à 85 % d'occupation du disque des images, et il s'arrête à 80 %. Ce disque est le disque externe (/mnt/arcodange), que Longhorn partage et qu'il retire de l'ordonnancement dès 75 % (storage-minimal-available-percentage = 25, relevé). Le nettoyage laissait donc le disque en permanence au-dessus de la ligne de Longhorn.
Relevé le 2026-09-25 vers 18 h 30 UTC (/stats/summary et /configz de chaque kubelet, nodes.longhorn.io) :
Nœud
Rôle k3s
Disque des images (491 Go)
Disque Longhorn
Seuils du kubelet
pi1
server
75 %
Schedulable
85 / 80
pi2
agent
53 %
Schedulable
85 / 80
pi3
agent
82 %
Schedulable=False
85 / 80
Signalé sur pi3 : environ 270 Go d'images Docker, dont 272 anciennes versions du front Kadans (~450 Mo chacune).
Le réglage
--kubelet-arg=image-gc-high-threshold=65 et --kubelet-arg=image-gc-low-threshold=55, ajoutés à extra_server_args (pi1) et à extra_agent_args (pi2, pi3) dans playbooks/system/system_k3s.yml. Le rôle serveur/agent se décide à l'exécution : le premier hôte trié devient le serveur. kubectl get nodes le confirme : pi1 est control-plane.
La forme reste sans guillemets, comme les deux --kubelet-arg déjà en place (le piège du \= du 11/08 est décrit juste au-dessus dans le fichier). 65 % laisse 10 points de marge sous la ligne de Longhorn.
Le ramasse-miettes du kubelet agit-il avec Docker ? Oui, et il agit déjà
--docker fait passer le kubelet par le cri-dockerd embarqué dans k3s : containerRuntimeEndpoint: unix:///run/k3s/cri-dockerd/cri-dockerd.sock (dans /configz, sur les 3 nœuds).
cri-dockerd rend au kubelet le système de fichiers des images : imageFs.capacityBytes = 491,1 Go, soit le disque externe, sur les 3 nœuds. C'est sur ce pourcentage que le ramasse-miettes décide.
Il exécute aussi les suppressions : kubelet_image_garbage_collected_total{reason="space"} 371 sur pi3, depuis le démarrage de son kubelet (2026-04-13). Il a donc déjà franchi 85 %, nettoyé jusqu'à 80 %, et le disque est remonté depuis. Le seuil était le problème, pas le mécanisme.
Aucun minuteur docker image prune n'est donc nécessaire. On garderait cette solution de repli si le mécanisme cessait d'agir. ⚠ Un until= y porte sur la date de création de l'image, pas sur celle de son téléchargement.
L'ADR docs/adr/20260407-docker-storage-gitea-runner.md attribuait la disparition des images du runner au « garbage collection automatique de Docker ». Docker n'en a pas pour les images : c'était ce ramasse-miettes du kubelet, quand le data-root était encore sur la carte SD (à 89 %).
Ce qu'il ne supprime pas :
une image référencée par un conteneur, même arrêté. cri-dockerd appelle RemoveImage sans force et Docker refuse. L'épingle pin-ci-node-playwright (rôle ci_base_image), le runner et les conteneurs compose de Gitea et Postgres sont donc protégés ;
le cache de build, les couches des conteneurs, les volumes, les réplicas Longhorn.
Sans le disque externe
Le réglage ne nomme aucun chemin. Si un Pi démarre sans disque, le data-root retombe sur la carte SD (voir #63), le kubelet y lit alors le disque des images, et le même seuil protège la SD. La contrainte du 2026-09-25 est respectée : ce changement n'ajoute aucune dépendance au disque.
agent : même suite sans --disable traefik, suivie de {{ kubelet_reserved_args | default('') }}
yamllint -d relaxed et ansible-lint --offline : mêmes constats que sur main, aucun nouveau (1 erreur trailing-spaces et 3 avertissements côté yamllint, 15 constats côté ansible-lint, tous hors des lignes modifiées). Le dépôt n'a de configuration ni pour l'un ni pour l'autre, et aucune CI ne couvre ansible/.
k3s redémarre sur chaque nœud : l'unité est régénérée. Sur pi1, l'API k3s est donc brièvement indisponible.
Premier passage (toutes les 5 min après le redémarrage) : pi3 et pi1 sont au-dessus de 65 %, et le kubelet y supprime des images jusqu'à 55 %. Sur pi3, ça fait jusqu'à ~130 Go d'images supprimées d'un coup sur un disque USB. Mieux vaut l'appliquer dans un moment calme. Aujourd'hui à 18 h 20 UTC, le Docker de pi3 expirait déjà ses créations de conteneurs (operation timeout: context deadline exceeded), cause non établie.
⚠ Si ce qui n'est pas une image inutilisée dépasse déjà 55 % du disque, le kubelet supprime toutes les images inutilisées à chaque passage, puis journalise FreeDiskSpaceFailed. Les images sont retéléchargées au besoin, sans perte. Sur pi3, cri-dockerd annonce 120 Go d'images pour 400 Go occupés, dont ~83 Go de réplicas Longhorn. Le reste peut être du cache de build (les builds CI tournent sur pi3), que le kubelet ne touche pas. docker system df sur pi3 avant d'appliquer le dira.
Vérifier après application
# les seuils sont pris (attendu : 65 et 55 sur les 3 nœuds)
for n in pi1 pi2 pi3; do kubectl --context default get --raw /api/v1/nodes/$n/proxy/configz \
| jq -c '.kubeletconfig | {n: "'$n'", hi: .imageGCHighThresholdPercent, lo: .imageGCLowThresholdPercent}'; done
# occupation du disque des images (attendu : sous 65 % après un ou deux passages)
kubectl --context default get --raw /api/v1/nodes/pi3/proxy/stats/summary \
| jq '.node.runtime.imageFs | 100 - (.availableBytes * 100 / .capacityBytes | floor)'
# le compteur de suppressions monte
kubectl --context default get --raw /api/v1/nodes/pi3/proxy/metrics | grep kubelet_image_garbage_collected_total
# Longhorn reprend pi3 (attendu : Schedulable=True)
kubectl --context default -n longhorn-system get nodes.longhorn.io pi3 \
-o jsonpath='{range .status.diskStatus.*.conditions[*]}{.type}={.status} {end}{"\n"}'
# le ramasse-miettes ne tourne pas à vide (attendu : aucun résultat)
kubectl --context default get events -A --field-selector reason=FreeDiskSpaceFailed
Documentation
vibe/guidebooks/factory-provisioning/ansible/01-system.md : la ligne 7 et son récit mentionnent désormais les arguments du kubelet et le seuil. Last Updated passe au 2026-09-25.
Liens
Audit « démarrer sans disque » demandé avec ce réglage : #63 (issue séparée, trop long pour cette description)
Même famille : #60 (rétention du registre de paquets, la SD de pi2 pleine d'images kadans)
## Pourquoi
Le kubelet ne supprime les images inutilisées qu'à **85 %** d'occupation du disque des images, et il s'arrête à **80 %**. Ce disque est le disque externe (`/mnt/arcodange`), que Longhorn partage et qu'il retire de l'ordonnancement dès **75 %** (`storage-minimal-available-percentage` = 25, relevé). Le nettoyage laissait donc le disque **en permanence au-dessus** de la ligne de Longhorn.
Relevé le 2026-09-25 vers 18 h 30 UTC (`/stats/summary` et `/configz` de chaque kubelet, `nodes.longhorn.io`) :
| Nœud | Rôle k3s | Disque des images (491 Go) | Disque Longhorn | Seuils du kubelet |
|---|---|---|---|---|
| pi1 | server | **75 %** | Schedulable | 85 / 80 |
| pi2 | agent | 53 % | Schedulable | 85 / 80 |
| pi3 | agent | **82 %** | **Schedulable=False** | 85 / 80 |
Signalé sur pi3 : environ 270 Go d'images Docker, dont 272 anciennes versions du front Kadans (~450 Mo chacune).
## Le réglage
`--kubelet-arg=image-gc-high-threshold=65` et `--kubelet-arg=image-gc-low-threshold=55`, ajoutés à `extra_server_args` (pi1) **et** à `extra_agent_args` (pi2, pi3) dans `playbooks/system/system_k3s.yml`. Le rôle serveur/agent se décide à l'exécution : le premier hôte trié devient le serveur. `kubectl get nodes` le confirme : pi1 est `control-plane`.
La forme reste **sans guillemets**, comme les deux `--kubelet-arg` déjà en place (le piège du `\=` du 11/08 est décrit juste au-dessus dans le fichier). 65 % laisse 10 points de marge sous la ligne de Longhorn.
## Le ramasse-miettes du kubelet agit-il avec Docker ? Oui, et il agit déjà
- `--docker` fait passer le kubelet par le cri-dockerd embarqué dans k3s : `containerRuntimeEndpoint: unix:///run/k3s/cri-dockerd/cri-dockerd.sock` (dans `/configz`, sur les 3 nœuds).
- cri-dockerd rend au kubelet le système de fichiers des images : `imageFs.capacityBytes` = 491,1 Go, soit le disque externe, sur les 3 nœuds. C'est sur ce pourcentage que le ramasse-miettes décide.
- Il exécute aussi les suppressions : **`kubelet_image_garbage_collected_total{reason="space"} 371` sur pi3**, depuis le démarrage de son kubelet (2026-04-13). Il a donc déjà franchi 85 %, nettoyé jusqu'à 80 %, et le disque est remonté depuis. Le seuil était le problème, pas le mécanisme.
- Aucun minuteur `docker image prune` n'est donc nécessaire. On garderait cette solution de repli si le mécanisme cessait d'agir. ⚠ Un `until=` y porte sur la date de **création** de l'image, pas sur celle de son téléchargement.
- L'ADR `docs/adr/20260407-docker-storage-gitea-runner.md` attribuait la disparition des images du runner au « garbage collection automatique de Docker ». Docker n'en a pas pour les images : c'était ce ramasse-miettes du kubelet, quand le `data-root` était encore sur la carte SD (à 89 %).
Ce qu'il **ne** supprime **pas** :
- une image référencée par un conteneur, même arrêté. cri-dockerd appelle `RemoveImage` sans `force` et Docker refuse. L'épingle `pin-ci-node-playwright` (rôle `ci_base_image`), le runner et les conteneurs compose de Gitea et Postgres sont donc protégés ;
- le cache de build, les couches des conteneurs, les volumes, les réplicas Longhorn.
## Sans le disque externe
Le réglage ne nomme aucun chemin. Si un Pi démarre sans disque, le `data-root` retombe sur la carte SD (voir #63), le kubelet y lit alors le disque des images, et le même seuil protège la SD. La contrainte du 2026-09-25 est respectée : ce changement n'ajoute aucune dépendance au disque.
## Vérifications locales (aucun hôte touché)
- `ansible-playbook --syntax-check` : `playbooks/system/system_k3s.yml` → code 0 ; `playbooks/01_system.yml` → code 0.
- Rendu des variables (YAML chargé) :
- serveur : `--docker --disable traefik --kubelet-arg=container-log-max-files=5 --kubelet-arg=container-log-max-size=10Mi --kubelet-arg=image-gc-high-threshold=65 --kubelet-arg=image-gc-low-threshold=55`
- agent : même suite sans `--disable traefik`, suivie de `{{ kubelet_reserved_args | default('') }}`
- `yamllint -d relaxed` et `ansible-lint --offline` : **mêmes constats que sur `main`**, aucun nouveau (1 erreur `trailing-spaces` et 3 avertissements côté yamllint, 15 constats côté ansible-lint, tous hors des lignes modifiées). Le dépôt n'a de configuration ni pour l'un ni pour l'autre, et aucune CI ne couvre `ansible/`.
## À l'application : ce qui va se passer
```
ansible-playbook -i ansible/arcodange/factory/inventory ansible/arcodange/factory/playbooks/system/system_k3s.yml
```
- **k3s redémarre sur chaque nœud** : l'unité est régénérée. Sur pi1, l'API k3s est donc brièvement indisponible.
- **Premier passage** (toutes les 5 min après le redémarrage) : pi3 et pi1 sont au-dessus de 65 %, et le kubelet y supprime des images jusqu'à 55 %. Sur pi3, ça fait jusqu'à ~130 Go d'images supprimées d'un coup sur un disque USB. Mieux vaut l'appliquer dans un moment calme. Aujourd'hui à 18 h 20 UTC, le Docker de pi3 expirait déjà ses créations de conteneurs (`operation timeout: context deadline exceeded`), cause non établie.
- ⚠ **Si ce qui n'est pas une image inutilisée dépasse déjà 55 % du disque**, le kubelet supprime **toutes** les images inutilisées à chaque passage, puis journalise `FreeDiskSpaceFailed`. Les images sont retéléchargées au besoin, sans perte. Sur pi3, cri-dockerd annonce 120 Go d'images pour 400 Go occupés, dont ~83 Go de réplicas Longhorn. Le reste peut être du cache de build (les builds CI tournent sur pi3), que le kubelet ne touche pas. **`docker system df` sur pi3 avant d'appliquer** le dira.
## Vérifier après application
```
# les seuils sont pris (attendu : 65 et 55 sur les 3 nœuds)
for n in pi1 pi2 pi3; do kubectl --context default get --raw /api/v1/nodes/$n/proxy/configz \
| jq -c '.kubeletconfig | {n: "'$n'", hi: .imageGCHighThresholdPercent, lo: .imageGCLowThresholdPercent}'; done
# occupation du disque des images (attendu : sous 65 % après un ou deux passages)
kubectl --context default get --raw /api/v1/nodes/pi3/proxy/stats/summary \
| jq '.node.runtime.imageFs | 100 - (.availableBytes * 100 / .capacityBytes | floor)'
# le compteur de suppressions monte
kubectl --context default get --raw /api/v1/nodes/pi3/proxy/metrics | grep kubelet_image_garbage_collected_total
# Longhorn reprend pi3 (attendu : Schedulable=True)
kubectl --context default -n longhorn-system get nodes.longhorn.io pi3 \
-o jsonpath='{range .status.diskStatus.*.conditions[*]}{.type}={.status} {end}{"\n"}'
# le ramasse-miettes ne tourne pas à vide (attendu : aucun résultat)
kubectl --context default get events -A --field-selector reason=FreeDiskSpaceFailed
```
## Documentation
`vibe/guidebooks/factory-provisioning/ansible/01-system.md` : la ligne 7 et son récit mentionnent désormais les arguments du kubelet et le seuil. Last Updated passe au 2026-09-25.
## Liens
- Audit « démarrer sans disque » demandé avec ce réglage : #63 (issue séparée, trop long pour cette description)
- Même famille : #60 (rétention du registre de paquets, la SD de pi2 pleine d'images kadans)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Le kubelet ne supprimait les images inutilisées qu'à 85 % d'occupation du
disque des images, et s'arrêtait à 80 %. Ce disque est le disque externe
(/mnt/arcodange), que Longhorn retire de l'ordonnancement dès 75 %
occupés : le nettoyage laissait donc le disque en permanence au-dessus de
la ligne de Longhorn. Relevé le 2026-09-25 sur pi3 : 82 %, disque
Longhorn Schedulable=False, 272 anciennes images du front Kadans.
Ajout de --kubelet-arg=image-gc-high-threshold=65 et
--kubelet-arg=image-gc-low-threshold=55 au serveur (pi1) ET aux agents
(pi2, pi3). Le mécanisme s'applique bien au runtime Docker : k3s --docker
passe par cri-dockerd, qui rend au kubelet le système de fichiers des
images et exécute ses suppressions (371 images déjà supprimées sur pi3,
kubelet_image_garbage_collected_total{reason="space"}).
Indépendant du disque externe : sans lui, les images tombent sur la carte
SD, et le même seuil la protège.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Lien croisé : le même jour, Prometheus est muet à cause du stockage de pi3 (journaux ext4 abandonnés sur 7 volumes Longhorn entre 18 h 18 et 18 h 23 UTC, charge 58 à 71), pas à cause de sa configuration de scrutation. Voir arcodange-org/tools#59. Ce réglage soulage le disque de pi3, mais ne règle pas à lui seul ce qui arrive au TSDB.
Lien croisé : le même jour, Prometheus est muet à cause du stockage de pi3 (journaux ext4 abandonnés sur 7 volumes Longhorn entre 18 h 18 et 18 h 23 UTC, charge 58 à 71), pas à cause de sa configuration de scrutation. Voir arcodange-org/tools#59. Ce réglage soulage le disque de pi3, mais ne règle pas à lui seul ce qui arrive au TSDB.
You are not authorized to merge this pull request.
This pull request can be merged automatically.
This branch is out-of-date with the base branch
View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Pourquoi
Le kubelet ne supprime les images inutilisées qu'à 85 % d'occupation du disque des images, et il s'arrête à 80 %. Ce disque est le disque externe (
/mnt/arcodange), que Longhorn partage et qu'il retire de l'ordonnancement dès 75 % (storage-minimal-available-percentage= 25, relevé). Le nettoyage laissait donc le disque en permanence au-dessus de la ligne de Longhorn.Relevé le 2026-09-25 vers 18 h 30 UTC (
/stats/summaryet/configzde chaque kubelet,nodes.longhorn.io) :Signalé sur pi3 : environ 270 Go d'images Docker, dont 272 anciennes versions du front Kadans (~450 Mo chacune).
Le réglage
--kubelet-arg=image-gc-high-threshold=65et--kubelet-arg=image-gc-low-threshold=55, ajoutés àextra_server_args(pi1) et àextra_agent_args(pi2, pi3) dansplaybooks/system/system_k3s.yml. Le rôle serveur/agent se décide à l'exécution : le premier hôte trié devient le serveur.kubectl get nodesle confirme : pi1 estcontrol-plane.La forme reste sans guillemets, comme les deux
--kubelet-argdéjà en place (le piège du\=du 11/08 est décrit juste au-dessus dans le fichier). 65 % laisse 10 points de marge sous la ligne de Longhorn.Le ramasse-miettes du kubelet agit-il avec Docker ? Oui, et il agit déjà
--dockerfait passer le kubelet par le cri-dockerd embarqué dans k3s :containerRuntimeEndpoint: unix:///run/k3s/cri-dockerd/cri-dockerd.sock(dans/configz, sur les 3 nœuds).imageFs.capacityBytes= 491,1 Go, soit le disque externe, sur les 3 nœuds. C'est sur ce pourcentage que le ramasse-miettes décide.kubelet_image_garbage_collected_total{reason="space"} 371sur pi3, depuis le démarrage de son kubelet (2026-04-13). Il a donc déjà franchi 85 %, nettoyé jusqu'à 80 %, et le disque est remonté depuis. Le seuil était le problème, pas le mécanisme.docker image prunen'est donc nécessaire. On garderait cette solution de repli si le mécanisme cessait d'agir. ⚠ Ununtil=y porte sur la date de création de l'image, pas sur celle de son téléchargement.docs/adr/20260407-docker-storage-gitea-runner.mdattribuait la disparition des images du runner au « garbage collection automatique de Docker ». Docker n'en a pas pour les images : c'était ce ramasse-miettes du kubelet, quand ledata-rootétait encore sur la carte SD (à 89 %).Ce qu'il ne supprime pas :
RemoveImagesansforceet Docker refuse. L'épinglepin-ci-node-playwright(rôleci_base_image), le runner et les conteneurs compose de Gitea et Postgres sont donc protégés ;Sans le disque externe
Le réglage ne nomme aucun chemin. Si un Pi démarre sans disque, le
data-rootretombe sur la carte SD (voir #63), le kubelet y lit alors le disque des images, et le même seuil protège la SD. La contrainte du 2026-09-25 est respectée : ce changement n'ajoute aucune dépendance au disque.Vérifications locales (aucun hôte touché)
ansible-playbook --syntax-check:playbooks/system/system_k3s.yml→ code 0 ;playbooks/01_system.yml→ code 0.--docker --disable traefik --kubelet-arg=container-log-max-files=5 --kubelet-arg=container-log-max-size=10Mi --kubelet-arg=image-gc-high-threshold=65 --kubelet-arg=image-gc-low-threshold=55--disable traefik, suivie de{{ kubelet_reserved_args | default('') }}yamllint -d relaxedetansible-lint --offline: mêmes constats que surmain, aucun nouveau (1 erreurtrailing-spaceset 3 avertissements côté yamllint, 15 constats côté ansible-lint, tous hors des lignes modifiées). Le dépôt n'a de configuration ni pour l'un ni pour l'autre, et aucune CI ne couvreansible/.À l'application : ce qui va se passer
operation timeout: context deadline exceeded), cause non établie.FreeDiskSpaceFailed. Les images sont retéléchargées au besoin, sans perte. Sur pi3, cri-dockerd annonce 120 Go d'images pour 400 Go occupés, dont ~83 Go de réplicas Longhorn. Le reste peut être du cache de build (les builds CI tournent sur pi3), que le kubelet ne touche pas.docker system dfsur pi3 avant d'appliquer le dira.Vérifier après application
Documentation
vibe/guidebooks/factory-provisioning/ansible/01-system.md: la ligne 7 et son récit mentionnent désormais les arguments du kubelet et le seuil. Last Updated passe au 2026-09-25.Liens
🤖 Generated with Claude Code
Le kubelet ne supprimait les images inutilisées qu'à 85 % d'occupation du disque des images, et s'arrêtait à 80 %. Ce disque est le disque externe (/mnt/arcodange), que Longhorn retire de l'ordonnancement dès 75 % occupés : le nettoyage laissait donc le disque en permanence au-dessus de la ligne de Longhorn. Relevé le 2026-09-25 sur pi3 : 82 %, disque Longhorn Schedulable=False, 272 anciennes images du front Kadans. Ajout de --kubelet-arg=image-gc-high-threshold=65 et --kubelet-arg=image-gc-low-threshold=55 au serveur (pi1) ET aux agents (pi2, pi3). Le mécanisme s'applique bien au runtime Docker : k3s --docker passe par cri-dockerd, qui rend au kubelet le système de fichiers des images et exécute ses suppressions (371 images déjà supprimées sur pi3, kubelet_image_garbage_collected_total{reason="space"}). Indépendant du disque externe : sans lui, les images tombent sur la carte SD, et le même seuil la protège. Co-Authored-By: Claude Opus 5.5 <[email protected]>Lien croisé : le même jour, Prometheus est muet à cause du stockage de pi3 (journaux ext4 abandonnés sur 7 volumes Longhorn entre 18 h 18 et 18 h 23 UTC, charge 58 à 71), pas à cause de sa configuration de scrutation. Voir arcodange-org/tools#59. Ce réglage soulage le disque de pi3, mais ne règle pas à lui seul ce qui arrive au TSDB.
View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.