From e5a1395c120178a4126dbc0da00bf0cb041f4070 Mon Sep 17 00:00:00 2001 From: Goyban Date: Wed, 19 Aug 2026 15:54:37 +0200 Subject: [PATCH] added cloudflare-tunnel --- README.md | 1 + cloudflare-tunnel/.env.example | 20 +++++++++ cloudflare-tunnel/README.md | 82 ++++++++++++++++++++++++++++++++++ cloudflare-tunnel/compose.yaml | 21 +++++++++ docs/cloudflare-tunnel.md | 10 +++-- 5 files changed, 131 insertions(+), 3 deletions(-) create mode 100644 cloudflare-tunnel/.env.example create mode 100644 cloudflare-tunnel/README.md create mode 100644 cloudflare-tunnel/compose.yaml diff --git a/README.md b/README.md index 5aa3dc2..6910050 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ handled separately — see [Exposing services](#exposing-services). | Service | Port | Notes | |---------|------|-------| +| [cloudflare-tunnel](cloudflare-tunnel/) | — | Outbound connector. Exposes other services without opening a port | | [jellyfin](jellyfin/) | 8096 | Media streaming. Also 7359/udp for auto-discovery | | [openwebui](openwebui/) | 3033 | LLM chat UI, talks to an external model backend | | [owui-ollama](owui-ollama/) | 3034 | Open WebUI bundled with Ollama | diff --git a/cloudflare-tunnel/.env.example b/cloudflare-tunnel/.env.example new file mode 100644 index 0000000..f69b850 --- /dev/null +++ b/cloudflare-tunnel/.env.example @@ -0,0 +1,20 @@ +# Copy to .env and adjust. .env is git-ignored; this file is not. +# NEVER put the real token in this file. + +# ─── Required ─────────────────────────────────────────────────────── +# Cloudflare Zero Trust tunnel token. +# +# Dashboard -> Zero Trust -> Networks -> Tunnels -> (your tunnel) -> Configure +# https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/ +# +# The dashboard shows a pre-filled `docker run ... --token ` command. +# Copy only the token itself, not the whole command. +# +# Treat it like a password: anyone holding it can route traffic into your +# network. If it leaks, delete the tunnel in the dashboard and make a new one +# — the token cannot be rotated on its own. +TUNNEL_TOKEN= + +# ─── Shared ───────────────────────────────────────────────────────── +# Defaulted to Etc/UTC in compose.yaml. Only affects log timestamps here. +#TZ=Europe/Paris diff --git a/cloudflare-tunnel/README.md b/cloudflare-tunnel/README.md new file mode 100644 index 0000000..58cc337 --- /dev/null +++ b/cloudflare-tunnel/README.md @@ -0,0 +1,82 @@ +# Cloudflare Tunnel + +The `cloudflared` connector. It dials **out** to Cloudflare and Cloudflare +routes public traffic back down that connection — so a service reaches the +internet with no port forwarded, no static IP and no VPS. + +One tunnel serves every service; you don't need a copy of this per app. + +## Quick start + +```bash +cp .env.example .env +$EDITOR .env # paste TUNNEL_TOKEN +docker compose up -d +``` + +Get the token at **Dashboard → Zero Trust → Networks → Tunnels →** *(your +tunnel)* **→ Configure**. The page shows a pre-filled `docker run … --token +` command — copy only the token part. + +Docs: + +Then add routes in the same dashboard: **Public Hostname → Service**, e.g. +`gitea.example.com → http://host.docker.internal:3000`. + +## Ports + +**None.** Nothing listens on the host — that's the entire point. If you find +yourself adding a `ports:` block here, something has been misunderstood. + +## Why it looks like this + +**The token lives in `.env`, and compose refuses to start without it.** It's +written `${TUNNEL_TOKEN:?…}` rather than with a default, because a +blank token gives you a container that starts, retries, and quietly routes +nothing. The token is a genuine credential — it authorises routing traffic +into your network — so it never belongs in a committed file. + +**No `PUID`/`PGID`.** Unlike most services here, this isn't a LinuxServer +image and doesn't understand those variables. It writes nothing to disk and +needs no volumes, so there's no ownership to get right. `TZ` is kept only +because it affects log timestamps. + +**`extra_hosts: host.docker.internal:host-gateway`.** Routes are configured in +the dashboard, and the connector has to resolve whatever hostname you put +there. This line lets you point at services published on the Docker host +without hardcoding the LAN IP — which would otherwise break the day your +router hands out a different lease. + +**`--no-autoupdate`.** The binary can update itself in place, which means the +thing routing your traffic changes without you doing anything. Pinning that +off makes updates a deliberate `docker compose pull`. + +**No config file, no volume.** This is the remotely-managed style of tunnel: +routing lives in the Cloudflare dashboard, not on disk. The trade is that your +routing config isn't in this repo — the alternative (`cloudflared tunnel +create` plus a mounted `config.yml`) is version-controllable but more setup. + +## Gotchas + +- **Don't put video through it.** Cloudflare's terms restrict serving large + non-HTML content, and streaming a media library is the classic way to get an + account flagged. Jellyfin belongs behind your own reverse proxy. +- **~100 MB upload cap** on the free plan. Photo backup and file sync break on + this, sometimes with nothing clearer than a `413`. +- **The token can't be rotated.** If it leaks, delete the tunnel and create a + new one. +- **A dead tunnel looks like a DNS problem.** If a hostname stops resolving, + check `docker compose logs cloudflared` before suspecting Cloudflare. + +## Related + +- [When to use a tunnel, and when not to](../docs/cloudflare-tunnel.md) — the + longer write-up, including the comparison against a VPS reverse proxy. +- [Reverse proxy with Caddy](../docs/reverse-proxy.md) +- [Single sign-on with Authentik](../docs/authentik-sso.md) — note Cloudflare + Access can also sit in front of a tunnel; running both is usually redundant. + +## Links + +- Connector docs: +- Image: diff --git a/cloudflare-tunnel/compose.yaml b/cloudflare-tunnel/compose.yaml new file mode 100644 index 0000000..645307c --- /dev/null +++ b/cloudflare-tunnel/compose.yaml @@ -0,0 +1,21 @@ +services: + cloudflaretunnel: + image: cloudflare/cloudflared:latest + container_name: cloudflare-tunnel + restart: unless-stopped + command: tunnel --no-autoupdate run + + environment: + # Only affects log timestamps here — nothing is written to disk. + - TZ=${TZ:-Etc/UTC} + # A real secret: it grants the ability to route traffic into your network. + # Dashboard -> Zero Trust -> Networks -> Tunnels -> (tunnel) -> Configure + - TUNNEL_TOKEN=${TUNNEL_TOKEN:?get it from Cloudflare Zero Trust > Networks > Tunnels > Configure} + + # No `ports:` and no PUID/PGID on purpose — see the README. + + # Lets dashboard routes reach services published on the Docker host, e.g. + # gitea.example.com -> http://host.docker.internal:3000 + # Drop this if you route to containers by name on a shared network. + extra_hosts: + - host.docker.internal:host-gateway diff --git a/docs/cloudflare-tunnel.md b/docs/cloudflare-tunnel.md index 96f89af..d015be3 100644 --- a/docs/cloudflare-tunnel.md +++ b/docs/cloudflare-tunnel.md @@ -33,15 +33,19 @@ TODO: your preferred flow — dashboard-created tunnel vs. `cloudflared tunnel create`. The dashboard route is easier to show on video; the CLI route is easier to keep in version control. +The connector itself is a service in this repo — see +[`cloudflare-tunnel/`](../cloudflare-tunnel/) for the compose file, the token +setup and the gotchas. In short: + ```yaml services: - cloudflared: + cloudflaretunnel: image: cloudflare/cloudflared:latest - container_name: cloudflared + container_name: cloudflare-tunnel restart: unless-stopped command: tunnel --no-autoupdate run environment: - - TUNNEL_TOKEN=${CLOUDFLARE_TUNNEL_TOKEN} + - TUNNEL_TOKEN=${TUNNEL_TOKEN:?} ``` The tunnel token is a **real secret** — it grants the ability to route