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 <noreply@anthropic.com>
This commit is contained in:
Andras Bacsai
2026-09-25 23:18:34 +02:00
co-authored by Claude Opus 5.5
parent 165d2f3dc8
commit f92ee342a1
8 changed files with 306 additions and 22 deletions
@@ -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
+2 -3
View File
@@ -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
}
/**
+91
View File
@@ -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', [
@@ -0,0 +1,33 @@
<?php
use App\Models\Server;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\Log;
return new class extends Migration
{
/**
* Remove the Traefik dashboard router labels from saved proxy configurations.
* Only the database is changed. The proxy shows a restart notice and applies the fix on the next restart.
*/
public function up(): void
{
Server::query()->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
{
//
}
};
@@ -17,19 +17,16 @@
</span>
<div class="min-w-0 flex-1">
<p class="text-[13px] leading-4 font-semibold text-neutral-950 dark:text-fg">
The saved proxy configuration has not been applied
</p>
<p class="mt-0.5 text-[11px] leading-4 text-neutral-600 dark:text-fg-dim">
Restart the proxy to apply these changes.
@if ($canRestart)
<button type="button"
class="ml-0.5 inline-flex items-center gap-0.5 font-semibold text-coollabs transition-colors hover:text-coollabs-100 dark:text-warning dark:hover:text-warning/80"
@click="open = false; document.getElementById('server-mobile-restart-proxy-trigger')?.click()">
Restart proxy
<x-reicon name="arrow-right" class="size-2.5" />
</button>
@endif
Your configuration changed, please restart the proxy.
</p>
@if ($canRestart)
<button type="button"
class="mt-0.5 inline-flex items-center gap-0.5 text-[11px] leading-4 font-semibold text-coollabs transition-colors hover:text-coollabs-100 dark:text-warning dark:hover:text-warning/80"
@click="open = false; document.getElementById('server-mobile-restart-proxy-trigger')?.click()">
Restart proxy
<x-reicon name="arrow-right" class="size-2.5" />
</button>
@endif
</div>
</div>
</div>
@@ -36,9 +36,7 @@
@if (
$server->proxy->last_applied_settings &&
$server->proxy->last_saved_settings !== $server->proxy->last_applied_settings)
<x-callout type="warning" title="Configuration out of sync">
Restart the proxy to apply the saved configuration.
</x-callout>
<x-callout type="warning" title="Your configuration changed, please restart the proxy." />
@else
<div class="flex items-start gap-3">
<div
@@ -0,0 +1,162 @@
<?php
use App\Actions\Proxy\GetProxyConfiguration;
use App\Enums\ProxyTypes;
use App\Models\InstanceSettings;
use App\Models\Server;
use App\Models\Team;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use Symfony\Component\Yaml\Yaml;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->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');
});
+4 -4
View File
@@ -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 () {