Expose a Local Dev Server Publicly: localtunnel vs ngrok

Blog / Python · May 27, 2018 · Updated June 10, 2026 · 9 min read
Expose a Local Dev Server Publicly: localtunnel vs ngrok

The fastest way to expose a local development server to the public internet is a tunnel: run one command and you get a public HTTPS URL that proxies straight to your machine, with no router port-forwarding, firewall changes, or deploy. For a throwaway share with zero install, npx localtunnel --port 8000 works in seconds. For anything serious — testing webhooks, OAuth callbacks, or a client demo — ngrok (ngrok http 8000) is the de-facto standard, while Cloudflare Tunnel (cloudflared) and Tailscale Funnel give you free, persistent, or private-by-default alternatives.

This guide is a 2026 refresh of an older localtunnel-only tutorial. localtunnel still works, but it is lightly maintained and can be flaky, so the real decision today is which tunnel fits your use case. Below are the exact commands for each tool, a comparison table, the Django settings you must add (ALLOWED_HOSTS and CSRF_TRUSTED_ORIGINS), and the security caveats people learn the hard way.

Key takeaways

  • A tunnel beats port-forwarding. It gives you a public HTTPS URL with no router or firewall changes, and you can shut it down the moment you are done.
  • localtunnel is the zero-install quick option (npx localtunnel --port 8000) but is lightly maintained, hands out random *.loca.lt URLs, and shows visitors an interstitial.
  • ngrok is the standard for real work: stable URLs, a built-in request inspector, replayable requests, and authentication. Its free tier includes one static domain; reserved/custom domains are paid.
  • Cloudflare Tunnel (cloudflared) is free and great for persistent named tunnels on a domain you already run on Cloudflare; it also offers a no-account quick tunnel.
  • Tailscale Funnel exposes a service over your tailnet to the public with a valid *.ts.net cert — ideal for team/private use you control.
  • Django needs config: add the tunnel host to ALLOWED_HOSTS and the full https:// origin to CSRF_TRUSTED_ORIGINS, or logins and POSTs will fail.
  • A tunnel exposes your dev box to the internet. Never tunnel a production or secret service, add auth, and stop the tunnel when finished.

What does "exposing a local dev server" actually mean?

Your development server (Django on :8000, a Node app on :3000, etc.) normally listens only on localhost, which nothing outside your machine can reach. A tunnel runs a small agent on your machine that opens an outbound connection to a relay service; the relay then publishes a public URL and forwards every incoming request back down that connection to your local port. Because the connection is outbound, it works behind NAT, home routers, and corporate firewalls without any inbound rules.

The common reasons you need this:

  • Testing webhooks from third parties that must POST to a public URL — Stripe, Twilio, GitHub, Slack, or payment callbacks (PayU, Razorpay, PayPal).
  • OAuth redirect testing, where the provider must redirect back to a real https:// URL.
  • Previewing on a real mobile device, or sharing a quick client demo of work-in-progress without deploying.

What's the fastest option? localtunnel

localtunnel is the lowest-friction choice: no account, and with npx no install at all. It assigns a public URL like https://shy-cats-jump.loca.lt that proxies to your local port for as long as the command is running.

# Zero-install, one-off tunnel to local port 8000:
npx localtunnel --port 8000

# Or install the CLI globally and use the short `lt` command:
npm install -g localtunnel
lt --port 8000

# Request a specific subdomain (granted if it's free):
lt --port 8000 --subdomain myapp
# -> https://myapp.loca.lt

Two caveats to know before you rely on it. First, localtunnel is lightly maintained and the public relay can be slow or drop connections. Second, first-time visitors hit an interstitial reminder page that asks for the tunnel's password — which is your public IP address (you can fetch it at https://loca.lt/mytunnelpassword). That friction makes localtunnel fine for a quick personal test but awkward for sharing with a client or pointing a webhook at it. For those, reach for ngrok or Cloudflare Tunnel.

ngrok: the de-facto standard

ngrok is the tool most teams reach for. After a one-time authtoken setup, ngrok http 8000 gives you a stable public HTTPS URL plus a local request inspector at http://localhost:4040 where you can see every request and response and replay them — invaluable when debugging a webhook you can't easily re-trigger.

# One-time setup (free account gives you an authtoken):
ngrok config add-authtoken <YOUR_AUTHTOKEN>

