diff --git a/README.md b/README.md index cafff116f..8670e9c76 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ Special thanks to our biggest sponsors! * [Juxtdigital](https://juxtdigital.dev/?ref=coolify.io) - A digital agency offering web development, design, and digital marketing services for businesses. * [Saasykit](https://saasykit.com/?ref=coolify.io) - A Laravel-based boilerplate providing essential components and features for building SaaS applications quickly. * [Massivegrid](https://massivegrid.com/?ref=coolify.io) - A cloud hosting provider offering scalable infrastructure solutions for businesses of all sizes. -* [LiquidWeb](https://liquidweb.com/?utm_source=coolify.io) - Fast web hosting provider. +* [LiquidWeb](https://liquidweb.com/?utm_source=coolify.io) - A Fast web hosting provider. ## Github Sponsors ($40+) diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index a63f67857..a6f24aaad 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -6,14 +6,12 @@ use App\Jobs\CheckAndStartSentinelJob; use App\Jobs\CheckForUpdatesJob; use App\Jobs\CheckHelperImageJob; use App\Jobs\CleanupInstanceStuffsJob; -use App\Jobs\CleanupStaleMultiplexedConnections; use App\Jobs\DatabaseBackupJob; use App\Jobs\DockerCleanupJob; use App\Jobs\PullTemplatesFromCDN; use App\Jobs\RegenerateSslCertJob; use App\Jobs\ScheduledTaskJob; use App\Jobs\ServerCheckJob; -use App\Jobs\ServerCleanupMux; use App\Jobs\ServerStorageCheckJob; use App\Jobs\UpdateCoolifyJob; use App\Models\InstanceSettings; @@ -24,6 +22,7 @@ use App\Models\Team; use Illuminate\Console\Scheduling\Schedule; use Illuminate\Foundation\Console\Kernel as ConsoleKernel; use Illuminate\Support\Carbon; +use Illuminate\Support\Facades\Log; class Kernel extends ConsoleKernel { @@ -102,10 +101,14 @@ class Kernel extends ConsoleKernel $servers = $this->allServers->whereRelation('settings', 'is_usable', true)->whereRelation('settings', 'is_reachable', true)->get(); } foreach ($servers as $server) { - if ($server->isSentinelEnabled()) { - $this->scheduleInstance->job(function () use ($server) { - CheckAndStartSentinelJob::dispatch($server); - })->cron($this->updateCheckFrequency)->timezone($this->instanceTimezone)->onOneServer(); + try { + if ($server->isSentinelEnabled()) { + $this->scheduleInstance->job(function () use ($server) { + CheckAndStartSentinelJob::dispatch($server); + })->cron($this->updateCheckFrequency)->timezone($this->instanceTimezone)->onOneServer(); + } + } catch (\Exception $e) { + Log::error('Error pulling images: '.$e->getMessage()); } } $this->scheduleInstance->job(new CheckHelperImageJob) @@ -141,35 +144,47 @@ class Kernel extends ConsoleKernel } foreach ($servers as $server) { - $serverTimezone = data_get($server->settings, 'server_timezone', $this->instanceTimezone); - if (validate_timezone($serverTimezone) === false) { - $serverTimezone = config('app.timezone'); - } - - // Sentinel check - $lastSentinelUpdate = $server->sentinel_updated_at; - if (Carbon::parse($lastSentinelUpdate)->isBefore(now()->subSeconds($server->waitBeforeDoingSshCheck()))) { - // Check container status every minute if Sentinel does not activated - if (isCloud()) { - $this->scheduleInstance->job(new ServerCheckJob($server))->timezone($serverTimezone)->everyFiveMinutes()->onOneServer(); - } else { - $this->scheduleInstance->job(new ServerCheckJob($server))->timezone($serverTimezone)->everyMinute()->onOneServer(); + try { + $serverTimezone = data_get($server->settings, 'server_timezone', $this->instanceTimezone); + if (validate_timezone($serverTimezone) === false) { + $serverTimezone = config('app.timezone'); } - // $this->scheduleInstance->job(new \App\Jobs\ServerCheckNewJob($server))->everyFiveMinutes()->onOneServer(); - $this->scheduleInstance->job(new ServerStorageCheckJob($server))->cron($server->settings->server_disk_usage_check_frequency)->timezone($serverTimezone)->onOneServer(); - } + // Sentinel check + $lastSentinelUpdate = $server->sentinel_updated_at; + if (Carbon::parse($lastSentinelUpdate)->isBefore(now()->subSeconds($server->waitBeforeDoingSshCheck()))) { + // Check container status every minute if Sentinel does not activated + if (isCloud()) { + $this->scheduleInstance->job(new ServerCheckJob($server))->timezone($serverTimezone)->everyFiveMinutes()->onOneServer(); + } else { + $this->scheduleInstance->job(new ServerCheckJob($server))->timezone($serverTimezone)->everyMinute()->onOneServer(); + } + // $this->scheduleInstance->job(new \App\Jobs\ServerCheckNewJob($server))->everyFiveMinutes()->onOneServer(); - $this->scheduleInstance->job(new DockerCleanupJob($server))->cron($server->settings->docker_cleanup_frequency)->timezone($serverTimezone)->onOneServer(); + $serverDiskUsageCheckFrequency = data_get($server->settings, 'server_disk_usage_check_frequency', '0 * * * *'); + if (isset(VALID_CRON_STRINGS[$serverDiskUsageCheckFrequency])) { + $serverDiskUsageCheckFrequency = VALID_CRON_STRINGS[$serverDiskUsageCheckFrequency]; + } + $this->scheduleInstance->job(new ServerStorageCheckJob($server))->cron($serverDiskUsageCheckFrequency)->timezone($serverTimezone)->onOneServer(); + } - // Cleanup multiplexed connections every hour - // $this->scheduleInstance->job(new ServerCleanupMux($server))->hourly()->onOneServer(); + $dockerCleanupFrequency = data_get($server->settings, 'docker_cleanup_frequency', '0 * * * *'); + if (isset(VALID_CRON_STRINGS[$dockerCleanupFrequency])) { + $dockerCleanupFrequency = VALID_CRON_STRINGS[$dockerCleanupFrequency]; + } + $this->scheduleInstance->job(new DockerCleanupJob($server))->cron($dockerCleanupFrequency)->timezone($serverTimezone)->onOneServer(); - // Temporary solution until we have better memory management for Sentinel - if ($server->isSentinelEnabled()) { - $this->scheduleInstance->job(function () use ($server) { - $server->restartContainer('coolify-sentinel'); - })->daily()->onOneServer(); + // Cleanup multiplexed connections every hour + // $this->scheduleInstance->job(new ServerCleanupMux($server))->hourly()->onOneServer(); + + // Temporary solution until we have better memory management for Sentinel + if ($server->isSentinelEnabled()) { + $this->scheduleInstance->job(function () use ($server) { + $server->restartContainer('coolify-sentinel'); + })->daily()->onOneServer(); + } + } catch (\Exception $e) { + Log::error('Error checking resources: '.$e->getMessage()); } } } @@ -203,24 +218,28 @@ class Kernel extends ConsoleKernel } foreach ($finalScheduledBackups as $scheduled_backup) { - if (isset(VALID_CRON_STRINGS[$scheduled_backup->frequency])) { - $scheduled_backup->frequency = VALID_CRON_STRINGS[$scheduled_backup->frequency]; - } + try { + if (isset(VALID_CRON_STRINGS[$scheduled_backup->frequency])) { + $scheduled_backup->frequency = VALID_CRON_STRINGS[$scheduled_backup->frequency]; + } + $server = $scheduled_backup->server(); + $serverTimezone = data_get($server->settings, 'server_timezone', $this->instanceTimezone); - $server = $scheduled_backup->server(); - $serverTimezone = data_get($server->settings, 'server_timezone', $this->instanceTimezone); + if (validate_timezone($serverTimezone) === false) { + $serverTimezone = config('app.timezone'); + } - if (validate_timezone($serverTimezone) === false) { - $serverTimezone = config('app.timezone'); + if (isset(VALID_CRON_STRINGS[$scheduled_backup->frequency])) { + $scheduled_backup->frequency = VALID_CRON_STRINGS[$scheduled_backup->frequency]; + } + $serverTimezone = data_get($server->settings, 'server_timezone', $this->instanceTimezone); + $this->scheduleInstance->job(new DatabaseBackupJob( + backup: $scheduled_backup + ))->cron($scheduled_backup->frequency)->timezone($serverTimezone)->onOneServer(); + } catch (\Exception $e) { + Log::error('Error scheduling backup: '.$e->getMessage()); + Log::error($e->getTraceAsString()); } - - if (isset(VALID_CRON_STRINGS[$scheduled_backup->frequency])) { - $scheduled_backup->frequency = VALID_CRON_STRINGS[$scheduled_backup->frequency]; - } - $serverTimezone = data_get($server->settings, 'server_timezone', $this->instanceTimezone); - $this->scheduleInstance->job(new DatabaseBackupJob( - backup: $scheduled_backup - ))->cron($scheduled_backup->frequency)->timezone($serverTimezone)->onOneServer(); } } @@ -267,18 +286,23 @@ class Kernel extends ConsoleKernel } foreach ($finalScheduledTasks as $scheduled_task) { - $server = $scheduled_task->server(); - if (isset(VALID_CRON_STRINGS[$scheduled_task->frequency])) { - $scheduled_task->frequency = VALID_CRON_STRINGS[$scheduled_task->frequency]; - } - $serverTimezone = data_get($server->settings, 'server_timezone', $this->instanceTimezone); + try { + $server = $scheduled_task->server(); + if (isset(VALID_CRON_STRINGS[$scheduled_task->frequency])) { + $scheduled_task->frequency = VALID_CRON_STRINGS[$scheduled_task->frequency]; + } + $serverTimezone = data_get($server->settings, 'server_timezone', $this->instanceTimezone); - if (validate_timezone($serverTimezone) === false) { - $serverTimezone = config('app.timezone'); + if (validate_timezone($serverTimezone) === false) { + $serverTimezone = config('app.timezone'); + } + $this->scheduleInstance->job(new ScheduledTaskJob( + task: $scheduled_task + ))->cron($scheduled_task->frequency)->timezone($serverTimezone)->onOneServer(); + } catch (\Exception $e) { + Log::error('Error scheduling task: '.$e->getMessage()); + Log::error($e->getTraceAsString()); } - $this->scheduleInstance->job(new ScheduledTaskJob( - task: $scheduled_task - ))->cron($scheduled_task->frequency)->timezone($serverTimezone)->onOneServer(); } } diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 67708bd32..c28f22742 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -2284,7 +2284,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); } else { if ($this->use_build_server) { $this->execute_remote_command( - ["{$this->coolify_variables} docker compose --project-name {$this->application->uuid} --project-directory {$this->configuration_dir} -f {$this->configuration_dir}{$this->docker_compose_location} up --build -d", 'hidden' => true], + ["{$this->coolify_variables} docker compose --project-name {$this->application->uuid} --project-directory {$this->configuration_dir} -f {$this->configuration_dir}{$this->docker_compose_location} up --pull always --build -d", 'hidden' => true], ); } else { $this->execute_remote_command( diff --git a/app/Livewire/Destination/New/Docker.php b/app/Livewire/Destination/New/Docker.php index 0e60025e5..eb768d191 100644 --- a/app/Livewire/Destination/New/Docker.php +++ b/app/Livewire/Destination/New/Docker.php @@ -35,10 +35,18 @@ class Docker extends Component $this->network = new Cuid2; $this->servers = Server::isUsable()->get(); if ($server_id) { - $this->selectedServer = $this->servers->find($server_id) ?: $this->servers->first(); + $foundServer = $this->servers->find($server_id) ?: $this->servers->first(); + if (! $foundServer) { + throw new \Exception('Server not found.'); + } + $this->selectedServer = $foundServer; $this->serverId = $this->selectedServer->id; } else { - $this->selectedServer = $this->servers->first(); + $foundServer = $this->servers->first(); + if (! $foundServer) { + throw new \Exception('Server not found.'); + } + $this->selectedServer = $foundServer; $this->serverId = $this->selectedServer->id; } $this->generateName(); diff --git a/app/Livewire/Project/Application/DeploymentNavbar.php b/app/Livewire/Project/Application/DeploymentNavbar.php index 87b40d4dc..66f387fcf 100644 --- a/app/Livewire/Project/Application/DeploymentNavbar.php +++ b/app/Livewire/Project/Application/DeploymentNavbar.php @@ -53,13 +53,13 @@ class DeploymentNavbar extends Component public function cancel() { $kill_command = "docker rm -f {$this->application_deployment_queue->deployment_uuid}"; - $build_server_id = $this->application_deployment_queue->build_server_id; + $build_server_id = $this->application_deployment_queue->build_server_id ?? $this->application->destination->server_id; $server_id = $this->application_deployment_queue->server_id ?? $this->application->destination->server_id; try { if ($this->application->settings->is_build_server_enabled) { - $server = Server::find($build_server_id); + $server = Server::ownedByCurrentTeam()->find($build_server_id); } else { - $server = Server::find($server_id); + $server = Server::ownedByCurrentTeam()->find($server_id); } if ($this->application_deployment_queue->logs) { $previous_logs = json_decode($this->application_deployment_queue->logs, associative: true, flags: JSON_THROW_ON_ERROR); diff --git a/app/Livewire/Project/Service/Configuration.php b/app/Livewire/Project/Service/Configuration.php index d1744b178..b36a860ce 100644 --- a/app/Livewire/Project/Service/Configuration.php +++ b/app/Livewire/Project/Service/Configuration.php @@ -32,7 +32,7 @@ class Configuration extends Component return [ "echo-private:user.{$userId},ServiceStatusChanged" => 'check_status', 'check_status', - 'refresh' => '$refresh', + 'refreshStatus' => '$refresh', ]; } @@ -99,7 +99,7 @@ class Configuration extends Component $this->service->databases->each(function ($database) { $database->refresh(); }); - $this->dispatch('refresh'); + $this->dispatch('refreshStatus'); } catch (\Exception $e) { return handleError($e, $this); } diff --git a/app/Livewire/Project/Service/Navbar.php b/app/Livewire/Project/Service/Navbar.php index e6a1271e1..5da425cbd 100644 --- a/app/Livewire/Project/Service/Navbar.php +++ b/app/Livewire/Project/Service/Navbar.php @@ -40,6 +40,7 @@ class Navbar extends Component return [ "echo-private:user.{$userId},ServiceStatusChanged" => 'serviceStarted', 'envsUpdated' => '$refresh', + 'refreshStatus' => '$refresh', ]; } diff --git a/app/Livewire/Server/Show.php b/app/Livewire/Server/Show.php index 6d267b9c8..b0e6d8858 100644 --- a/app/Livewire/Server/Show.php +++ b/app/Livewire/Server/Show.php @@ -7,6 +7,7 @@ use App\Actions\Server\StopSentinel; use App\Events\ServerReachabilityChanged; use App\Models\Server; use Livewire\Attributes\Computed; +use Livewire\Attributes\Locked; use Livewire\Attributes\Validate; use Livewire\Component; @@ -50,6 +51,9 @@ class Show extends Component #[Validate(['required'])] public bool $isBuildServer; + #[Locked] + public bool $isBuildServerLocked = false; + #[Validate(['required'])] public bool $isMetricsEnabled; @@ -95,6 +99,9 @@ class Show extends Component try { $this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail(); $this->syncData(); + if (! $this->server->isEmpty()) { + $this->isBuildServerLocked = true; + } } catch (\Throwable $e) { return handleError($e, $this); } diff --git a/app/Models/S3Storage.php b/app/Models/S3Storage.php index 33f4fa37c..e9d674650 100644 --- a/app/Models/S3Storage.php +++ b/app/Models/S3Storage.php @@ -43,8 +43,18 @@ class S3Storage extends BaseModel public function testConnection(bool $shouldSave = false) { try { - set_s3_target($this); - Storage::disk('custom-s3')->files(); + $disk = Storage::build([ + 'driver' => 's3', + 'region' => $this['region'], + 'key' => $this['key'], + 'secret' => $this['secret'], + 'bucket' => $this['bucket'], + 'endpoint' => $this['endpoint'], + 'use_path_style_endpoint' => true, + ]); + // Test the connection by listing files with ListObjectsV2 (S3) + $disk->files(); + $this->unusable_email_sent = false; $this->is_usable = true; } catch (\Throwable $e) { @@ -53,13 +63,14 @@ class S3Storage extends BaseModel $mail = new MailMessage; $mail->subject('Coolify: S3 Storage Connection Error'); $mail->view('emails.s3-connection-error', ['name' => $this->name, 'reason' => $e->getMessage(), 'url' => route('storage.show', ['storage_uuid' => $this->uuid])]); - $users = collect([]); - $members = $this->team->members()->get(); - foreach ($members as $user) { - if ($user->isAdmin()) { - $users->push($user); - } - } + + // Load the team with its members and their roles explicitly + $team = $this->team()->with(['members' => function ($query) { + $query->withPivot('role'); + }])->first(); + + // Get admins directly from the pivot relationship for this specific team + $users = $team->members()->wherePivotIn('role', ['admin', 'owner'])->get(['users.id', 'users.email']); foreach ($users as $user) { send_user_an_email($mail, $user->email); } diff --git a/app/Models/Server.php b/app/Models/Server.php index f3edd82fb..187685d66 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -1341,4 +1341,11 @@ $schema://$host { throw new \Exception('Invalid proxy type.'); } } + + public function isEmpty() + { + return $this->applications()->count() == 0 && + $this->databases()->count() == 0 && + $this->services()->count() == 0; + } } diff --git a/bootstrap/helpers/s3.php b/bootstrap/helpers/s3.php deleted file mode 100644 index 7029377a4..000000000 --- a/bootstrap/helpers/s3.php +++ /dev/null @@ -1,17 +0,0 @@ -set('filesystems.disks.custom-s3', [ - 'driver' => 's3', - 'region' => $s3['region'], - 'key' => $s3['key'], - 'secret' => $s3['secret'], - 'bucket' => $s3['bucket'], - 'endpoint' => $s3['endpoint'], - 'use_path_style_endpoint' => true, - 'aws_url' => $s3->awsUrl(), - ]); -} diff --git a/config/constants.php b/config/constants.php index 2cf0123c0..4a10d22ef 100644 --- a/config/constants.php +++ b/config/constants.php @@ -2,7 +2,7 @@ return [ 'coolify' => [ - 'version' => '4.0.0-beta.392', + 'version' => '4.0.0-beta.394', 'helper_version' => '1.0.6', 'realtime_version' => '1.0.5', 'self_hosted' => env('SELF_HOSTED', true), diff --git a/public/svgs/convex.svg b/public/svgs/convex.svg new file mode 100644 index 000000000..7fd02e9d6 --- /dev/null +++ b/public/svgs/convex.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/views/components/status/running.blade.php b/resources/views/components/status/running.blade.php index 27a6d7181..a1a93efac 100644 --- a/resources/views/components/status/running.blade.php +++ b/resources/views/components/status/running.blade.php @@ -26,7 +26,7 @@ @endphp @if ($showUnhealthyHelper) + helper="Unhealthy state. This doesn't mean that the resource is malfunctioning.

- If the resource is accessible, it indicates that no health check is configured - it is not mandatory.
- If the resource is not accessible (returning 404 or 503), it may indicate that a health check is needed and has not passed. Your action is required.

More details in the documentation.">
@forelse ($this->logLines as $line)
$line['command'] ?? false, + 'mt-2' => isset($line['command']) && $line['command'], 'flex gap-2 dark:hover:bg-coolgray-500 hover:bg-gray-100', ])> {{ $line['timestamp'] }} $line['hidden'], 'text-red-500' => $line['stderr'], - 'font-bold' => $line['command'] ?? false, + 'font-bold' => isset($line['command']) && $line['command'], 'whitespace-pre-wrap', - ])>{!! $line['line'] !!} + ])>{!! (isset($line['command']) && $line['command'] ? '[CMD]: ' : '') . trim($line['line']) !!}
@empty No logs yet. diff --git a/resources/views/livewire/project/database/heading.blade.php b/resources/views/livewire/project/database/heading.blade.php index 9d34f7dcf..81346ca69 100644 --- a/resources/views/livewire/project/database/heading.blade.php +++ b/resources/views/livewire/project/database/heading.blade.php @@ -1,4 +1,4 @@ -