NuFiDocs

Cloudflare tunnel

Expose a stack that has no public address, with Cloudflare Access in front of every browser-facing host.

When the server sits inside an office network with no inbound access, or you would rather not run TLS and DNS yourself, a Cloudflare tunnel is the least work. cloudflared on the host opens an outbound connection; Cloudflare receives requests for your hostnames and forwards them down it. No public IP, no firewall change.

This is how NuFi's own on-prem stack in Seoul is reached: five hostnames under codechi.me, four behind Cloudflare Access, the API without. The record of that setup, with every click, is deploy/platform/docs/cloudflare-tunnel-setup.md.

The hostnames

HostnameTargetAccess
chat.example.comlocalhost:3080, the NuFi apprequired
console.example.comlocalhost:3001, NuFi Consolerequired
langfuse.example.comlocalhost:3000required
grafana.example.comlocalhost:3030required
api.example.comlocalhost:4000, the gatewaynone: callers send a gateway key

The ports are the compose stack's host ports. api.example.com stays outside Access on purpose: SDKs and curl authenticate with an Authorization: Bearer key, not a browser session, and Access would block them.

Setting it up

  1. Add the domain to Cloudflare and move its nameservers there.
  2. Turn on Zero Trust. The free tier covers 50 users.
  3. Create the tunnel under Networks → Tunnels, connector type cloudflared, and copy the install command; it carries the token.
  4. Install cloudflared on the host with that command, as a service:
    sudo cloudflared service install <token>
    sudo systemctl status cloudflared
  5. Add the public hostnames in the tunnel's configuration, one per row above, each pointing at http://localhost:<port>. cloudflared runs on the host, so it reaches the compose services through their host ports.
  6. Tell the stack its new addresses. The app, the console and Langfuse put absolute URLs into emails, OAuth callbacks and share links:
    cd deploy/platform
    ./scripts/update-domain.sh example.com --yes
    docker compose up -d
    It rewrites LIBRECHAT_URL, CONSOLE_URL and LANGFUSE_NEXTAUTH_URL in .env to chat., console. and langfuse. under the domain. The app's own DOMAIN_CLIENT and DOMAIN_SERVER are hardcoded to http://localhost:3080 in the compose file; change those two lines by hand.

Access policies

For every hostname except the API: Zero Trust → Access → Applications → Add, type Self-hosted, the hostname, one policy: Allow, include emails ending in your domain. Visitors sign in with their corporate identity at Cloudflare, then meet the app's own sign-in. A stolen app session alone does not reach the host.

Cookies across subdomains

Cloudflare passes Set-Cookie through unchanged, so sharing the session between the chat and the console works the same way as with any proxy: COOKIE_DOMAIN and COOKIE_SAMESITE=lax on the chat service. SSO and reverse proxy.

Limits

  • 100-second responses. The free tier closes a response that streams for longer than 100 seconds. Long generations on slow models hit it; the paid tier raises it.
  • Access and the API. Anything you put behind Access stops answering to non-browser clients. Keep the gateway hostname out.

When it does not work

SymptomCause
404 from Cloudflareno public hostname route, or a typo in it
502 or 521cloudflared reached Cloudflare but not the local port; check the service is up and the port matches
Access sign-in loopsclock skew on the host; sync NTP
a long answer stops at about 100 secondsthe free-tier timeout