--- 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 `` 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://..ts.net (cert MagicDNS auto) [container -ts (sidecar tailscale)] │ network: -internal (bridge dédié) ▼ [container ]: ``` - 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..ts.net/" networks: app_internal: name: myapp-internal driver: bridge ``` `serve.json` : ```json { "TCP": { "443": { "HTTPS": true } }, "Web": { "myapp..ts.net:443": { "Handlers": { "/": { "Proxy": "http://myapp:" } } } }, "AllowFunnel": { "myapp..ts.net:443": false } } ``` > ⚠ Ne pas mettre `hostname: ` sur le sidecar si le service voisin s'appelle déjà `` : 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