← tous les articles

Déployer un site statique sans confier les clés R2 à GitHub

16 juin 2026 · 3 min de lecture

ci-cdn8ngithub-actionscloudflare-r2docker-swarm

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 :

  1. 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…
  2. Le node Compression aplatit les chemins. blog/x/index.html ressort en index.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.