Skip to main content

Custom domains & TLS

Denia is its own L7 ingress and ACME client — there is no separate Traefik/nginx or certbot. Bringing a domain online is a verify-then-issue flow (ADR-013, ADR-020).

1. Point DNS

Create an A/AAAA record for the hostname pointing at the node's public IP.

2. Attach the domain

curl -fsS -X POST \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"hostname":"app.example.com"}' \
https://your-node.example.com/v1/services/$SID/domains

It starts unverified and is not yet in the ingress route table.

3. Verify ownership

curl -fsS -X POST \
-H "Authorization: Bearer $TOKEN" \
https://your-node.example.com/v1/services/$SID/domains/$DID/verify

Denia serves a token at GET /.well-known/denia-challenge/{token} that is only returned when the request Host matches, so verification passes only once DNS resolves to this node.

4. TLS is automatic

Enable tls_enabled on the service (requires DENIA_ACME_EMAIL). For each verified domain Denia issues a certificate over ACME HTTP-01 via instant-acme, serves it per-SNI, persists it 0600 under <tls_dir>, and renews it on a background scan.

:::caution Use staging while testing Set DENIA_ACME_DIRECTORY_URL to the Let's Encrypt staging endpoint for non-production to avoid burning production rate limits, then switch back. :::

Serving the control plane over a domain

The control plane itself can be served on the same ingress by setting DENIA_CONTROL_DOMAIN (+ DENIA_CONTROL_TLS); a workload service may not claim that hostname. See ADR-035.

Troubleshooting

If TLS fails: confirm DNS resolves here, the domain is verified, DENIA_ACME_EMAIL is set, and :80 is reachable from the public internet (HTTP-01 requirement). More in Troubleshooting.