Skip to content

Commit 7c6515b

Browse files
andrasbacsaiclaude
andcommitted
fix: resolve Docker Compose service name collisions with dots
Services like "api.test" and "api-test" now create unique Traefik labels by normalizing dots to hyphens and adding a 4-char hash suffix. Update docker_compose_domains storage to use original service names instead of transformed names to prevent key collisions. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent bf428a0 commit 7c6515b

File tree

2 files changed

+33
-11
lines changed

2 files changed

+33
-11
lines changed

bootstrap/helpers/docker.php

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,16 @@
1111
use Symfony\Component\Yaml\Yaml;
1212
use Visus\Cuid2\Cuid2;
1313

14+
/**
15+
* Generate a stable 4-character hash from a service name for uniqueness
16+
* This is used to differentiate services like "api.test" and "api-test"
17+
* which would otherwise collide when normalized.
18+
*/
19+
function serviceNameHash(string $serviceName): string
20+
{
21+
return substr(md5($serviceName), 0, 4);
22+
}
23+
1424
function getCurrentApplicationContainerStatus(Server $server, int $id, ?int $pullRequestId = null, ?bool $includePullrequests = false): Collection
1525
{
1626
$containers = collect([]);
@@ -437,8 +447,13 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
437447
$http_label = "http-{$loop}-{$uuid}";
438448
$https_label = "https-{$loop}-{$uuid}";
439449
if ($service_name) {
440-
$http_label = "http-{$loop}-{$uuid}-{$service_name}";
441-
$https_label = "https-{$loop}-{$uuid}-{$service_name}";
450+
// Normalize service name for Traefik labels by replacing dots with hyphens
451+
// This prevents label parsing issues with service names like "api.test"
452+
// Add a 4-char hash to ensure uniqueness for services like "api.test" vs "api-test"
453+
$normalized_service_name = str($service_name)->replace('.', '-')->value();
454+
$hash = serviceNameHash($service_name);
455+
$http_label = "http-{$loop}-{$uuid}-{$normalized_service_name}-{$hash}";
456+
$https_label = "https-{$loop}-{$uuid}-{$normalized_service_name}-{$hash}";
442457
}
443458
if (str($image)->contains('ghost')) {
444459
$labels->push("traefik.http.middlewares.redir-ghost-{$uuid}.redirectregex.regex=^{$path}/(.*)");

bootstrap/helpers/parsers.php

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -534,27 +534,30 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
534534
if ($resource->build_pack === 'dockercompose') {
535535
// Check if a service with this name actually exists
536536
$serviceExists = false;
537+
$actualServiceName = null;
537538
foreach ($services as $serviceName => $service) {
538539
$transformedServiceName = str($serviceName)->replace('-', '_')->replace('.', '_')->value();
539540
if ($transformedServiceName === $fqdnFor) {
540541
$serviceExists = true;
542+
$actualServiceName = $serviceName; // Store the ORIGINAL service name
541543
break;
542544
}
543545
}
544546

545547
// Only add domain if the service exists
546-
if ($serviceExists) {
548+
if ($serviceExists && $actualServiceName) {
547549
$domains = collect(json_decode(data_get($resource, 'docker_compose_domains'))) ?? collect([]);
548-
$domainExists = data_get($domains->get($fqdnFor), 'domain');
550+
// Use the ORIGINAL service name as the key to avoid collisions
551+
$domainExists = data_get($domains->get($actualServiceName), 'domain');
549552
$envExists = $resource->environment_variables()->where('key', $key->value())->first();
550553
if (str($domainExists)->replace('http://', '')->replace('https://', '')->value() !== $envExists->value) {
551554
$envExists->update([
552555
'value' => $url,
553556
]);
554557
}
555558
if (is_null($domainExists)) {
556-
// Put URL in the domains array instead of FQDN
557-
$domains->put((string) $fqdnFor, [
559+
// Put URL in the domains array using ORIGINAL service name
560+
$domains->put((string) $actualServiceName, [
558561
'domain' => $url,
559562
]);
560563
$resource->docker_compose_domains = $domains->toJson();
@@ -589,26 +592,30 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
589592
if ($resource->build_pack === 'dockercompose') {
590593
// Check if a service with this name actually exists
591594
$serviceExists = false;
595+
$actualServiceName = null;
592596
foreach ($services as $serviceName => $service) {
593597
$transformedServiceName = str($serviceName)->replace('-', '_')->replace('.', '_')->value();
594598
if ($transformedServiceName === $urlFor) {
595599
$serviceExists = true;
600+
$actualServiceName = $serviceName; // Store the ORIGINAL service name
596601
break;
597602
}
598603
}
599604

600605
// Only add domain if the service exists
601-
if ($serviceExists) {
606+
if ($serviceExists && $actualServiceName) {
602607
$domains = collect(json_decode(data_get($resource, 'docker_compose_domains'))) ?? collect([]);
603-
$domainExists = data_get($domains->get($urlFor), 'domain');
608+
// Use the ORIGINAL service name as the key to avoid collisions
609+
$domainExists = data_get($domains->get($actualServiceName), 'domain');
604610
$envExists = $resource->environment_variables()->where('key', $key->value())->first();
605611
if ($domainExists !== $envExists->value) {
606612
$envExists->update([
607613
'value' => $urlWithPort,
608614
]);
609615
}
610616
if (is_null($domainExists)) {
611-
$domains->put((string) $urlFor, [
617+
// Put URL in the domains array using ORIGINAL service name
618+
$domains->put((string) $actualServiceName, [
612619
'domain' => $urlWithPort,
613620
]);
614621
$resource->docker_compose_domains = $domains->toJson();
@@ -1067,8 +1074,8 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
10671074
if ($resource->build_pack !== 'dockercompose') {
10681075
$domains = collect([]);
10691076
}
1070-
$changedServiceName = str($serviceName)->replace('-', '_')->replace('.', '_')->value();
1071-
$fqdns = data_get($domains, "$changedServiceName.domain");
1077+
// Use the original service name for lookup (no transformation needed)
1078+
$fqdns = data_get($domains, "$serviceName.domain");
10721079
// Generate SERVICE_FQDN & SERVICE_URL for dockercompose
10731080
if ($resource->build_pack === 'dockercompose') {
10741081
foreach ($domains as $forServiceName => $domain) {

0 commit comments

Comments
 (0)