Guides
Servers and containers
Run Cloudflare Tunnel routes on a VPS, AWS EC2, Azure, Google Cloud, Docker or Kubernetes with teitunnel, as a sandboxed systemd service, with a web dashboard and an API.
The app is for your computer. On a server, the teitunnel command does the same job from
the terminal: it adds routes through the same reviewed plans, and runs the connectors
itself, as a system service, or in a container. Use it for anything that should stay online
when your laptop is closed: a home server, a VPS, a Raspberry Pi, a Docker host or a
Kubernetes cluster.
Nothing about the machine's network matters: the connector only makes outbound connections
to Cloudflare, so no inbound port, public IP, load balancer or firewall rule is needed. Keep
your services listening on localhost (or a private network).
The API token
A server usually has no keychain, so Teitunnel doesn't store the token there. Give it to each command through the environment instead:
export CLOUDFLARE_API_TOKEN=… # or
export CLOUDFLARE_API_TOKEN_FILE=/run/secrets/cloudflare_api_tokenTEITUNNEL_API_TOKEN (and TEITUNNEL_API_TOKEN_FILE) work too. The token is used for that
command only and never written to disk. Create it in the Cloudflare dashboard (an
account-owned token is best for servers) with Account · Cloudflare Tunnel · Edit,
Zone · DNS · Edit and Zone · Zone · Read; add the two Access permissions if
routes should require a login. On a machine that does have a
keychain, teitunnel setup stores it there once instead.
Any Linux server (VPS, EC2, Azure VM, Compute Engine)
Install the command
Download the teitunnel-cli archive for your architecture from the
releases and put teitunnel on the PATH. Then get a
verified cloudflared if the machine has none:
teitunnel cloudflared install # the latest release from Cloudflare, checked, into Teitunnel's data folder
teitunnel cloudflared statusAdd routes
export CLOUDFLARE_API_TOKEN=…
teitunnel route add app.yourhost.com 3000 --yes # creates this server's tunnel
teitunnel route add api.yourhost.com 8080 --yes
teitunnel routes # status--yes applies without asking; leave it out to review each plan first.
Keep it running
sudo -E teitunnel always-on on # a systemd service, started at boot
teitunnel always-on statusRun as root, always-on installs a system unit in /etc/systemd/system: it starts at
boot, restarts when it stops, and runs sandboxed (NoNewPrivileges, ProtectSystem=strict,
a read-only home, writing only its own folder). As a normal user it installs a user unit
instead, which stops at logout unless lingering is on (loginctl enable-linger).
always-on off removes it. --tunnel NAME limits it to one of the machine's tunnels, and
-a picks the account.
Without systemd, run teitunnel up under the supervisor you have (runit, s6, OpenRC, a
@reboot job): it runs every tunnel of the machine in the foreground and stops them on
SIGTERM. In a folder with a teitunnel.yml, up applies it
first (--no-project skips it).
On macOS and Windows, always-on on installs a launchd agent or a scheduled task for your
user instead, the same as Keep running when Teitunnel quits in the app.
Docker
The image teispace/teitunnel (also ghcr.io/teispace/teitunnel) has the CLI and
Cloudflare's own cloudflared, runs as a non-root user on a distroless base, keeps its state
in /data, and runs up by default:
docker volume create teitunnel
docker run --rm -e CLOUDFLARE_API_TOKEN -v teitunnel:/data \
teispace/teitunnel route add app.yourhost.com http://web:80 --yes
docker run -d --name teitunnel --restart unless-stopped \
-e CLOUDFLARE_API_TOKEN -v teitunnel:/data --network my-app teispace/teitunnelThe image is multi-arch (linux/amd64, linux/arm64) and tagged x.y.z, x.y and
latest: pin a version (:0.2.0) or follow a minor release (:0.2) to choose when it
changes.
Routes point at other containers by name (http://web:80) when they share a network. The
container's health check is teitunnel routes --check, which fails unless every route is
live.
Docker Compose
services:
teitunnel:
image: teispace/teitunnel:0.2
environment:
CLOUDFLARE_API_TOKEN_FILE: /run/secrets/cloudflare_api_token
secrets: [cloudflare_api_token]
volumes: [teitunnel:/data]
restart: unless-stopped
web:
image: nginx:alpine
secrets:
cloudflare_api_token:
file: ./cloudflare_api_token
volumes:
teitunnel:docker compose up -d
docker compose run --rm teitunnel route add app.yourhost.com http://web:80 --yes
docker compose restart teitunnelThe Docker Compose tutorial walks through it end to end,
and teitunnel export docker-compose writes a Compose service running plain cloudflared
for a tunnel you already have.
Kubernetes
Run one replica with the token in a Secret and /data on a small volume:
apiVersion: apps/v1
kind: Deployment
metadata: { name: teitunnel }
spec:
replicas: 1
selector: { matchLabels: { app: teitunnel } }
template:
metadata: { labels: { app: teitunnel } }
spec:
containers:
- name: teitunnel
image: teispace/teitunnel:0.2
env:
- name: CLOUDFLARE_API_TOKEN
valueFrom: { secretKeyRef: { name: cloudflare, key: api-token } }
volumeMounts: [{ name: data, mountPath: /data }]
volumes:
- name: data
persistentVolumeClaim: { claimName: teitunnel }Route to Services by their cluster name:
kubectl exec deploy/teitunnel -- teitunnel route add app.yourhost.com http://web.default.svc:80 --yesWeb dashboard and API
teitunnel serve runs the tunnels like up and adds a small web dashboard: every tunnel
and route with its status, adding and removing routes and tunnels (each change reviewed
before it's applied), shares on your domains, and a JSON API with an MCP endpoint.
teitunnel serve --set-password # once; or set TEITUNNEL_WEB_PASSWORD
teitunnel serve # http://127.0.0.1:8765It listens on this machine only. To reach it from elsewhere, publish it through Teitunnel itself with a login:
teitunnel route add admin.yourhost.com http://127.0.0.1:8765 --allow team@yourhost.comor pass --listen 0.0.0.0:8765 --allow-remote behind a TLS proxy, with --secure-cookies.
For automation, create an API key (teitunnel api-key create deploy, shown once) and call
the API with Authorization: Bearer <key>. A change is two calls: POST /api/preview
returns the plan and its fingerprint, and POST /api/apply applies exactly that plan (or
answers 409 if Cloudflare changed in between). Every endpoint is in the
Server API reference.
Watching a server
| Command | Shows |
|---|---|
teitunnel routes / routes --check | Every route's status; --check exits with 1 unless all are live |
teitunnel doctor / doctor --fix | Problems, and the safe fixes (Doctor checks) |
teitunnel uptime | Uptime per route, checked every minute through Cloudflare while up or serve runs |
teitunnel analytics | Edge traffic per route (needs Zone · Analytics · Read) |
teitunnel top --here | A live dashboard in the terminal |
teitunnel export config-yaml | The routes as cloudflared's config.yml |
Things only the app does by itself: deleting Snapshots whose time
is up, and delivering webhook inboxes. On a server, run
teitunnel inbox deliver on a timer.
Temporary shares from a server
teitunnel share 8080 prints a random trycloudflare.com address for as long as the
command runs; --on demo.yourhost.com uses your own domain instead, and --for 2h ends it
by itself.
Troubleshooting
Related
Last updated on
AI agents (MCP)
Let Claude Code, Cursor, VS Code, Codex, Windsurf, Zed, Claude Desktop, Gemini CLI, Copilot CLI, opencode, Kiro, LM Studio or Junie share your dev server, manage Cloudflare Tunnel routes, debug webhooks and fix problems through Teitunnel's MCP server, with your approval for every change.
Several tunnels
Run several Cloudflare tunnels on one machine to keep staging apart from production, each with its own connector, or run a tunnel made elsewhere.