From f92ee342a1372e8b328dcb3b69a12b820047370e Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 25 Sep 2026 23:18:34 +0200 Subject: [PATCH] fix(proxy): remove legacy Traefik dashboard labels from saved configs Older Coolify versions saved Traefik configs with a router that sends requests with the proxy container name as Host header (for example traefik-coolify-proxy) on port 80 to api@internal. New installs no longer get these labels, but saved configs keep them after an upgrade. - Replace only the unchanged legacy labels with traefik.enable=false, keep comments and formatting, and skip routers the user customized. - A migration and GetProxyConfiguration fix the saved config in the database only. The proxy is not restarted; the UI asks for a restart. - Change the pending notice to "Your configuration changed, please restart the proxy." - Resolve conflict markers committed in the Server Proxy component, which broke the proxy page. Co-Authored-By: Claude Opus 5.5 --- app/Actions/Proxy/GetProxyConfiguration.php | 4 + app/Livewire/Server/Proxy.php | 5 +- bootstrap/helpers/proxy.php | 91 ++++++++++ ...remove_legacy_traefik_dashboard_labels.php | 33 ++++ .../proxy-configuration-warning.blade.php | 21 +-- .../views/livewire/server/proxy.blade.php | 4 +- .../LegacyTraefikDashboardLabelsTest.php | 162 ++++++++++++++++++ tests/Feature/Proxy/RestartProxyTest.php | 8 +- 8 files changed, 306 insertions(+), 22 deletions(-) create mode 100644 database/migrations/2026_09_25_210000_remove_legacy_traefik_dashboard_labels.php create mode 100644 tests/Feature/Proxy/LegacyTraefikDashboardLabelsTest.php diff --git a/app/Actions/Proxy/GetProxyConfiguration.php b/app/Actions/Proxy/GetProxyConfiguration.php index d910b7b0aa..6de9e03de7 100644 --- a/app/Actions/Proxy/GetProxyConfiguration.php +++ b/app/Actions/Proxy/GetProxyConfiguration.php @@ -44,6 +44,10 @@ class GetProxyConfiguration if (empty(trim($proxy_configuration ?? ''))) { $proxy_configuration = $this->backfillFromDisk($server); } + + if (! empty(trim($proxy_configuration ?? '')) && removeLegacyTraefikDashboardExposure($server)) { + $proxy_configuration = $server->proxy->get('last_saved_proxy_configuration'); + } } // Generate default configuration as last resort diff --git a/app/Livewire/Server/Proxy.php b/app/Livewire/Server/Proxy.php index 5c3624a2b6..af30b3a02b 100644 --- a/app/Livewire/Server/Proxy.php +++ b/app/Livewire/Server/Proxy.php @@ -201,7 +201,6 @@ class Proxy extends Component } } -<<<<<<< Updated upstream public function getTraefikVersionForWarningProperty(): ?string { if ($this->server->detected_traefik_version) { @@ -218,7 +217,8 @@ class Proxy extends Component } return $matches[1]; -======= + } + public function loadTraefikCertificates(): void { $this->traefikCertificates = []; @@ -243,7 +243,6 @@ class Proxy extends Component } catch (\Throwable $e) { handleError($e, $this); } ->>>>>>> Stashed changes } /** diff --git a/bootstrap/helpers/proxy.php b/bootstrap/helpers/proxy.php index 137fa43c13..6e70b2fcb1 100644 --- a/bootstrap/helpers/proxy.php +++ b/bootstrap/helpers/proxy.php @@ -422,6 +422,97 @@ function extractCustomProxyCommands(Server $server, string $existing_config): ar return $custom_commands; } + +/** + * Removes the dashboard router labels that older Coolify versions generated for Traefik. + * These labels route requests with the proxy container name as Host header to the Traefik API and dashboard. + * Comments and formatting are kept. A router or label set that the user changed is not touched. + */ +function removeLegacyTraefikDashboardLabels(string $configuration): string +{ + $legacyLabels = [ + 'traefik.enable=true', + 'traefik.http.routers.traefik.entrypoints=http', + 'traefik.http.routers.traefik.service=api@internal', + 'traefik.http.services.traefik.loadbalancer.server.port=8080', + ]; + + try { + $yaml = Yaml::parse($configuration); + } catch (Throwable) { + return $configuration; + } + + $expected = $yaml; + $changed = false; + foreach (['services.traefik.labels', 'services.traefik.deploy.labels'] as $path) { + $labels = data_get($yaml, $path); + if (! is_array($labels) || ! array_is_list($labels) || array_filter($labels, 'is_string') !== $labels) { + continue; + } + + $traefikLabels = array_filter($labels, fn (string $label) => str_starts_with($label, 'traefik.')); + if (! in_array('traefik.http.routers.traefik.service=api@internal', $traefikLabels, true) || array_diff($traefikLabels, $legacyLabels) !== []) { + continue; + } + + $newLabels = []; + foreach ($labels as $label) { + if ($label === 'traefik.enable=true') { + $newLabels[] = 'traefik.enable=false'; + } elseif (! in_array($label, $legacyLabels, true)) { + $newLabels[] = $label; + } + } + data_set($expected, $path, $newLabels); + $changed = true; + } + + if (! $changed) { + return $configuration; + } + + $fixed = preg_replace('/^([ \t]*-[ \t]*([\'"]?))traefik\.enable=true(\2[ \t]*)(?=\R|\z)/m', '$1traefik.enable=false$3', $configuration); + foreach (array_slice($legacyLabels, 1) as $label) { + $fixed = preg_replace('/^[ \t]*-[ \t]*([\'"]?)'.preg_quote($label, '/').'\1[ \t]*(?:\R|\z)/m', '', $fixed); + } + + // Keep the original when the line edit also changed other parts of the file. + try { + return Yaml::parse($fixed) === $expected ? $fixed : $configuration; + } catch (Throwable) { + return $configuration; + } +} + +/** + * Saves the Traefik configuration without the legacy dashboard labels. The next proxy restart applies it. + */ +function removeLegacyTraefikDashboardExposure(Server $server): bool +{ + $configuration = $server->proxy->get('last_saved_proxy_configuration'); + if ($server->proxyType() !== ProxyTypes::TRAEFIK->value || ! is_string($configuration) || blank($configuration)) { + return false; + } + + $fixed = removeLegacyTraefikDashboardLabels($configuration); + if ($fixed === $configuration) { + return false; + } + + // The running proxy still uses the old configuration, so the UI must ask for a restart. + if (blank($server->proxy->get('last_applied_settings'))) { + $server->proxy->last_applied_settings = md5(base64_encode($configuration)); + } + $server->proxy->last_saved_proxy_configuration = $fixed; + $server->proxy->last_saved_settings = md5(base64_encode($fixed)); + $server->save(); + + Log::info('Removed legacy Traefik dashboard labels from the proxy configuration', ['server_id' => $server->id]); + + return true; +} + function generateDefaultProxyConfiguration(Server $server, array $custom_commands = []) { Log::info('Generating default proxy configuration', [ diff --git a/database/migrations/2026_09_25_210000_remove_legacy_traefik_dashboard_labels.php b/database/migrations/2026_09_25_210000_remove_legacy_traefik_dashboard_labels.php new file mode 100644 index 0000000000..19a745b614 --- /dev/null +++ b/database/migrations/2026_09_25_210000_remove_legacy_traefik_dashboard_labels.php @@ -0,0 +1,33 @@ +chunkById(100, function ($servers) { + foreach ($servers as $server) { + try { + removeLegacyTraefikDashboardExposure($server); + } catch (Throwable $e) { + Log::warning('Could not remove legacy Traefik dashboard labels', [ + 'server_id' => $server->id, + 'error' => $e->getMessage(), + ]); + } + } + }); + } + + public function down(): void + { + // + } +}; diff --git a/resources/views/components/proxy-configuration-warning.blade.php b/resources/views/components/proxy-configuration-warning.blade.php index 51f4282944..5deb10dc21 100644 --- a/resources/views/components/proxy-configuration-warning.blade.php +++ b/resources/views/components/proxy-configuration-warning.blade.php @@ -17,19 +17,16 @@

