Skip to content

Tailnet browser access — serve files/content temporarily across devices

S2 · Pattern ✓ Stable 2026-04-18

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

Stack two tools, both identity-gated by the tailnet:

  1. A local HTTP server that exposes a directory or rendered site on localhost:PORT — anything simple that speaks HTTP
  2. tailscale serve to make localhost:PORT reachable 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.

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.

Terminal window
# Install
cargo install miniserve
# or on Debian/Ubuntu
sudo apt install miniserve # check version; cargo gets newer
# Serve current directory on localhost:8080
miniserve .
# Serve a specific directory with a prettier title + QR code
miniserve --title "My dir" --qrcode /path/to/dir
# Read-only listing, no upload, on a chosen port
miniserve --port 9001 --random-route /path/to/dir

Why miniserve vs alternatives:

AlternativeWhy miniserve wins
python -m http.serverWorks but plain. Miniserve has search, QR, tarballs, styled HTML
npx http-serverNeeds node + npm network call. Miniserve is static binary
caddy file-serverCaddy is heavier (meant for production HTTPS); miniserve for ad-hoc
darkhttpdSimilar 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.

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.

Terminal window
# Expose localhost:8080 at https://<this-machine>.<tailnet>.ts.net
tailscale serve --https=443 --bg http://localhost:8080
# Shorter alternative: reverse proxy with path prefix
tailscale serve --set-path=/files http://localhost:8080
# List active serve configs
tailscale serve status
# Tear down
tailscale serve reset

HTTPS 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:

Terminal window
# 1. Start miniserve in the directory of interest
miniserve --title "Project files" --qrcode /path/to/project-dir &
# 2. Expose it across the tailnet
tailscale serve --bg --https=443 http://localhost:8080

Output prints a URL like https://desktop-xyz.your-tailnet.ts.net/. Open it on your phone’s browser — browse, download, search. When done:

Terminal window
tailscale serve reset
kill %1 # kill the miniserve background job

Worked 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””
Terminal window
# 1. Build the site
cd site && bun run build
# 2. Serve the static output
miniserve --index index.html ./dist
# 3. Expose across the tailnet
tailscale serve --bg --https=443 http://localhost:8080

Open 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.):

Terminal window
# Static-export the vault to /tmp/vault-html (using your preferred tool)
# Then serve:
miniserve --title "Vault preview" /tmp/vault-html
tailscale serve --bg --https=443 http://localhost:8080
LayerProtects against
Tailscale identityOnly 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 defaultNo on-path eavesdropping within the tailnet
Temporary exposuretailscale serve reset takes everything down — no lingering access
Firewall unchangedNo 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.
  • miniserve has no built-in auth — rely on the tailnet for that
  • Directory traversal is blocked by miniserve; custom apps behind tailscale serve inherit their own security posture
  • 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
  • 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