fix(analytics): regenerate stored Caddy labels when toggling traffic analytics

Caddy logs a site only when its container labels contain the log
directives. Deployments reuse the stored custom_labels, so applications
whose labels were stored before analytics was enabled never got the log
directives, and a redeploy did not help.

Toggling traffic analytics on a Caddy server now regenerates the stored
labels of applications with read-only labels. The destination uses the
current server instance, because the cached server can still have the
old analytics setting.

Fixes #12117

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andras Bacsai
2026-10-08 12:38:26 +02:00
co-authored by Claude Opus 5.5
parent be7369c9cc
commit 44e4566a9b
3 changed files with 65 additions and 0 deletions
+4
View File
@@ -101,3 +101,7 @@
- In remote commands, use absolute paths (`docker compose -f <dir>/docker-compose.yml`, `--project-directory <dir>`), `echo ... | tee <file> > /dev/null`, and `find` instead of globs. scp also runs as the SSH user; stage files outside `/data/coolify`.
- The parsers add sudo only to line starts (and after `&&`, `||`, `|`, `$(`). Put `if`/`else` branches on their own lines. When a redirect or `cd` must stay, make the whole line one `sh -c '...'` script; it runs as `sudo sh -c` without inner sudo or bash.
- Tests that replace `DatabaseStartCommandExecutor` or use root servers miss non-root bugs. Test the parsed commands of a non-root server.
## Cached and stored state
- `StandaloneDocker`/`SwarmDocker` `server` uses `Server::findCached()`, a per-process identity map that is flushed only when the `servers` row updates. A `ServerSetting` save does not flush it, so pass the current `Server` instance (`setRelation('server', $server)`) when generated output depends on just-saved settings.
- Application deployments use the stored `custom_labels` when they exist. When a server-level setting changes the generated labels, regenerate the stored labels of read-only (`is_container_label_readonly_enabled`) applications, or a redeploy keeps the old labels.
@@ -4,7 +4,9 @@ namespace App\Actions\Server;
use App\Actions\Proxy\GetProxyConfiguration;
use App\Actions\Proxy\SaveProxyConfiguration;
use App\Enums\ProxyTypes;
use App\Jobs\RestartProxyJob;
use App\Models\Application;
use App\Models\Server;
use App\Services\ProxyPortParser;
use Lorisleiva\Actions\Concerns\AsAction;
@@ -41,6 +43,10 @@ class ConfigureTrafficAnalytics
$server->settings->save();
$server->refresh();
if ($server->proxyType() === ProxyTypes::CADDY->value) {
$this->regenerateReadonlyApplicationLabels($server);
}
// A proxy the user stopped stays stopped; its next start applies the saved configuration.
$restartProxy = $hasProxy && ! $this->proxyIsStopped($server);
if ($restartProxy) {
@@ -79,6 +85,23 @@ class ConfigureTrafficAnalytics
}
}
/**
* Caddy logs a site only when its labels contain the log directives. Deployments use the stored labels,
* so read-only (Coolify-managed) labels must be regenerated, or a redeploy keeps the old logging state.
*/
private function regenerateReadonlyApplicationLabels(Server $server): void
{
$server->applications()
->filter(fn (Application $application): bool => (bool) $application->settings?->is_container_label_readonly_enabled
&& $application->destination?->server_id === $server->id)
->each(function (Application $application) use ($server): void {
// The cached server of the destination can still have the old analytics setting.
$application->destination->setRelation('server', $server);
$application->custom_labels = base64_encode(implode("\n", generateLabelsApplication($application)));
$application->save();
});
}
private function proxyIsStopped(Server $server): bool
{
return (bool) $server->proxy->get('force_stop')
@@ -5,7 +5,11 @@ use App\Actions\Proxy\SaveProxyConfiguration;
use App\Actions\Server\ConfigureTrafficAnalytics;
use App\Actions\Server\StartSentinel;
use App\Jobs\RestartProxyJob;
use App\Models\Application;
use App\Models\Environment;
use App\Models\Project;
use App\Models\Server;
use App\Models\StandaloneDocker;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Queue;
@@ -184,3 +188,37 @@ it('restarts a running proxy to apply the configuration', function (string $prox
expect($server->fresh()->isTrafficAnalyticsEnabled())->toBeTrue();
Queue::assertPushed(RestartProxyJob::class);
})->with(['TRAEFIK', 'CADDY']);
it('regenerates the stored Caddy labels of applications with read-only labels', function () {
Queue::fake();
StartSentinel::partialMock()->shouldReceive('handle');
GetProxyConfiguration::partialMock()->shouldReceive('handle')->andReturn("services:\n caddy:\n volumes: []\n");
SaveProxyConfiguration::partialMock()->shouldReceive('handle');
$server = trafficAnalyticsProxyServer($this, 'CADDY');
$environment = Environment::factory()->create(['project_id' => Project::factory()->create(['team_id' => $this->team->id])->id]);
$destination = StandaloneDocker::query()->where('server_id', $server->id)->firstOrFail();
$createApplication = fn (): Application => Application::factory()->createOne([
'environment_id' => $environment->id,
'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(),
'fqdn' => 'https://example.com',
]);
$readonly = $createApplication();
$readonly->update(['custom_labels' => base64_encode(implode("\n", generateLabelsApplication($readonly->fresh())))]);
$editable = $createApplication();
$editable->settings->update(['is_container_label_readonly_enabled' => false]);
$editable->update(['custom_labels' => base64_encode('caddy_0=https://custom.example.com')]);
$logsTraffic = fn (Application $application): bool => str_contains(base64_decode($application->fresh()->custom_labels), 'log.output=file /traffic/access.log');
expect($logsTraffic($readonly))->toBeFalse();
ConfigureTrafficAnalytics::run($server, true);
expect($logsTraffic($readonly))->toBeTrue()
->and(base64_decode($editable->fresh()->custom_labels))->toBe('caddy_0=https://custom.example.com');
ConfigureTrafficAnalytics::run($server->fresh(), false);
expect($logsTraffic($readonly))->toBeFalse();
});