The split mode is closest to the Cloudflare Access posture: public tripwires
keep working, while management traffic only works from your tailnet.
Prerequisites
- Docker + Docker Compose.
- A Tailscale account.
- A reusable Tailscale auth key. Split mode starts two Tailscale nodes, so a one-use key will register only the first one.
Step 1 — One-time tailnet configuration
These settings live in the Tailscale admin console and only need to be done once per tailnet.-
Enable HTTPS Certificates —
DNS→ Enable HTTPS. This lets Tailscale issue certificates for*.<tailnet>.ts.net. -
Enable Funnel —
Funnel→ Enable Funnel for this tailnet. Without this, Funnel config is ignored and the public hostname is tailnet-only. -
If you use ACLs, add a tag owner so the Docker nodes can register tagged:
Step 2 — Generate an auth key
Go to Settings → Keys → Generate auth key with these properties:
Copy the
tskey-auth-... value.
Option A — Simple Funnel
This is the existing one-host setup. It exposes the whole app publicly through Funnel; dashboard and API routes still require mantis auth../scripts/setup.sh generates the two secrets Compose won’t start without —
POSTGRES_PASSWORD and MANTIS_API_KEY_PEPPER — so you only fill in the
Tailscale and URL values above. A plain cp .env.example .env leaves
POSTGRES_PASSWORD empty and the stack refuses to boot.
Verify:
Option B — Split Serve + Funnel
This is the recommended Tailscale posture when you want/api/* and the
dashboard off the public internet.
It starts two Tailscale sidecars:
mantis-private.<tailnet>.ts.netuses Tailscale Serve only. This is where you open the dashboard and point the CLI.mantis-public.<tailnet>.ts.netuses Tailscale Funnel. Mantis allows only public routes on this host:/c/*,/status/*, and/api/wallet/*.
./scripts/setup.sh first to create .env with the two required secrets
(POSTGRES_PASSWORD and MANTIS_API_KEY_PEPPER), then add the split-host
configuration to it:
/status/nonexistentreaches mantis and returns404 not_monitored./loginreturns a plain404./api/keysreturns a plain404, not a mantis auth challenge.
PUBLIC_BASE_URL already handles that.
mantis doctor verifies the private API, then checks the public hostname hides
/login and /api/* while leaving /status/* reachable.
Public edge limits
Tailscale Funnel is not a full application-layer WAF. Split mode is the main protection: only/c/*, /status/*, and /api/wallet/* should be reachable
on the public Funnel hostname.
If the public hostname will be widely exposed or advertised, use one of these:
- Prefer Cloudflare Tunnel instead of Funnel when you own a domain and want per-path URL and rate limiting rules.
- Or put nginx/Caddy between Tailscale and Mantis, then point the Tailscale Serve/Funnel config at that proxy.
edge-limits.md.
Optional public routes
The public-only guard is deliberately tight. These are opt-in:Gotchas
- Funnel is public, Serve is private. Tailscale Serve can identify tailnet users and obeys tailnet ACLs; Funnel is for unauthenticated public internet exposure. The split setup uses both.
- One port cannot be both private and public on one Tailscale node. That is why split mode uses two Tailscale containers and two hostnames.
- Funnel hostnames are
*.ts.net. Tailscale Funnel is tied to tailnet DNS names. If you needmantis.yourdomain.complus browser SSO, Cloudflare Tunnel + Access is still the cleaner fit. - Apple Wallet callbacks live under
/api/wallet/*. They stay public in split mode because installed passes need to call back without being on your tailnet. - The dev inbox is intentionally unsafe for production. It is an
unauthenticated capture buffer. Keep
ENABLE_DEV_INBOX=0unless you are actively testing.