Skip to main content

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.proxy routes. 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 403 unless 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 the cl_sticky cookie.

Expose a service over HTTPS​

Routes are part of a stack, so you add them in the stack editor, then check the result in the Proxy view.

  1. 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.
  2. Turn on certificate issuance once: Settings > Network > Reverse proxy, switch on Issue certificates automatically (needs Cloudflare DNS below), and set a Contact email.
  3. Add a route to the service in your stack (see the CLI tab for the YAML) and deploy.
  4. 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, on or off.
/proxy
Proxy, Routes: what the proxy serves right now.

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.

/settings/network?page=proxy
Settings, Network, Reverse proxy.

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.

Option
Type / default
Reload
Description
hostrequired
stringno default
n/a
Hostname the route answers. Lowercased; a bare label gets the network's domain; @ means the domain itself; a wildcard covers one label (*.example.com).
Watch out: Trailing dot is stripped.
networkrequired
stringno default
n/a
Declared network the route is published on. Decides the domain and the network gate.
Watch out: The network must exist and have a domain.
portrequired
u16no default
n/a
Container port the proxy forwards to.
stringno default
n/a
Only requests under this path match the route.
booldefault true
n/a
Use a certificate for the route.
Watch out: A route with tls: false is still served on the HTTPS listener too.
booldefault true
n/a
Let Upgrade requests through. Set false to refuse them.
Watch out: When refused the proxy answers 400 connection upgrades are not enabled for this route. The research file listed the default as false; the code default is true.
sizedefault no limit
n/a
Largest request body, for example 10MB. Bigger requests get 413 and never reach the container.
Watch out: Set it on anything exposed to the internet.
durationdefault 30s
n/a
How long the backend may take to respond before 504.
Watch out: Error text: ADDR did not respond within LIMIT. The doc comment in the code says an unset value waits forever; the proxy applies 30s.
booldefault false
n/a
Forward the visitor's Host header instead of the backend's address.
Watch out: Turn it on for apps that build absolute URLs.
booldefault false
n/a
Publish the hostname with Cloudflare's proxy (orange cloud) on.
Watch out: Public networks only. On only if every route behind the host sets it; any route, static route or redirect on the same host without it keeps the record DNS-only.
boolno default
n/a
Pin a visitor to one backend with the cl_sticky cookie.
listno default
n/a
Named middlewares: redirect_https, strip_prefix, redirect_regex, headers, cors, authenticated.
Watch out: redirect_https is accepted but redundant: the HTTP listener already redirects (308) to HTTPS.

CORS 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:

MethodHow it proves ownershipNeeds
HTTP-01The 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-01Clouduler 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​

  1. 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.
  2. Open Settings > Network and the Cloudflare DNS page. Switch on Manage DNS with Cloudflare.
  3. Under DNS zones list your zone, comma separated (for example example.com).
  4. 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.
  5. 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.

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​

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.

/dns
DNS: certificates, DNS records and the TLS fingerprint.

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.

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​

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).

/proxy/redirects
Proxy, Redirects.
/proxy/static
Proxy, Static backends.

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.

Option
Type / default
Reload
Description
booldefault true
live reload
Runs the reverse proxy on every node.
portdefault 80
live reload
Plain HTTP listener. Serves tls: false routes, ACME HTTP-01 challenges and redirects everything else to HTTPS. Below 1024 needs root.
Watch out: Must differ from https_port, web.port and web.backend_port.
portdefault 443
live reload
TLS listener for routes.
Watch out: Error if equal to the dashboard port: proxy.https_port: web.port already listens on PORT.
enumdefault all-nodes
live reload
all-nodes: every node's address is published for every proxied hostname. owning-node: only the nodes running the containers are published, stale records are deleted and the scheduler co-locates containers that share a hostname.
Watch out: No UI control; edit the file. See DNS publishing.
booldefault false
live reload
Turns on the cluster DNS server that answers by the client's network.
Watch out: Re-read every 5 seconds.
list of network namesdefault [] (every subnetted network the node has an address on)
live reload
Networks to listen on, at the node's address on each. A public network is served only when listed.
Watch out: Errors: no network named NAME; NAME has no node address to listen on.
portdefault 53
live reload
UDP and TCP port of the cluster DNS server. Below 1024 needs root.
Watch out: Must not be 0.
booldefault false
live reload
Issue and renew certificates through ACME. Issuance is DNS-01 through Cloudflare.
Watch out: Needs a cloudflare section: proxy.acme.enabled: certificate issuance is DNS-01 only, so it needs a cloudflare section.
URLdefault Let's Encrypt production
live reload
ACME directory. Point at staging while testing.
Watch out: Not validated; a malformed URL is found only when a certificate is requested.
stringdefault none
live reload
Address registered with the CA for expiry warnings.
Watch out: Not validated.

Quirks and gotchas​