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.