J'ai remplacé Supabase par Convex et l'ai auto-hébergé gratuitement. Voici le guide complet.

9 min read

12 conteneurs Docker pour Supabase, un seul pour Convex — et zéro limite de bande passante quand vous l'auto-hébergez. Guide de migration complet inclus.


Je finissais ma session de code, prêt à aller siroter une piña colada sur la terrasse. Puis : une alerte Convex. Limite d'usage presque atteinte.

Mon envie d'alcool s'est évaporée instantanément.

Première réaction : « Impossible. On est le 3. Le quota vient de se remettre à zéro il y a deux jours. » Deuxième : identifier quel projet était le coupable. Troisième : examiner les requêtes. Et là, c'était évident — db.query("metrics").collect() partout. Scan complet de table à chaque mutation. 12 500 lignes. Quatre requêtes réactives qui se relançaient en boucle à chaque modification d'un seul document.

C'est moi qui avais écrit ce code. Enfin, Claude Code. Même combat.

Ça faisait un moment que j'y pensais — auto-héberger Convex. J'avais déjà vérifié que c'était possible. Je ne savais juste pas comment. Ce soir-là, j'avais ma réponse au pourquoi c'était nécessaire.


TL;DR : L'auto-hébergement Supabase nécessite 12 conteneurs Docker et 8 Go de RAM minimum (pas viable sur un VPS partagé). Convex est plus propre à auto-héberger : un conteneur, même API, bande passante illimitée. Le seul vrai piège, c'est le routage multi-ports du reverse proxy. Voici le guide complet, incluant le prompt de migration qui évite l'erreur de deux lignes que j'ai faite.



Ingénieur technique comparant des solutions d'infrastructure cloud avec une visualisation en bande dessinée
Quand votre infrastructure passe du chaos au zen en un seul déploiement

Pourquoi j'ai quitté Supabase (version courte)

Cette histoire commence en fait plus tôt. Avant Convex, avant l'alerte de bande passante — il y avait un problème Supabase.

Supabase cloud bouffait une marge que je ne faisais pas. Alors j'ai fait ce que tout dev sensé fait : j'ai regardé l'auto-hébergement. Gratuit, non ? Il suffit de lancer les conteneurs.

Un jour plus tard. Douze conteneurs configurés, Traefik qui routait enfin vers les bons services après trois faux départs, Kong qui se disputait avec chaque variable d'environnement que je lui balançais, le réseau inter-services qui faisait sa propre tambouille pour des raisons qui m'ont pris deux heures à déboguer. Mon VPS transpirait. Mais ça tournait.

Puis j'ai lancé docker ps.

supabase-db. supabase-studio. supabase-kong. supabase-auth. supabase-storage. supabase-realtime. supabase-meta. supabase-imgproxy. supabase-vector.

Douze conteneurs. Pour un backend. Kong seul peut monter à 2,5 Go sous charge. Minimum 8 Go de RAM recommandés en production. J'ai un VPS qui fait tourner cinq autres services. C'est non.

(Vous pouvez techniquement désactiver les services inutilisés pour réduire l'usage des ressources. Mais là vous maintenez une stack partielle et customisée sur un serveur que vous partagez avec tout le reste. Le contrôle total, qu'ils disent.)

Il me fallait quelque chose de plus léger. C'est comme ça que j'ai trouvé Convex, et découvert un ensemble d'avantages auxquels je ne m'attendais pas. Si vous voulez le tableau complet, j'ai couvert pourquoi Convex est devenu ma stack par défaut pour construire des SaaS IA avec Claude Code. Version courte : tout en TypeScript, requêtes réactives par défaut, auto-migrations au déploiement. Et Claude Code génère du code qui compile du premier coup parce qu'il travaille dans un seul langage sur toute la stack au lieu de jongler entre migrations SQL, Deno Edge Functions, et frontend TypeScript dans des contextes séparés.

