diff --git a/app/api/mcp/route.ts b/app/api/mcp/route.ts index 1ebf627b..90bd1058 100644 --- a/app/api/mcp/route.ts +++ b/app/api/mcp/route.ts @@ -2306,9 +2306,16 @@ 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. + // PHP code: load service, find target inner app, save fqdn, then + // INLINE-REPLACE ${SERVICE_URL_} / ${SERVICE_FQDN_} + // interpolations in docker_compose_raw with the literal user-provided + // URL. This bypasses Coolify's parse() — which iterates ALL apps in + // a multi-container template (twenty + worker + redis + postgres) + // and overwrites SERVICE_URL_ env vars based on whichever + // ServiceApplication.fqdn it sees first, often producing the + // *.sslip.io fallback. Hardcoding the URL into the compose template + // makes the deploy survive future parse() calls (which the deploy + // job runs internally on every redeploy). const phpCode = ` $service = App\\Models\\Service::where('uuid', '${appUuid}')->first(); if (!$service) { echo 'service-not-found'; exit; } @@ -2317,8 +2324,42 @@ $target = ${inner === 'auto' ? '$apps->first(fn($a) => !str_contains(strtolower( if (!$target) { echo 'inner-app-not-found'; exit; } $target->fqdn = '${fqdnsForUrl}'; $target->save(); + +// Parse the saved fqdn into base URL + bare host so we can substitute. +$first = explode(',', $target->fqdn)[0]; +preg_match('#^(https?://)([^:/]+)(?::(\\\\d+))?#', $first, $mm); +$scheme = $mm[1] ?? 'https://'; +$host = $mm[2] ?? ''; +$urlBase = $scheme . $host; +$fqdnBase = $host; + +// Compose template uses the inner app NAME uppercased, dashes→underscores. +$svcVar = strtoupper(str_replace('-', '_', $target->name)); +$raw = $service->docker_compose_raw ?? ''; + +// Replace port-specific first so the bare match below doesn't eat the prefix. +$raw = preg_replace_callback( + '/\\\\$\\\\{SERVICE_URL_' . preg_quote($svcVar, '/') . '_(\\\\d+)\\\\}/', + fn($m) => $urlBase . ':' . $m[1], + $raw +); +$raw = preg_replace_callback( + '/\\\\$\\\\{SERVICE_FQDN_' . preg_quote($svcVar, '/') . '_(\\\\d+)\\\\}/', + fn($m) => $fqdnBase . ':' . $m[1], + $raw +); +$raw = str_replace('\\\${SERVICE_URL_' . $svcVar . '}', $urlBase, $raw); +$raw = str_replace('\\\${SERVICE_FQDN_' . $svcVar . '}', $fqdnBase, $raw); + +$service->docker_compose_raw = $raw; +$service->save(); + +// updateCompose() rewrites the rendered docker_compose output and the +// SERVICE_URL_/SERVICE_FQDN_ env vars from $target->fqdn. We deliberately +// SKIP $service->parse() because it picks the wrong inner app in +// multi-container templates and reverts env vars to sslip.io. updateCompose($target); -$service->parse(); + echo 'fqdn-saved=' . $target->fresh()->fqdn; `; const result = await runOnCoolifyHost(