We use cookies to enhance your experience on the site
CodeWorlds
Back to collections
Guide12 min read

tunnl.gg - Expose local applications to the internet with a single SSH command

tunnl.gg exposes a local app to the internet with one SSH command, no account and no install. Limits, self hosting, and a comparison with ngrok.

tunnl.gg - Expose local applications to the internet with a single SSH command

You're working on an app at localhost:3000 and suddenly need to show it to a client, test Stripe webhooks, or check how it works on a colleague's phone. What do you do? Deploy to staging? Set up ngrok with tokens and accounts? Or maybe just type a single SSH command and have a public URL in 2 seconds?

tunnl.gg is a minimalist SSH-based tunneling tool that does exactly one thing - and does it well. It exposes your local application to the internet without any installation, without creating an account, without configuring tokens. The only thing you need is SSH, which you already have on your machine.

What is tunnl.gg?

tunnl.gg is a free, open-source SSH tunneling service written in Go. It lets you expose any application running on localhost (or your local network) at a public HTTPS address with an automatic SSL certificate. The entire process comes down to a single command:

Code
Bash
ssh -t -R 80:localhost:8080 proxy.tunnl.gg

After running this command, you get a random subdomain, e.g., https://crisp-cedar-c8b5a1d2.tunnl.gg, where your local application is accessible from anywhere in the world.

The project was created by klipitkas and is available on GitHub under the MIT license.

Why tunnl.gg?

Zero installation, zero configuration

This is tunnl.gg's biggest advantage. You don't need to:

  • Install any client or CLI
  • Create an account
  • Generate API tokens
  • Configure anything

SSH is preinstalled on virtually every operating system - macOS, Linux, and even Windows 10+ has a built-in SSH client.

Comparison with alternatives

Featuretunnl.ggngroklocaltunnelCloudflare Tunnel
InstallationNo (SSH)Yes (CLI)Yes (npm)Yes (CLI)
AccountNoYesNoYes
Free planYes (100%)LimitedYesYes
HTTPSAutomaticAutomaticAutomaticAutomatic
Custom subdomainNoPaidOptionalYes
WebSocketYesYesYesYes
Open sourceYes (MIT)NoYesClient (Apache 2.0)
Self-hostingYesNoYesNo

When to choose tunnl.gg?

tunnl.gg works perfectly when you:

  • Need to quickly show something to a client or colleague
  • Are testing webhooks (Stripe, GitHub, Slack)
  • Are debugging the mobile version of your app on a physical device
  • Are working on OAuth integration and need a public callback URL
  • Don't want to install yet another tool on your system

How to use tunnl.gg?

Basic usage

Exposing a local server on port 8080:

Code
Bash
ssh -t -R 80:localhost:8080 proxy.tunnl.gg

The -t flag is required - it allocates a pseudo-terminal (TTY), allowing the server to display your tunnel URL. After connecting, you'll see:

Code
TEXT
Tunnel established!
https://crisp-cedar-c8b5a1d2.tunnl.gg

Exposing different ports

If your app runs on port 3000 (e.g., Next.js, React dev server):

Code
Bash
ssh -t -R 80:localhost:3000 proxy.tunnl.gg

App on port 5173 (Vite):

Code
Bash
ssh -t -R 80:localhost:5173 proxy.tunnl.gg

Exposing a remote host

You can also expose an application running on another machine in your local network:

Code
Bash
ssh -t -R 80:192.168.1.100:3000 proxy.tunnl.gg

Stable connection

For longer sessions, it's worth adding keep-alive so the SSH connection doesn't get interrupted:

Code
Bash
ssh -t -R 80:localhost:8080 -o ServerAliveInterval=60 proxy.tunnl.gg

Bypassing the warning page

tunnl.gg displays a warning page (interstitial) on the first browser visit - this is phishing protection. If you're using an API or curl, you can skip it:

Code
Bash
curl -H "tunnl-skip-browser-warning: 1" https://subdomain.tunnl.gg

Architecture and how it works

tunnl.gg consists of four main components:

1. SSH server (port 22)

Accepts SSH connections with the -R flag (remote port forwarding). It doesn't require authentication - this is an intentional design decision for a free service. For each connection:

  • Generates a memorable subdomain in adjective-noun-hex format (e.g., happy-tiger-a1b2c3d4)
  • Creates an internal TCP listener
  • Registers the tunnel in the central registry
  • Sends the URL to the client via the SSH channel

2. HTTP server (port 80)

Redirects all traffic to HTTPS with a 301. It issues no certificates itself: you supply them in advance, with Certbot for instance, since the code carries no built in ACME support.

