Install

viaduct needs Python 3.11 or newer and pipx, which keeps the CLI in its own isolated environment and on your PATH.

$ pipx install viaduct-sh

That installs both the viaduct client and the viaductd server. Confirm it is on your path:

$ viaduct --version

Start a tunnel

Point viaduct http at any local port. It prints a public HTTPS URL and forwards traffic to that port until you stop it.

  1. Run your app on some local port, for example a dev server on 8080.
  2. Open the tunnel with the command below.
  3. Share the printed URL. Every request to it rides the tunnel to your localhost.
$ viaduct http 8080
bash
✓ tunnel live

  https://sunny-otter.viaduct.sh
  → http://localhost:8080
  press q to stop
Nothing listening yet? viaduct still opens the tunnel and serves a friendly 502 page until your app comes up, so you can start them in either order.

Stop the tunnel with q or Ctrl+C. On a drop it reconnects on its own, and by default each reconnect gets a fresh URL. To keep the same one, pin it.

Keep the same URL (--pin)

Add --pin to keep the same public URL for a given port across reconnects and restarts. Useful for a webhook endpoint you register once, or a demo link you want to stay put.

$ viaduct http 8080 --pin
bash
✓ tunnel live

  https://amber-stoat-9f3ac41b2c7d.viaduct.sh
  → http://localhost:8080
  press q to stop
With --pin the name is server-assigned. It is derived from a random secret stored once at ~/.config/viaduct/pin.key; different ports get different stable names, and deleting that file rotates to a new one. To choose the string yourself, use --name below.

Choose a name (--name)

Want a specific subdomain rather than a random one? Pass --name. If it is free and passes a quick check, the server assigns it and pins it, so it stays put across reconnects, like --pin but with a name you picked.

$ viaduct http 8080 --name myapp
bash
✓ tunnel live

  https://myapp.viaduct.sh
  → http://localhost:8080
  press q to stop
Names are first come, first served and validated on the server. Use 3 to 63 characters of a-z, 0-9 and hyphens (no leading or trailing hyphen). Reserved and unsuitable names are refused. Availability is per server, so if a name is already taken on the one you connect to you will be told, pick another or try a --region. --name and --pin cannot be combined.

Inspect requests (--inspect)

The traffic inspector prints each request as it happens: method, path, status, and timing. Ideal for debugging webhooks without leaving your terminal. Start it with --inspect:

$ viaduct http 8080 --inspect
bash
✓ tunnel live

  https://funny-otter.viaduct.sh
  → http://localhost:8080

14:02:01  GET  /                       200 5ms
14:02:04  POST /webhooks/stripe        200 61ms
14:02:06  GET  /favicon.ico            404 1ms
14:02:09  GET  /ws                     101 3ms

You do not have to decide up front. Press i on a running tunnel to turn the inspector on or off at any time, with no reconnect and no change to your URL. Starting with --inspect just has it on from the first request.

For a full walkthrough of pointing a provider at your machine, see receiving webhooks on localhost.

Combine flags

Flags stack. Pin a stable URL and watch its traffic at the same time:

$ viaduct http 8080 --pin --inspect

The order does not matter, and the local port can be any port your app listens on.

Protect a tunnel (--basic-auth, --allow-ip)

A tunnel is public by default: anyone with the URL reaches your local app. Put a gate in front of it with a password, a bearer token, an IP allowlist, or any combination.

$ viaduct http 3000 --basic-auth alice:secret

That's an HTTP Basic prompt in the browser. Omit the :secret and you're asked for the password at the terminal instead of typing it into the command line. For APIs and webhooks, require a token:

$ viaduct http 3000 --bearer $TOKEN

Or restrict by address. Repeat --allow-ip or comma-separate; each is a single IP or a CIDR range:

$ viaduct http 3000 --allow-ip 203.0.113.4 --allow-ip 10.0.0.0/8

Keep credentials out of your shell history with an environment variable (VIADUCT_BASIC_AUTH, VIADUCT_BEARER) or a line in ~/.config/viaduct/config.toml. Change the pages visitors see with --auth-message (401) and --deny-message (403), and the browser prompt's label with --auth-realm.

What a visitor without credentials sees, served by viaductd itself. Change the wording with --auth-message.

Checked at the edge, not on your machine. viaductd verifies every request before it opens a connection to you, so an unauthorised visitor gets a clean 401 or 403 and never reaches your app. Only hashes of your password and token ever leave your machine, never the plaintext. This is access control for sharing a link with the right people, not a shield against denial-of-service traffic.

