Aller au contenu principal

Écrire Compose toi-même

Le contrat compose de production — réseaux, alias, sondes, volumes — pour écrire la stack sans la CLI.

13 min de lecture

Cette page est ce qu’une stack doit reproduire quand tu écris Compose ou Kubernetes toi-même au lieu de lancer tale deploy : quels services tiennent l’état, les noms DNS, les sondes, les volumes. Le chemin CLI reste dans Démarrage rapide et Montées de version.

Quand ce chemin est le bon

Prends la CLI quand tu peux la faire tourner. Prends cette page quand tu écris Compose, ou quand tu maps le même contrat sur Kubernetes.

Cette page quandLa CLI quand
Tu écris le compose de production, ou un mapping cluster — air-gap, automation déjà en place, pas de CLI sur l’hôteDémarrage rapide plus tale deploy quand tu veux le blue-green, tale backup et tale rollback

Il n’existe pas de chart Helm officiel.

Avec état et sans état

Dix services, deux sortes. Les services avec état tiennent les disques et l’identité fixe — tu les recrées, tu perds des données ou le DNS casse. Les services sans état sont des replicas interchangeables d’une image ; tu les recrées sur place à l’upgrade. Un seul fichier compose est le défaut. Des fichiers séparés ou Kubernetes t’appartiennent, tant que les noms DNS, le réseau sandbox isolé et l’ordre de démarrage restent.

100%

sandbox-llm-gateway est le chemin harness : l’api le provisionne, et un conteneur de session l’atteint sous llm-gateway. bgutil-provider est le sidecar PO-token YouTube du worker, en best-effort — l’ingest de liens vidéo se dégrade sans lui.

Les trois services sans état partagent une image (ghcr.io/tale-project/tale/tale-platform:<version>). TALE_ROLE choisit api ou worker au boot ; l’étage web est la même image sans ce rôle. Épingle chaque image tale-* sur le même tag de release pour que les contrats de wire ne dérivent pas.

SorteServices
Avec étatproxy, db, object-store, sandbox, sandbox-egress, sandbox-llm-gateway, bgutil-provider
Sans étatplatform, backend-api, backend-worker

Les images

Chaque image tale-* est publiée sur la GitHub Container Registry sous le même tag de release, un seul numéro de version épingle donc toute la stack. Deux services tournent sur des images upstream qui ont leurs propres versions.

ServiceImage
platform, backend-api, backend-workerghcr.io/tale-project/tale/tale-platform:<version>
proxyghcr.io/tale-project/tale/tale-proxy:<version>
dbghcr.io/tale-project/tale/tale-db:<version>
sandboxghcr.io/tale-project/tale/tale-sandbox:<version>
sandbox-egressghcr.io/tale-project/tale/tale-sandbox-egress:<version>
sandbox-llm-gatewayghcr.io/tale-project/tale/tale-sandbox-llm-gateway:<version>
object-storeminio/minio:RELEASE.2025-04-22T22-12-26Z
bgutil-providerbrainicism/bgutil-ytdlp-pot-provider:1.3.1

Une image n’est pas un service compose : le spawner crée chaque conteneur de session depuis ghcr.io/tale-project/tale/tale-sandbox-runtime:<version>, et son défaut intégré est le tag local que construit la stack de développement — un hôte qui ne l’a jamais construit nomme l’image de la registry dans SANDBOX_RUNTIME_IMAGE, sinon Run code, le rendu web et la génération de documents échouent tous sur une image introuvable. Les exemples ci-dessous épinglent la release que documente cette page ; remplace le tag par la release que tu installes.

Les services sans état

Le fichier ci-dessous, ce sont les trois rôles sans état — alias, /ping en liveness, TALE_ROLE, NET_ADMIN. Pose les services avec état dans le même fichier ou ailleurs ; les tableaux de cette page disent ce qu’ils doivent encore faire. Épingle le tag d’image et remplis .env depuis la Référence d’environnement.

