Spunto Lite : le pilier Build, ramassé dans un seul conteneur que vous faites tourner vous-même
J’ai pris la moitié « environnements de dev » de Spunto — projets, workers jetables, un IDE dans le navigateur, un terminal persistant — et je l’ai mise dans un seul conteneur auto-hébergeable qui parle directement à votre socket Docker. Ce qu’il contient, comment il est branché, et les trois choses plus délicates que prévu.
Mathieu
Fondateur, Spunto
Spunto, c’est deux piliers : Build (des environnements de dev isolés) et Run (les observer). C’est une plateforme — des organisations, des rôles, une flotte de machines que vous connectez, une authentification devant le tout. C’est la bonne forme quand plusieurs personnes et plusieurs machines sont en jeu, et exactement la mauvaise quand ce que vous voulez vraiment, c’est « un conteneur propre avec un éditeur dedans, sur ce portable, maintenant ».
C’est la seconde chose que je voulais sans arrêt. Pas un compte cloud, pas une machine à enregistrer — juste le pilier Build, sur la machine déjà posée devant moi. Je l’ai donc sorti dans un projet à part : Spunto Lite, un plan de contrôle d’environnements de dev local, dans un seul conteneur, qui parle directement à votre propre socket Docker. Pas de multi-tenant, pas d’agents distants, pas de cloud dans la boucle.
docker run -d --name spunto-lite -p 80:80 \
-v /var/run/docker.sock:/var/run/docker.sock \
-v "$HOME/.ssh:/host-ssh:ro" \
-v spunto-lite-data:/app/data \
ghcr.io/spuntodotnet/spunto-lite:latestOuvrez http://localhost, il est là. Ou clonez le dépôt et lancez docker compose up -d --build si vous préférez le voir se construire. Trois montages, et chacun raconte toute l’histoire : le socket Docker est ce qui lui permet de faire quoi que ce soit, votre ~/.ssh est ce qui fait que git push fonctionne toujours dans les workers, et le volume nommé est l’endroit où vit son fichier SQLite.
Deux objets, comme sur la plateforme hébergée. Un projet décrit un environnement : image de base, features devcontainer, extensions d’IDE, dépôts à cloner, ports à transférer, hooks postCreate / postStart. Un worker est un conteneur jetable construit à partir de cette description, avec code-server dedans pour avoir un vrai éditeur dans un onglet du navigateur, plus un terminal persistant.
Ce qui en fait autre chose qu’un jouet, et un outil dont je me sers tous les jours :
- Des modèles de départ — Next.js, Vite + React, Astro, FastAPI, Express, ou un projet docker-compose (ce dernier ajoute la feature
docker-in-docker, pour avoir un daemon imbriqué). Chacun génère une application qui fonctionne dans/workspacepuis la lance : une installation neuve passe de rien à un serveur de dev en marche sans que vous ayez écrit de spec. - Des secrets, par projet ou par utilisateur, chiffrés au repos et injectés dans l’environnement du worker.
- Votre identité SSH, pour que les commits et les push faits depuis un worker soient les vôtres — et une deploy key par projet, qu’il peut générer pour vous quand un remote git générique en demande une.
- Des specs de projet versionnées : modifier un projet incrémente une version et pré-construit une image, si bien que le prochain worker démarre à chaud au lieu de rejouer votre
postCreatedepuis zéro. - Des ports transférés sur leurs propres noms d’hôte — votre serveur de dev apparaît à
worker-<id>-3000.localhostpendant que l’éditeur reste àworker-<id>.localhost.
Une application Next.js, servie par un petit serveur Node maison (server.ts) qui fait double emploi : il sert de reverse proxy aux sous-domaines des workers et héberge le WebSocket du terminal. dockerode parle à /var/run/docker.sock. SQLite (via Drizzle) garde projets, workers et secrets dans un seul fichier, sur un volume. C’est toute la stack — pas de file d’attente, pas de Redis, pas de Postgres, pas de processus agent par machine.
La conséquence importante de passer par le socket de l’hôte : les workers sont des frères, pas des enfants. Ce sont des conteneurs ordinaires sur votre daemon ; vous pouvez donc redémarrer ou mettre à jour le plan de contrôle sans déranger ce qui tournait — et si un jour vous voulez fouiller dans un worker directement avec docker exec, rien ne vous en empêche.
Les bind mounts ne veulent pas dire ce que vous croyez depuis un conteneur. Le plan de contrôle tourne dans Docker et demande au daemon de créer les workers : chaque chemin des Binds d’un worker est donc résolu par le daemon — sur l’hôte. Un chemin qui n’existe que dans le système de fichiers du plan de contrôle n’a aucun sens pour lui : demandez-le et vous obtenez un dossier vide, sans un mot. C’est pour cela que vos clés SSH font un détour. Le ~/.ssh de l’hôte est monté en lecture seule dans le plan de contrôle, qui lit la clé choisie et la fait écrire dans le ~/.ssh du worker par son script de setup, puis génère un unique ~/.ssh/config qui liste les identités dans un ordre fixe (votre clé d’abord, la deploy key du projet ensuite) pour que git over SSH ne tombe pas sur la mauvaise. Plus de pièces mobiles qu’un bind mount — mais c’est la seule version qui marche depuis un conteneur.
Isoler les workers et leur servir de proxy tirent en sens inverse. Chaque worker a son propre réseau bridge : deux workers ne voient pas les services l’un de l’autre — un bon défaut, qui permet aussi à deux projets d’écouter tous les deux sur le port 3000 sans conflit. Mais du coup le plan de contrôle ne peut pas les joindre non plus, ce qui casse tout l’intérêt de worker-<id>.localhost. La solution : le plan de contrôle se rattache lui-même au réseau de chaque worker quand celui-ci démarre (et s’en détache quand il est supprimé). Quelques lignes, mais le genre de chose qu’on ne découvre que quand la première requête du proxy reste pendue.
*.localhost est un DNS wildcard gratuit, et c’est la raison pour laquelle le port 80 est le défaut. Chrome, Edge et Firefox résolvent tout ce qui se trouve sous .localhost vers 127.0.0.1, sans toucher à /etc/hosts ni installer dnsmasq — c’est ce qui permet aux sous-domaines par worker de fonctionner en local. Le revers : le routage par sous-domaine ne paraît gratuit que sur le port par défaut. Lancez le plan de contrôle sur :3900 et chaque URL de worker se voit ajouter un :3900 — ça marche, mais c’est nettement moins agréable. Vous pouvez changer PORT ; vous le payez simplement dans chaque lien.
Parce qu’à la cinquième fois que vous tapez le même docker run avec quatre montages, un réseau, un fichier d’environnement et un script postCreate, vous avez écrit un plan de contrôle — simplement, il vit dans l’historique de votre shell et ne marche que sur votre machine, dans votre tête. Lite, c’est cela rendu explicite : la spec est un enregistrement, le worker se reproduit à partir d’elle, et l’environnement est une URL que vous pouvez vous envoyer sur un autre appareil.
Il n’essaie pas non plus d’être DevPod ou Codespaces. Pas d’abstraction de fournisseur, pas de cible cloud — une machine, un daemon, une interface devant. C’est cette étroitesse qui le garde en un seul conteneur plutôt qu’en chaîne d’outils.
Lite, c’est une machine et un pilier. Dès que vous voulez plusieurs machines, d’autres personnes, ou des logs et des statistiques sur l’ensemble, c’est la plateforme hébergée — la même forme de spec de projet : on recrée un projet, ce n’est pas une migration.
Ce que je voulais avant tout, c’est que le point de départ honnête soit gratuit et local. La plupart des outils d’environnement vous demandent de vous inscrire avant de savoir s’ils conviennent à votre façon de travailler ; celui-ci demande un docker run.
Le dépôt est ici : spuntodotnet/spunto-lite, sous licence MIT. Issues et PR bienvenues.
Mathieu
Fondateur, SpuntoConstruit Spunto — des environnements de développement auto-hébergés, pour les ingénieurs qui veulent que leurs machines travaillent pour eux, pas contre eux.