guest@6admin.io:~$

Scale-to-zero : Sablier rallume Swarm et Kubernetes à la demande

Un service qu’on consulte une fois par semaine tourne cent pour cent du temps. On l’a installé pour un besoin ponctuel, une QA qu’on ouvre le lundi, un wiki d’équipe, un outil d’admin qu’on ne sort qu’en cas d’incident. Il occupe en permanence sa part de RAM, sa part de CPU, ses ports, sa place dans le monitoring et son lot de mises à jour de sécurité. L’éteindre résout tout d’un coup, sauf qu’il faut alors le rallumer à la main au moment où on en a besoin et personne ne se souvient de la commande. Le scale-to-zero, c’est l’idée de n’allumer un service que quand quelqu’un le demande et de l’éteindre quand plus personne ne s’en sert, sans que l’utilisateur ait à savoir qu’il dormait. Sablier est un petit serveur d’API qu’on pose à côté du reverse proxy: le proxy intercepte chaque requête, appelle l’API pour savoir si la cible dort, la réveille le cas échéant et affiche une page d’attente le temps qu’elle démarre. Une fois la cible prête, la requête passe et une session s’ouvre, refermée après sessionDuration, au terme de quoi Sablier éteint la charge.

Scale-to-zero n’est pas de l’autoscaling

Les deux sont voisins et pourtant différents. L’autoscaling horizontal ajoute ou retire des répliques selon la charge et c’est le travail du HPA de Kubernetes piloté par des métriques ou de KEDA piloté par des événements. Le scale-to-zero va plus loin, il descend jusqu’à zéro réplique et éteint vraiment la charge quand plus personne ne s’en sert. Le HPA ne sait pas descendre à zéro, il lui faut au moins une réplique pour avoir des métriques à lire. KEDA sait le faire sur des événements, une file qui se remplit, une tâche planifiée et il sait même le faire sur du trafic HTTP par un add-on dédié, mais ce chemin-là est réputé capricieux, avec ses faux 502 le temps que la cible démarre et ses réglages qui n’en finissent plus. Sablier prend un chemin différent, il ne regarde pas la charge, il regarde la porte d’entrée: le reverse proxy intercepte la requête, réveille la cible, puis la sert. Cette distinction compte pour choisir l’outil, mais elle compte aussi pour lire la facture, puisqu’un service éteint ne coûte rien alors qu’un service au repos à une réplique coûte déjà sa RAM et son CPU. Et là où l’autoscaling est un sujet de Kubernetes, le scale-to-zero de Sablier marche aussi bien sur Docker seul, sur Swarm, sur Podman ou sur du LXC, ce qui est précisément son intérêt quand on n’a pas de cluster et qu’on n’en veut pas.

Sablier en trois phrases

Sablier est un petit serveur d’API qu’on pose à côté du reverse proxy. Le proxy intercepte chaque requête, appelle l’API pour savoir si la cible dort, la réveille le cas échéant et affiche une page d’attente le temps qu’elle démarre. Une fois la cible prête, la requête passe et une session s’ouvre, renouvelée à chaque coup et refermée après sessionDuration, au terme de quoi Sablier éteint la charge. Un point n’est pas négociable: la cible doit exposer un healthcheck. C’est la seule façon pour Sablier de distinguer un conteneur démarré d’un conteneur prêt à recevoir du trafic et sans lui il réveille dans le vide ou croit prêt ce qui ne l’est pas encore. Le reste est une affaire de stratégie et de provider.

Les deux stratégies

Deux types d’attente se partagent le travail et c’est le consommateur qui tranche, un humain devant une page ou un programme qui attend une réponse.

La stratégie dynamic est l’attente avec page: elle renvoie tout de suite une page d’attente, un thème qui se rafraîchit seul et bascule sur le service dès qu’il répond, c’est le comportement qu’on attend d’un site et c’est le bon choix quand c’est un humain qui navigue, avec des thèmes intégrés qui suffisent largement.