yaml
# Stateless app tier. No container_name: --scale needs free names.
# Add db, proxy, sandbox, … in this file or another — your call. In one file,
# add depends_on: { db: { condition: service_healthy }, … } as well.
services:
  platform:
    image: ghcr.io/tale-project/tale/tale-platform:0.5.11
    env_file: [.env]
    volumes: ['config-data:/app/data:ro']
    restart: unless-stopped
    stop_grace_period: 45s
    healthcheck:
      test:
        [
          'CMD-SHELL',
          'curl -sf http://localhost:3000/api/health && [ -f /tmp/platform-ready ]',
        ]
      interval: 5s
      timeout: 3s
      retries: 3
      start_period: 180s
    networks:
      internal:
        aliases: [platform]
  backend-api:
    image: ghcr.io/tale-project/tale/tale-platform:0.5.11
    environment:
      TALE_ROLE: api
      PORT: '3005'
      TALE_CONFIG_DIR: /app/data
      DATABASE_URL: postgresql://tale:${DB_PASSWORD}@db:5432/tale_app
      SANDBOX_URL: http://sandbox:8003
      SANDBOX_HTTP_API_BASE_URL: http://backend-api:3005
      OBJECT_STORE_ENDPOINT: http://object-store:9000
    env_file: [.env]
    volumes: ['config-data:/app/data']
    cap_add: [NET_ADMIN]
    restart: unless-stopped
    healthcheck:
      test: ['CMD-SHELL', 'curl -sf http://localhost:3005/ping']
      interval: 10s
      timeout: 3s
      retries: 3
      start_period: 30s
    networks:
      internal:
        aliases: [backend-api]
      sandbox:
        aliases: [backend-api]
  backend-worker:
    image: ghcr.io/tale-project/tale/tale-platform:0.5.11
    environment:
      TALE_ROLE: worker
      TALE_CONFIG_DIR: /app/data
      DATABASE_URL: postgresql://tale:${DB_PASSWORD}@db:5432/tale_app
      SANDBOX_URL: http://sandbox:8003
      SANDBOX_HTTP_API_BASE_URL: http://backend-api:3005
      OBJECT_STORE_ENDPOINT: http://object-store:9000
    env_file: [.env]
    volumes: ['config-data:/app/data']
    cap_add: [NET_ADMIN]
    restart: unless-stopped
    healthcheck: { disable: true }
    networks: [internal]
volumes:
  config-data:
networks:
  internal:
  sandbox:
    name: tale-sandbox-net
    internal: true
    enable_ipv6: false

Il n’existe pas de compose de production versionné à recopier. La CLI génère une paire de fichiers et l’efface après up. Ton fichier n’a pas à lui ressembler.

Les secrets que tu génères avant le premier boot

tale init génère chaque secret et écrit le .env ; sans la CLI, ce travail est le tien. La Référence d’environnement dit ce que fait chaque variable — les quatre ci-dessous sont celles qu’une stack montée à la main oublie le plus souvent, parce que le fichier d’exemple les laisse commentées pour que la CLI les remplisse.

VariableValeurCe qui casse sans elle
SANDBOX_TOKENopenssl rand -hex 32Le spawner s’arrête au démarrage. Il tient le socket docker de l’hôte et répond à chaque conteneur de session, il n’a donc pas de mode non signé ; le backend signe chaque appel au spawner avec la même valeur.
OBJECT_STORE_ACCESS_KEYtale, ou un nom à toiLe backend logue object store (skipped) au boot et refuse chaque upload. Il n’existe pas de défaut d’image pour elle — l’utilisateur root du store porte la même valeur.
OBJECT_STORE_SECRET_KEYopenssl rand -hex 32Même saut, même silence. Une rotation plus tard rend orphelins tous les blobs déjà écrits sous l’ancien credential.
OBJECT_STORE_PUBLIC_ENDPOINTton SITE_URLLes uploads échouent dans le navigateur avec une erreur réseau : l’URL présignée que le backend distribue pointe vers le http://object-store:9000 interne, qu’aucun navigateur ne joint.

L’endpoint public se pose avant de démarrer, pas après. Le backend seede la connexion blob par défaut du déploiement dans le volume de config au premier boot (default/object-storage/connection.json) et ne réécrit jamais un défaut déjà présent — poser la variable sur une instance déjà démarrée ne change donc rien. Pour réparer une instance dans cet état, ajoute "publicEndpoint": "<ton SITE_URL>" dans ce fichier et redémarre le backend.

Réseaux et noms DNS

