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

6.4 KiB

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:

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:

Verify:

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

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:

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

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


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.