Tailnet browser access — serve files/content temporarily across devices
The problem
Section titled “The problem”I’m on my phone / laptop / a different device on the tailnet. The files or rendered output I need to look at live on my desktop. Options I want to avoid:
- SCP/SFTP back and forth — too slow, wrong mental model for “browse and click around”
- Cloud-drive sync — overkill for a one-off peek; may leak content off-device
- Opening a port to the public internet — creates attack surface for what’s meant to be temporary
- rdesktop / VNC / Screen Share — full-screen remoting is heavier than needed; fixed resolution
- Drag-and-drop via messaging app — doesn’t preserve directory structure; manual
The solution
Section titled “The solution”Stack two tools, both identity-gated by the tailnet:
- A local HTTP server that exposes a directory or rendered site on
localhost:PORT— anything simple that speaks HTTP tailscale serveto makelocalhost:PORTreachable at a stable tailnet URL, over Tailscale’s identity-verified transport
The content is readable from any device on my tailnet. Nobody else can reach it. It’s identity-gated at the network layer by Tailscale — no auth config needed.
Server layer: miniserve
Section titled “Server layer: miniserve”Rust single binary. One command produces a browsable HTML listing of a directory with optional search, QR code, tarball downloads, and upload support. Nothing to configure; nothing to clean up.
# Installcargo install miniserve# or on Debian/Ubuntusudo apt install miniserve # check version; cargo gets newer
# Serve current directory on localhost:8080miniserve .
# Serve a specific directory with a prettier title + QR codeminiserve --title "My dir" --qrcode /path/to/dir
# Read-only listing, no upload, on a chosen portminiserve --port 9001 --random-route /path/to/dirWhy miniserve vs alternatives:
| Alternative | Why miniserve wins |
|---|---|
python -m http.server | Works but plain. Miniserve has search, QR, tarballs, styled HTML |
npx http-server | Needs node + npm network call. Miniserve is static binary |
caddy file-server | Caddy is heavier (meant for production HTTPS); miniserve for ad-hoc |
darkhttpd | Similar in philosophy but fewer features; miniserve’s QR + search are nice for mobile |
For serving built site output (not file listings), any static server works — bun run preview, python -m http.server, or miniserve’s --index flag if you want fancy listing layered on a built index.html.
Network / identity layer: tailscale serve
Section titled “Network / identity layer: tailscale serve”tailscale serve exposes a local port as a URL on your tailnet’s MagicDNS (<machine>.<tailnet>.ts.net) with automatic HTTPS certificates provisioned by Tailscale. Only devices on your tailnet can reach it. No NAT traversal, no port forwarding, no dynamic DNS.
# Expose localhost:8080 at https://<this-machine>.<tailnet>.ts.nettailscale serve --https=443 --bg http://localhost:8080
# Shorter alternative: reverse proxy with path prefixtailscale serve --set-path=/files http://localhost:8080
# List active serve configstailscale serve status
# Tear downtailscale serve resetHTTPS certs are automatic (via Let’s Encrypt, scoped to your tailnet).
Alternative: tailscale funnel (public exposure — usually NOT what you want)
Section titled “Alternative: tailscale funnel (public exposure — usually NOT what you want)”Worked recipe: “let me browse these files from my phone”
Section titled “Worked recipe: “let me browse these files from my phone””On the desktop:
# 1. Start miniserve in the directory of interestminiserve --title "Project files" --qrcode /path/to/project-dir &
# 2. Expose it across the tailnettailscale serve --bg --https=443 http://localhost:8080Output prints a URL like https://desktop-xyz.your-tailnet.ts.net/. Open it on your phone’s browser — browse, download, search. When done:
tailscale serve resetkill %1 # kill the miniserve background jobWorked recipe: “preview my built site on my phone before I push”
Section titled “Worked recipe: “preview my built site on my phone before I push””# 1. Build the sitecd site && bun run build
# 2. Serve the static outputminiserve --index index.html ./dist
# 3. Expose across the tailnettailscale serve --bg --https=443 http://localhost:8080Open on phone → verify responsive breakpoints, check reading flow, test links without a deploy round-trip.
Worked recipe: “browse my Obsidian vault’s rendered output from anywhere on the tailnet”
Section titled “Worked recipe: “browse my Obsidian vault’s rendered output from anywhere on the tailnet””Chain with obsidian-cli or a static-export tool (Obsidian’s export plugins, quartz, etc.):
# Static-export the vault to /tmp/vault-html (using your preferred tool)# Then serve:miniserve --title "Vault preview" /tmp/vault-htmltailscale serve --bg --https=443 http://localhost:8080Security model
Section titled “Security model”| Layer | Protects against |
|---|---|
| Tailscale identity | Only devices you’ve explicitly added to your tailnet can reach the URL |
| Tailscale ACLs (optional) | Further restrict which tailnet members can access — e.g. “only my own devices, not my team’s” |
| HTTPS by default | No on-path eavesdropping within the tailnet |
| Temporary exposure | tailscale serve reset takes everything down — no lingering access |
| Firewall unchanged | No public ports opened; attack surface is unchanged |
What this does NOT protect:
- Anything readable by miniserve is readable by ALL tailnet members (unless ACLs restrict). If you have shared tailnet members, scope accordingly.
miniservehas no built-in auth — rely on the tailnet for that- Directory traversal is blocked by miniserve; custom apps behind
tailscale serveinherit their own security posture
When to use this pattern
Section titled “When to use this pattern”- Reviewing content across devices you own
- Quick “can you look at this?” with tailnet-shared collaborators (if applicable)
- Previewing built-but-not-deployed artifacts
- Browsing logs / reports on a remote dev machine
- Sanity-checking file structures without dragging through SSH
When NOT to use this pattern
Section titled “When NOT to use this pattern”- Persistent / production serving → use a real reverse proxy (Caddy, nginx) with proper cert management
- Public access needed → use
tailscale funnel, Cloudflare Tunnels, or traditional hosting - Mutable / write access needed from multiple users → miniserve upload is basic; reach for Syncthing, Seafile, or Nextcloud
- Sensitive auth required → add an auth layer in front (Authelia, oauth2-proxy) — miniserve has basic HTTP-auth but it’s not strong
Related
Section titled “Related”- Cross-device SSH pattern — the terminal-level version of “access my desktop from elsewhere”
- Terminal emulator stack research — why cross-device access is a first-class concern in agentic workflows
- My tool picks — where Tailscale sits in the stack
- Known issues — running log of cross-device quirks worth knowing about