Déployer un site statique sans confier les clés R2 à GitHub
Cette vitrine se déploie toute seule : git push → quelques minutes plus tard, le site
est à jour, sans rien faire à la main. Contrainte que je me suis fixée : GitHub ne doit
jamais détenir les clés d’écriture de R2 (le stockage de prod). Le secret reste côté
homelab, point.
Le découpage : construire ≠ déployer
git push → GitHub Actions : build Astro → upload-artifact "site-dist" (stocké chez GitHub)
→ POST webhook n8n (jeton partagé) ← AUCUNE clé R2 ici
n8n (homelab, qui détient les secrets) :
→ crée un job Swarm éphémère (monte les Docker secrets) : pull artefact + sync → R2
→ supprime le job → flush du cache → notif ntfy
GitHub fait ce qu’il fait bien (builder dans un runner jetable) et stocke le résultat comme artefact. n8n vient le chercher via l’API GitHub et le pousse sur R2. Les clés R2 vivent uniquement comme Docker secrets sur le cluster — jamais sur GitHub, jamais dans le workflow, jamais en variable d’environnement n8n.
La première version, élégante… et cassée
J’ai d’abord voulu tout faire en nodes n8n natifs : télécharger le zip, le décompresser, uploader chaque fichier sur R2. Trois murs :
- Les nodes AWS S3 de n8n renvoient 403/400 sur R2 (endpoint custom) — leur signature
est taillée pour AWS. Le node S3 générique (avec
forcePathStyle) passe mieux, mais… - Le node Compression aplatit les chemins.
blog/x/index.htmlressort enindex.html. Sur un site à arborescence, tout entre en collision : il ne restait que ~7 fichiers à plat. Rédhibitoire.
Leçon intermédiaire : un orchestrateur de workflows n’est pas un outil de synchro de fichiers. Ne pas forcer.
La version qui marche : un job éphémère + Docker secrets
n8n appelle l’API Portainer pour créer un replicated-job Swarm jetable (alpine)
qui monte trois Docker secrets et lance un script :
# secrets montés dans /run/secrets, jamais dans le workflow
curl …/actions/runs/$RUN_ID/artifacts # résout l'artefact (token GitHub)
unzip site.zip -d /tmp/dist # arborescence PRÉSERVÉE
aws s3 sync /tmp/dist s3://www/ --delete # content-types + purge des orphelins
aws s3 sync préserve les chemins et devine les content-types — exactement ce que les
nodes n8n n’arrivaient pas à faire. Le job tourne, n8n poll son état jusqu’à la fin,
puis le supprime (cleanup), et enfin force-update le service du gateway.
Le flush sans coupure
Le cache HTML du gateway est réglé à 24 h — il faut donc le vider à chaque déploiement.
Or vider le cache = redémarrer le service. Pour que ça ne coupe jamais le site, le gateway
tourne en 2 répliques avec update_config: { order: start-first } : Swarm démarre une
réplique fraîche avant d’arrêter l’ancienne. Mesuré pendant un --force : 70 requêtes
sur 70 en 200, zéro coupure.
La leçon
Sépare où tu construis de qui détient les secrets de prod. Le build peut tourner n’importe où (un runner jetable) ; les clés d’écriture, elles, restent du côté que tu contrôles — ici, des Docker secrets que seul un job éphémère monte. Et garde en tête qu’un node “décompresse” peut t’aplatir une arborescence sans prévenir.
Au final : git push, et le reste — build, sync R2, flush, notif sur le téléphone —
s’enchaîne tout seul, sans jamais exposer une clé de trop.
📬 La newsletter homelab
Les nouveaux articles + retours d'expérience self-hosting, sans spam. Désinscription en un clic.