Files
outline-sync/TAILSCALE_PRD.md
Claude 319a96adac
All checks were successful
Deploy / deploy (push) Successful in 15s
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 <noreply@anthropic.com>
2026-03-07 21:27:04 +01:00

195 lines
6.4 KiB
Markdown

# 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=<outline api token>
TS_AUTHKEY=<tailscale auth key>
```
---
## 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.