En bref — ce site ne tourne pas chez un hébergeur clé en main : il est self-hosted sur mon propre VPS. Astro en rendu serveur, un CMS Directus inaccessible depuis l'extérieur, le tout conteneurisé avec Docker, exposé par Caddy, et déployé automatiquement à chaque git push via Forgejo Actions. Ce n'est pas le choix le plus simple — c'est le choix qui me rend maître de bout en bout, et qui entretient précisément la compétence que je veux garder affutée.
J'aurais pu mettre ce site en ligne en cinq minutes sur une plateforme clé en main, sans serveur à gérer. Et ce n'est pas faute d'avoir essayé les alternatives : avant d'en arriver au self-hosting, j'ai publié des sites statiques sur GitLab Pages et Cloudflare Pages, déployé sur Vercel, monté des serveurs sur DigitalOcean. Chacune a ses mérites, et je les recommande volontiers selon le contexte. Mais pour mes projets, j'ai fini par choisir l'inverse : tout héberger moi-même. Pas par dogmatisme, mais pour trois raisons que j'assume — le contrôle total de l'infrastructure, le coût, et surtout l'apprentissage. Opérer son infra de A à Z, ce n'est pas une corvée à déléguer : c'est exactement la compétence cloud / CI-CD que je veux démontrer plutôt que déclarer. Ce site est mon terrain de démonstration autant que ma vitrine.
Voici comment il est construit, du conteneur au déploiement — et ce que ce choix coûte vraiment.
La stack, en une phrase
Astro en rendu serveur (SSR) consomme un CMS Directus headless ; les deux tournent dans des conteneurs Docker sur un VPS, derrière un reverse proxy Caddy qui gère le TLS, et chaque push sur la branche principale déclenche un déploiement automatique orchestré par Forgejo Actions. Chaque brique a été choisie pour une raison précise — déroulons-les.
Astro en SSR, pas en statique
Le réflexe, avec Astro, serait de générer un site statique. J'ai délibérément opté pour le rendu serveur (output: 'server', adaptateur Node en mode standalone). La raison est simple : le contenu vit dans Directus. En SSR, chaque page est rendue à la requête en interrogeant le CMS — je publie un article dans Directus et il apparaît immédiatement, sans rebuild ni redéploiement. Le compromis assumé, c'est un petit serveur Node à faire tourner en permanence là où un site statique se contenterait de fichiers ; en échange, le contenu est toujours frais et le CMS pleinement découplé du code.
Docker : une image, deux étapes
L'application est empaquetée dans une image Docker construite en multi-stage : une étape de build qui compile le site, puis une étape d'exécution allégée qui n'embarque que le strict nécessaire.
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:22-alpine AS runner
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev --ignore-scripts
COPY --from=builder /app/dist ./dist
ENV HOST=0.0.0.0 PORT=4321
CMD ["node", "./dist/server/entry.mjs"]L'image finale part d'une base Alpine, sans les dépendances de développement : plus légère, plus rapide à déployer, et avec une surface d'attaque réduite. C'est la même discipline que pour le code — n'embarquer en production que ce qui sert vraiment.
Le conteneur ne tourne pas « à nu » : la configuration Compose lui pose des garde-fous de production.
services:
app:
container_name: florianbouchet-app
restart: unless-stopped
mem_limit: 256m
cpus: 0.5
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:4321/"]
interval: 30s
retries: 3
logging:
options: { max-size: "10m", max-file: "3" }Limites de mémoire et de CPU pour qu'un projet ne puisse pas affamer ses voisins sur le VPS, redémarrage automatique en cas de crash, healthcheck pour que l'orchestrateur sache si l'app répond vraiment, et rotation des logs pour ne pas saturer le disque. Self-hoster proprement, ce n'est pas « lancer un conteneur » : c'est lui donner les mêmes égards qu'en production managée.
Le réseau : Directus n'est jamais exposé
Point de sécurité que je tiens à souligner : le CMS n'est accessible que depuis l'intérieur. Directus vit sur un réseau Docker interne, et l'application l'atteint par son nom de service privé (http://infra-directus:8055), jamais par une URL publique. Le site dialogue avec lui côté serveur, avec un token en lecture seule. Seule l'application est exposée au monde ; la base de contenu reste dans l'ombre. C'est la défense en profondeur appliquée à l'infrastructure : moins on expose de surface, moins on a à défendre.
Caddy : le reverse proxy qui fait le TLS tout seul
Devant le conteneur, Caddy joue le rôle de point d'entrée public. Sa configuration tient en une poignée de lignes, et il gère le certificat HTTPS automatiquement — obtention et renouvellement compris, sans que j'aie à y penser.
florianbouchet.fr {
reverse_proxy florianbouchet-app:4321
}Tout le trafic public passe par là, est chiffré par Caddy, puis transmis au conteneur sur son port interne. Le HTTPS « qui marche tout seul » est exactement le genre de friction en moins qui rend le self-hosting viable au quotidien.
Le déploiement : un git push, et c'est en ligne
C'est la pièce qui transforme le self-hosting d'une corvée manuelle en flux fluide. Un pipeline Forgejo Actions se déclenche à chaque push sur la branche principale : il se connecte au VPS en SSH, récupère la dernière version du code et reconstruit le conteneur.
ssh deploy@vps.nocleus.com bash -s <<'REMOTE'
set -euo pipefail
cd /opt/projects/florianbouchet
git fetch origin
git reset --hard origin/main
docker compose up -d --build --wait --wait-timeout 300
REMOTEDeux détails comptent ici. Le --wait fait que le déploiement ne se déclare terminé que lorsque le conteneur est réellement healthy — si l'app ne démarre pas, le pipeline échoue au lieu de laisser une version cassée en ligne. Et le build s'exécute attaché au réseau interne, pour pouvoir joindre Directus pendant la compilation si une page en a besoin. Concrètement : je commite, je pousse, et trois à cinq minutes plus tard la nouvelle version est en ligne, ou rien n'a bougé. Pas d'état intermédiaire douteux.
Un détail que j'aime : planifier sans cron
Le SSR offre un bénéfice inattendu que j'ai exploité récemment. Comme chaque page est rendue à la requête en interrogeant Directus, il m'a suffi d'un filtre — ne servir que les articles dont la date de publication est passée — pour obtenir une planification automatique. Un article peut être marqué « publié » avec une date future : il reste invisible (liste, sitemap, URL directe) jusqu'à son jour, puis apparaît tout seul — sans tâche planifiée, sans cron, sans rien à maintenir. L'architecture rend le service, là où un site statique aurait exigé un rebuild programmé. D'ailleurs, l'article que vous lisez a très probablement été mis en ligne de cette façon.
Ce site n'est qu'un locataire
Un point qui change toute l'équation économique : ce site n'est pas seul sur son serveur. Il partage un unique VPS avec une dizaine d'autres projets — d'autres applications web, un CRM, de l'analytics respectueuse du RGPD, mon propre serveur Git avec son runner d'intégration continue, du monitoring, du suivi d'erreurs. Tous derrière le même Caddy, tous adossés à des services mutualisés : une instance PostgreSQL, un Valkey (le fork libre de Redis) et un moteur de recherche, partagés entre les projets plutôt que dupliqués pour chacun.
Mutualiser ne veut pas dire mélanger. Chaque projet a son propre utilisateur PostgreSQL cantonné à sa base, son préfixe de clés isolé sur le cache, son conteneur aux ressources plafonnées. C'est la même obsession d'isolation que pour le multi-tenant applicatif, transposée à l'échelle du serveur : un projet ne doit jamais pouvoir lire les données d'un autre, ni l'asphyxier en consommant toute la machine. Et le jour où l'un d'eux décolle, le faire migrer vers son propre serveur dédié reste trivial — un dump, un restore.
Cette mutualisation s'accompagne du minimum vital d'un ops sérieux : un pare-feu qui ne laisse passer que l'essentiel, des garde-barrières contre les intrusions, des mises à jour de sécurité automatiques, et surtout des sauvegardes chiffrées envoyées chaque nuit hors du serveur — que je prends soin de tester par des restaurations à blanc. Une sauvegarde qu'on n'a jamais restaurée n'est qu'une promesse ; tant qu'on ne l'a pas éprouvée, on ne sait pas si elle tiendra le jour où tout brûle. C'est tout cela qui se cache derrière « le prix d'un café par mois et par projet » : non pas un hébergement au rabais, mais une infrastructure de production, mutualisée intelligemment.
Ce que le self-hosting coûte — et ce qu'il rapporte
Je ne vais pas prétendre que c'est gratuit. Self-hoster, c'est devenir son propre ops : la supervision, les sauvegardes, les mises à jour de sécurité du serveur, la disponibilité — tout repose sur moi. Si le VPS tombe à 3 h du matin, il n'y a pas d'astreinte d'un hébergeur pour le relever. Pour beaucoup de projets, surtout en équipe ou à fort trafic, une plateforme managée est le choix rationnel, et je le recommanderais sans état d'âme.
Mais pour mes projets, la balance penche clairement de l'autre côté. Le contrôle complet — je sais exactement où vivent mes données et comment tout s'articule. Le coût — on vient de le voir, un seul serveur porte tous mes projets pour une poignée d'euros. Et la maîtrise — chaque incident résolu, chaque pipeline affiné, chaque conteneur durci est une compétence qui se sédimente. C'est cette maîtrise de bout en bout, du conteneur au reverse proxy, que je tiens à garder vivante.
Un hébergeur clé en main vous épargne l'infrastructure ; le self-hosting vous la fait comprendre. Et comprendre son infrastructure, c'est pouvoir la réparer, l'optimiser et la sécuriser — au lieu d'espérer que quelqu'un d'autre le fasse à votre place.