# 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.