Security and privacy

viaduct is a raw byte splice. The server reads only the head of each request (the request line and headers) to find the Host it should route to, then splices the bytes both ways untouched. Request and response bodies are never parsed or buffered, and WebSocket upgrades pass straight through. It is a pipe, not a proxy that reads your traffic.

What leaves your machine

The tunnel carries your app's traffic to whoever holds the URL, so treat a public tunnel like any public link. The one thing never sent in the clear is your credentials: with --basic-auth or --bearer, only a hash of the password or token leaves your machine, never the plaintext.

Auth is checked at the edge

viaductd verifies --basic-auth, --bearer and --allow-ip before it opens a connection to you, so an unauthorised visitor gets a clean 401 or 403 and never reaches your app. The flags are in Protect a tunnel.

The inspector reads the line, not the body

--inspect peeks at the first packet of each request and response to read the method, path and status code, once, and prints a line. It never buffers the body, it is off by default, and it costs nothing when off.

Want no third party in the path at all? Run your own server. Point the client at your own viaductd and your traffic never touches viaduct.sh. See the self-hosting guide. Access control is for sharing a link with the right people, not a shield against denial-of-service traffic.

Custom domains (bring your own)

Serve a tunnel on your own domain with a single CNAME. There's no extra flag, the mapping lives in your DNS.

  1. Pin the tunnel so it has a stable name: viaduct http 8080 --pin gives you something like funny-otter-9a1c2b3d4e5f.viaduct.sh.
  2. Add a CNAME at your registrar (the exact screen differs per provider): point demo.example.com at that funny-otter-9a1c2b3d4e5f.viaduct.sh name.
  3. Open it. The first request auto-provisions an HTTPS certificate, then https://demo.example.com serves your local app.
Why a CNAME, not an A record? viaductd follows the CNAME back to your tunnel to know where to route, so it has to point at the *.viaduct.sh name rather than straight at an IP. And because the certificate is only issued once your domain already points here, you can only serve domains whose DNS you actually control.

Regions

viaduct.sh operates tunnel nodes in five regions. Run your tunnel on the one nearest your traffic with --region; with no flag you get London by default.

Region--regionTunnel URL
London (default)lonname.viaduct.sh
New Yorknycname.nyc.viaduct.sh
Singaporesgname.sg.viaduct.sh
Sydneysydname.syd.viaduct.sh
Bangaloreblrname.blr.viaduct.sh
$ viaduct http 8080 --region nyc   # -> name.nyc.viaduct.sh
$ viaduct http 8080 --region sg    # -> name.sg.viaduct.sh

List them any time with viaduct regions.

On latency: a tunnel always ends at your machine, so a region mainly helps when your visitors and your machine are both far from the default. Pick the node nearest your traffic; there's no benefit to switching if London is already close.

All options

Every flag on viaduct http. One worth calling out: --host-header. Some dev servers (Vite, Next.js, Django, Rails) keep an allow-list of hostnames and reject a request whose Host is the public tunnel URL ("Invalid Host header" / "Blocked request"). --host-header localhost makes your app see the request as if it arrived on localhost, while visitors still use the public URL.

OptionDefaultDescription
--server host:portviaduct.sh:4443The viaductd server to dial.
--region lon|nyc|sg|syd|blrnonePick a server region; a shortcut for --server.
--pinoffKeep the same public URL across reconnects (server-assigned stable subdomain).
--name SUBDOMAINnoneRequest a specific subdomain, pinned if it is free. Validated server-side; 3 to 63 chars of a-z, 0-9, hyphens. Not combinable with --pin.
--host-header HOSToffRewrite the Host header your app sees (e.g. localhost), for dev servers that reject unknown hosts.
--basic-auth USER:PASSoffRequire HTTP Basic auth, checked at the edge. Omit :PASS to be prompted for it.
--bearer TOKENoffRequire an Authorization: Bearer TOKEN header.
--allow-ip CIDRoffOnly allow these addresses (single IPs or CIDR ranges). Repeat the flag or comma-separate.
--auth-message TEXTdefaultMessage on the 401 page when credentials are missing or wrong.
--deny-message TEXTdefaultMessage on the 403 page when an address is not on the allowlist.
--auth-realm NAMEviaductRealm shown in the browser's Basic-auth prompt.
--inspectoffLog each request: method, path, status, and time.
--pool-size N40Idle data connections kept ready for incoming requests.
--tls / --no-tlson, off for localhostTLS to the tunnel port.
--tls-ca PATHnoneExtra CA bundle to trust (for a self-signed server).