La stratégie blocking est l’attente sans page: elle garde la connexion ouverte et fait patienter le client jusqu’à ce que la cible soit prête, sans rien lui servir entre-temps, ce qui est parfait pour un appel d’API, une sonde ou un client qui ne sait pas afficher de page, à condition de fixer un délai raisonnable.

Un réglage qui sauve la mise, c’est l’ignorance de certains agents: les moniteurs de disponibilité qui tapent toutes les minutes ne doivent pas réveiller la pile pour rien. On liste les motifs à ignorer et leurs requêtes reçoivent un 200 immédiat sans rien démarrer, ce qui évite qu’un UptimeRobot garde le service chaud vingt-quatre heures sur vingt-quatre. Et pour les cas où Sablier lui-même tombe, le drapeau failOpen décide si on laisse passer la requête vers le backend ou si on renvoie une erreur, un arbitrage entre disponibilité et cohérence qu’il vaut mieux choisir consciemment.

Quel provider

Sablier parle à six moteurs différents et le choix se fait en une ligne. La colonne qui compte pour cette maison est la dernière, ce que chaque provider doit avoir le droit de faire sur la machine hôte.

ProviderCe qu’il piloteAccès nécessaire
dockerconteneurs Docker locauxsocket Docker en écriture, compatible compose
swarmservices Swarm depuis un managersocket du manager, répliques à zéro
podmanconteneurs Podmansocket utilisateur, fonctionne en rootless
kubernetesDeployments et StatefulSetsAPI K8s scopée par RBAC, in-cluster ou non
proxmox_lxcconteneurs LXC ProxmoxAPI Proxmox par jeton
systemdunités systemdD-Bus, unités système ou utilisateur

[edit: 2026] Un chart Helm officiel est venu depuis pour installer le serveur dans un cluster, avec le ServiceAccount et le ClusterRole qui vont bien.

Traefik, la vraie config

Côté Traefik, il y a deux morceaux: le module à charger dans la config statique et le middleware à définir dans la config dynamique. À l’époque le module vit encore sous le compte de l’auteur et se charge en trois lignes.

experimental:
  plugins:
    sablier:
      moduleName: "github.com/acouvreur/sablier"
      version: "v1.2.0"

Le middleware, lui, dit à Traefik où joindre Sablier, à quel groupe appartient la cible, combien de temps garder la session ouverte et quels agents ignorer.

http:
  middlewares:
    my-sablier:
      plugin:
        sablier:
          sablierUrl: http://sablier:10000
          group: my-app-group
          sessionDuration: 1m
          ignoreUserAgent:
            - curl
            - "(?i)uptimerobot"
          dynamic:
            displayName: My Services
            theme: hacker-terminal
            refreshFrequency: 5s

Le piège qui donne à l’article ses dents est là: quand une cible tombe à zéro, Traefik ne voit plus son conteneur, donc plus ses labels, donc plus ses règles de routage et la requête n’atteint jamais le middleware censé la réveiller. À l’époque la seule issue est de sortir le routage des labels et de le poser dans un provider fichier, un fichier chargé à part que Traefik garde toujours sous la main. Le problème reste entier du côté de Swarm et de Kubernetes, où il faut respectivement centraliser le middleware sur un service qui ne s’éteint pas et activer le support des services vides.

[edit: 2026] Depuis Traefik v3.6, le provider Docker sait enregistrer les conteneurs arrêtés avec l’option d’annonce adaptée, ce qui rend les labels utilisables même à zéro réplique, mais le problème reste entier côté Swarm et Kubernetes.

Swarm

Sur Swarm le piège est plus vicieux: dès qu’un service tombe à zéro réplique, Traefik le sort de son pool et ses labels de routage disparaissent avec lui. La parade consiste à définir le middleware sur un service qui ne s’éteint jamais, typiquement Sablier lui-même et à ne garder sur la cible que de quoi s’annoncer et se faire rejoindre.