Une inquiétude que j'avais : le vendor lock-in. Convex est passé open source en février 2025. Cette inquiétude s'est évaporée.

Donc j'ai migré. Tout a fonctionné. Et puis, trois mois plus tard, j'ai failli atteindre la limite de 1 Go/jour de bande passante.


Le réveil brutal : 821 Mo sur 1 Go

Le problème, c'étaient les requêtes que j'avais écrites. (Enfin, vous savez qui... 😉)

db.query("metrics").collect() c'est un scan complet de table. Toutes les lignes, à chaque fois. Sur une table de 12 500 lignes. Et comme les requêtes réactives Convex se re-exécutent dès que n'importe quel document de la table interrogée change, elles ne tournaient pas qu'une fois. Elles tournaient en permanence.

Quatre d'entre elles : getTopContents scannait toutes les métriques pour grouper par contenu, crossPlatformMetrics agrégeait sur toutes les plateformes, les requêtes de vélocité scannaient toute la table pour les sparklines, mediumOverview lisait toutes les distributions pour les comptes de followers.

Le 3 mars, cette combinaison a consommé 821 Mo de mon quota quotidien de 1 Go. En soirée 💀

Avant que vous disiez « ajoute juste des index » — je sais. J'y suis arrivé. Mais l'histoire ne s'arrête pas là.


