60 seconds, any dev machine
The shortest path is a tunneling tool. You run one command; you get a public HTTPS URL; you paste it in Slack.
# Pick one — whichever you already have installed:
tunnel21 http 3000
ngrok http 3000
cloudflared tunnel --url http://localhost:3000
# Each prints a public HTTPS URL. Done. If you don't have any of these installed, our install is:
curl -sSf https://21tunnel.com/install | sh
tunnel21 http 3000
At this point you have a URL like
https://random-8-chars.21tunnel.app/ that
forwards to localhost:3000. You can paste it
in chat, a calendar invite, anywhere a URL goes.
Does your framework bind to 0.0.0.0?
Some dev servers (Django, Rails, Vite's strict
mode) default to binding only to 127.0.0.1
— which means the tunnel agent can't reach them if
it's running in a container or WSL. Check your
dev command for a --host 0.0.0.0 equivalent
if the tunnel returns “connection refused.”
Make it nice to share
The default random URL is fine for five-minute shares but embarrassing for clients. Three improvements that take under a minute:
Use a readable subdomain
Reserve a subdomain once, reuse it forever:
tunnel21 http 3000 --domain=alice-dev.21tunnel.app
# → always https://alice-dev.21tunnel.app Our Hobby tier (free) includes one reserved subdomain; Pro adds three. ngrok charges $10/mo for this.
Use your own domain
Even better: a URL on a domain your team already knows.
# DNS: add a CNAME on your domain →
# dev.mycompany.com CNAME alice-dev.21tunnel.app
# Then:
tunnel21 http 3000 --domain=dev.mycompany.com
Now the URL you share is https://dev.mycompany.com/
with TLS termination handled for you. Free on Hobby;
ngrok locks this to paid tiers.
Add a landing page, not just a raw app
If you're sharing with non-engineers, a one-line banner inside the app saying “Preview build · not production” saves 30 confused Slack messages.
Persistent URLs that survive restarts
The #1 frustration with casual tunnel sharing: you restart the agent, the URL changes, and every webhook and every bookmarked link breaks.
Three ways to get a persistent URL:
- Reserved subdomain — described above. Agent restarts keep the same public URL.
- Long-running agent — run the tunnel
agent as a background service (systemd, launchd,
pm2,tmux). Don't Ctrl-C it; let it reconnect automatically when your laptop wakes up. - Agent in a config file — put the
tunnel definition in
~/.21tunnel/config.toml(or equivalent), sotunnel21 startreproduces the exact same tunnel across machines:
# ~/.21tunnel/config.toml
[tunnels.dev]
addr = 3000
proto = "http"
domain = "alice-dev.21tunnel.app"
[tunnels.api]
addr = 3001
proto = "http"
domain = "alice-api.21tunnel.app"
# Start both with one command:
tunnel21 start --all When “team” means more than one person
Passing URLs around Slack works for a team of two. For three or more, you want:
- Shared domain naming. A convention
like
https://preview-<branch>.mycompany.comso everyone knows where to find the thing. - Per-person reserved subdomains.
alice.dev.mycompany.com,bob.dev.mycompany.com. No URL collisions. Our Team tier gives each seat their own subdomain allocation. - CI preview URLs. Instead of people sharing their laptop, GitHub Actions spins up an ephemeral tunnel per PR, posts the URL as a PR comment, tears it down on merge. This is the professional-end of this workflow.
# .github/workflows/preview.yml (abbreviated)
name: Preview URL
on: pull_request
jobs:
preview:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run build
- run: npm start &
- run: curl -sSf https://21tunnel.com/install | sh
- run: tunnel21 http 3000 --domain=pr-${{ github.event.number }}.preview.mycompany.com Stop strangers from loading it
A public URL is public. Bots will find it. Three levels of protection, in order of how paranoid you need to be:
1. Basic auth at the tunnel
A single username + password in front of the tunnel. Enough to keep scanners out; fine for client previews.
tunnel21 http 3000 --basic-auth alice:correct-horse-battery-staple 2. OAuth / OIDC gate
Require a real identity provider. The tunnel intercepts unauthenticated requests, redirects to Google / GitHub / your OIDC provider, and only forwards after the user proves who they are.
tunnel21 http 3000 \
--oidc-issuer=https://accounts.google.com \
--oidc-allow-email="*@mycompany.com" This is the right default for internal tools — anyone on your Google Workspace can hit the URL, no one else can.
3. IP allowlist
If your team is on a VPN with known egress IPs, you can limit by CIDR.
tunnel21 http 3000 --cidr-allow 10.0.0.0/8 --cidr-allow 203.0.113.0/24 Combine these: OIDC + IP allowlist + basic auth is overkill for most teams but trivial to set up if your compliance story demands it.
Five pitfalls to avoid
- Sharing from a machine that sleeps. The moment your laptop lid closes, the tunnel dies. For anything longer-lived than a meeting, move the agent to a VM, a Pi, or CI.
- Leaking secrets in the URL. Dev
servers sometimes expose
/debugroutes, env-var dumps, or unauthenticated admin panels. Do acurlsweep of the common routes before you share:
Anything that doesn't return 404 or 401 deserves a closer look before you paste the URL.for p in /admin /debug /env /.env /server-status /api/internal; do curl -sS -o /dev/null -w "%{http_code} $p\n" "$URL$p" done - Forgetting to disable debug tools. Rails' debug mode, Django's debug toolbar, Flask's werkzeug console — all leak extensively. Disable before sharing.
- Auth that looks like auth but isn't. “Only people with the link can access it” is not auth. Links leak. Use basic auth or OIDC if the content is non-public.
- Leaving tunnels up overnight.
The single most common source of accidentally-public
dev servers is the tunnel you meant to close at 6pm
and forgot about. Set a reminder, or use
--timeout 2hto auto-expire.
Shortest useful workflow: reserve a subdomain, put the tunnel in a config file, run it as a background service, add an OIDC gate for anything non-public. That's 10 minutes of one-time setup and then sharing localhost is a one-liner every other day.
Free on Hobby (one reserved subdomain, custom domain via CNAME, basic auth): try 21tunnel. Or compare options in our roundup.