Documentation
Getting started
Everything you need to put a local port on a public URL. Install once, then start a tunnel, pin a stable URL, or watch requests live. Every command below is copy-paste ready.
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.
- Run your app on some local port, for example a dev server on
8080. - Open the tunnel with the command below.
- Share the printed URL. Every request to it rides the tunnel to your
localhost.
$ viaduct http 8080
✓ tunnel live https://sunny-otter.viaduct.sh → http://localhost:8080 press q to stop
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
✓ tunnel live https://amber-stoat-9f3ac41b2c7d.viaduct.sh → http://localhost:8080 press q to stop
~/.config/viaduct/pin.key. Different ports get different
stable names; delete that file to rotate to a new one.
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
✓ 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.
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.
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.
- Pin the tunnel so it has a stable name:
viaduct http 8080 --pingives you something likefunny-otter-9a1c2b3d4e5f.viaduct.sh. - Add a CNAME at your registrar (the exact screen differs per provider): point
demo.example.comat thatfunny-otter-9a1c2b3d4e5f.viaduct.shname. - Open it. The first request auto-provisions an HTTPS certificate, then
https://demo.example.comserves your local app.
*.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 | --region | Tunnel URL |
|---|---|---|
lon | name.viaduct.sh | |
nyc | name.nyc.viaduct.sh | |
sg | name.sg.viaduct.sh | |
syd | name.syd.viaduct.sh | |
blr | name.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.
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.
| Option | Default | Description |
|---|---|---|
--server host:port | viaduct.sh:4443 | The viaductd server to dial. |
--region lon|nyc|sg|syd|blr | none | Pick a server region; a shortcut for --server. |
--pin | off | Keep the same public URL across reconnects (stable subdomain). |
--host-header HOST | off | Rewrite the Host header your app sees (e.g. localhost), for dev servers that reject unknown hosts. |
--basic-auth USER:PASS | off | Require HTTP Basic auth, checked at the edge. Omit :PASS to be prompted for it. |
--bearer TOKEN | off | Require an Authorization: Bearer TOKEN header. |
--allow-ip CIDR | off | Only allow these addresses (single IPs or CIDR ranges). Repeat the flag or comma-separate. |
--auth-message TEXT | default | Message on the 401 page when credentials are missing or wrong. |
--deny-message TEXT | default | Message on the 403 page when an address is not on the allowlist. |
--auth-realm NAME | viaduct | Realm shown in the browser's Basic-auth prompt. |
--inspect | off | Log each request: method, path, status, and time. |
--pool-size N | 40 | Idle data connections kept ready for incoming requests. |
--tls / --no-tls | on, off for localhost | TLS to the tunnel port. |
--tls-ca PATH | none | Extra 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
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
--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.