fix(api): persist Docker Compose domain ports as overrides

Normalize Compose domains on create and update, retaining explicit ports in
`domain_port_overrides` while storing port-free domain values. Preserve empty
Compose FQDNs and cover both API flows with feature tests.
This commit is contained in:
Andras Bacsai 2026-09-05 14:24:24 +02:00
parent 208f7720bc
commit 47c61feb84
3 changed files with 97 additions and 3 deletions

View file

@ -27,6 +27,7 @@
use App\Support\ValidationPatterns;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
@ -1455,7 +1456,9 @@ private function create_application(Request $request, $type)
$request->offsetUnset('docker_compose_domains');
}
if ($dockerComposeDomainsJson->count() > 0) {
[$dockerComposeDomainsJson, $domainPortOverrides] = $this->normalizeDockerComposeDomainPorts($dockerComposeDomainsJson);
$application->docker_compose_domains = json_encode($dockerComposeDomainsJson);
$application->domain_port_overrides = $domainPortOverrides;
}
$repository_url_parsed = Url::fromString($request->git_repository);
$git_host = $repository_url_parsed->getHost();
@ -1719,7 +1722,9 @@ private function create_application(Request $request, $type)
$request->offsetUnset('docker_compose_domains');
}
if ($dockerComposeDomainsJson->count() > 0) {
[$dockerComposeDomainsJson, $domainPortOverrides] = $this->normalizeDockerComposeDomainPorts($dockerComposeDomainsJson);
$application->docker_compose_domains = json_encode($dockerComposeDomainsJson);
$application->domain_port_overrides = $domainPortOverrides;
}
$application->fqdn = $fqdn;
$application->git_repository = str($gitRepository)->trim()->toString();
@ -1950,7 +1955,9 @@ private function create_application(Request $request, $type)
$request->offsetUnset('docker_compose_domains');
}
if ($dockerComposeDomainsJson->count() > 0) {
[$dockerComposeDomainsJson, $domainPortOverrides] = $this->normalizeDockerComposeDomainPorts($dockerComposeDomainsJson);
$application->docker_compose_domains = json_encode($dockerComposeDomainsJson);
$application->domain_port_overrides = $domainPortOverrides;
}
$application->fqdn = $fqdn;
$application->private_key_id = $privateKey->id;
@ -3369,7 +3376,12 @@ public function update_by_uuid(Request $request)
}
if ($dockerComposeDomainsJson->count() > 0) {
[$dockerComposeDomainsJson, $domainPortOverrides] = $this->normalizeDockerComposeDomainPorts(
$dockerComposeDomainsJson,
$application->domain_port_overrides,
);
data_set($data, 'docker_compose_domains', json_encode($dockerComposeDomainsJson));
data_set($data, 'domain_port_overrides', $domainPortOverrides);
}
$requestHasNoindexDomains = $request->has('noindex_domains');
data_forget($data, 'noindex_domains');
@ -6116,4 +6128,28 @@ public function remove_destination(Request $request): JsonResponse
return response()->json(['message' => 'Destination detached.']);
}
/**
* @param Collection<string, array{domain: ?string, redirect?: string}> $domains
* @param array<string, int|string>|null $existingOverrides
* @return array{Collection<string, array{domain: ?string, redirect?: string}>, ?array<string, int>}
*/
private function normalizeDockerComposeDomainPorts(Collection $domains, ?array $existingOverrides = null): array
{
$allDomains = $domains
->pluck('domain')
->filter()
->implode(',');
$normalized = DomainPortOverrides::normalize($allDomains, $existingOverrides);
$domains = $domains->map(function (array $entry): array {
$entry['domain'] = collect(ValidationPatterns::applicationDomainList($entry['domain'] ?? null))
->map(fn (string $domain): string => DomainPortOverrides::withoutPort($domain))
->implode(',');
return $entry;
});
return [$domains, $normalized['overrides']];
}
}

View file

@ -3,6 +3,7 @@
namespace App\Models;
use App\Enums\ApplicationDeploymentStatus;
use App\Enums\BuildPackTypes;
use App\Services\ConfigurationGenerator;
use App\Services\DeploymentConfiguration\ApplicationConfigurationSnapshot;
use App\Services\DeploymentConfiguration\ConfigurationDiff;
@ -291,9 +292,11 @@ protected static function booted()
if ($application->fqdn === '') {
$application->fqdn = null;
}
$normalized = DomainPortOverrides::normalize($application->fqdn, $application->domain_port_overrides);
$application->fqdn = $normalized['fqdn'];
$application->domain_port_overrides = $normalized['overrides'];
if ($application->build_pack !== BuildPackTypes::DOCKERCOMPOSE->value || filled($application->fqdn)) {
$normalized = DomainPortOverrides::normalize($application->fqdn, $application->domain_port_overrides);
$application->fqdn = $normalized['fqdn'];
$application->domain_port_overrides = $normalized['overrides'];
}
$payload['fqdn'] = $application->fqdn;
$application->syncNoindexDomains();
}

View file

@ -165,6 +165,61 @@ function recommendedApplicationSettingsPayload(): array
->and($labels)->not->toContain('loadbalancer.server.port=3000');
});
test('compose domain ports are stored as overrides when updating through the API', function () {
$this->application->update([
'build_pack' => 'dockercompose',
'docker_compose_raw' => "services:\n api:\n image: nginx\n frontend:\n image: nginx\n",
'docker_compose_domains' => json_encode([
'api' => ['domain' => 'https://api.example.com'],
'frontend' => ['domain' => 'https://app.example.com'],
]),
]);
$this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
->patchJson("/api/v1/applications/{$this->application->uuid}", [
'docker_compose_domains' => [
['name' => 'api', 'domain' => 'https://api.example.com'],
['name' => 'frontend', 'domain' => 'https://app.example.com:80'],
],
])
->assertOk();
$application = $this->application->fresh();
$domains = json_decode($application->docker_compose_domains, true);
expect(data_get($domains, 'frontend.domain'))->toBe('https://app.example.com')
->and($application->domain_port_overrides)->toBe([
'https://app.example.com' => 80,
]);
});
test('compose domain ports are stored as overrides when creating through the API', function () {
Queue::fake();
$response = $this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
->postJson('/api/v1/applications/public', [
'project_uuid' => $this->project->uuid,
'environment_uuid' => $this->environment->uuid,
'server_uuid' => $this->server->uuid,
'git_repository' => 'https://gitlab.com/coolify/compose-domain-port-test',
'git_branch' => 'main',
'build_pack' => 'dockercompose',
'autogenerate_domain' => false,
'docker_compose_domains' => [
['name' => 'frontend', 'domain' => 'https://app.example.com:80'],
],
])
->assertCreated();
$application = Application::where('uuid', $response->json('uuid'))->firstOrFail();
$domains = json_decode($application->docker_compose_domains, true);
expect(data_get($domains, 'frontend.domain'))->toBe('https://app.example.com')
->and($application->domain_port_overrides)->toBe([
'https://app.example.com' => 80,
]);
});
test('http basic auth updates regenerate managed labels', function () {
$this->application->settings->update(['is_container_label_readonly_enabled' => true]);
$this->application->update([