Files
_Assistant_Lead_Tech/knowledge/infra/patterns/tailscale.md
T
MaksTinyWorkshop ef24d85d57 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.
2026-06-25 10:31:22 +02:00

4.2 KiB


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.


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

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 :

{
  "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