From 47c61feb8470e353f953baceca774735bae86b6d Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sat, 5 Sep 2026 14:24:24 +0200 Subject: [PATCH] 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. --- .../Api/ApplicationsController.php | 36 ++++++++++++ app/Models/Application.php | 9 ++- .../Api/ApplicationSettingsApiTest.php | 55 +++++++++++++++++++ 3 files changed, 97 insertions(+), 3 deletions(-) diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php index 487d319e1..dbe0f8633 100644 --- a/app/Http/Controllers/Api/ApplicationsController.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -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 $domains + * @param array|null $existingOverrides + * @return array{Collection, ?array} + */ + 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']]; + } } diff --git a/app/Models/Application.php b/app/Models/Application.php index 7f7ce5ea1..2ac352149 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -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(); } diff --git a/tests/Feature/Api/ApplicationSettingsApiTest.php b/tests/Feature/Api/ApplicationSettingsApiTest.php index 55d25927a..a4c22a2b7 100644 --- a/tests/Feature/Api/ApplicationSettingsApiTest.php +++ b/tests/Feature/Api/ApplicationSettingsApiTest.php @@ -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([