Custom domains
Attach a customer-owned domain to Apoxy's default Gateway with zero-downtime TLS provisioning.
A DomainRecord binds a hostname you own (e.g., api.example.com) to a Gateway, Proxy, Tunnel, or EdgeFunction on Apoxy. Apoxy handles DNS delegation, ACME certificate issuance, and renewal.
How it works
For customer-owned hostnames, Apoxy uses a CNAME model. You keep authority over your zone; Apoxy only needs you to point the hostname at a zone we control (cname.apoxy.net) so we can serve traffic and issue certificates on your behalf.
Minimal setup
apiVersion: core.apoxy.dev/v1alpha3
kind: DomainRecord
metadata:
name: api.example.com
spec:
name: api.example.com
tls: {}
target:
ref:
group: gateway.apoxy.dev
kind: Gateway
name: defaultApply the DomainRecord, then create the CNAME at your registrar:
api.example.com. CNAME api.example.com.cname.apoxy.net.Apoxy finds your project-scoped target via apoxy domainrecord get <name> -o yaml — the controller puts the exact value into status.
Once the CNAME propagates, Apoxy issues a TLS certificate using the ACME HTTP-01 challenge (Let's Encrypt / Google Trust Services). This takes roughly 30–60 seconds.
Zero-downtime migration (pre-issuance)
The default HTTP-01 flow has a side effect: the certificate is only issued after you flip your main CNAME. Until status.conditions.TLSReady=True, browsers hitting the new endpoint see a certificate error — typically a 30–60 second window.
To eliminate that window, delegate the ACME validation to Apoxy before you cut over:
Step 1 — apply the DomainRecord
Same as above.
Step 2 — add the ACME challenge delegation CNAME
At your registrar, create an additional CNAME:
_acme-challenge.api.example.com. CNAME _acme-challenge.api.example.com.cname.apoxy.net.The target must be _acme-challenge.<your-full-domain>.cname.apoxy.net. — include every label of your hostname verbatim, including the TLD. Dropping the TLD or a mid-label is the most common mistake and the delegation check will never pass.
| Hostname | Correct target | Common wrong target |
|---|---|---|
api.example.com | _acme-challenge.api.example.com.cname.apoxy.net. | _acme-challenge.api.example.cname.apoxy.net. (missing .com) |
app.example.co.uk | _acme-challenge.app.example.co.uk.cname.apoxy.net. | _acme-challenge.app.example.co.cname.apoxy.net. (missing .uk) |
If you want the exact string without typing it yourself, apoxy domainrecord get <name> -o yaml prints the expected target in the ACMEChallengeDelegationReady condition message.
This tells the ACME CA to look up the validation TXT record inside our zone, so we can prove domain ownership via DNS-01 without any traffic flowing through Apoxy yet.
Step 3 — wait for the certificate
Apoxy detects the delegation and pre-issues the certificate. Watch the conditions:
apoxy domainrecord get api.example.com -o yamlYou should see:
status:
conditions:
- type: ACMEChallengeDelegationReady
status: "True"
reason: ACMEChallengeDelegationConfigured
- type: TLSReady
status: "True"
reason: CertificateIssued
- type: CNAMEConfigured
status: "False" # main record not flipped yet — expected
reason: CNAMENotConfiguredTLSReady=True will land before CNAMEConfigured=True. At that point the certificate is sitting in our gateway, ready for traffic.
Step 4 — flip the main CNAME
Now change your main record at your registrar:
api.example.com. CNAME api.example.com.cname.apoxy.net.The next DNS resolution hits Apoxy. Our gateway serves the already-issued certificate from the first request — no cert error, no refresh loop.
Step 5 — keep the delegation CNAME
Leave _acme-challenge.api.example.com pointing at our zone. Renewal uses the same DNS-01 path, so keeping the delegation means renewals stay zero-intervention.
Zero-downtime migration — HTTP-only (no TLS)
If your DomainRecord leaves spec.tls unset (HTTP-only), there is no ACME certificate whose issuance could double as proof of ownership. Apoxy instead accepts an explicit ownership challenge at a dedicated label:
_apoxy-challenge.api.example.com. CNAME _apoxy-verify.cname.apoxy.net.The target is a single fixed string — no FQDN embedding, no per-domain token. Copy it verbatim.
Once the challenge resolves, the controller marks the DomainRecord with OwnershipVerified=True and admits the hostname to the data plane. From that point, the moment you flip your main CNAME the gateway starts serving traffic for it — the same zero-downtime effect as the TLS flow, without needing a certificate.
TLS customers get OwnershipVerified=True for free once their cert lands (a live cert from ACME is ownership proof). Setting _apoxy-challenge in addition is optional for TLS — the _acme-challenge delegation already covers both the pre-issued cert and ownership.
You can leave the _apoxy-challenge delegation in place permanently; it's idempotent and doesn't need to be touched across renewals or cutovers.
Condition reference
| Condition | Meaning |
|---|---|
OwnershipVerified | Gates serving. True when Apoxy has proof you control this hostname — either via TLSReady=True (ACME verified), a valid _apoxy-challenge CNAME, or (for non-TLS records) a successful main-CNAME cutover. This is what the edge gateway actually checks before admitting the hostname. |
TLSReady | A certificate has been issued and is ready to serve. |
ACMEChallengeDelegationReady | Your _acme-challenge.<fqdn> CNAME points into our managed zone, so we can pre-issue certs via DNS-01. |
CNAMEConfigured | Your main CNAME currently resolves to our zone. Operator-facing; does not gate serving. |
TargetReady | The backend (Gateway / Proxy / Tunnel / EdgeFunction) this record points at is healthy. |
Ready | Aggregate: all of the customer-visible setup above is green. Dashboards and CLI show this; the data plane does not key on it. |
Fallback
If you skip step 2 (no _acme-challenge CNAME), Apoxy falls back to HTTP-01 after you flip the main CNAME. That still works — we will try to create certificates on the fly and you just get the standard 30–60s window between cutover and TLSReady=True.
Troubleshooting
-
ACMEChallengeDelegationReady=Falseafter adding the CNAME — DNS caching. Wait up to the record's TTL at the customer resolver, then re-check withdig CNAME _acme-challenge.api.example.com. -
TLSReady=Falsewith reasonCertificateError— check the status message. Common causes: delegation CNAME points to the wrong target (verify it ends in.<cname-zone-suffix>.exactly — see the label-match table in Step 2), or the ACME account is rate-limited. -
Want to see what target to use —
apoxy domainrecord get <name> -o yamlshows the full delegation target in the condition message. -
Fixed a typo in the delegation CNAME but it still shows
False— two DNS caches may hold stale answers until they expire:- The old CNAME value at recursive resolvers — pinned for the TTL you set at your registrar (commonly 300s–3600s).
- The NXDOMAIN for the broken target inside
cname.apoxy.net— cached up to the zone's SOA MINIMUM TTL, 300s (5 minutes).
To minimize this window, drop your registrar's CNAME TTL (e.g. to 60s) before you first publish it, so any typo is easy to correct. If you've already been bitten, there's no shortcut — wait out the longer of the two TTLs, then
apoxy domainrecord get <name> -o yamlshould flipACMEChallengeDelegationReady=Trueon the next reconcile (runs every ~30s). -
ACME rate-limit errors in cosmos logs — issuance workflows now honor the CA's
Retry-Afterheader and sleep in-place instead of thrashing, so a single rate-limited domain will not burn the shared quota. A certificate that was in flight during a rate-limit event will resume automatically once the window clears; checkapoxy domainrecord get <name>— TLSReady will flip toTruewithout further action.