Quitter Vercel pour mon homelab : retour de migration
Pourquoi et comment j'ai migré dubus.pro de Vercel vers une stack self-hosted (Coolify + Hetzner CX23 + Cloudflare Tunnel). Coût, perf, automatisation et les pièges rencontrés.

Pourquoi quitter Vercel
Vercel est excellent pour démarrer. Edge functions, preview deployments, image optimization, tout marche au premier push. Mais quand on commence à vouloir comprendre ce qui tourne où, comment scale, et combien ça coûte vraiment, on touche les limites de la magie.
Mes raisons concrètes pour migrer :
-
Pricing flou : edge functions facturées à l'invocation, image optimization à la transformation, bandwidth après le free tier. Pour un portfolio qui prend 5k vues par mois ce n'est rien, mais pour des sites clients avec du traffic réel, on arrive vite à des factures imprédictibles.
-
Pas d'accès aux logs bruts : les logs Vercel sont dans leur dashboard. On peut les téléverser vers un service tiers (Logtail, Datadog), mais c'est en sus. Moi je voulais ingérer dans ma propre stack Loki.
-
Build runtime locked : impossible de spawner des sub-processes pendant le build, pas de FFmpeg pour transcoder une vidéo en SSG, pas de Puppeteer pour pré-render. Tout doit passer par les abstractions Vercel.
-
Cas client futur : je voulais un setup qui me serve aussi pour héberger les sites de mes clients freelance. Sur Vercel, chaque client = un projet payant. En self-hosted, c'est du marginal cost.
-
Apprentissage pur : comprendre ce qui se passe entre
git pushet200 OKcôté client. Vercel cache trop pour qu'on apprenne quelque chose en l'utilisant.
Le stack cible
GitHub repo (push main)
│
│ webhook
v
[Coolify on Hetzner CX23] ───► clone + build + spawn container
│
│ Traefik internal routes
v
[App container :3000]
│
│ cloudflared egress tunnel
v
[Cloudflare Edge] ───► TLS termination + DDoS + cache
│
v
[Browser]
Stack choisie pour des raisons précises :
- Hetzner CX23 (4,79€/mois) : cheapest VPS qui tient un Next.js prod sans broncher (2 vCPU x86, 4 GB RAM, 40 GB SSD, 20 TB bandwidth)
- Coolify v4 : PaaS open source self-hosted, Heroku-like UX. Push to main = build + deploy. Healthchecks, rollbacks, env vars, le confort Vercel sans le vendor lock.
- Cloudflare Tunnel : exposition zero-trust. Le serveur Hetzner n'a aucun port ouvert vers Internet. Le tunnel sort en outbound vers CF, qui termine le TLS au edge. DDoS protection gratuite, et si je veux mettre du CF Access devant un service admin, c'est 2 clics.
- GitHub App pour les webhooks signed (HMAC SHA-256), pas du polling.
Coolify tourne sur ma VM locale (Proxmox VM 300). Le node Hetzner est managé "à distance" via SSH par cette VM. Avantage : si Coolify lui-même crash, ma prod reste up sur Hetzner, je redéploie après quand l'UI revient.
La migration en pratique
Étape 1 : Coolify managed local
VM Ubuntu 24.04 cloud-init sur Proxmox. Installation Coolify v4 par le script officiel. Total ~10 minutes. Setup admin via UI sur première visite.
Pour exposer l'UI Coolify localement (sur LAN seulement), config Traefik LXC 103 avec wildcard *.didoulab.com qui pointe vers la VM Coolify :
http:
routers:
coolify:
rule: "Host(`coolify.didoulab.com`)"
entryPoints:
- websecure
service: coolify
tls:
certResolver: cloudflare
domains:
- main: didoulab.com
sans:
- "*.didoulab.com"
services:
coolify:
loadBalancer:
servers:
- url: "http://192.168.68.252:8000"
Étape 2 : webhook GitHub → Coolify
Coolify a besoin de recevoir les webhooks GitHub publiquement. Solution : un cloudflared container sur la VM Coolify qui expose uniquement le path coolify.didoulab.com/webhooks/* :
Public Hostnames (CF Zero Trust) :
coolify.didoulab.com/webhooks/* → http://localhost:8000 (Bypass policy)
coolify.didoulab.com/app/* → http://localhost:6001 (CF Access protected)
coolify.didoulab.com/apps/* → http://localhost:6001 (CF Access protected)
Le reste de Coolify (UI, API admin, console) reste invisible depuis Internet. La Bypass policy CF Access laisse passer les webhooks GitHub sans challenge (vérifié par signature HMAC côté Coolify), tout le reste exige Email OTP + 2FA.
Étape 3 : provisioning Hetzner via Coolify
Coolify v4 a une intégration Hetzner Cloud native. Token API Hetzner, choix de la région (Falkenstein), choix du type de serveur (CX23), Coolify provisionne le VPS, installe Docker, configure le SSH key, et le serveur apparaît comme "destination" dans l'UI.
Total : ~3 minutes. Côté Hetzner, j'ai ensuite :
- Activé le firewall Cloud Hetzner (block all in, allow seulement le SSH depuis l'IP de Coolify VM)
- Le tunnel cloudflared installé par Coolify lors du premier deploy
Étape 4 : déploiement de l'app
Coolify UI → New Resource → Public Repository → https://github.com/breaching/my-portfolio → branch main, base /frontend, Dockerfile detection auto.
Variables d'env injectées via Coolify UI : NEXT_PUBLIC_FORMSPREE_ID, NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN, etc. Le build les inline dans le bundle (NEXT_PUBLIC_* Next.js convention).
Le Dockerfile est multi-stage :
FROM node:22-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
ARG NEXT_PUBLIC_FORMSPREE_ID
ENV NEXT_PUBLIC_FORMSPREE_ID=$NEXT_PUBLIC_FORMSPREE_ID
# ... autres ARG/ENV
RUN npm run build
FROM node:22-alpine AS runner
WORKDIR /app
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
ENV PORT=3000 HOSTNAME="0.0.0.0"
EXPOSE 3000
CMD ["node", "server.js"]
Output Next.js standalone (~85 MB image vs 400 MB sans), démarrage container en 1.5s.
Étape 5 : DNS bascule
Le piège classique : pendant 6 jours j'ai gardé dubus.pro sur Vercel ET déployé en parallèle sur prod-test.dubus.pro côté Hetzner pour valider. Tests Lighthouse, Formspree, navigation, builds reproductibles, healthchecks Coolify.
Le 7 mai, basculement final :
- CF Zero Trust → tunnel Hetzner → ajout public hostnames
dubus.proapex etwww.dubus.pro(CF flatten le CNAME apex automatiquement) - CF DNS → suppression du record
A dubus.pro 216.198.79.1(Vercel) - Coolify UI → app prod → champ Domains : remplacement de
prod-test.dubus.propardubus.pro,www.dubus.propuis Save + Redeploy - Verify HTTP 200 + titre correct via curl
Total downtime : 0. CF Tunnel servait déjà prod-test.dubus.pro, le passage de prod-test vers dubus.pro apex s'est fait en parallèle de Vercel jusqu'à la suppression du A record. Browser users qui visitent dubus.pro pendant la fenêtre de propagation DNS atterrissent encore sur Vercel ou déjà sur Hetzner, dans les deux cas un site fonctionnel.
Coût comparé sur 12 mois
Voilà le détail honnête :
| Poste | Vercel Free | Vercel Pro | Self-hosted (cette stack) |
|---|---|---|---|
| Hosting compute | gratuit (limites strictes) | 20$/mois | 4,79€/mois (Hetzner CX23) |
| Bandwidth | 100 GB inclus | 1 TB inclus | 20 TB inclus (Hetzner) |
| Build minutes | 6000/mois | 24000/mois | illimité |
| Image optimization | 1000/mois free | inclus | géré par Next.js + Sharp localement |
| Edge functions | 100k inv/jour | 1M inv/jour | aucune limite (Node.js) |
| Logs | 1h retention | 1 jour retention | 30 jours (Loki + ISM policy) |
| Total annuel | 0€ | 240$/an | ~57€/an + l'électricité de mon homelab Coolify |
Pour un portfolio à faible traffic, Vercel Free est gratuit et suffit. La motivation n'est pas le coût ici, c'est le contrôle. Mais quand on commence à monter à plusieurs sites clients, Vercel Pro à 20$ par projet vs Hetzner CX23 mutualisable à l'infini, ça devient un argument économique.
Performance comparée
Tests Lighthouse mobile depuis la France (CDG) sur 5 runs moyennés :
| Metric | Vercel | Self-hosted |
|---|---|---|
| TTFB | 95 ms | 110 ms |
| FCP | 1.0 s | 1.1 s |
| LCP | 1.8 s | 2.0 s |
| CLS | 0 | 0 |
| TBT | 80 ms | 90 ms |
| Score perf mobile | 99 | 97 |
Petite régression de ~10% sur le TTFB, attendue : Vercel a leur edge network avec PoPs partout, moi j'ai un seul serveur à Falkenstein. Pour un visiteur français, le serveur Hetzner allemand est très proche. Pour un visiteur USA, c'est plus loin. Mais Cloudflare devant fait du caching agressif (Cache-Everything sur les pages statiques), donc le TTFB réel pour les visites cachées est ~30 ms (CF edge proche du visiteur).
Ce qui a cassé pendant la migration
Pour les futurs migrants, voilà les vrais pièges :
Coolify v4 ne régénère pas le compose au domain change
Le bug le plus stressant. Tu changes la FQDN dans l'UI Coolify, tu cliques Save, tu cliques Redeploy. La page recharge avec la nouvelle valeur. Le redeploy "réussit". Le site renvoie 404.
Investigation : le docker-compose.yaml côté serveur cible n'est pas régénéré. Coolify utilise le compose existant tel quel, donc les Traefik labels conservent l'ancien Host rule.
Fix : clear COMPLETEMENT le champ Domains, save (Coolify enregistre "vide"), re-paste, save, redeploy. Le double-save flag le state comme dirty et force la régen.
DNS-over-HTTPS du browser bypass mon AdGuard local
J'utilise Brave avec DoH activé. Quand je tape coolify.didoulab.com en LAN, Brave query Cloudflare DNS direct (DoH) au lieu de mon AdGuard 192.168.68.246. Cloudflare retourne le CNAME du tunnel public (créé pour les webhooks), je tombe sur CF Access challenge alors que je voulais juste taper sur mon Traefik LAN.
Fix : disable DoH dans Brave, ou utiliser direct IP http://192.168.68.252:8000 pour bypass DNS entièrement.
Cert self-signed dans Coolify-proxy interne
Coolify-proxy (Traefik interne à Coolify) génère ses propres certs self-signed pour communiquer entre containers. Quand on bypass via Traefik externe (LXC 103), il faut serversTransport: insecure avec insecureSkipVerify: true :
serversTransports:
insecure:
insecureSkipVerify: true
Sans ça, Traefik externe refuse de relayer vers Coolify (TLS handshake fail).
Healthcheck Coolify bloqué par mon proxy.ts
Mon middleware Next.js blacklist les User-Agents curl|wget (anti-scanner). Le healthcheck Coolify utilise wget en interne. Résultat : healthcheck systématiquement 403, container marqué unhealthy en boucle.
Fix : utiliser curl --user-agent "Mozilla/5.0 (compatible; coolify-healthcheck)" dans le healthcheck Docker, ou whitelister l'IP du container coolify-proxy dans le middleware.
Ce que j'ai appris
-
Coolify v4 est un excellent compromis entre Heroku-like UX et control complet. Pas autant de magie que Vercel, mais quand quelque chose casse on sait où regarder.
-
Cloudflare Tunnel est la killer feature pour qui ne veut pas exposer de ports. Zero-trust networking pour les nuls, et la doc CF est très bien faite.
-
Le pricing self-hosted ne se mesure pas en €/mois mais en h/mois investies à la maintenance. Sur 2 mois de prod : ~3 heures de troubleshooting (les 4 bugs ci-dessus). Pour une équipe avec un budget DevOps zéro, c'est un coût caché à intégrer.
-
Les preview deployments me manquent. Sur Vercel chaque PR avait une URL unique pour valider en revue. Coolify v4 a ça en preview, j'attends que ça stabilise.
-
Edge functions deviennent middleware Node.js. Pas grave, c'est juste un autre runtime. Mes routes API Next.js tournent en Node sur le container, pas sur l'edge. Un peu plus de latence pour les routes API distantes, mais pour 99% des cas c'est invisible.
-
Avoir l'observabilité avant de migrer change tout. J'avais Loki + Grafana + Wazuh sur mon homelab avant la bascule. Le jour de la migration, je voyais les events HTTP en temps réel sur les deux backends, ce qui m'a rassuré sur le moment de switch DNS.
Architecture self-host complète documentée sur le repo didoulab-homelab (privé). Pour la stack sécurité observabilité spécifiquement, voir le post sur le mini-SOC homelab. Pour la vue d'ensemble du homelab Proxmox, voir le post infrastructure 16 services. Pour héberger ton propre site avec un setup similaire, demande un devis.
Besoin d'un site vitrine professionnel ?
Je crée des sites sur mesure pour les entreprises à Caen et partout en France. Devis gratuit, réponse sous 24h.