capitalisation: triage 95_a_capitaliser + création domaine infra

Triage des 27 propositions du buffer de capitalisation (skill
capitalisation-triage), avec vérification des doublons contre la base.

Intégré dans knowledge/ (23 entrées):
- backend: redis (compensation incrBy non-atomique), nestjs (injection
  cassée sous tsx watch; guard write mode dégradé), async (test rollback
  pipeline multi-fichiers), contracts (idempotence POST), auth (disclosure
  comptes soft-deleted), prisma (index partial soft-delete), llm-providers
  (nouveau: OAuth vs API key, prompt caching).
- frontend: tests (garde-fous parking Later), navigation (fichiers
  non-route sous src/app Expo Router), general (type client vs payload
  backend), state (fallback catch-all mapping DB→UI).
- workflow: story-tracking (statut BMAD vs narratif obsolète).
- product: general (nouveau: doc feature store sans UI).
- infra: NOUVEAU DOMAINE (traefik, tailscale, docker, docker-networking,
  reverse-proxy-paths, sidecar tailscale) + 00_INDEX.md.

Autres:
- 90_debug_et_postmortem.md: post-mortem réseau Docker partagé hors compose.
- Rejeté 3 doublons (types enum contracts, getter PrismaService, $transaction).
- Buffer 95_a_capitaliser.md purgé et restauré à son état initial.
- _projects.conf: MAJ statuts epics + ajout app-rl799.
This commit is contained in:
MaksTinyWorkshop
2026-06-25 10:31:22 +02:00
parent 1c876309f1
commit ef24d85d57
31 changed files with 1042 additions and 27 deletions
+13
View File
@@ -0,0 +1,13 @@
# Infra — Patterns validés — Index
Patterns d'infrastructure (Docker, reverse proxy, Tailscale, homelab NUC) testés et validés en conditions réelles.
Avant toute proposition infra, identifie le fichier dont le nom et la description matchent le domaine traité, puis lis-le.
---
| Fichier | Domaine | Entrées clés |
|---------|---------|--------------|
| `docker-networking.md` | Bridges Docker, communication container ↔ hôte | Bridges isolés / service hôte injoignable, conteneuriser plutôt que firewall, `network_mode: host`, anti-pattern `host-gateway` |
| `reverse-proxy-paths.md` | Servir une app sous un sous-chemin (Traefik) | App consciente du préfixe (stripPrefix ou non), chemins relatifs, app racine, conflits de path multi-apps |
| `tailscale.md` | Sidecar Tailscale pour app incompatible subpath | Quand l'utiliser, architecture, squelette compose, `serve.json`, init obligatoire, coûts |
@@ -0,0 +1,57 @@
---
title: Infra — Patterns validés : Docker networking
domain: infra
bucket: patterns
tags: [docker, networking, bridge, traefik, host-gateway, firewall]
applies_to: [architecture, implementation, debug]
severity: medium
validated_on: 2026-06-25
source_projects: [_Assistant_Cuisine]
---
# Infra — Patterns validés : Docker networking
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/infra/patterns/README.md` pour l'index complet.
---
<a id="pattern-bridge-isole-service-hote"></a>
## Bridges Docker isolés — un container ne peut pas joindre un service hôte par défaut
### Constat
Un container connecté à un bridge custom (ex. `traefik-net` en `172.21.0.0/16`) ne peut **pas** atteindre par défaut :
- l'IP hôte côté `docker0` (`172.17.0.1`)
- l'IP hôte sur d'autres interfaces (ex. IP Tailscale `100.x.x.x`)
- `host.docker.internal` (qui résout vers `172.17.0.1`, donc même blocage)
Même quand l'hôte écoute bien sur `0.0.0.0:PORT`, l'appel depuis le container part en timeout. Cause : sous Docker 29 + iptables/nftables strict, les bridges sont isolés entre eux et le forward bridge → host est bloqué par défaut.
**Conséquence** : faire pointer un router Traefik vers `host.docker.internal:PORT` pour atteindre un service systemd hôte ne fonctionne pas en bridge mode.
### Bonnes pratiques / solutions (par ordre de propreté)
**Solution 1 (préférée) — Conteneuriser le service hôte**
Mettre le service dans un container connecté au même bridge (`traefik-net`). La communication passe par le DNS Docker interne (`servicename:port`). Aucune complexité réseau, fonctionne nativement. C'est presque toujours possible (ex. code-server via l'image `lscr.io/linuxserver/code-server`).
**Solution 2 — Traefik en `network_mode: host`**
Traefik abandonne son bridge custom et écoute directement sur les interfaces hôte. Il atteint tous les services hôte trivialement, MAIS perd l'accès aux containers via bridge : il faut alors leur publier des ports hôte, ce qui défait l'intérêt du reverse proxy interne. Réservé aux setups simples où Traefik ne sert que des services hôte.
**Solution 3 — Ouvrir manuellement le firewall**
```bash
sudo iptables -I DOCKER-USER -s 172.21.0.0/16 -d 172.21.0.1 -p tcp --dport <PORT> -j ACCEPT
```
Fragile : règle volatile, à refaire au reboot ou à l'upgrade Docker. Si on prend ce chemin, documenter et provisionner via une unit systemd.
### Anti-pattern : `extra_hosts: host.docker.internal:host-gateway`
Cette ligne déclare seulement un alias DNS dans le `/etc/hosts` du container ; elle ne crée **aucune route réseau**. Si le firewall Docker bloque, l'alias ne sert à rien : le container résout `host.docker.internal` mais le SYN reste bloqué.
**Règle** : avant de partir sur la Solution 2 ou 3, toujours vérifier qu'on ne peut pas conteneuriser le service. C'est presque toujours possible et toujours plus propre.
- Contexte technique : Docker 29 / Traefik / NUC — _Assistant_Cuisine 04-05-2026
@@ -0,0 +1,57 @@
---
title: Infra — Patterns validés : Reverse proxy & sous-chemins
domain: infra
bucket: patterns
tags: [traefik, reverse-proxy, pathprefix, stripprefix, subpath, spa]
applies_to: [architecture, implementation, debug]
severity: medium
validated_on: 2026-06-25
source_projects: [_Assistant_Cuisine]
---
# Infra — Patterns validés : Reverse proxy & sous-chemins
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/infra/patterns/README.md` pour l'index complet.
---
<a id="pattern-app-sous-chemin-reverse-proxy"></a>
## Servir une app sous un sous-chemin via reverse proxy
Le pattern qui fonctionne dépend de la façon dont l'app gère ses URLs internes (assets, API, redirects). Trois cas.
### Cas 1 — App nativement consciente du préfixe
L'app prend une option de base path et émet ses URLs internes préfixées. Côté Traefik, lire la doc de l'app pour savoir si un `stripPrefix` est requis — il n'y a **pas de règle universelle** :
- **App qui reçoit le path complet et sait quoi en faire** (ex. Cooklang `cook server --url-prefix /cuisine`) : `PathPrefix(/cuisine)` **sans** `stripPrefix`.
- **App qui veut recevoir `/` mais émet ses assets sous le préfixe** (ex. Portainer `--base-url=/portainer`) : `PathPrefix(/portainer)` **avec** `stripPrefix /portainer`.
### Cas 2 — App qui sert uniquement des chemins relatifs
Si l'app utilise des URLs relatives partout (`<a href="./xxx">`, `<script src="./xxx">`, redirects relatifs aussi), elle fonctionne sous n'importe quel sous-chemin avec un simple `stripPrefix` côté Traefik. L'app ne sait pas où elle est, mais le navigateur résout correctement les URLs relatives (cas observé sur code-server).
⚠️ Vérifier qu'**aucune URL absolue** ne traîne dans le HTML/JS. Un `href="/static/..."` (au lieu de `href="./static/..."` ou `href="static/..."`) casse le rendu.
### Cas 3 — App qui veut être à la racine
Beaucoup de SPA modernes (Homepage, Vite/Next statiques) hardcodent `href="/_next/..."` etc. Le path prefix casse alors les assets. Solutions :
- Sous-domaine dédié (`homepage.example.com`) plutôt que sous-chemin.
- Sinon, plugin de réécriture de body (ex. `traefik/plugin-rewritebody`) qui patche le HTML à la volée.
- Si l'app est incompatible subpath ET doit rester derrière Tailscale : sidecar Tailscale dédié (voir `knowledge/infra/patterns/tailscale.md`).
**Règle** : tester en priorité avec un simple `PathPrefix + stripPrefix`. Si les assets cassent, lire la doc de l'app pour une option de base path. Si rien d'officiel, basculer sur sous-domaine (ou sidecar Tailscale).
---
<a id="pattern-conflit-path-multi-apps"></a>
## Conflit de path entre apps multiples — anticipation
Quand plusieurs apps cohabitent sur le même domaine :
- **Avant** d'ajouter une app, vérifier que son préfixe ne va pas matcher des routes d'une autre app — voir le cas `/api` dans `knowledge/infra/risques/traefik.md`.
- Préférer des préfixes longs et distinctifs (`/cuisine`, `/portainer`, `/code`) plutôt que génériques (`/app`, `/admin`, `/dashboard`).
- Documenter dans le compose Traefik le mapping path → service pour garder une vue d'ensemble.
- Contexte technique : Traefik v3 / NUC — _Assistant_Cuisine 04-05-2026
+114
View File
@@ -0,0 +1,114 @@
---
title: Infra — Patterns validés : Sidecar Tailscale
domain: infra
bucket: patterns
tags: [tailscale, sidecar, docker, subpath, magicdns, tls, spa]
applies_to: [architecture, implementation]
severity: medium
validated_on: 2026-06-25
source_projects: [apps/stirling-pdf]
---
# Infra — Patterns validés : Sidecar Tailscale
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/infra/patterns/README.md` pour l'index complet.
---
<a id="pattern-sidecar-tailscale-subpath"></a>
## Sidecar Tailscale pour app incompatible avec subpath
Quand une app refuse d'être servie sous un sous-chemin (assets et fetch API hardcodés sur `/`) et qu'on veut la garder derrière Tailscale, on lui attache un **sidecar `tailscale/tailscale`** qui rejoint le tailnet avec son propre hostname et termine le TLS via un cert MagicDNS.
### Quand l'utiliser
- L'app expose une SPA Vite/Next/CRA avec assets hardcodés sur `/`.
- L'app fait des fetch API en chemin absolu (`/api/...`) sans respecter un base path.
- Patcher l'upstream ou injecter un `<base href>` est non trivial / fragile.
- **Critère de décision** : si la première page charge mais que les assets JS retournent 404 derrière Traefik, c'est ce cas (cf. bug upstream Stirling-PDF #5072).
### Architecture
```
tailnet
▼ https://<app>.<tailnet>.ts.net (cert MagicDNS auto)
[container <app>-ts (sidecar tailscale)]
│ network: <app>-internal (bridge dédié)
[container <app>]:<port>
```
- Le sidecar **rejoint le tailnet** comme un nœud à part (compte dans le quota devices).
- Cert TLS délivré automatiquement par `tailscale serve` via `serve.json`.
- L'app reste servie sur `/` côté interne ; **Traefik n'est pas dans la boucle**.
- Réseau Docker dédié, jamais branché sur `traefik-net`.
### Squelette docker-compose
```yaml
services:
myapp:
image: monimage:tag
networks: [app_internal]
# PAS d'exposition de ports sur l'hôte
# PAS de labels traefik.*
tailscale:
image: tailscale/tailscale:v1.98.3
container_name: myapp-ts
# ⚠ PAS de `hostname: myapp` ici (cf. risque collision DNS Docker)
environment:
TS_AUTHKEY: ${MYAPP_TS_AUTHKEY}
TS_HOSTNAME: myapp
TS_STATE_DIR: /var/lib/tailscale
TS_SERVE_CONFIG: /config/serve.json
TS_USERSPACE: "false"
volumes:
- /srv/docker-data/apps/myapp/tailscale/state:/var/lib/tailscale
- /srv/docker-data/apps/myapp/tailscale/serve:/config:ro
- /dev/net/tun:/dev/net/tun
cap_add: [NET_ADMIN, SYS_MODULE]
networks: [app_internal]
labels:
# Auto-discovery homepage : la tile pointe vers le FQDN tailnet, pas vers Traefik
- "homepage.group=Apps"
- "homepage.name=MyApp"
- "homepage.href=https://myapp.<tailnet>.ts.net/"
networks:
app_internal:
name: myapp-internal
driver: bridge
```
`serve.json` :
```json
{
"TCP": { "443": { "HTTPS": true } },
"Web": {
"myapp.<tailnet>.ts.net:443": {
"Handlers": { "/": { "Proxy": "http://myapp:<port-interne>" } }
}
},
"AllowFunnel": { "myapp.<tailnet>.ts.net:443": false }
}
```
> ⚠ Ne pas mettre `hostname: <app>` sur le sidecar si le service voisin s'appelle déjà `<app>` : collision DNS Docker non déterministe. `TS_HOSTNAME` fixe le nom côté tailnet et est décorrélé du DNS Docker interne. Voir `knowledge/infra/risques/docker.md`.
### Init obligatoire
1. Générer une **auth key Tailscale Reusable, non-Ephemeral** dans l'admin console.
2. Stocker le state dans un volume **persistant** (sinon ré-enrôlement à chaque restart et pollution du tailnet).
3. Si le tailnet a "Machine approval" activé : approuver le nœud manuellement après le premier `up`.
4. **Disable key expiry** sur le nœud après enrôlement (sinon il faut refournir une authkey à l'expiration, par défaut 90 j).
### Coût du pattern
- Un nœud tailnet par app (limite gratuite : 100 devices).
- Pas de middlewares Traefik partagés (rewriteBody pour `lang="fr"`, basic auth, etc.) → à accepter, ou patcher l'upstream.
- Double restart si l'app upstream change : le sidecar continue à serve même si l'app est down → 502 propre, pas de cascade.
- Contexte technique : Docker / Tailscale / Spring Boot + Vite — apps/stirling-pdf 27-05-2026