Deux réseaux Docker portent chaque saut. Un réseau compose ordinaire suffit pour le plan interne. Le pont sandbox doit s’appeler tale-sandbox-net et être internal pour que le spawner puisse docker run --network tale-sandbox-net et qu’un conteneur de session ne joigne pas Internet sans passer par sandbox-egress. Un pont sans internal est un chemin ouvert vers l’extérieur.

Nom que le processus résoutQui répondRéseaux
backend-apiChaque replica api saine encore attachéeinternal, sandbox
platformChaque replica d’étage web saine encore attachéeinternal
knowledge-dbLe service db (la production replie le corpus dans le même Postgres)internal
object-storeMinIOinternal
sandboxLe spawner sandboxinternal, sandbox
sandbox-egressLe proxy d’egressinternal, sandbox
llm-gatewaysandbox-llm-gatewayinternal, sandbox
HOST (ton hostname public)proxy, pour qu’un conteneur puisse faire un hairpin vers l’URL publiqueinternal

Les workers n’ont pas d’alias partagé. Rien n’adresse un worker par nom ; ils ne font que prendre des jobs dans la queue. Les alias suffixés par une couleur (backend-api-blue, platform-green) ne servent que pour un blue-green pendant que deux versions tournent à la fois.

Le proxy envoie les voies API app vers backend-api:3005 (BACKEND_UPSTREAM). Il envoie /api/health et la SPA vers platform:3000, et il sonde platform sur /api/health. Fais échouer cette sonde sur une replica web en drain et Caddy marque tout le site down.

Volumes

Nomme ces volumes logiques dans ton compose. Un seul fichier peut laisser compose les créer. Marque-les external seulement si quelque chose hors de ce fichier doit monter les mêmes disques.

VolumeQui le monteCe qu’il tient
config-dataBackend en lecture-écriture, platform en lecture seule, sandbox en lecture seule sous /app/platform-configConfig d’org : agents, skills, fournisseurs, gouvernance, SSO, branding
db-datadb sous /var/lib/postgresql/datatale_app et tale_knowledge
db-backupdb sous /var/lib/postgresql/backupCible de backup Postgres dans le conteneur
object-store-dataobject-store sous /dataBlobs
caddy-data, caddy-configproxyCertificats et état Caddy
llm-gateway-datasandbox-llm-gateway sous /app/dataClés virtuelles par session

Les instances montées depuis avant 0.5.11 peuvent encore avoir un volume convex-data à côté de config-data. La CLI copie le magasin une fois et ne supprime jamais l’ancien volume. Un premier boot écrit à la main sur un hôte neuf n’a pas besoin de convex-data.

Sondes de santé

Liveness et readiness sont deux questions différentes. Les mélanger coupe une replica en drain du DNS avant la fin du travail en vol, ou garde une replica pas prête dans le pool.

ServiceSondeCe qu’elle veut dire
backend-apiGET /ping sur :3005Liveness. Reste 200 pendant que la replica draine. Docker et Caddy s’en servent.
backend-apiGET /ready sur :3005Readiness. 503 dès que cette replica draine. Le déploiement pose la question ; Docker et Caddy non.
platformGET /api/health et fichier /tmp/platform-readyPrête à servir la SPA. Garde ça à 200 tant que la replica tient encore l’alias platform.
backend-workerAucuneLe worker n’expose pas de HTTP. Désactive le healthcheck web cuit dans l’image, sinon la replica lit unhealthy en permanence.
proxyhttp://127.0.0.1:2020/healthSanté admin de Caddy.
dbpg_isready et fichier /tmp/.db_readyPostgres accepte les connexions et l’init est fini (base de connaissances et extensions). start_period 120s. Arrête le conteneur avec SIGINT, pas SIGTERM.
object-storemc ready localMinIO accepte les écritures.
sandboxGET /health sur :8003Le spawner est up. Ne publie pas ce port sur un hôte public.
sandbox-egressTCP 127.0.0.1:3128tinyproxy écoute. Ne sonde pas un hôte externe.
sandbox-llm-gatewayGET /health sur :8080La gateway est up.

Env que Compose doit injecter

La Référence d’environnement est chaque variable que le processus lit depuis .env. Les lignes ci-dessous sont ce que le fichier compose doit poser lui-même — les défauts de l’image pointent le processus vers le mauvais hôte.

