From bd993123c08d6bd52b92223fccf49ac17e7d1c37 Mon Sep 17 00:00:00 2001 From: Mark Henderson Date: Thu, 30 Apr 2026 11:28:25 -0700 Subject: [PATCH] fix: rolling deploys + service custom-domain support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two product gaps surfaced from the twenty-live debugging session: 1. Vibn frontend now has a healthcheck on / port 3000. Coolify will wait for the new container to be healthy before swapping traffic, so deploys no longer drop in-flight chat SSE streams. (Setting was applied via Coolify API; commit just documents.) 2. apps_domains_set now handles SERVICES (template-based apps like Twenty CRM, n8n) — not just applications. Setting service_apps.fqdn in the DB alone gets reverted by Coolify's deploy pipeline, so we replicate the Livewire EditDomain.php save flow via tinker over SSH: write fqdn → save → updateCompose() → service.parse(). After this apps_deploy regenerates Traefik labels with the custom domain. Auto-detects service vs application by uuid lookup. New { port } parameter lets the AI pin the upstream port for services that require one (Coolify hard-fails the save without it). Tool description rewritten with the new behavior + a worked example so the AI uses the right pipeline first try. Made-with: Cursor --- app/api/mcp/route.ts | 81 ++++++++++++++++++++++++++++++++++++++++++-- lib/ai/vibn-tools.ts | 18 +++++++--- 2 files changed, 93 insertions(+), 6 deletions(-) diff --git a/app/api/mcp/route.ts b/app/api/mcp/route.ts index d0d8d688..1ebf627b 100644 --- a/app/api/mcp/route.ts +++ b/app/api/mcp/route.ts @@ -2256,7 +2256,7 @@ async function toolAppsDomainsSet(principal: Principal, params: Record `https://${d}${port ? `:${port}` : ''}`) + .join(','); + // PHP code: load service, find target inner app by name (or auto-pick), + // set fqdn, save, regenerate compose, re-parse. All quoting carefully + // escaped because we shell this through ssh. + const phpCode = ` +$service = App\\Models\\Service::where('uuid', '${appUuid}')->first(); +if (!$service) { echo 'service-not-found'; exit; } +$apps = $service->applications()->get(); +$target = ${inner === 'auto' ? '$apps->first(fn($a) => !str_contains(strtolower($a->name), \'worker\') && !str_contains(strtolower($a->name), \'job\')) ?? $apps->first()' : `$apps->firstWhere('name', '${inner}')`}; +if (!$target) { echo 'inner-app-not-found'; exit; } +$target->fqdn = '${fqdnsForUrl}'; +$target->save(); +updateCompose($target); +$service->parse(); +echo 'fqdn-saved=' . $target->fresh()->fqdn; +`; + const result = await runOnCoolifyHost( + `docker exec coolify php artisan tinker --execute=${shellEscape(phpCode)}` + ); + const out = (result.stdout || '').trim(); + if (out.includes('service-not-found') || out.includes('inner-app-not-found')) { + return NextResponse.json({ + error: out, + stderr: result.stderr, + }, { status: 404 }); + } + return NextResponse.json({ + result: { + uuid: appUuid, + kind: 'service', + domains: normalized, + innerApp: inner, + tinkerOut: out, + summaryHint: `Custom domain saved on service ${appUuid}. Now call apps_deploy { uuid: "${appUuid}" } to regenerate Traefik labels and bring the new domain live.`, + }, + }); + } + + const app = await getApplicationInWorkspace(appUuid, ownedUuids); + const buildPack = (app.build_pack ?? 'nixpacks') as string; await setApplicationDomains(appUuid, normalized, { forceOverride: true, buildPack, @@ -2284,6 +2353,7 @@ async function toolAppsDomainsSet(principal: Principal, params: Record