Reverse proxy Nginx (image stable-alpine) qui sert l'app upstream ou une page de maintenance 503 selon l'IP du client. - Modes whitelist/blacklist commutables via MAINTENANCE_MODE - Liste IPv4 via MAINTENANCE_IP_LIST (séparée par virgules, validée au boot) - Logique en directives Nginx natives (geo + map), zéro Lua/njs - Page statique sobre HTML+CSS inline, zéro JS, zéro réseau sortant - Log dédié /var/log/nginx/maintenance.log pour les requêtes bloquées - Validation des env vars dans /docker-entrypoint.d/10-init.sh - Stack Docker + docker-compose pour dev local et tests - 6 cas de tests d'intégration (whitelist/blacklist x autorisée/bloquée + log + nginx -t) - Lint shellcheck propre sur tous les scripts shell
19 KiB
PROMPT.md — Reverse Proxy Nginx avec Page de Maintenance Conditionnelle par IP
Document de spécification autonome du projet. Toute personne (humaine ou agent IA) peut s'y référer pour comprendre, implémenter et maintenir le projet sans contexte externe.
1. Vision du projet
Construire un reverse proxy Nginx qui, selon l'adresse IP du client, redirige les requêtes vers une application locale (proxy_pass) ou affiche une page de maintenance statique avec un code HTTP 503 Service Unavailable. Le projet est packagé en Docker, agnostique de l'application backend, et la logique (mode whitelist / blacklist + liste d'IP + upstream) est entièrement pilotée par variables d'environnement au démarrage du conteneur.
Cas d'usage typique : exposer une application en pré-production à une équipe interne uniquement, ou bloquer ponctuellement quelques IP indésirables pendant qu'on travaille sur un correctif.
2. Stack technique
| Composant | Choix | Confiance | Justification |
|---|---|---|---|
| Reverse proxy | Nginx (image nginx:stable-alpine) |
🟢 élevée | Standard de l'industrie, déterministe, configuration purement déclarative. |
| Logique conditionnelle | Directives natives geo + map + if (pas de Lua, pas de njs) |
🟢 élevée | Couvre 100 % du besoin sans dépendance tierce. Évite les modules dynamiques. |
| Templating de config | envsubst (depuis gettext) appelé par l'entrypoint Nginx officiel via /etc/nginx/templates/ |
🟢 élevée | Mécanisme officiel de l'image Nginx, zéro magie, pas de runtime supplémentaire. |
| Page de maintenance | HTML + CSS inline, system font stack, icône SVG inline, zéro JavaScript | 🟢 élevée | Sert même si tout est cassé en aval. Pas de dépendance réseau (pas de Google Fonts, pas de CDN). |
| Packaging | Docker + docker-compose (pour le dev local et la démo) | 🟢 élevée | Reproductible, mêmes commandes en local et en prod. |
| Entrypoint | Script bash (validation des env vars + nginx -t) |
🟢 élevée | Échec rapide au boot si conf invalide, plutôt qu'un 502 silencieux. |
| Tests | Scripts bash + curl lancés contre un conteneur de test (avec un upstream simulé en nginx:alpine ou python -m http.server) |
🟢 élevée | Boîte noire = vérité terrain. Pas de framework de test à apprendre. |
| Lint shell | shellcheck sur tous les .sh |
🟢 élevée | Outil standard, signaux nets. |
| Validation conf | nginx -t systématique avant tout commit (pre-commit local) |
🟢 élevée | Coût zéro, attrape 90 % des bugs. |
Aucune techno 🟡 ou 🔵 dans ce projet. Toutes les briques sont dans la zone de fiabilité maximale d'un agent comme moi : Nginx natif, bash POSIX, Docker, curl. Aucune librairie de niche.
3. Architecture et structure des dossiers
appli_test/
├── PROMPT.md # Ce fichier — source de vérité du projet
├── README.md # Quickstart : comment lancer, comment configurer
├── CHANGELOG.md # Conventional Changelog
├── .editorconfig # Cohérence indentation entre éditeurs
├── .gitignore
├── .dockerignore
├── Dockerfile # Image basée sur nginx:stable-alpine
├── docker-compose.yml # Stack de dev : proxy + upstream factice
├── docker-compose.test.yml # Stack de tests d'intégration
│
├── nginx/
│ ├── templates/
│ │ └── default.conf.template # Template traité par envsubst au boot
│ └── snippets/
│ └── maintenance-log.conf # Format de log dédié maintenance.log
│
├── public/
│ └── maintenance.html # Page statique servie en 503 (HTML+CSS inline)
│
├── scripts/
│ ├── entrypoint.sh # Validation env + génération conf + nginx -t + exec nginx
│ ├── build-ip-list.sh # Transforme MAINTENANCE_IP_LIST="a,b,c" en directives geo
│ └── lint.sh # Lance shellcheck + nginx -t
│
└── tests/
├── run.sh # Orchestrateur : up compose.test, attend, lance les cas, tear down
├── cases/
│ ├── whitelist_authorized_ip.sh # IP listée → 200 (proxy OK)
│ ├── whitelist_unauthorized_ip.sh # IP non listée → 503 + page maintenance
│ ├── blacklist_blocked_ip.sh # IP listée → 503
│ ├── blacklist_normal_ip.sh # IP non listée → 200
│ ├── log_dedicated.sh # Une requête bloquée apparaît dans maintenance.log
│ └── nginx_syntax.sh # `nginx -t` retourne 0
└── fixtures/
└── upstream/ # Petit serveur factice pour simuler l'app cible
Pourquoi cette structure ?
nginx/templates/est le chemin que l'entrypoint officiel de l'image Nginx scanne automatiquement pourenvsubst. On suit la convention upstream.public/séparé du templating : la page statique n'a pas à passer par envsubst.scripts/regroupe tout le shell pour qu'il soit lintable et testable d'un coup.tests/cases/: un fichier.shpar scénario, lisible, débogable individuellement.
4. Spécification fonctionnelle détaillée
4.1 Variables d'environnement (contrat d'API)
| Variable | Obligatoire | Exemple | Description |
|---|---|---|---|
MAINTENANCE_MODE |
✅ | whitelist ou blacklist |
Sémantique de la liste. whitelist = seules les IP listées passent, le reste voit la maintenance. blacklist = inverse. |
MAINTENANCE_IP_LIST |
✅ | 81.92.47.8,10.0.0.42,192.168.1.5 |
Liste d'IP IPv4 individuelles séparées par virgules. Espaces tolérés autour des virgules. Pas de CIDR, pas d'IPv6 (out of scope MVP). |
UPSTREAM_HOST |
✅ | 127.0.0.1:3000 |
Hôte:port de l'application backend. Le proxy fait proxy_pass http://$UPSTREAM_HOST. |
LISTEN_PORT |
❌ (défaut 8080) |
80 |
Port d'écoute du conteneur. |
SERVER_NAME |
❌ (défaut _) |
app.example.com |
Valeur de server_name dans le bloc Nginx. |
4.2 Comportement
- Topologie : Nginx est directement exposé au client. L'IP source =
$remote_addr. Aucune confiance accordée àX-Forwarded-Forreçu de l'extérieur. - Code HTTP : la page de maintenance est servie avec
503 Service Unavailable(et un en-têteRetry-After: 3600indicatif). - Scope : la maintenance s'applique à tout le domaine — chaque path, chaque méthode, y compris assets et appels API. Aucune exclusion (pas de healthcheck particulier, pas d'exception assets).
- Logging :
access.logstandard pour toutes les requêtes (format combined).maintenance.logdédié : ne contient que les requêtes ayant abouti sur la page de maintenance. Format :$time_iso8601 $remote_addr "$request" "$http_user_agent".
- Pas de bypass token : le seul critère discriminant est l'IP. Pas de cookie magique, pas de header de contournement.
- Validation au démarrage : si
MAINTENANCE_MODEn'est niwhitelistniblacklist, ou siMAINTENANCE_IP_LISTest vide, ou siUPSTREAM_HOSTest absent, l'entrypoint échoue immédiatement avec un message clair sur stderr et exit code 1. Pas de fallback silencieux.
4.3 Logique Nginx (cœur du projet)
Le mécanisme repose sur deux directives :
# Construit dynamiquement par envsubst depuis MAINTENANCE_IP_LIST
geo $ip_in_list {
default 0;
81.92.47.8 1;
10.0.0.42 1;
# ... une ligne par IP, générée au boot
}
# MAINTENANCE_MODE = whitelist → on bloque si pas dans la liste (ip_in_list=0)
# MAINTENANCE_MODE = blacklist → on bloque si dans la liste (ip_in_list=1)
map "$maintenance_mode:$ip_in_list" $is_in_maintenance {
default 0;
"whitelist:0" 1;
"blacklist:1" 1;
}
L'astuce : $maintenance_mode est lui-même injecté par envsubst, et MAINTENANCE_IP_LIST est transformée en lignes geo par scripts/build-ip-list.sh avant que Nginx ne démarre.
5. Conventions de codage
5.1 Nginx
- Indentation : 4 espaces, jamais de tabulations.
- Une directive par ligne, accolades sur leur propre ligne pour les blocs longs.
- Commentaires uniquement quand le pourquoi n'est pas évident depuis la lecture. Jamais de commentaire qui paraphrase la directive.
- Pas de
ifdans les blocslocationsauf cas spécifiquement supportés (cf. If Is Evil). On préfèremap+try_files+return. - Toute conf doit passer
nginx -t; aucune exception.
5.2 Bash
- Shebang
#!/usr/bin/env bashpartout. set -euo pipefailen première ligne après le shebang.- Toutes les variables entre guillemets :
"$VAR","${ARR[@]}". - Vérification d'arguments en début de script avec message d'usage.
- Tous les scripts
.shdoivent passershellchecksans warning.
5.3 HTML/CSS (page de maintenance)
- Un seul fichier
public/maintenance.htmlautonome. - CSS inline dans
<style>, pas de fichier externe. - System font stack :
system-ui, -apple-system, "Segoe UI", Roboto, sans-serif. - Icône SVG inline pour l'illustration (pas de
<img>qui pourrait 404). - Layout centré (flex), max-width raisonnable (~480px), respiration visuelle.
- Doit être lisible sans JS et sans réseau.
5.4 Conventional Commits
Format obligatoire :
<type>(<scope>): <description courte impérative>
[corps optionnel expliquant le pourquoi]
Types autorisés : feat, fix, docs, refactor, test, chore, ci.
❌ Ne JAMAIS ajouter de ligne Co-Authored-By: Claude ... dans les messages de commit. Aucune attribution d'agent IA dans l'historique Git.
5.5 Git workflow — trunk-based
- Tous les commits vont directement sur
main. - Pas de branches
feature/*, pas de pull requests internes. - Conséquence : chaque commit doit être atomique, vert, et déployable.
nginx -t+tests/run.shdoivent passer avant chaque commit. - Tags Git pour les versions notables :
v0.1.0,v0.2.0, etc.
6. Outils et plugins Claude Code
Cette section décrit comment un agent Claude Code doit travailler sur ce projet.
6.1 Superpowers — skills à utiliser systématiquement
| Skill | Quand l'invoquer |
|---|---|
superpowers:brainstorming |
Avant toute modification non triviale du comportement (nouvelle env var, nouveau code HTTP, nouvelle exclusion de path). On clarifie l'intention avant le code. |
superpowers:test-driven-development |
Pour toute feature comportementale : écrire d'abord un cas de test dans tests/cases/ qui échoue, puis modifier la conf Nginx pour qu'il passe. |
superpowers:systematic-debugging |
Dès qu'un test échoue ou qu'une requête ne se comporte pas comme attendu. Pas de "patch jusqu'à ce que ça marche". |
superpowers:verification-before-completion |
Avant de déclarer une tâche terminée : nginx -t ✅, tests/run.sh ✅, shellcheck scripts/*.sh ✅. Évidence avant assertion. |
superpowers:writing-plans |
Pour toute évolution dépassant 2-3 fichiers (ex : ajout du support CIDR ultérieurement). |
6.2 Superpowers — skills non utilisés pour ce projet
superpowers:using-git-worktrees: non applicable. Le projet est en trunk-based, mono-développeur, scope minimal. Si le projet grossit et que des features parallèles deviennent nécessaires, ce skill pourra être réintroduit — mais pas avant.
6.3 Context7
- À utiliser quand on a un doute sur une directive Nginx récente, une option de l'image Docker
nginx:stable-alpine, ou la sémantique deenvsubst. - Inutile pour le HTML/CSS/bash basique — c'est dans la zone 🟢.
- Préférer Context7 à la recherche web pour toute documentation officielle.
6.4 Frontend Design
- Applicable uniquement à
public/maintenance.html. Si on veut faire évoluer le visuel (au-delà du sobre actuel), on peut invoquer ce skill. - Pour le MVP, le design est intentionnellement simple : pas besoin du skill au démarrage.
6.5 Commit Commands
- Tout commit doit suivre les Conventional Commits (cf. § 5.4).
- Interdiction absolue de
Co-Authored-Bydans les commits. - Commits atomiques : un commit = un changement cohérent et testé.
7. Phases de développement
MVP en une seule phase (choix utilisateur). Le projet est suffisamment petit pour qu'un découpage en phases produirait plus de cérémonie que de valeur.
Phase 1 — MVP complet
- Squelette projet :
Dockerfile,docker-compose.yml,.gitignore,.dockerignore,.editorconfig,README.mdquickstart,CHANGELOG.md. - Page de maintenance
public/maintenance.html: HTML+CSS inline, system fonts, icône SVG, layout centré, message sobre en français. - Template Nginx
nginx/templates/default.conf.template:- Bloc
serverécoutant sur${LISTEN_PORT}. - Directives
geo(générées dynamiquement) etmappour la décision. location /qui :- sert
/maintenance.htmlavec status 503 +Retry-Aftersi$is_in_maintenance = 1, - fait
proxy_pass http://${UPSTREAM_HOST}sinon.
- sert
- Log dédié
maintenance.logconditionné par$is_in_maintenance.
- Bloc
- Script
scripts/build-ip-list.sh: transformeMAINTENANCE_IP_LISTen lignes Nginx dans un fichier inclus par le template. - Script
scripts/entrypoint.sh: valide les env vars (MAINTENANCE_MODE∈ {whitelist, blacklist},MAINTENANCE_IP_LISTnon vide,UPSTREAM_HOSTdéfini), appellebuild-ip-list.sh, lancenginx -t, puisexec nginx -g "daemon off;". - Tests d'intégration
tests/run.sh+ 6 cas danstests/cases/couvrant whitelist (autorisée + bloquée), blacklist (bloquée + normale), log dédié, syntaxe Nginx. docker-compose.test.yml: stack qui combine le proxy avec un upstream factice (imagepython:3.12-alpinelançant unhttp.serverou un autrenginx:alpinequi sert un texte fixe).scripts/lint.sh:shellcheck scripts/*.sh tests/run.sh tests/cases/*.sh+nginx -t(via conteneur jetable).- README.md : usage (
docker run -e MAINTENANCE_MODE=... -e MAINTENANCE_IP_LIST=... -e UPSTREAM_HOST=... -p 8080:8080 image), exemples docker-compose, comment lancer les tests, comment ajouter une IP. - Validation finale : tous les tests verts, image Docker construit, README à jour.
8. Design & UX (page de maintenance)
Direction esthétique : sobre avec un peu de style. Pas minimaliste austère, pas branding. Niveau "page d'erreur soignée d'une bonne app indé".
| Élément | Choix |
|---|---|
| Palette | Fond #fafaf9 (presque blanc cassé), texte #1c1917 (presque noir), accent #0891b2 (cyan calme) pour l'icône. |
| Typographie | System font stack, titre 28-32px, corps 16px, line-height 1.6. |
| Icône | SVG inline ~64px représentant un outil (clé, engrenage, ou cône de chantier). Couleur d'accent. |
| Layout | Flex centré horizontalement et verticalement, min-height: 100vh, max-width contenu ~480px, padding généreux. |
| Ton | Neutre, en français, rassurant : « Site en maintenance. Nous revenons très vite. » + sous-titre court. Aucune date estimée (pas demandé). |
| Responsive | Mobile-first par défaut (largeurs en pourcentage / max-width). |
| Accessibilité | Contraste AA minimum, <title> explicite, lang="fr" sur <html>. |
9. Règles impératives
Liste de règles non négociables. Tout PR/commit qui les viole doit être refusé.
- Aucune dépendance Lua, njs, ou module Nginx tiers. On reste sur les directives natives.
- Aucun JavaScript dans la page de maintenance. Elle doit fonctionner navigateur texte / curl / w3m.
- Aucune requête réseau sortante depuis la page de maintenance (pas de Google Fonts, pas de CDN, pas d'analytics).
nginx -tdoit passer avant chaque commit.- Tous les scripts shell doivent passer
shellchecksans warning. - Tous les tests d'intégration doivent passer avant chaque commit sur
main. - Aucun secret en dur dans l'image, les templates ou les scripts. Tout passe par variables d'environnement.
- Aucune écriture sur le disque par Nginx en dehors des logs (
access.log,error.log,maintenance.log). - La page de maintenance retourne strictement
503, jamais200ni307. - La maintenance s'applique à tout le domaine. Aucune exclusion de path tant que ce n'est pas une exigence explicite et discutée.
- Aucune ligne
Co-Authored-Bydans les messages de commit. - Toute IP doit être validée syntaxiquement par
build-ip-list.shavant d'être injectée dans la conf Nginx (regex IPv4). Une IP malformée → erreur de boot, pas un silence. - Validation explicite à l'entrypoint. Si une variable obligatoire manque ou est invalide, le conteneur sort en erreur avec un message lisible. Pas de défaut implicite.
- Pas de bypass token, pas de cookie magique, pas de header de contournement. Le seul critère est l'IP.
10. Stratégie de fiabilité
Cette section décrit comment un agent doit se comporter face à l'incertitude.
10.1 Toutes les technos sont 🟢 dans ce projet
Aucun composant 🟡 ni 🔵. Si une évolution future introduit une techno 🟡 (ex : OpenResty/Lua) ou 🔵 (ex : njs, module dynamique exotique), elle doit être :
- explicitement marquée avec son niveau de confiance dans une mise à jour de PROMPT.md,
- accompagnée d'une vérification Context7 pour récupérer la doc officielle à jour,
- isolée dans un commit dédié (pas mêlée à d'autres changements), pour pouvoir être revertée proprement.
10.2 Quand un pattern Nginx ne marche pas comme prévu
- Lire
error.logd'abord. 80 % du temps, il y a un message explicite. - Reproduire avec un cas de test minimal dans
tests/cases/. - Ne pas ajouter de
ifdans unelocationcomme rustine — c'est presque toujours le mauvais outil. - Si la directive native ne suffit pas, proposer une alternative dans un commit séparé et prévenir l'utilisateur avant de la merger.
10.3 Vérification systématique avant tout Done
Checklist à exécuter mentalement avant de déclarer quoi que ce soit terminé :
docker compose -f docker-compose.test.yml up --abort-on-container-exitretourne 0 ?tests/run.shafficheOKpour chaque cas ?scripts/lint.shne produit aucune sortie ?nginx -tpasse sur le template instancié avec un jeu d'env vars représentatif ?README.mdreflète bien les env vars actuelles ?CHANGELOG.mdmentionne le changement ?
Évidence avant assertion : ne jamais dire « ça marche » sans avoir relancé les tests dans la session courante. Le skill superpowers:verification-before-completion formalise cette discipline.
10.4 Quand demander à l'utilisateur
- Toute extension du contrat d'env vars (nouvelle variable, changement de format).
- Tout changement de comportement HTTP (code de retour, en-têtes).
- Toute exception au scope « maintenance s'applique à tout le domaine ».
- Toute introduction d'un composant 🟡 ou 🔵.
Pour le reste (refactor interne, amélioration de la page de maintenance, ajout de tests), l'agent peut avancer en autonomie tant qu'il respecte les règles impératives du § 9.
Fin du PROMPT.md. Toute évolution de ce document doit être commitée avec un message docs(prompt): <changement> et discutée avec l'utilisateur en amont si elle modifie le scope ou le contrat d'API.