# Open a tunnel to local port 8000:
ngrok http 8000
# Forwarding  https://1a2b-3c4d.ngrok-free.app -> http://localhost:8000

# Free tier includes ONE static domain per account -> reuse the same URL:
ngrok http --url=https://your-name.ngrok-free.app 8000

# Inspect & replay traffic in your browser:
#   http://localhost:4040

The free tier hands out a random URL each run, but now also includes one static domain per account so your webhook/OAuth config can stay fixed during development. Reserved custom domains, multiple endpoints, and team features are on paid plans. ngrok can also require authentication (basic auth, OAuth, or its traffic-policy rules) so only the people you intend can reach the tunnel — worth turning on whenever the URL leaves your machine.

Cloudflare Tunnel (cloudflared): free and persistent

If you already run a domain on Cloudflare, Cloudflare Tunnel is the strongest free option for a persistent URL. There are two modes. A quick tunnel needs no account and prints a random *.trycloudflare.com URL; a named tunnel is tied to your Cloudflare account and a subdomain of your own domain, so the URL survives restarts.

# Quick tunnel: no account, random https URL, great for a fast share:
cloudflared tunnel --url http://localhost:8000
# -> https://random-words.trycloudflare.com

# Persistent NAMED tunnel on your own Cloudflare domain:
cloudflared tunnel login                       # authorize once
cloudflared tunnel create myapp                 # creates a stable tunnel
cloudflared tunnel route dns myapp dev.example.com
cloudflared tunnel run --url http://localhost:8000 myapp
# -> https://dev.example.com (stable across restarts)

Tailscale Funnel: public, but on your terms

Tailscale Funnel publishes a local service to the public internet over your tailnet, with an automatic valid TLS certificate on a https://<machine>.<tailnet>.ts.net name. It is the best fit when a small team needs to share a dev service and you want Tailscale's identity and access model around it. (Its sibling, tailscale serve, keeps the service private to your tailnet — use funnel only when you genuinely need the public internet.) Funnel must be enabled for the node in your Tailscale admin console first.

# Expose local port 8000 to the public internet over HTTPS:
tailscale funnel 8000
# -> https://my-laptop.tailnet-name.ts.net

# Run it in the background and check / stop it:
tailscale funnel --bg 8000
tailscale funnel status
tailscale funnel off

# Tip: VS Code also has built-in Port Forwarding (Ports panel ->
# Forward a Port -> set visibility Public) using its dev tunnels.

localtunnel vs ngrok vs cloudflared vs Tailscale Funnel

There is no single winner — pick by use case. Use the table below as a quick decision aid.

Tool Install Quick command Persistent URL Custom domain Free? Best for
localtunnel npx / npm -g npx localtunnel --port 8000 No (random *.loca.lt) No Fully free Zero-setup throwaway shares
ngrok binary / pkg mgr ngrok http 8000 Free static domain; reserved = paid Paid Free tier + paid Webhooks, request inspector, auth
Cloudflare Tunnel cloudflared binary cloudflared tunnel --url http://localhost:8000 Yes (named tunnels) Yes (your CF domain) Free Persistent tunnels on your own domain
Tailscale Funnel tailscale client tailscale funnel 8000 Yes (*.ts.net) No (ts.net only) Free tier Team/private dev shared securely

Rule of thumb: quick personal test → localtunnel or a Cloudflare quick tunnel; webhook/OAuth/client demo → ngrok; a stable URL on your own domain → Cloudflare named tunnel; sharing inside a team you control → Tailscale Funnel.

How do I make this work with Django?

Django will reject requests arriving at an unfamiliar host. Out of the box you'll see a DisallowedHost error, and any form/login/admin POST will fail CSRF validation. Fix both by telling Django to trust the tunnel domain. A leading dot in ALLOWED_HOSTS matches all subdomains; CSRF_TRUSTED_ORIGINS must include the scheme (https://) since Django 4.0 and supports a wildcard subdomain.

# settings.py  (development only)

# 1) Let the tunnel host reach the dev server:
ALLOWED_HOSTS = [
    "localhost",
    "127.0.0.1",
    ".loca.lt",            # localtunnel
    ".ngrok-free.app",     # ngrok
    ".trycloudflare.com",  # Cloudflare quick tunnel
    ".ts.net",             # Tailscale Funnel
]