Flags can also live in ~/.config/viaduct/config.toml instead of the command line:

server = "viaduct.sh:4443"
tls    = true

Stop and update

Stop a running tunnel with q or Ctrl+C. It drains in-flight requests before exiting.

Update to the latest release at any time:

$ viaduct upgrade

viaduct upgrade reinstalls the latest release from PyPI. On an interactive terminal the client also shows a one-line notice, at most once a day, when a newer release exists.

List and stop tunnels

Running several at once? viaduct list shows the tunnels open on this machine, and viaduct kill stops them (gracefully, so in-flight requests finish) from any shell.

$ viaduct list
bash
NAME           PORT   URL                               UP     PID
funny-otter    8080   https://funny-otter.viaduct.sh    2m   41821
mighty-coati   3000   https://mighty-coati.viaduct.sh   30s  41850
$ viaduct kill funny-otter   # by name, or pid
$ viaduct kill --all         # stop every tunnel on this machine

These are machine-local: they only see tunnels you started on this box, and never touch anyone else's.

From your code

You do not need the CLI. Install viaduct-sh and open a tunnel straight from Python, handy for scripts and for tests that need a real public URL (webhooks).

import viaduct

async with viaduct.tunnel(8080) as t:      # async
    print(t.url)                           # https://funny-otter.viaduct.sh

with viaduct.tunnel_sync(8080) as t:       # sync (scripts, notebooks)
    print(t.url)

A public URL in your tests

Installing viaduct-sh also registers a pytest fixture, viaduct_tunnel, that hands a test a real public URL and tears it down afterwards. Ideal for exercising inbound webhooks in CI.

def test_stripe_webhook(viaduct_tunnel):
    url = viaduct_tunnel(8000)             # your local test server is now public
    stripe.WebhookEndpoint.create(url=url + "/hook")
    ...                                    # torn down automatically

Node

There is a zero-dependency Node client too, published as viaduct-sh on npm and living in the repo.

import { tunnel } from "viaduct-sh";

const t = await tunnel(3000);
console.log(t.url);   // or, no install:  npx viaduct-sh 3000

Run your own server

The client defaults to the hosted viaduct.sh, but the point of viaduct is that you run your own. One Ubuntu droplet runs Caddy (public HTTPS) and viaductd, and one script sets it all up. Then point the client at it:

$ viaduct http 8080 --server your-domain.com:4443
IP allowlists and your own server. On the hosted servers, viaductd sits behind Caddy, which passes it the real visitor address, so --allow-ip works out of the box. If you run viaductd with no such trusted front, start it with --trust-peer-ip so it reads the direct connection's address instead. Only turn that on when a front you control is not adding the forwarded-for header, otherwise a client could spoof its own address.

Troubleshooting

The pages viaductd serves name the problem, and most come down to a handful of causes. Here is what each one means and how to clear it.

404, tunnel not found

The address has no live tunnel behind it: the tunnel closed, or the link is an old one. Start it again. By default each run gets a fresh URL, so use --pin or --name to keep a stable one.

502, nothing is running here

The tunnel is up, but nothing is listening on the local port it forwards to. Start your app on that port and refresh. viaduct opens the tunnel even before your app is up, so the order you start them in does not matter.

503, tunnel busy

The tunnel ran out of free connections for a moment. The page carries a Retry-After and refreshes itself, so it usually clears on its own within a few seconds.

401 or 403 on a protected tunnel

The visitor has no valid credentials (401) or their address is not on the allowlist (403). Share the password or token, or add their IP with --allow-ip. Both are checked at the edge, so neither reaches your app.

That name is not allowed, or already in use

The --name you asked for is reserved or is already taken on the server you reached. Pick another, or try a different --region, availability is per server.

Your client is too old for this server

Run viaduct upgrade and reconnect.

The URL keeps changing on reconnect

On a dropped connection viaduct reconnects on its own, and by default each reconnect gets a fresh URL. Pin it with --pin or --name to keep the same one across drops.

The first request to your own server is slow, or will not connect

On a server you run, the first request to a new subdomain provisions its TLS certificate, which takes a few seconds, then it is instant. If it never connects, check the client can reach the server's host:port and that wildcard DNS points at the box. See the self-hosting guide.