kwiss → jeremy · transmission de savoir 🐒

Un sous-domaine HTTPS par dossier, sans rien configurer.

Le système devproxy : tu crées un dossier de projet sur le serveur, et une minute plus tard il a son sous-domaine en HTTPS avec un vrai certificat. Cette page que tu lis est servie par ce système. Méta, hein.

01L'idée

Zéro config manuelle de reverse proxy. Une convention de dossiers fait tout le travail :

~/workspace/montruc/ https://montruc.dev.tondomaine.com  (dev)
~/deploy/montruc/ https://montruc.tondomaine.com  (prod)

Le port est choisi par un petit fichier optionnel .devproxy.json à la racine du projet :

// un seul service :
{ "port": 3939 }

// monorepo multi-services → montruc-web.dev.…, montruc-api.dev.… :
{ "web": 3939, "api": 4000 }

// pas de fichier → port 3000 en dev, 3001 en prod

02Les trois briques

Un script bash, lancé par cron toutes les minutes. Il scanne ~/workspace/ et ~/deploy/, lit les .devproxy.json, et régénère le Caddyfile complet. Il ne recharge Caddy que si le fichier a changé (un simple diff) — donc le cron chaque minute ne coûte rien.
Des blocs Caddy wildcard. Un bloc *.dev.domaine et un bloc *.domaine, avec un matcher host par projet vers son reverse_proxy localhost:<port>. Les sous-domaines inconnus tombent sur une page fallback statique.
TLS à la demande. Pas de certificat wildcard (qui exigerait un challenge DNS) : Caddy émet un certificat Let's Encrypt à la première visite de chaque sous-domaine. Seul prérequis DNS : deux enregistrements A wildcard (*.domaine et *.dev.domaine) vers l'IP du serveur.
Les deux pièges qui font la différence : chaque reverse_proxy a flush_interval -1 (sans ça, le SSE et le streaming sont cassés — silencieusement) et dial_timeout 2s (sans ça, visiter un projet dont l'app est éteinte bloque 30 secondes au lieu de tomber direct sur le fallback).

03Le workflow quotidien

mkdir ~/workspace/montruc && cd ~/workspace/montruc
echo '{ "port": 3942 }' > .devproxy.json
bun dev --port 3942

# 1 minute plus tard (ou : sudo devproxy-sync pour forcer)
# → https://montruc.dev.tondomaine.com  ✓ HTTPS, certif valide

04Le deploy en prod (bonus git-based)

Même philosophie pour la prod : un git push et c'est en ligne.

05Le prompt à donner à Claude

Remplace les trois variables de la première ligne (DOMAIN, USER, EMAIL), colle ça dans Claude Code sur ton serveur, et laisse-le bosser. Il vérifie le DNS d'abord et te dira quels enregistrements créer si besoin.

Mets en place un système de reverse proxy automatique sur ce serveur. Variables : DOMAIN=tondomaine.com, USER=jeremy, EMAIL=jeremy@tondomaine.com. Voici la spec exacte, exécute-la étape par étape et vérifie chaque étape avant de passer à la suivante.

## Objectif
Chaque dossier dans /home/$USER/workspace/<projet>/ doit être servi automatiquement en HTTPS sur <projet>.dev.$DOMAIN, et chaque dossier dans /home/$USER/deploy/<projet>/ sur <projet>.$DOMAIN. Aucune config manuelle par projet, juste un fichier .devproxy.json optionnel pour choisir le port.

## Prérequis à vérifier d'abord (et installer si absent)
1. Caddy installé et activé en service systemd (paquet officiel caddy).
2. DNS : vérifie avec dig que *.$DOMAIN et *.dev.$DOMAIN (teste un sous-domaine au hasard, ex: test123.dev.$DOMAIN) résolvent vers l'IP publique de ce serveur. Si non, arrête-toi et dis-moi quels enregistrements A wildcard créer.
3. Ports 80 et 443 ouverts.
4. Crée les dossiers /home/$USER/workspace, /home/$USER/deploy, /home/$USER/git, /var/www/fallback.

## Étape 1 : page fallback
Crée /var/www/fallback/index.html — une page simple et sympa qui dit que rien ne tourne sur ce sous-domaine.

## Étape 2 : le script /usr/local/bin/devproxy-sync (chmod +x)
Un script bash qui :
- Définit WORKSPACE=/home/$USER/workspace, DEPLOYDIR=/home/$USER/deploy, CADDYFILE=/etc/caddy/Caddyfile, FALLBACK=/var/www/fallback, DEV_DOMAIN=dev.$DOMAIN, PROD_DOMAIN=$DOMAIN.
- Fonction scan_dir(basedir, default_port) : pour chaque sous-dossier, lit .devproxy.json s'il existe.
  - S'il contient une clé "port" (format { "port": 3939 }) → une entrée <projet>:<port>.
  - Sinon, pour chaque paire "nom": port du JSON (format { "web": 3939, "api": 4000 }) → une entrée <projet>-<nom>:<port>.
  - Pas de fichier → <projet>:<default_port>.
  - Parse le JSON avec grep -oP (pas de dépendance à jq), comme ceci :
    simple_port=$(grep -oP '"port"\s*:\s*\K\d+' "$config")
    et pour le multi-app : grep -oP "\"[^\"]+\"\s*:\s*\d+" puis extraction nom/port.
- Génère un Caddyfile temporaire (mktemp) avec :
  - Bloc global : email $EMAIL, et on_demand_tls { ask http://localhost:5555 }.
  - Un site http://localhost:5555 { respond 200 } (endpoint d'approbation TLS).
  - Une ligne : import /etc/caddy/sites.d/*.caddyfile (pour les sites manuels hors système ; crée aussi le dossier /etc/caddy/sites.d).
  - Un bloc *.dev.$DOMAIN et un bloc *.$DOMAIN, chacun avec tls { on_demand }, puis pour chaque entrée :
    @<matcher> host <sousdomaine>.<domaine>
    handle @<matcher> {
        reverse_proxy localhost:<port> {
            flush_interval -1
            transport http {
                dial_timeout 2s
            }
        }
    }
    (matcher = sous-domaine + domaine avec les - et . remplacés par _, pour être un nom valide)
  - À la fin de chaque bloc wildcard : un handle {} par défaut + handle_errors {} qui servent /var/www/fallback/index.html en file_server.
- Compare avec diff au Caddyfile actuel ; SEULEMENT s'il diffère : sudo cp puis sudo systemctl reload caddy (ou start si caddy n'est pas actif).
- IMPORTANT : flush_interval -1 sur chaque reverse_proxy (sinon le SSE/streaming est cassé), dial_timeout 2s (sinon on attend 30s quand l'app est éteinte).

## Étape 3 : sudoers
Crée /etc/sudoers.d/devproxy (validé avec visudo -c) autorisant $USER sans mot de passe à exécuter exactement : cp vers /etc/caddy/Caddyfile, systemctl reload caddy, systemctl start caddy.

## Étape 4 : cron
Ajoute au crontab de $USER : * * * * * /usr/local/bin/devproxy-sync

## Étape 5 : système de deploy git
Crée un script /usr/local/bin/new-deploy-project <projet> [port] [commande_build] qui :
- git init --bare /home/$USER/git/<projet>.git
- Installe un hook post-receive qui : ne déploie que les push sur main ; GIT_WORK_TREE=/home/$USER/deploy/<projet> git checkout -f main ; cd dans le dossier ; bun install --frozen-lockfile ; bun run build ; sudo systemctl restart <projet>.service ; logge tout dans /home/$USER/deploy/<projet>.deploy.log avec tee.
- Génère /etc/systemd/system/<projet>.service (Type=simple, User=$USER, WorkingDirectory=/home/$USER/deploy/<projet>, ExecStart=bun run start, Restart=always, Environment=PORT=<port>) et fait daemon-reload + enable.
- Écrit { "port": <port> } dans /home/$USER/deploy/<projet>/.devproxy.json (crée le dossier).
- Ajoute au sudoers.d/devproxy la permission systemctl restart <projet>.service pour $USER.
- Affiche à la fin : git remote add deploy ssh://$USER@<ce-serveur>/home/$USER/git/<projet>.git puis git push deploy main.

## Étape 6 : vérification de bout en bout
1. Lance /usr/local/bin/devproxy-sync à la main, montre-moi le Caddyfile généré, vérifie caddy validate.
2. Crée un projet test : mkdir /home/$USER/workspace/hello, .devproxy.json avec { "port": 3998 }, lance un petit serveur HTTP sur 3998 (python3 -m http.server 3998 ou bun), relance devproxy-sync.
3. curl -sI https://hello.dev.$DOMAIN et montre-moi que ça répond 200 avec un vrai certificat.
4. curl un sous-domaine inexistant et vérifie qu'on tombe sur la page fallback.
5. Nettoie le projet test.

Note : j'utilise bun comme package manager. Travaille étape par étape, montre-moi les commandes importantes, et si une étape échoue, corrige avant de continuer.