Caddy
The Caddy image Belune ships, which features need extra modules, and what Belune requires of a Caddy you run yourself.
Belune puts every domain behind Caddy and configures it through Caddy's admin API. Which Caddy you run decides which optional features are available. Nothing else changes.
The Default Image
Belune's stack runs ghcr.io/belune-hq/belune-caddy:2: upstream Caddy plus seven optional modules (rate limiting, the Cloudflare, Route 53 and DigitalOcean DNS providers, a WAF, CrowdSec, and layer 4 proxying). Today only rate limiting is something you can use from the dashboard; the rest ship ahead of their features so that turning them on later doesn't mean replacing the proxy again.
You don't have to use it. Stock upstream caddy:2-alpine is a supported setup, not a degraded one: every core function (routing, automatic and custom TLS, basic auth, headers, IP allowlists, redirects, path routing) works the same. Set BELUNE_CADDY_IMAGE in .env to switch, and run update.sh (or docker compose up -d) to recreate the proxy. See the configuration reference for the variable.
Recreating the Caddy container, which is also what upgrading to 0.2.0 does, makes your domains briefly unreachable while the new container starts.
Features That Need a Module
Belune asks the running Caddy which modules it has (caddy list-modules, run inside its container) and checks again whenever the container is recreated. It never guesses from the image name.
| Feature | Module it needs |
|---|---|
| Rate Limit | http.handlers.rate_limit |
When the module is missing the feature is not configured, never an error:
- You can't enable it, and the type is shown disabled in the domain's route features with the reason.
- A feature you saved earlier stays stored but shows as Not active, and the domain keeps serving without it. Switch back to an image with the module and it applies on the next reconcile, with no re-entry.
- If Belune can't reach Caddy to ask (for instance while it restarts), it makes no claim either way: you can still save the feature, and a domain is only ever served with a feature the running Caddy is known to have.
Bring Your Own Caddy
Belune manages Caddy entirely through its admin API; it never writes a Caddyfile. A Caddy that works with Belune needs:
- The admin API reachable from Belune, at
CADDY_ADMIN_URL(defaulthttp://localhost:2019; the bundled stack setshttp://caddy:2019). Keep it off public interfaces: it can reconfigure everything. - A single HTTP server named
srv0. Every route is written tosrv0by name. A Caddyfile with one:80site block produces it; a:80, :443block would split intosrv0andsrv1and strand the routes on the wrong one. Belune sets the listeners to:80and:443, the automatic-HTTPS policy and the TLS connection policy itself. - A Docker container Belune can find. It looks for the label
belune-system=caddy, or the name inCADDY_CONTAINER_NAME(defaultinfra-caddy-1), which is the way to name one that doesn't carry the label. Belune runscaddy list-modulesinside it and joins it to each project's Docker network so it can reach your apps. A Caddy outside Docker can still be routed to, but you take over the networking, and because Belune can't ask it for its modules, module-backed features are left off. - The standard handlers Belune emits, all part of upstream Caddy:
reverse_proxy,rewrite,static_response,subroute,headersandauthentication(HTTP basic), with the host, path and remote IP matchers; theload_pemcertificate loader; and the file log writer with the filter encoder. Any upstream build has them. - The access log volume. Belune writes a JSON access log to
/var/log/caddy/access.loginside the Caddy container and reads it from its own, so both mount the same volume there. The file holds request entries only, and the secrets that travel in request URLs (a deploy hook token, and the?token=of a password-reset or invitation link) are replaced with[REDACTED]before they are written. Caddy's own container log, which the Maintenance panel shows, carries diagnostics such as certificate errors but no request lines; requests are in each application's request log.
Belune treats its own routes as the source of truth and re-asserts them every 30 seconds, so a host route you add to Caddy by hand will be removed. Optional modules are a bonus, never a requirement.
The Proxy Page
Admins get a Proxy page in the sidebar, with a tab each for Caddy, CrowdSec and the WAF. The Caddy tab shows what Belune can tell about the proxy itself: which container it is and how that container was found, whether the admin API answers, the version and image, the loaded modules, whether the request log is being read, how much traffic Caddy is proxying to apps and how fast, and how the live routes compare with the database.
Every line separates "could not be determined" from "not there". If Belune cannot list Caddy's modules because the proxy is restarting, the page says the modules are unknown rather than that they are missing. When Caddy runs outside Docker there is no container to inspect, and the page says so instead of reporting an error. The CrowdSec tab reports whether your Caddy includes the module; its controls are not part of this release. The WAF tab is described below.
The traffic figures come from Caddy's own metrics, so they cover requests that reached an app and not redirects or the dashboard itself. They need servers { metrics } in the Caddyfile, which Belune's stack already sets. A Caddy you run yourself needs the same.
The WAF
The WAF tab turns on a web application firewall in front of your applications, and it only detects. Nothing is blocked. With it on, requests to your applications are checked against the OWASP Core Rule Set by the http.handlers.waf module (Coraza) that the Belune Caddy image carries, and each rule that matches is recorded so you can see what would have been blocked. It is off until you turn it on, and the switch covers every application at once.
What to know before turning it on:
- It inspects your applications, not the dashboard. The dashboard's own API takes commands, scripts and credentials as ordinary input, which a generic rule set reads as attacks.
- Large uploads are never touched. Request bodies over 128 KiB, or of unknown length, are not inspected, so a payload inside one goes unseen. The module's defaults were measured to stall and truncate bodies over about 1 MiB even in detection-only mode, so this is what keeps uploads working. URLs, headers, query strings and smaller bodies are inspected.
- Only the rule, severity, client address and host are stored. The request, its URL and the matched text are not, because they can carry your users' credentials. Each row is one rule matching one request, so a single request can produce several.
- False positives are shared. One noisy application fills the same detection log as the rest. That is tolerable while nothing is blocked, and it is the reason blocking, when it arrives, will be per application.
Detections are kept for seven days and at most 200,000 are kept, whichever limit is reached first, so an attack cannot fill the disk. The retention is a setting on the Server page. If events are lost, because Belune was too busy to record them or a log line did not have the expected format, the tab says so rather than showing a count that quietly reads low.
The three states of "does this Caddy have the module" stay separate here too. If Belune cannot list Caddy's modules, the tab says it cannot tell yet and still lets you turn the switch on, which then takes effect once Belune can ask. Only a Caddy that is known to lack the module is refused. With stock Caddy the WAF is simply not available, and everything else keeps working.
The rule matches also appear in the proxy's log on the Server page. Because they carry the matched text, that view shows each one as a summary, the rule, the client and the host, with the payload and URL withheld. The unredacted lines stay in Docker's own log for the proxy container on the host.
The page is read-only. Belune owns the route table and rewrites anything it does not recognise within thirty seconds, so Caddy's configuration cannot be edited here. The running configuration can be viewed, with private keys and password hashes removed on the server. Reconcile Routes stays on the Server page, beside the other maintenance actions.