- 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>
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:
- URL: https://login.tailscale.com/admin/machines
- Find
domverse→ Edit route settings → enable172.29.0.0/16
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-uiusesnetwork_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_URLuses the direct Docker IP172.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-syncnode (after first deploy) so the container auth key doesn't expire - Verify
domverse-berlinhas--accept-routesso 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:
- Generate new key at https://login.tailscale.com/admin/settings/keys
- Update
TS_AUTHKEYin.envon 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.