From 271849c88e6a978971c38131b9b828d245c94bec Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Mon, 26 Oct 2026 13:10:09 +0100
Subject: [PATCH 1/2] fix(scheduled-tasks): show the server timezone in the
schedule help text
Closes #10904
---
.../Project/Shared/ScheduledTask/Add.php | 5 +++++
.../Project/Shared/ScheduledTask/Show.php | 4 ++++
.../shared/scheduled-task/add.blade.php | 2 +-
.../shared/scheduled-task/show.blade.php | 2 +-
.../ScheduledJobManagerDispatchTest.php | 20 +++++++++++++++++++
5 files changed, 31 insertions(+), 2 deletions(-)
diff --git a/app/Livewire/Project/Shared/ScheduledTask/Add.php b/app/Livewire/Project/Shared/ScheduledTask/Add.php
index df49f6cf0e..a7b14e04c5 100644
--- a/app/Livewire/Project/Shared/ScheduledTask/Add.php
+++ b/app/Livewire/Project/Shared/ScheduledTask/Add.php
@@ -39,6 +39,9 @@ class Add extends Component
public int $timeout = 300;
+ #[Locked]
+ public string $serverTimezone = 'UTC';
+
protected $rules = [
'name' => 'required|string',
'command' => 'required|string',
@@ -74,6 +77,8 @@ class Add extends Component
throw new \Exception('Invalid resource type');
}
+ $this->serverTimezone = getServerTimezone($this->resource->destination?->server);
+
if ($this->containerNames->count() > 0) {
$this->container = $this->containerNames->first();
}
diff --git a/app/Livewire/Project/Shared/ScheduledTask/Show.php b/app/Livewire/Project/Shared/ScheduledTask/Show.php
index 5f5689b5de..1ba42b9d4e 100644
--- a/app/Livewire/Project/Shared/ScheduledTask/Show.php
+++ b/app/Livewire/Project/Shared/ScheduledTask/Show.php
@@ -45,12 +45,16 @@ class Show extends Component
#[Validate(['integer', 'required', 'min:60', 'max:36000'])]
public $timeout = 300;
+ #[Locked]
+ public string $serverTimezone = 'UTC';
+
public function mount(Application|Service $resource, ScheduledTask $task): void
{
$this->resource = $resource;
$this->type = $resource->type();
$this->parameters = get_route_parameters();
$this->task = $resource->scheduled_tasks()->whereKey($task->id)->firstOrFail();
+ $this->serverTimezone = getServerTimezone($this->task->server());
$this->syncData();
}
diff --git a/resources/views/livewire/project/shared/scheduled-task/add.blade.php b/resources/views/livewire/project/shared/scheduled-task/add.blade.php
index b365bb4aef..dee7f3731c 100644
--- a/resources/views/livewire/project/shared/scheduled-task/add.blade.php
+++ b/resources/views/livewire/project/shared/scheduled-task/add.blade.php
@@ -2,7 +2,7 @@
diff --git a/resources/views/livewire/project/shared/scheduled-task/show.blade.php b/resources/views/livewire/project/shared/scheduled-task/show.blade.php
index 481fd98253..66d6bb8afe 100644
--- a/resources/views/livewire/project/shared/scheduled-task/show.blade.php
+++ b/resources/views/livewire/project/shared/scheduled-task/show.blade.php
@@ -36,7 +36,7 @@
label="Name" required />
and(ScheduledJobState::query()->where('schedule_key', 'midnight')->value('last_scheduled_for')->toDateTimeString())->toBe('2026-10-03 04:00:00');
});
+it('runs a scheduled task in the server timezone, not in the instance timezone', function (string $serverTimezone, string $utcNow, bool $expectDispatch) {
+ config(['constants.coolify.self_hosted' => true]);
+ InstanceSettings::unguarded(fn () => InstanceSettings::firstOrCreate(['id' => 0]))->update(['instance_timezone' => 'Europe/Istanbul']);
+ Carbon::setTestNow(Carbon::parse($utcNow, 'UTC'));
+ Queue::fake();
+
+ $application = createScheduledTaskApplication();
+ $application->destination->server->settings()->update(['server_timezone' => $serverTimezone]);
+ createScheduledApplicationTask($application, ['frequency' => '59 23 * * *']);
+
+ (new ScheduledJobManager)->handle();
+
+ Queue::assertPushed(ScheduledTaskJob::class, $expectDispatch ? 1 : 0);
+})->with([
+ // 23:59 in Istanbul (UTC+3) is 20:59Z.
+ 'UTC server at 23:59 Istanbul' => ['UTC', '2026-10-26 20:59:00', false],
+ 'UTC server at 23:59 UTC' => ['UTC', '2026-10-26 23:59:00', true],
+ 'Istanbul server at 23:59 Istanbul' => ['Europe/Istanbul', '2026-10-26 20:59:00', true],
+]);
+
function createStaleEnqueuedOccurrence(string $scheduleKey, string $jobType, int $resourceId, Carbon $scheduledFor, int $minutesAgo = ScheduledJobDeliveryService::ENQUEUED_STALE_AFTER_MINUTES + 1): ScheduledJobDelivery
{
return ScheduledJobDelivery::create([
From ea1b701a9471d14f6ddfc8bb0bfe805440131e11 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Mon, 26 Oct 2026 13:24:36 +0100
Subject: [PATCH 2/2] fix(ports): skip the PORT mismatch warning when readonly
labels are disabled
Closes #10941
Co-Authored-By: Claude Opus 5.5
---
app/Jobs/ApplicationDeploymentJob.php | 5 +-
app/Livewire/Project/Application/General.php | 5 ++
...ationDeploymentControlVarFilteringTest.php | 21 ++++++
.../ApplicationPortMismatchWarningTest.php | 66 +++++++++++++++++++
4 files changed, 95 insertions(+), 2 deletions(-)
create mode 100644 tests/Feature/ApplicationPortMismatchWarningTest.php
diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php
index 3a00704813..ea3ce9191c 100644
--- a/app/Jobs/ApplicationDeploymentJob.php
+++ b/app/Jobs/ApplicationDeploymentJob.php
@@ -1927,8 +1927,9 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$envs->push($this->runtimeEnvironmentAssignment($env));
}
- // Check for PORT environment variable mismatch with ports_exposes
- if ($this->build_pack !== 'dockercompose') {
+ // Check for PORT environment variable mismatch with ports_exposes.
+ // With readonly labels disabled, custom labels control proxy routing, so skip the check.
+ if ($this->build_pack !== 'dockercompose' && $this->application->settings->is_container_label_readonly_enabled) {
$detectedPort = $this->application->detectPortFromEnvironment(false);
if ($detectedPort && ! empty($ports) && ! in_array($detectedPort, $ports)) {
$this->application_deployment_queue->addLogEntry(
diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php
index 09a634c681..a58d87f307 100644
--- a/app/Livewire/Project/Application/General.php
+++ b/app/Livewire/Project/Application/General.php
@@ -992,6 +992,11 @@ class General extends Component
public function getDetectedPortInfoProperty(): ?array
{
+ // With readonly labels disabled, Ports Exposes is read-only and custom labels control proxy routing.
+ if (! $this->application->settings->is_container_label_readonly_enabled) {
+ return null;
+ }
+
$detectedPort = $this->application->detectPortFromEnvironment();
if (! $detectedPort) {
diff --git a/tests/Feature/ApplicationDeploymentControlVarFilteringTest.php b/tests/Feature/ApplicationDeploymentControlVarFilteringTest.php
index d19a382e0e..caf92c69e4 100644
--- a/tests/Feature/ApplicationDeploymentControlVarFilteringTest.php
+++ b/tests/Feature/ApplicationDeploymentControlVarFilteringTest.php
@@ -2029,3 +2029,24 @@ it('keeps heredoc bodies unchanged when it adds build secrets to a Dockerfile',
"RUN --mount=type=secret,id=API_URL,env=API_URL python -c 'print(1)'",
]);
});
+
+it('warns in the deployment log when PORT does not match ports_exposes', function () {
+ [$application, $server] = makeDeploymentControlVarFixture(['ports_exposes' => '80']);
+ createApplicationEnvironmentVariable($application, ['key' => 'PORT', 'value' => '3000']);
+
+ [$job, $reflection] = makeControlVarFilteringJob($application, $server);
+ invokeDeploymentJobMethod($job, $reflection, 'generate_runtime_environment_variables');
+
+ expect(collect($job->recordedLogEntries)->contains(fn (string $entry) => str_contains($entry, 'PORT environment variable (3000)')))->toBeTrue();
+});
+
+it('does not warn about a PORT mismatch when readonly labels are disabled', function () {
+ [$application, $server] = makeDeploymentControlVarFixture(['ports_exposes' => '80']);
+ $application->settings()->update(['is_container_label_readonly_enabled' => false]);
+ createApplicationEnvironmentVariable($application, ['key' => 'PORT', 'value' => '3000']);
+
+ [$job, $reflection] = makeControlVarFilteringJob($application, $server);
+ invokeDeploymentJobMethod($job, $reflection, 'generate_runtime_environment_variables');
+
+ expect(collect($job->recordedLogEntries)->contains(fn (string $entry) => str_contains($entry, 'PORT environment variable')))->toBeFalse();
+});
diff --git a/tests/Feature/ApplicationPortMismatchWarningTest.php b/tests/Feature/ApplicationPortMismatchWarningTest.php
new file mode 100644
index 0000000000..c47de2718b
--- /dev/null
+++ b/tests/Feature/ApplicationPortMismatchWarningTest.php
@@ -0,0 +1,66 @@
+team = Team::factory()->create();
+ $this->user = User::factory()->create();
+ $this->team->members()->attach($this->user->id, ['role' => 'owner']);
+
+ $this->actingAs($this->user);
+ session(['currentTeam' => $this->team]);
+ InstanceSettings::unguarded(function () {
+ InstanceSettings::updateOrCreate(['id' => 0], []);
+ });
+
+ $project = Project::factory()->create(['team_id' => $this->team->id]);
+ $environment = Environment::factory()->create(['project_id' => $project->id]);
+ $server = Server::factory()->create(['team_id' => $this->team->id]);
+ $destination = StandaloneDocker::where('server_id', $server->id)->first()
+ ?? StandaloneDocker::factory()->create(['server_id' => $server->id, 'network' => 'coolify-test']);
+
+ $this->application = Application::factory()->create([
+ 'environment_id' => $environment->id,
+ 'destination_id' => $destination->id,
+ 'destination_type' => StandaloneDocker::class,
+ 'build_pack' => 'dockerimage',
+ 'ports_exposes' => '80',
+ ]);
+
+ EnvironmentVariable::create([
+ 'key' => 'PORT',
+ 'value' => '3000',
+ 'resourceable_type' => Application::class,
+ 'resourceable_id' => $this->application->id,
+ 'is_preview' => false,
+ ]);
+});
+
+test('shows a PORT mismatch warning when readonly labels are enabled', function () {
+ Livewire::test(General::class, ['application' => $this->application->fresh()])
+ ->assertSee('PORT mismatch detected');
+});
+
+/**
+ * Regression for #10941: with readonly labels disabled, the Ports Exposes field is
+ * read-only and custom labels control proxy routing, so the warning cannot be acted on.
+ */
+test('does not show a PORT mismatch warning when readonly labels are disabled', function () {
+ $this->application->settings()->update(['is_container_label_readonly_enabled' => false]);
+
+ Livewire::test(General::class, ['application' => $this->application->fresh()])
+ ->assertDontSee('PORT mismatch detected');
+});