whoami:
  image: acouvreur/whoami:v1.10.2
  deploy:
    replicas: 0
    labels:
      - sablier.enable=true
      - sablier.group=my-group
      - traefik.enable=true
      - traefik.docker.lbswarm=true
      - traefik.http.routers.whoami.rule=Host(`whoami.example.org`)
      - traefik.http.routers.whoami.middlewares=whoami-sablier@swarm

La stratégie bloqueuse y est d’ailleurs déconseillée à cause de la façon dont Traefik évacue les services sans réplique, donc sur Swarm on reste en dynamique. C’est un peu l’histoire de ce durcissement de Swarm, la plateforme fait bien son travail tant qu’on ne lui demande pas ce qu’elle ne sait pas faire.

Kubernetes

Sur Kubernetes le contrat est plus direct: Sablier parle à l’API, monte les Deployments et les StatefulSets de zéro à un et les redescend, pour peu qu’on lui donne un rôle scopé et qu’on opte en marquant la charge.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: whoami
  labels:
    sablier.enable: "true"
    sablier.group: mygroup
spec:
  replicas: 0

[edit: 2026] Les clés sont passées depuis au domaine sablierapp.dev/enable et sablierapp.dev/group, suivant le passage du projet sous l’organisation sablierapp.

Un détail compte: la clé qui active la découverte doit être un label, parce que Sablier filtre les charges côté serveur par sélecteur, alors que les autres clés peuvent être des annotations, ce qui est même nécessaire quand la valeur contient une virgule ou un deux-points, comme une plage horaire. Le readiness ne se paramètre pas, Kubernetes a déjà les sondes des Pods et Sablier considère la charge prête dès que les répliques courantes rejoignent les répliques voulues, donc la seule chose à faire côté Traefik est d’autoriser les services sans endpoints, sinon la route disparaît avant même que le réveil commence. La stratégie bloqueuse n’est pas encore supportée ici, on reste en dynamique.

Le prix du socket

Il y a une facture que la démo ne montre pas et que ce blog a l’habitude de présenter: pour éteindre et rallumer des conteneurs, Sablier doit parler au socket Docker et ce dialogue n’est pas en lecture seule. Démarrer un conteneur est une écriture, arrêter un service Swarm est une écriture, donc l’accès qu’il faut lui donner est un accès capable d’agir sur le moteur, ce qui, sur une machine donnée, équivaut à un accès de fait au niveau de l’hôte. Ce n’est pas une fatalité pour autant. On peut lui présenter un proxy de socket qui n’expose que le strict nécessaire, la lecture des conteneurs et les appels de démarrage et d’arrêt. Le socket Docker en lecture seule explique la mécanique et sa limite. On peut aussi choisir un provider qui n’a pas ce pouvoir: Kubernetes lui donne une API scopée par un ServiceAccount et un RBAC, Podman se contente d’un socket utilisateur parce qu’il tourne sans privilèges, comme le détaille Docker rootless . Swarm réduit la surface à un seul manager. Le sujet revient chaque fois qu’on branche un outil sur le moteur et la bonne question n’est pas comment l’éviter mais quel est le plus petit privilège qui laisse l’outil faire son travail.

Arrêter ou étrangler

[edit: 2026] Le mode échelle décrit dans cette section, qui étrangle la charge au lieu de l’arrêter, est arrivé bien plus tard (après la v1.10.0, mi-2025); à l’époque, seule l’option arrêter/redémarrer existait.

Le comportement par défaut est d’arrêter la charge et de la redémarrer au prochain appel, ce qui veut dire un cold start égal au temps de démarrage de la cible. Pour un service léger c’est imperceptible, pour une grosse base de données c’est rédhibitoire et Sablier propose une alternative: le mode échelle garde la charge vivante mais l’étrangle. On déclare des ressources au repos et des ressources en session et entre les deux rien ne redémarre.

labels:
  - "sablier.enable=true"
  - "sablier.group=myapp"
  - "sablier.idle.replicas=1"
  - "sablier.idle.cpu=0.1"
  - "sablier.idle.memory=64m"
  - "sablier.active.replicas=1"
  - "sablier.active.cpu=2.0"
  - "sablier.active.memory=512m"