3. HTTPS server (port 443)

Terminates TLS and proxies requests back through SSH tunnels to users' local applications.

4. Stats endpoint (port 9090)

Accessible only from localhost, provides metrics: active tunnels, unique IPs, total requests, blocked addresses.

Request flow

Code
TEXT
Browser → HTTPS (tunnl.gg) → TLS termination → SSH tunnel → localhost:port
  1. Browser sends a request to https://happy-tiger-a1b2c3d4.tunnl.gg
  2. HTTPS server terminates TLS
  3. Based on the subdomain, it finds the corresponding SSH tunnel
  4. Proxies the request through the SSH connection
  5. The request reaches your local application
  6. The response travels back the same way

Limits and restrictions

tunnl.gg has sensible limits to protect against abuse:

LimitValueDescription
Tunnels per IP3Maximum 3 simultaneous tunnels
Total tunnels1000Global server limit
Requests10/s (burst 20)Rate limiting per tunnel
Request body128 MBMaximum upload size
Response body128 MBMaximum download size
WebSocket1 GB/directionData limit per connection
Tunnel lifetime24hMaximum duration
Inactivity timeout2hAuto-close on no traffic
Connections/min per IP10Flood protection
IP block1hAbuse penalty

Technical limitations

  • Only supports HTTP/HTTPS traffic (no TCP/UDP)
  • No custom subdomains - always random
  • TLS is terminated server-side (server sees unencrypted traffic)
  • Single-server architecture (no scaling)
  • No tunnel authentication

Self-hosting

tunnl.gg is fully open-source and can be deployed on your own server. This is a great option if you:

  • Need control over your data
  • Want your own domain
  • Need higher limits

