🛑 Le constat : un CMS pour un site sans contenu dynamique
Mon ancien portfolio tournait sous WordPress. C’est un outil puissant, mais pour un site vitrine dont le contenu change une fois par mois, c’est un moteur de rendu dynamique mobilisé pour servir des pages qui ne varient jamais entre deux visiteurs.
Quatre dettes techniques s’accumulaient :
- Une stack disproportionnée : Apache, PHP et MySQL pour produire du HTML identique à chaque requête.
- Une surface d’attaque à entretenir : le cœur de WordPress et chaque extension doivent être maintenus à jour. Une extension abandonnée devient une porte d’entrée.
- Un contenu difficile à versionner : les articles vivent dans une base de données. Impossible de les relire dans une pull request, impossible de revenir proprement en arrière.
- Un TTFB dépendant de la charge : chaque visite déclenche des requêtes SQL et une exécution PHP.
Je voulais une plateforme qui reflète la manière dont je travaille : prévisible, versionnée, avec une surface d’exposition réduite au strict nécessaire.
🛠️ La réponse : tout pré-générer
J’ai opté pour une architecture JAMstack. Le principe : produire l’intégralité du HTML au moment de la compilation, et ne servir ensuite que des fichiers statiques.
1. Le moteur : Astro
Astro a une particularité utile ici : il n’envoie aucun JavaScript par défaut. Les composants sont rendus au build ; le navigateur reçoit du HTML et du CSS. Le peu de JS présent sur ce site (bascule de thème, filtres du journal) est explicitement demandé.
Le contenu vit dans des fichiers Markdown typés avec Zod. Chaque article est un fichier dans Git : je peux le relire dans un diff, le corriger, revenir dessus.
2. L’infrastructure : build multi-étapes, exécution Nginx
L’image de production est construite en deux temps. Node compile le site ; seul le résultat est copié dans une image Nginx.
# --- ETAPE 1 : BUILD ---
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci # Reproductible, contrairement a npm install
COPY . .
RUN npm run build # Genere /app/dist
# --- ETAPE 2 : RUNTIME ---
FROM nginx:1.30-alpine
RUN apk update && apk upgrade --no-cache # Correctifs de securite Alpine a jour
COPY --from=builder --chown=nginx:nginx /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
USER nginx # Le serveur ne tourne pas en root
EXPOSE 8080
CMD ["nginx", "-g", "daemon off;"]
Ce que cette structure apporte, précisément :
- L’image finale ne contient ni Node, ni npm, ni le code source. Il n’y a plus d’interpréteur côté serveur à exploiter. Cela ne rend pas le site invulnérable — une faille dans Nginx, une injection via mon propre contenu Markdown ou une mauvaise configuration restent possibles — mais la surface d’attaque côté serveur est considérablement réduite.
- L’image passe de plusieurs centaines de mégaoctets à quelques dizaines.
- Le processus ne tourne pas en root, et n’écoute pas sur un port privilégié.
3. Podman plutôt que Docker
Le conteneur est décrit dans une unité systemd via Quadlet. Pas de démon central, un démarrage au boot géré par l’init du système, des logs dans le journal, et un redémarrage automatique en cas de crash.
Il ne publie aucun port sur l’hôte : il rejoint le réseau interne de mon reverse proxy, qui l’atteint par son nom d’hôte et assure seul la terminaison TLS.
📊 Ce que la bascule a changé
| Indicateur | Avant (WordPress) | Après (Astro + Nginx) |
|---|---|---|
| Rendu des pages | Dynamique, à chaque requête | Pré-généré au build |
| Requêtes base de données | Plusieurs par page | Aucune |
| Composants à patcher | Cœur CMS + extensions | Nginx |
| Versionnement du contenu | Base de données | Git |
| Taille de l’image | Lourde (PHP + Apache) | Alpine, quelques dizaines de Mo |
Les gains de temps de chargement sont réels et attendus — servir un fichier statique est structurellement plus rapide que de le générer. Je ne publie pas de chiffre précis ici : un score de performance dépend du réseau, du terminal et du moment de la mesure, et une capture d’écran d’un outil de benchmark ne prouve pas grand-chose. Mesurez sur votre propre connexion.
🔮 L’écosystème autour
Cette architecture ne vit pas seule :
- Intégration et déploiement continus : chaque poussée sur
maindéclenche la construction de l’image, un scan de vulnérabilités, la publication sur un registre, puis le redémarrage du service sur mon serveur. Aucun accès SSH n’est utilisé — c’est l’objet d’un article dédié. - En-têtes de sécurité et politique de sécurité du contenu posés au niveau de Nginx, vérifiés automatiquement après chaque déploiement.
⚖️ Ce que j’ai perdu au passage
Par honnêteté, cette architecture n’est pas gratuite.
Publier demande un build. Corriger une faute de frappe implique un commit, un pipeline, une image. Sur WordPress, c’était trois clics. Pour un site personnel c’est un bon compromis ; pour une rédaction qui publie dix fois par jour, ce serait absurde.
Il n’y a plus d’interface d’administration. Écrire suppose un éditeur de texte et Git. C’est une barrière réelle pour un contributeur non technique.
La complexité s’est déplacée, elle n’a pas disparu. J’ai supprimé PHP et MySQL, et j’ai ajouté un pipeline, un registre d’images, une unité systemd et un reverse proxy. Le résultat est plus solide et plus reproductible, mais il demande de comprendre chacune de ces couches.
C’était le but. Ce site est aussi un terrain d’exercice.
