Error: '.$error);
return;
}
} catch (\Throwable $e) {
return handleError($e, $this);
+ } finally {
+ $this->dispatch('refreshServerShow');
+ $this->server->refresh();
}
}
-
- public function mount()
- {
- $this->parameters = get_route_parameters();
- }
}
diff --git a/app/Livewire/Settings/Index.php b/app/Livewire/Settings/Index.php
index 754f0929b..9747329f6 100644
--- a/app/Livewire/Settings/Index.php
+++ b/app/Livewire/Settings/Index.php
@@ -25,6 +25,10 @@ class Index extends Component
public string $update_check_frequency;
+ public $timezones;
+
+ public bool $disable_two_step_confirmation;
+
protected string $dynamic_config_path = '/data/coolify/proxy/dynamic';
protected Server $server;
@@ -38,6 +42,8 @@ class Index extends Component
'settings.instance_name' => 'nullable',
'settings.allowed_ips' => 'nullable',
'settings.is_auto_update_enabled' => 'boolean',
+ 'settings.public_ipv4' => 'nullable',
+ 'settings.public_ipv6' => 'nullable',
'auto_update_frequency' => 'string',
'update_check_frequency' => 'string',
'settings.instance_timezone' => 'required|string|timezone',
@@ -51,16 +57,18 @@ class Index extends Component
'settings.custom_dns_servers' => 'Custom DNS servers',
'settings.allowed_ips' => 'Allowed IPs',
'settings.is_auto_update_enabled' => 'Auto Update Enabled',
+ 'settings.public_ipv4' => 'IPv4',
+ 'settings.public_ipv6' => 'IPv6',
'auto_update_frequency' => 'Auto Update Frequency',
'update_check_frequency' => 'Update Check Frequency',
+ 'settings.instance_timezone' => 'Instance Timezone',
];
- public $timezones;
-
public function mount()
{
if (isInstanceAdmin()) {
$this->settings = instanceSettings();
+ loggy($this->settings);
$this->do_not_track = $this->settings->do_not_track;
$this->is_auto_update_enabled = $this->settings->is_auto_update_enabled;
$this->is_registration_enabled = $this->settings->is_registration_enabled;
@@ -69,6 +77,7 @@ public function mount()
$this->auto_update_frequency = $this->settings->auto_update_frequency;
$this->update_check_frequency = $this->settings->update_check_frequency;
$this->timezones = collect(timezone_identifiers_list())->sort()->values()->toArray();
+ $this->disable_two_step_confirmation = $this->settings->disable_two_step_confirmation;
} else {
return redirect()->route('dashboard');
}
@@ -83,6 +92,7 @@ public function instantSave()
$this->settings->is_api_enabled = $this->is_api_enabled;
$this->settings->auto_update_frequency = $this->auto_update_frequency;
$this->settings->update_check_frequency = $this->update_check_frequency;
+ $this->settings->disable_two_step_confirmation = $this->disable_two_step_confirmation;
$this->settings->save();
$this->dispatch('success', 'Settings updated!');
}
@@ -170,15 +180,16 @@ public function checkManually()
}
}
- public function updatedSettingsInstanceTimezone($value)
- {
- $this->settings->instance_timezone = $value;
- $this->settings->save();
- $this->dispatch('success', 'Instance timezone updated.');
- }
-
public function render()
{
return view('livewire.settings.index');
}
+
+ public function toggleTwoStepConfirmation()
+ {
+ $this->settings->disable_two_step_confirmation = true;
+ $this->settings->save();
+ $this->disable_two_step_confirmation = true;
+ $this->dispatch('success', 'Two step confirmation has been disabled.');
+ }
}
diff --git a/app/Livewire/Source/Github/Create.php b/app/Livewire/Source/Github/Create.php
index f85e8646e..103c5c9fb 100644
--- a/app/Livewire/Source/Github/Create.php
+++ b/app/Livewire/Source/Github/Create.php
@@ -23,7 +23,7 @@ class Create extends Component
public function mount()
{
- $this->name = generate_random_name();
+ $this->name = substr(generate_random_name(), 0, 34); // GitHub Apps names can only be 34 characters long
}
public function createGitHubApp()
diff --git a/app/Models/Application.php b/app/Models/Application.php
index 07aeb4c5b..846d7df4c 100644
--- a/app/Models/Application.php
+++ b/app/Models/Application.php
@@ -1400,13 +1400,21 @@ public static function getDomainsByUuid(string $uuid): array
return [];
}
- public function getMetrics(int $mins = 5)
+ public function getCpuMetrics(int $mins = 5)
{
$server = $this->destination->server;
$container_name = $this->uuid;
if ($server->isMetricsEnabled()) {
$from = now()->subMinutes($mins)->toIso8601ZuluString();
- $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false);
+ if (isDev() && $server->id === 0) {
+ $process = Process::run("curl -H \"Authorization: Bearer {$this->settings->sentinel_token}\" http://host.docker.internal:8888/api/container/{$container_name}/cpu/history?from=$from");
+ if ($process->failed()) {
+ throw new \Exception($process->errorOutput());
+ }
+ $metrics = $process->output();
+ } else {
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false);
+ }
if (str($metrics)->contains('error')) {
$error = json_decode($metrics, true);
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
@@ -1415,14 +1423,41 @@ public function getMetrics(int $mins = 5)
}
throw new \Exception($error);
}
- $metrics = str($metrics)->explode("\n")->skip(1)->all();
- $parsedCollection = collect($metrics)->flatMap(function ($item) {
- return collect(explode("\n", trim($item)))->map(function ($line) {
- [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line));
- $cpu_usage_percent = number_format($cpu_usage_percent, 2);
+ $metrics = json_decode($metrics, true);
+ $parsedCollection = collect($metrics)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['percent']];
+ });
- return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage];
- });
+ return $parsedCollection->toArray();
+ }
+ }
+
+ public function getMemoryMetrics(int $mins = 5)
+ {
+ $server = $this->destination->server;
+ $container_name = $this->uuid;
+ if ($server->isMetricsEnabled()) {
+ $from = now()->subMinutes($mins)->toIso8601ZuluString();
+ if (isDev() && $server->id === 0) {
+ $process = Process::run("curl -H \"Authorization: Bearer {$this->settings->sentinel_token}\" http://host.docker.internal:8888/api/container/{$container_name}/memory/history?from=$from");
+ if ($process->failed()) {
+ throw new \Exception($process->errorOutput());
+ }
+ $metrics = $process->output();
+ } else {
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false);
+ }
+ if (str($metrics)->contains('error')) {
+ $error = json_decode($metrics, true);
+ $error = data_get($error, 'error', 'Something is not okay, are you okay?');
+ if ($error == 'Unauthorized') {
+ $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
+ }
+ throw new \Exception($error);
+ }
+ $metrics = json_decode($metrics, true);
+ $parsedCollection = collect($metrics)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['used']];
});
return $parsedCollection->toArray();
@@ -1459,7 +1494,9 @@ public function generateConfig($is_json = false)
return $config;
}
- public function setConfig($config) {
+
+ public function setConfig($config)
+ {
$config = $config;
$validator = Validator::make(['config' => $config], [
diff --git a/app/Models/EnvironmentVariable.php b/app/Models/EnvironmentVariable.php
index 9f8e4b342..f77d73db8 100644
--- a/app/Models/EnvironmentVariable.php
+++ b/app/Models/EnvironmentVariable.php
@@ -44,7 +44,7 @@ class EnvironmentVariable extends Model
'version' => 'string',
];
- protected $appends = ['real_value', 'is_shared'];
+ protected $appends = ['real_value', 'is_shared', 'is_really_required'];
protected static function booted()
{
@@ -74,6 +74,9 @@ protected static function booted()
'version' => config('version'),
]);
});
+ static::saving(function (EnvironmentVariable $environmentVariable) {
+ $environmentVariable->updateIsShared();
+ });
}
public function service()
@@ -130,6 +133,13 @@ public function realValue(): Attribute
);
}
+ protected function isReallyRequired(): Attribute
+ {
+ return Attribute::make(
+ get: fn () => $this->is_required && str($this->real_value)->isEmpty(),
+ );
+ }
+
protected function isShared(): Attribute
{
return Attribute::make(
@@ -210,4 +220,11 @@ protected function key(): Attribute
set: fn (string $value) => str($value)->trim()->replace(' ', '_')->value,
);
}
+
+ protected function updateIsShared(): void
+ {
+ $type = str($this->value)->after('{{')->before('.')->value;
+ $isShared = str($this->value)->startsWith('{{'.$type) && str($this->value)->endsWith('}}');
+ $this->is_shared = $isShared;
+ }
}
diff --git a/app/Models/InstanceSettings.php b/app/Models/InstanceSettings.php
index bb3d1478b..8ac6e892a 100644
--- a/app/Models/InstanceSettings.php
+++ b/app/Models/InstanceSettings.php
@@ -2,6 +2,7 @@
namespace App\Models;
+use App\Jobs\PullHelperImageJob;
use App\Notifications\Channels\SendsEmail;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
@@ -21,8 +22,23 @@ class InstanceSettings extends Model implements SendsEmail
'is_auto_update_enabled' => 'boolean',
'auto_update_frequency' => 'string',
'update_check_frequency' => 'string',
+ 'sentinel_token' => 'encrypted',
];
+ protected static function booted(): void
+ {
+ static::updated(function ($settings) {
+ if ($settings->isDirty('helper_version')) {
+ Server::chunkById(100, function ($servers) {
+ foreach ($servers as $server) {
+ PullHelperImageJob::dispatch($server);
+ }
+ });
+ }
+ });
+
+ }
+
public function fqdn(): Attribute
{
return Attribute::make(
@@ -85,17 +101,4 @@ public function getTitleDisplayName(): string
return "[{$instanceName}]";
}
-
- public function helperVersion(): Attribute
- {
- return Attribute::make(
- get: function ($value) {
- if (isDev()) {
- return 'latest';
- }
-
- return $value;
- }
- );
- }
}
diff --git a/app/Models/ScheduledDatabaseBackup.php b/app/Models/ScheduledDatabaseBackup.php
index 3921e32e4..473fc7b4b 100644
--- a/app/Models/ScheduledDatabaseBackup.php
+++ b/app/Models/ScheduledDatabaseBackup.php
@@ -51,7 +51,6 @@ public function server()
}
}
-
return null;
}
}
diff --git a/app/Models/Server.php b/app/Models/Server.php
index 0eca3c168..04380fad9 100644
--- a/app/Models/Server.php
+++ b/app/Models/Server.php
@@ -7,6 +7,8 @@
use App\Jobs\PullSentinelImageJob;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
+use Illuminate\Database\Eloquent\SoftDeletes;
+use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Process;
@@ -43,7 +45,7 @@
class Server extends BaseModel
{
- use SchemalessAttributesTrait;
+ use SchemalessAttributesTrait,SoftDeletes;
public static $batch_counter = 0;
@@ -95,7 +97,8 @@ protected static function booted()
}
}
});
- static::deleting(function ($server) {
+
+ static::forceDeleting(function ($server) {
$server->destinations()->each(function ($destination) {
$destination->delete();
});
@@ -525,9 +528,20 @@ public function forceDisableServer()
Storage::disk('ssh-mux')->delete($this->muxFilename());
}
+ public function sentinelHeartbeat(bool $isReset = false)
+ {
+ $this->sentinel_updated_at = $isReset ? now()->subMinutes(6000) : now();
+ $this->save();
+ }
+
+ public function isSentinelLive()
+ {
+ return Carbon::parse($this->sentinel_updated_at)->isAfter(now()->subMinutes(4));
+ }
+
public function isSentinelEnabled()
{
- return $this->isMetricsEnabled() || $this->isServerApiEnabled();
+ return ($this->isMetricsEnabled() || $this->isServerApiEnabled()) && ! $this->isBuildServer();
}
public function isMetricsEnabled()
@@ -537,7 +551,7 @@ public function isMetricsEnabled()
public function isServerApiEnabled()
{
- return $this->settings->is_server_api_enabled;
+ return $this->settings->is_sentinel_enabled;
}
public function checkServerApi()
@@ -555,7 +569,6 @@ public function checkServerApi()
ray($process->exitCode(), $process->output(), $process->errorOutput());
throw new \Exception("Server API is not reachable on http://{$server_ip}:12172");
}
-
}
}
@@ -579,7 +592,15 @@ public function getCpuMetrics(int $mins = 5)
{
if ($this->isMetricsEnabled()) {
$from = now()->subMinutes($mins)->toIso8601ZuluString();
- $cpu = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$this->settings->metrics_token}\" http://localhost:8888/api/cpu/history?from=$from'"], $this, false);
+ if (isDev() && $this->id === 0) {
+ $process = Process::run("curl -H \"Authorization: Bearer {$this->settings->sentinel_token}\" http://host.docker.internal:8888/api/cpu/history?from=$from");
+ if ($process->failed()) {
+ throw new \Exception($process->errorOutput());
+ }
+ $cpu = $process->output();
+ } else {
+ $cpu = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$this->settings->sentinel_token}\" http://localhost:8888/api/cpu/history?from=$from'"], $this, false);
+ }
if (str($cpu)->contains('error')) {
$error = json_decode($cpu, true);
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
@@ -588,17 +609,13 @@ public function getCpuMetrics(int $mins = 5)
}
throw new \Exception($error);
}
- $cpu = str($cpu)->explode("\n")->skip(1)->all();
- $parsedCollection = collect($cpu)->flatMap(function ($item) {
- return collect(explode("\n", trim($item)))->map(function ($line) {
- [$time, $cpu_usage_percent] = explode(',', trim($line));
- $cpu_usage_percent = number_format($cpu_usage_percent, 0);
-
- return [(int) $time, (float) $cpu_usage_percent];
- });
+ $cpu = json_decode($cpu, true);
+ $parsedCollection = collect($cpu)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['percent']];
});
- return $parsedCollection->toArray();
+ return $parsedCollection;
+
}
}
@@ -606,7 +623,15 @@ public function getMemoryMetrics(int $mins = 5)
{
if ($this->isMetricsEnabled()) {
$from = now()->subMinutes($mins)->toIso8601ZuluString();
- $memory = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$this->settings->metrics_token}\" http://localhost:8888/api/memory/history?from=$from'"], $this, false);
+ if (isDev() && $this->id === 0) {
+ $process = Process::run("curl -H \"Authorization: Bearer {$this->settings->sentinel_token}\" http://host.docker.internal:8888/api/memory/history?from=$from");
+ if ($process->failed()) {
+ throw new \Exception($process->errorOutput());
+ }
+ $memory = $process->output();
+ } else {
+ $memory = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$this->settings->sentinel_token}\" http://localhost:8888/api/memory/history?from=$from'"], $this, false);
+ }
if (str($memory)->contains('error')) {
$error = json_decode($memory, true);
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
@@ -615,14 +640,9 @@ public function getMemoryMetrics(int $mins = 5)
}
throw new \Exception($error);
}
- $memory = str($memory)->explode("\n")->skip(1)->all();
- $parsedCollection = collect($memory)->flatMap(function ($item) {
- return collect(explode("\n", trim($item)))->map(function ($line) {
- [$time, $used, $free, $usedPercent] = explode(',', trim($line));
- $usedPercent = number_format($usedPercent, 0);
-
- return [(int) $time, (float) $usedPercent];
- });
+ $memory = json_decode($memory, true);
+ $parsedCollection = collect($memory)->map(function ($metric) {
+ return [(int) $metric['time'], (float) $metric['usedPercent']];
});
return $parsedCollection->toArray();
@@ -977,7 +997,8 @@ public function team()
public function isProxyShouldRun()
{
- if ($this->proxyType() === ProxyTypes::NONE->value || $this->settings->is_build_server) {
+ // TODO: Do we need "|| $this->proxy->force_stop" here?
+ if ($this->proxyType() === ProxyTypes::NONE->value || $this->isBuildServer()) {
return false;
}
@@ -1041,6 +1062,38 @@ public function isSwarmWorker()
return data_get($this, 'settings.is_swarm_worker');
}
+ public function status(): bool
+ {
+ ['uptime' => $uptime] = $this->validateConnection(false);
+ if ($uptime) {
+ if ($this->unreachable_notification_sent === true) {
+ $this->update(['unreachable_notification_sent' => false]);
+ }
+ } else {
+ // $this->server->team?->notify(new Unreachable($this->server));
+ foreach ($this->applications as $application) {
+ $application->update(['status' => 'exited']);
+ }
+ foreach ($this->databases as $database) {
+ $database->update(['status' => 'exited']);
+ }
+ foreach ($this->services as $service) {
+ $apps = $service->applications()->get();
+ $dbs = $service->databases()->get();
+ foreach ($apps as $app) {
+ $app->update(['status' => 'exited']);
+ }
+ foreach ($dbs as $db) {
+ $db->update(['status' => 'exited']);
+ }
+ }
+
+ return false;
+ }
+
+ return true;
+ }
+
public function validateConnection($isManualCheck = true)
{
config()->set('constants.ssh.mux_enabled', ! $isManualCheck);
diff --git a/app/Models/ServerSetting.php b/app/Models/ServerSetting.php
index c44a393b4..b1ed92d95 100644
--- a/app/Models/ServerSetting.php
+++ b/app/Models/ServerSetting.php
@@ -24,7 +24,7 @@
'is_logdrain_newrelic_enabled' => ['type' => 'boolean'],
'is_metrics_enabled' => ['type' => 'boolean'],
'is_reachable' => ['type' => 'boolean'],
- 'is_server_api_enabled' => ['type' => 'boolean'],
+ 'is_sentinel_enabled' => ['type' => 'boolean'],
'is_swarm_manager' => ['type' => 'boolean'],
'is_swarm_worker' => ['type' => 'boolean'],
'is_usable' => ['type' => 'boolean'],
@@ -35,9 +35,9 @@
'logdrain_highlight_project_id' => ['type' => 'string'],
'logdrain_newrelic_base_uri' => ['type' => 'string'],
'logdrain_newrelic_license_key' => ['type' => 'string'],
- 'metrics_history_days' => ['type' => 'integer'],
- 'metrics_refresh_rate_seconds' => ['type' => 'integer'],
- 'metrics_token' => ['type' => 'string'],
+ 'sentinel_metrics_history_days' => ['type' => 'integer'],
+ 'sentinel_metrics_refresh_rate_seconds' => ['type' => 'integer'],
+ 'sentinel_token' => ['type' => 'string'],
'docker_cleanup_frequency' => ['type' => 'string'],
'docker_cleanup_threshold' => ['type' => 'integer'],
'server_id' => ['type' => 'integer'],
@@ -53,8 +53,66 @@ class ServerSetting extends Model
protected $casts = [
'force_docker_cleanup' => 'boolean',
'docker_cleanup_threshold' => 'integer',
+ 'sentinel_token' => 'encrypted',
];
+ protected static function booted()
+ {
+ static::creating(function ($setting) {
+ try {
+ if (str($setting->sentinel_token)->isEmpty()) {
+ $setting->generateSentinelToken(save: false);
+ }
+ if (str($setting->sentinel_custom_url)->isEmpty()) {
+ $url = $setting->generateSentinelUrl(save: false);
+ if (str($url)->isEmpty()) {
+ $setting->is_sentinel_enabled = false;
+ } else {
+ $setting->is_sentinel_enabled = true;
+ }
+ }
+ } catch (\Throwable $e) {
+ loggy('Error creating server setting: '.$e->getMessage());
+ }
+ });
+ }
+
+ public function generateSentinelToken(bool $save = true)
+ {
+ $data = [
+ 'server_uuid' => $this->server->uuid,
+ ];
+ $token = json_encode($data);
+ $encrypted = encrypt($token);
+ $this->sentinel_token = $encrypted;
+ if ($save) {
+ $this->save();
+ }
+
+ return $encrypted;
+ }
+
+ public function generateSentinelUrl(bool $save = true)
+ {
+ $domain = null;
+ $settings = InstanceSettings::get();
+ if ($this->server->isLocalhost()) {
+ $domain = 'http://host.docker.internal:8000';
+ } elseif ($settings->fqdn) {
+ $domain = $settings->fqdn;
+ } elseif ($settings->ipv4) {
+ $domain = $settings->ipv4.':8000';
+ } elseif ($settings->ipv6) {
+ $domain = $settings->ipv6.':8000';
+ }
+ $this->sentinel_custom_url = $domain;
+ if ($save) {
+ $this->save();
+ }
+
+ return $domain;
+ }
+
public function server()
{
return $this->belongsTo(Server::class);
diff --git a/app/Models/Service.php b/app/Models/Service.php
index 0036a9fda..0af1adf22 100644
--- a/app/Models/Service.php
+++ b/app/Models/Service.php
@@ -297,7 +297,7 @@ public function extraFields()
'key' => 'CP_DISABLE_HTTPS',
'value' => data_get($disable_https, 'value'),
'rules' => 'required',
- 'customHelper' => "If you want to use https, set this to 0. Variable name: CP_DISABLE_HTTPS",
+ 'customHelper' => 'If you want to use https, set this to 0. Variable name: CP_DISABLE_HTTPS',
],
]);
}
@@ -997,8 +997,8 @@ public function extraFields()
break;
case $image->contains('mysql'):
$userVariables = ['SERVICE_USER_MYSQL', 'SERVICE_USER_WORDPRESS', 'MYSQL_USER'];
- $passwordVariables = ['SERVICE_PASSWORD_MYSQL', 'SERVICE_PASSWORD_WORDPRESS', 'MYSQL_PASSWORD','SERVICE_PASSWORD_64_MYSQL'];
- $rootPasswordVariables = ['SERVICE_PASSWORD_MYSQLROOT', 'SERVICE_PASSWORD_ROOT','SERVICE_PASSWORD_64_MYSQLROOT'];
+ $passwordVariables = ['SERVICE_PASSWORD_MYSQL', 'SERVICE_PASSWORD_WORDPRESS', 'MYSQL_PASSWORD', 'SERVICE_PASSWORD_64_MYSQL'];
+ $rootPasswordVariables = ['SERVICE_PASSWORD_MYSQLROOT', 'SERVICE_PASSWORD_ROOT', 'SERVICE_PASSWORD_64_MYSQLROOT'];
$dbNameVariables = ['MYSQL_DATABASE'];
$mysql_user = $this->environment_variables()->whereIn('key', $userVariables)->first();
$mysql_password = $this->environment_variables()->whereIn('key', $passwordVariables)->first();
@@ -1232,7 +1232,6 @@ public function scheduled_tasks(): HasMany
public function environment_variables(): HasMany
{
-
return $this->hasMany(EnvironmentVariable::class)->orderByRaw("LOWER(key) LIKE LOWER('SERVICE%') DESC, LOWER(key) ASC");
}
@@ -1316,4 +1315,20 @@ public function networks()
return $networks;
}
+
+ protected function isDeployable(): Attribute
+ {
+ return Attribute::make(
+ get: function () {
+ $envs = $this->environment_variables()->where('is_required', true)->get();
+ foreach ($envs as $env) {
+ if ($env->is_really_required) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ );
+ }
}
diff --git a/app/Models/StandaloneClickhouse.php b/app/Models/StandaloneClickhouse.php
index e4341b1b9..6274f51b2 100644
--- a/app/Models/StandaloneClickhouse.php
+++ b/app/Models/StandaloneClickhouse.php
@@ -272,7 +272,7 @@ public function getMetrics(int $mins = 5)
$container_name = $this->uuid;
if ($server->isMetricsEnabled()) {
$from = now()->subMinutes($mins)->toIso8601ZuluString();
- $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false);
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false);
if (str($metrics)->contains('error')) {
$error = json_decode($metrics, true);
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
diff --git a/app/Models/StandaloneDragonfly.php b/app/Models/StandaloneDragonfly.php
index 94ab2d745..3555e7afd 100644
--- a/app/Models/StandaloneDragonfly.php
+++ b/app/Models/StandaloneDragonfly.php
@@ -272,7 +272,7 @@ public function getMetrics(int $mins = 5)
$container_name = $this->uuid;
if ($server->isMetricsEnabled()) {
$from = now()->subMinutes($mins)->toIso8601ZuluString();
- $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false);
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false);
if (str($metrics)->contains('error')) {
$error = json_decode($metrics, true);
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
diff --git a/app/Models/StandaloneKeydb.php b/app/Models/StandaloneKeydb.php
index 335c8931c..4725ca533 100644
--- a/app/Models/StandaloneKeydb.php
+++ b/app/Models/StandaloneKeydb.php
@@ -272,7 +272,7 @@ public function getMetrics(int $mins = 5)
$container_name = $this->uuid;
if ($server->isMetricsEnabled()) {
$from = now()->subMinutes($mins)->toIso8601ZuluString();
- $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false);
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false);
if (str($metrics)->contains('error')) {
$error = json_decode($metrics, true);
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
diff --git a/app/Models/StandaloneMariadb.php b/app/Models/StandaloneMariadb.php
index c6c08dee5..8f1a2c1ee 100644
--- a/app/Models/StandaloneMariadb.php
+++ b/app/Models/StandaloneMariadb.php
@@ -272,7 +272,7 @@ public function getMetrics(int $mins = 5)
$container_name = $this->uuid;
if ($server->isMetricsEnabled()) {
$from = now()->subMinutes($mins)->toIso8601ZuluString();
- $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false);
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false);
if (str($metrics)->contains('error')) {
$error = json_decode($metrics, true);
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
diff --git a/app/Models/StandaloneMongodb.php b/app/Models/StandaloneMongodb.php
index 99893b1d1..41b2ce9eb 100644
--- a/app/Models/StandaloneMongodb.php
+++ b/app/Models/StandaloneMongodb.php
@@ -292,7 +292,7 @@ public function getMetrics(int $mins = 5)
$container_name = $this->uuid;
if ($server->isMetricsEnabled()) {
$from = now()->subMinutes($mins)->toIso8601ZuluString();
- $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false);
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false);
if (str($metrics)->contains('error')) {
$error = json_decode($metrics, true);
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
diff --git a/app/Models/StandaloneMysql.php b/app/Models/StandaloneMysql.php
index f2a5b5c14..da2ac070f 100644
--- a/app/Models/StandaloneMysql.php
+++ b/app/Models/StandaloneMysql.php
@@ -273,7 +273,7 @@ public function getMetrics(int $mins = 5)
$container_name = $this->uuid;
if ($server->isMetricsEnabled()) {
$from = now()->subMinutes($mins)->toIso8601ZuluString();
- $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false);
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false);
if (str($metrics)->contains('error')) {
$error = json_decode($metrics, true);
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
diff --git a/app/Models/StandalonePostgresql.php b/app/Models/StandalonePostgresql.php
index 1b18a5ca7..e0f42269d 100644
--- a/app/Models/StandalonePostgresql.php
+++ b/app/Models/StandalonePostgresql.php
@@ -274,7 +274,7 @@ public function getMetrics(int $mins = 5)
$container_name = $this->uuid;
if ($server->isMetricsEnabled()) {
$from = now()->subMinutes($mins)->toIso8601ZuluString();
- $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false);
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false);
if (str($metrics)->contains('error')) {
$error = json_decode($metrics, true);
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
diff --git a/app/Models/StandaloneRedis.php b/app/Models/StandaloneRedis.php
index a5868e243..097d6b0de 100644
--- a/app/Models/StandaloneRedis.php
+++ b/app/Models/StandaloneRedis.php
@@ -210,7 +210,12 @@ public function databaseType(): Attribute
protected function internalDbUrl(): Attribute
{
return new Attribute(
- get: fn () => "redis://:{$this->redis_password}@{$this->uuid}:6379/0",
+ get: function () {
+ $redis_version = $this->getRedisVersion();
+ $username_part = version_compare($redis_version, '6.0', '>=') ? "{$this->redis_username}:" : '';
+
+ return "redis://{$username_part}{$this->redis_password}@{$this->uuid}:6379/0";
+ }
);
}
@@ -219,7 +224,10 @@ protected function externalDbUrl(): Attribute
return new Attribute(
get: function () {
if ($this->is_public && $this->public_port) {
- return "redis://:{$this->redis_password}@{$this->destination->server->getIp}:{$this->public_port}/0";
+ $redis_version = $this->getRedisVersion();
+ $username_part = version_compare($redis_version, '6.0', '>=') ? "{$this->redis_username}:" : '';
+
+ return "redis://{$username_part}{$this->redis_password}@{$this->destination->server->getIp}:{$this->public_port}/0";
}
return null;
@@ -227,6 +235,13 @@ protected function externalDbUrl(): Attribute
);
}
+ public function getRedisVersion()
+ {
+ $image_parts = explode(':', $this->image);
+
+ return $image_parts[1] ?? '0.0';
+ }
+
public function environment()
{
return $this->belongsTo(Environment::class);
@@ -268,7 +283,7 @@ public function getMetrics(int $mins = 5)
$container_name = $this->uuid;
if ($server->isMetricsEnabled()) {
$from = now()->subMinutes($mins)->toIso8601ZuluString();
- $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false);
+ $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false);
if (str($metrics)->contains('error')) {
$error = json_decode($metrics, true);
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
@@ -295,4 +310,33 @@ public function isBackupSolutionAvailable()
{
return false;
}
+
+ public function redisPassword(): Attribute
+ {
+ return new Attribute(
+ get: function () {
+ $password = $this->runtime_environment_variables()->where('key', 'REDIS_PASSWORD')->first();
+ if (! $password) {
+ return null;
+ }
+
+ return $password->value;
+ },
+
+ );
+ }
+
+ public function redisUsername(): Attribute
+ {
+ return new Attribute(
+ get: function () {
+ $username = $this->runtime_environment_variables()->where('key', 'REDIS_USERNAME')->first();
+ if (! $username) {
+ return null;
+ }
+
+ return $username->value;
+ }
+ );
+ }
}
diff --git a/bootstrap/helpers/databases.php b/bootstrap/helpers/databases.php
index 950eb67b6..e12910f82 100644
--- a/bootstrap/helpers/databases.php
+++ b/bootstrap/helpers/databases.php
@@ -1,5 +1,6 @@
name = generate_database_name('redis');
- $database->redis_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
+ $redis_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
$database->environment_id = $environment_id;
$database->destination_id = $destination->id;
$database->destination_type = $destination->getMorphClass();
@@ -57,6 +58,20 @@ function create_standalone_redis($environment_id, $destination_uuid, ?array $oth
}
$database->save();
+ EnvironmentVariable::create([
+ 'key' => 'REDIS_PASSWORD',
+ 'value' => $redis_password,
+ 'standalone_redis_id' => $database->id,
+ 'is_shared' => false,
+ ]);
+
+ EnvironmentVariable::create([
+ 'key' => 'REDIS_USERNAME',
+ 'value' => 'default',
+ 'standalone_redis_id' => $database->id,
+ 'is_shared' => false,
+ ]);
+
return $database;
}
diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php
index 397bce029..55985b84f 100644
--- a/bootstrap/helpers/docker.php
+++ b/bootstrap/helpers/docker.php
@@ -335,10 +335,11 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
if (preg_match('/coolify\.traefik\.middlewares=(.*)/', $item, $matches)) {
return explode(',', $matches[1]);
}
+
return null;
})->flatten()
- ->filter()
- ->unique();
+ ->filter()
+ ->unique();
}
foreach ($domains as $loop => $domain) {
try {
@@ -388,7 +389,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
if ($path !== '/') {
// Middleware handling
$middlewares = collect([]);
- if ($is_stripprefix_enabled && !str($image)->contains('ghost')) {
+ if ($is_stripprefix_enabled && ! str($image)->contains('ghost')) {
$labels->push("traefik.http.middlewares.{$https_label}-stripprefix.stripprefix.prefixes={$path}");
$middlewares->push("{$https_label}-stripprefix");
}
@@ -402,7 +403,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$labels = $labels->merge($redirect_to_non_www);
$middlewares->push($to_non_www_name);
}
- if ($redirect_direction === 'www' && !str($host)->startsWith('www.')) {
+ if ($redirect_direction === 'www' && ! str($host)->startsWith('www.')) {
$labels = $labels->merge($redirect_to_www);
$middlewares->push($to_www_name);
}
@@ -417,7 +418,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$middlewares = collect([]);
if ($is_gzip_enabled) {
$middlewares->push('gzip');
- }
+ }
if (str($image)->contains('ghost')) {
$middlewares->push('redir-ghost');
}
diff --git a/bootstrap/helpers/proxy.php b/bootstrap/helpers/proxy.php
index 33d19d9cc..496017217 100644
--- a/bootstrap/helpers/proxy.php
+++ b/bootstrap/helpers/proxy.php
@@ -241,6 +241,7 @@ function generate_default_proxy_configuration(Server $server)
'ports' => [
'80:80',
'443:443',
+ '443:443/udp',
],
'labels' => [
'coolify.managed=true',
diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php
index cfdea81fb..cd0eb709a 100644
--- a/bootstrap/helpers/shared.php
+++ b/bootstrap/helpers/shared.php
@@ -126,7 +126,7 @@ function refreshSession(?Team $team = null): void
}
function handleError(?Throwable $error = null, ?Livewire\Component $livewire = null, ?string $customErrorMessage = null)
{
- ray($error);
+ loggy($error);
if ($error instanceof TooManyRequestsException) {
if (isset($livewire)) {
return $livewire->dispatch('error', "Too many requests. Please try again in {$error->secondsUntilAvailable} seconds.");
@@ -142,6 +142,10 @@ function handleError(?Throwable $error = null, ?Livewire\Component $livewire = n
return 'Duplicate entry found. Please use a different name.';
}
+ if ($error instanceof \Illuminate\Database\Eloquent\ModelNotFoundException) {
+ abort(404);
+ }
+
if ($error instanceof Throwable) {
$message = $error->getMessage();
} else {
@@ -164,10 +168,10 @@ function get_route_parameters(): array
function get_latest_sentinel_version(): string
{
try {
- $response = Http::get('https://cdn.coollabs.io/sentinel/versions.json');
+ $response = Http::get('https://cdn.coollabs.io/coolify/versions.json');
$versions = $response->json();
- return data_get($versions, 'sentinel.version');
+ return data_get($versions, 'coolify.sentinel.version');
} catch (\Throwable $e) {
//throw $e;
ray($e->getMessage());
@@ -1338,13 +1342,6 @@ function isAnyDeploymentInprogress()
exit(0);
}
-function generateSentinelToken()
-{
- $token = Str::random(64);
-
- return $token;
-}
-
function isBase64Encoded($strValue)
{
return base64_encode(base64_decode($strValue, true)) === $strValue;
@@ -3569,6 +3566,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
]);
} else {
if ($value->startsWith('$')) {
+ $isRequired = false;
if ($value->contains(':-')) {
$value = replaceVariables($value);
$key = $value->before(':');
@@ -3583,11 +3581,13 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
$key = $value->before(':');
$value = $value->after(':?');
+ $isRequired = true;
} elseif ($value->contains('?')) {
$value = replaceVariables($value);
$key = $value->before('?');
$value = $value->after('?');
+ $isRequired = true;
}
if ($originalValue->value() === $value->value()) {
// This means the variable does not have a default value, so it needs to be created in Coolify
@@ -3598,6 +3598,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
], [
'is_build_time' => false,
'is_preview' => false,
+ 'is_required' => $isRequired,
]);
// Add the variable to the environment so it will be shown in the deployable compose file
$environment[$parsedKeyValue->value()] = $resource->environment_variables()->where('key', $parsedKeyValue)->where($nameOfId, $resource->id)->first()->value;
@@ -3611,6 +3612,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
'value' => $value,
'is_build_time' => false,
'is_preview' => false,
+ 'is_required' => $isRequired,
]);
}
@@ -3787,7 +3789,6 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
service_name: $serviceName,
image: $image,
predefinedPort: $predefinedPort
-
));
}
}
@@ -3985,13 +3986,14 @@ function instanceSettings()
return InstanceSettings::get();
}
-function loadConfigFromGit(string $repository, string $branch, string $base_directory, int $server_id, int $team_id) {
+function loadConfigFromGit(string $repository, string $branch, string $base_directory, int $server_id, int $team_id)
+{
$server = Server::find($server_id)->where('team_id', $team_id)->first();
- if (!$server) {
+ if (! $server) {
return;
}
- $uuid = new Cuid2();
+ $uuid = new Cuid2;
$cloneCommand = "git clone --no-checkout -b $branch $repository .";
$workdir = rtrim($base_directory, '/');
$fileList = collect([".$workdir/coolify.json"]);
@@ -4009,6 +4011,33 @@ function loadConfigFromGit(string $repository, string $branch, string $base_dire
try {
return instant_remote_process($commands, $server);
} catch (\Exception $e) {
- // continue
+ // continue
}
}
+
+function loggy($message = null, array $context = [])
+{
+ if (! isDev()) {
+ return;
+ }
+ if (function_exists('ray') && config('app.debug')) {
+ ray($message, $context);
+ }
+ if (is_null($message)) {
+ return app('log');
+ }
+
+ return app('log')->debug($message, $context);
+}
+function sslipDomainWarning(string $domains)
+{
+ $domains = str($domains)->trim()->explode(',');
+ $showSslipHttpsWarning = false;
+ $domains->each(function ($domain) use (&$showSslipHttpsWarning) {
+ if (str($domain)->contains('https') && str($domain)->contains('sslip')) {
+ $showSslipHttpsWarning = true;
+ }
+ });
+
+ return $showSslipHttpsWarning;
+}
diff --git a/composer.json b/composer.json
index fbd77d0cf..b17c3bf4e 100644
--- a/composer.json
+++ b/composer.json
@@ -15,6 +15,7 @@
"laravel/fortify": "^v1.16.0",
"laravel/framework": "^v11",
"laravel/horizon": "^5.29.1",
+ "laravel/pail": "^1.1",
"laravel/prompts": "^0.1.6",
"laravel/sanctum": "^v4.0",
"laravel/socialite": "^v5.14.0",
diff --git a/composer.lock b/composer.lock
index 0b8da82d0..981e723d4 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "c47adf3684eb727e22503937435c0914",
+ "content-hash": "943975ec232403b96a40d215253492d8",
"packages": [
{
"name": "amphp/amp",
@@ -3144,6 +3144,83 @@
},
"time": "2024-10-08T18:23:02+00:00"
},
+ {
+ "name": "laravel/pail",
+ "version": "v1.1.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/laravel/pail.git",
+ "reference": "b33ad8321416fe86efed7bf398f3306c47b4871b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/laravel/pail/zipball/b33ad8321416fe86efed7bf398f3306c47b4871b",
+ "reference": "b33ad8321416fe86efed7bf398f3306c47b4871b",
+ "shasum": ""
+ },
+ "require": {
+ "ext-mbstring": "*",
+ "illuminate/console": "^10.24|^11.0",
+ "illuminate/contracts": "^10.24|^11.0",
+ "illuminate/log": "^10.24|^11.0",
+ "illuminate/process": "^10.24|^11.0",
+ "illuminate/support": "^10.24|^11.0",
+ "nunomaduro/termwind": "^1.15|^2.0",
+ "php": "^8.2",
+ "symfony/console": "^6.0|^7.0"
+ },
+ "require-dev": {
+ "laravel/pint": "^1.13",
+ "orchestra/testbench": "^8.12|^9.0",
+ "pestphp/pest": "^2.20",
+ "pestphp/pest-plugin-type-coverage": "^2.3",
+ "phpstan/phpstan": "^1.10",
+ "symfony/var-dumper": "^6.3|^7.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "1.x-dev"
+ },
+ "laravel": {
+ "providers": [
+ "Laravel\\Pail\\PailServiceProvider"
+ ]
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Laravel\\Pail\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Taylor Otwell",
+ "email": "taylor@laravel.com"
+ },
+ {
+ "name": "Nuno Maduro",
+ "email": "enunomaduro@gmail.com"
+ }
+ ],
+ "description": "Easily delve into your Laravel application's log files directly from the command line.",
+ "homepage": "https://github.com/laravel/pail",
+ "keywords": [
+ "laravel",
+ "logs",
+ "php",
+ "tail"
+ ],
+ "support": {
+ "issues": "https://github.com/laravel/pail/issues",
+ "source": "https://github.com/laravel/pail"
+ },
+ "time": "2024-10-15T20:06:24+00:00"
+ },
{
"name": "laravel/prompts",
"version": "v0.1.25",
diff --git a/config/sentry.php b/config/sentry.php
index ade6923ac..e8b6ab098 100644
--- a/config/sentry.php
+++ b/config/sentry.php
@@ -7,7 +7,7 @@
// The release version of your application
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
- 'release' => '4.0.0-beta.360',
+ 'release' => '4.0.0-beta.361',
// When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'),
diff --git a/config/testing.php b/config/testing.php
new file mode 100644
index 000000000..41b8eadf0
--- /dev/null
+++ b/config/testing.php
@@ -0,0 +1,6 @@
+ env('DUSK_TEST_EMAIL', 'test@example.com'),
+ 'dusk_test_password' => env('DUSK_TEST_PASSWORD', 'password'),
+];
diff --git a/config/version.php b/config/version.php
index 5639fc8a8..0e83ff40e 100644
--- a/config/version.php
+++ b/config/version.php
@@ -1,3 +1,3 @@
boolean('is_metrics_enabled')->default(false);
$table->integer('metrics_refresh_rate_seconds')->default(5);
$table->integer('metrics_history_days')->default(30);
- $table->string('metrics_token')->default(generateSentinelToken());
+ $table->string('metrics_token')->nullable();
});
}
diff --git a/database/migrations/2024_06_25_184323_update_db.php b/database/migrations/2024_06_25_184323_update_db.php
index f1b175a9c..8f9405b86 100644
--- a/database/migrations/2024_06_25_184323_update_db.php
+++ b/database/migrations/2024_06_25_184323_update_db.php
@@ -4,6 +4,7 @@
use App\Models\Server;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Visus\Cuid2\Cuid2;
@@ -14,44 +15,45 @@
*/
public function up(): void
{
- Schema::table('applications', function (Blueprint $table) {
- $table->dropColumn('docker_compose_pr_location');
- $table->dropColumn('docker_compose_pr');
- $table->dropColumn('docker_compose_pr_raw');
- });
- Schema::table('subscriptions', function (Blueprint $table) {
- $table->dropColumn('lemon_subscription_id');
- $table->dropColumn('lemon_order_id');
- $table->dropColumn('lemon_product_id');
- $table->dropColumn('lemon_variant_id');
- $table->dropColumn('lemon_variant_name');
- $table->dropColumn('lemon_customer_id');
- $table->dropColumn('lemon_status');
- $table->dropColumn('lemon_renews_at');
- $table->dropColumn('lemon_update_payment_menthod_url');
- $table->dropColumn('lemon_trial_ends_at');
- $table->dropColumn('lemon_ends_at');
- });
- Schema::table('environment_variables', function (Blueprint $table) {
- $table->string('uuid')->nullable()->after('id');
- });
+ try {
+ Schema::table('applications', function (Blueprint $table) {
+ $table->dropColumn('docker_compose_pr_location');
+ $table->dropColumn('docker_compose_pr');
+ $table->dropColumn('docker_compose_pr_raw');
+ });
+ Schema::table('subscriptions', function (Blueprint $table) {
+ $table->dropColumn('lemon_subscription_id');
+ $table->dropColumn('lemon_order_id');
+ $table->dropColumn('lemon_product_id');
+ $table->dropColumn('lemon_variant_id');
+ $table->dropColumn('lemon_variant_name');
+ $table->dropColumn('lemon_customer_id');
+ $table->dropColumn('lemon_status');
+ $table->dropColumn('lemon_renews_at');
+ $table->dropColumn('lemon_update_payment_menthod_url');
+ $table->dropColumn('lemon_trial_ends_at');
+ $table->dropColumn('lemon_ends_at');
+ });
+ Schema::table('environment_variables', function (Blueprint $table) {
+ $table->string('uuid')->nullable()->after('id');
+ });
- EnvironmentVariable::all()->each(function (EnvironmentVariable $environmentVariable) {
- $environmentVariable->update([
- 'uuid' => (string) new Cuid2,
- ]);
- });
- Schema::table('environment_variables', function (Blueprint $table) {
- $table->string('uuid')->nullable(false)->change();
- });
- Schema::table('server_settings', function (Blueprint $table) {
- $table->integer('metrics_history_days')->default(7)->change();
- });
- Server::all()->each(function (Server $server) {
- $server->settings->update([
- 'metrics_history_days' => 7,
- ]);
- });
+ EnvironmentVariable::all()->each(function (EnvironmentVariable $environmentVariable) {
+ $environmentVariable->update([
+ 'uuid' => (string) new Cuid2,
+ ]);
+ });
+ Schema::table('environment_variables', function (Blueprint $table) {
+ $table->string('uuid')->nullable(false)->change();
+ });
+ Schema::table('server_settings', function (Blueprint $table) {
+ $table->integer('metrics_history_days')->default(7)->change();
+ });
+
+ DB::table('server_settings')->update(['metrics_history_days' => 7]);
+ } catch (\Exception $e) {
+ loggy($e);
+ }
}
/**
diff --git a/database/migrations/2024_07_18_123458_add_force_cleanup_server.php b/database/migrations/2024_07_18_123458_add_force_cleanup_server.php
index a33665bd0..ea3695b3f 100644
--- a/database/migrations/2024_07_18_123458_add_force_cleanup_server.php
+++ b/database/migrations/2024_07_18_123458_add_force_cleanup_server.php
@@ -12,7 +12,7 @@
public function up(): void
{
Schema::table('server_settings', function (Blueprint $table) {
- $table->boolean('is_force_cleanup_enabled')->default(false)->after('is_sentinel_enabled');
+ $table->boolean('is_force_cleanup_enabled')->default(false);
});
}
diff --git a/database/migrations/2024_10_11_114331_add_required_env_variables.php b/database/migrations/2024_10_11_114331_add_required_env_variables.php
new file mode 100644
index 000000000..4fde0c2bb
--- /dev/null
+++ b/database/migrations/2024_10_11_114331_add_required_env_variables.php
@@ -0,0 +1,28 @@
+boolean('is_required')->default(false);
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('environment_variables', function (Blueprint $table) {
+ $table->dropColumn('is_required');
+ });
+ }
+};
diff --git a/database/migrations/2024_10_14_090416_update_metrics_token_in_server_settings.php b/database/migrations/2024_10_14_090416_update_metrics_token_in_server_settings.php
new file mode 100644
index 000000000..d5c38501f
--- /dev/null
+++ b/database/migrations/2024_10_14_090416_update_metrics_token_in_server_settings.php
@@ -0,0 +1,54 @@
+dropColumn('metrics_token');
+ $table->dropColumn('metrics_refresh_rate_seconds');
+ $table->dropColumn('metrics_history_days');
+ $table->dropColumn('is_server_api_enabled');
+
+ $table->boolean('is_sentinel_enabled')->default(false);
+ $table->text('sentinel_token')->nullable();
+ $table->integer('sentinel_metrics_refresh_rate_seconds')->default(10);
+ $table->integer('sentinel_metrics_history_days')->default(7);
+ $table->integer('sentinel_push_interval_seconds')->default(60);
+ $table->string('sentinel_custom_url')->nullable();
+ });
+ Schema::table('servers', function (Blueprint $table) {
+ $table->dateTime('sentinel_updated_at')->default(now());
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('server_settings', function (Blueprint $table) {
+ $table->string('metrics_token')->nullable();
+ $table->integer('metrics_refresh_rate_seconds')->default(5);
+ $table->integer('metrics_history_days')->default(30);
+ $table->boolean('is_server_api_enabled')->default(false);
+
+ $table->dropColumn('is_sentinel_enabled');
+ $table->dropColumn('sentinel_token');
+ $table->dropColumn('sentinel_metrics_refresh_rate_seconds');
+ $table->dropColumn('sentinel_metrics_history_days');
+ $table->dropColumn('sentinel_push_interval_seconds');
+ $table->dropColumn('sentinel_custom_url');
+ });
+ Schema::table('servers', function (Blueprint $table) {
+ $table->dropColumn('sentinel_updated_at');
+ });
+ }
+};
diff --git a/database/migrations/2024_10_15_172139_add_is_shared_to_environment_variables.php b/database/migrations/2024_10_15_172139_add_is_shared_to_environment_variables.php
new file mode 100644
index 000000000..eb878e2f6
--- /dev/null
+++ b/database/migrations/2024_10_15_172139_add_is_shared_to_environment_variables.php
@@ -0,0 +1,22 @@
+boolean('is_shared')->default(false);
+ });
+ }
+
+ public function down()
+ {
+ Schema::table('environment_variables', function (Blueprint $table) {
+ $table->dropColumn('is_shared');
+ });
+ }
+}
diff --git a/database/migrations/2024_10_16_120026_move_redis_password_to_envs.php b/database/migrations/2024_10_16_120026_move_redis_password_to_envs.php
new file mode 100644
index 000000000..fa01e8e85
--- /dev/null
+++ b/database/migrations/2024_10_16_120026_move_redis_password_to_envs.php
@@ -0,0 +1,41 @@
+where('id', $redis->id)->value('redis_password');
+ EnvironmentVariable::create([
+ 'standalone_redis_id' => $redis->id,
+ 'key' => 'REDIS_PASSWORD',
+ 'value' => $redis_password,
+ ]);
+ EnvironmentVariable::create([
+ 'standalone_redis_id' => $redis->id,
+ 'key' => 'REDIS_USERNAME',
+ 'value' => 'default',
+ ]);
+ }
+ });
+ Schema::table('standalone_redis', function (Blueprint $table) {
+ $table->dropColumn('redis_password');
+ });
+ } catch (\Exception $e) {
+ echo 'Moving Redis passwords to envs failed.';
+ echo $e->getMessage();
+ }
+ }
+}
diff --git a/database/migrations/2024_10_16_192133_add_confirmation_settings_to_instance_settings_table.php b/database/migrations/2024_10_16_192133_add_confirmation_settings_to_instance_settings_table.php
new file mode 100644
index 000000000..7040daf44
--- /dev/null
+++ b/database/migrations/2024_10_16_192133_add_confirmation_settings_to_instance_settings_table.php
@@ -0,0 +1,22 @@
+boolean('disable_two_step_confirmation')->default(false);
+ });
+ }
+
+ public function down()
+ {
+ Schema::table('instance_settings', function (Blueprint $table) {
+ $table->dropColumn('disable_two_step_confirmation');
+ });
+ }
+};
diff --git a/database/migrations/2024_10_17_093722_add_soft_delete_to_servers.php b/database/migrations/2024_10_17_093722_add_soft_delete_to_servers.php
new file mode 100644
index 000000000..7a7f28e24
--- /dev/null
+++ b/database/migrations/2024_10_17_093722_add_soft_delete_to_servers.php
@@ -0,0 +1,28 @@
+softDeletes();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('servers', function (Blueprint $table) {
+ $table->dropSoftDeletes();
+ });
+ }
+};
diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php
index be5083108..6e66c64f4 100644
--- a/database/seeders/DatabaseSeeder.php
+++ b/database/seeders/DatabaseSeeder.php
@@ -26,6 +26,8 @@ public function run(): void
S3StorageSeeder::class,
StandalonePostgresqlSeeder::class,
OauthSettingSeeder::class,
+ DisableTwoStepConfirmationSeeder::class,
+ SentinelSeeder::class,
]);
}
}
diff --git a/database/seeders/DisableTwoStepConfirmationSeeder.php b/database/seeders/DisableTwoStepConfirmationSeeder.php
new file mode 100644
index 000000000..c43bf1b01
--- /dev/null
+++ b/database/seeders/DisableTwoStepConfirmationSeeder.php
@@ -0,0 +1,20 @@
+updateOrInsert(
+ [],
+ ['disable_two_step_confirmation' => true]
+ );
+ }
+}
diff --git a/database/seeders/ProductionSeeder.php b/database/seeders/ProductionSeeder.php
index 206f04d6b..90b9d46ff 100644
--- a/database/seeders/ProductionSeeder.php
+++ b/database/seeders/ProductionSeeder.php
@@ -186,6 +186,7 @@ public function run(): void
$this->call(OauthSettingSeeder::class);
$this->call(PopulateSshKeysDirectorySeeder::class);
+ $this->call(SentinelSeeder::class);
}
}
diff --git a/database/seeders/SentinelSeeder.php b/database/seeders/SentinelSeeder.php
new file mode 100644
index 000000000..117ba6782
--- /dev/null
+++ b/database/seeders/SentinelSeeder.php
@@ -0,0 +1,31 @@
+settings->sentinel_token)->isEmpty()) {
+ $server->settings->generateSentinelToken();
+ }
+ if (str($server->settings->sentinel_custom_url)->isEmpty()) {
+ $url = $server->settings->generateSentinelUrl();
+ if (str($url)->isEmpty()) {
+ $server->settings->is_sentinel_enabled = false;
+ $server->settings->save();
+ }
+ }
+ } catch (\Throwable $e) {
+ loggy("Error: {$e->getMessage()}\n");
+ }
+ }
+ });
+ }
+}
diff --git a/docker/coolify-realtime/package.json b/docker/coolify-realtime/package.json
index 90d4f77db..146e6b90a 100644
--- a/docker/coolify-realtime/package.json
+++ b/docker/coolify-realtime/package.json
@@ -4,7 +4,7 @@
"dependencies": {
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.5.0",
- "cookie": "^0.6.0",
+ "cookie": "^0.7.0",
"axios": "1.7.5",
"dotenv": "^16.4.5",
"node-pty": "^1.0.0",
diff --git a/docker/dev/Dockerfile b/docker/dev/Dockerfile
index 63832dc36..d2381f764 100644
--- a/docker/dev/Dockerfile
+++ b/docker/dev/Dockerfile
@@ -5,34 +5,38 @@ ARG TARGETPLATFORM
ARG CLOUDFLARED_VERSION=2024.4.1
ARG POSTGRES_VERSION=15
-RUN apt-get update
-# Postgres version requirements
-RUN apt install dirmngr ca-certificates software-properties-common gnupg gnupg2 apt-transport-https curl -y
-RUN curl -fSsL https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /usr/share/keyrings/postgresql.gpg > /dev/null
-RUN echo deb [arch=amd64,arm64,ppc64el signed-by=/usr/share/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt/ jammy-pgdg main | tee -a /etc/apt/sources.list.d/postgresql.list
+# Use build arguments for caching
+ARG BUILDTIME_DEPS="dirmngr ca-certificates software-properties-common gnupg gnupg2 apt-transport-https curl"
+ARG RUNTIME_DEPS="postgresql-client-$POSTGRES_VERSION php8.2-pgsql openssh-client git git-lfs jq lsof"
-RUN apt-get update
-RUN apt-get install postgresql-client-$POSTGRES_VERSION -y
+# Install dependencies
+RUN --mount=type=cache,target=/var/cache/apt \
+ apt-get update && \
+ apt-get install -y $BUILDTIME_DEPS && \
+ curl -fSsL https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /usr/share/keyrings/postgresql.gpg > /dev/null && \
+ echo deb [arch=amd64,arm64,ppc64el signed-by=/usr/share/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt/ jammy-pgdg main | tee -a /etc/apt/sources.list.d/postgresql.list && \
+ apt-get update && \
+ apt-get install -y $RUNTIME_DEPS && \
+ apt-get -y autoremove && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/*
-# Coolify requirements
-RUN apt-get install -y php8.2-pgsql openssh-client git git-lfs jq lsof
-RUN apt-get -y autoremove && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/*
COPY --chmod=755 docker/dev/etc/s6-overlay/ /etc/s6-overlay/
COPY docker/dev/nginx.conf /etc/nginx/conf.d/custom.conf
-RUN echo "alias ll='ls -al'" >>/etc/bash.bashrc
-RUN echo "alias a='php artisan'" >>/etc/bash.bashrc
+RUN echo "alias ll='ls -al'" >>/etc/bash.bashrc && \
+ echo "alias a='php artisan'" >>/etc/bash.bashrc
RUN mkdir -p /usr/local/bin
-RUN /bin/bash -c "if [[ ${TARGETPLATFORM} == 'linux/amd64' ]]; then \
+RUN --mount=type=cache,target=/root/.cache \
+ /bin/bash -c "if [[ ${TARGETPLATFORM} == 'linux/amd64' ]]; then \
echo 'amd64' && \
curl -sSL https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared \
;fi"
-RUN /bin/bash -c "if [[ ${TARGETPLATFORM} == 'linux/arm64' ]]; then \
+RUN --mount=type=cache,target=/root/.cache \
+ /bin/bash -c "if [[ ${TARGETPLATFORM} == 'linux/arm64' ]]; then \
echo 'arm64' && \
curl -L https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-arm64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared \
;fi"
diff --git a/lang/ar.json b/lang/ar.json
index c5ec96c8d..4b9afbe99 100644
--- a/lang/ar.json
+++ b/lang/ar.json
@@ -26,5 +26,12 @@
"input.code": "الرمز لمرة واحدة",
"input.recovery_code": "رمز الاسترداد",
"button.save": "حفظ",
- "repository.url": "أمثلة للمستودعات العامة، استخدم https://.... للمستودعات الخاصة، استخدم git@....
سيتم تحديد الفرع main لـ https://github.com/coollabsio/coolify-examples سيتم تحديد الفرع nodejs-fastify لـ https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify سيتم تحديد الفرع main لـ https://gitea.com/sedlav/expressjs.git سيتم تحديد الفرع main لـ https://gitlab.com/andrasbacsai/nodejs-example.git."
+ "repository.url": "أمثلة للمستودعات العامة، استخدم https://.... للمستودعات الخاصة، استخدم git@....
سيتم تحديد الفرع main لـ https://github.com/coollabsio/coolify-examples سيتم تحديد الفرع nodejs-fastify لـ https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify سيتم تحديد الفرع main لـ https://gitea.com/sedlav/expressjs.git سيتم تحديد الفرع main لـ https://gitlab.com/andrasbacsai/nodejs-example.git.",
+ "service.stop": "سيتم إيقاف هذه الخدمة.",
+ "resource.docker_cleanup": "قم بتشغيل Docker Cleanup (قم بإزالة الصور غير المستخدمة وذاكرة التخزين المؤقت للمنشئ).",
+ "resource.non_persistent": "سيتم حذف جميع البيانات غير الدائمة.",
+ "resource.delete_volumes": "حذف جميع المجلدات والملفات المرتبطة بهذا المورد بشكل دائم.",
+ "resource.delete_connected_networks": "حذف جميع الشبكات غير المحددة مسبقًا والمرتبطة بهذا المورد بشكل دائم.",
+ "resource.delete_configurations": "حذف جميع ملفات التعريف من الخادم بشكل دائم.",
+ "database.delete_backups_locally": "حذف كافة النسخ الاحتياطية نهائيًا من التخزين المحلي."
}
diff --git a/lang/en.json b/lang/en.json
index fa69c7035..5ea474b02 100644
--- a/lang/en.json
+++ b/lang/en.json
@@ -33,5 +33,6 @@
"resource.delete_volumes": "Permanently delete all volumes associated with this resource.",
"resource.delete_connected_networks": "Permanently delete all non-predefined networks associated with this resource.",
"resource.delete_configurations": "Permanently delete all configuration files from the server.",
- "database.delete_backups_locally": "All backups will be permanently deleted from local storage."
+ "database.delete_backups_locally": "All backups will be permanently deleted from local storage.",
+ "warning.sslipdomain": "Your configuration is saved, but sslip domain with https is NOT recommended, because Let's Encrypt servers with this public domain are rate limited (SSL certificate validation will fail).
Use your own domain instead."
}
diff --git a/lang/ro.json b/lang/ro.json
new file mode 100644
index 000000000..db1aa85db
--- /dev/null
+++ b/lang/ro.json
@@ -0,0 +1,37 @@
+{
+ "auth.login": "Autentificare",
+ "auth.login.azure": "Autentificare prin Microsoft",
+ "auth.login.bitbucket": "Autentificare prin Bitbucket",
+ "auth.login.github": "Autentificare prin GitHub",
+ "auth.login.gitlab": "Autentificare prin Gitlab",
+ "auth.login.google": "Autentificare prin Google",
+ "auth.already_registered": "Sunteți deja înregistrat?",
+ "auth.confirm_password": "Confirmați parola",
+ "auth.forgot_password": "Ați uitat parola",
+ "auth.forgot_password_send_email": "Trimiteți e-mail-ul pentru resetarea parolei",
+ "auth.register_now": "Înregistrare",
+ "auth.logout": "Deconectare",
+ "auth.register": "Înregistrare",
+ "auth.registration_disabled": "Înregistrarea este dezactivată. Vă rugăm să contactați administratorul site-ului.",
+ "auth.reset_password": "Resetare parolă",
+ "auth.failed": "Autentificare nereușită. Vă rugăm să verificați datele introduse.",
+ "auth.failed.callback": "A apărut o eroare în timpul autentificării cu furnizorul extern.",
+ "auth.failed.password": "Parola furnizată este incorectă.",
+ "auth.failed.email": "Nu putem găsi un utilizator cu această adresă de e-mail.",
+ "auth.throttle": "Prea multe încercări de autentificare. Vă rugăm să încercați din nou în :seconds secunde.",
+ "input.name": "Nume",
+ "input.email": "E-mail",
+ "input.password": "Parolă",
+ "input.password.again": "Repetați parola",
+ "input.code": "Cod de unică folosință",
+ "input.recovery_code": "Cod de recuperare",
+ "button.save": "Salvare",
+ "repository.url": "Exemple Pentru depozite publice, utilizați https://.... Pentru depozite private, utilizați git@....
https://github.com/coollabsio/coolify-examples va fi selectată ramura main https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify va fi selectată ramura nodejs-fastify. https://gitea.com/sedlav/expressjs.git va fi selectată ramura main. https://gitlab.com/andrasbacsai/nodejs-example.git va fi selectată ramura main.",
+ "service.stop": "Acest serviciu va fi oprit.",
+ "resource.docker_cleanup": "Executați curățarea Docker (eliminați imaginile neutilizate și memoria cache a constructorului).",
+ "resource.non_persistent": "Toate datele nepersistente vor fi șterse.",
+ "resource.delete_volumes": "Ștergeți definitiv toate volumele asociate cu această resursă.",
+ "resource.delete_connected_networks": "Ștergeți definitiv toate rețelele non-predefinite asociate cu această resursă.",
+ "resource.delete_configurations": "Ștergeți definitiv toate fișierele de configurare de pe server.",
+ "database.delete_backups_locally": "Toate copiile de rezervă vor fi șterse definitiv din stocarea locală."
+}
diff --git a/openapi.yaml b/openapi.yaml
index 91d5c1443..d2616e9c6 100644
--- a/openapi.yaml
+++ b/openapi.yaml
@@ -98,6 +98,10 @@ paths:
is_static:
type: boolean
description: 'The flag to indicate if the application is static.'
+ static_image:
+ type: string
+ enum: ['nginx:alpine']
+ description: 'The static image.'
install_command:
type: string
description: 'The install command.'
@@ -323,6 +327,10 @@ paths:
is_static:
type: boolean
description: 'The flag to indicate if the application is static.'
+ static_image:
+ type: string
+ enum: ['nginx:alpine']
+ description: 'The static image.'
install_command:
type: string
description: 'The install command.'
@@ -548,6 +556,10 @@ paths:
is_static:
type: boolean
description: 'The flag to indicate if the application is static.'
+ static_image:
+ type: string
+ enum: ['nginx:alpine']
+ description: 'The static image.'
install_command:
type: string
description: 'The install command.'
@@ -3093,7 +3105,7 @@ paths:
security:
-
bearerAuth: []
- /healthcheck:
+ /health:
get:
summary: Healthcheck
description: 'Healthcheck endpoint.'
@@ -4959,7 +4971,7 @@ components:
type: boolean
is_reachable:
type: boolean
- is_server_api_enabled:
+ is_sentinel_enabled:
type: boolean
is_swarm_manager:
type: boolean
@@ -4981,11 +4993,11 @@ components:
type: string
logdrain_newrelic_license_key:
type: string
- metrics_history_days:
+ sentinel_metrics_history_days:
type: integer
- metrics_refresh_rate_seconds:
+ sentinel_metrics_refresh_rate_seconds:
type: integer
- metrics_token:
+ sentinel_token:
type: string
docker_cleanup_frequency:
type: string
diff --git a/public/svgs/affine.svg b/public/svgs/affine.svg
new file mode 100644
index 000000000..d8063e920
--- /dev/null
+++ b/public/svgs/affine.svg
@@ -0,0 +1,88 @@
+
diff --git a/public/svgs/calcom.svg b/public/svgs/calcom.svg
new file mode 100644
index 000000000..446b16655
--- /dev/null
+++ b/public/svgs/calcom.svg
@@ -0,0 +1,9 @@
+
\ No newline at end of file
diff --git a/public/svgs/cloudbeaver.svg b/public/svgs/cloudbeaver.svg
new file mode 100644
index 000000000..4a7634766
--- /dev/null
+++ b/public/svgs/cloudbeaver.svg
@@ -0,0 +1,7 @@
+
diff --git a/public/svgs/cryptgeon.png b/public/svgs/cryptgeon.png
new file mode 100644
index 000000000..be121cfd0
Binary files /dev/null and b/public/svgs/cryptgeon.png differ
diff --git a/public/svgs/dify.png b/public/svgs/dify.png
new file mode 100644
index 000000000..326acf789
Binary files /dev/null and b/public/svgs/dify.png differ
diff --git a/public/svgs/edgedb.svg b/public/svgs/edgedb.svg
new file mode 100644
index 000000000..a906f7f7e
--- /dev/null
+++ b/public/svgs/edgedb.svg
@@ -0,0 +1,3 @@
+
diff --git a/public/svgs/flowise.png b/public/svgs/flowise.png
new file mode 100644
index 000000000..6b0be0d2a
Binary files /dev/null and b/public/svgs/flowise.png differ
diff --git a/public/svgs/freshrss.png b/public/svgs/freshrss.png
new file mode 100644
index 000000000..d1a75118f
Binary files /dev/null and b/public/svgs/freshrss.png differ
diff --git a/public/svgs/heyform.svg b/public/svgs/heyform.svg
new file mode 100644
index 000000000..ff29ca654
--- /dev/null
+++ b/public/svgs/heyform.svg
@@ -0,0 +1,5 @@
+
diff --git a/public/svgs/homebox.svg b/public/svgs/homebox.svg
new file mode 100644
index 000000000..08670bbb9
--- /dev/null
+++ b/public/svgs/homebox.svg
@@ -0,0 +1,11 @@
+
diff --git a/public/svgs/immich.svg b/public/svgs/immich.svg
new file mode 100644
index 000000000..9d844a772
--- /dev/null
+++ b/public/svgs/immich.svg
@@ -0,0 +1,66 @@
+
+
+
diff --git a/public/svgs/kimai.svg b/public/svgs/kimai.svg
new file mode 100644
index 000000000..35b146972
--- /dev/null
+++ b/public/svgs/kimai.svg
@@ -0,0 +1,67 @@
+
\ No newline at end of file
diff --git a/public/svgs/libretranslate.svg b/public/svgs/libretranslate.svg
new file mode 100644
index 000000000..103d47d60
--- /dev/null
+++ b/public/svgs/libretranslate.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/svgs/litequeen.svg b/public/svgs/litequeen.svg
new file mode 100644
index 000000000..aa0b8e038
--- /dev/null
+++ b/public/svgs/litequeen.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/svgs/mindsdb.svg b/public/svgs/mindsdb.svg
new file mode 100644
index 000000000..53799dd1c
--- /dev/null
+++ b/public/svgs/mindsdb.svg
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/svgs/mosquitto.png b/public/svgs/mosquitto.png
new file mode 100644
index 000000000..eb287a7cd
Binary files /dev/null and b/public/svgs/mosquitto.png differ
diff --git a/public/svgs/ntfy.svg b/public/svgs/ntfy.svg
new file mode 100644
index 000000000..9e5b5136f
--- /dev/null
+++ b/public/svgs/ntfy.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/svgs/osticket.png b/public/svgs/osticket.png
new file mode 100644
index 000000000..65885b71b
Binary files /dev/null and b/public/svgs/osticket.png differ
diff --git a/public/svgs/owncloud.svg b/public/svgs/owncloud.svg
new file mode 100644
index 000000000..83631e3f5
--- /dev/null
+++ b/public/svgs/owncloud.svg
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/svgs/peppermint.png b/public/svgs/peppermint.png
new file mode 100644
index 000000000..38db83de0
Binary files /dev/null and b/public/svgs/peppermint.png differ
diff --git a/public/svgs/qbittorrent.svg b/public/svgs/qbittorrent.svg
new file mode 100644
index 000000000..69d8cf62a
--- /dev/null
+++ b/public/svgs/qbittorrent.svg
@@ -0,0 +1,16 @@
+
+
+ qbittorrent-new-light
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/svgs/traccar.png b/public/svgs/traccar.png
new file mode 100644
index 000000000..c747aea05
Binary files /dev/null and b/public/svgs/traccar.png differ
diff --git a/public/svgs/transmission.svg b/public/svgs/transmission.svg
new file mode 100644
index 000000000..9a11f77f4
--- /dev/null
+++ b/public/svgs/transmission.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/svgs/unsend.svg b/public/svgs/unsend.svg
new file mode 100644
index 000000000..f5ff6fabc
--- /dev/null
+++ b/public/svgs/unsend.svg
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/svgs/vvveb.svg b/public/svgs/vvveb.svg
new file mode 100644
index 000000000..2b66b3087
--- /dev/null
+++ b/public/svgs/vvveb.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/svgs/zep.png b/public/svgs/zep.png
new file mode 100644
index 000000000..7d51b32dc
Binary files /dev/null and b/public/svgs/zep.png differ
diff --git a/public/svgs/zipline.png b/public/svgs/zipline.png
new file mode 100644
index 000000000..2b8f6972d
Binary files /dev/null and b/public/svgs/zipline.png differ
diff --git a/resources/views/components/forms/checkbox.blade.php b/resources/views/components/forms/checkbox.blade.php
index 439fc4ad2..fed6ad77f 100644
--- a/resources/views/components/forms/checkbox.blade.php
+++ b/resources/views/components/forms/checkbox.blade.php
@@ -14,7 +14,10 @@
'w-full' => $fullWidth,
])>
@if (!$hideLabel)
-