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>
195 lines
6.4 KiB
Markdown
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.
|