Add a custom domain to your app with automatic HTTPS

Add a custom domain to your app: create the A record, add the name in ox, and Caddy gets and renews the HTTPS certificate. What each domain status means.

Last updated 2026-10-10

On this page

To add a custom domain with HTTPS, create an A record for the name that points at your server's IP. Then add the name on the project's Settings tab, or run ox domains <project> add <name>. Caddy on your server gets a free certificate for the name and renews it by itself, so there is nothing to install or renew by hand.

You create an A record that points the name at your server. While the Domains card is open, ox looks the name up again once its last answer is 30 seconds old, and the card shows what it found, from checking DNS… to HTTPS on. Caddy on your server gets the certificate over ports 80 and 443 and renews it by itself.

Point the domain at your server with an A record

Do this first. At your DNS provider, create an A record for the name, with your server's IP address as the value. The Domains card shows the address: Once added, point an A record at <ip>. When the server has an IPv6 address, the card also asks for an AAAA record. ox domains shop prints the same:

ox domains shop
Point each domain's A record at 203.0.113.7.

For a site at the root of a domain, like example.com, the record's name is usually @.

Create the record before anyone looks the name up, you or ox's check. A DNS resolver that looks up a name before its record exists can remember the answer no such name for a while, often from a few minutes up to an hour. It keeps giving that answer after you create the record.

The www name

When www.example.com also points at the server, ox sends it to example.com with a permanent redirect. To serve www.example.com instead, list it as a domain yourself. A wildcard record such as *.example.com does not change this.

A name that a wildcard answers, like app.example.com under *.example.com, gets no www redirect. That way the wildcard does not cost a certificate for every such name. List www.app.example.com yourself if you want it.

Add a custom domain

Once the record exists, add the name to ox. In the dashboard, the review before a project's first deploy has an Address card with two choices: the free test address (shown in full), or your own domain. Pick your own domain, type the name and press Add. After the first deploy, the same card is on the project's Settings tab. From a terminal:

Add a domain
ox domains shop add shop.example.com --wait

Adding a domain saves it and redeploys the app, so the server starts serving the name. On a project that has not deployed yet, the name is saved and goes live on the first deploy. ox domains shop remove shop.example.com takes a name off again.

You can also list names in ox.toml with domains = ["shop.example.com"] (top-level keys). For production, the names in ox.toml come first and the ones you add in ox come after. The list marks each name from ox.toml, and you remove those by editing the file. Staging and previews never take production's ox.toml names.

How the HTTPS certificate is issued

Caddy, the web server ox puts on your server, gets a free certificate for each domain and renews it before it runs out. You do nothing.

To prove the name is yours, the certificate authority connects to the name on ports 80 and 443. So the name must already point at your server, and both ports must be open to the internet. If your cloud provider has a firewall in front of the server, open them there. Until the name points at the server, there is no certificate, and Caddy tries again by itself.

Staging and branch previews can get HTTPS names too, under a preview domain.

When the certificate authority rate-limits a domain

Let's Encrypt issues at most 50 certificates a week for one registered domain, such as example.com with every name under it. It issues at most 5 a week for the same exact name. ox gets one certificate per name: each domain, each www. redirect, and each staging copy and branch preview under your preview domain. It uses no wildcard certificate. A busy week of previews, or many sites under one domain, can reach the limit.

The name then has no HTTPS for a while. ox doctor and the server page say the certificate authority rate-limited shop.example.com; it retries automatically after 2026-10-09 10:20 UTC, and the fix line says which limit it was. Caddy asks again by itself and gets the certificate once the limit allows it. For the 50 a week per domain, or the 5 for one name, the only fix is to wait.

Let's Encrypt also limits a name that failed validation too often. A name that does not point at your server yet runs into this one. Fix its A record (see its row on the Domains card), and Caddy gets the certificate once that limit lifts.

To stay under the weekly limits, use fewer distinct names under one domain. Put the preview domain on a domain of its own, so previews never use up production's certificates.

What each domain status means

While the Domains card is open, ox looks up each name at a public DNS resolver, again whenever its last answer is 30 seconds old. The row shows what it found:

The row saysIt means
checking DNS…The first lookup has not come back yet.
waiting for DNSThe name has no address yet. Create the A record, or wait for it to spread.
points elsewhereThe name has an address, but not your server's. Change the record.
HTTPS onThe name points at your server, so the server can get its certificate.
CloudflareCloudflare's proxy is in front, which hides where the name points. In Cloudflare, set its A record to your server's IP.
No certificateCloudflare's proxy has no certificate for the name.

The row turns to HTTPS on by itself once the record resolves. You do not have to add the domain again.

If the custom domain does not work

  • The row stays on waiting for DNS: check the record's name and type at your DNS provider. A new record can take a while to reach every resolver, and longer if the name was looked up before it existed.
  • The row says points elsewhere: the name still points at an old host. The row says It resolves to <ip>, not this server. with the record to set.
  • No certificate behind Cloudflare: Cloudflare's free certificate does not cover a name two levels deep, like a.b.example.com. Set the record to DNS only, so your server gets its own certificate, or add a certificate for the name in Cloudflare.
  • The browser warns about the certificate: the name has pointed at the server only for a moment, or a firewall in front of the server blocks ports 80 and 443. Open them and give Caddy a minute.

Troubleshooting has the same steps in short.