Service hostnames ARE settable and readable on Coolify 4.1.2 (urls) — adopt it, delete the manual-UI step #73

Closed
opened 2026-07-16 12:16:45 +00:00 by dan-claude-bot · 2 comments
dan-claude-bot commented 2026-07-16 12:16:45 +00:00 (Migrated from github.com)

Summary

The #72 audit refuted the load-bearing claim behind cast's biggest remaining manual step. semantics.md says it was "re-checked and holds: Coolify 4.1.2 exposes no flat domains on a service, on any route", and draft.ts says per-container fqdn "can neither read nor write". Both halves are false at tag v4.1.2:

  • Write: ServicesController accepts urls — an array of {name, url} with comma-separated URLs per container — on both POST /services (allowlist + validation at ServicesController.php:296,320-324) and PATCH /services/{uuid} (allowlist :962). applyServiceUrls() (:45-135) matches urls[].name to a ServiceApplication and sets fqdn, with cross-team domain-conflict detection (409 unless force_domain_override=true — which cast never sends, per existing policy). Landed upstream in coollabsio/coolify#7929, merged 2026-01-14 — well before 4.1.2.
  • Read: GET /services/{uuid} does $service->load(['applications', 'databases']) (:736) and removeSensitiveData does not hide fqdn. Create/PATCH responses even return 'domains' => $service->applications()->pluck('fqdn').

The original claim was half-technically-true (no field literally named domains) and operationally false. It is the #51 arc repeating: a defect filed as a limitation never got re-checked.

What is genuinely missing is on cast's side: the manifest's flat services.<name>.domains: string[] has no per-container name to build urls[] from. Compose applications already solve this exact shape with service_domains (map of compose service name → URLs).

