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 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.
Add the domain to Cloudflare and move its nameservers there.
Turn on Zero Trust. The free tier covers 50 users.
Create the tunnel under Networks → Tunnels, connector type
cloudflared, and copy the install command; it carries the token.
Install cloudflared on the host with that command, as a service:
sudo cloudflared service install <token>sudo systemctl status cloudflared
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.
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 --yesdocker 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.
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.
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.
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.