| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
You have a service running somewhere. zt gives it a secure public endpoint through Cloudflare Zero Trust — no dashboard clicking, one command.
zt up portainer --docker --allow you@example.com
# → https://portainer.yourdomain.com (ZT-protected, live in ~15s)zt up <name> <port> creates the tunnel, ingress rules, DNS record, and Access policy, and installs it as a service that survives reboots. zt down <name> removes everything it created. See What it does for the full list.
zt doesn't care what's on the other end of the port — the same command covers a few different jobs:
Self-hosted apps & AI tools — put a dashboard or a local model behind a real HTTPS URL with login, instead of leaving it on localhost or opening a port:
zt up grafana 3000 --docker --allow you@example.com
zt up open-webui 8080 --docker --allow you@example.comHome automation — reach Home Assistant, Frigate, or Node-RED from outside your LAN without a VPN client on every device:
zt up home-assistant 8123 --docker --allow you@example.comPR preview environments — give each pull request its own public URL for review, and tear it down when the PR closes:
flowchart LR
A[PR opened] --> B[CI builds & starts container<br/>localhost:3000]
B --> C["zt up pr-142 3000 --docker --public"]
C --> D[https://pr-142.example.com]
E[PR closed] --> F["zt down pr-142"]
F --> G[Tunnel, DNS, Access all removed]
# on PR open, in CI, after the container is up on localhost:3000
# --public: preview links need to be viewable without a Cloudflare Access
# login, same as a Vercel/Netlify preview URL — swap in --allow if your
# previews should stay restricted to your team
zt up pr-142 3000 --docker --public
# on PR close
zt down pr-142Self-hosted preview URLs without Vercel, Netlify, or a hosted preview platform. casablanque-code/cfzt ships a composite action wrapping the zt up/zt down calls above and reflecting them in the GitHub Deployments UI — see GitHub Action below.
casablanque-code/cfzt is also a composite action wrapping zt up/zt down for exactly the PR-preview flow above, plus a GitHub Deployment + Deployment Status so the preview URL shows up in the PR's own UI, not just build logs.
- uses: casablanque-code/cfzt@v0.10.0
with:
mode: up # or: down
name: pr-${{ github.event.number }}
port: '3000'
docker: 'true'
public: 'true' # or use `allow:` to restrict to your team
domain: example.com
cloudflare-api-token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
cloudflare-account-id: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}Full example workflow, input reference, and notes on state/matching across CI runs: docs/github-action.md.
zt up <name> <port> --allow <mail@example.com> automatically:
flowchart LR
A[Docker / local service] --> B(zt)
B --> C[Cloudflare Tunnel]
B --> D[DNS]
B --> E[Access]
B --> F[Lifecycle<br/>systemd / LaunchAgent / Task Scheduler]
C & D & E & F --> G[https://name.example.com]
zt down <name> attempts to remove all created resources. It looks the tunnel up in local state by default; pass --remote to resolve it directly from Cloudflare by name instead, for tearing down from a different machine than the one zt up ran on (e.g. CI — see the GitHub Action section).
zt drives cloudflared but doesn't install or manage it — use your platform's package manager so future upgrades are a normal upgrade/update, not a manual re-download:
| Platform | Install | Upgrade |
|---|---|---|
| macOS (Homebrew) | brew install cloudflared | brew upgrade cloudflared |
| Debian/Ubuntu (apt) | see Cloudflare's apt repo setup | sudo apt update && sudo apt install --only-upgrade cloudflared |
| Other Linux / binary | Releases | re-download and replace the binary |
| Windows | Releases | re-download and replace the binary — see Windows support |
zt doctor checks the installed version and, if it's outdated, prints the right upgrade command for how it detects cloudflared was installed (Homebrew/apt) or falls back to the releases link otherwise.
curl -fsSL https://raw.githubusercontent.com/casablanque-code/cfzt/main/install.sh | bashDownloads the correct binary for your OS and architecture, verifies the SHA-256 checksum, and places it in /usr/local/bin/zt.
go install github.com/casablanque-code/cfzt/cmd/zt@latestgit clone https://github.com/casablanque-code/cfzt
cd cfzt
go build -o zt ./cmd/zt
sudo mv zt /usr/local/bin/brew tap casablanque-code/cfzt https://github.com/casablanque-code/cfzt
brew install ztThe formula lives in this repo's Formula/ folder and updates itself automatically on every release (same mechanism as the Scoop bucket below). brew upgrade zt picks up new versions.
Download from Releases and place in your PATH. Each release includes .sha256 checksum files and a combined checksums.txt.
scoop bucket add cfzt https://github.com/casablanque-code/cfzt
scoop install cfzt/ztThe bucket lives in this repo's bucket/ folder and updates itself automatically on every release. To pick up a new version, run scoop update (no arguments) first — that's what actually git pulls the bucket repos — then scoop update zt. Running scoop update zt on its own can still report the old version if the bucket itself hasn't been refreshed yet.
zt.exe is a console app — double-clicking it in Explorer will show a "you need to open cmd.exe" prompt; always run it from a terminal. See docs/windows.md for that and other Windows-specific gotchas (PATH setup, task scheduling, troubleshooting).
zt up installs a Task Scheduler task (zt-<name>, logon trigger, restart-on-failure) — the same auto-start/auto-restart guarantee systemd/launchd give on Linux/macOS. It runs under your own logon session with no stored credentials and no admin prompt, same permission model as systemctl --user / a per-user LaunchAgent.
Windows install: Scoop (recommended, self-updating). A WinGet package is in progress — see the issue tracker for status.
Full details, gotchas, and Windows-specific troubleshooting: docs/windows.md.
zt initYou will be prompted for three values:
| Field | Where to find it |
|---|---|
| API Token | Cloudflare → My Profile → API Tokens |
| Account ID | Cloudflare dashboard → right sidebar |
| Domain | Your domain as it appears in Cloudflare (e.g. example.com) |
zt init validates the token and domain against the Cloudflare API before saving. Config is stored at ~/.zt-config.json (mode 0600).
Alternatively, set ZT_API_TOKEN, ZT_ACCOUNT_ID, and ZT_DOMAIN in the environment — useful on ephemeral CI runners or containers where writing a config file isn't desirable, or to swap the token without re-running zt init. Any of the three, if set, overrides the corresponding value from ~/.zt-config.json; set all three and zt init isn't needed at all.
zt up <name> <port> --allow you@example.com
# or: zt up <name> <port> --public--allow or --public is required — zt always makes you choose explicitly whether a service needs a login or not; there's no implicit default.
# Restrict access to specific email (Cloudflare sends OTP to that address)
zt up portainer 9000 --allow you@example.com
# Multiple allowed emails
zt up vault 8200 --allow alice@example.com --allow bob@example.com
# Auto-detect port from a running Docker container
zt up portainer --docker --allow you@example.com
# No Zero Trust gate — public access, no Access app created
zt up api 8080 --public
# Force TCP if QUIC is blocked by your ISP
zt up portainer 9000 --allow you@example.com --tcpThe service becomes available at https://<name>.<domain>.
The tunnel is registered as a system service and survives reboots automatically.
zt down portainerStops the system service, removes local config files, deletes the DNS record, removes the Zero Trust Access app, and deletes the tunnel from Cloudflare.
zt list # or: zt lsNAME URL PORT PROTOCOL ACCESS STATUS MANAGED BY portainer https://portainer.example.com 9000 auto (quic) ZT (1 email) running systemd grafana https://grafana.example.com 3000 http2 (TCP) public stopped pid 84291
zt status portainerportainer URL: https://portainer.example.com Port: 9000 Tunnel ID: 07fc193d-d05e-48eb-bb00-22be71823b14 Managed by: systemd Protocol: http2 (TCP) Access: ZT (1 email) Status: running Created: 2026-05-27 00:01:08 Log: /root/.zt/tunnels/portainer/cloudflared.log
# last 50 lines
zt logs portainer
# last 100 lines
zt logs portainer -n 100
# follow (like tail -f)
zt logs portainer -f
# show logs inline with status
zt status portainer --logszt export # writes zt.yaml in the current directory
zt apply zt.yaml # recreate the same services elsewhere (zt init first)zt export snapshots everything zt manages into a portable manifest (credentials excluded, safe to commit). zt apply diffs it against local state and only creates what's missing — re-running it is safe, existing tunnels are reported and skipped, never modified or deleted automatically.
Manifest format, the full apply/skip behavior, and an example: docs/backup-restore.md.
cloudflared falls back from QUIC to HTTP/2 when UDP is blocked, but never retries QUIC on its own even after the network recovers (cloudflare/cloudflared#1534).
zt watchdog enable # install as a background service, checks every 30s
zt watchdog status # check if it's running
zt watchdog disable # remove itHow it detects fallback, backoff behavior, and a known race with manual restarts: docs/watchdog.md.
zt doctor System
✓ cloudflared installed
version: cloudflared version 2024.1.0
Cloudflare
✓ API token valid
✓ domain example.com found in Cloudflare
Tunnel: portainer
✓ systemd service zt-portainer.service active
✓ local service on port 9000 reachable
✓ DNS resolves portainer.example.com
✓ Cloudflare tunnel exists
✓ all checks passed
zt version
# or: zt --versionBoth print the running version and, unless ZT_NO_UPDATE_CHECK=1 is set, do a quick (1.5s timeout) check against GitHub for a newer release — silently skipped if there's no network, since zt is meant to work fine offline. zt doctor does the same check and surfaces it alongside the rest of the system report.
| Flag | Description |
|---|---|
| -o <path> | Output path (default: zt.yaml in current directory) |
| Flag | Description |
|---|---|
| --force | Accept a manifest that changed since it was last applied |
zt apply <file> reads the manifest at <file> and creates any missing services. It remembers the content of the manifest it last applied; if <file> has changed since then — edited, or replaced by something else entirely — it shows the diff and refuses to proceed until re-run with --force.
| Subcommand | Description |
|---|---|
| enable | Install and start the watchdog as a background service |
| disable | Stop and remove the watchdog service |
| status | Show whether the watchdog is running |
One of --allow or --public is required.
| Flag | Description |
|---|---|
| --allow <email> | Restrict access to this email via Cloudflare Access (repeatable) |
| --public | No Zero Trust gate — skip Access app entirely |
| --docker | Auto-detect port from a running Docker container with this name |
| --container-port <n> | Which container-side port to expose when the container publishes more than one (requires --docker) |
| --tcp | Force TCP (http2) — use if QUIC/UDP is blocked by your ISP |
| --protocol <proto> | Protocol: auto (default), quic, http2 |
| --force | Replace an existing DNS record for the hostname even if zt didn't create it; also delete a same-name Cloudflare tunnel that zt didn't create |
Every tunnel zt creates is tagged with Cloudflare Tunnel metadata managed_by: cfzt. If zt up finds a tunnel with the target name that already carries that tag (typically a stale one left behind by an interrupted or torn-down run), it deletes and recreates it automatically. If it finds a same-name tunnel without the tag — created via the Cloudflare dashboard, another tool, or an older cfzt release — it refuses and exits with an error rather than deleting someone else's tunnel; pass --force to delete it anyway.
Same idea for DNS: replacing a record zt didn't create (with --force) prints a visible warning naming the record type and what it was pointing to, instead of deleting it silently. This is a plain printed warning, not an interactive confirmation prompt — a blocking prompt would break the CI-driven PR-preview use case (see docs/github-action.md), so review the printed warning in your run logs if you're not watching the terminal live.
| Flag | Description |
|---|---|
| -n <lines> | Number of lines to show (default: 50) |
| -f | Follow log output |
| Flag | Description |
|---|---|
| --logs | Show recent log output inline |
zt status <name> and zt <name> status are equivalent — same for logs, restart, and down.
zt uses Cobra, which ships tab-completion out of the box:
# bash (current shell)
source <(zt completion bash)
# bash (persist)
zt completion bash | sudo tee /etc/bash_completion.d/zt
# zsh
zt completion zsh > "${fpath[1]}/_zt"
# fish
zt completion fish > ~/.config/fish/completions/zt.fish
Run zt completion --help for details per shell.
zt --version
There is currently no zt update command — zt is a single static binary with no auto-updater. To upgrade, re-run the install script or go install (see Install), which simply overwrites the existing binary in place:
curl -fsSL https://raw.githubusercontent.com/casablanque-code/cfzt/main/install.sh | bash
~/.zt-config.json # credentials (0600) ~/.zt-state.json # tunnel state (0600) ~/.zt/tunnels/<name>/ # cloudflared config, credentials, log per tunnel
Full layout including per-platform service files: docs/file-layout.md.
Run zt doctor first — most issues are diagnosed automatically.
Common issues (cloudflared not found, 502 Bad Gateway, tunnel shows stopped, DNS record conflicts, Windows task not starting, and more) with fixes: docs/troubleshooting.md.
MIT
| Back | FazBrowse Home | New Git URL |