One step setup (Traefik, including Coolify)
When the proxy is Traefik with a file provider directory, Levelrail can manage the routes itself: one file per domain in the directory Traefik watches, rewritten on every reconcile pass and removed when the domain goes away.setup --confirm and
proxy verify therefore wait (up to APP_PROXY_VERIFY_TIMEOUT, default 8s)
for the router to show up, and a route that is still not loaded is reported as
the pending step “waiting for the proxy to load the route”, not as an error.
Run proxy verify again once Traefik has reloaded.
Setup, in order:
- Detect. It reads the container publishing host port 80 or 443 through the
Docker API (no
dockerCLI): its arguments, mounts, published ports and extra hosts. A mountedtraefik.ymlis read instead of the arguments when Traefik loads one. Nothing is defaulted: anything it cannot determine is listed and setup stops with409and the exact gap. - TLS upstream. It turns on
tls_terminated_upstreamand setspublic_https_portto the host port the proxy publishes for HTTPS. This instance stops requesting certificates and stops redirecting HTTP itself. - Routes. It saves the detected values and writes the route files.
- Verify. For every route it runs a TLS handshake to this server’s public
address with the domain as SNI (issuer and expiry of the certificate the
proxy serves), then a request with the domain as
Host. When Traefik’s API is published without authentication, it also asks Traefik whether the router is loaded.
levelrail-cli proxy disable, which removes the files it wrote and keeps everything else.
What is detected on Coolify
Coolify startscoolify-proxy with:
Each domain gets
/data/coolify/proxy/dynamic/levelrail-managed-<domain>.yaml:
an HTTPS router with the certificate resolver, an HTTP router that redirects to
HTTPS, and a service pointing at the ingress HTTP port (app domains) or the
dashboard listener (the primary domain). Coolify lists these files next to its
own coolify.yaml and default_redirect_503.yaml; leave them to Levelrail.
When the listener is on loopback
The proxy runs in a container, so it can’t reach a listener bound to127.0.0.1. Setup then stops with upstream_unreachable and the exact
drop-in, for example:
systemctl daemon-reload && systemctl restart levelrail. That address is
internal to the host, not reachable from the internet.
Safety model
The control plane writes into a directory another product owns, so it is strict about what it touches:- Only files named
levelrail-managed-*.yamlwhose first line is# managed by levelrail. A file with the prefix but without the header is reported and never overwritten or deleted. - Symlinks are never followed; the directory is resolved once and re-checked before every change, and a directory that resolves outside its parent is refused.
- Writes go to a temporary file in the same directory, then
rename, mode0644, so Traefik never reads a half-written file. - Domains are validated strictly: no wildcards, ports, paths or separators, and a domain that would need rewriting to become a file name is rejected.
- If a route cannot be rendered (for example the upstream became unreachable), the existing file is kept rather than deleted.
- Every write and removal is audited as
proxy_route.writtenandproxy_route.removed. - If the control plane cannot write the directory (another user, a read-only
mount or a systemd sandbox), setup reports
not_writablewith the uid, the owner and mode of the directory, or theReadWritePaths=line to add.
Settings
PUT /api/v1/settings/proxy-integration sets them by hand, for a Traefik whose
directory is not on a host mount detection can see, or with a TOML static file:
levelrail-cli proxy setup --dynamic-dir DIR covers the common case of a
directory detection could not map.
From the dashboard
Open Domains, then “Put the dashboard behind your existing proxy”. Enter the domain, check the detected proxy, and copy the configuration. “Check it works” resolves the domain, confirms it points at this server, and requests/healthz over HTTPS.
From the CLI
What it generates
If the proxy runs in a container and the dashboard listens on loopback, the
guide also gives a systemd drop-in that moves the dashboard to that network’s
gateway address. That address is internal to the host, not reachable from the
internet, and a drop-in survives upgrades.
Certificates once a proxy owns TLS
When TLS is terminated by the proxy, the platform reports one certificate state for each domain: the certificate the proxy actually serves (issuer, expiry, “renews automatically”), read from the last route verification. Nothing that this server stored earlier is shown or alerted on:- A stale certificate request failure from an earlier attempt, the “another service answers port 80” guidance and the DNS-01 provider prompt are hidden, and the recorded failure is cleared when you enable proxy mode.
- The internal certificate this server stored before the proxy took over is not
listed under Settings > Certificates, in the security checks or in the
domain doctor, and
cert_expiring,cert_expiryandcert_renewal_stalledalert rules ignore it. GET /api/v1/domainsandGET /api/v1/certificatescarry atls_ownerfield (acme,internal,proxy,custom);levelrail-cli domains certificatesprints it as the OWNER column.
levelrail-cli proxy --verify).
Links and the public HTTPS port
When a proxy owns 80 and 443 and forwards to this ingress on its own ports (for example8088 and 8443), the certificate lives on the proxy and clients
reach the proxy on 443. Set Public HTTPS port under Domains > Platform
ingress (or levelrail-cli settings ingress set --public-https-port 443) so
every link the platform builds (automatic URLs, preview URLs, emailed links)
uses the port clients actually use. 443 produces links with no port suffix,
any other value is appended, and 0 keeps the ingress listen port.
Several instances behind one proxy
Each instance can’t own 80 or 443 and must not run ACME. Give every instance its own ingress ports, let one proxy hold 80 and 443, and turn on TLS is terminated by a proxy in front (--tls-terminated-upstream) on each:
https links with the public port (443 when unset),
and does not redirect HTTP itself because the proxy does. Set
APP_INGRESS_TRUSTED_PROXIES to the proxy’s address range so X-Forwarded-Proto
and X-Forwarded-Host are honoured only from it, never from the open internet.
The dashboard shows “HTTPS is handled by your proxy” instead of a certificate
error. DNS for each domain still points at the proxy host.
Traefik, one host rule per domain and one backend port per instance:
APP_INGRESS_HTTP_ADDR=:8088, instance B with
APP_INGRESS_HTTP_ADDR=:8089. Forward to the HTTP port: TLS ends at the proxy.
API
GET /api/v1/system/reverse-proxy?domain=D&proxy=traefik&verify=true, or
?app=NAME for an app’s domain.
Managed routes: GET /api/v1/system/proxy-integration (detection, settings,
ingress, per-domain state and the setup checklist), POST .../setup with
{"confirm": true}, POST .../apply, POST .../verify?domain=D, and
PUT /api/v1/settings/proxy-integration. Reading needs the read ability,
every write needs root. APP_PROXY_VERIFY_TIMEOUT bounds each probe
(default 8s).
The port and mode above are public_https_port and tls_terminated_upstream on
PUT /api/v1/settings/ingress.
The check only probes a domain that resolves to this server’s own public
address.