Proxy and TLS
Clouduler runs a built-in reverse proxy on every node. You publish a web app by adding a route to its stack: a hostname, a network and a container port. Clouduler answers on ports 80 and 443, forwards the request to whichever node runs the container, and gets a Let's Encrypt certificate for the hostname automatically.
This page covers exposing a service, certificates, network gating, allowed hosts and the Proxy views of the web UI. Publishing the hostname in public DNS is covered in DNS publishing.
How it works
- Every node listens. Each node runs the HTTP (80) and HTTPS (443) listeners. The routing table is built from the cluster's replicated state, so any node can answer for any hostname and forwards to the node that runs the container. That is why you can point DNS at any of your nodes.
- Routes come from stacks. A service declares
x-clouduler.proxyroutes. The table is rebuilt every 5 seconds. - Certificates are issued by the leader only. The current raft leader asks Let's Encrypt for certificates, stores them in raft, and every node serves them. A leader change moves issuance. A certificate is renewed when it has 30 days left. Until one arrives, a node serves a self-signed fallback certificate and the browser shows a warning. The issuance pass runs about every 5 minutes, so a brand new hostname can wait up to that long.
- Network gating. A route is published on one of your networks. If that network is a subnetted one (internal or private), the proxy answers
403unless the connecting machine is inside that subnet. A route on a public network is open to everyone. - Matching. An exact hostname match is tried first, then a one-label wildcard such as
*.example.com. The longest path prefix wins. With several backends the proxy rotates between them (round-robin). Sticky sessions use thecl_stickycookie.
Expose a service over HTTPS
- Web UI
- CLI / config
Routes are part of a stack, so you add them in the stack editor, then check the result in the Proxy view.
- Make sure the network you publish on has a DNS domain (Settings > Network > Networks). A route on a network without a domain fails validation with
network X has no domain, so it cannot serve an HTTP route. - Turn on certificate issuance once: Settings > Network > Reverse proxy, switch on Issue certificates automatically (needs Cloudflare DNS below), and set a Contact email.
- Add a route to the service in your stack (see the CLI tab for the YAML) and deploy.
- Open Proxy (
/proxy). The Routes tab shows every route the proxy serves right now: host, path, stack, service, port, network, TLS, websockets, expiry and health. The TLS column reads{n}d left,onoroff.
An empty table says Nothing is published yet. A banner reads n route(s) return a bad gateway with the hint Check that the service is running and listening on its port. when a backend is down. The tag 502 on <node> names the node that failed.
Add a route under the service in the stack file. host is a bare label, so it gets the network's domain appended:
services:
vaultwarden:
image: vaultwarden/server:1.32.0
networks: [wan]
x-clouduler:
proxy:
- host: vault # becomes vault.example.com on the wan network
network: wan # the network that decides who may connect
port: 80 # container port
websockets: true
clouduler stack deploy vaultwarden.yaml
clouduler status # "certificates & dns" section shows pending or ERROR
Turn on issuance in the cluster config:
cloudflare:
zones:
- example.com
proxy:
enabled: true
acme:
enabled: true
directory_url: https://acme-staging-v02.api.letsencrypt.org/directory # staging while testing
Then clouduler config reload. Remove directory_url to use the production Let's Encrypt directory once staging works.
The CLI has no command for the Cloudflare token, certificate renewal, DNS reconcile, redirects or static routes. Use the web UI for those.
Route options
These keys live in each x-clouduler.proxy entry. The complete stack file is described in the stack reference. A route change applies when you redeploy the stack.
hostrequiredstringno defaultnetworkrequiredstringno defaultbooldefault truebooldefault truesizedefault no limitdurationdefault 30sbooldefault falsebooldefault falselistno defaultCORS cannot combine credentials with a wildcard origin, and header names and values are validated at deploy time.
Certificates
Clouduler uses ACME (Let's Encrypt by default). There are two ways an ACME server can check that you own a hostname:
| Method | How it proves ownership | Needs |
|---|---|---|
| HTTP-01 | The CA fetches a token from http://host/.well-known/acme-challenge/... on port 80. | The hostname reachable from the internet on port 80. Cannot issue wildcards. |
| DNS-01 | Clouduler writes a _acme-challenge TXT record (TTL 60) through the Cloudflare API and removes it afterwards. | A Cloudflare API token. Works for hosts that are not reachable from the internet, and for wildcards. |
Clouduler's validation states that certificate issuance is DNS-01 only: proxy.acme.enabled is refused without a cloudflare section. Every name inside one of your configured Cloudflare zones is issued with DNS-01. HTTP-01 is an opt-in fallback used only for a route host written with a trailing dot (host: foo.example.net.) that no zone covers. Ordinary hostnames never use HTTP-01. The trailing dot is otherwise stripped, and @ means the zone apex. Either way the whole ACME pass needs the Cloudflare zone list and token, even for HTTP-01. Without a token the daemon logs no cloudflare token yet, ... will not run.
Connect Cloudflare
- Web UI
- CLI / config
- Create a token in Cloudflare with Zone, DNS and Edit permissions on your zone. The step-by-step is in the Cloudflare API token guide.
- Open Settings > Network and the Cloudflare DNS page. Switch on Manage DNS with Cloudflare.
- Under DNS zones list your zone, comma separated (for example
example.com). - Paste the token into API token and press Save token. Later use Rotate token. It is sealed and stored as soon as you press Save.
- Apply the page. Network domains and the dashboard hostname must all sit inside a listed zone.
The token is saved without being checked: a wrong token is only found when the first Cloudflare call fails with cloudflare auth failed.
The zone list is part of the config. The token is a sealed secret named _clouduler_cloudflare_token and has no CLI command; set it in the UI.
cloudflare:
zones:
- example.com
Wildcard certificates
A route host such as *.apps.example.com matches exactly one label (a.apps.example.com, not a.b.apps.example.com). Wildcards need DNS-01, which is why the Cloudflare setup is required. A wildcard route host gets one wildcard certificate (not one per concrete hostname), and a request for a.apps.example.com is served from it.
Check and renew certificates
- Web UI
- CLI / config
Open DNS (/dns). The Certificates panel lists every hostname with a tag: pending, error: {message}, {n} days left, expired or live. A banner n certificates could not be issued appears when issuance fails. Reconcile DNS runs the DNS and certificate passes immediately.
To force a new certificate, use the renew action on the row. The dialog titled Renew <fqdn>? warns: "The current certificate is removed before a new one is requested, so the host may briefly lack TLS." The self-signed fallback can then be served for up to about 5 minutes.
clouduler status prints pending and failed certificate and DNS work:
certificate app.example.com: pending
dns app.example.com: ERROR 2m ago: cloudflare auth failed: ...
Renewal and reconcile have no CLI command. They are API calls (POST /api/certificates/{fqdn}/renew, POST /api/dns/reconcile).
Network gating: why you get a 403
A route on an internal or private network is private by design. The proxy compares the real TCP peer address (never X-Forwarded-For) with the network's subnet. If the visitor is outside it, the answer is:
403: this route is only reachable from its own network
Two other sources also pass: the overlay subnets of stacks published on that route's network, and every nat network's subnet (so a container on the same node reaching the route through the host is not blocked). There is no special case for Tailscale or Headscale; to reach a private route over a VPN, the VPN address must be inside the network's subnet. A stack whose allowed: list does not include the host gets 403: stack X may not reach H through network N: it is not in the network's allowed list. A route on a public network is never gated.
To publish a service on the internet, put its route on a public network. To keep it LAN-only, use a private network and expect the 403 from outside. See Networking.
Allowed hosts
A stack can limit what its containers may reach with allowed:. A hostname entry is enforced at the proxy: a blocked request gets 403. A host:port entry is enforced by the firewall instead. See the stack reference and Networking.
Headers and websockets
The proxy adds X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host to the request and removes hop-by-hop headers. It adds no header naming the logged-in user, even with the authenticated middleware (see Access and login). Use the route's headers middleware to add your own.
HTTPS negotiates HTTP/1.1 only; HTTP/2 is not offered. Websockets are allowed by default.
Redirects and static backends
- Web UI
- CLI / config
On Proxy, the Redirects tab sends a hostname to another URL (New redirect: host, target, status 301, 302, 307 or 308). Static backends publishes something Clouduler does not run, like another box on your LAN (New static backend: host and host:port). Both apply immediately, with no restart. The Ingress rules tab is for WireGuard ports (see Networking).
No CLI. Use the web UI or the HTTP API (/api/redirects, /api/redirects/{id}, /api/static-routes, /api/static-routes/{id}; all admin CRUD).
Redirect errors you may see: host must not be empty, target must not be empty, status must be 301, 302, 307 or 308, got N. Static backend errors: backend must be host:port, N is not a valid port, no network named X, no node named X.
Options
Cluster-level proxy settings in cluster.yaml.
portdefault 80portdefault 443enumdefault all-nodesbooldefault falselist of network namesdefault [] (every subnetted network the node has an address on)portdefault 53booldefault falseURLdefault Let's Encrypt productionstringdefault noneQuirks and gotchas
Related
- DNS publishing
- Access and login for the
authenticatedmiddleware - Networking
- Cloudflare API token
- Ports and firewall
- cluster.yaml