feat(domains): support per-domain internal port overrides (#11594)

This commit is contained in:
Andras Bacsai
2026-09-02 19:39:19 +02:00
committed by GitHub
parent 7a7564e6b3
commit e2e91fbb85
70 changed files with 4025 additions and 297 deletions
+14 -4
View File
@@ -530,7 +530,7 @@ function isNoindexDomain(string $domain, ?Collection $noindex_domains): bool
->contains(ValidationPatterns::normalizeApplicationDomainUrl($domain));
}
function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?bool $is_stripprefix_enabled = true, ?string $service_name = null, ?string $image = null, string $redirect_direction = 'both', ?string $predefinedPort = null, bool $is_http_basic_auth_enabled = false, ?string $http_basic_auth_username = null, ?string $http_basic_auth_password = null, ?Collection $noindex_domains = null)
function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?bool $is_stripprefix_enabled = true, ?string $service_name = null, ?string $image = null, string $redirect_direction = 'both', ?string $predefinedPort = null, bool $is_http_basic_auth_enabled = false, ?string $http_basic_auth_username = null, ?string $http_basic_auth_password = null, ?Collection $noindex_domains = null, array $domainPortOverrides = [])
{
$labels = collect([]);
if ($serviceLabels) {
@@ -554,7 +554,8 @@ function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains,
if ($schema === 'https' && ! $is_force_https_enabled) {
$siteAddress = "http://{$host}, https://{$host}";
}
$port = $url->getPort();
$portlessDomain = ServiceApplication::withoutPort($domain);
$port = $url->getPort() ?? ($domainPortOverrides[$portlessDomain] ?? null);
$handle = 'handle_path';
if (! $is_stripprefix_enabled) {
$handle = 'handle';
@@ -600,7 +601,7 @@ function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains,
return $labels->sort();
}
function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?bool $is_stripprefix_enabled = true, ?string $service_name = null, bool $generate_unique_uuid = false, ?string $image = null, string $redirect_direction = 'both', bool $is_http_basic_auth_enabled = false, ?string $http_basic_auth_username = null, ?string $http_basic_auth_password = null, ?Collection $noindex_domains = null, bool $escape_redirect_replacement_for_compose = true)
function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?bool $is_stripprefix_enabled = true, ?string $service_name = null, bool $generate_unique_uuid = false, ?string $image = null, string $redirect_direction = 'both', bool $is_http_basic_auth_enabled = false, ?string $http_basic_auth_username = null, ?string $http_basic_auth_password = null, ?Collection $noindex_domains = null, bool $escape_redirect_replacement_for_compose = true, array $domainPortOverrides = [])
{
$labels = collect([]);
$labels->push('traefik.enable=true');
@@ -655,7 +656,8 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$host = $url->getHost();
$path = $url->getPath();
$schema = $url->getScheme();
$port = $url->getPort();
$portlessDomain = ServiceApplication::withoutPort($domain);
$port = $url->getPort() ?? ($domainPortOverrides[$portlessDomain] ?? null);
if (is_null($port) && ! is_null($onlyPort)) {
$port = $onlyPort;
}
@@ -898,6 +900,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
http_basic_auth_username: $application->http_basic_auth_username,
http_basic_auth_password: $application->http_basic_auth_password,
noindex_domains: $noindexDomains,
domainPortOverrides: $application->domain_port_overrides ?? [],
));
break;
case ProxyTypes::CADDY->value:
@@ -914,6 +917,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
http_basic_auth_username: $application->http_basic_auth_username,
http_basic_auth_password: $application->http_basic_auth_password,
noindex_domains: $noindexDomains,
domainPortOverrides: $application->domain_port_overrides ?? [],
));
break;
}
@@ -931,6 +935,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
http_basic_auth_password: $application->http_basic_auth_password,
noindex_domains: $noindexDomains,
escape_redirect_replacement_for_compose: false,
domainPortOverrides: $application->domain_port_overrides ?? [],
));
$labels = $labels->merge(fqdnLabelsForCaddy(
network: $application->destination->network,
@@ -945,6 +950,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
http_basic_auth_username: $application->http_basic_auth_username,
http_basic_auth_password: $application->http_basic_auth_password,
noindex_domains: $noindexDomains,
domainPortOverrides: $application->domain_port_overrides ?? [],
));
}
}
@@ -972,6 +978,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
http_basic_auth_password: $application->http_basic_auth_password,
noindex_domains: $noindexDomains,
escape_redirect_replacement_for_compose: false,
domainPortOverrides: $preview->domain_port_overrides ?? [],
));
break;
case ProxyTypes::CADDY->value:
@@ -987,6 +994,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
http_basic_auth_username: $application->http_basic_auth_username,
http_basic_auth_password: $application->http_basic_auth_password,
noindex_domains: $noindexDomains,
domainPortOverrides: $preview->domain_port_overrides ?? [],
));
break;
}
@@ -1003,6 +1011,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
http_basic_auth_password: $application->http_basic_auth_password,
noindex_domains: $noindexDomains,
escape_redirect_replacement_for_compose: false,
domainPortOverrides: $preview->domain_port_overrides ?? [],
));
$labels = $labels->merge(fqdnLabelsForCaddy(
network: $application->destination->network,
@@ -1016,6 +1025,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
http_basic_auth_username: $application->http_basic_auth_username,
http_basic_auth_password: $application->http_basic_auth_password,
noindex_domains: $noindexDomains,
domainPortOverrides: $preview->domain_port_overrides ?? [],
));
}
}
+42 -27
View File
@@ -1265,24 +1265,16 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
$fqdns = collect([]);
}
} else {
$fqdns = $fqdns->map(function ($fqdn) use ($pullRequestId, $resource) {
$preview = ApplicationPreview::findPreviewByApplicationAndPullId($resource->id, $pullRequestId);
$url = Url::fromString($fqdn);
$template = $resource->preview_url_template;
$host = $url->getHost();
$schema = $url->getScheme();
$portInt = $url->getPort();
$port = $portInt !== null ? ':'.$portInt : '';
$random = new_public_id();
$preview_fqdn = str_replace('{{random}}', $random, $template);
$preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn);
$preview_fqdn = str_replace('{{pr_id}}', $pullRequestId, $preview_fqdn);
$preview_fqdn = "$schema://$preview_fqdn{$port}";
$preview->fqdn = $preview_fqdn;
$preview->save();
return $preview_fqdn;
});
$generatedDomains = $fqdns->map(
fn ($fqdn) => $preview->generatedPreviewDomain((string) $fqdn)
);
$fqdns = $generatedDomains->pluck('url');
$preview->fqdn = $fqdns->implode(',');
$preview->domain_port_overrides = $generatedDomains
->filter(fn (array $generated): bool => filled($generated['port']))
->mapWithKeys(fn (array $generated): array => [$generated['url'] => $generated['port']])
->all();
$preview->save();
}
}
}
@@ -1359,6 +1351,14 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
$redirectDirection = in_array($composeRedirect, ['www', 'non-www', 'both'], true)
? $composeRedirect
: 'both';
$previewForPorts = $isPullRequest
? ($resource->previews()->find($preview_id) ?? ApplicationPreview::where('application_id', $resource->id)->where('pull_request_id', $pullRequestId)->first())
: null;
$domainPortOverrides = $isPullRequest
? ($previewForPorts?->domain_port_overrides ?? [])
: ($originalResource->domain_port_overrides ?? []);
$exposedPorts = $originalResource->settings->is_static ? [80] : $originalResource->ports_exposes_array;
$onlyPort = count($exposedPorts) > 0 ? $exposedPorts[0] : null;
if (! $use_network_mode && (! $shouldGenerateLabelsExactly || $server->proxyType() === ProxyTypes::TRAEFIK->value)) {
$serviceLabels = addTraefikDockerNetworkLabel($serviceLabels, $baseNetwork->first());
}
@@ -1374,8 +1374,10 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
is_stripprefix_enabled: $originalResource->isStripprefixEnabled(),
service_name: $serviceName,
image: $image,
onlyPort: $onlyPort,
noindex_domains: $noindexDomains,
redirect_direction: $redirectDirection
redirect_direction: $redirectDirection,
domainPortOverrides: $domainPortOverrides,
));
break;
case ProxyTypes::CADDY->value:
@@ -1389,9 +1391,11 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
is_stripprefix_enabled: $originalResource->isStripprefixEnabled(),
service_name: $serviceName,
image: $image,
onlyPort: $onlyPort,
predefinedPort: $predefinedPort,
noindex_domains: $noindexDomains,
redirect_direction: $redirectDirection
redirect_direction: $redirectDirection,
domainPortOverrides: $domainPortOverrides,
));
break;
}
@@ -1405,8 +1409,10 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
is_stripprefix_enabled: $originalResource->isStripprefixEnabled(),
service_name: $serviceName,
image: $image,
onlyPort: $onlyPort,
noindex_domains: $noindexDomains,
redirect_direction: $redirectDirection
redirect_direction: $redirectDirection,
domainPortOverrides: $domainPortOverrides,
));
$serviceLabels = $serviceLabels->merge(fqdnLabelsForCaddy(
network: $labelNetwork,
@@ -1418,9 +1424,11 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
is_stripprefix_enabled: $originalResource->isStripprefixEnabled(),
service_name: $serviceName,
image: $image,
onlyPort: $onlyPort,
predefinedPort: $predefinedPort,
noindex_domains: $noindexDomains,
redirect_direction: $redirectDirection
redirect_direction: $redirectDirection,
domainPortOverrides: $domainPortOverrides,
));
}
}
@@ -1849,11 +1857,7 @@ function serviceParser(Service $resource): Collection
// Only save fqdn to ServiceApplication, not ServiceDatabase
if ($isServiceApplication && is_null($savedService->fqdn)) {
// Save URL (with scheme) to database, not FQDN
if ((int) $resource->compose_parsing_version >= 5 && version_compare(config('constants.coolify.version'), '4.0.0-beta.420.7', '>=')) {
$savedService->fqdn = $urlWithPort;
} else {
$savedService->fqdn = $urlWithPort;
}
$savedService->fqdn = $url;
$savedService->save();
}
@@ -2636,6 +2640,9 @@ function serviceParser(Service $resource): Collection
$redirectDirection = in_array(data_get($originalResource, 'redirect'), ['www', 'non-www', 'both'], true)
? data_get($originalResource, 'redirect')
: 'both';
$onlyPort = $originalResource instanceof ServiceApplication
? ($originalResource->getRequiredPort() ?? $predefinedPort)
: $predefinedPort;
if (! $use_network_mode && (! $shouldGenerateLabelsExactly || $server->proxyType() === ProxyTypes::TRAEFIK->value)) {
$serviceLabels = addTraefikDockerNetworkLabel($serviceLabels, $baseNetwork->first());
}
@@ -2651,6 +2658,8 @@ function serviceParser(Service $resource): Collection
is_stripprefix_enabled: $originalResource->isStripprefixEnabled(),
service_name: $serviceName,
image: $image,
onlyPort: $onlyPort,
domainPortOverrides: $originalResource->domain_port_overrides ?? [],
noindex_domains: $noindexDomains,
redirect_direction: $redirectDirection
));
@@ -2666,7 +2675,9 @@ function serviceParser(Service $resource): Collection
is_stripprefix_enabled: $originalResource->isStripprefixEnabled(),
service_name: $serviceName,
image: $image,
onlyPort: $onlyPort,
predefinedPort: $predefinedPort,
domainPortOverrides: $originalResource->domain_port_overrides ?? [],
noindex_domains: $noindexDomains,
redirect_direction: $redirectDirection
));
@@ -2682,6 +2693,8 @@ function serviceParser(Service $resource): Collection
is_stripprefix_enabled: $originalResource->isStripprefixEnabled(),
service_name: $serviceName,
image: $image,
onlyPort: $onlyPort,
domainPortOverrides: $originalResource->domain_port_overrides ?? [],
noindex_domains: $noindexDomains,
redirect_direction: $redirectDirection
));
@@ -2695,7 +2708,9 @@ function serviceParser(Service $resource): Collection
is_stripprefix_enabled: $originalResource->isStripprefixEnabled(),
service_name: $serviceName,
image: $image,
onlyPort: $onlyPort,
predefinedPort: $predefinedPort,
domainPortOverrides: $originalResource->domain_port_overrides ?? [],
noindex_domains: $noindexDomains,
redirect_direction: $redirectDirection
));
+54 -20
View File
@@ -3103,6 +3103,12 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$redirectDirection = in_array(data_get($savedService, 'redirect'), ['www', 'non-www', 'both'], true)
? data_get($savedService, 'redirect')
: 'both';
$domainPortOverrides = $savedService instanceof ServiceApplication
? ($savedService->domain_port_overrides ?? [])
: [];
$onlyPort = $savedService instanceof ServiceApplication
? ($savedService->getRequiredPort() ?? $predefinedPort)
: $predefinedPort;
if ($shouldGenerateLabelsExactly) {
switch ($resource->server->proxyType()) {
case ProxyTypes::TRAEFIK->value:
@@ -3115,8 +3121,10 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
is_stripprefix_enabled: $savedService->isStripprefixEnabled(),
service_name: $serviceName,
image: data_get($service, 'image'),
onlyPort: $onlyPort,
noindex_domains: $noindexDomains,
redirect_direction: $redirectDirection
redirect_direction: $redirectDirection,
domainPortOverrides: $domainPortOverrides,
));
break;
case ProxyTypes::CADDY->value:
@@ -3130,8 +3138,11 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
is_stripprefix_enabled: $savedService->isStripprefixEnabled(),
service_name: $serviceName,
image: data_get($service, 'image'),
onlyPort: $onlyPort,
predefinedPort: $predefinedPort,
noindex_domains: $noindexDomains,
redirect_direction: $redirectDirection
redirect_direction: $redirectDirection,
domainPortOverrides: $domainPortOverrides,
));
break;
}
@@ -3145,8 +3156,10 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
is_stripprefix_enabled: $savedService->isStripprefixEnabled(),
service_name: $serviceName,
image: data_get($service, 'image'),
onlyPort: $onlyPort,
noindex_domains: $noindexDomains,
redirect_direction: $redirectDirection
redirect_direction: $redirectDirection,
domainPortOverrides: $domainPortOverrides,
));
$serviceLabels = $serviceLabels->merge(fqdnLabelsForCaddy(
network: $resource->destination->network,
@@ -3158,8 +3171,11 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
is_stripprefix_enabled: $savedService->isStripprefixEnabled(),
service_name: $serviceName,
image: data_get($service, 'image'),
onlyPort: $onlyPort,
predefinedPort: $predefinedPort,
noindex_domains: $noindexDomains,
redirect_direction: $redirectDirection
redirect_direction: $redirectDirection,
domainPortOverrides: $domainPortOverrides,
));
}
}
@@ -3858,6 +3874,13 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$fqdns = str($fqdns)->explode(',');
if ($pull_request_id !== 0) {
$preview = $resource->previews()->find($preview_id);
if (! $preview) {
try {
$preview = ApplicationPreview::findPreviewByApplicationAndPullId($resource->id, $pull_request_id);
} catch (ModelNotFoundException) {
throw new RuntimeException('Preview not found.');
}
}
$docker_compose_domains = json_decode(data_get($preview, 'docker_compose_domains') ?: '[]', true) ?: [];
if (count($docker_compose_domains) > 0) {
$found_fqdn = getComposeServiceDomainString($docker_compose_domains, (string) $serviceName);
@@ -3867,22 +3890,20 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$fqdns = collect([]);
}
} else {
$fqdns = $fqdns->map(function ($fqdn) use ($pull_request_id, $resource) {
$preview = ApplicationPreview::findPreviewByApplicationAndPullId($resource->id, $pull_request_id);
$url = Url::fromString($fqdn);
$template = $resource->preview_url_template;
$host = $url->getHost();
$schema = $url->getScheme();
$random = new_public_id();
$preview_fqdn = str_replace('{{random}}', $random, $template);
$preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn);
$preview_fqdn = str_replace('{{pr_id}}', $pull_request_id, $preview_fqdn);
$preview_fqdn = "$schema://$preview_fqdn";
$preview->fqdn = $preview_fqdn;
$preview->save();
return $preview_fqdn;
});
$generatedDomains = $fqdns->map(
fn ($fqdn) => $preview->generatedPreviewDomain((string) $fqdn)
);
$fqdns = $generatedDomains->pluck('url');
$preview->fqdn = $fqdns->implode(',');
$generatedOverrides = $generatedDomains
->filter(fn (array $generated): bool => filled($generated['port']))
->mapWithKeys(fn (array $generated): array => [$generated['url'] => $generated['port']])
->all();
$preview->domain_port_overrides = array_replace(
$preview->domain_port_overrides ?? [],
$generatedOverrides,
);
$preview->save();
}
}
$noindexDomains = $pull_request_id !== 0 ? $fqdns : $resource->noindexDomains();
@@ -3891,6 +3912,11 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$redirectDirection = in_array($composeRedirect, ['www', 'non-www', 'both'], true)
? $composeRedirect
: 'both';
$domainPortOverrides = $pull_request_id === 0
? ($resource->domain_port_overrides ?? [])
: ($preview?->domain_port_overrides ?? []);
$exposedPorts = $resource->settings->is_static ? [80] : $resource->ports_exposes_array;
$onlyPort = count($exposedPorts) > 0 ? $exposedPorts[0] : null;
if ($shouldGenerateLabelsExactly) {
switch ($server->proxyType()) {
case ProxyTypes::TRAEFIK->value:
@@ -3904,8 +3930,10 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
is_force_https_enabled: $resource->isForceHttpsEnabled(),
is_gzip_enabled: $resource->isGzipEnabled(),
is_stripprefix_enabled: $resource->isStripprefixEnabled(),
onlyPort: $onlyPort,
noindex_domains: $noindexDomains,
redirect_direction: $redirectDirection,
domainPortOverrides: $domainPortOverrides,
)
);
break;
@@ -3920,8 +3948,10 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
is_force_https_enabled: $resource->isForceHttpsEnabled(),
is_gzip_enabled: $resource->isGzipEnabled(),
is_stripprefix_enabled: $resource->isStripprefixEnabled(),
onlyPort: $onlyPort,
noindex_domains: $noindexDomains,
redirect_direction: $redirectDirection,
domainPortOverrides: $domainPortOverrides,
)
);
break;
@@ -3937,8 +3967,10 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
is_force_https_enabled: $resource->isForceHttpsEnabled(),
is_gzip_enabled: $resource->isGzipEnabled(),
is_stripprefix_enabled: $resource->isStripprefixEnabled(),
onlyPort: $onlyPort,
noindex_domains: $noindexDomains,
redirect_direction: $redirectDirection,
domainPortOverrides: $domainPortOverrides,
)
);
$serviceLabels = $serviceLabels->merge(
@@ -3951,8 +3983,10 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
is_force_https_enabled: $resource->isForceHttpsEnabled(),
is_gzip_enabled: $resource->isGzipEnabled(),
is_stripprefix_enabled: $resource->isStripprefixEnabled(),
onlyPort: $onlyPort,
noindex_domains: $noindexDomains,
redirect_direction: $redirectDirection,
domainPortOverrides: $domainPortOverrides,
)
);
}