# Quick tunnels

###### Open a tunnel to a local port without writing any config: three subcommands cover HTTP, HTTPS, and raw TCP.

Quick tunnels are designed for "I just want to share this for a minute": you run one command, you get a Public URL, you hit Ctrl+C and it's gone. They auto-spawn the daemon if it isn't running, and they don't load any of your saved projects, so they're safe to use alongside a long-running setup.

#### HTTP

```sh
localcan http 3000
```

Opens a tunnel to `http://localhost:3000`. LocalCan generates a random `.local` name (e.g. `swift-falcon.local`) and a Public URL on the LocalCan.dev domain. The `.local` name is served over HTTPS, and `http://swift-falcon.local` redirects to it. LocalCan turns on the local HTTP and HTTPS proxy automatically if it is not already running.

Without a license, `localcan http` still works but only the `.local` URL is published. The command prints the Free plan signup link and skips the Public URL. Run `localcan license activate <key>` once you have a key.

If you have a [custom domain](/docs/public-urls/custom-domains) verified for your account, pin the tunnel to it with `--domain` (handy for OAuth callbacks and other URLs you can't change between runs):

```sh
localcan http 3000 --domain preview.example.com
```

`--domain` only accepts a custom domain you own. Random `*.localcan.dev` subdomains can't be requested by name, the backend assigns them.

#### Virtual hosts and headers

By default the tunnel forwards to `localhost` and passes the public hostname through as the Host header. An app served as a virtual host, such as a Laravel Herd or Valet site at `myapp.test` or an nginx `server_name`, needs to see its own hostname to serve the right site. Point the tunnel at it with `--host` and set the Host header with `-H`:

```sh
localcan http 80 --host myapp.test -H "Host: {{target_host}}"
```

`-H "Name: value"` can be repeated. Values take the same templates as [Headers](/docs/configuration/headers): `{{target_host}}` is the target's hostname, `{{original_host}}` the public one. The rules apply to the `.local` name too, since it is proxied to the same target. `--host` also works for `tcp`, `-H` does not (there are no HTTP headers to set).

While the tunnel is open, the live display shows the connecting status, the local URLs and the Public URL, and incoming requests with timestamp, method, status, and path:

```plaintext
Connecting tunnel to http://localhost:3000...
Forwarding https://swift-falcon.local → localhost:3000
Forwarding http://swift-falcon.local → localhost:3000
Forwarding https://swift-falcon-12.localcan.dev → localhost:3000

Requests (Ctrl+C to stop):

15:04:21    GET 200  /
15:04:21    GET 200  /_next/static/css/app.css
15:04:23   POST 201  /api/orders
```

The request view holds the most recent ten and updates in place, no scrollback noise.

#### HTTPS

```sh
localcan https 8443
```

Same as `http`, but for local apps that already terminate TLS. LocalCan trusts your local server's certificate even if it's self-signed. The live display is identical to HTTP. Without a license the `.local` URL is published the same way as for `localcan http`, with no Public URL.

#### TCP

```sh
localcan tcp 5432
```

Opens a raw TCP tunnel, useful for databases, SSH, and other non-HTTP services. The Public URL takes the form `tcp://*.localcan.dev:<port>`. `--domain` is not supported for TCP tunnels.

TCP tunnels require a paid license. Without one `localcan tcp` exits with an error (there's no `.local` fallback for raw TCP, since Bonjour/mDNS publishes only the HTTP/HTTPS proxy).

The live display shows active and total connection counts:

```plaintext
Connections (Ctrl+C to stop):

  Active: 2    Total: 17
```

#### Scripting with `--json`

For pipelines and AI agents, add `--json` to stream the tunnel as newline-delimited JSON, one event per line on stdout. Human status messages stay on stderr, so stdout is clean to pipe into `jq`. The flag works for `http`, `https`, and `tcp`. Agents connected over [MCP](/docs/ai-agents/mcp) can call `create_public_url` instead, which returns the URL without holding a foreground process.

The one-shot commands like `localcan tunnel ls --json` print a single JSON document. Quick tunnels are long-running, so they emit a stream instead:

```plaintext
{"event":"forwarding","time":"2026-05-20T15:04:18+02:00","scheme":"https","url":"swift-falcon.local","target":"localhost:3000"}
{"event":"forwarding","time":"2026-05-20T15:04:18+02:00","scheme":"http","url":"swift-falcon.local","target":"localhost:3000"}
{"event":"forwarding","time":"2026-05-20T15:04:19+02:00","scheme":"https","url":"swift-falcon-12.localcan.dev","target":"localhost:3000"}
{"event":"request","time":"2026-05-20T15:04:21+02:00","method":"GET","status":200,"path":"/","duration_ms":42,"response_bytes":1530}
```

Every line carries an `event` field and a `time` (RFC3339). For `request` events `time` is when the request was captured. For `forwarding` and `connections` events it is when the line was emitted. `forwarding` events announce the local and Public URLs as they come up. `request` events report one completed request each, with status, duration, and response size folded in (so no cross-line correlation is needed). `response_bytes` is the full response body size on the wire. TCP tunnels emit `connections` events with `active` and `total` counts instead of requests.

When no Public URL is streamed, a `status` event says why rather than leaving you to infer it: `unlicensed` (no license, only the `.local` URL is served) or `url_pending` (the public tunnel did not return a URL within the connect window). If the tunnel dies mid-run, for example when a Free plan session expires, a `session_ended` status event precedes the exit. Fatal errors still go to stderr with a non-zero exit.

For a script or agent, read the Public URL from the first `forwarding` event (no polling needed), then leave the command running to hold the tunnel open:

```sh
localcan http 3000 --json | jq -r 'select(.event=="forwarding" and (.url | endswith("localcan.dev"))).url'
```

Or filter for completed requests:

```sh
localcan http 3000 --json | jq -c 'select(.event=="request")'
```

Fatal errors stay on stderr and the command exits non-zero, so a failed tunnel is detectable without parsing the stream.

#### What gets created

Each invocation writes a temporary project file at `~/.localcan/projects/_quick_<scheme>_<port>_<random>.yml` and tells the daemon to reload. For `http` and `https` tunnels it also enables the local HTTP and HTTPS proxy listeners so the `.local` URL is reachable on both schemes. When you Ctrl+C, the file is removed and the daemon reloads again (the proxy listeners stay enabled).

If both the `localcan` quick-tunnel process *and* its daemon exit abnormally, the next quick-tunnel invocation spawns a fresh daemon and sweeps any leftover `_quick_*.yml` files. If only the quick-tunnel process exits but the daemon keeps running, the orphan file stays in `projects/` until you remove it (or until the daemon is stopped and a new quick tunnel is run).

Because quick tunnels live in `projects/`, they show up in `localcan projects ls` and `localcan tunnel ls` while running.

#### When to use a saved project instead

Quick tunnels are great for one-offs. For anything you'll restart often (a permanent dev domain, a setup with multiple services) write a YAML project file and use [`localcan reload`](/docs/cli/daemon). See [Configuration → Projects](/docs/configuration/projects).

