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 () {