Skip to Content
Teitunnel

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_token

TEITUNNEL_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 status

Add 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 status

Run 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/teitunnel

The 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 teitunnel

The 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 --yes

Web 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:8765

It 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.com

or 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

CommandShows
teitunnel routes / routes --checkEvery route's status; --check exits with 1 unless all are live
teitunnel doctor / doctor --fixProblems, and the safe fixes (Doctor checks)
teitunnel uptimeUptime per route, checked every minute through Cloudflare while up or serve runs
teitunnel analyticsEdge traffic per route (needs Zone · Analytics · Read)
teitunnel top --hereA live dashboard in the terminal
teitunnel export config-yamlThe 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

Edit on GitHub

Last updated on

On this page