Il y a aussi de quoi tenir une machine à bout de bras. L’anti-affinité fait reculer une charge quand un autre groupe est actif, ce qui évite de faire tourner deux gros consommateurs de GPU ou de RAM en même temps. Une plage horaire borne les moments où une cible peut être réveillée. Un drapeau met tout à zéro au démarrage de Sablier, pratique après un reboot. Des webhooks préviennent un moniteur, un pipeline ou un bus d’événements à chaque démarrage et arrêt.

[edit: 2026] Le serveur expose depuis un point de métriques pour Prometheus et des traces OpenTelemetry, ce qui évite de deviner ce qui se passe la nuit.

Ce que ça coûte et où ça mord

Le prix à l’usage est minuscule: le projet annonce de l’ordre de 1,5 à 2 ms de latence ajoutée en régime établi, session chaude, pour un débit de plusieurs milliers de requêtes par seconde sur un seul cœur. En pratique, la latence qui compte n’est pas celle de Sablier mais celle de votre conteneur, puisque le cold start, c’est le temps de démarrage de la cible et Sablier n’y peut rien. Ce qui mord, c’est la cible elle-même. Sans healthcheck, Sablier ne sait pas quand la charge est prête et sert soit trop tôt soit trop tard, donc un conteneur sans sonde de disponibilité est un mauvais candidat. La page d’attente se rafraîchit toute seule et fonctionne partout, mais les navigateurs mobiles en font parfois des leurs avec les rafraîchissements d’onglet en arrière-plan, rien de bloquant mais bon à savoir. Enfin, les sessions et leur expiration ont leur propre horloge, avec une durée par défaut et un intervalle de vérification qu’on règle selon le CPU qu’on accepte de brûler, plus l’intervalle est court, plus on paie pour rien.

D’où ça vient

Le projet n’est pas né hier. Le dépôt Sablier a été créé le 20 octobre 2020, les premiers commits datent de la fin octobre de la même année et la première version publiée, la v1.2.0, arrive le 18 novembre 2022. C’est cette base que j’utilise pour ce billet.

[edit: 2026] Le projet a bien grandi depuis. La v1.3.0 a suivi le 10 mars 2023 et le billet de référence de l’époque, Docker scale-to-zero with Traefik and Sablier , écrit le 20 août 2023 avec l’image acouvreur/sablier:1.3.0 et Traefik v2.10, sur une démo publique encore lisible aujourd’hui. Le dépôt est passé sous l’organisation sablierapp, le plugin Traefik a été sorti dans son propre dépôt le 2 octobre 2024 et la version courante est la v1.18.0. La configuration a suivi le mouvement et l’ancien billet ne s’applique plus tel quel. Là où la démo de 2023 listait les cibles avec un champ names, on parle maintenant de group et là où elle chargeait un module github.com/acouvreur/sablier, on charge github.com/sablierapp/sablier-traefik-plugin. Deux chaînes à changer, sinon rien ne démarre.

Le verdict

Sablier est le bon outil quand la charge est faible et intermittente: une QA qu’on ouvre quelques heures par semaine, un outil d’admin, un homelab contraint en RAM où chaque service qui dort est un service rendu, une démonstration qu’on ne veut pas payer à l’heure. C’est là qu’il rend son plein service, parce qu’il transforme un parc de services qui tournent tout le temps en un parc qui ne consomme que quand on le sollicite. Il n’est pas le bon outil pour une charge réelle et continue, pour une cible qui met des minutes à démarrer sans healthcheck fiable ou pour un moniteur qui doit voir le service toujours debout, dans ces cas le cold start ou l’arrêt se paient plus cher que la ressource économisée. Il n’est pas non plus un prétexte pour donner à un conteneur un accès illimité à son moteur et c’est peut-être le vrai enseignement de l’histoire: l’élégance du scale-to-zero ne dispense pas de décider à quoi un outil a le droit de toucher.