Les corrections rapides (faites ça d'abord)

Le move 80/20 avant même de penser à l'auto-hébergement : corriger les requêtes.

Ajoutez des index à votre schéma :

metrics: defineTable({
  content_id: v.id("contents"),
  platform_name: v.string(),
  period: v.string(),
})
  .index("by_period", ["period"])
  .index("by_platform_period", ["platform_name", "period"])
  .index("by_content_id", ["content_id"]),

Filtrez par période au lieu de tout scanner :

// Avant : lit chaque ligne de la table
const allMetrics = await ctx.db.query("metrics").collect();

// Après : lit seulement le mois courant
const currentPeriod = new Date().toISOString().slice(0, 7); // "2026-03"
const metrics = await ctx.db
  .query("metrics")
  .withIndex("by_period", (q) => q.eq("period", currentPeriod))
  .collect();

Limitez les requêtes sparkline aux périodes récentes. Si vous dessinez un graphique sur 6 mois, construisez la liste des chaînes de période dont vous avez besoin et interrogez chacune avec un index. Ne lisez pas trois ans de données pour six points de données.

Ces trois changements ont réduit ma bande passante d'environ 80%. Claude Code avait écrit les requêtes originales — rapide à générer, zéro index, ça marche bien jusqu'à ce que ça ne marche plus. Les corrections ont pris environ une heure.

Mais le plafond de 1 Go est toujours là. Chaque nouvelle fonctionnalité l'entame. Chaque article importé, chaque rafraîchissement de dashboard temps réel. Le quota est structurel. L'optimisation fait gagner du temps.

L'auto-hébergement supprime le plafond.


La décision : auto-héberger ou rester sur le cloud ?

Si vous avez déjà un serveur qui fait tourner Docker, auto-héberger Convex coûte exactement 0€ de plus.

Un conteneur. Même CLI, mêmes librairies client, même API. Vous pointez votre app vers une URL différente et le reste de votre code ne change pas. Le backend est open source. Vous en êtes propriétaire.

Ça a du sens si vous avez déjà Docker et un reverse proxy configuré, si vous approchez des limites de bande passante, ou si vous voulez une souveraineté complète des données sur un projet où les SLA managés n'importent pas.

Ça n'a pas de sens si vous devriez louer un serveur juste pour ça — le tier cloud est moins cher que vous ne le pensez. Pareil si vous utilisez Convex Auth ou le stockage de fichiers CDN, qui n'existent pas encore dans la version auto-hébergée. Et si déboguer Traefik à 4h du matin vous semble être une mauvaise soirée, restez sur le cloud. Je dis ça sans jugement.

Ma situation : j'ai déjà un serveur dédié qui fait tourner Traefik, n8n, et plusieurs autres services. Ajouter un conteneur ne coûte rien. Le calcul était évident.


Pourquoi deux sous-domaines

Convex auto-hébergé expose deux ports depuis un seul conteneur. Port 3210 pour le backend — requêtes, mutations, WebSocket. Port 3211 pour les HTTP Actions — vos endpoints REST.

Deux ports, deux sous-domaines :

convex.votredomaine.com        → port 3210 (backend + WebSocket)
convex-http.votredomaine.com   → port 3211 (HTTP actions)

Cette architecture est aussi la cause racine du seul vrai problème que j'ai rencontré.


Le piège Traefik

Contexte rapide si vous n'avez jamais utilisé Traefik : c'est un reverse proxy qui se place devant vos conteneurs Docker et route le trafic entrant vers le bon service. Le truc sympa c'est qu'il se configure tout seul en lisant les labels sur vos conteneurs — pas de fichier de config à maintenir. La plupart des tutos d'auto-hébergement l'utilisent. Ça marche très bien.

Jusqu'à ce qu'un conteneur expose deux ports.

04:31:36 CET. Premier docker compose up -d. Logs Traefik, immédiatement :

ERR Router convex-actions cannot be linked automatically
  with multiple Services: ["convex-actions" "convex-backend"]
ERR Router convex-backend cannot be linked automatically
  with multiple Services: ["convex-actions" "convex-backend"]

Le conteneur Convex n'avait même pas fini de démarrer. Traefik refusait déjà le routage.

Mes labels initiaux ressemblaient à ça :

labels:
  - traefik.enable=true
  - traefik.http.routers.convex-backend.rule=Host(`convex.votredomaine.com`)
  - traefik.http.routers.convex-backend.entrypoints=websecure
  - traefik.http.routers.convex-backend.tls.certresolver=letsencrypt
  - traefik.http.services.convex-backend.loadbalancer.server.port=3210
  - traefik.http.routers.convex-actions.rule=Host(`convex-http.votredomaine.com`)
  - traefik.http.routers.convex-actions.entrypoints=websecure
  - traefik.http.routers.convex-actions.tls.certresolver=letsencrypt
  - traefik.http.services.convex-actions.loadbalancer.server.port=3211

Ça a l'air correct. Ça ne l'est pas.

Quand un seul conteneur Docker déclare plusieurs services Traefik, l'auto-linking plante. Traefik voit deux services et ne sait pas quel router correspond à quel service. Dans tous les tutos que vous avez jamais lus, il y a un conteneur et un port — donc le mapping est implicite et ça marche. Avec deux ports, vous devez le rendre explicite. Deux lignes :

labels:
  - traefik.enable=true
  - traefik.http.routers.convex-backend.rule=Host(`convex.votredomaine.com`)
  - traefik.http.routers.convex-backend.entrypoints=websecure
  - traefik.http.routers.convex-backend.tls.certresolver=letsencrypt
  - traefik.http.routers.convex-backend.service=convex-backend       # ← ça
  - traefik.http.services.convex-backend.loadbalancer.server.port=3210
  - traefik.http.routers.convex-actions.rule=Host(`convex-http.votredomaine.com`)
  - traefik.http.routers.convex-actions.entrypoints=websecure
  - traefik.http.routers.convex-actions.tls.certresolver=letsencrypt
  - traefik.http.routers.convex-actions.service=convex-actions       # ← ça
  - traefik.http.services.convex-actions.loadbalancer.server.port=3211

Deux minutes entre l'erreur et la correction. La règle : dès que vous routez plusieurs ports d'un conteneur à travers le provider Docker de Traefik, ajoutez des labels service= explicites. À chaque fois, sans exception.

Une note sur Cloudflare : Je fais tourner Cloudflare devant mes domaines avec le proxy désactivé (DNS-only, nuage gris) pour ces sous-domaines. Let's Encrypt doit atteindre votre serveur directement pour émettre les certificats. Si vous laissez le nuage orange actif, vous aurez un conflit SSL que vous passerez 20 minutes à attribuer à la mauvaise couche — DNS, puis Traefik, puis Convex. Désactivez le proxy pour la migration.

Ce serveur fait déjà tourner plusieurs services auto-hébergés, incluant l'infrastructure derrière la reconstruction de mon setup d'agent IA from scratch après qu'une dépréciation d'API m'ait forcé la main. La philosophie est la même : posséder la stack, supprimer les coûts récurrents, garder l'expérience développeur intacte.


Le prompt qui évite tout ça

Si vous utilisez Claude Code pour générer le docker-compose, donnez-lui ce contexte d'entrée :

Auto-héberger Convex (ghcr.io/get-convex/convex-backend) derrière Traefik.
Un seul conteneur, DEUX ports : 3210 (backend + WebSocket) et 3211 (HTTP actions).
Chaque port a besoin de son propre sous-domaine avec SSL.
Provider Docker Traefik avec Let's Encrypt déjà configuré.

Quatre lignes. C'est le contexte qui déclenche le pattern service= explicite. Sans ça, tous les LLM prennent par défaut l'hypothèse du tuto standard : un conteneur, un port, mapping implicite. Ça marche très bien jusqu'à ce que vous ayez deux ports.

Il hallucinera quand même d'autres trucs. Claude Code le fait toujours. Mais au moins pas celui-ci spécifiquement.


La migration (la partie que j'ai à peine faite)

J'ai demandé à Claude Code : « On peut migrer mon projet Convex cloud vers l'instance auto-hébergée ? »

J'avais à peine ouvert l'onglet docs pour comprendre par où commencer. Claude Code les avait déjà lues. « Vérification des prérequis. » Puis : connexion au serveur via SSH, lecture du docker-compose existant, pull de l'image, génération de la clé admin. Je regardais. C'était mon boulot — regarder un terminal défiler plus vite que je ne pouvais le lire pendant que mon stagiaire IA démolissait une tâche que j'avais mentalement budgétée à deux heures.

Quinze minutes plus tard il m'a dit de mettre à jour deux enregistrements DNS. Je l'ai fait. Terminé.

(Pour info : j'ai supervisé. Très attentivement. Depuis ma chaise.)

La séquence, pour les curieux : avec le conteneur qui tourne et les certificats SSL émis — Let's Encrypt prend environ 15 secondes, le premier curl retourne le code de sortie 60, pas de panique — la migration c'est cinq étapes.

Générer la clé admin :

docker compose exec backend ./generate_admin_key.sh

Commence par convex-self-hosted|. Stockez-la, ne la commitez jamais.

Créer .env.self-hosted à la racine de votre projet :

CONVEX_SELF_HOSTED_URL=https://convex.votredomaine.com
CONVEX_SELF_HOSTED_ADMIN_KEY=convex-self-hosted|votre-cle-ici

Chaque commande npx convex a besoin de --env-file .env.self-hosted à partir de maintenant. Sans ça, le CLI cible silencieusement le cloud. Facile à oublier, dangereux à rater.

Exporter depuis le cloud d'abord :

npx convex export --path convex-backup.zip

C'est votre rollback. Faites-le avant tout le reste.

Déployer le schéma, puis importer — dans cet ordre :

npx convex deploy --env-file .env.self-hosted --yes
npx convex import --env-file .env.self-hosted convex-backup.zip

Schéma d'abord crée les tables et index. L'import a besoin qu'ils existent. Inversez l'ordre et l'import plante.

Définir les variables d'environnement :

npx convex env list                                                    # lister les vars cloud d'abord
npx convex env set --env-file .env.self-hosted VOTRE_CLE "valeur"

Deux trucs qui piègent les gens : les variables NEXT_PUBLIC_* dans Next.js sont figées au moment du build. Les changer dans Vercel ne fait rien jusqu'à ce que vous redéployiez. Le dashboard a l'air mis à jour. L'app parle encore au cloud. Redéployez.

Et si vous avez des consommateurs d'HTTP Actions — un site statique, des webhooks externes — ils ont besoin que l'URL des HTTP Actions soit mise à jour séparément. Backend qui marche ne veut pas dire que les actions marchent. Port différent, sous-domaine différent, testez les deux.


Sauvegardes : votre problème maintenant

#!/bin/bash
BACKUP_DIR="/path/to/backups"
DATE=$(date +%Y%m%d-%H%M)
VOLUME_NAME="votre-projet_convex_data"  # vérifiez : docker volume ls

mkdir -p "$BACKUP_DIR"
docker run --rm \
  -v "${VOLUME_NAME}:/data:ro" \
  -v "${BACKUP_DIR}:/backup" \
  alpine tar czf "/backup/convex-${DATE}.tar.gz" -C / data

find "$BACKUP_DIR" -name "convex-*.tar.gz" -mtime +30 -delete

Ajoutez au cron, quotidien à 3h du matin. Le nom du volume inclut votre répertoire de projet comme préfixe — vérifiez avec docker volume ls si vous n'êtes pas sûr.

Gardez votre instance Convex Cloud vivante pendant 48h après la migration. Vos données cloud n'ont jamais été modifiées. Le rollback c'est un changement de variable d'env et un redéploiement. Trois minutes.


Résultats

Latence : auto-hébergé sur un serveur OVH en France tourne à environ 461ms aller-retour versus 413ms sur Convex Cloud US. Négligeable. Les utilisateurs EU sur un serveur EU pourraient voir une meilleure latence que le cloud US par défaut.

Anxiété de bande passante : disparue. J'ai livré deux nouvelles fonctionnalités de dashboard la semaine d'après sans penser une seule fois au quota.

La comparaison Supabase tient aussi au niveau infrastructure : un conteneur Convex au repos utilise moins de 200 Mo de RAM. Supabase auto-hébergé commence à 12 conteneurs et 8 Go recommandés. Pour un side project sur un VPS partagé, cette différence c'est tout.

Quelque part vers minuit, j'ai fermé le terminal. La piña colada était toujours sur la table, allez savoir comment.

Salud.


Checklist

□ Créer 2 enregistrements DNS A pointant vers l'IP de votre serveur
□ Désactiver le proxy Cloudflare (nuage gris) pour les deux sous-domaines
□ Écrire docker-compose.yml avec labels service= explicites sur les deux routers
□ docker compose up -d
□ Attendre ~15s pour les certificats SSL (code de sortie 60 au premier curl = normal)
□ Générer la clé admin
□ Créer .env.self-hosted
□ Exporter depuis le cloud : npx convex export --path backup.zip
□ Déployer le schéma : npx convex deploy --env-file .env.self-hosted --yes
□ Importer les données : npx convex import --env-file .env.self-hosted backup.zip
□ Définir les vars d'env : npx convex env set --env-file .env.self-hosted CLE valeur
□ Tester les requêtes backend
□ Tester les HTTP actions séparément (sous-domaine différent)
□ Mettre à jour NEXT_PUBLIC_CONVEX_URL + redéployer le frontend
□ Mettre à jour tous les consommateurs d'HTTP Actions
□ Configurer le cron de sauvegarde
□ Garder le cloud vivant 48h comme rollback
□ Vérifier que la sauvegarde tourne le lendemain

Sources


J'écris sur ce qui arrive vraiment quand vous construisez des projets en production avec des outils IA — pas le best-of. Si ça vous semble utile, abonnez-vous.


Cette image de couverture est 100% IA. Je suis le mec qui débogue les labels Traefik à 4h du matin — designer je ne suis pas.


Découvrez comment j'ai réduit ma stack de 12 à 1 conteneur Docker, sans compromettre mes performances de développement d'agents IA.

Rejoindre la newsletter