Requirements

  • Docker and Docker Compose
  • A domain with DNS A records (root and wildcard)
  • SSL certificate (Let's Encrypt)

DNS configuration

Add two A records pointing to your server's IP:

Code
TEXT
tunnl.yourdomain.com    →  SERVER_IP
*.tunnl.yourdomain.com  →  SERVER_IP

SSL certificate

Generate a wildcard certificate with Certbot:

Code
Bash
certbot certonly --manual --preferred-challenges dns \
  -d "tunnl.yourdomain.com" \
  -d "*.tunnl.yourdomain.com"

Docker Compose

No prebuilt image is published in any registry, so you build it from the cloned repository:

Code
YAML
services:
  tunnl:
    build: .
    ports:
      - "22:22"
      - "80:80"
      - "443:443"
    environment:
      - DOMAIN=tunnl.yourdomain.com
      - TLS_CERT=/certs/fullchain.pem
      - TLS_KEY=/certs/privkey.pem
    volumes:
      - ./data/certs:/certs:ro
      - ./data/host_key:/host_key
    restart: unless-stopped

Environment variables

VariableDefaultDescription
SSH_ADDR:22SSH server address
HTTP_ADDR:80HTTP server address
HTTPS_ADDR:443HTTPS server address
STATS_ADDR127.0.0.1:9090Metrics endpoint
HOST_KEY_PATHhost_keySSH host key path
TLS_CERT/etc/letsencrypt/live/tunnl.gg/fullchain.pemTLS certificate path
TLS_KEY/etc/letsencrypt/live/tunnl.gg/privkey.pemTLS private key path
DOMAINtunnl.ggService domain

Building from source

The project requires Go 1.24+:

Code
Bash
git clone https://github.com/klipitkas/tunnl.gg.git
cd tunnl.gg
make build

Available targets:

Code
Bash
make build        # Optimized version
make build-small  # Maximum size optimization (~6 MB)
make build-tiny   # The same plus upx packing when upx is installed
make build-dev    # Fast build with debug symbols
make build-all    # Linux and macOS, two architectures each (no Windows)

Security

Subdomain generation

tunnl.gg generates subdomains in the adjective-noun-hex8 format, yielding ~4.4 trillion possible combinations (32 adjectives x 32 nouns x 4 billion hex values). Enumeration is impractical.

Phishing protection

On the first browser visit, a warning page (interstitial) is displayed, informing the user that the content comes from a tunnel. After accepting, a cookie valid for 24 hours is set.

Security headers

Every response includes headers:

  • X-Content-Type-Options: nosniff
  • X-Frame-Options: DENY
  • X-XSS-Protection: 1; mode=block
  • Referrer-Policy: strict-origin-when-cross-origin

What to watch out for

  • No SSH authentication means anyone can create a tunnel
  • TLS terminated server-side - the operator can theoretically see the traffic
  • Don't use it for transmitting sensitive data through the public instance
  • Self-hosting solves these concerns

Practical use cases

Testing webhooks

Code
Bash
ssh -t -R 80:localhost:3000 proxy.tunnl.gg

Then in the Stripe dashboard, provide the URL: https://your-subdomain.tunnl.gg/api/webhooks/stripe

Client demo

Quick demo without deploying:

Code
Bash
ssh -t -R 80:localhost:3000 -o ServerAliveInterval=60 proxy.tunnl.gg

Send the URL to your client - it works on any device with a browser.

Testing on mobile devices

Instead of finding the IP on your local network:

Code
Bash
ssh -t -R 80:localhost:5173 proxy.tunnl.gg

Open the URL on your phone - HTTPS works out of the box.

For a phone on the same Wi-Fi this is more than the situation needs, since the app then travels across the public internet only to come back to a device standing next to you. Portless settles the same problem without leaving the local network, putting a named address in place of hunting for the machine's IP. The tunnel only wins once the phone sits on a different network or in somebody else's hands.

Debugging OAuth integrations

Many OAuth providers require a public callback URL:

Code
Bash
ssh -t -R 80:localhost:3000 proxy.tunnl.gg

Set the callback URL in the provider's config to https://your-subdomain.tunnl.gg/auth/callback.

What a tunnel will not replace

The boundary deserves naming, since tunnels get used for things they do not suit, and it always ends the same way.

A tunnel is not a staging environment. It lives while your laptop is on, connected, and holding an open session, and at most twenty four hours. A client who received the link on Friday afternoon and opened it on Monday sees an error. For something meant to stand for a week you need a deployment preview on Vercel or Railway, where each branch gets its own address and depends on nothing of yours.

Nor is a tunnel a way to expose a service permanently. No SSH authentication means anyone can create a tunnel on the public instance, and a random subdomain is the only barrier in front of whatever you exposed. That is protection by an unguessable address rather than access control. If an admin panel with no login sits behind the tunnel, you have published it.

The third boundary concerns encryption. Traffic is encrypted between the browser and the service, and between the service and your machine, while on the intermediate server it is decrypted. The operator of a public instance can technically see its contents. For a demo that is irrelevant; for a customer's personal data it is not, and then the right answer is your own instance or a Cloudflare tunnel with authentication.

The practical rule reads like this: tunnel what lasts hours and what you could show a stranger without consequence. Everything beyond that deserves a real deployment.

A few things that save time

The Host header arrives from the tunnel domain rather than from your localhost. Some frameworks reject such requests as disallowed, so if you see a header error instead of your application, add the domain to the allowed hosts list in the dev server configuration.

Hot module reloading travels through the same tunnel, but the client tries connecting to an address it knows from the build configuration. When a phone preview loads once and stops reacting to changes, that is usually the cause rather than a tunnel fault.

Absolute addresses in code break immediately. Anything with http://localhost:3000 hard coded points, on the far side of a tunnel, at the recipient's own machine. Relative paths solve it with no configuration.

When testing webhooks, remember that reconnecting gives you a new subdomain and the address in the provider's dashboard goes stale. For longer work on an integration that argues for your own instance with a fixed domain.

It also pays to check how your application behaves over a slower connection. Traffic passes through an intermediate server, so latency runs noticeably higher than hitting localhost, and that is often the only chance to see your own interface under conditions close to what a user on a phone in the field gets.

FAQ

Is tunnl.gg free?

Yes, 100%. There are no paid plans. The project is open-source under the MIT license.

Do I need an account?

No. There's no registration, login, tokens, or API keys.

Can I choose my own subdomain?

No, subdomains are randomly generated. If you need custom subdomains, consider ngrok or self-hosting with code modifications.

How long does a tunnel last?

Maximum 24 hours. After 2 hours of inactivity, the tunnel is automatically closed.

Does it support WebSocket?

Yes, with a 1 GB data limit in each direction and a 2-hour inactivity timeout.

Can I host it on my own server?

Yes, tunnl.gg is fully open-source. Self-hosting instructions can be found in the GitHub repository.

How does it differ from ngrok?

ngrok is a commercial tool with a rich feature set (dashboard, replay, custom domains). tunnl.gg is a minimalist, free alternative - zero installation, zero accounts, zero configuration.

When to reach for something else

When you need a fixed subdomain, a dashboard showing requests, and the ability to replay them, ngrok offers more, and during intensive integration work that difference earns its keep. For a service meant to stand longer and carry access control, a tunnel from a content delivery vendor is the right pick. And for something that should simply run without your computer, no tunnel is the answer, a deployment is.

Source code and self hosting instructions sit in the project repository, and the architecture description in a separate document.