internal/ingress/acme_test.go and end-to-end tests against a local CA in internal/ingress/acme_live_test.go. Those tests cannot prove issuance against a real public CA with real DNS and real reachability on ports 80 and 443. This runbook is that verification.
A first run is recorded at the bottom of this page, so you do not need to repeat it before trusting the feature. Repeating it on your own host is the fastest way to confirm your firewall is right. For a version that needs no DNS, use the Enable HTTPS card described in Domains and ingress.
Prerequisites
- A domain you control, with access to create an
AorAAAArecord. - A control plane reachable from the public internet (installed with
install.sh, aslevelrail.service, or in Docker). - DNS already pointing at the server’s public IP and propagated.
- Ports 80 and 443 open from the internet. HTTP-01 is the check where the CA requests a file over port 80 to prove you own the domain, so a cloud firewall, a router without port forwarding or
ufwin default-deny will break issuance while looking fine from the server itself. Wildcards need DNS-01 instead. - A
rootAPI token or admin dashboard access, since saving ingress settings (PUT /api/v1/settings/ingress) needs therootability.
Steps
1
Point DNS at the server
Wait for propagation. From a machine outside your network,
dig +short your-domain.example.com should return the server’s public IP. Do not continue until it does: issuing before DNS propagates is the most common failure, and each failed attempt counts against the CA’s rate limit.You can also ask the control plane directly:2
Set the primary domain
Open Domains (Infrastructure section of the sidebar, or
/domains) and set Primary domain to your hostname, for example dashboard.example.com. After saving, a Certificate status badge and a DNS check panel appear. Use the panel to confirm the platform resolves your record before enabling ACME.3
Turn on ACME, staging first
Switch on Issue real ACME certificates and fill in:
- Contact email: required. The CA uses it to reach you about problems such as forced revocation. Use a monitored address.
- Directory URL (optional): leave blank for Let’s Encrypt production.
https://acme-staging-v02.api.letsencrypt.org/directory. Staging certificates trigger browser warnings but exercise the same mechanism without touching production rate limits. When staging succeeds, clear the field and save again for a trusted certificate. Saving drops the previous issuer’s stored certificates so the new CA re-issues them.From the CLI:4
Wait for issuance
Saving does not issue immediately. Ingress reconciles continuously, and any routed domain qualifies for a certificate once ACME is on and an email is set, so issuance normally starts within moments. The next section says what success looks like.
Confirm it worked
- Dashboard: the Certificate status badge reads Healthy. The issuer names your CA (
Let's Encrypt, or an intermediate such asR11), never Levelrail’s internal issuer. The same data is atGET /api/v1/certificatesandlevelrail-cli domains certificates. - Expiry:
not_afteris about 90 days out for Let’s Encrypt. A lifetime of a few hours means you are still on the internal self-signed issuer: the toggle did not save, or the page is stale. - Renewal: nothing to configure. Caddy renews well before expiry (about 30 days out for a 90-day certificate). To see it work without waiting, look in the logs for
renewing certificateorcertificate obtained successfullybefore the currentnot_after. - Logs:
journalctl -u levelrail -n 200 --no-pager, or your container’s logs. Look for"logger":"tls.obtain"and"logger":"http.acme_client".
Expected log sequence for a successful issuance
Expected log sequence for a successful issuance
Common failures
DNS not propagated or wrong record
DNS not propagated or wrong record
The log shows a challenge failure such as
connection ... timed out or no such host right after “trying to solve challenge”.Check dig +short your-domain.example.com from outside your network, wait out the record’s TTL, then retry.Port 80 blocked
Port 80 blocked
dig resolves correctly, but the log shows the challenge request timing out or being refused. Typical causes are cloud security groups, ufw, routers without port forwarding, and another proxy holding port 80.Open ports 80 and 443 inbound. levelrail-cli doctor and the dashboard’s status page include reachability checks. See Firewall: ports 80 and 443.Rate limited
Rate limited
Let’s Encrypt production limits failed validations per account and hostname, and certificates per registered domain per week. Check their current limits, because they change. The log shows an explicit
rate limited or too many error. Use staging for repeated attempts. Production enable attempts from the one-click flow are also capped by APP_ACME_MAX_ATTEMPTS_PER_HOUR.Missing or invalid contact email
Missing or invalid contact email
The dashboard form rejects this before it reaches the API, and the backend rejects it the same way if you call the API directly.
Wrong issuer after switching CA
Wrong issuer after switching CA
Enabling ACME, or changing the directory URL (for example staging to production), drops the previous issuer’s stored certificates when you save so the new CA re-issues them. If a domain still shows the wrong issuer, confirm an app actually routes that domain: ACME applies only to domains in active use by a route.
Recorded run (2026-10-05)
Host: a 4 GB DigitalOcean droplet running the control plane from the publicinstall.sh, ports 80 and 443 open to the internet, no DNS records created. Hostname: <dashed-ip>.sslip.io for the dashboard and for every app without a domain.
What was done and seen:
- Staging first:
POST /api/v1/settings/ingress/httpswithstaging: true. HTTP-01 validated, certificate issued by(STAGING) ..., shown byGET /api/v1/certificates. - Production: Let’s Encrypt (
acme-v02) issued<dashed-ip>.sslip.ioand the generated hostnames of three apps in one batch. Checked from a second server, with its normal system trust store and no-k:curlverify result0, issuerLet's Encrypt(YE2),not_after90 days out, SAN equal to the hostname. http://<host>/answered308tohttps://<host>/.- HSTS was enabled and disabled from the settings toggle.
- Certificates were stored on disk, not in the database as this page and ADR 005 describe, so
GET /api/v1/certificates, the certificate expiry alert rules and the issuance audit trail all saw nothing. Storage is now the database, and existing on-disk certificates are imported on first start. - Port 80 was closed outside ACME challenges, so any plain
http://request was refused instead of redirected. - Changing the CA never took effect on a live reload, because certmagic reuses a valid certificate from any issuer. Saving now drops the other issuers’ certificates.
Strict-Transport-Securitywas not sent on the dashboard’s HTML page, only on API responses.- A failed issuance was invisible: only a log line. The CA’s error is now captured from Caddy’s certificate events and returned with a hint code.
got renewal info log lines), wildcard certificates through DNS-01, and a deliberately failing issuance against the production CA.
See also
- Domains and ingress: TLS options, DNS-01 wildcards and the one-click HTTPS flow.
- Installing Levelrail: initial setup and prerequisites.
- Troubleshooting: broader platform diagnostics.
- ADR 005: Caddy embedded ingress: the design decisions.