Custom domains
This guide serves an app from a domain you own, such as shop.example.com. The CLI enrolls the domain and prints the DNS records to publish. The domain serves the app after the records are published and a check finds the domain and its certificate active.
The commands use production. district login --staging records staging as the current environment, and every later command that takes no environment flag uses it. Each command reads the clone in the current directory, or takes a trailing positional repository argument from outside a clone.
What you need
- A domain at a DNS provider, and access to publish records in its zone. The zone of
shop.example.comisexample.com. - An app with a durable target. district app create publishes a repository as an app with a primary target, and district target create adds another one. A repository with no app is a usage error that names
district app create. - An available custom-domain slot. An app holds at most 2 custom domains. Each domain is attached to one durable target of the app and serves the deployment that target holds. A custom domain cannot serve a preview target because preview targets expire.
Add the domain
district domain add attaches the domain to one durable target of the app and enrolls it. It prints the target the domain serves, the DNS records the owner must publish, and the command that checks them.
$ district domain add shop.example.com
added shop.example.com (hdom_01K4…)
status ○ pending
target shopfront
Publish these DNS records, then run: district domain verify shop.example.com
TYPE HOST VALUE PURPOSE
CNAME shop hosting.ondistrict.net routing
TXT _district-verify district-verify=7d3f9c2e-8a41-4b6e-9f0c-1d2e3f4a5b6c ownership
Optional. Publish it to issue the certificate before you change the routing record:
CNAME _acme-challenge.shop a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d.dcv.cloudflare.com dcv-delegation
Each HOST is relative to example.com. If your provider asks for the full name, add .example.com to it.
The TXT record at the zone apex covers every hostname under it in this workspace.
If you cannot edit the zone apex, publish the same record at the hostname or at any level between.Without --target, a terminal asks which app target serves the domain:
Select the target for shop.example.com:
1) shopfront (shopfront.ondistrict.net) [primary]
2) next (next.ondistrict.net)
3) Create a new subdomain
Enter a number from 1 to 3, or press Enter for shopfront:An empty answer selects the primary target. An app with no primary target has no default. The last entry creates a target. The command asks for its label, offers the first label of the domain, and attaches the domain to the new target. Three answers that select nothing end the command, and the error names --target.
--target <label> names the target and skips the question. The label must be a durable target of the app. A label that names no durable target is a usage error that lists the durable labels, and the command enrolls nothing. A session with no terminal does not send a target. The API attaches the domain to the app's primary target and refuses an app with no primary target. Its message names --target.
The API refuses a request if another app holds the domain, the app already holds 2 domains, or the domain is inside a platform-owned zone. It also refuses a caller who cannot update the app. It uses its own message for each refusal. --json prints the domain, with dnsRecords, as one JSON object.
A repeat for a domain the app already holds asks for no target. It prints that domain and the records that apply at that time, and says which target the domain keeps.
Publish the DNS records
Publish the two required records at the DNS provider for the zone. Each HOST is relative to the zone. Most provider forms append the zone. A form that asks for the full name takes the host with .example.com after it, so shop becomes shop.example.com.
The routing record sends the domain's traffic to District. The ownership record is a TXT at _district-verify on the zone apex. Its value holds the workspace's token, so one record covers every hostname the workspace adds under that zone. If you cannot edit the zone apex, publish the same record at the hostname or at any level between. The domain serves after a check finds both records.
The dcv-delegation record is optional. It lets District issue the certificate before the routing record changes. Without it, District proves control of the domain to the certificate authority over HTTP after the routing record resolves, and renews the certificate the same way.
For an apex domain such as example.com, the routing record's host is @, the form's name for the zone itself. A CNAME on the zone is not valid DNS, so that row reads CNAME* and a footnote names the record types a provider offers instead:
TYPE HOST VALUE PURPOSE
CNAME* @ hosting.ondistrict.net routing
TXT _district-verify district-verify=7d3f9c2e-8a41-4b6e-9f0c-1d2e3f4a5b6c ownership
Optional. Publish it to issue the certificate before you change the routing record:
CNAME _acme-challenge a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d.dcv.cloudflare.com dcv-delegation
Each HOST is relative to example.com. If your provider asks for the full name, add .example.com to it.
The TXT record at the zone apex covers every hostname under it in this workspace.
If you cannot edit the zone apex, publish the same record at the hostname or at any level between.
* A root record cannot be a CNAME at most providers. Select ALIAS, ANAME or CNAME flattening for it.A recently enrolled domain can return no delegation record while the provider's read catches up. The command then names district domain status, which prints the records again.
Check that the domain serves the app
district domain verify checks the domain now instead of waiting for District's scheduled check. It prints the domain after the check and exits 0 only when the status is active, so a shell loop can wait for it:
$ until district domain verify shop.example.com; do sleep 30; done
hostname shop.example.com (hdom_01K4…)
target shopfront
status ○ pending
verified no
cert expires -
added 2026-08-25T09:00:00Z
If you published the records just now, wait a few minutes and run this command again. Automatic checks also run
while the domain is pending, less often as time passes. Read the records with: district domain status shop.example.com
shop.example.com is pending, not active
hostname shop.example.com (hdom_01K4…)
target shopfront
status ✔ active
verified yes
cert expires 2026-12-01T00:00:00Z
added 2026-08-25T09:00:00ZWhile the domain is not active, the command prints a note above the verdict: published records take minutes to propagate, District's checks run while the domain is pending, and district domain status prints the records.
One check of a domain runs at a time. For two minutes after a check starts, another call does not start a second check. It prints the domain as it stands. --json prints the domain as one JSON object, without the note, and uses the command's exit code.
Each district domain command exits 3 when the certificate provider does not answer. The domain is unchanged. Run the command again after the number of seconds in the message.
Check the state later
district domain list prints one row per domain: the host, the label of the target it serves, the status, whether it serves the app, the certificate expiry, and when it was added. - stands for a certificate that is not issued.
$ district domain list
HOSTNAME TARGET STATUS VERIFIED CERT EXPIRES ADDED
shop.example.com shopfront ✔ active yes 2026-12-01T00:00:00Z 2026-08-25T09:00:00Z
www.example.com next ○ pending no - 2026-08-26T09:00:00ZA domain holds one of four statuses:
pending: the platform waits for the records or the certificate.active: the domain serves the app.failing: the domain serves the app, and checks fail.detached: the platform no longer holds the enrollment.
For an app with no custom domain, the command prints nothing and exits 0. district domain status <hostname> reports one domain: its state, the target it serves, the certificate expiry, when it was added, and the same DNS records that district domain add printed.
Serve a domain from another target
A custom domain serves the deployment its target holds, so a domain on a target that is not primary serves what that target serves. district domain add shop.example.com --target next attaches the domain to the next target, and district deploy --target next then changes what the domain serves, without touching the primary target.
A repeat does not move a domain: --target with another label is a usage error that names district domain remove. To move a domain, remove it, then add it again with the other label.
district target delete refuses a target that custom domains serve, and its message names those domains. Remove each domain first.
Remove a domain
district domain remove removes the domain from the app, and the domain then stops serving the app. The command prints the resolved domain and the environment, then asks Remove <hostname>? [y/N]. --yes skips the question, and a session with no terminal needs it.
The owner's DNS records are not changed. Remove them at the DNS provider separately.