NomValeur sur une stack de production
TALE_ROLEapi sur backend-api, worker sur backend-worker. Unset sur platform.
PORT3005 sur l’api. Le défaut BACKEND_UPSTREAM du proxy est backend-api:3005.
TALE_CONFIG_DIR/app/data
DATABASE_URLpostgresql://tale:${DB_PASSWORD}@db:5432/tale_app
SANDBOX_URLhttp://sandbox:8003
SANDBOX_HTTP_API_BASE_URLhttp://backend-api:3005
OBJECT_STORE_ENDPOINThttp://object-store:9000
SANDBOX_EGRESS_NETWORKtale-sandbox-net
SANDBOX_EGRESS_PROXYhttp://sandbox-egress:3128
SANDBOX_TOKENLa même valeur partout. sandbox ne démarre pas sans lui ; le backend signe ses appels au spawner avec.
SANDBOX_RUNTIME_IMAGEghcr.io/tale-project/tale/tale-sandbox-runtime:<version> sur sandbox. Le défaut est un tag de build local qu’un hôte de production n’a pas.
BACKEND_UPSTREAMbackend-api:3005 sur proxy.
OBJECT_STORE_UPSTREAMobject-store:9000 sur proxy, pour que les URL présignées soient relayées sous /<bucket>/*.
OBJECT_STORE_BUCKETtale-blobs par défaut. Si tu le renommes, le même nom doit atteindre proxy et les deux rôles backend.
MINIO_ROOT_USER, MINIO_ROOT_PASSWORDSur object-store : le store lit ses propres noms, mappe donc OBJECT_STORE_ACCESS_KEY et OBJECT_STORE_SECRET_KEY dessus.
TALE_DB_ROLEUnset sur la db repliée. Le rôle par défaut crée tale_knowledge et applique les migrations du corpus ; platform les saute et laisse le corpus sans tables.

Capacités et mounts qui cassent s’ils manquent

Ils ont l’air optionnels et échouent fermés quand ils manquent.

ServiceDoit avoirCe qui casse sans
backend-api, backend-workercap_add: [NET_ADMIN]L’entrypoint ne peut pas poser la barrière iptables SSRF (IMDS, link-local, RFC1918).
sandbox-egresscap_drop: [ALL] puis NET_ADMIN, DAC_OVERRIDE, CHOWN, SETUID, SETGID, NET_BIND_SERVICEPas de barrière IMDS/RFC1918 ; tinyproxy ne peut ni binder ni abandonner ses privilèges.
sandbox/var/run/docker.sock et /var/lib/tale-sandbox montés en bind 1:1Le spawner ne peut pas créer les conteneurs de session ; les chemins workspace que le daemon monte ne correspondent pas.
dbstop_signal: SIGINT, stop_grace_period: 60s, shm_size: 256mbUn arrêt SIGTERM qui attend les clients finit en SIGKILL et peut laisser l’index BM25 avec une page à zéro.
platformstop_grace_period: 45sLa grâce Docker par défaut de 10s envoie SIGKILL à l’étage web au milieu du drain et coupe le HTTP/SSE en vol.
object-storeAucun port publiéLes URLs présignées passent par le proxy. Publier MinIO est une surface publique en plus.

Ne publie que 80 et 443 sur proxy. Tout le reste reste sur le réseau interne.

Ordre de démarrage

Monte les stores d’abord, puis le plan sandbox, puis l’étage app. Une api qui démarre avant que db et object-store soient sains crash-loop sur ENOTFOUND et sur une base manquante. Dans un seul fichier, depends_on avec service_healthy suffit.

bash
docker compose up -d
# Wait until db, object-store, proxy, sandbox, sandbox-egress, sandbox-llm-gateway
# report healthy. bgutil-provider is best-effort — YouTube ingest degrades without it.

Donne à chaque service une politique de redémarrage (restart: unless-stopped). Rien d’autre ne ramène un conteneur après un reboot de l’hôte ou un kill OOM, et une stack qui boote une fois et plus jamais est la panne que les opérateurs trouvent des semaines plus tard.

Les migrations de schéma tournent dans le backend au boot, sous un verrou advisory. Il n’y a pas d’étape migrate à part. Une replica qui ne peut pas appliquer une migration ne démarre pas ; laisse l’ancienne api tourner jusqu’à ce que la nouvelle soit saine.

Kubernetes

Pas de chart Helm, pas de manifeste officiel. Mappe le contrat Docker ; n’invente pas une seconde architecture.

DockerCluster
Sans état platform, backend-api, backend-workerDeployments. Même image ; TALE_ROLE choisit le processus. Ce sont ceux que tu scales.
Avec état db, object-store, proxy, plan sandboxStatefulSets (ou équivalent) plus les volumes de cette page. Ne fais pas tourner deux écrivains contre un seul disque.
Noms DNS compose (backend-api, platform, knowledge-db, sandbox, llm-gateway, …)Services avec ces noms. Le proxy et le sandbox les résolvent.
tale-sandbox-net marqué internalUne NetworkPolicy (ou un CNI isolé) qui bloque un pod de session vers Internet sauf par sandbox-egress.
GET /ping sur l’apiLiveness. Reste 200 pendant que la replica draine.
GET /ready sur l’apiLa question de readiness de ton rollout. Ne pointe pas le Service sur /ready si tu draines.
docker.sock sandbox et /var/lib/tale-sandbox montés en bind 1:1La partie dure. Le spawner crée les conteneurs de session ; le chemin workspace que le daemon monte doit matcher le chemin dans le spawner. Un cluster sans socket Docker (ou un équivalent) ne peut pas faire tourner le plan sandbox.
cap_add: [NET_ADMIN] sur le backendLa barrière iptables SSRF. Sans elle l’entrypoint ne peut pas verrouiller IMDS et RFC1918.

Ne publie que 80 et 443. Laisse Postgres, MinIO et le port sandbox hors de la liste de Services publics.

Recréer sur place les Deployments sans état est le défaut. Le zéro downtime, c’est un rolling update que tu construis.

Ce que tu perds sans la CLI

tale deploy n’est pas un compose up. Les commandes ci-dessous n’ont pas d’équivalent dans un fichier que tu maintiens.

Comportement CLICe que tu fais à la place
Bascule blue-green : démarrer la couleur inactive, attendre chaque replica, drain l’ancienne api, puis docker network disconnectRecréer sur place, ou implémenter la bascule toi-même. Disconnect coupe les connexions vivantes — drain d’abord.
tale backup / tale rollbackTes propres snapshots de volumes. Le rollback d’un minor ou d’un major est une restauration de snapshot, pas une down-migration.
Reprise flip-pending après un déploiement tuéTon propre enregistrement de la couleur vivante.
Copie du volume de config depuis convex-data sur un hôte d’avant 0.5.11Copie le magasin toi-même, ou démarre neuf.
/v1/drain sandbox avant un roll in-place du spawnerSIGTERM plus 30s de grâce à l’arrêt est la rampe ; les runs en vol meurent quand même si tu recrées sans drain.

Recréer sur place les services sans état est le défaut. Le zéro downtime est la partie que tu réimplémentes.

Ce que la production ne doit pas faire

Ça a l’air local et casse une instance publique.

Ne pasPourquoi
Publier 5432, 8003 ou MinIOSurface publique en plus. Les URLs présignées passent par le proxy.
Faire tourner un second Postgres pour le corpusLa production replie tale_knowledge dans db et alias ce service knowledge-db.
Épingler les noms sur l’étage appLes replicas ne peuvent pas partager un nom de conteneur.
Builder depuis les sources sur un hôte publicÉpingle ghcr.io/tale-project/tale/<image>:<tag>.
Livrer des secrets placeholderGénère-les avant le premier up.
Donner aux conteneurs de session un chemin vers InternetLe réseau sandbox (ou sa NetworkPolicy) doit être isolé.

Où cela s’inscrit

Tu as maintenant le contrat : quels services tiennent l’état, deux réseaux, les noms DNS que le proxy et le sandbox résolvent, les sondes à ne pas inverser, et ce que Kubernetes doit encore faire. La Référence d’environnement est chaque variable que les conteneurs lisent. Architecture des conteneurs est ce que chaque conteneur possède quand l’un d’eux meurt. La plupart des équipes veulent encore le démarrage rapide et tale deploy — cette page est le chemin quand ce wrapper est précisément ce que tu ne peux pas faire tourner.

© 2026 Tale par Ruler GmbH — certifié ISO 27001 et SOC 2.

Tale est sous licence MIT — libre d'utilisation, de modification et de distribution.