What to do

  • Give services a per-container domains vocabulary in the manifest (mirror the compose service_domains map; decide migration story for the existing flat domains field)
  • Write: send urls on service create and PATCH (serviceApiFields() currently drops domains — delete the drop)
  • Read: project per-container fqdn from GET /services/{uuid} into live fields (note: the list endpoint doesn't load applications; the per-uuid GET does)
  • Make it a diffed field like any other — the whole point
  • Delete the once-per-run warn in resolve.ts:673 and the two UNCAPTURED.md rows ("service hostnames" in NO_API_COVERAGE, the per-service draft marker in draft.ts:616) — and have --emit-draft actually emit the hostnames it can now read
  • Correct semantics.md's "Known limitations" entry the same way #51 corrected the backup one: struck through, kept, with the reason nobody looked

Caveats to carry into the implementation

  • 4.1.2 has no per-container sub-route; urls replaces fqdn per named container. (next adds GET/PATCH /services/{uuid}/applications/{app_uuid} — nicer, but nothing here needs to wait for it.)
  • Domain-conflict 409s surface exactly like the application-side ones (preflightDomainConflicts handles apps; service writes can now hit the same refusal).

Found in #72 (finding 1).

## Summary The #72 audit refuted the load-bearing claim behind cast's biggest remaining manual step. `semantics.md` says it was "re-checked and holds: Coolify 4.1.2 exposes no flat `domains` on a service, on any route", and `draft.ts` says per-container `fqdn` "can neither read nor write". Both halves are false at tag `v4.1.2`: - **Write:** `ServicesController` accepts `urls` — an array of `{name, url}` with comma-separated URLs per container — on **both** `POST /services` (allowlist + validation at `ServicesController.php:296,320-324`) and `PATCH /services/{uuid}` (allowlist `:962`). `applyServiceUrls()` (`:45-135`) matches `urls[].name` to a `ServiceApplication` and sets `fqdn`, with cross-team domain-conflict detection (409 unless `force_domain_override=true` — which cast never sends, per existing policy). Landed upstream in coollabsio/coolify#7929, merged 2026-01-14 — well before 4.1.2. - **Read:** `GET /services/{uuid}` does `$service->load(['applications', 'databases'])` (`:736`) and `removeSensitiveData` does **not** hide `fqdn`. Create/PATCH responses even return `'domains' => $service->applications()->pluck('fqdn')`. The original claim was half-technically-true (no field literally named `domains`) and operationally false. It is the #51 arc repeating: a defect filed as a limitation never got re-checked. What is genuinely missing is on **cast's side**: the manifest's flat `services.<name>.domains: string[]` has no per-container name to build `urls[]` from. Compose applications already solve this exact shape with `service_domains` (map of compose service name → URLs). ## What to do - [ ] Give services a per-container domains vocabulary in the manifest (mirror the compose `service_domains` map; decide migration story for the existing flat `domains` field) - [ ] Write: send `urls` on service create and PATCH (`serviceApiFields()` currently *drops* domains — delete the drop) - [ ] Read: project per-container `fqdn` from `GET /services/{uuid}` into live fields (note: the *list* endpoint doesn't load `applications`; the per-uuid GET does) - [ ] Make it a **diffed field** like any other — the whole point - [ ] Delete the once-per-run warn in `resolve.ts:673` and the two UNCAPTURED.md rows ("service hostnames" in `NO_API_COVERAGE`, the per-service draft marker in `draft.ts:616`) — and have `--emit-draft` actually emit the hostnames it can now read - [ ] Correct `semantics.md`'s "Known limitations" entry the same way #51 corrected the backup one: struck through, kept, with the reason nobody looked ## Caveats to carry into the implementation - 4.1.2 has no per-container sub-route; `urls` replaces fqdn per named container. (`next` adds `GET/PATCH /services/{uuid}/applications/{app_uuid}` — nicer, but nothing here needs to wait for it.) - Domain-conflict 409s surface exactly like the application-side ones (`preflightDomainConflicts` handles apps; service writes can now hit the same refusal). Found in #72 (finding 1).
dan-claude-bot commented 2026-07-16 15:52:58 +00:00 (Migrated from github.com)

Done in #81 (merged, bbca3cf).

Services now speak the same per-container vocabulary a dockercompose app does:

  • Manifest: service_domains: { <container>: [url] }; the flat, never-honored domains is removed from ServiceSpec (it could not name the container urls[] requires).
  • Write: serviceApiFields builds urls on create and update.
  • Read/diff: a supplementary GET /services/{uuid} per service (attachServiceDomains, gated to diff/apply like backups) projects applications[].fqdn back — so a declared hostname is compared every run. The manual-UI step is gone.
  • Pre-flight: a service create's domains join desiredDomainsOfCreate — the more important because a conflicting service create is deleted server-side before the 409 (applyServiceUrls rollback).
  • service_domains is canonicalized (keys + URL arrays sorted) on both sides, so container ordering never false-drifts.

The warn at resolve.ts:673, the drop in serviceApiFields(), and the UNCAPTURED/NO_API_COVERAGE rows are deleted; semantics.md's struck-through-precedent entry is corrected #51-style.

Two limits stated out loud rather than papered over:

  1. The read fails closed — an unreachable/unrecognized GET /services/{uuid} aborts rather than projecting empty and re-PATCHing forever.
  2. inventory --emit-draft does not yet make the per-service GET, so a drafted service's hostnames are still declared by hand (same shape as backups — tracked in #75).

Downstream sync: heavy-duty/incubator#17 migrates umami to service_domains.

Done in #81 (merged, `bbca3cf`). Services now speak the same per-container vocabulary a dockercompose app does: - **Manifest:** `service_domains: { <container>: [url] }`; the flat, never-honored `domains` is removed from `ServiceSpec` (it could not name the container `urls[]` requires). - **Write:** `serviceApiFields` builds `urls` on **create and update**. - **Read/diff:** a supplementary `GET /services/{uuid}` per service (`attachServiceDomains`, gated to `diff`/`apply` like backups) projects `applications[].fqdn` back — so a declared hostname is compared every run. The manual-UI step is gone. - **Pre-flight:** a service create's domains join `desiredDomainsOfCreate` — the more important because a conflicting service create is **deleted server-side before the 409** (`applyServiceUrls` rollback). - `service_domains` is canonicalized (keys + URL arrays sorted) on both sides, so container ordering never false-drifts. The warn at `resolve.ts:673`, the drop in `serviceApiFields()`, and the UNCAPTURED/`NO_API_COVERAGE` rows are deleted; `semantics.md`'s struck-through-precedent entry is corrected #51-style. **Two limits stated out loud** rather than papered over: 1. The read **fails closed** — an unreachable/unrecognized `GET /services/{uuid}` aborts rather than projecting empty and re-PATCHing forever. 2. `inventory --emit-draft` does not yet make the per-service GET, so a drafted service's hostnames are still declared by hand (same shape as backups — tracked in #75). Downstream sync: heavy-duty/incubator#17 migrates umami to `service_domains`.
dan-claude-bot commented 2026-07-16 15:57:44 +00:00 (Migrated from github.com)

The draft-capture half called out above is now tracked in #83.

The draft-capture half called out above is now tracked in #83.
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#73
No description provided.