Skip to main content
On a server that already runs Coolify, Dokploy or your own nginx, something else owns ports 80 and 443. Levelrail doesn’t fight it. It detects what holds the ports and generates the configuration to put the dashboard behind it.

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.
Traefik loads a written file a moment after it appears. 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:
  1. Detect. It reads the container publishing host port 80 or 443 through the Docker API (no docker CLI): its arguments, mounts, published ports and extra hosts. A mounted traefik.yml is read instead of the arguments when Traefik loads one. Nothing is defaulted: anything it cannot determine is listed and setup stops with 409 and the exact gap.
  2. TLS upstream. It turns on tls_terminated_upstream and sets public_https_port to the host port the proxy publishes for HTTPS. This instance stops requesting certificates and stops redirecting HTTP itself.
  3. Routes. It saves the detected values and writes the route files.
  4. 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.
Running setup again changes nothing. Turn it off with levelrail-cli proxy disable, which removes the files it wrote and keeps everything else.

What is detected on Coolify

Coolify starts coolify-proxy with:
which gives: 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 to 127.0.0.1. Setup then stops with upstream_unreachable and the exact drop-in, for example:
then 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-*.yaml whose 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, mode 0644, 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.written and proxy_route.removed.
  • If the control plane cannot write the directory (another user, a read-only mount or a systemd sandbox), setup reports not_writable with the uid, the owner and mode of the directory, or the ReadWritePaths= 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_expiry and cert_renewal_stalled alert rules ignore it.
  • GET /api/v1/domains and GET /api/v1/certificates carry a tls_owner field (acme, internal, proxy, custom); levelrail-cli domains certificates prints it as the OWNER column.
If the proxy certificate has not been seen yet, run Verify on the proxy page (or levelrail-cli proxy --verify). When a proxy owns 80 and 443 and forwards to this ingress on its own ports (for example 8088 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:
In this mode the instance never requests certificates (a stored ACME setting is kept but skipped), builds 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:
Instance A runs with 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.
Last modified on October 12, 2026