My App

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 postgres côté server
  • Ou connect ETIMEDOUT 172.X.X.X:5432
  • docker inspect montre les services sur des networks différents (ex: server sur dokploy-network, postgres sur bell-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 services

Tous 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 SIGILL

Pendant 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 :

  1. Lit les SQL files générés par drizzle-kit generate (en local)
  2. Split par --> statement-breakpoint
  3. Execute chaque statement via pg directement
  4. Trace les migrations appliquées dans __bell_migrations

Workflow :

  • Local : bun drizzle-kit generate --name=<change> → produit packages/db/src/migrations/000N_<change>.sql
  • Commit le fichier SQL
  • Au boot serveur (Dokploy) : entrypoint.sh lance node src/migrate.mjs qui 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 ps montre le container (unhealthy) malgré que le service réponde HTTP 200 quand on curl depuis l'intérieur
  • Traefik refuse de router vers ce backend → 404 côté client
  • Le log Next.js dit ✓ Ready in 0ms mais 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 1

Utiliser 127.0.0.1 (IPv4 explicite) au lieu de localhost.


5. Postgres init.sql ne se rejoue pas

Symptôme

  • init.sql créé CREATE EXTENSION vector mais le serveur dit toujours Failed query: CREATE EXTENSION IF NOT EXISTS "vector" au démarrage
  • Image pgvector/pgvector:pg17 correcte
  • pg_available_extensions montre bien vector

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 -v détruit les volumes) puis up -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 build

Dans 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 pour docker 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.

On this page