diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index e7da0bed4e..96bc5a7f83 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/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 /> '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'); +}); diff --git a/tests/Feature/ScheduledJobManagerDispatchTest.php b/tests/Feature/ScheduledJobManagerDispatchTest.php index d904fe016d..b02e1bb0a2 100644 --- a/tests/Feature/ScheduledJobManagerDispatchTest.php +++ b/tests/Feature/ScheduledJobManagerDispatchTest.php @@ -1176,6 +1176,26 @@ it('records a midnight schedule in the server timezone, not in UTC', function () ->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([