Domain uniqueness is instance-wide, but cast plans project-scoped — so apply can plan a create Coolify will refuse #44

Closed
opened 2026-07-14 17:46:50 +00:00 by dan-claude-bot · 1 comment
dan-claude-bot commented 2026-07-14 17:46:50 +00:00 (Migrated from github.com)

What happened

A first apply, planning create application core, got:

POST /applications/private-github-app → 409:
{"message":"Domain conflicts detected. Use force_domain_override=true to proceed.",
 "conflicts":[{"domain":"http://api.89.167.19.110.sslip.io",
               "resource_name":"core","resource_uuid":"tqsmnzdde6oxz3fhl63e2xvl",
               "resource_type":"application","service_name":"api",
               "message":"Domain ... is already in use by application 'core' (service: api)"}]}

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 apply can 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:

Domain <d> is already claimed by application <name> (<uuid>), which is not in project <p> / environment <e> — cast cannot see or manage it. It is most likely residue from a previous run cleaned up by deleting a project (project deletion does not delete resources). Delete it, or change the domain.

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 create plans. GET /applications lists every application on the instance; a per-app GET exposes 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.

## What happened A first apply, planning `create application core`, got: ``` POST /applications/private-github-app → 409: {"message":"Domain conflicts detected. Use force_domain_override=true to proceed.", "conflicts":[{"domain":"http://api.89.167.19.110.sslip.io", "resource_name":"core","resource_uuid":"tqsmnzdde6oxz3fhl63e2xvl", "resource_type":"application","service_name":"api", "message":"Domain ... is already in use by application 'core' (service: api)"}]} ``` 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 `apply` can 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: > Domain `<d>` is already claimed by application `<name>` (`<uuid>`), which is **not in project `<p>` / environment `<e>`** — cast cannot see or manage it. It is most likely residue from a previous run cleaned up by deleting a project (project deletion does not delete resources). Delete it, or change the domain. **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 `create` plans.** `GET /applications` lists every application on the instance; a per-app `GET` exposes 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.
dan-claude-bot commented 2026-07-14 17:55:48 +00:00 (Migrated from github.com)

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 landing app had been given the api hostname by a bad hostname-overlay entry, so cast created core (claiming api.<ip>.sslip.io for its api compose service), then tried to create landing with that same domain, and Coolify refused. Two consecutive runs produced two different resource_uuids in the 409 — which read like surviving orphans, but were simply the freshly-created core of 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 /applications would catch both cases). But the first thing this error needs is much cheaper:

cast should name the resource it was creating when a create fails

Today the failure is printed as a bare Coolify response:

POST /applications/private-github-app → 409: {"message":"Domain conflicts detected...","conflicts":[{"resource_name":"core",...}]}

Nothing in that line says cast was creating landing when it happened. The only name in the output is core — 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.

creating application landing → 409: domain http://api...sslip.io is already claimed by
  application 'core' (service: api, uuid ed85cs...) — created by this same apply.

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.

## 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 `landing` app had been given the *api* hostname by a bad hostname-overlay entry, so cast created `core` (claiming `api.<ip>.sslip.io` for its `api` compose service), then tried to create `landing` with that same domain, and Coolify refused. Two consecutive runs produced two different `resource_uuid`s in the 409 — which read like surviving orphans, but were simply the freshly-created `core` of 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 /applications` would catch both cases). But the *first* thing this error needs is much cheaper: ### `cast` should name the resource it was creating when a create fails Today the failure is printed as a bare Coolify response: ``` POST /applications/private-github-app → 409: {"message":"Domain conflicts detected...","conflicts":[{"resource_name":"core",...}]} ``` Nothing in that line says cast was creating **`landing`** when it happened. The only name in the output is `core` — 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. ``` creating application landing → 409: domain http://api...sslip.io is already claimed by application 'core' (service: api, uuid ed85cs...) — created by this same apply. ``` 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.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: heavy-duty/cast#44
No description provided.