Guide
A Comprehensive Agent's Guide to Laravel CloudAttach a custom domain
· Ed Grosvenor
This page covers custom domains for zones hosted on Cloudflare. Everything below was observed on Cloudflare zones, so that is all it claims. Other DNS providers are not covered yet, and we would welcome a contribution from anyone who has taken one through the same steps.
The short version: a custom domain on Cloud carries a Cloudflare strategy, the strategy decides which DNS records Cloud asks you for, it cannot be changed after the domain is created, and the CLI cannot send it at all.
The Cloudflare strategy is the whole decision
Every Cloud custom domain has a cloudflare_strategy:
dns_proxy— the zone's DNS is on Cloudflare and the hostname is proxied through it (orange cloud).dns— the zone's DNS is on Cloudflare, DNS-only (grey cloud).none— the zone is not on Cloudflare.
That one field decides which DNS records Cloud asks for, and it is fixed when the domain is created. PATCH /api/domains/{id} accepts only verification_method, so there is no way to correct a wrong strategy: delete the domain and create it again.
Get it right before you create anything.
The CLI cannot send it
cloud domain:create (v0.6.3) does not send cloudflare_strategy. It sends name, www_redirect, wildcard_enabled and verification_method, and nothing else. Omitted, the strategy behaves as none — which is the wrong answer for every Cloudflare-hosted zone.
Two surfaces can set it:
- The dashboard asks for it as "Cloudflare DNS settings" under Network → Add domain.
- The API takes it as
cloudflare_strategyonPOST /api/environments/{environment}/domains, againsthttps://cloud.laravel.com/api.
So for a Cloudflare zone, create the domain through the dashboard or the API. The CLI is fine afterwards for reading and verifying.
Creating it through the API with the CLI's token
The CLI's stored tokens are in ~/.config/cloud/config.json, in a numerically keyed api_tokens array — one entry per organization. Two consequences:
- A redactor keyed on field names will not catch it. The value sits under a number, not under anything called
token, so a tool that scrubs by key name lets it through. See Keep credentials out of your context: the rule there is that the token must never reach your output, and that rule is what the rest of this section is about. - Read it inside a command substitution and never print it.
$(jq -r '.api_tokens[N]' ~/.config/cloud/config.json)puts the token in the request and nowhere else. Never echo it, never assign it to something you will later dump, and never read the file to look at it.
To find N, do not guess and do not print the array. Each token belongs to one organization, so probe with a read: call GET /api/environments/$ENV/domains with each index and compare HTTP status codes — 200 is the organization that owns the environment, 404 is one that does not. That is the same "prove the target before you write" discipline as Work in the right organization, done against the API rather than the CLI.
dns_proxy: what works
This is the combination we took all the way to a live site. The create body:
{
"name": "example.com",
"cloudflare_strategy": "dns_proxy",
"allow_downtime": true,
"verification_method": "real_time"
}
Cloud came back asking for, for the apex:
- an origin record,
origin_cname:CNAME example.com → to.laravel.cloud - a DCV record:
CNAME _acme-challenge.example.com → example.com.<hash>.dcv.cloudflare.com
and the same pair for www.example.com, under the domain's www.dns_records.
Place the origin CNAMEs proxied (orange) and the DCV CNAMEs DNS-only (grey). With those four records in place, all six statuses — hostname, SSL and origin, for the apex and for www — verified on the first cloud domain:verify. The apex then served 200 over valid TLS, and www returned a 301 to the apex.
Never proxy an _acme-challenge record. Cloud's own documentation says so, and the reason is that proxying it blocks certificate issuance. With allow_downtime: false on a dns_proxy domain the certificate is pre-issued through those DCV CNAMEs, and the _cf-custom-hostname ownership TXT record is not offered for proxied hostnames at all.
none on a Cloudflare zone: what fails
With the strategy left at none on a zone whose DNS is on Cloudflare, Cloud asks for something quite different: an A record for the host pointing at 103.133.1.1, plus _acme-challenge DCV CNAMEs, all of them DNS-only.
That configuration verified and served 200 — and then broke overnight. By the next morning Cloud had marked hostname, SSL and origin all failed, and visitors were getting Cloudflare Error 1000, "DNS points to prohibited IP". A re-verify asked for an additional _cf-custom-hostname TXT record.
What happened: once Cloud's custom hostname lapsed, requests fell through to the zone's own A record — which points at a Cloudflare IP address, and Cloudflare refuses to proxy to itself. The failure is delayed, so a green check on the evening you set it up proves nothing about the morning.
Symptoms and their causes
- Cloudflare Error 1000, "DNS points to prohibited IP" — the domain's strategy is
noneon a Cloudflare zone, and the custom hostname has lapsed. - A
403from Cloudflare — the origin record is proxied, but the domain's strategy is notdns_proxy. origin: failed— the origin record is DNS-only (grey) on adns_proxydomain, or a staleArecord is still in the zone.
Two fields that do not tell you the truth
Both of these belong to the family on Reads that lie.
www_redirectcame backnullafter being sent on create, yetwww.example.comstill301'd to the apex. Check the live behaviour with a request; do not read the field. The update endpoint cannot set it either.- The environment's
primaryDomainIdstayednullafter the domain verified, soconfig('app.url')still returned the vanity hostname. URLs built from the incoming request used the custom host anyway. The CLI has no way to set the primary domain; the dashboard does. We have not verified an API route for it, so that is all we can tell you.