diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php index a0f810480..69c6c1f14 100644 --- a/bootstrap/helpers/docker.php +++ b/bootstrap/helpers/docker.php @@ -11,6 +11,16 @@ use Spatie\Url\Url; use Symfony\Component\Yaml\Yaml; use Visus\Cuid2\Cuid2; +/** + * Generate a stable 4-character hash from a service name for uniqueness + * This is used to differentiate services like "api.test" and "api-test" + * which would otherwise collide when normalized. + */ +function serviceNameHash(string $serviceName): string +{ + return substr(md5($serviceName), 0, 4); +} + function getCurrentApplicationContainerStatus(Server $server, int $id, ?int $pullRequestId = null, ?bool $includePullrequests = false): Collection { $containers = collect([]); @@ -467,8 +477,13 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ $http_label = "http-{$loop}-{$uuid}"; $https_label = "https-{$loop}-{$uuid}"; if ($service_name) { - $http_label = "http-{$loop}-{$uuid}-{$service_name}"; - $https_label = "https-{$loop}-{$uuid}-{$service_name}"; + // Normalize service name for Traefik labels by replacing dots with hyphens + // This prevents label parsing issues with service names like "api.test" + // Add a 4-char hash to ensure uniqueness for services like "api.test" vs "api-test" + $normalized_service_name = str($service_name)->replace('.', '-')->value(); + $hash = serviceNameHash($service_name); + $http_label = "http-{$loop}-{$uuid}-{$normalized_service_name}-{$hash}"; + $https_label = "https-{$loop}-{$uuid}-{$normalized_service_name}-{$hash}"; } if (str($image)->contains('ghost')) { $labels->push("traefik.http.middlewares.redir-ghost-{$uuid}.redirectregex.regex=^{$path}/(.*)"); diff --git a/bootstrap/helpers/parsers.php b/bootstrap/helpers/parsers.php index 43ba58e59..652e7ba57 100644 --- a/bootstrap/helpers/parsers.php +++ b/bootstrap/helpers/parsers.php @@ -601,24 +601,27 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int if ($resource->build_pack === 'dockercompose') { // Check if a service with this name actually exists $serviceExists = false; + $actualServiceName = null; foreach ($services as $serviceNameKey => $service) { $transformedServiceName = str($serviceNameKey)->replace('-', '_')->replace('.', '_')->value(); if ($transformedServiceName === $serviceName) { $serviceExists = true; + $actualServiceName = $serviceNameKey; // Store the ORIGINAL service name break; } } // Only add domain if the service exists - if ($serviceExists) { + if ($serviceExists && $actualServiceName) { $domains = collect(json_decode(data_get($resource, 'docker_compose_domains'))) ?? collect([]); - $domainExists = data_get($domains->get($serviceName), 'domain'); + // Use the ORIGINAL service name as the key to avoid collisions + $domainExists = data_get($domains->get($actualServiceName), 'domain'); // Update domain using URL with port if applicable $domainValue = $port ? $urlWithPort : $url; if (is_null($domainExists)) { - $domains->put($serviceName, [ + $domains->put($actualServiceName, [ 'domain' => $domainValue, ]); $resource->docker_compose_domains = $domains->toJson(); @@ -1077,8 +1080,8 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int if ($resource->build_pack !== 'dockercompose') { $domains = collect([]); } - $changedServiceName = str($serviceName)->replace('-', '_')->replace('.', '_')->value(); - $fqdns = data_get($domains, "$changedServiceName.domain"); + // Use the original service name for lookup (no transformation needed) + $fqdns = data_get($domains, "$serviceName.domain"); // Generate SERVICE_FQDN & SERVICE_URL for dockercompose if ($resource->build_pack === 'dockercompose') { foreach ($domains as $forServiceName => $domain) {