Domain uniqueness is instance-wide, but cast plans project-scoped — so apply can plan a create Coolify will refuse #44
Labels
No labels
blocked
blocker:ci-red
blocker:conflict
blocker:drill-pending
blocker:unrequested
bug
claimed
documentation
enhancement
epic
merge-next
needs-triage
ready
release
scope:apply
scope:capture
scope:coolify-api
scope:fleet
scope:manifest
scope:secrets
stale
state:addressing
state:bots-reviewing
state:building
state:needs-human
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: heavy-duty/cast#44
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What happened
A first apply, planning
create application core, got:An application cast cannot see — it is not in the project and environment cast is working in — already owns the domain cast is about to claim.
The structural point
cast plans inside one project + one environment. Coolify enforces domain uniqueness across the entire instance. So
applycan produce a plan that is internally consistent, correct against everything cast can observe, and still be refused for a reason that is invisible from cast's own scope. Today the only signal is a raw Coolify 409 arriving mid-apply, after the project and environment have already been created.This is the same family as #41 (the multi-destination 400): a Coolify-wide constraint that a project-scoped tool cannot pre-flight, surfacing as an untranslated error at the worst moment.
Where the orphan came from — and why this will keep happening
The conflicting app is residue from an earlier failed apply that was cleaned up by deleting the Coolify project in the UI. Deleting the project did not delete its resources — so the app survived, invisible to cast (no project cast queries contains it), while still holding the domain globally.
That is worth stating plainly because it is the whole argument for #43: hand-deletion is not a reset. It is a way to manufacture orphans that the tool cannot see and that still hold instance-wide state (domains — and, presumably, volumes and networks). Every iteration of "battle-test the first apply" currently ends by creating one.
Suggested
1. Translate the 409 (as #41 asks for the 400). Name the conflicting resource, its UUID, and — critically — the fact that it is outside the project cast is applying, which is the part the operator cannot work out from Coolify's message:
2. Never send
force_domain_override=true. Coolify offers it in the error and it is the wrong answer: two resources sharing a domain is a routing coin-flip, and Coolify's own response says so ("can cause routing conflicts and unpredictable behavior"). If cast ever gains the flag it must be an explicit operator act, never a retry.3. Consider a pre-flight for
createplans.GET /applicationslists every application on the instance; a per-appGETexposes its domains. Checking the desired domains against that before mutating anything would turn a mid-apply 409 into a refusal that costs nothing. It is N+1 calls on a create-only path, which is cheap and only ever happens on a first apply.Correction to the diagnosis above — and a cheap fix that would have prevented it
The conflicting application was not an orphan from a previous run. It was
core, created by the same apply, seconds earlier.The manifest's
landingapp had been given the api hostname by a bad hostname-overlay entry, so cast createdcore(claimingapi.<ip>.sslip.iofor itsapicompose service), then tried to createlandingwith that same domain, and Coolify refused. Two consecutive runs produced two differentresource_uuids in the 409 — which read like surviving orphans, but were simply the freshly-createdcoreof each run.The instance-wide-vs-project-scoped point in the issue still stands (a domain held by a resource outside cast's project is invisible to its plan, and a pre-flight against
GET /applicationswould catch both cases). But the first thing this error needs is much cheaper:castshould name the resource it was creating when a create failsToday the failure is printed as a bare Coolify response:
Nothing in that line says cast was creating
landingwhen it happened. The only name in the output iscore— the other side of the conflict — so the error reads as "core is already there" when it means "landing collides with core". We diagnosed it as pre-existing orphan state and went looking for ghosts.Every mutating call should carry the resource it is acting on into its error. It is a one-line change and it is the difference between "cast collided with itself, fix your overlay" and an hour of hunting invisible orphans.