From 319a96adace92bd236f128cc7d37ca0b85915a6f Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 7 Mar 2026 21:27:04 +0100 Subject: [PATCH] Add Tailscale sidecar for internal Outline API access - ts-outline-sync sidecar joins Tailscale and shares network namespace with the app container (network_mode: service:ts-*) - Traefik labels on sidecar; app container has no direct network exposure - OUTLINE_URL now uses internal Docker IP 172.29.0.7:3000 via Tailscale subnet route (domverse.de advertises 172.29.0.0/16) - Add TAILSCALE_PRD.md documenting the full setup and admin checklist Co-Authored-By: Claude Sonnet 4.6 --- TAILSCALE_PRD.md | 194 +++++++++++++++++++++++++++++++++++++++++++++ docker-compose.yml | 35 ++++++-- 2 files changed, 223 insertions(+), 6 deletions(-) create mode 100644 TAILSCALE_PRD.md diff --git a/TAILSCALE_PRD.md b/TAILSCALE_PRD.md new file mode 100644 index 0000000..ff51591 --- /dev/null +++ b/TAILSCALE_PRD.md @@ -0,0 +1,194 @@ +# Tailscale Networking — Outline Sync Setup + +**Date:** 2026-03-07 +**Status:** Active + +--- + +## Problem + +`outline-sync-ui` runs on `domverse-berlin.eu` (Gitea server). +It needs to call the Outline API at `http://outline:3000`, which is on the `domnet` Docker network on `domverse.de` (main VPS) — unreachable from outside that host. + +Using the public URL `https://outline.domverse.de` works but routes through Traefik + the public internet. Tailscale provides a direct, encrypted, internal path. + +--- + +## Architecture + +``` +domverse-berlin.eu domverse.de +┌──────────────────────────────┐ ┌────────────────────────────────┐ +│ outline-sync-ui container │ │ domnet (172.29.0.0/16) │ +│ network_mode: service:ts-* │ │ ┌──────────────────────────┐ │ +│ │ │ │ │ outline 172.29.0.7:3000│ │ +│ ts-outline-sync container │ │ └──────────────────────────┘ │ +│ ├── domverse network │ │ │ +│ └── tailscale0 │◄─────────►│ tailscale0 100.104.53.109 │ +│ 100.104.209.22 │ Tailscale │ subnet router: 172.29.0.0/16 │ +└──────────────────────────────┘ └────────────────────────────────┘ + +Traefik (domverse-berlin.eu) + → domverse network + → ts-outline-sync container (port 8080) + → outline-sync-ui (shared network namespace) + → Tailscale → 172.29.0.7:3000 (outline) +``` + +--- + +## Tailscale Node Configuration + +### domverse.de — 100.104.53.109 + +**Role:** Subnet router. Exposes the entire `domnet` Docker network over Tailscale. + +**Command used to configure:** +```bash +sudo tailscale up \ + --accept-routes \ + --advertise-routes=172.29.0.0/16 \ + --advertise-exit-node +``` + +**Required after running:** Approve the route in Tailscale Admin Console: +- URL: https://login.tailscale.com/admin/machines +- Find `domverse` → Edit route settings → enable `172.29.0.0/16` + +**Verify:** +```bash +tailscale debug prefs | grep -A5 '"AdvertiseRoutes"' +# should show: "172.29.0.0/16" +``` + +### domverse-berlin.eu — 100.104.209.22 + +**Role:** Tailscale sidecar for `outline-sync-ui`. Joins Tailscale as a node and accepts the subnet routes advertised by `domverse.de`. + +**Configured via:** `ts-outline-sync` container in `docker-compose.yml` (see below). + +**Requires:** `TS_AUTHKEY` environment variable — generate a reusable auth key at: +https://login.tailscale.com/admin/settings/keys + +--- + +## docker-compose.yml Pattern + +```yaml +name: outline-sync + +services: + ts-outline-sync: + image: tailscale/tailscale + container_name: ts-outline-sync + hostname: outline-sync + environment: + - TS_AUTHKEY=${TS_AUTHKEY} + - TS_STATE_DIR=/var/lib/tailscale + - TS_USERSPACE=false + volumes: + - tailscale-state:/var/lib/tailscale + - /dev/net/tun:/dev/net/tun + cap_add: + - NET_ADMIN + - NET_RAW + restart: unless-stopped + networks: + - default + - domverse + labels: + # Traefik sees this container on domverse and proxies to port 8080 + # (served by outline-sync-ui which shares this network namespace) + - "traefik.docker.network=domverse" + - "traefik.enable=true" + - "traefik.http.routers.outline-sync.rule=Host(`sync.domverse-berlin.eu`)" + - "traefik.http.routers.outline-sync.entrypoints=https" + - "traefik.http.routers.outline-sync.tls.certresolver=http" + - "traefik.http.routers.outline-sync.middlewares=authentik@docker" + - "traefik.http.services.outline-sync.loadbalancer.server.port=8080" + + outline-sync-ui: + build: . + container_name: outline-sync-ui + restart: unless-stopped + depends_on: + - ts-outline-sync + network_mode: "service:ts-outline-sync" + environment: + - OUTLINE_URL=${OUTLINE_URL:-http://172.29.0.7:3000} + - OUTLINE_TOKEN=${OUTLINE_TOKEN} + +volumes: + tailscale-state: + driver: local + +networks: + default: {} + domverse: + external: true +``` + +**Key points:** +- `outline-sync-ui` uses `network_mode: service:ts-outline-sync` — shares the sidecar's full network namespace (domverse + tailscale0) +- Traefik labels are on `ts-outline-sync`, not the app container +- `OUTLINE_URL` uses the direct Docker IP `172.29.0.7:3000` — reachable via Tailscale subnet route +- No ports exposed directly; only Traefik reaches the app + +--- + +## .env File (on domverse-berlin.eu, gitignored) + +``` +OUTLINE_URL=http://172.29.0.7:3000 +OUTLINE_TOKEN= +TS_AUTHKEY= +``` + +--- + +## Tailscale Admin Checklist + +One-time steps in https://login.tailscale.com/admin: + +- [ ] **Approve subnet route** on `domverse`: Machines → domverse → Edit route settings → ✓ `172.29.0.0/16` +- [ ] **Disable key expiry** on the `outline-sync` node (after first deploy) so the container auth key doesn't expire +- [ ] Verify `domverse-berlin` has `--accept-routes` so it sees the subnet + +--- + +## Troubleshooting + +**Container can't reach 172.29.0.7:3000:** +```bash +# Inside ts-outline-sync container: +docker exec ts-outline-sync tailscale ping 172.29.0.7 +docker exec ts-outline-sync curl -s http://172.29.0.7:3000/api/auth.info +``` + +**Route not accepted:** +```bash +# On domverse.de: +tailscale debug prefs | grep AdvertiseRoutes +# Must show 172.29.0.0/16 +# Also approve in admin console if not yet done +``` + +**Tailscale auth key expired:** +- Generate new key at https://login.tailscale.com/admin/settings/keys +- Update `TS_AUTHKEY` in `.env` on domverse-berlin.eu +- `docker compose up -d --force-recreate ts-outline-sync` + +--- + +## Why Not Public HTTPS? + +`https://outline.domverse.de` also works and requires no Tailscale setup. Trade-offs: + +| | Tailscale (172.29.0.7:3000) | Public HTTPS | +|---|---|---| +| Latency | Lower (direct) | Higher (Traefik round-trip) | +| Auth dependency | None | Outline must be reachable publicly | +| Complexity | Sidecar + admin steps | Minimal | +| Traffic | Internal only | Exits to internet and back | + +Tailscale is preferred for production; public HTTPS is a valid fallback. diff --git a/docker-compose.yml b/docker-compose.yml index 70aa751..c8ad58b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,13 +1,21 @@ name: outline-sync services: - outline-sync-ui: - build: . - container_name: outline-sync-ui - restart: unless-stopped + ts-outline-sync: + image: tailscale/tailscale + container_name: ts-outline-sync + hostname: outline-sync environment: - - OUTLINE_URL=${OUTLINE_URL:-https://outline.domverse.de} - - OUTLINE_TOKEN=${OUTLINE_TOKEN} + - TS_AUTHKEY=${TS_AUTHKEY} + - TS_STATE_DIR=/var/lib/tailscale + - TS_USERSPACE=false + volumes: + - tailscale-state:/var/lib/tailscale + - /dev/net/tun:/dev/net/tun + cap_add: + - NET_ADMIN + - NET_RAW + restart: unless-stopped networks: - default - domverse @@ -20,6 +28,21 @@ services: - "traefik.http.routers.outline-sync.middlewares=authentik@docker" - "traefik.http.services.outline-sync.loadbalancer.server.port=8080" + outline-sync-ui: + build: . + container_name: outline-sync-ui + restart: unless-stopped + depends_on: + - ts-outline-sync + network_mode: "service:ts-outline-sync" + environment: + - OUTLINE_URL=${OUTLINE_URL:-http://172.29.0.7:3000} + - OUTLINE_TOKEN=${OUTLINE_TOKEN} + +volumes: + tailscale-state: + driver: local + networks: default: {} domverse: