Home/ Blog/ Tutorial

Share localhost with your team in 60 seconds.

The minimal setup — one command, HTTPS URL, works on any network. Then: how to keep the URL stable across restarts, how to password-gate it, and when to graduate to a real preview-deploy workflow.

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:

  1. Reserved subdomain — described above. Agent restarts keep the same public URL.
  2. 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.
  3. Agent in a config file — put the tunnel definition in ~/.21tunnel/config.toml (or equivalent), so tunnel21 start reproduces 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.com so 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

  1. 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.
  2. Leaking secrets in the URL. Dev servers sometimes expose /debug routes, env-var dumps, or unauthenticated admin panels. Do a curl sweep of the common routes before you share:
    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
    Anything that doesn't return 404 or 401 deserves a closer look before you paste the URL.
  3. Forgetting to disable debug tools. Rails' debug mode, Django's debug toolbar, Flask's werkzeug console — all leak extensively. Disable before sharing.
  4. 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.
  5. 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 2h to 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.