# 2) Trust the tunnel origin for CSRF (scheme is REQUIRED on Django 4.0+):
CSRF_TRUSTED_ORIGINS = [
    "https://*.loca.lt",
    "https://*.ngrok-free.app",
    "https://*.trycloudflare.com",
    "https://*.ts.net",
]

# Run the dev server as usual (the tunnel agent connects to localhost):
#   python manage.py runserver 8000
# For previewing on another device over the SAME Wi-Fi instead of a tunnel,
# bind to all interfaces:  python manage.py runserver 0.0.0.0:8000

Is it safe to expose a local server? Security caveats

A tunnel is a real door to your machine while it is open, so treat it with care:

  • Never tunnel a production or secret service. Expose only the dev app you mean to share — not a database admin, internal dashboard, or anything holding real credentials.
  • Assume the URL is public. Random-looking tunnel URLs are not secret; crawlers and scanners find them. Add authentication (ngrok auth/OAuth, Cloudflare Access, Tailscale's identity model, or app-level login) for anything sensitive.
  • Mind Django DEBUG. With DEBUG = True, an error page leaks your settings, file paths, and environment to anyone hitting the tunnel. Keep exposure short and don't share stack traces.
  • Update ALLOWED_HOSTS / CSRF_TRUSTED_ORIGINS as shown above rather than setting ALLOWED_HOSTS = ['*'], which disables a real protection.
  • Shut the tunnel down when you're done (Ctrl+C, tailscale funnel off, stop cloudflared). An idle, forgotten tunnel is the one that bites you.

Frequently Asked Questions

Is localtunnel still maintained?

localtunnel still works and remains a handy zero-install option via npx localtunnel --port 8000, but it is only lightly maintained. Its public relay can be slow or drop connections, URLs are random *.loca.lt subdomains, and first-time visitors must clear an interstitial that asks for the tunnel's password (your public IP). It's fine for a quick personal test; for webhooks, OAuth, or client demos, ngrok or Cloudflare Tunnel are more reliable.

ngrok vs localtunnel — which should I use?

Use localtunnel for the absolute fastest, no-account, throwaway share. Use ngrok when the tunnel needs to be dependable: it provides stable URLs (including one free static domain), a request inspector at http://localhost:4040 for viewing and replaying traffic, and built-in authentication. For testing third-party webhooks and OAuth flows, ngrok's inspector and stable URL make it the practical default.

Is it safe to expose my local server to the internet?

It's safe if you're deliberate. A tunnel opens a public door to your machine while it runs, so expose only the dev app you intend to share, never a production or secret service. Add authentication for anything sensitive, keep Django DEBUG in mind because error pages leak internals, avoid ALLOWED_HOSTS = ['*'], and stop the tunnel as soon as you're finished.

Why do I get a DisallowedHost or CSRF error through a tunnel?

Because the request arrives at a host Django doesn't recognize. Add the tunnel domain to ALLOWED_HOSTS (a leading dot like .ngrok-free.app matches subdomains) to clear DisallowedHost, and add the full origin with scheme to CSRF_TRUSTED_ORIGINS (for example https://*.ngrok-free.app) to fix CSRF failures on logins and form POSTs. The scheme is required on Django 4.0 and later.

How do I expose a local server without installing anything?

Use npx localtunnel --port 8000, which downloads and runs localtunnel on the fly with no global install and no account. Cloudflare's quick tunnel (cloudflared tunnel --url http://localhost:8000) is similarly account-free, though it does require the small cloudflared binary. Both print a public HTTPS URL immediately.

Can I get a permanent or custom URL for my tunnel?

Yes. ngrok's free tier includes one static domain, and paid plans add reserved/custom domains. Cloudflare Tunnel gives a persistent URL on your own domain via a named tunnel (cloudflared tunnel create). Tailscale Funnel provides a stable *.ts.net name tied to your machine. localtunnel can only request a subdomain (--subdomain myapp) when it happens to be free, so it isn't a reliable permanent URL.

Where to go next

Tunnels are one piece of a fast local feedback loop. If you work terminal-first, see our guide to Vim for Python development, and if you're on Windows, our walkthrough for setting up a Python development environment on Windows covers the surrounding toolchain.

MicroPyramid has built and shipped Python and Django applications for 12+ years across 50+ delivered projects — including the webhook integrations, payment callbacks, and OAuth flows that make tunnels worth the trouble. If you want an experienced team to design, build, or modernize your product, explore our Python development services or our broader web development services.

Share this article