- The saved proxy configuration has not been applied -

-

- Restart the proxy to apply these changes. - @if ($canRestart) - - @endif + Your configuration changed, please restart the proxy.

+ @if ($canRestart) + + @endif
diff --git a/resources/views/livewire/server/proxy.blade.php b/resources/views/livewire/server/proxy.blade.php index 04b10cb609..bfb6f2a44f 100644 --- a/resources/views/livewire/server/proxy.blade.php +++ b/resources/views/livewire/server/proxy.blade.php @@ -36,9 +36,7 @@ @if ( $server->proxy->last_applied_settings && $server->proxy->last_saved_settings !== $server->proxy->last_applied_settings) - - Restart the proxy to apply the saved configuration. - + @else
withoutDefer(); + InstanceSettings::forceCreate(['id' => 0]); +}); + +/** + * The Traefik configuration that Coolify generated before the dashboard exposure fix. + */ +function legacyTraefikConfiguration(array $extraLabels = [], bool $swarm = false): string +{ + $labels = [ + 'traefik.enable=true', + 'traefik.http.routers.traefik.entrypoints=http', + 'traefik.http.routers.traefik.service=api@internal', + 'traefik.http.services.traefik.loadbalancer.server.port=8080', + 'coolify.managed=true', + 'coolify.proxy=true', + ...$extraLabels, + ]; + $service = [ + 'image' => 'traefik:v3.6', + 'ports' => ['80:80', '443:443', '8080:8080'], + 'command' => ['--api.dashboard=true', '--api.insecure=false', '--providers.docker.exposedbydefault=false'], + ]; + if ($swarm) { + $service['deploy'] = ['labels' => $labels]; + } else { + $service['container_name'] = 'coolify-proxy'; + $service['labels'] = $labels; + } + + return "# my custom comment\n".Yaml::dump(['name' => 'coolify-proxy', 'services' => ['traefik' => $service]], 10, 2); +} + +function traefikServerWithConfiguration(string $configuration, bool $applied = true): Server +{ + $server = Server::factory()->create(['team_id' => Team::factory()->create()->id]); + $server->proxy->type = ProxyTypes::TRAEFIK->value; + $server->proxy->status = 'running'; + $server->proxy->last_saved_proxy_configuration = $configuration; + $server->proxy->last_saved_settings = md5(base64_encode($configuration)); + $server->proxy->last_applied_settings = $applied ? $server->proxy->last_saved_settings : null; + $server->save(); + + return $server->fresh(); +} + +test('the legacy dashboard router labels are replaced with traefik.enable=false', function (bool $swarm) { + $configuration = legacyTraefikConfiguration(swarm: $swarm); + + $fixed = removeLegacyTraefikDashboardLabels($configuration); + $labels = data_get(Yaml::parse($fixed), $swarm ? 'services.traefik.deploy.labels' : 'services.traefik.labels'); + + expect($labels)->toBe(['traefik.enable=false', 'coolify.managed=true', 'coolify.proxy=true']) + ->and($fixed)->toContain('# my custom comment') + ->and($fixed)->not->toContain('api@internal') + ->and(removeLegacyTraefikDashboardLabels($fixed))->toBe($fixed); +})->with(['standalone' => false, 'swarm' => true]); + +test('quoted legacy labels are also replaced', function () { + $configuration = str_replace( + ['- traefik.enable=true', '- traefik.http.routers.traefik.service=api@internal'], + ["- 'traefik.enable=true'", '- "traefik.http.routers.traefik.service=api@internal"'], + legacyTraefikConfiguration(), + ); + + $labels = data_get(Yaml::parse(removeLegacyTraefikDashboardLabels($configuration)), 'services.traefik.labels'); + + expect($labels)->toBe(['traefik.enable=false', 'coolify.managed=true', 'coolify.proxy=true']); +}); + +test('a dashboard router that the user changed is kept', function (array $extraLabels) { + $configuration = legacyTraefikConfiguration($extraLabels); + + expect(removeLegacyTraefikDashboardLabels($configuration))->toBe($configuration); +})->with([ + 'custom rule' => [['traefik.http.routers.traefik.rule=Host(`traefik.example.com`)']], + 'auth middleware' => [['traefik.http.routers.traefik.middlewares=auth']], + 'other router on the proxy' => [['traefik.http.routers.other.rule=Host(`other.example.com`)']], +]); + +test('configurations without the legacy dashboard router are not changed', function () { + $configuration = Yaml::dump(['services' => ['traefik' => ['labels' => ['traefik.enable=false', 'coolify.managed=true', 'coolify.proxy=true']]]], 10, 2); + + expect(removeLegacyTraefikDashboardLabels($configuration))->toBe($configuration) + ->and(removeLegacyTraefikDashboardLabels("services:\n traefik: [\n"))->toBe("services:\n traefik: [\n"); +}); + +test('loading a legacy configuration saves the fix and marks the proxy for restart', function () { + $server = traefikServerWithConfiguration(legacyTraefikConfiguration()); + expect($server->hasPendingProxyConfiguration())->toBeFalse(); + + $configuration = GetProxyConfiguration::run($server); + + $server->refresh(); + expect($configuration)->not->toContain('api@internal') + ->and($server->proxy->last_saved_proxy_configuration)->toBe($configuration) + ->and($server->proxy->last_saved_settings)->toBe(md5(base64_encode($configuration))) + ->and($server->hasPendingProxyConfiguration())->toBeTrue(); +}); + +test('the fix marks a running proxy for restart when no applied settings were recorded', function () { + $legacy = legacyTraefikConfiguration(); + $server = traefikServerWithConfiguration($legacy, applied: false); + + GetProxyConfiguration::run($server); + + $server->refresh(); + expect($server->proxy->last_applied_settings)->toBe(md5(base64_encode($legacy))) + ->and($server->hasPendingProxyConfiguration())->toBeTrue(); +}); + +test('the migration fixes saved legacy configurations without connecting to servers', function () { + $legacy = traefikServerWithConfiguration(legacyTraefikConfiguration()); + $custom = traefikServerWithConfiguration(legacyTraefikConfiguration(['traefik.http.routers.traefik.middlewares=auth'])); + $caddy = Server::factory()->create(['team_id' => Team::factory()->create()->id]); + $caddy->proxy->type = ProxyTypes::CADDY->value; + $caddy->proxy->last_saved_proxy_configuration = 'services: {}'; + $caddy->save(); + + (require database_path('migrations/2026_09_25_210000_remove_legacy_traefik_dashboard_labels.php'))->up(); + + expect($legacy->fresh()->proxy->last_saved_proxy_configuration)->not->toContain('api@internal') + ->and($legacy->fresh()->hasPendingProxyConfiguration())->toBeTrue() + ->and($custom->fresh()->proxy->last_saved_proxy_configuration)->toContain('api@internal') + ->and($custom->fresh()->hasPendingProxyConfiguration())->toBeFalse() + ->and($caddy->fresh()->proxy->last_saved_proxy_configuration)->toBe('services: {}'); +}); + +test('the pending proxy notice asks the user to restart the proxy', function () { + $team = Team::factory()->create(); + $user = User::factory()->create(); + $user->teams()->attach($team, ['role' => 'admin']); + $server = Server::factory()->create(['team_id' => $team->id]); + $server->settings()->update(['is_reachable' => true, 'is_usable' => true]); + $server->proxy->type = ProxyTypes::TRAEFIK->value; + $server->proxy->status = 'running'; + $server->proxy->last_saved_settings = 'new-hash'; + $server->proxy->last_applied_settings = 'old-hash'; + $server->save(); + + $this->actingAs($user); + session(['currentTeam' => $team]); + + Livewire::test('server.navbar', ['server' => $server->fresh()]) + ->assertSee('Your configuration changed, please restart the proxy.') + ->assertDontSee('The saved proxy configuration has not been applied'); +}); diff --git a/tests/Feature/Proxy/RestartProxyTest.php b/tests/Feature/Proxy/RestartProxyTest.php index 0c393c03b5..092066565e 100644 --- a/tests/Feature/Proxy/RestartProxyTest.php +++ b/tests/Feature/Proxy/RestartProxyTest.php @@ -97,7 +97,7 @@ test('running proxy shows pending configuration warning when saved settings diff $component = Livewire::test('server.navbar', ['server' => $server->fresh()]) ->assertSee('Changes pending') - ->assertSee('The saved proxy configuration has not been applied') + ->assertSee('Your configuration changed, please restart the proxy.') ->assertSee('Restart proxy'); $server->refresh(); @@ -107,7 +107,7 @@ test('running proxy shows pending configuration warning when saved settings diff $component->call('showNotification') ->assertDispatched('proxy-configuration-state-changed', pending: false, traefikOutdated: false) - ->assertDontSee('The saved proxy configuration has not been applied'); + ->assertDontSee('Your configuration changed, please restart the proxy.'); }); test('running proxy hides pending configuration warning when saved settings match applied settings', function () { @@ -123,7 +123,7 @@ test('running proxy hides pending configuration warning when saved settings matc session(['currentTeam' => $team]); $component = Livewire::test('server.navbar', ['server' => $server->fresh()]) - ->assertDontSee('The saved proxy configuration has not been applied'); + ->assertDontSee('Your configuration changed, please restart the proxy.'); $server->refresh(); $server->proxy->last_saved_settings = 'new-saved-hash'; @@ -132,7 +132,7 @@ test('running proxy hides pending configuration warning when saved settings matc $component->dispatch('refreshServerShow') ->assertDispatched('proxy-configuration-state-changed', pending: true, traefikOutdated: false) ->assertSee('Changes pending') - ->assertSee('The saved proxy configuration has not been applied'); + ->assertSee('Your configuration changed, please restart the proxy.'); }); test('admin can stop a proxy while it is starting', function () {