Déploiement — pièges & fixes
Retours d'expérience Dokploy + Docker + Bun pour Bell. À lire avant tout deploy.
Déploiement Bell sur Dokploy — pièges connus
Ce document liste les pièges qu'on a rencontrés lors du premier déploiement Bell sur Dokploy, et les fixes définitifs qui sont maintenant dans le repo. À relire avant tout nouveau projet Bell ou avant un changement infra, pour ne plus jamais y revenir.
1. Network Docker : toujours dokploy-network (external)
Symptôme
getaddrinfo EAI_AGAIN postgrescôté server- Ou
connect ETIMEDOUT 172.X.X.X:5432 docker inspectmontre les services sur des networks différents (ex: server surdokploy-network, postgres surbell-app-ccpozq_default)
Cause
Dokploy attache automatiquement les services qui ont un domain au
dokploy-network pour le routing Traefik. Si le compose déclare un network
bridge custom, Dokploy ne touche pas aux autres services → les containers
exposés et les containers DB se retrouvent sur des networks séparés.
Pire : un network bridge custom peut entrer en conflit de subnet avec d'autres projets Dokploy sur le VPS (~30+ containers chez nous) → DNS résout vers une IP fantôme → TCP timeout.
Fix définitif (déjà appliqué)
Dans docker-compose.prod.yml et docker-compose.preprod.yml :
networks:
dokploy-network:
external: true
services:
server:
networks:
- dokploy-network
postgres:
networks:
- dokploy-network
# ... tous les servicesTous les services Bell partagent le même dokploy-network. Pas de
subnet custom, pas de conflit, DNS Docker garantit la résolution.
2. Bun 1.3.8 segfault sur CPU x86_64 baseline
Symptôme
panic: Segmentation fault at address 0x134C8
oh no: Bun has crashed. This indicates a bug in Bun, not your code.
error: script "build" was terminated by signal SIGILLPendant next build (Next.js), ou pendant drizzle-kit push/migrate,
sur les VPS avec CPU x86_64 baseline (sans AVX2). Sur Mac M1/ARM
le build passe. C'est non-déterministe : parfois sur le TS check,
parfois sur "Collecting page data", parfois sur l'introspection DB.
Fix définitif
Build avec Node.js, runtime avec Bun. Pattern multi-stage dans les 3 Dockerfiles Next.js :
# Stage deps : Bun (parse bun.lock natif)
FROM oven/bun:1.3.8-alpine AS deps
RUN bun install --frozen-lockfile --ignore-scripts
# Stage build : Node (pas de segfault)
FROM node:20-alpine AS build
COPY --from=deps /app/node_modules ./node_modules
COPY --from=deps /app/apps/<APP>/node_modules ./apps/<APP>/node_modules
# Important : copier aussi les node_modules des packages utilisés
COPY --from=deps /app/packages/env/node_modules ./packages/env/node_modules
# ...
RUN cd apps/<APP> && node_modules/.bin/next build
# Stage runtime : Bun (image légère + perf)
FROM oven/bun:1.3.8-alpine AS runtime
COPY --from=build /app/apps/<APP>/.next/standalone ./
CMD ["bun", "apps/<APP>/server.js"]Pour le server Elysia : ajouter apk add --no-cache nodejs dans la
stage runtime, pour pouvoir lancer drizzle-kit via Node (Bun segfault
sur l'introspection schema).
Workaround complémentaire (apps/web/next.config.ts)
typescript: {
ignoreBuildErrors: true, // skip TS check au build Docker
},Le check TS doit être fait séparément en CI (bun tsc --noEmit)
ou en pre-push hook.
3. drizzle-kit push / migrate boucle infinie
Symptôme
[⣷] Pulling schema from database...
[⣯] Pulling schema from database...
[⣟] Pulling schema from database...
...Spinner sans fin (jamais d'erreur, jamais de fin). Idem pour
drizzle-kit migrate qui boucle sur "applying migrations...".
Cause
drizzle-kit fait une introspection Postgres avant de calculer le diff.
Sur certaines configurations (notamment avec le type vector pgvector),
l'introspection ne termine jamais.
Fix définitif
Script JS minimaliste packages/db/src/migrate.mjs qui :
- Lit les SQL files générés par
drizzle-kit generate(en local) - Split par
--> statement-breakpoint - Execute chaque statement via
pgdirectement - Trace les migrations appliquées dans
__bell_migrations
Workflow :
- Local :
bun drizzle-kit generate --name=<change>→ produitpackages/db/src/migrations/000N_<change>.sql - Commit le fichier SQL
- Au boot serveur (Dokploy) :
entrypoint.shlancenode src/migrate.mjsqui applique les nouveaux SQL files
⚠️ Ne jamais utiliser drizzle-kit push en prod : stateless, pas de
journal, peut bloquer + destructif. Toujours generate + migrate.
4. Healthcheck Docker : 127.0.0.1 pas localhost
Symptôme
docker psmontre le container(unhealthy)malgré que le service réponde HTTP 200 quand oncurldepuis l'intérieur- Traefik refuse de router vers ce backend → 404 côté client
- Le log Next.js dit
✓ Ready in 0msmais le wget refuse la connexion
Cause
Sur Alpine Linux, localhost est résolu en IPv6 (::1) en premier.
Bun (et Node Next.js standalone) bind par défaut sur IPv4 0.0.0.0 →
le healthcheck wget http://localhost:3001/ essaie IPv6 → refused →
healthcheck fail → container marqué unhealthy.
Fix définitif (dans tous les Dockerfile)
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
CMD wget -qO- http://127.0.0.1:${PORT:-3001}/ -O /dev/null || exit 1Utiliser 127.0.0.1 (IPv4 explicite) au lieu de localhost.
5. Postgres init.sql ne se rejoue pas
Symptôme
init.sqlcrééCREATE EXTENSION vectormais le serveur dit toujoursFailed query: CREATE EXTENSION IF NOT EXISTS "vector"au démarrage- Image
pgvector/pgvector:pg17correcte pg_available_extensionsmontre bienvector
Cause
docker-entrypoint-initdb.d/*.sql ne s'exécute qu'au tout premier
démarrage d'un volume vierge. Si le volume Postgres existe déjà
(d'un précédent deploy), init.sql ne tourne pas.
Fix
- Nouveau projet : laisser init.sql créer pgvector au premier boot
- Reset preprod :
docker compose down -v(le-vdétruit les volumes) puisup -d→ init.sql se rejoue - Production : ne jamais détruire le volume. Si on a besoin de pgvector sur une DB existante, l'ajouter manuellement via psql
Safety net dans le code
packages/db/src/setup-extensions.ts est lancé au boot serveur en mode
best-effort : si la query CREATE EXTENSION fail, on continue (probable
que l'extension soit déjà créée). Verbose error logging + fallback check
via pg_extension.
6. NEXT_PUBLIC_* vars doivent être passées au build, pas au runtime
Symptôme
Le client Next.js fait des requêtes vers la mauvaise URL (ex: localhost
ou l'ancien domain) au lieu du domain de prod.
Cause
Next.js inline les NEXT_PUBLIC_* dans le bundle client au moment du
build. Les changer en runtime ne fait rien (le JS est déjà compilé).
Fix définitif
Dans apps/web/Dockerfile :
ARG NEXT_PUBLIC_SERVER_URL
ENV NEXT_PUBLIC_SERVER_URL=$NEXT_PUBLIC_SERVER_URL
RUN cd apps/web && node_modules/.bin/next buildDans le compose :
web:
build:
args:
NEXT_PUBLIC_SERVER_URL: ${NEXT_PUBLIC_SERVER_URL}Et dans Dokploy → Environment, set NEXT_PUBLIC_SERVER_URL=https://api.<domain>
avant de Deploy (pas après).
7. Workspace node_modules Bun isolated linker
Symptôme
Au build Node : Cannot find module '@t3-oss/env-nextjs' (ou autre dep
transitive de @bell/env/@bell/api/etc.).
Cause
Bun 1.3 utilise par défaut le isolated linker : chaque workspace a
ses propres node_modules/ avec des symlinks vers le store
/app/node_modules/.bun/<pkg>@<v>/. Le node_modules/ racine ne
contient que les deps de la racine du monorepo.
Pour qu'une app résolve les deps transitive de ses packages workspace,
il faut copier les node_modules de chaque package importé.
Fix définitif (dans apps/web/Dockerfile)
FROM node:20-alpine AS build
COPY --from=deps /app/node_modules ./node_modules
COPY --from=deps /app/apps/web/node_modules ./apps/web/node_modules
COPY --from=deps /app/packages/api/node_modules ./packages/api/node_modules
COPY --from=deps /app/packages/auth/node_modules ./packages/auth/node_modules
COPY --from=deps /app/packages/db/node_modules ./packages/db/node_modules
COPY --from=deps /app/packages/env/node_modules ./packages/env/node_modules
COPY --from=deps /app/packages/ui/node_modules ./packages/ui/node_modules
COPY . .
RUN cd apps/web && node_modules/.bin/next buildÀ adapter pour chaque app selon ses imports @bell/*.
8. container_name + Docker Compose v2
Comportement à connaître
Quand on définit container_name: bell-preprod-server :
- Le container est nommé
bell-preprod-server(utile pourdocker logs) - Le service name (
server) reste résolvable en DNS, MAIS - Le container ne peut pas scaler (replicas > 1)
⚠️ Si tu déclares aussi networks: default: aliases: [postgres] sur un
service, ça interfère avec Dokploy qui retire le default network compose
→ casse le routing. Ne pas ajouter d'aliases manuels sur les services
Dokploy.
9. Backend qui ne tourne pas → bypass auth dashboard
Pour la démo
apps/web/src/app/(staff)/dashboard/page.tsx skip l'auth Better Auth
quand DEPLOYMENT_ENV=preprod. Sert une MOCK_SESSION pour pouvoir
naviguer le dashboard sans backend.
À garder pour preprod (utile démo), à retirer en prod une fois la
stack stable. Le compose passe DEPLOYMENT_ENV=production côté prod
donc le bypass ne s'active jamais en prod.
Checklist de validation post-deploy
# 1. Tous les containers healthy
docker ps | grep bell-preprod
# 2. Tous sur le même network
docker inspect bell-preprod-server bell-preprod-postgres \
--format '{{.Name}}: {{range $k := .NetworkSettings.Networks}}{{$k}} {{end}}'
# 3. API répond
curl -s https://api.<domain>/health
# → {"ok":true,"timestamp":"..."}
# 4. Web répond
curl -I https://app.<domain>/
# → HTTP/2 200, Server: next.js (ou via Traefik)
# 5. Logs server propres (no retry loop)
docker logs --tail 30 bell-preprod-server
# Doit voir : "🔔 Bell server running on http://localhost:3000"Si l'un des 5 fail, arrêter et diagnostiquer avant de patcher — la plupart des bugs viennent de patches successifs qui cachent la vraie cause.