← tous les articles

Un conteneur, un monde : héberger un jeu à état sur une infra de l'éphémère

11 août 2026 · 4 min de lecture

docker-swarmstatefulpostgresbootstrap

Le cluster héberge surtout des apps sans état : le disque dur, c’est Postgres et R2, le conteneur est jetable. On le déploie en start-first (la nouvelle tâche démarre avant que l’ancienne meure, zéro coupure), on l’éteint à 0 réplica quand personne ne s’en sert. Des réflexes optimisés pour l’oubli.

Puis arrive un candidat qui coche exactement la case inverse : OR_AI, un jeu de stratégie spatiale par navigateur. Empire persistant, temps réel, des « mondes » qui tournent en continu. Et une contrainte affichée en gros dans son Dockerfile :

⚠ Un conteneur = UN monde. L’état vit en mémoire, une boucle tourne chaque seconde : ne jamais mettre à l’échelle un service de monde au-delà d’une réplique.

L’état vit en RAM, et il avance tout seul

Chaque monde est un processus Node : les joueurs, la galaxie, les flottes vivent dans des Map en mémoire. Un setInterval d’une seconde fait avancer la production, la recherche, les combats. Postgres n’est pas la vérité vivante — c’est le filet : un flush SQL périodique, plus une sauvegarde propre sur SIGTERM.

Deux conséquences qui retournent les réflexes du parc :

  • Jamais deux instances du même monde. Deux boucles sur la même base se marcheraient dessus et corromperaient l’état. Donc replicas: 1, et surtout order: stop-first — l’exact opposé du start-first habituel : on tue l’ancienne tâche avant de démarrer la nouvelle, quitte à une micro-coupure. Le chevauchement, ici, est un bug.
  • Pas de scale-to-zero. Un monde persistant doit continuer à ticker même sans joueur connecté — l’empire pousse pendant la nuit. L’endormir, c’est arrêter le temps.

Trois mondes, donc trois services replicas: 1 en stop-first, épinglés sur un nœud (leurs images téléversées et leur registre sont un état local non-régénérable), devant un Postgres en cluster Patroni. Sur le papier, propre. On déploie.

Le serpent qui se mord la queue

Au premier déploiement, les trois mondes partent… et crash-loopent. Normal : leur base n’existe pas encore, ni leur fichier de config. Le plan était de créer les bases juste après, avec l’outil du jeu (create-world, qui se connecte en tant que rôle applicatif oria).

Sauf que create-world refuse de démarrer :

remaining connection slots are reserved for roles with the SUPERUSER attribute

Traduction : plus une seule connexion Postgres libre pour un rôle non-superuser. Or qui mange toutes les connexions ? Les trois mondes qui crash-loopent. À chaque redémarrage, chacun ouvre son pool vers Postgres, échoue, meurt, recommence — assez vite pour saturer les max_connections. Le serpent se mord la queue :

les mondes n'ont pas de base ──► ils crash-loopent
        ▲                                │
        │                                ▼
create-world ne peut créer la base ◄── ils saturent les connexions Postgres

On ne peut pas créer la base parce que les mondes qui n’ont pas de base saturent la base.

Le correctif : tout éteindre pour amorcer

La sortie est contre-intuitive sur une infra qui vise le zéro-coupure : éteindre explicitement les mondes le temps de l’amorçage.

docker service scale oria_w1=0 oria_w2=0 oria_w3=0
# ... créer le rôle, créer les 3 bases + schéma + galaxie ...
docker service scale oria_w1=1 oria_w2=1 oria_w3=1

Connexions libérées, create-world se connecte, sème les trois bases, écrit les configs, et les mondes remontent — cette fois avec un endroit où vivre. Le premier démarrage d’un service à état n’est pas un déploiement, c’est un accouchement : il faut parfois faire le vide avant.

La leçon

Une fleet optimisée pour le stateless jetable — start-first, scale-to-zero — applique exactement les mauvais réflexes à un service dont l’état vit en mémoire et avance tout seul. Là, c’est stop-first, toujours allumé, un conteneur par monde. Et pour amorcer un tel service, le geste qui débloque est souvent celui qu’on évite en prod : tout éteindre.

Le reste n’était que façade — une image privée que les nœuds ne pouvaient pas tirer, un sous-domaine de second niveau que le certificat gratuit ne couvrait pas. Des portes qu’on ouvre une à une. La vraie surprise, c’était l’inversion : héberger quelque chose de vivant sur une infra faite pour ce qui s’oublie.

Les trois mondes tournent : oria.zebbox.net. Le reste du homelab est public.


📬 La newsletter homelab

Les nouveaux articles + retours d'expérience self-hosting, sans spam. Désinscription en un clic.