Coolify OVH
& Migration Supabase
sans catastrophe.
Tout ce qu'il faut activer sur le nouveau Coolify, et tout ce qui peut planter si tu oublies de le faire pendant la migration Supabase. Format QQOQCP, liens entre sections.
DNS vs hébergement Fondement
Confusion classique : avoir le domaine "chez OVH" ne veut pas dire que les apps tournent "chez OVH". Tant qu'on n'a pas compris cette distinction, on bascule le wildcard trop tôt et on casse la prod. Note pédagogique complète à étudier hors-ligne : notes/2026-05-18-dns-vs-hebergement-strategie-migration.md.
Les deux étapes quand un utilisateur tape https://saas.zoomali.io
# 1. Résolution DNS (le "carnet d'adresses")
Navigateur → "à quelle IP correspond saas.zoomali.io ?"
DNS OVH → "194.164.72.46" # Hostinger
# 2. Connexion à l'app (l'"hébergement")
Navigateur → ouvre HTTPS sur 194.164.72.46
Hostinger → un container Docker répond pour saas.zoomali.io
Le DNS dit OÙ aller. Il ne fait pas tourner les apps. Tu peux très bien avoir le DNS chez le fournisseur A et l'hébergement chez le fournisseur B. C'est exactement la situation actuelle de zoomali.io.
Distinction DNS vs Hébergement
| DNS | Hébergement | |
|---|---|---|
| Rôle | Annuaire nom → IP |
Serveurs qui font tourner les containers Docker |
| Chez zoomali.io | OVH (dns109.ovh.net) ✅ |
Hostinger (194.164.72.46) pour ~28 apps + OVH (51.38.39.111) pour Coolify et ce qui est migré |
| Géré via | Console DNS OVH | Coolify (UI) sur chaque VPS |
État actuel de la zone DNS
*.zoomali.io A 194.164.72.46 # wildcard → Hostinger (~28 apps)
coolify.zoomali.io A 51.38.39.111 # explicite → OVH
doccoolify.zoomali.io A 51.38.39.111 # explicite → OVH (test du 2026-05-18)
OVH résout chaque sous-domaine en cherchant d'abord une entrée explicite. S'il n'en trouve pas, il applique le wildcard *. Donc saas.zoomali.io n'a pas d'entrée explicite → wildcard → Hostinger.
Pourquoi on ne bascule pas *.zoomali.io sur OVH d'un coup
| Raison | Conséquence si on basculait maintenant |
|---|---|
| Continuité de service | Les ~28 apps Hostinger deviennent instantanément inaccessibles (OVH ne sait rien d'elles). |
| Rate limit Let's Encrypt | Limite à 50 certs/semaine sur zoomali.io. Caddy voudrait générer 30+ certs en cascade → blocage. |
| Rollback impossible | En cas de problème, retour arrière sur chaque app au lieu d'un simple "retire l'entrée A". |
Analogie déménagement : tu ne fais pas suivre tout ton courrier d'un coup. Tu poses une étiquette de réacheminement par expéditeur (= entrée A explicite chez OVH) seulement pour ce que tu as déjà déplacé. Le reste continue d'aller à l'ancienne adresse (= wildcard Hostinger) tant que tu n'as pas fini les cartons.
Workflow pour basculer UNE app de Hostinger vers OVH
# Ordre strict, à respecter pour chaque app
1. [OVH DNS] Créer entrée A : <sous-domaine>.zoomali.io → 51.38.39.111
2. [Attendre] Propagation DNS ~1-5 min (dig +short <sous-domaine>.zoomali.io)
3. [Coolify] Déployer l'app avec ce domaine custom (Settings → Domains)
4. [Caddy] Génère cert Let's Encrypt automatiquement (challenge HTTP-01)
5. [Test] Ouvrir https://<sous-domaine>.zoomali.io en navigation privée
6. [Hostinger] Arrêter l'app correspondante (propre, pas obligatoire)
L'ordre 1 → 3 est non-négociable. Si tu déploies dans Coolify avant que le DNS pointe sur OVH, le challenge HTTP-01 de Let's Encrypt va frapper sur Hostinger qui ne sait pas répondre → cert refusé → app inaccessible en HTTPS.
Le piège des sous-domaines auto-générés par Coolify
Quand tu ajoutes une app dans Coolify, par défaut il lui colle un sous-domaine aléatoire type c709jf1181lx59qev3xepvom.zoomali.io. Ce sous-domaine n'a pas d'entrée A explicite chez OVH → tombe sur le wildcard → Hostinger → Let's Encrypt échoue → l'app est inaccessible.
Règle absolue : dans Coolify, toujours remplacer le domaine auto par un domaine custom (et créer son entrée A chez OVH avant) avant le premier déploiement de l'app.
Réponse aux questions fréquentes
| Question | Réponse |
|---|---|
| "J'ai le domaine chez OVH, c'est suffisant pour migrer ?" | Non. Le domaine chez OVH = juste le DNS. Les apps doivent encore être physiquement déplacées vers le VPS OVH. |
| "Je dois rentrer les 28 sous-domaines chez OVH dès maintenant ?" | Non. Tu crées l'entrée A juste avant de migrer chaque app, une par une. |
| "Quand est-ce que je bascule le wildcard sur OVH ?" | À la toute fin (Sprint 4 du plan migration), quand plus aucune app ne tourne sur Hostinger. C'est le cutover final. |
| "Et si une app migrée déconne ?" | Tu supprimes son entrée A chez OVH → le wildcard reprend la main → l'app retombe sur Hostinger en ~60 secondes (rollback gratuit). |
Commandes de diagnostic
# Vérifier où pointe un sous-domaine
dig +short <sous-domaine>.zoomali.io
→ 51.38.39.111 # OVH (basculé)
→ 194.164.72.46 # Hostinger (pas encore basculé ou propagation en cours)
# Suivre la génération du certificat Let's Encrypt
docker logs -f coolify-proxy 2>&1 | grep -iE "<sous-domaine>|acme|certificate"
# Tester l'app sans passer par le DNS (forcer la résolution)
curl -k --resolve <sous-domaine>.zoomali.io:443:51.38.39.111 https://<sous-domaine>.zoomali.io
Source Git — GitHub App Étape zéro
Sans ça, Coolify ne peut pas puller tes repos. À configurer avant tout déploiement d'application — avant même les Shared Variables.
coolify.<ton-domaine> → Sources → Add New → GitHub App → suivre le flow OAuth. GitHub te redirige vers Coolify automatiquement à la fin.Ce que ça débloque : l'auto-deploy sur push
Une fois la GitHub App connectée, dans chaque app Coolify tu peux activer Auto Deploy. Le flux devient :
# Flux automatique après configuration
git push origin main
→ GitHub reçoit le push
→ GitHub envoie un webhook à Coolify (via le webhook_secret sécurisé)
→ Coolify pull le nouveau code
→ Coolify rebuild le container
→ Coolify redéploie
→ Notification Telegram "✅ bi.zoomali.io déployé"
Zéro intervention manuelle. Du push au déploiement en production en 2-3 minutes.
Paramètre à activer dans chaque app
| Paramètre | Où | Valeur recommandée |
|---|---|---|
| Auto Deploy | App → General → Auto Deploy | Activé sur la branche main |
| Watch branches | App → General → Branch | main uniquement en prod |
| Preview Deployments | App → General → Preview Deployments | À décider app par app — crée un sous-domaine par PR GitHub |
webhook_secret par GitHub App et le configure côté GitHub. Ne pas modifier manuellement ce secret — il sert à vérifier que les webhooks viennent bien de GitHub et pas d'une source externe.
Shared Variables Priorité haute
Configurer avant le premier déploiement d'application. Tout le reste en dépend.
coolify.<ton-domaine> → Team Settings → Shared VariablesVariables à créer au niveau Team
| Variable | Description | Sensibilité |
|---|---|---|
| SUPABASE_URL | URL publique de l'instance Supabase OVH | Normale |
| SUPABASE_ANON_KEY | Clé publique Supabase (côté client) | Normale |
| SUPABASE_SERVICE_ROLE_KEY | Clé service Supabase (accès total) | Haute — ne jamais exposer côté client |
| JWT_SECRET | Secret de signature des tokens — voir point critique S-01 | Critique — identique à Hostinger |
| BREVO_SMTP_HOST | smtp-relay.brevo.com | Normale |
| BREVO_SMTP_PORT | 587 | Normale |
| BREVO_API_KEY | Clé API Brevo pour les envois transactionnels | Moyenne |
| OPENAI_API_KEY | Clé API OpenAI partagée entre apps | Haute — facturation directe |
| TELEGRAM_BOT_TOKEN | Si partagé entre apps (pas le bot de monitoring) | Moyenne |
Notifications Telegram Priorité haute
Recevoir un message à chaque déploiement réussi ou raté, directement dans ta conversation Telegram.
coolify.<ton-domaine> → Team Settings → Notifications → Add Notification → Telegram@zoomali_bot. Chat ID : ton ALLOWED_CHAT_ID. Événements : Deployment Success + Deployment Failed + Container Stopped.@zoomali_bot) peut recevoir les notifications Coolify dans le même chat. Une seule conversation pour tout surveiller.
Réseau privé Docker Priorité haute
Tes apps et Supabase sont sur le même VPS. Fais-les parler en interne, pas par Internet.
coolify. Il suffit d'utiliser le nom du service comme hostname.Exemple concret
# Avant (passe par Internet)
SUPABASE_DB_URL=postgresql://postgres:password@db.zoomali.io:5432/postgres
# Après (réseau Docker interne — plus rapide, plus sûr)
SUPABASE_DB_URL=postgresql://postgres:password@supabase-db:5432/postgres
Health Checks + Restart auto Priorité moyenne
Si un container plante, Coolify le redémarre seul et t'envoie une notification Telegram.
/health dans le code de chaque microservice.GET /health → retourne 200 OK. Interval : 30s. Timeout : 5s. Retries : 3 avant redémarrage.| Type d'app | Endpoint | Interval | Timeout |
|---|---|---|---|
| Next.js / API REST | GET /api/health | 30s | 5s |
| n8n | GET /healthz | 60s | 10s |
| Supabase API | GET /rest/v1/ | 60s | 10s |
Backups S3 — Cloudflare R2 Critique
Si le VPS meurt, tu perds au maximum 24h de données et tu reconstruis en quelques heures. Sans ça, tu perds tout.
multibrasservices-backups.Étape 2 : Coolify → Team Settings → S3 Storages → Add.
https://<account-id>.r2.cloudflarestorage.com. Region : auto. Fréquence : quotidien. Rétention : 30 jours.Volumes à sauvegarder
| Volume | Taille estimée (après nettoyage) | Fréquence | Rétention |
|---|---|---|---|
| PostgreSQL Supabase (dump) | ~150 Mo compressé | Quotidien | 30 jours |
| Volume n8n (SQLite + workflows) | ~200 Mo compressé | Quotidien | 30 jours |
| Config Coolify | ~10 Mo | Hebdomadaire | 90 jours |
# Config Coolify → S3 Storage
Endpoint : https://<account-id>.r2.cloudflarestorage.com
Bucket : multibrasservices-backups
Region : auto
Access Key: → Cloudflare Dashboard → R2 → Manage API tokens
Secret Key: → idem
Rollback en un clic Natif
Coolify garde l'historique des déploiements. Retour arrière immédiat sans toucher à Git.
Restriction d'accès Selon les apps
Mettre une app en ligne visible sur Internet mais accessible uniquement à certains utilisateurs. Deux méthodes actives selon le contexte, une méthode avancée en réserve.
Méthode A — Supabase Auth Méthode principale
Pour toutes les apps que tu développes toi-même. Tu as déjà l'infra — autant l'utiliser.
supabase.auth.signInWithPassword(). Si la session est valide → contenu affiché. Sinon → redirect vers le login.bi.zoomali.io → accès équipe
saas.zoomali.io → accès clients
Tout outil Next.js/React que tu codes
/login + middleware Next.js qui vérifie supabase.auth.getSession(). Si pas de session → redirect /login. Le navigateur mémorise la session.// middleware.ts — Next.js
export async function middleware(request) {
const { data: { session } } = await supabase.auth.getSession()
if (!session) {
return NextResponse.redirect(new URL('/login', request.url))
}
}
Méthode B — Basic Auth via Caddy Services tiers
Pour les services que tu déploies mais dont tu ne contrôles pas le code. Pas de code, mais nécessite de hasher le mot de passe au terminal.
Settings. Deux chemins selon ta version de Coolify : toggle natif dans Configuration → General → HTTP Basic Authentication (ajouté en beta.434, avril 2025) ou, pour les déploiements Docker Compose, labels Caddy dans Configuration → Advanced → Labels. Dans les deux cas, le mot de passe doit être hashé en bcrypt — pas de texte clair.
App → Configuration → General → HTTP Basic Authentication → activer + username + mot de passe hashé.
Option 2 (Docker Compose / toutes versions)
App → Configuration → Advanced → Labels → ajouter :
caddy_0.basicauth.0_MON_USER=$$2a$$14$$hash...(les
$ du hash bcrypt doivent être doublés en $$ dans compose)
Supabase Studio → interface BDD
Tout outil open source déployé sans auth intégrée
Coolify → sécurité supplémentaire si souhaité
Générer le hash bcrypt (obligatoire)
Caddy refuse les mots de passe en clair. Commande à lancer une fois sur n'importe quelle machine où Caddy est installé (ou sur le VPS OVH) :
# Génère un hash bcrypt du mot de passe
caddy hash-password -p "ton_mot_de_passe"
# Sortie : $2a$14$xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# → coller ce hash dans l'UI ou dans le label (en doublant les $ dans compose)
$ est un caractère spécial. Le hash $2a$14$abc... doit être écrit $$2a$$14$$abc... dans les labels — sinon Docker tronque la valeur et Caddy refuse l'auth.
Méthode C — Tailscale Option avancée — plus tard
À ne pas activer maintenant pour ne pas complexifier la stack. Documenté ici pour référence future.
Tailscale est un VPN mesh qui fait disparaître une app d'Internet complètement. Seuls les appareils connectés à ton réseau Tailscale peuvent y accéder. Gratuit jusqu'à 100 appareils.
Récap — Quelle méthode pour quel service dans ta stack
| Service | Méthode | Raison |
|---|---|---|
| bi.zoomali.io, saas.zoomali.io | Supabase Auth | Apps que tu codes, gestion des rôles utilisateurs |
| Ce guide HTML et outils perso | Supabase Auth | Ton compte Supabase existant, visible mais privé |
| n8n | Basic Auth Coolify | Doit rester public pour les webhooks entrants |
| Supabase Studio | Basic Auth Coolify | Service tiers, API doit rester publique |
| Coolify | Login natif Coolify | Sécurité renforcée dans les versions récentes — suffisant |
| Tailscale | Option future | Ne pas activer maintenant — complexité inutile à ce stade |
JWT_SECRET Critique absolu
Si tu rates ça, tous tes utilisateurs connectés sont déconnectés de force au moment du cutover. Leurs sessions deviennent invalides.
.env du déploiement Supabase, variable JWT_SECRET. Claude Hostinger peut la récupérer (sans la commiter).Règle absolue : Le JWT_SECRET OVH doit être identique octet pour octet au JWT_SECRET Hostinger.
Ne pas copier-coller via un fichier committé. Transmettre via une variable d'environnement directe ou un canal sécurisé hors Git.
Une fois défini dans les Shared Variables Coolify, ne plus jamais le modifier en prod sans plan de déconnexion maîtrisée.
GOTRUE_URI_ALLOW_LIST Priorité haute
Actuellement, seul saas.zoomali.io est autorisé comme redirect URI. Toutes tes autres apps échouent silencieusement à la redirection post-login.
saas.zoomali.io.GOTRUE_URI_ALLOW_LIST. L'utilisateur reste bloqué après login sans message d'erreur clair.gotrue. OVH : dans les Shared Variables Coolify dès le déploiement Supabase.https://*.zoomali.io/** + https://*.zoomali.fr/** couvre tous les sous-domaines actuels et futurs.# Valeur actuelle (trop restrictive)
GOTRUE_URI_ALLOW_LIST=https://saas.zoomali.io/**
# Valeur cible (couvre tout)
GOTRUE_URI_ALLOW_LIST=https://saas.zoomali.io/**,https://bi.zoomali.io/**,https://*.zoomali.io/**,https://*.zoomali.fr/**
saas.zoomali.io immédiatement après.
SERVICE_PASSWORD_VAULTENC — pgsodium Cas non applicable ici
Le pendant du JWT_SECRET pour le chiffrement des données en base via Supabase Vault. Vérification faite côté Hostinger : le Vault n'est pas utilisé, donc cette clé n'a pas besoin d'être copiée. Section conservée pour référence future.
SELECT count(*) FROM vault.secrets; côté Hostinger renvoie 0. Aucune donnée n'est chiffrée par pgsodium. Décision actée : nouvelle clé SERVICE_PASSWORD_VAULTENC générée côté OVH (Coolify le fait automatiquement), sans copie depuis Hostinger.
Pourquoi tu n'es pas concerné
Le Supabase Vault est une table vault.secrets où l'on stocke des secrets chiffrés au repos dans Postgres, via l'extension pgsodium (libsodium). Cas d'usage typique : une Edge Function qui lit une clé Stripe depuis la base au runtime plutôt que de la passer en variable d'env.
Toi, tu stockes tes secrets dans les Shared Variables Coolify — pas dans la base. Tes 0 entrées dans vault.secrets confirment qu'aucune donnée chiffrée ne dépend de cette clé. Une nouvelle clé côté OVH ne casse rien.
Si un jour tu actives le Vault — checklist
pgsodium (table vault.secrets) côté Postgres.SERVICE_PASSWORD_VAULTENC dans le .env Supabase. À transmettre hors-repo, comme le JWT_SECRET.SELECT count(*) FROM vault.secrets; sur la base source.Préservation des UUID auth.users Priorité haute
Tes 24 utilisateurs ont des UUID qui servent de clé étrangère dans toutes tes tables métier. Changer ces UUID = casser toutes les relations de données.
bi_users.auth_id, bi_invoices.user_id).auth.users → auth.identities → auth.refresh_tokens. Puis les tables métier du schema public.pg_dump des tables auth.* depuis Hostinger. Import via psql sur OVH avec --data-only. Les hashes bcrypt sont préservés → mots de passe inchangés.auth.* prend 2 minutes. Le risque est faible en volume, mais la procédure doit être respectée dans l'ordre.
Migration n8n SQLite Priorité haute
n8n ne stocke pas ses données dans PostgreSQL. C'est une base SQLite dans un volume Docker. La migration se fait différemment.
pg_dump ne fonctionnera pas. La migration se fait par copie du volume Docker complet.scp ou rsync entre VPS.Re-enregistrement webhooks Telegram Priorité moyenne
Tes 4 bots Telegram configurés dans n8n sont en mode webhook. Après migration, ils pointent toujours vers l'ancienne URL n8n Hostinger.
setWebhook avec la nouvelle URL. Ou via les credentials n8n si le workflow le permet.curl -X POST "https://api.telegram.org/bot<TOKEN>/setWebhook" -d "url=https://n8n.zoomali.io/webhook/<id>" — à répéter pour chacun des 4 bots.OAuth Google / Microsoft Priorité moyenne
Si tes apps permettent la connexion via Google ou Microsoft, les redirect URIs OAuth doivent inclure les nouveaux domaines OVH.
redirect_uri_mismatch.Microsoft : portal.azure.com → App registrations → Redirect URIs.
Dans quel ordre faire quoi
Un ordre qui garantit qu'à chaque étape, tu peux reculer sans dommages.
- Configurer Coolify OVH — Shared Variables + Notifications Avant tout déploiement. Récupérer JWT_SECRET depuis Hostinger hors-repo (pgsodium non requis — vault vide vérifié). → C-01 / C-02 / S-01 / S-03
- Configurer les Backups S3 — Cloudflare R2 Avant de migrer la moindre donnée en prod. → C-05
- Corriger GOTRUE_URI_ALLOW_LIST sur Hostinger Décision déjà validée. Claude Hostinger peut agir maintenant. → S-02
- Nettoyer binaryData n8n sur Hostinger Décision déjà validée. Réduit le volume de transfert de 3 Go à ~500 Mo. → S-05
- Déployer Supabase sur OVH via Coolify Avec les bonnes valeurs : JWT_SECRET identique, GOTRUE_URI_ALLOW_LIST élargi, réseau privé configuré. → C-03 / S-02
- Migrer auth.users → puis tables métier Dans l'ordre. UUID préservés. Hashes bcrypt préservés. → S-04
- Valider Supabase OVH sur zoomali.fr (staging) Tester login, RLS, lecture/écriture avant de toucher à zoomali.io.
- Migrer n8n — arrêt Hostinger + copie volume + démarrage OVH Puis re-enregistrer les 4 webhooks Telegram. → S-05 / S-06
- Migrer les apps une par une — cutover DNS progressif Pour chaque app : ajouter redirect URI OAuth si besoin → déployer OVH → tester → baisser TTL → couper DNS. → S-07 / C-06
- Décommissionner Hostinger Seulement quand 0 trafic depuis plusieurs jours et tous les backups S3 confirmés.