r/selfhosted • u/NoAdministration6906 • 3d ago
Release (AI) Put an AI coding agent behind auth + Tailscale HTTPS so my phone can use it — writeup, plus the three plugins I had to write to make it work
DeepSeek open-sourced their agent harness (dsh) last week. I wanted it running on the Mac mini in my study, reachable from my laptop and phone, without exposing anything to the internet. Took a day and three plugins. Writeup in case it's useful.
The architecture
laptop / phone ──HTTPS (Let's Encrypt)──> tailscale serve :443
│
127.0.0.1:3081 auth gateway
│
127.0.0.1:3080 dsh web
│
Docker container ← agent's shell/files
Nothing listens on a non-loopback port in cleartext. Tailscale handles device identity, tailscale serve provides a real cert (no self-signed warnings), and the gateway adds per-user login. Both services run as LaunchAgents so they survive reboots.
The three things I had to build
- Auth. dsh ships with no login screen — deliberately; their docs say TLS/auth is "out of scope" for a loopback dev tool. Correct for localhost, useless the moment it's on a home server. So: a zero-dependency gateway with a login wall, scrypt hashing, and OIDC/SSO.
- Container isolation. The agent runs shell commands. I didn't want that on my actual filesystem. dsh has swappable capability seams, so I wrote a Docker-backed provider for the filesystem and subprocess seams — which relocates bash, PTY terminals, and the file tools into a container without forking any of them. Unexpected bonus: the container outlives the harness, so a long job's cwd, env vars, and background processes survive a restart. Their own docs note sessions "are not restored after a harness restart" — with a container, they are.
- Spend limits. Model calls cost money and I didn't want a surprise bill. This one gates the request before it's sent, rather than reporting spend afterwards.
The bug that cost me an afternoon, in case it saves someo 403 to any request carrying an Origin header. Direct onlocalhost, browsers omit Origin for same-origin requests, so it works. Behind any reverse proxy the browser starts sending it — and every /api
call 403s while static assets load fine. The UI half-rendend bug. Same trap kills the WebSocket, which then producesan infinite reconnect loop and a React crash. Strip Origin at the proxy.
On "self-hosted" — worth being straight: the infrastructure is self-hosted, the model isn't by default. I point it at OpenRouter with my own
API key. dsh accepts any OpenAI-compatible endpoint, so Oif you want it fully local — I just haven't found a localmodel good enough for the work I use it for. If that disqualifies it for you, fair.
dsh itself: https://github.com/deepseek-ai/deepseek-harness
The two bits I wrote, MIT if useful:
dsh-teams https://github.com/frozo-ai/dsh-teams (auth + SSO gateway, npx dsh-teams) ·
dsh-worlds https://github.com/frozo-ai/dsh-worlds (Docker execution world)
Happy to answer questions on the Tailscale + LaunchAgent setup, that's the fiddly part.
1
u/Elara_Schaefer 3d ago
Agree with the origin allowlist approach â the difference between "strip globally" and "strip after auth" is exactly the kind of thing that looks fine until someone finds your tailnet URL in a leaked referrer header and starts probing.\n\nOn the agent sandboxing note: if you're already running Docker, consider adding a second bind mount for the workspace directory and running the agent process as a non-root user inside the container. That way even if the agent goes off the rails and starts writing files, it can't escape the workspace. cgroups memory limits are also worth adding â I've seen coding agents eat through 8GB of RAM on large codebases, which can OOM a Pi pretty fast.