Skip to content

Instantly share code, notes, and snippets.

@lenybernard
Created August 4, 2026 10:20
Show Gist options
  • Select an option

  • Save lenybernard/017fc870448611f9f5906b3cae01e230 to your computer and use it in GitHub Desktop.

Select an option

Save lenybernard/017fc870448611f9f5906b3cae01e230 to your computer and use it in GitHub Desktop.
# Stack locale de développement, orchestrée par le Makefile : services
# (réplique Supabase — mêmes ports et clés de démo que la CLI, cf.
# supabase/config.toml —, Redis, Strapi, Studio) et apps Node conteneurisées.
# Adapté du docker-compose self-hosted officiel de Supabase. Toutes les
# valeurs sont des défauts de dev surchargeables ; les clés (x-dev-values)
# vivent chiffrées dans le .env racine → toujours passer par make (dotenvx),
# jamais par `docker compose` nu.
#
# ⚠ Les scripts .docker/postgres/init ne s'exécutent qu'au premier démarrage
# du volume : après les avoir modifiés, `make db-reset`.
#
# Chaque composant porte un profil, sélectionnable via `make up` (checkbox,
# persisté dans .env.local sous COMPOSE_PROFILES) ou explicitement (make up
# apps=<...>, cf. scripts/dev-apps.mts) :
# supabase / studio / redis / strapi → services
# app / site / panier / backend / tools → un conteneur par app,
# daemon nx partagé (voir la section « Apps Node » en fin de fichier)
# dbtools → sqitch (migrations), seeder (données de test) — via make db-*
# functions → edge-runtime Deno (supabase/functions)
name: tet
x-dev-values:
# Clés JWT de démo de la CLI Supabase, signées avec le secret de dev ;
# identiques à celles des .env des apps. Publiques par définition, mais
# stockées chiffrées (dotenvx) dans le .env racine pour ne pas déclencher
# les scanners de secrets : les cibles make déchiffrent avant d'appeler
# compose (cf. COMPOSE dans le Makefile). Un `docker compose` lancé à la
# main récupère les valeurs `encrypted:…` du .env — le garde-fou de
# l'entrypoint kong échoue alors explicitement.
jwt-secret: &jwt-secret ${SUPABASE_JWT_SECRET:?déchiffré du .env racine par make via dotenvx}
anon-key: &anon-key ${SUPABASE_ANON_KEY:?déchiffré du .env racine par make via dotenvx}
service-role-key: &service-role-key ${SUPABASE_SERVICE_ROLE_KEY:?déchiffré du .env racine par make via dotenvx}
services:
# ——————————————————————————— Base de données ———————————————————————————
# Postgres 15 (parité prod/CI, cf. supabase/config.toml major_version) —
# l'image Supabase embarque extensions (pg_net, pg_cron, http…), rôles et
# schémas auth/storage requis par les migrations sqitch.
db:
profiles: [ supabase ]
image: supabase/postgres:15.14.1.159
ports:
- "54322:5432"
command:
- postgres
- -c
- config_file=/etc/postgresql/postgresql.conf
- -c
- log_min_messages=fatal
environment:
POSTGRES_HOST: /var/run/postgresql
PGPORT: "5432"
POSTGRES_PORT: "5432"
PGPASSWORD: &pg-password ${POSTGRES_PASSWORD:-postgres}
POSTGRES_PASSWORD: *pg-password
PGDATABASE: postgres
POSTGRES_DB: postgres
JWT_SECRET: *jwt-secret
JWT_EXP: "3600"
volumes:
- ./.docker/postgres/init/98-webhooks.sql:/docker-entrypoint-initdb.d/init-scripts/98-webhooks.sql:ro
- ./.docker/postgres/init/99-roles.sql:/docker-entrypoint-initdb.d/init-scripts/99-roles.sql:ro
- ./.docker/postgres/init/99-jwt.sql:/docker-entrypoint-initdb.d/init-scripts/99-jwt.sql:ro
- ./.docker/postgres/init/99-realtime.sql:/docker-entrypoint-initdb.d/migrations/99-realtime.sql:ro
- db-data:/var/lib/postgresql/data
- db-config:/etc/postgresql-custom
healthcheck:
test: [ "CMD", "pg_isready", "-U", "postgres", "-h", "localhost" ]
interval: 5s
timeout: 5s
retries: 10
# ————————————————————— Passerelle API (port 54321) —————————————————————
kong:
profiles: [ supabase ]
image: kong:2.8.5-alpine
platform: linux/amd64
ports:
- "54321:8000"
# Kong 2.8 ne lit pas l'environnement dans sa config déclarative :
# l'entrypoint rend le template en substituant les deux clés (sed ciblé,
# pas d'eval : le reste du fichier n'est jamais interprété par le shell),
# après avoir vérifié qu'elles sont déchiffrées (lancement via make,
# pas compose nu).
entrypoint:
- /bin/sh
- -c
- |
set -eu
case "$$SUPABASE_ANON_KEY$$SUPABASE_SERVICE_ROLE_KEY" in
*encrypted:*|'')
echo '✗ clés Supabase absentes ou non déchiffrées — lancez via make (dotenvx déchiffre le .env racine)' >&2
exit 1;;
esac
sed -e "s|\$$SUPABASE_ANON_KEY|$$SUPABASE_ANON_KEY|" \
-e "s|\$$SUPABASE_SERVICE_ROLE_KEY|$$SUPABASE_SERVICE_ROLE_KEY|" \
/home/kong/kong.template.yml > /home/kong/kong.yml
exec /docker-entrypoint.sh kong docker-start
environment:
KONG_DATABASE: "off"
KONG_DECLARATIVE_CONFIG: /home/kong/kong.yml
KONG_DNS_ORDER: LAST,A,CNAME
KONG_PLUGINS: request-transformer,cors,key-auth,acl
KONG_NGINX_PROXY_PROXY_BUFFER_SIZE: 160k
KONG_NGINX_PROXY_PROXY_BUFFERS: 64 160k
SUPABASE_ANON_KEY: *anon-key
SUPABASE_SERVICE_ROLE_KEY: *service-role-key
volumes:
- ./.docker/kong/kong.template.yml:/home/kong/kong.template.yml:ro
depends_on:
gotrue:
condition: service_started
rest:
condition: service_started
healthcheck:
test: [ "CMD", "kong", "health" ]
interval: 5s
timeout: 5s
retries: 5
# ————————————————————————— Authentification ————————————————————————————
# Réplique supabase/config.toml : site_url, confirmations, sujets et
# templates d'emails FR (servis par mail-templates), rate limits e2e.
gotrue:
profiles: [ supabase ]
image: supabase/gotrue:v2.193.1
depends_on:
db:
condition: service_healthy
environment:
GOTRUE_API_HOST: 0.0.0.0
GOTRUE_API_PORT: "9999"
API_EXTERNAL_URL: http://localhost:54321
GOTRUE_DB_DRIVER: postgres
GOTRUE_DB_DATABASE_URL: postgres://supabase_auth_admin:${POSTGRES_PASSWORD:-postgres}@db:5432/postgres
GOTRUE_SITE_URL: http://localhost:3000
# Wildcards : autorise les redirections vers n'importe quel port localhost
# (worktrees sur ports décalés). Le glob gotrue ne traverse pas « / »,
# d'où les deux motifs (racine + chemins).
GOTRUE_URI_ALLOW_LIST: http://localhost:*,http://localhost:*/**
GOTRUE_DISABLE_SIGNUP: "false"
GOTRUE_JWT_ADMIN_ROLES: service_role
GOTRUE_JWT_AUD: authenticated
GOTRUE_JWT_DEFAULT_GROUP_NAME: authenticated
GOTRUE_JWT_EXP: "3600"
GOTRUE_JWT_SECRET: *jwt-secret
GOTRUE_JWT_ISSUER: http://localhost:54321/auth/v1
GOTRUE_EXTERNAL_EMAIL_ENABLED: "true"
GOTRUE_MAILER_AUTOCONFIRM: "false"
GOTRUE_MAILER_SECURE_EMAIL_CHANGE_ENABLED: "false"
GOTRUE_MAILER_URLPATHS_INVITE: http://localhost:54321/auth/v1/verify
GOTRUE_MAILER_URLPATHS_CONFIRMATION: http://localhost:54321/auth/v1/verify
GOTRUE_MAILER_URLPATHS_RECOVERY: http://localhost:54321/auth/v1/verify
GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE: http://localhost:54321/auth/v1/verify
GOTRUE_MAILER_TEMPLATES_MAGIC_LINK: http://mail-templates/magic_link.html
GOTRUE_MAILER_TEMPLATES_CONFIRMATION: http://mail-templates/confirmation.html
GOTRUE_MAILER_TEMPLATES_RECOVERY: http://mail-templates/recovery.html
GOTRUE_MAILER_TEMPLATES_EMAIL_CHANGE: http://mail-templates/email_change.html
GOTRUE_MAILER_SUBJECTS_MAGIC_LINK: Connectez-vous à Territoires en Transitions
GOTRUE_MAILER_SUBJECTS_CONFIRMATION: Connectez-vous à Territoires en Transitions
GOTRUE_MAILER_SUBJECTS_RECOVERY: Réinitialiser votre mot de passe sur Territoires en Transitions
GOTRUE_MAILER_SUBJECTS_EMAIL_CHANGE: Confirmer le changement de votre email sur Territoires en Transitions
GOTRUE_SMTP_HOST: mailpit
GOTRUE_SMTP_PORT: "1025"
GOTRUE_SMTP_ADMIN_EMAIL: admin@territoiresentransitions.fr
GOTRUE_SMTP_SENDER_NAME: Territoires en Transitions
GOTRUE_SMTP_MAX_FREQUENCY: 1s
GOTRUE_RATE_LIMIT_EMAIL_SENT: "50"
GOTRUE_RATE_LIMIT_TOKEN_REFRESH: "50"
GOTRUE_RATE_LIMIT_VERIFY: "50"
GOTRUE_RATE_LIMIT_OTP: "50"
healthcheck:
test: [ "CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:9999/health" ]
interval: 5s
timeout: 5s
retries: 3
# Sert les templates d'emails FR à gotrue (comme le fait la CLI)
mail-templates:
profiles: [ supabase ]
image: nginx:1.29-alpine
volumes:
- ./supabase/templates:/usr/share/nginx/html:ro
# ———————————————————————————— API REST ——————————————————————————————————
rest:
profiles: [ supabase ]
image: postgrest/postgrest:v13.0.7
depends_on:
db:
condition: service_healthy
environment:
PGRST_DB_URI: postgres://authenticator:${POSTGRES_PASSWORD:-postgres}@db:5432/postgres
PGRST_DB_SCHEMAS: public,storage # supabase/config.toml [api].schemas
PGRST_DB_EXTRA_SEARCH_PATH: public,extensions
PGRST_DB_MAX_ROWS: "5000"
PGRST_DB_ANON_ROLE: anon
PGRST_DB_USE_LEGACY_GUCS: "false"
PGRST_JWT_SECRET: *jwt-secret
PGRST_APP_SETTINGS_JWT_SECRET: *jwt-secret
PGRST_APP_SETTINGS_JWT_EXP: "3600"
# ———————————————————————————— Realtime ——————————————————————————————————
realtime:
profiles: [ supabase ]
image: supabase/realtime:v2.119.0
depends_on:
db:
condition: service_healthy
networks:
default:
aliases:
- realtime-dev.supabase-realtime # le tenant est déduit du sous-domaine
environment:
PORT: "4000"
DB_HOST: db
DB_PORT: "5432"
DB_USER: supabase_admin
DB_PASSWORD: *pg-password
DB_NAME: postgres
DB_AFTER_CONNECT_QUERY: SET search_path TO _realtime
DB_ENC_KEY: supabaserealtime
API_JWT_SECRET: *jwt-secret
METRICS_JWT_SECRET: *jwt-secret
SECRET_KEY_BASE: ${REALTIME_SECRET_KEY_BASE:?déchiffré du .env racine par make via dotenvx}
ERL_AFLAGS: -proto_dist inet_tcp
DNS_NODES: "''"
RLIMIT_NOFILE: "10000"
APP_NAME: realtime
SEED_SELF_HOST: "true"
RUN_JANITOR: "true"
# ———————————————————————————— Storage ———————————————————————————————————
storage:
profiles: [ supabase ]
image: supabase/storage-api:v1.67.13
depends_on:
db:
condition: service_healthy
rest:
condition: service_started
environment:
ANON_KEY: *anon-key
SERVICE_KEY: *service-role-key
AUTH_JWT_SECRET: *jwt-secret
DATABASE_URL: postgres://supabase_storage_admin:${POSTGRES_PASSWORD:-postgres}@db:5432/postgres
POSTGREST_URL: http://rest:3000
FILE_SIZE_LIMIT: "52428800" # 50MiB (supabase/config.toml)
STORAGE_BACKEND: file
FILE_STORAGE_BACKEND_PATH: /var/lib/storage
TENANT_ID: stub
REGION: stub
GLOBAL_S3_BUCKET: stub
ENABLE_IMAGE_TRANSFORMATION: "false" # pas d'imgproxy (comme en CI)
volumes:
- storage-data:/var/lib/storage
healthcheck:
test: [ "CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:5000/status" ]
interval: 5s
timeout: 5s
retries: 3
start_period: 10s
# ————————————————— Supabase Studio (interface d'admin) ——————————————————
meta:
profiles: [ studio ]
image: supabase/postgres-meta:v0.96.6
depends_on:
db:
condition: service_healthy
environment:
PG_META_PORT: "8080"
PG_META_DB_HOST: db
PG_META_DB_PORT: "5432"
PG_META_DB_NAME: postgres
PG_META_DB_USER: postgres
PG_META_DB_PASSWORD: *pg-password
CRYPTO_KEY: ${PG_META_CRYPTO_KEY:-dev-crypto-key}
studio:
profiles: [ studio ]
image: supabase/studio:latest
ports:
- "54323:3000" # supabase/config.toml [studio].port
depends_on:
db:
condition: service_healthy
meta:
condition: service_started
environment:
HOSTNAME: 0.0.0.0
STUDIO_PG_META_URL: http://meta:8080
POSTGRES_HOST: db
POSTGRES_PORT: "5432"
POSTGRES_DB: postgres
POSTGRES_PASSWORD: *pg-password
POSTGRES_USER_READ_WRITE: postgres
PG_META_CRYPTO_KEY: ${PG_META_CRYPTO_KEY:-dev-crypto-key}
PGRST_DB_SCHEMAS: public,storage
PGRST_DB_MAX_ROWS: "5000"
PGRST_DB_EXTRA_SEARCH_PATH: public,extensions
DEFAULT_ORGANIZATION_NAME: Territoires en Transitions
DEFAULT_PROJECT_NAME: tet-local
SUPABASE_URL: http://kong:8000
SUPABASE_PUBLIC_URL: http://localhost:54321
SUPABASE_ANON_KEY: *anon-key
SUPABASE_SERVICE_KEY: *service-role-key
AUTH_JWT_SECRET: *jwt-secret
ENABLED_FEATURES_LOGS_ALL: "false"
healthcheck:
test: [ "CMD-SHELL", "node -e \"fetch('http://localhost:3000/api/platform/profile').then((r) => {if (r.status !== 200) throw new Error(r.status)})\"" ]
timeout: 10s
interval: 5s
retries: 3
start_period: 20s
# —————————————————————— Emails de test (Mailpit) ————————————————————————
mailpit:
profiles: [ supabase ]
image: axllent/mailpit:v1.30.2
ports:
- "54324:8025" # interface web
- "54325:1025" # SMTP
environment:
MP_SMTP_AUTH_ACCEPT_ANY: "1"
MP_SMTP_AUTH_ALLOW_INSECURE: "1"
# ———————————————————————— Redis (files BullMQ) ——————————————————————————
redis:
profiles: [ redis ]
image: redis:8-alpine
ports:
- "6379:6379"
volumes:
- redis-data:/data
healthcheck:
test: [ "CMD", "redis-cli", "ping" ]
interval: 10s
timeout: 5s
retries: 5
# ————————————————— Outillage base de données (profil tools) —————————————
# Migrations sqitch : réutilise l'image de la CI (.github/actions/sqitch-local),
# lancée ponctuellement par `make db-migrate` (docker compose run --rm).
sqitch:
profiles: [ dbtools ]
build:
context: .github/actions/sqitch-local
entrypoint: [ sqitch ] # neutralise l'entrypoint act
working_dir: /repo
environment:
SQITCH_TARGET: db:pg://postgres:${POSTGRES_PASSWORD:-postgres}@db:5432/postgres
volumes:
- ./sqitch.conf:/repo/sqitch.conf:ro
- ./data_layer:/repo/data_layer:ro
depends_on:
db:
condition: service_healthy
# Données de test : exécute data_layer/seed/*.sh (psql fourni par l'image db)
seeder:
profiles: [ dbtools ]
image: supabase/postgres:15.14.1.151
entrypoint: [ sh ]
working_dir: /data_layer
environment:
PG_URL: postgresql://postgres:${POSTGRES_PASSWORD:-postgres}@db:5432/postgres
volumes:
- ./data_layer:/data_layer:ro
depends_on:
db:
condition: service_healthy
# ———————————————————— Edge functions (profil functions) —————————————————
functions:
profiles: [ functions ]
image: supabase/edge-runtime:v1.74.2
# main est monté À CÔTÉ des functions, pas dedans : un point de montage
# imbriqué dans un bind mount read-only ne peut pas être créé (le dossier
# main/ n'existe pas dans supabase/functions, et ne doit pas y exister —
# la CLI supabase le prendrait pour une function à déployer).
command: [ start, --main-service, /home/deno/main ]
environment:
JWT_SECRET: *jwt-secret
SUPABASE_URL: http://kong:8000
SUPABASE_ANON_KEY: *anon-key
SUPABASE_SERVICE_ROLE_KEY: *service-role-key
SUPABASE_DB_URL: postgresql://postgres:${POSTGRES_PASSWORD:-postgres}@db:5432/postgres
VERIFY_JWT: "false"
# Les emails des functions partent dans Mailpit (http://localhost:54324)
# via son API d'envoi — jamais vers les vrais fournisseurs (Resend).
MAILPIT_URL: http://mailpit:8025
volumes:
- ./supabase/functions:/home/deno/functions:ro
- ./.docker/edge-runtime/main:/home/deno/main:ro
depends_on:
db:
condition: service_healthy
mailpit:
condition: service_started
# ———————————————————————————— CMS Strapi —————————————————————————————————
# Postgres dédié (indépendant du Postgres Supabase), comme le Strapi Cloud
# distant — nécessaire notamment pour `strapi transfer` (make cms-pull).
strapi-db:
profiles: [ strapi ]
image: postgres:18-alpine
environment:
POSTGRES_DB: strapi
POSTGRES_USER: strapi
POSTGRES_PASSWORD: ${STRAPI_DB_PASSWORD:-strapi}
volumes:
- strapi-db-data:/var/lib/postgresql
healthcheck:
test: [ "CMD", "pg_isready", "-U", "strapi" ]
interval: 5s
timeout: 5s
retries: 10
# npm install s'exécute au premier démarrage (node_modules en volume nommé) :
# le premier boot prend quelques minutes.
strapi:
profiles: [ strapi ]
build:
context: .docker/strapi
args:
UID: ${UID:-1000}
GID: ${GID:-1000}
ports:
- "1337:1337"
depends_on:
strapi-db:
condition: service_healthy
environment:
HOST: 0.0.0.0
PORT: "1337"
APP_KEYS: ${STRAPI_APP_KEYS:-devKeyA,devKeyB}
API_TOKEN_SALT: ${STRAPI_API_TOKEN_SALT:-dev-api-token-salt}
ADMIN_JWT_SECRET: ${STRAPI_ADMIN_JWT_SECRET:-dev-admin-jwt-secret}
JWT_SECRET: ${STRAPI_JWT_SECRET:-dev-jwt-secret}
TRANSFER_TOKEN_SALT: ${STRAPI_TRANSFER_TOKEN_SALT:-dev-transfer-token-salt}
# Token API read-only seedé au bootstrap (strapi/src/index.ts) pour que le
# site lise l'API locale. Doit rester égal à NEXT_PUBLIC_STRAPI_KEY dans
# apps/site/.env. Valeur non secrète : Strapi local jetable, read-only.
STRAPI_LOCAL_READONLY_TOKEN: ${STRAPI_LOCAL_READONLY_TOKEN:-local-dev-readonly-token}
DATABASE_CLIENT: postgres
DATABASE_HOST: strapi-db
DATABASE_PORT: "5432"
DATABASE_NAME: strapi
DATABASE_USERNAME: strapi
DATABASE_PASSWORD: ${STRAPI_DB_PASSWORD:-strapi}
volumes:
- ./strapi:/app
- strapi-node-modules:/app/node_modules
# —————————————————— Apps Node en mode dev (un conteneur par app) —————————
# Un service par app (profil compose = nom de l'app), sélection explicite :
# `make up apps=app,backend` → supabase,redis,app,backend. Autour des apps,
# trois services mutualisés qui portent tous les profils d'apps :
# deps → install one-shot des dépendances (volume node-modules)
# nx-daemon → daemon nx PARTAGÉ entre conteneurs : socket + état sur le
# volume nx-workspace-data (jamais le .nx/ de l'hôte, dont les
# chemins/PIDs n'ont aucun sens en conteneur). C'est le
# scénario multi-terminaux supporté nativement par nx.
# libs → pré-build one-shot des libs partagées (@tet/domain, ui…) via
# UN graphe nx : dédup + ordre topologique + cache ; les ^build
# des apps font ensuite des cache-hits au lieu de 6 builds à
# froid concurrents (cause de l'échec de la 1re approche
# per-app).
# Réseau hôte partout : les localhost des .env (Supabase :54321/:54322,
# Redis :6379, fronts :300x, backend :8080) fonctionnent tels quels, HMR
# compris. PID hôte partout : nx trace vivacité et parenté des tâches
# partagées par des PID (running-tasks) — des namespaces PID séparés les
# rendent incohérents (PID renumérotés à 1 → tâches mortes « vivantes »,
# fausses invocations récursives inter-apps). Avec pid: host, on est
# exactement dans le scénario multi-terminaux que nx supporte. Ce mode
# s'utilise depuis le checkout principal uniquement (bind mount .:/repo —
# cf. guard worktree du Makefile).
# Install des dépendances dans le volume node-modules (one-shot, relancé à
# chaque up : incrémental) : token Bryntum lu dans le .env racine, rebuild
# des natifs (canvas, binaire supabase). Hors conteneur d'app car l'install
# n'a rien d'interactif. Équivalent de `make install`.
deps:
profiles: &app-profiles [ app, site, panier, backend, tools ]
image: tet-node-dev # socle construit par `make up` (node-base)
working_dir: /repo
init: true
restart: "no"
network_mode: host
pid: host
volumes:
- .:/repo
- node-modules:/repo/node_modules
- pnpm-store:/home/node/.local/share/pnpm
command: [ "dotenvx", "run", "--env-keys-file=.env.keys", "--ignore=MISSING_ENV_FILE", "-f", ".env.local", "-f", ".env", "--", "sh", "-c", "pnpm install && pnpm rebuild canvas supabase" ]
nx-daemon:
profiles: *app-profiles
image: tet-node-dev
working_dir: /repo
command: [ "sh", ".docker/apps/nx-daemon-entrypoint.sh" ]
init: true
restart: unless-stopped
network_mode: host
pid: host
environment: &nx-env
NX_DAEMON: "true"
NX_TUI: ${NX_TUI:-false} # sortie linéaire préfixée (TUI incompatible détaché)
NX_WORKSPACE_DATA_DIRECTORY: /home/node/.nx-workspace-data
# limite dure de 95 caractères sur le chemin socket ($NX_SOCKET_DIR/d.sock)
NX_SOCKET_DIR: /home/node/.nx-workspace-data/sock
ulimits:
# dockerd limite les conteneurs à 1024 fichiers ouverts (default systemd),
# trop peu pour les bundlers sur un monorepo de cette taille.
nofile: &app-nofile { soft: 524288, hard: 524288 }
volumes: &app-volumes
- .:/repo
- node-modules:/repo/node_modules
- pnpm-store:/home/node/.local/share/pnpm
- nx-cache:/home/node/.nx-cache # artefacts de cache nx
- nx-workspace-data:/home/node/.nx-workspace-data # daemon + index cache
depends_on:
deps: { condition: service_completed_successfully }
healthcheck:
test: [ "CMD", "node", "-e", "const s=require('net').connect(process.env.NX_SOCKET_DIR+'/d.sock');s.on('connect',()=>process.exit(0));s.on('error',()=>process.exit(1))" ]
interval: 5s
timeout: 5s
retries: 12
start_period: 30s
libs:
profiles: *app-profiles
image: tet-node-dev
working_dir: /repo
init: true
restart: "no"
network_mode: host
pid: host
environment: *nx-env
ulimits:
nofile: *app-nofile
volumes: *app-volumes
command: [ "pnpm", "exec", "nx", "run-many", "-t", "build-deps", "-p", "app", "site", "panier", "backend", "tools", "--output-style=stream" ]
depends_on:
deps: { condition: service_completed_successfully }
nx-daemon: { condition: service_healthy }
# Les 6 apps : même squelette (ancre &app-service), seuls APP, le profil,
# le healthcheck (port) et les dépendances d'infra changent. Entrypoint
# générique .docker/apps/app-entrypoint.sh → dotenvx (env per-app) + nx dev.
# start_period généreux : premier boot Next/Turbopack à froid.
# Les 6 apps : même squelette (ancre &app-service), seuls APP, le profil,
# le port et les dépendances d'infra changent. Le port est interpolé depuis
# l'environnement appelant (*_PORT, défaut = port standard) : un worktree
# (make up, projet tet-wt<slot>) source son .env.local et obtient ses ports
# décalés — le process d'app ET le healthcheck lisent la même variable.
app: &app-service
profiles: [ app ]
image: tet-node-dev
working_dir: /repo
command: [ "sh", ".docker/apps/app-entrypoint.sh" ]
init: true
restart: unless-stopped # encaisse un crash d'app (ex. fuite Turbopack dev)
network_mode: host
pid: host
environment:
<<: *nx-env
APP: app
APP_PORT: ${APP_PORT:-3000}
ulimits:
nofile: *app-nofile
volumes: *app-volumes
depends_on: &app-depends
deps: { condition: service_completed_successfully }
nx-daemon: { condition: service_healthy }
libs: { condition: service_completed_successfully }
kong: { condition: service_started }
# Healthcheck TCP volontairement minimal : une requête HTTP ferait un SSR
# complet de la page toutes les 10 s, or l'instrumentation async de debug
# du runtime dev de Next/React trace CHAQUE opération asynchrone dans une
# Map globale sans éviction (stack capturée par entrée) — la nourrir en
# continu fait déborder la Map (« RangeError: Map maximum size exceeded »)
# et crash-loop le serveur. Port ouvert = même critère que l'ancien
# executor @nx/next:server (waitForPortOpen).
healthcheck:
test: [ "CMD", "node", "-e", "const s=require('net').connect(+process.env.APP_PORT,'127.0.0.1');s.on('connect',()=>process.exit(0));s.on('error',()=>process.exit(1))" ]
interval: 10s
timeout: 10s
retries: 6
start_period: 240s
site:
<<: *app-service
profiles: [ site ]
environment:
<<: *nx-env
APP: site
SITE_PORT: ${SITE_PORT:-3001}
depends_on:
<<: *app-depends
strapi: { condition: service_started }
healthcheck:
test: [ "CMD", "node", "-e", "const s=require('net').connect(+process.env.SITE_PORT,'127.0.0.1');s.on('connect',()=>process.exit(0));s.on('error',()=>process.exit(1))" ]
interval: 10s
timeout: 10s
retries: 6
start_period: 240s
panier:
<<: *app-service
profiles: [ panier ]
environment:
<<: *nx-env
APP: panier
PANIER_PORT: ${PANIER_PORT:-3002}
healthcheck:
test: [ "CMD", "node", "-e", "const s=require('net').connect(+process.env.PANIER_PORT,'127.0.0.1');s.on('connect',()=>process.exit(0));s.on('error',()=>process.exit(1))" ]
interval: 10s
timeout: 10s
retries: 6
start_period: 240s
backend:
<<: *app-service
profiles: [ backend ]
environment:
<<: *nx-env
APP: backend
BACKEND_PORT: ${BACKEND_PORT:-8080}
depends_on:
<<: *app-depends
db: { condition: service_healthy }
redis: { condition: service_healthy }
healthcheck:
# GET /version : hors préfixe api (main.ts setGlobalPrefix exclude)
test: [ "CMD", "node", "-e", "fetch('http://localhost:'+process.env.BACKEND_PORT+'/version').then(r=>process.exit(r.ok?0:1),()=>process.exit(1))" ]
interval: 10s
timeout: 10s
retries: 6
start_period: 180s
tools:
<<: *app-service
profiles: [ tools ]
environment:
<<: *nx-env
APP: tools
TOOLS_PORT: ${TOOLS_PORT:-8081}
depends_on:
<<: *app-depends
db: { condition: service_healthy }
redis: { condition: service_healthy }
healthcheck:
test: [ "CMD", "node", "-e", "fetch('http://localhost:'+process.env.TOOLS_PORT+'/version').then(r=>process.exit(r.ok?0:1),()=>process.exit(1))" ]
interval: 10s
timeout: 10s
retries: 6
start_period: 180s
volumes:
db-data:
db-config:
storage-data:
redis-data:
strapi-db-data:
strapi-node-modules:
node-modules: # Store pnpm et artefacts de cache nx : nom FIXE (sans préfixe de projet)
# pour être partagés entre la stack principale `tet` et les projets
# tet-wt<slot> des worktrees — le nom correspond aux volumes existants du
# projet tet, aucune migration. node-modules et nx-workspace-data restent,
# eux, par projet (dépendances de la branche, état daemon par workspace).
pnpm-store:
name: tet_pnpm-store
nx-cache:
name: tet_nx-cache
nx-workspace-data: # état daemon nx + index de cache des conteneurs (jamais le .nx/ hôte)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment