Tunnels & PZ_TUNNEL

PZ_TUNNEL is the single environment variable that opts a process or Docker container into Port Zero. Set it, bind to port 0, and the daemon does the rest.

The full-domain rule

The value must be a full domain name, including its suffix. Nothing is ever appended implicitly — the daemon takes the value verbatim.

PZ_TUNNEL=hello.portzero.local                   # valid (Local tunnel)
PZ_TUNNEL=web--alice.tunnel.portzero.cloud         # valid (Cloud tunnel)
PZ_TUNNEL=hello                                   # not valid — no suffix

The suffix decides the target

SuffixTarget
.portzero.localLocal tunnel — served entirely on this machine, free and open source.
<name>--<username>.tunnel.portzero.cloudCloud tunnel — reachable from the internet, requires portzero login and a subscription.

See How it works for what happens on each path, and Cloud tunnels for the hosted product.

Must be set BEFORE launch

The daemon reads each process’s environment from outside the process — the kernel’s frozen copy of the environment captured at process start (/proc/<pid>/environ on Linux, sysctl KERN_PROCARGS2 on macOS, the process’s PEB on Windows). Setting the variable inside a running process (Python os.environ[...], Node process.env[...], Go os.Setenv(...)) only updates the in-process copy — the daemon never sees it, and discovery silently fails with no error.

Correct ways to set it before launch:

# direnv (recommended): export in .envrc, then `direnv allow`
export PZ_TUNNEL=hello.portzero.local

# shell one-off
export PZ_TUNNEL=hello.portzero.local && python3 server.py

# docker (passed at container start)
docker run -e PZ_TUNNEL=hello.portzero.local -p 0:8080 myimage

Children inherit the variable from their parent shell or launcher, so the actual listening process is discovered even when the tag was applied a level up.

Bind to port 0

Always bind your listener to port 0 so the OS assigns a free port at random — this is what makes port conflicts disappear across projects, shells, and machines. The daemon discovers the real port; you never hardcode it.

Template variables

The value can contain placeholders, resolved automatically at runtime, so parallel copies of a project each get a distinct, stable URL:

PlaceholderResolves to
{service}Service or executable name
{project}Git repository directory name
{branch}Current git branch
{worktree}Git worktree name (falls back to {branch})
{user}Your OS login username
{machine}Hostname
{uid}Short identifier from your account (first 8 characters of your account UUID; falls back to {user} when not logged in)
{username}Your portzero.cloud account username. Recommended as the trailing -- segment (the namespace) of the single label before .tunnel. Falls back to {uid}{user}
PZ_TUNNEL="{project}-{branch}--{username}.tunnel.portzero.cloud" python3 -m http.server 0
# → e.g. myapp-main--alice.tunnel.portzero.cloud

Note the -- before {username}: on the shared apex the name and the username namespace are joined with a double hyphen, not a dot (see below).

Run a different branch in a separate worktree and it gets its own stable URL at the same time, with no conflict.

Single-label names on the shared cloud domain

On the shared tunnel.portzero.cloud apex a tunnel name stays a single DNS label. Express hierarchy with a double hyphen -- inside that label, not with extra dots:

PZ_TUNNEL=pr-142--myapp--alice.tunnel.portzero.cloud

_ is not usable (invalid in hostnames, banned in certificate SANs), and a segment must not contain an ambiguous internal -- (no leading/trailing hyphen inside a ---separated part). Dotted/nested names are supported only under a verified custom domain that you own.

Companion environment variables

These optional variables are read from the same process/container environment as PZ_TUNNEL, and like it, must be set before launch:

VariablePurpose
PZ_TUNNEL_HTTP_PORTChoose which HTTP port to forward when a process listens on more than one: an explicit port, or CHOOSE_LOWEST (default) / CHOOSE_HIGHEST.
PZ_TUNNEL_PORTSExtra raw port mappings for non-HTTP forwarding: local:tunnel[;local:tunnel...] (e.g. 9222:9222).
PZ_HEALTH_PATHHTTP path that returns 2xx once the endpoint is ready to serve traffic (e.g. /health). Bare paths are rooted automatically.

PZ_HEALTH_PATH is stored on the discovered tunnel and shown in the HEALTH column of portzero status. portzero wait <domain> --healthy polls this path until it returns 2xx, which is how CI and Playwright webServer blocks gate on real readiness rather than mere port-up — see Dev-to-production commands for the full command set.