Add Tailscale sidecar for internal Outline API access
All checks were successful
Deploy / deploy (push) Successful in 15s
All checks were successful
Deploy / deploy (push) Successful in 15s
- 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>
This commit is contained in:
194
TAILSCALE_PRD.md
Normal file
194
TAILSCALE_PRD.md
Normal file
@@ -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=<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.
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user