Hosting behind a Cloudflare Tunnel (no open ports)¶
Run the Full tier on one machine and publish app., api. and auth. on your domain through a
Cloudflare Tunnel,
without opening any inbound port. Cloudflare terminates TLS; Caddy keeps routing by hostname and keeps
hiding the paths that must not be public (/actuator, the Keycloak admin console and master realm).
Use the Full tier here. The Lite tier runs Keycloak in dev mode on an embedded store and is for localhost only.
What you need¶
- A domain whose DNS is on Cloudflare (nameservers delegated), and
cloudflaredon the host. - Docker with about 8 GiB free for the Full tier's default services (core, web, sidecar, Postgres, Keycloak and its database, PgBouncer, Valkey, SeaweedFS, Caddy).
1. Configure the stack¶
scripts/vegaduta up --full --domain example.com # published release images; add --build to build from source
With VEGADUTA_VERSION empty (a git checkout), the CLI looks up the latest published release and
records it in .env. Then in deploy/compose/.env (the CLI writes BASE_DOMAIN and the PUBLIC_* URLs for you):
CADDY_AUTO_HTTPS=off # Cloudflare terminates TLS
CADDY_SITE_SCHEME=http:// # Caddy serves plain HTTP on :80 for the tunnel to forward to
PUBLIC_BIND_ADDR=127.0.0.1 # Caddy listens on loopback only; the tunnel is the only way in
Both Caddy settings are needed: CADDY_AUTO_HTTPS=off alone leaves the sites expecting TLS on :443 and
nothing listens on :80. ACME_EMAIL may stay empty in this mode. Re-run scripts/vegaduta up after
changing them; the Keycloak reconcile step adds the web clients' redirect URIs derived from
PUBLIC_APP_URL and PUBLIC_ADMIN_URL when missing (existing entries, including ones you added by hand, are kept), so a domain change after first start does not end in
Invalid parameter: redirect_uri.
2. Create the tunnel and DNS routes¶
cloudflared tunnel login # opens a browser: authorise the zone
cloudflared tunnel create my-stage # writes ~/.cloudflared/<id>.json (a secret; never commit it)
for h in app api auth www; do cloudflared tunnel route dns my-stage $h.example.com; done
The bare domain (example.com) needs the same route, but route dns cannot create it when an A/AAAA/CNAME record already exists there: delete or edit that record in the Cloudflare dashboard (DNS → Records) so it is a CNAME to <tunnel id>.cfargotunnel.com, proxied. route dns refuses to replace an existing record; add --overwrite-dns only when you mean to repoint a
hostname (for example from a retired tunnel).
~/.cloudflared/config.yml:
tunnel: <tunnel id>
credentials-file: /home/you/.cloudflared/<tunnel id>.json
ingress:
- { hostname: app.example.com, service: http://localhost:80 }
- { hostname: api.example.com, service: http://localhost:80 }
- { hostname: auth.example.com, service: http://localhost:80 }
- { hostname: example.com, service: http://localhost:80 } # redirects to app.
- { hostname: www.example.com, service: http://localhost:80 }
- service: http_status:404
All hostnames go to Caddy on port 80; it picks the upstream from the Host header. Add admin. (and
codeintel., builder., livekit. when those profiles run) the same way. Start it with
cloudflared tunnel run my-stage, or as a service so it survives a reboot.
3. Verify¶
| Check | Expect |
|---|---|
https://app.example.com |
200, the console |
https://api.example.com/api/system/status/live |
200 |
https://auth.example.com/realms/agentic-ai/.well-known/openid-configuration |
200, issuer is https://auth.example.com/realms/agentic-ai |
https://api.example.com/actuator/health |
404 (hidden by Caddy) |
https://auth.example.com/admin/ and /realms/master/ |
404 (reach the admin console on the loopback port, or over an SSH tunnel) |
| Sign in at the console | redirected to auth.example.com, no redirect_uri error |
Troubleshooting¶
| Symptom | Cause |
|---|---|
| 530 on every hostname | cloudflared is not running |
| Error 1033 on one hostname | That hostname's DNS still points at another (old/stopped) tunnel; re-route it. 1033 on all of them: the host slept or cloudflared stopped; keep the machine awake (caffeinate -is) or run cloudflared as a service |
| 502 on every hostname | Tunnel is up but nothing answers on localhost:80: Caddy is down or still serving TLS only (CADDY_SITE_SCHEME unset) |
| Caddy restarts every minute | An old Caddyfile with an empty ACME_EMAIL; update to a release that has the optional email directive |
Invalid parameter: redirect_uri |
The realm was imported with other PUBLIC_* values; re-run scripts/vegaduta up (reconcile) |
| 401 from the API after sign-in | Core must accept the public issuer: KEYCLOAK_ISSUER_URI is derived from BASE_DOMAIN; list any older issuer in KEYCLOAK_ADDITIONAL_ISSUERS |
Email (verification, resets) needs KEYCLOAK_SMTP_*; without them keep KC_VERIFY_EMAIL=false.