diff --git a/.ai/lessons.md b/.ai/lessons.md
index 84d6bc1992..5cde3ef34e 100644
--- a/.ai/lessons.md
+++ b/.ai/lessons.md
@@ -101,3 +101,7 @@
- In remote commands, use absolute paths (`docker compose -f
/docker-compose.yml`, `--project-directory `), `echo ... | tee > /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.
diff --git a/app/Actions/Server/ConfigureTrafficAnalytics.php b/app/Actions/Server/ConfigureTrafficAnalytics.php
index 287df678a1..78f85689c4 100644
--- a/app/Actions/Server/ConfigureTrafficAnalytics.php
+++ b/app/Actions/Server/ConfigureTrafficAnalytics.php
@@ -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')
diff --git a/tests/Feature/TrafficAnalytics/ConfigureTrafficAnalyticsTest.php b/tests/Feature/TrafficAnalytics/ConfigureTrafficAnalyticsTest.php
index 765f4aa5dc..36d17cd39c 100644
--- a/tests/Feature/TrafficAnalytics/ConfigureTrafficAnalyticsTest.php
+++ b/tests/Feature/TrafficAnalytics/ConfigureTrafficAnalyticsTest.php
@@ -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();
+});