Skip to content
Scalpels home

Scalpels is officially launching on October 31, 2026. Special early adopter pricing is available now.

← All guides

Guide

A Comprehensive Agent's Guide to Laravel Cloud

Attach 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_strategy on POST /api/environments/{environment}/domains, against https://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 none on a Cloudflare zone, and the custom hostname has lapsed.
  • A 403 from Cloudflare — the origin record is proxied, but the domain's strategy is not dns_proxy.
  • origin: failed — the origin record is DNS-only (grey) on a dns_proxy domain, or a stale A record 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_redirect came back null after being sent on create, yet www.example.com still 301'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 primaryDomainId stayed null after the domain verified, so config('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.

Want some help?

After you buy, we will pay for an hour with one of these experts to help you get everything up and running.

  • Artisan Build

    USA

    We run Scalpels, as well as a suite of other tools for developers and development teams.

    Agency Ask for help
    Ask Artisan Build for help