diff --git a/README.md b/README.md
index 56bee004e..34c40acf5 100644
--- a/README.md
+++ b/README.md
@@ -5,13 +5,13 @@
Coolify is an open-source & self-hostable alternative to Heroku / Netlify / Vercel / etc.
-It helps you to manage your servers, applications, databases on your own hardware, all you need is SSH connection. You can manage VPS, Bare Metal, Raspberry PI's anything.
+It helps you manage your servers, applications, and databases on your own hardware; you only need an SSH connection. You can manage VPS, Bare Metal, Raspberry PIs, and anything else.
-Imagine if you could have the ease of a cloud but with your own servers. That is **Coolify**.
+Imagine having the ease of a cloud but with your own servers. That is **Coolify**.
-No vendor lock-in, which means that all the configuration for your applications/databases/etc are saved to your server. So if you decide to stop using Coolify (oh nooo), you could still manage your running resources. You just lose the automations and all the magic. 🪄️
+No vendor lock-in, which means that all the configurations for your applications/databases/etc are saved to your server. So, if you decide to stop using Coolify (oh nooo), you could still manage your running resources. You lose the automations and all the magic. 🪄️
-For more information, take a look at our landing page [here](https://coolify.io).
+For more information, take a look at our landing page at [coolify.io](https://coolify.io).
# Installation
@@ -22,36 +22,40 @@ You can find the installation script source [here](./scripts/install.sh).
# Support
-Contact us [here](https://coolify.io/docs/contact).
+Contact us at [coolify.io/docs/contact](https://coolify.io/docs/contact).
# Donations
-To stay completely free, open-source, no feature behind paywall and evolve the project, we need your help. If you like Coolify, please consider donating to help us fund the future development of the project.
+To stay completely free and open-source, with no feature behind the paywall and evolve the project, we need your help. If you like Coolify, please consider donating to help us fund the project's future development.
-https://coolify.io/sponsorships
+[coolify.io/sponsorships](https://coolify.io/sponsorships)
Thank you so much!
Special thanks to our biggest sponsors!
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
## Github Sponsors ($40+)
-
-
-
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
@@ -83,9 +87,9 @@ Special thanks to our biggest sponsors!
# Cloud
-If you do not want to self-host Coolify, there is a paid cloud version available: https://app.coolify.io
+If you do not want to self-host Coolify, there is a paid cloud version available: [app.coolify.io](https://app.coolify.io)
-For more information & pricing, take a look at our landing page [here](https://coolify.io).
+For more information & pricing, take a look at our landing page [coolify.io](https://coolify.io).
## Why should I use the Cloud version?
The recommended way to use Coolify is to have one server for Coolify and one (or more) for the resources you are deploying. A server is around 4-5$/month.
@@ -109,7 +113,7 @@ By subscribing to the cloud version, you get the Coolify server for the same pri
-
+
diff --git a/app/Actions/Database/StopDatabaseProxy.php b/app/Actions/Database/StopDatabaseProxy.php
index 984225435..1b262c898 100644
--- a/app/Actions/Database/StopDatabaseProxy.php
+++ b/app/Actions/Database/StopDatabaseProxy.php
@@ -2,6 +2,7 @@
namespace App\Actions\Database;
+use App\Events\DatabaseStatusChanged;
use App\Models\ServiceDatabase;
use App\Models\StandaloneClickhouse;
use App\Models\StandaloneDragonfly;
@@ -28,5 +29,6 @@ class StopDatabaseProxy
instant_remote_process(["docker rm -f {$uuid}-proxy"], $server);
$database->is_public = false;
$database->save();
+ DatabaseStatusChanged::dispatch();
}
}
diff --git a/app/Actions/Server/StartSentinel.php b/app/Actions/Server/StartSentinel.php
index eea429c79..b79bc8f67 100644
--- a/app/Actions/Server/StartSentinel.php
+++ b/app/Actions/Server/StartSentinel.php
@@ -12,12 +12,15 @@ class StartSentinel
public function handle(Server $server, $version = 'latest', bool $restart = false)
{
if ($restart) {
- instant_remote_process(['docker rm -f coolify-sentinel'], $server, false);
+ StopSentinel::run($server);
}
+ $metrics_history = $server->settings->metrics_history_days;
+ $refresh_rate = $server->settings->metrics_refresh_rate_seconds;
+ $token = $server->settings->metrics_token;
instant_remote_process([
- "docker run --rm --pull always -d -e \"SCHEDULER=true\" --name coolify-sentinel -v /var/run/docker.sock:/var/run/docker.sock -v /data/coolify/metrics:/app/metrics -v /data/coolify/logs:/app/logs --pid host --health-cmd \"curl --fail http://127.0.0.1:8888/api/health || exit 1\" --health-interval 10s --health-retries 3 ghcr.io/coollabsio/sentinel:$version",
+ "docker run --rm --pull always -d -e \"TOKEN={$token}\" -e \"SCHEDULER=true\" -e \"METRICS_HISTORY={$metrics_history}\" -e \"REFRESH_RATE={$refresh_rate}\" --name coolify-sentinel -v /var/run/docker.sock:/var/run/docker.sock -v /data/coolify/metrics:/app/metrics -v /data/coolify/logs:/app/logs --pid host --health-cmd \"curl --fail http://127.0.0.1:8888/api/health || exit 1\" --health-interval 10s --health-retries 3 ghcr.io/coollabsio/sentinel:$version",
'chown -R 9999:root /data/coolify/metrics /data/coolify/logs',
'chmod -R 700 /data/coolify/metrics /data/coolify/logs',
- ], $server, false);
+ ], $server, true);
}
}
diff --git a/app/Actions/Server/StopSentinel.php b/app/Actions/Server/StopSentinel.php
new file mode 100644
index 000000000..21ffca3bd
--- /dev/null
+++ b/app/Actions/Server/StopSentinel.php
@@ -0,0 +1,16 @@
+all_servers->where('settings.is_usable', true)->where('settings.is_reachable', true)->where('ip', '!=', '1.2.3.4');
foreach ($servers as $server) {
- if (config('coolify.is_sentinel_enabled')) {
+ if ($server->isSentinelEnabled()) {
$schedule->job(new PullSentinelImageJob($server))->everyFiveMinutes()->onOneServer();
}
$schedule->job(new PullHelperImageJob($server))->everyFiveMinutes()->onOneServer();
diff --git a/app/Http/Controllers/Webhook/Bitbucket.php b/app/Http/Controllers/Webhook/Bitbucket.php
index b9035b755..059438ff4 100644
--- a/app/Http/Controllers/Webhook/Bitbucket.php
+++ b/app/Http/Controllers/Webhook/Bitbucket.php
@@ -130,12 +130,23 @@ class Bitbucket extends Controller
$deployment_uuid = new Cuid2(7);
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if (! $found) {
- ApplicationPreview::create([
- 'git_type' => 'bitbucket',
- 'application_id' => $application->id,
- 'pull_request_id' => $pull_request_id,
- 'pull_request_html_url' => $pull_request_html_url,
- ]);
+ if ($application->build_pack === 'dockercompose') {
+ $pr_app = ApplicationPreview::create([
+ 'git_type' => 'bitbucket',
+ 'application_id' => $application->id,
+ 'pull_request_id' => $pull_request_id,
+ 'pull_request_html_url' => $pull_request_html_url,
+ 'docker_compose_domains' => $application->docker_compose_domains,
+ ]);
+ $pr_app->generate_preview_fqdn_compose();
+ } else {
+ ApplicationPreview::create([
+ 'git_type' => 'bitbucket',
+ 'application_id' => $application->id,
+ 'pull_request_id' => $pull_request_id,
+ 'pull_request_html_url' => $pull_request_html_url,
+ ]);
+ }
}
queue_application_deployment(
application: $application,
diff --git a/app/Http/Controllers/Webhook/Gitea.php b/app/Http/Controllers/Webhook/Gitea.php
index 388481949..e6d91efd6 100644
--- a/app/Http/Controllers/Webhook/Gitea.php
+++ b/app/Http/Controllers/Webhook/Gitea.php
@@ -165,12 +165,24 @@ class Gitea extends Controller
$deployment_uuid = new Cuid2(7);
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if (! $found) {
- ApplicationPreview::create([
- 'git_type' => 'gitea',
- 'application_id' => $application->id,
- 'pull_request_id' => $pull_request_id,
- 'pull_request_html_url' => $pull_request_html_url,
- ]);
+ if ($application->build_pack === 'dockercompose') {
+ $pr_app = ApplicationPreview::create([
+ 'git_type' => 'gitea',
+ 'application_id' => $application->id,
+ 'pull_request_id' => $pull_request_id,
+ 'pull_request_html_url' => $pull_request_html_url,
+ 'docker_compose_domains' => $application->docker_compose_domains,
+ ]);
+ $pr_app->generate_preview_fqdn_compose();
+ } else {
+ ApplicationPreview::create([
+ 'git_type' => 'gitea',
+ 'application_id' => $application->id,
+ 'pull_request_id' => $pull_request_id,
+ 'pull_request_html_url' => $pull_request_html_url,
+ ]);
+ }
+
}
queue_application_deployment(
application: $application,
diff --git a/app/Http/Controllers/Webhook/Github.php b/app/Http/Controllers/Webhook/Github.php
index 403438193..a030e31ca 100644
--- a/app/Http/Controllers/Webhook/Github.php
+++ b/app/Http/Controllers/Webhook/Github.php
@@ -170,12 +170,23 @@ class Github extends Controller
$deployment_uuid = new Cuid2(7);
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if (! $found) {
- ApplicationPreview::create([
- 'git_type' => 'github',
- 'application_id' => $application->id,
- 'pull_request_id' => $pull_request_id,
- 'pull_request_html_url' => $pull_request_html_url,
- ]);
+ if ($application->build_pack === 'dockercompose') {
+ $pr_app = ApplicationPreview::create([
+ 'git_type' => 'github',
+ 'application_id' => $application->id,
+ 'pull_request_id' => $pull_request_id,
+ 'pull_request_html_url' => $pull_request_html_url,
+ 'docker_compose_domains' => $application->docker_compose_domains,
+ ]);
+ $pr_app->generate_preview_fqdn_compose();
+ } else {
+ ApplicationPreview::create([
+ 'git_type' => 'github',
+ 'application_id' => $application->id,
+ 'pull_request_id' => $pull_request_id,
+ 'pull_request_html_url' => $pull_request_html_url,
+ ]);
+ }
}
queue_application_deployment(
application: $application,
diff --git a/app/Http/Controllers/Webhook/Gitlab.php b/app/Http/Controllers/Webhook/Gitlab.php
index a3d7712eb..f6e6cf7e7 100644
--- a/app/Http/Controllers/Webhook/Gitlab.php
+++ b/app/Http/Controllers/Webhook/Gitlab.php
@@ -180,12 +180,23 @@ class Gitlab extends Controller
$deployment_uuid = new Cuid2(7);
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if (! $found) {
- ApplicationPreview::create([
- 'git_type' => 'gitlab',
- 'application_id' => $application->id,
- 'pull_request_id' => $pull_request_id,
- 'pull_request_html_url' => $pull_request_html_url,
- ]);
+ if ($application->build_pack === 'dockercompose') {
+ $pr_app = ApplicationPreview::create([
+ 'git_type' => 'gitlab',
+ 'application_id' => $application->id,
+ 'pull_request_id' => $pull_request_id,
+ 'pull_request_html_url' => $pull_request_html_url,
+ 'docker_compose_domains' => $application->docker_compose_domains,
+ ]);
+ $pr_app->generate_preview_fqdn_compose();
+ } else {
+ ApplicationPreview::create([
+ 'git_type' => 'gitlab',
+ 'application_id' => $application->id,
+ 'pull_request_id' => $pull_request_id,
+ 'pull_request_html_url' => $pull_request_html_url,
+ ]);
+ }
}
queue_application_deployment(
application: $application,
diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php
index 72d8c0ad1..65e9f2e93 100644
--- a/app/Jobs/ApplicationDeploymentJob.php
+++ b/app/Jobs/ApplicationDeploymentJob.php
@@ -9,6 +9,7 @@ use App\Events\ApplicationStatusChanged;
use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
use App\Models\ApplicationPreview;
+use App\Models\EnvironmentVariable;
use App\Models\GithubApp;
use App\Models\GitlabApp;
use App\Models\Server;
@@ -827,6 +828,9 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_BRANCH')->isEmpty()) {
$envs->push("COOLIFY_BRANCH={$local_branch}");
}
+ if ($this->application->environment_variables_preview->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) {
+ $envs->push("COOLIFY_CONTAINER_NAME={$this->container_name}");
+ }
foreach ($sorted_environment_variables_preview as $env) {
$real_value = $env->real_value;
if ($env->version === '4.0.0-beta.239') {
@@ -868,6 +872,9 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
if ($this->application->environment_variables->where('key', 'COOLIFY_BRANCH')->isEmpty()) {
$envs->push("COOLIFY_BRANCH={$local_branch}");
}
+ if ($this->application->environment_variables->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) {
+ $envs->push("COOLIFY_CONTAINER_NAME={$this->container_name}");
+ }
foreach ($sorted_environment_variables as $env) {
$real_value = $env->real_value;
if ($env->version === '4.0.0-beta.239') {
@@ -877,7 +884,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$real_value = '\''.$real_value.'\'';
} else {
$real_value = escapeEnvVariables($env->real_value);
- ray($real_value);
}
}
$envs->push($env->key.'='.$real_value);
@@ -946,9 +952,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
}
}
- private function framework_based_notification()
+ private function laravel_finetunes()
{
- // Laravel old env variables
if ($this->pull_request_id === 0) {
$nixpacks_php_fallback_path = $this->application->environment_variables->where('key', 'NIXPACKS_PHP_FALLBACK_PATH')->first();
$nixpacks_php_root_dir = $this->application->environment_variables->where('key', 'NIXPACKS_PHP_ROOT_DIR')->first();
@@ -956,9 +961,22 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$nixpacks_php_fallback_path = $this->application->environment_variables_preview->where('key', 'NIXPACKS_PHP_FALLBACK_PATH')->first();
$nixpacks_php_root_dir = $this->application->environment_variables_preview->where('key', 'NIXPACKS_PHP_ROOT_DIR')->first();
}
- if ($nixpacks_php_fallback_path?->value === '/index.php' && $nixpacks_php_root_dir?->value === '/app/public' && $this->newVersionIsHealthy === false) {
- $this->application_deployment_queue->addLogEntry('There was a change in how Laravel is deployed. Please update your environment variables to match the new deployment method. More details here: https://coolify.io/docs/resources/laravel', 'stderr');
+ if (! $nixpacks_php_fallback_path) {
+ $nixpacks_php_fallback_path = new EnvironmentVariable();
+ $nixpacks_php_fallback_path->key = 'NIXPACKS_PHP_FALLBACK_PATH';
+ $nixpacks_php_fallback_path->value = '/index.php';
+ $nixpacks_php_fallback_path->application_id = $this->application->id;
+ $nixpacks_php_fallback_path->save();
}
+ if (! $nixpacks_php_root_dir) {
+ $nixpacks_php_root_dir = new EnvironmentVariable();
+ $nixpacks_php_root_dir->key = 'NIXPACKS_PHP_ROOT_DIR';
+ $nixpacks_php_root_dir->value = '/app/public';
+ $nixpacks_php_root_dir->application_id = $this->application->id;
+ $nixpacks_php_root_dir->save();
+ }
+
+ return [$nixpacks_php_fallback_path, $nixpacks_php_root_dir];
}
private function rolling_update()
@@ -1005,7 +1023,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->application_deployment_queue->addLogEntry('Rolling update completed.');
}
}
- $this->framework_based_notification();
}
private function health_check()
@@ -1366,17 +1383,20 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
throw new RuntimeException('Nixpacks failed to detect the application type. Please check the documentation of Nixpacks: https://nixpacks.com/docs/providers');
}
}
+
if ($this->saved_outputs->get('nixpacks_plan')) {
$this->nixpacks_plan = $this->saved_outputs->get('nixpacks_plan');
if ($this->nixpacks_plan) {
$this->application_deployment_queue->addLogEntry("Found application type: {$this->nixpacks_type}.");
$this->application_deployment_queue->addLogEntry("If you need further customization, please check the documentation of Nixpacks: https://nixpacks.com/docs/providers/{$this->nixpacks_type}");
$parsed = Toml::Parse($this->nixpacks_plan);
+
// Do any modifications here
$this->generate_env_variables();
$merged_envs = $this->env_args->merge(collect(data_get($parsed, 'variables', [])));
$aptPkgs = data_get($parsed, 'phases.setup.aptPkgs', []);
if (count($aptPkgs) === 0) {
+ $aptPkgs = ['curl', 'wget'];
data_set($parsed, 'phases.setup.aptPkgs', ['curl', 'wget']);
} else {
if (! in_array('curl', $aptPkgs)) {
@@ -1388,6 +1408,12 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
data_set($parsed, 'phases.setup.aptPkgs', $aptPkgs);
}
data_set($parsed, 'variables', $merged_envs->toArray());
+ $is_laravel = data_get($parsed, 'variables.IS_LARAVEL', false);
+ if ($is_laravel) {
+ $variables = $this->laravel_finetunes();
+ data_set($parsed, 'variables.NIXPACKS_PHP_FALLBACK_PATH', $variables[0]->value);
+ data_set($parsed, 'variables.NIXPACKS_PHP_ROOT_DIR', $variables[1]->value);
+ }
$this->nixpacks_plan = json_encode($parsed, JSON_PRETTY_PRINT);
$this->application_deployment_queue->addLogEntry("Final Nixpacks plan: {$this->nixpacks_plan}", hidden: true);
}
@@ -1841,13 +1867,25 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->nixpacks_plan}' | base64 -d | tee /artifacts/thegameplan.json > /dev/null"), 'hidden' => true]);
if ($this->force_rebuild) {
$this->execute_remote_command([
- executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->build_image_name} {$this->workdir}"), 'hidden' => true,
+ executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->build_image_name} {$this->workdir} -o {$this->workdir}"), 'hidden' => true,
]);
+ $build_command = "docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile {$this->build_args} --progress plain -t {$this->build_image_name} {$this->workdir}";
} else {
$this->execute_remote_command([
- executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->build_image_name} {$this->workdir}"), 'hidden' => true,
+ executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->build_image_name} {$this->workdir} -o {$this->workdir}"), 'hidden' => true,
]);
+ $build_command = "docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile {$this->build_args} --progress plain -t {$this->build_image_name} {$this->workdir}";
}
+
+ $base64_build_command = base64_encode($build_command);
+ $this->execute_remote_command(
+ [
+ executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), 'hidden' => true,
+ ],
+ [
+ executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), 'hidden' => true,
+ ]
+ );
$this->execute_remote_command([executeInDocker($this->deployment_uuid, 'rm /artifacts/thegameplan.json'), 'hidden' => true]);
} else {
if ($this->force_rebuild) {
@@ -1866,7 +1904,6 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
]
);
}
-
$dockerfile = base64_encode("FROM {$this->application->static_image}
WORKDIR /usr/share/nginx/html/
LABEL coolify.deploymentId={$this->deployment_uuid}
@@ -1929,13 +1966,24 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->nixpacks_plan}' | base64 -d | tee /artifacts/thegameplan.json > /dev/null"), 'hidden' => true]);
if ($this->force_rebuild) {
$this->execute_remote_command([
- executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->production_image_name} {$this->workdir}"), 'hidden' => true,
+ executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->production_image_name} {$this->workdir} -o {$this->workdir}"), 'hidden' => true,
]);
+ $build_command = "docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}";
} else {
$this->execute_remote_command([
- executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->production_image_name} {$this->workdir}"), 'hidden' => true,
+ executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->production_image_name} {$this->workdir} -o {$this->workdir}"), 'hidden' => true,
]);
+ $build_command = "docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}";
}
+ $base64_build_command = base64_encode($build_command);
+ $this->execute_remote_command(
+ [
+ executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), 'hidden' => true,
+ ],
+ [
+ executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), 'hidden' => true,
+ ]
+ );
$this->execute_remote_command([executeInDocker($this->deployment_uuid, 'rm /artifacts/thegameplan.json'), 'hidden' => true]);
} else {
if ($this->force_rebuild) {
@@ -2184,10 +2232,14 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
ray($code);
if ($code !== 69420) {
// 69420 means failed to push the image to the registry, so we don't need to remove the new version as it is the currently running one
- $this->application_deployment_queue->addLogEntry('Deployment failed. Removing the new version of your application.', 'stderr');
- $this->execute_remote_command(
- ["docker rm -f $this->container_name >/dev/null 2>&1", 'hidden' => true, 'ignore_errors' => true]
- );
+ if ($this->application->settings->is_consistent_container_name_enabled || isset($this->application->settings->custom_internal_name)) {
+ // do not remove already running container
+ } else {
+ $this->application_deployment_queue->addLogEntry('Deployment failed. Removing the new version of your application.', 'stderr');
+ $this->execute_remote_command(
+ ["docker rm -f $this->container_name >/dev/null 2>&1", 'hidden' => true, 'ignore_errors' => true]
+ );
+ }
}
}
}
diff --git a/app/Jobs/DockerCleanupJob.php b/app/Jobs/DockerCleanupJob.php
index 50728ba86..e637fb6d4 100644
--- a/app/Jobs/DockerCleanupJob.php
+++ b/app/Jobs/DockerCleanupJob.php
@@ -60,7 +60,7 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
Log::info('No need to clean up '.$this->server->name);
}
} catch (\Throwable $e) {
- send_internal_notification('DockerCleanupJob failed with: '.$e->getMessage());
+ // send_internal_notification('DockerCleanupJob failed with: '.$e->getMessage());
ray($e->getMessage());
throw $e;
}
diff --git a/app/Jobs/PullSentinelImageJob.php b/app/Jobs/PullSentinelImageJob.php
index 30b36d99f..f8c769382 100644
--- a/app/Jobs/PullSentinelImageJob.php
+++ b/app/Jobs/PullSentinelImageJob.php
@@ -50,7 +50,7 @@ class PullSentinelImageJob implements ShouldBeEncrypted, ShouldQueue
}
ray('Sentinel image is up to date');
} catch (\Throwable $e) {
- send_internal_notification('PullSentinelImageJob failed with: '.$e->getMessage());
+ // send_internal_notification('PullSentinelImageJob failed with: '.$e->getMessage());
ray($e->getMessage());
throw $e;
}
diff --git a/app/Jobs/ServerStatusJob.php b/app/Jobs/ServerStatusJob.php
index c30dadff3..c7321a74c 100644
--- a/app/Jobs/ServerStatusJob.php
+++ b/app/Jobs/ServerStatusJob.php
@@ -46,7 +46,7 @@ class ServerStatusJob implements ShouldBeEncrypted, ShouldQueue
if ($this->server->isFunctional()) {
$this->cleanup(notify: false);
$this->remove_unnecessary_coolify_yaml();
- if (config('coolify.is_sentinel_enabled')) {
+ if ($this->server->isSentinelEnabled()) {
$this->server->checkSentinel();
}
}
diff --git a/app/Livewire/Boarding/Index.php b/app/Livewire/Boarding/Index.php
index b787ed0cc..7acf5ed87 100644
--- a/app/Livewire/Boarding/Index.php
+++ b/app/Livewire/Boarding/Index.php
@@ -12,7 +12,7 @@ use Livewire\Component;
class Index extends Component
{
- protected $listeners = ['serverInstalled' => 'validateServer'];
+ protected $listeners = ['refreshBoardingIndex' => 'validateServer'];
public string $currentState = 'welcome';
diff --git a/app/Livewire/Charts/ServerCpu.php b/app/Livewire/Charts/ServerCpu.php
new file mode 100644
index 000000000..5f3283009
--- /dev/null
+++ b/app/Livewire/Charts/ServerCpu.php
@@ -0,0 +1,59 @@
+poll || $this->interval <= 10) {
+ $this->loadData();
+ if ($this->interval > 10) {
+ $this->poll = false;
+ }
+ }
+ }
+
+ public function loadData()
+ {
+ try {
+ $metrics = $this->server->getCpuMetrics($this->interval);
+ $metrics = collect($metrics)->map(function ($metric) {
+ return [$metric[0], $metric[1]];
+ });
+ $this->dispatch("refreshChartData-{$this->chartId}", [
+ 'seriesData' => $metrics,
+ ]);
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function setInterval()
+ {
+ if ($this->interval <= 10) {
+ $this->poll = true;
+ }
+ $this->loadData();
+ }
+}
diff --git a/app/Livewire/Charts/ServerMemory.php b/app/Livewire/Charts/ServerMemory.php
new file mode 100644
index 000000000..911f267f6
--- /dev/null
+++ b/app/Livewire/Charts/ServerMemory.php
@@ -0,0 +1,59 @@
+poll || $this->interval <= 10) {
+ $this->loadData();
+ if ($this->interval > 10) {
+ $this->poll = false;
+ }
+ }
+ }
+
+ public function loadData()
+ {
+ try {
+ $metrics = $this->server->getMemoryMetrics($this->interval);
+ $metrics = collect($metrics)->map(function ($metric) {
+ return [$metric[0], $metric[1]];
+ });
+ $this->dispatch("refreshChartData-{$this->chartId}", [
+ 'seriesData' => $metrics,
+ ]);
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function setInterval()
+ {
+ if ($this->interval <= 10) {
+ $this->poll = true;
+ }
+ $this->loadData();
+ }
+}
diff --git a/app/Livewire/Project/Application/DeploymentNavbar.php b/app/Livewire/Project/Application/DeploymentNavbar.php
index cbbe98d99..b3e39d23d 100644
--- a/app/Livewire/Project/Application/DeploymentNavbar.php
+++ b/app/Livewire/Project/Application/DeploymentNavbar.php
@@ -54,9 +54,9 @@ class DeploymentNavbar extends Component
public function cancel()
{
+ $kill_command = "docker rm -f {$this->application_deployment_queue->deployment_uuid}";
+ $server_id = $this->application_deployment_queue->server_id ?? $this->application->destination->server_id;
try {
- $kill_command = "docker rm -f {$this->application_deployment_queue->deployment_uuid}";
- $server_id = $this->application_deployment_queue->server_id ?? $this->application->destination->server_id;
$server = Server::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);
@@ -84,6 +84,7 @@ class DeploymentNavbar extends Component
'current_process_id' => null,
'status' => ApplicationDeploymentStatus::CANCELLED_BY_USER->value,
]);
+ next_after_cancel($server);
}
}
}
diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php
index 60cdee48e..06ff7b1de 100644
--- a/app/Livewire/Project/Application/General.php
+++ b/app/Livewire/Project/Application/General.php
@@ -347,7 +347,9 @@ class General extends Component
public function submit($showToaster = true)
{
try {
- $this->set_redirect();
+ if ($this->application->isDirty('redirect')) {
+ $this->set_redirect();
+ }
$this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim();
$this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
diff --git a/app/Livewire/Project/Application/Previews.php b/app/Livewire/Project/Application/Previews.php
index ca911339e..f29cd43ce 100644
--- a/app/Livewire/Project/Application/Previews.php
+++ b/app/Livewire/Project/Application/Previews.php
@@ -131,6 +131,12 @@ class Previews extends Component
}
}
+ public function add_and_deploy(int $pull_request_id, ?string $pull_request_html_url = null)
+ {
+ $this->add($pull_request_id, $pull_request_html_url);
+ $this->deploy($pull_request_id, $pull_request_html_url);
+ }
+
public function deploy(int $pull_request_id, ?string $pull_request_html_url = null)
{
try {
diff --git a/app/Livewire/Project/Database/Heading.php b/app/Livewire/Project/Database/Heading.php
index 61dafa76f..ae88ac12b 100644
--- a/app/Livewire/Project/Database/Heading.php
+++ b/app/Livewire/Project/Database/Heading.php
@@ -12,7 +12,6 @@ use App\Actions\Database\StartPostgresql;
use App\Actions\Database\StartRedis;
use App\Actions\Database\StopDatabase;
use App\Actions\Docker\GetContainersStatus;
-use App\Jobs\ContainerStatusJob;
use Livewire\Component;
class Heading extends Component
diff --git a/app/Livewire/Project/Database/Postgresql/General.php b/app/Livewire/Project/Database/Postgresql/General.php
index 38cac2e5c..1c5d39055 100644
--- a/app/Livewire/Project/Database/Postgresql/General.php
+++ b/app/Livewire/Project/Database/Postgresql/General.php
@@ -25,7 +25,17 @@ class General extends Component
public ?string $db_url_public = null;
- protected $listeners = ['refresh', 'save_init_script', 'delete_init_script'];
+ public function getListeners()
+ {
+ $userId = auth()->user()->id;
+
+ return [
+ "echo-private:user.{$userId},DatabaseStatusChanged" => 'database_stopped',
+ 'refresh',
+ 'save_init_script',
+ 'delete_init_script',
+ ];
+ }
protected $rules = [
'database.name' => 'required',
@@ -69,6 +79,11 @@ class General extends Component
$this->server = data_get($this->database, 'destination.server');
}
+ public function database_stopped()
+ {
+ $this->dispatch('success', 'Database proxy stopped. Database is no longer publicly accessible.');
+ }
+
public function instantSaveAdvanced()
{
try {
diff --git a/app/Livewire/Project/New/PublicGitRepository.php b/app/Livewire/Project/New/PublicGitRepository.php
index 739061f1f..7ac7883dc 100644
--- a/app/Livewire/Project/New/PublicGitRepository.php
+++ b/app/Livewire/Project/New/PublicGitRepository.php
@@ -128,8 +128,8 @@ class PublicGitRepository extends Component
) {
$this->repository_url = $this->repository_url.'.git';
}
- if (str($this->repository_url)->contains('github.com')) {
- $this->repository_url = str($this->repository_url)->before('.git')->value();
+ if (str($this->repository_url)->contains('github.com') && str($this->repository_url)->endsWith('.git')) {
+ $this->repository_url = str($this->repository_url)->beforeLast('.git')->value();
}
} catch (\Throwable $e) {
return handleError($e, $this);
@@ -140,7 +140,6 @@ class PublicGitRepository extends Component
$this->get_branch();
$this->selected_branch = $this->git_branch;
} catch (\Throwable $e) {
- ray($e->getMessage());
if (! $this->branch_found && $this->git_branch == 'main') {
try {
$this->git_branch = 'master';
diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/All.php b/app/Livewire/Project/Shared/EnvironmentVariable/All.php
index 4c06bfe23..d67dae19e 100644
--- a/app/Livewire/Project/Shared/EnvironmentVariable/All.php
+++ b/app/Livewire/Project/Shared/EnvironmentVariable/All.php
@@ -112,7 +112,6 @@ class All extends Component
$this->resource->environment_variables_preview()->whereNotIn('key', array_keys($variables))->delete();
} else {
$variables = parseEnvFormatToArray($this->variables);
- ray($variables, $this->variables);
$this->resource->environment_variables()->whereNotIn('key', array_keys($variables))->delete();
}
foreach ($variables as $key => $variable) {
diff --git a/app/Livewire/Project/Shared/Logs.php b/app/Livewire/Project/Shared/Logs.php
index 008d743ed..e646f8a26 100644
--- a/app/Livewire/Project/Shared/Logs.php
+++ b/app/Livewire/Project/Shared/Logs.php
@@ -64,7 +64,7 @@ class Logs extends Component
return;
$server = data_get($this->resource, 'destination.server');
if ($server->isFunctional()) {
- $this->cpu = $server->getMetrics();
+ $this->cpu = $server->getCpuMetrics();
}
}
diff --git a/app/Livewire/Project/Shared/Metrics.php b/app/Livewire/Project/Shared/Metrics.php
new file mode 100644
index 000000000..d9d7dd3ef
--- /dev/null
+++ b/app/Livewire/Project/Shared/Metrics.php
@@ -0,0 +1,64 @@
+poll || $this->interval <= 10) {
+ $this->loadData();
+ if ($this->interval > 10) {
+ $this->poll = false;
+ }
+ }
+ }
+
+ public function loadData()
+ {
+ try {
+ $metrics = $this->resource->getMetrics($this->interval);
+ $cpuMetrics = collect($metrics)->map(function ($metric) {
+ return [$metric[0], $metric[1]];
+ });
+ $memoryMetrics = collect($metrics)->map(function ($metric) {
+ return [$metric[0], $metric[2]];
+ });
+ $this->dispatch("refreshChartData-{$this->chartId}-cpu", [
+ 'seriesData' => $cpuMetrics,
+ ]);
+ $this->dispatch("refreshChartData-{$this->chartId}-memory", [
+ 'seriesData' => $memoryMetrics,
+ ]);
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function setInterval()
+ {
+ if ($this->interval <= 10) {
+ $this->poll = true;
+ }
+ $this->loadData();
+ }
+
+ public function render()
+ {
+ return view('livewire.project.shared.metrics');
+ }
+}
diff --git a/app/Livewire/Server/ConfigureCloudflareTunnels.php b/app/Livewire/Server/ConfigureCloudflareTunnels.php
index 7d2103e37..f7306a5b5 100644
--- a/app/Livewire/Server/ConfigureCloudflareTunnels.php
+++ b/app/Livewire/Server/ConfigureCloudflareTunnels.php
@@ -21,7 +21,7 @@ class ConfigureCloudflareTunnels extends Component
$server->settings->is_cloudflare_tunnel = true;
$server->settings->save();
$this->dispatch('success', 'Cloudflare Tunnels configured successfully.');
- $this->dispatch('serverInstalled');
+ $this->dispatch('refreshServerShow');
} catch (\Throwable $e) {
return handleError($e, $this);
}
@@ -37,7 +37,7 @@ class ConfigureCloudflareTunnels extends Component
$server->save();
$server->settings->save();
$this->dispatch('success', 'Cloudflare Tunnels configured successfully.');
- $this->dispatch('serverInstalled');
+ $this->dispatch('refreshServerShow');
} catch (\Throwable $e) {
return handleError($e, $this);
}
diff --git a/app/Livewire/Server/Form.php b/app/Livewire/Server/Form.php
index 263ff6367..5616123a5 100644
--- a/app/Livewire/Server/Form.php
+++ b/app/Livewire/Server/Form.php
@@ -2,6 +2,9 @@
namespace App\Livewire\Server;
+use App\Actions\Server\StartSentinel;
+use App\Actions\Server\StopSentinel;
+use App\Jobs\PullSentinelImageJob;
use App\Models\Server;
use Livewire\Component;
@@ -36,7 +39,12 @@ class Form extends Component
'server.settings.is_build_server' => 'required|boolean',
'server.settings.concurrent_builds' => 'required|integer|min:1',
'server.settings.dynamic_timeout' => 'required|integer|min:1',
+ 'server.settings.is_metrics_enabled' => 'required|boolean',
+ 'server.settings.metrics_token' => 'required',
+ 'server.settings.metrics_refresh_rate_seconds' => 'required|integer|min:1',
+ 'server.settings.metrics_history_days' => 'required|integer|min:1',
'wildcard_domain' => 'nullable|url',
+ 'server.settings.is_server_api_enabled' => 'required|boolean',
];
protected $validationAttributes = [
@@ -52,7 +60,11 @@ class Form extends Component
'server.settings.is_build_server' => 'Build Server',
'server.settings.concurrent_builds' => 'Concurrent Builds',
'server.settings.dynamic_timeout' => 'Dynamic Timeout',
-
+ 'server.settings.is_metrics_enabled' => 'Metrics',
+ 'server.settings.metrics_token' => 'Metrics Token',
+ 'server.settings.metrics_refresh_rate_seconds' => 'Metrics Interval',
+ 'server.settings.metrics_history_days' => 'Metrics History',
+ 'server.settings.is_server_api_enabled' => 'Server API',
];
public function mount()
@@ -69,18 +81,59 @@ class Form extends Component
public function updatedServerSettingsIsBuildServer()
{
- $this->dispatch('serverInstalled');
+ $this->dispatch('refreshServerShow');
$this->dispatch('serverRefresh');
$this->dispatch('proxyStatusUpdated');
}
+ public function checkPortForServerApi()
+ {
+ try {
+ if ($this->server->settings->is_server_api_enabled === true) {
+ $this->server->checkServerApi();
+ $this->dispatch('success', 'Server API is reachable.');
+ }
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
public function instantSave()
{
try {
refresh_server_connection($this->server->privateKey);
$this->validateServer(false);
$this->server->settings->save();
+ $this->server->save();
$this->dispatch('success', 'Server updated.');
+ $this->dispatch('refreshServerShow');
+ if ($this->server->isSentinelEnabled()) {
+ PullSentinelImageJob::dispatchSync($this->server);
+ ray('Sentinel is enabled');
+ if ($this->server->settings->isDirty('is_metrics_enabled')) {
+ $this->dispatch('reloadWindow');
+ }
+ if ($this->server->settings->isDirty('is_server_api_enabled') && $this->server->settings->is_server_api_enabled === true) {
+ ray('Starting sentinel');
+
+ }
+ } else {
+ ray('Sentinel is not enabled');
+ StopSentinel::dispatch($this->server);
+ }
+ // $this->checkPortForServerApi();
+
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ public function restartSentinel()
+ {
+ try {
+ $version = get_latest_sentinel_version();
+ StartSentinel::run($this->server, $version, true);
+ $this->dispatch('success', 'Sentinel restarted.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
diff --git a/app/Livewire/Server/Show.php b/app/Livewire/Server/Show.php
index 7ebf90115..0751b186e 100644
--- a/app/Livewire/Server/Show.php
+++ b/app/Livewire/Server/Show.php
@@ -14,7 +14,7 @@ class Show extends Component
public $parameters = [];
- protected $listeners = ['serverInstalled' => '$refresh'];
+ protected $listeners = ['refreshServerShow' => '$refresh'];
public function mount()
{
diff --git a/app/Livewire/Server/ValidateAndInstall.php b/app/Livewire/Server/ValidateAndInstall.php
index bd33937e0..422cae779 100644
--- a/app/Livewire/Server/ValidateAndInstall.php
+++ b/app/Livewire/Server/ValidateAndInstall.php
@@ -143,7 +143,8 @@ class ValidateAndInstall extends Component
} else {
$this->docker_version = $this->server->validateDockerEngineVersion();
if ($this->docker_version) {
- $this->dispatch('serverInstalled');
+ $this->dispatch('refreshServerShow');
+ $this->dispatch('refreshBoardingIndex');
$this->dispatch('success', 'Server validated.');
} else {
$this->error = 'Docker Engine version is not 22+. Please install Docker manually before continuing: documentation.';
diff --git a/app/Models/Application.php b/app/Models/Application.php
index 2a34caf7e..fdb8796ef 100644
--- a/app/Models/Application.php
+++ b/app/Models/Application.php
@@ -235,11 +235,6 @@ class Application extends BaseModel
return "{$this->source->html_url}/{$this->git_repository}/commit/{$link}";
}
- if (strpos($this->git_repository, 'git@') === 0) {
- $git_repository = str_replace(['git@', ':', '.git'], ['', '/', ''], $this->git_repository);
-
- return "https://{$git_repository}/commit/{$link}";
- }
if (str($this->git_repository)->contains('bitbucket')) {
$git_repository = str_replace('.git', '', $this->git_repository);
$url = Url::fromString($git_repository);
@@ -248,6 +243,11 @@ class Application extends BaseModel
return $url->__toString();
}
+ if (strpos($this->git_repository, 'git@') === 0) {
+ $git_repository = str_replace(['git@', ':', '.git'], ['', '/', ''], $this->git_repository);
+
+ return "https://{$git_repository}/commit/{$link}";
+ }
return $this->git_repository;
}
@@ -532,7 +532,7 @@ class Application extends BaseModel
public function get_last_successful_deployment()
{
- return ApplicationDeploymentQueue::where('application_id', $this->id)->where('status', 'finished')->where('pull_request_id', 0)->orderBy('created_at', 'desc')->first();
+ return ApplicationDeploymentQueue::where('application_id', $this->id)->where('status', ApplicationDeploymentStatus::FINISHED)->where('pull_request_id', 0)->orderBy('created_at', 'desc')->first();
}
public function get_last_days_deployments()
@@ -1177,5 +1177,32 @@ class Application extends BaseModel
}
return [];
+ public function getMetrics(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 (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 = 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);
+
+ return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage];
+ });
+ });
+
+ return $parsedCollection->toArray();
+ }
}
}
diff --git a/app/Models/Server.php b/app/Models/Server.php
index b1419dc0e..7a99940fd 100644
--- a/app/Models/Server.php
+++ b/app/Models/Server.php
@@ -5,12 +5,11 @@ namespace App\Models;
use App\Actions\Server\InstallDocker;
use App\Enums\ProxyTypes;
use App\Jobs\PullSentinelImageJob;
-use App\Notifications\Server\Revived;
-use App\Notifications\Server\Unreachable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Process;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Illuminate\Support\Stringable;
@@ -462,10 +461,44 @@ $schema://$host {
Storage::disk('ssh-mux')->delete($this->muxFilename());
}
+ public function isSentinelEnabled()
+ {
+ return $this->isMetricsEnabled() || $this->isServerApiEnabled();
+ }
+
+ public function isMetricsEnabled()
+ {
+ return $this->settings->is_metrics_enabled;
+ }
+
+ public function isServerApiEnabled()
+ {
+ return $this->settings->is_server_api_enabled;
+ }
+
+ public function checkServerApi()
+ {
+ if ($this->isServerApiEnabled()) {
+ $server_ip = $this->ip;
+ if (isDev()) {
+ if ($this->id === 0) {
+ $server_ip = 'localhost';
+ }
+ }
+ $command = "curl -s http://{$server_ip}:12172/api/health";
+ $process = Process::timeout(5)->run($command);
+ if ($process->failed()) {
+ ray($process->exitCode(), $process->output(), $process->errorOutput());
+ throw new \Exception("Server API is not reachable on http://{$server_ip}:12172");
+ }
+
+ }
+ }
+
public function checkSentinel()
{
ray("Checking sentinel on server: {$this->name}");
- if ($this->is_metrics_enabled) {
+ if ($this->isSentinelEnabled()) {
$sentinel_found = instant_remote_process(['docker inspect coolify-sentinel'], $this, false);
$sentinel_found = json_decode($sentinel_found, true);
$status = data_get($sentinel_found, '0.State.Status', 'exited');
@@ -478,21 +511,57 @@ $schema://$host {
}
}
- public function getMetrics()
+ public function getCpuMetrics(int $mins = 5)
{
- if ($this->is_metrics_enabled) {
- $from = now()->subMinutes(5)->toIso8601ZuluString();
- $cpu = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl http://localhost:8888/api/cpu/history?from=$from'"], $this, false);
+ 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 (str($cpu)->contains('error')) {
+ $error = json_decode($cpu, 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);
+ }
$cpu = str($cpu)->explode("\n")->skip(1)->all();
$parsedCollection = collect($cpu)->flatMap(function ($item) {
return collect(explode("\n", trim($item)))->map(function ($line) {
- [$time, $value] = explode(',', trim($line));
+ [$time, $cpu_usage_percent] = explode(',', trim($line));
+ $cpu_usage_percent = number_format($cpu_usage_percent, 0);
- return [(int) $time, (float) $value];
+ return [(int) $time, (float) $cpu_usage_percent];
});
- })->toArray();
+ });
- return $parsedCollection;
+ return $parsedCollection->toArray();
+ }
+ }
+
+ 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 (str($memory)->contains('error')) {
+ $error = json_decode($memory, 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);
+ }
+ $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];
+ });
+ });
+
+ return $parsedCollection->toArray();
}
}
diff --git a/app/Models/Service.php b/app/Models/Service.php
index 7851eb58a..8adca3424 100644
--- a/app/Models/Service.php
+++ b/app/Models/Service.php
@@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Collection;
+use Symfony\Component\Yaml\Yaml;
class Service extends BaseModel
{
@@ -837,6 +838,13 @@ class Service extends BaseModel
$commands[] = "mkdir -p $workdir";
$commands[] = "cd $workdir";
+ $json = Yaml::parse($this->docker_compose);
+ foreach ($json['services'] as $service => $config) {
+ $envs = collect($config['environment']);
+ $envs->push("COOLIFY_CONTAINER_NAME=$service-{$this->uuid}");
+ data_set($json, "services.$service.environment", $envs->toArray());
+ }
+ $this->docker_compose = Yaml::dump($json);
$docker_compose_base64 = base64_encode($this->docker_compose);
$commands[] = "echo $docker_compose_base64 | base64 -d | tee docker-compose.yml > /dev/null";
$envs = $this->environment_variables()->get();
diff --git a/app/Models/StandaloneClickhouse.php b/app/Models/StandaloneClickhouse.php
index c5e252c34..e968db18d 100644
--- a/app/Models/StandaloneClickhouse.php
+++ b/app/Models/StandaloneClickhouse.php
@@ -226,4 +226,33 @@ class StandaloneClickhouse extends BaseModel
{
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
+
+ public function getMetrics(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 (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 = 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);
+
+ return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage];
+ });
+ });
+
+ return $parsedCollection->toArray();
+ }
+ }
}
diff --git a/app/Models/StandaloneDragonfly.php b/app/Models/StandaloneDragonfly.php
index 8c739d984..c6718acfe 100644
--- a/app/Models/StandaloneDragonfly.php
+++ b/app/Models/StandaloneDragonfly.php
@@ -226,4 +226,33 @@ class StandaloneDragonfly extends BaseModel
{
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
+
+ public function getMetrics(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 (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 = 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);
+
+ return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage];
+ });
+ });
+
+ return $parsedCollection->toArray();
+ }
+ }
}
diff --git a/app/Models/StandaloneKeydb.php b/app/Models/StandaloneKeydb.php
index 5216681c9..142f960aa 100644
--- a/app/Models/StandaloneKeydb.php
+++ b/app/Models/StandaloneKeydb.php
@@ -226,4 +226,33 @@ class StandaloneKeydb extends BaseModel
{
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
+
+ public function getMetrics(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 (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 = 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);
+
+ return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage];
+ });
+ });
+
+ return $parsedCollection->toArray();
+ }
+ }
}
diff --git a/app/Models/StandaloneMariadb.php b/app/Models/StandaloneMariadb.php
index 33fd2cbc2..7e6d2e0d1 100644
--- a/app/Models/StandaloneMariadb.php
+++ b/app/Models/StandaloneMariadb.php
@@ -226,4 +226,33 @@ class StandaloneMariadb extends BaseModel
{
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
+
+ public function getMetrics(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 (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 = 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);
+
+ return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage];
+ });
+ });
+
+ return $parsedCollection->toArray();
+ }
+ }
}
diff --git a/app/Models/StandaloneMongodb.php b/app/Models/StandaloneMongodb.php
index 0cc52b3f7..df895bb34 100644
--- a/app/Models/StandaloneMongodb.php
+++ b/app/Models/StandaloneMongodb.php
@@ -246,4 +246,33 @@ class StandaloneMongodb extends BaseModel
{
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
+
+ public function getMetrics(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 (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 = 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);
+
+ return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage];
+ });
+ });
+
+ return $parsedCollection->toArray();
+ }
+ }
}
diff --git a/app/Models/StandaloneMysql.php b/app/Models/StandaloneMysql.php
index 174736f77..bd160f877 100644
--- a/app/Models/StandaloneMysql.php
+++ b/app/Models/StandaloneMysql.php
@@ -227,4 +227,33 @@ class StandaloneMysql extends BaseModel
{
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
+
+ public function getMetrics(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 (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 = 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);
+
+ return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage];
+ });
+ });
+
+ return $parsedCollection->toArray();
+ }
+ }
}
diff --git a/app/Models/StandalonePostgresql.php b/app/Models/StandalonePostgresql.php
index a5bf4dc2a..114d376e8 100644
--- a/app/Models/StandalonePostgresql.php
+++ b/app/Models/StandalonePostgresql.php
@@ -227,4 +227,33 @@ class StandalonePostgresql extends BaseModel
{
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
+
+ public function getMetrics(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 (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 = 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);
+
+ return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage];
+ });
+ });
+
+ return $parsedCollection->toArray();
+ }
+ }
}
diff --git a/app/Models/StandaloneRedis.php b/app/Models/StandaloneRedis.php
index ed379750e..022cd8d09 100644
--- a/app/Models/StandaloneRedis.php
+++ b/app/Models/StandaloneRedis.php
@@ -222,4 +222,33 @@ class StandaloneRedis extends BaseModel
{
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
+
+ public function getMetrics(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 (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 = 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);
+
+ return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage];
+ });
+ });
+
+ return $parsedCollection->toArray();
+ }
+ }
}
diff --git a/app/View/Components/ApexCharts.php b/app/View/Components/ApexCharts.php
new file mode 100644
index 000000000..6b86055d9
--- /dev/null
+++ b/app/View/Components/ApexCharts.php
@@ -0,0 +1,34 @@
+chartId = $chartId;
+ $this->seriesData = $seriesData;
+ $this->categories = $categories;
+ $this->seriesName = $seriesName ?? 'Series';
+ }
+
+ /**
+ * Get the view / contents that represent the component.
+ */
+ public function render(): View|Closure|string
+ {
+ return view('components.apex-charts');
+ }
+}
diff --git a/bootstrap/helpers/applications.php b/bootstrap/helpers/applications.php
index 376b0f2aa..816a13853 100644
--- a/bootstrap/helpers/applications.php
+++ b/bootstrap/helpers/applications.php
@@ -65,7 +65,7 @@ function force_start_deployment(ApplicationDeploymentQueue $deployment)
function queue_next_deployment(Application $application)
{
$server_id = $application->destination->server_id;
- $next_found = ApplicationDeploymentQueue::where('server_id', $server_id)->where('status', 'queued')->get()->sortBy('created_at')->first();
+ $next_found = ApplicationDeploymentQueue::where('server_id', $server_id)->where('status', ApplicationDeploymentStatus::QUEUED)->get()->sortBy('created_at')->first();
if ($next_found) {
$next_found->update([
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
@@ -79,7 +79,7 @@ function queue_next_deployment(Application $application)
function next_queuable(string $server_id, string $application_id): bool
{
- $deployments = ApplicationDeploymentQueue::where('server_id', $server_id)->whereIn('status', ['in_progress', 'queued'])->get()->sortByDesc('created_at');
+ $deployments = ApplicationDeploymentQueue::where('server_id', $server_id)->whereIn('status', ['in_progress', ApplicationDeploymentStatus::QUEUED])->get()->sortByDesc('created_at');
$same_application_deployments = $deployments->where('application_id', $application_id);
$in_progress = $same_application_deployments->filter(function ($value, $key) {
return $value->status === 'in_progress';
@@ -98,3 +98,26 @@ function next_queuable(string $server_id, string $application_id): bool
return true;
}
+function next_after_cancel(?Server $server = null)
+{
+ if ($server) {
+ $next_found = ApplicationDeploymentQueue::where('server_id', data_get($server, 'id'))->where('status', ApplicationDeploymentStatus::QUEUED)->get()->sortBy('created_at');
+ if ($next_found->count() > 0) {
+ foreach ($next_found as $next) {
+ $server = Server::find($next->server_id);
+ $concurrent_builds = $server->settings->concurrent_builds;
+ $inprogress_deployments = ApplicationDeploymentQueue::where('server_id', $next->server_id)->whereIn('status', [ApplicationDeploymentStatus::QUEUED])->get()->sortByDesc('created_at');
+ if ($inprogress_deployments->count() < $concurrent_builds) {
+ $next->update([
+ 'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
+ ]);
+
+ dispatch(new ApplicationDeploymentJob(
+ application_deployment_queue_id: $next->id,
+ ));
+ }
+ break;
+ }
+ }
+ }
+}
diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php
index 7994c10af..b9dc685b7 100644
--- a/bootstrap/helpers/shared.php
+++ b/bootstrap/helpers/shared.php
@@ -79,6 +79,10 @@ function backup_dir(): string
{
return base_configuration_dir().'/backups';
}
+function metrics_dir(): string
+{
+ return base_configuration_dir().'/metrics';
+}
function generate_readme_file(string $name, string $updated_at): string
{
@@ -158,10 +162,10 @@ function get_route_parameters(): array
function get_latest_sentinel_version(): string
{
try {
- $response = Http::get('https://cdn.coollabs.io/coolify/versions.json');
+ $response = Http::get('https://cdn.coollabs.io/sentinel/versions.json');
$versions = $response->json();
- return data_get($versions, 'coolify.sentinel.version');
+ return data_get($versions, 'sentinel.version');
} catch (\Throwable $e) {
//throw $e;
ray($e->getMessage());
@@ -2282,3 +2286,10 @@ function isAnyDeploymentInprogress()
echo "No deployments in progress.\n";
exit(0);
}
+
+function generateSentinelToken()
+{
+ $token = Str::random(64);
+
+ return $token;
+}
diff --git a/config/constants.php b/config/constants.php
index 444d144a8..861b645ed 100644
--- a/config/constants.php
+++ b/config/constants.php
@@ -22,8 +22,8 @@ return [
],
'services' => [
// Temporary disabled until cache is implemented
- 'official' => 'https://cdn.coollabs.io/coolify/service-templates.json',
- // 'official' => 'https://raw.githubusercontent.com/coollabsio/coolify/main/templates/service-templates.json',
+ // 'official' => 'https://cdn.coollabs.io/coolify/service-templates.json',
+ 'official' => 'https://raw.githubusercontent.com/coollabsio/coolify/main/templates/service-templates.json',
],
'limits' => [
'trial_period' => 0,
diff --git a/config/coolify.php b/config/coolify.php
index c7cfe6101..a6d6d8581 100644
--- a/config/coolify.php
+++ b/config/coolify.php
@@ -14,5 +14,4 @@ return [
'helper_image' => env('HELPER_IMAGE', 'ghcr.io/coollabsio/coolify-helper:latest'),
'is_horizon_enabled' => env('HORIZON_ENABLED', true),
'is_scheduler_enabled' => env('SCHEDULER_ENABLED', true),
- 'is_sentinel_enabled' => env('SENTINEL_ENABLED', false),
];
diff --git a/config/sentry.php b/config/sentry.php
index 33a24edfb..caa659921 100644
--- a/config/sentry.php
+++ b/config/sentry.php
@@ -7,7 +7,7 @@ return [
// 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.297',
+ 'release' => '4.0.0-beta.298',
// When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'),
diff --git a/config/session.php b/config/session.php
index c7b176a5a..44ca7ded9 100644
--- a/config/session.php
+++ b/config/session.php
@@ -18,7 +18,7 @@ return [
|
*/
- 'driver' => env('SESSION_DRIVER', 'redis'),
+ 'driver' => env('SESSION_DRIVER', 'database'),
/*
|--------------------------------------------------------------------------
diff --git a/config/version.php b/config/version.php
index 06c1e6c66..ddcd3f2d4 100644
--- a/config/version.php
+++ b/config/version.php
@@ -1,3 +1,3 @@
dropColumn('is_metrics_enabled');
+ });
+ Schema::table('server_settings', function (Blueprint $table) {
+ $table->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());
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('servers', function (Blueprint $table) {
+ $table->boolean('is_metrics_enabled')->default(true);
+ });
+ Schema::table('server_settings', function (Blueprint $table) {
+ $table->dropColumn('is_metrics_enabled');
+ $table->dropColumn('metrics_refresh_rate_seconds');
+ $table->dropColumn('metrics_history_days');
+ $table->dropColumn('metrics_token');
+ });
+ }
+};
diff --git a/database/migrations/2024_06_20_102551_add_server_api_sentinel.php b/database/migrations/2024_06_20_102551_add_server_api_sentinel.php
new file mode 100644
index 000000000..b840195af
--- /dev/null
+++ b/database/migrations/2024_06_20_102551_add_server_api_sentinel.php
@@ -0,0 +1,28 @@
+boolean('is_server_api_enabled')->default(false);
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('server_settings', function (Blueprint $table) {
+ $table->dropColumn('is_server_api_enabled');
+ });
+ }
+};
diff --git a/lang/ar.json b/lang/ar.json
new file mode 100644
index 000000000..c5ec96c8d
--- /dev/null
+++ b/lang/ar.json
@@ -0,0 +1,30 @@
+{
+ "auth.login": "تسجيل الدخول",
+ "auth.login.azure": "تسجيل الدخول باستخدام Microsoft",
+ "auth.login.bitbucket": "تسجيل الدخول باستخدام Bitbucket",
+ "auth.login.github": "تسجيل الدخول باستخدام GitHub",
+ "auth.login.gitlab": "تسجيل الدخول باستخدام Gitlab",
+ "auth.login.google": "تسجيل الدخول باستخدام Google",
+ "auth.already_registered": "هل سبق لك التسجيل؟",
+ "auth.confirm_password": "تأكيد كلمة المرور",
+ "auth.forgot_password": "نسيت كلمة المرور",
+ "auth.forgot_password_send_email": "إرسال بريد إلكتروني لإعادة تعيين كلمة المرور",
+ "auth.register_now": "تسجيل",
+ "auth.logout": "تسجيل الخروج",
+ "auth.register": "تسجيل",
+ "auth.registration_disabled": "تم تعطيل التسجيل. يرجى التواصل مع المسؤول.",
+ "auth.reset_password": "إعادة تعيين كلمة المرور",
+ "auth.failed": "هذه البيانات لا تتطابق مع سجلاتنا.",
+ "auth.failed.callback": "فشل في معالجة استدعاء من مزود تسجيل الدخول.",
+ "auth.failed.password": "كلمة المرور المقدمة غير صحيحة.",
+ "auth.failed.email": "لا يمكننا العثور على مستخدم بهذا البريد الإلكتروني.",
+ "auth.throttle": "عدد محاولات تسجيل الدخول كثيرة جدًا. يرجى المحاولة مرة أخرى في :seconds ثانية.",
+ "input.name": "الاسم",
+ "input.email": "البريد الإلكتروني",
+ "input.password": "كلمة المرور",
+ "input.password.again": "كلمة المرور مرة أخرى",
+ "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."
+}
diff --git a/lang/de.json b/lang/de.json
new file mode 100644
index 000000000..29fec629f
--- /dev/null
+++ b/lang/de.json
@@ -0,0 +1,30 @@
+{
+ "auth.login": "Anmelden",
+ "auth.login.azure": "Mit Microsoft anmelden",
+ "auth.login.bitbucket": "Mit Bitbucket anmelden",
+ "auth.login.github": "Mit GitHub anmelden",
+ "auth.login.gitlab": "Mit GitLab anmelden",
+ "auth.login.google": "Mit Google anmelden",
+ "auth.already_registered": "Bereits registriert?",
+ "auth.confirm_password": "Passwort bestätigen",
+ "auth.forgot_password": "Passwort vergessen",
+ "auth.forgot_password_send_email": "Passwort zurücksetzen E-Mail senden",
+ "auth.register_now": "Registrieren",
+ "auth.logout": "Abmelden",
+ "auth.register": "Registrieren",
+ "auth.registration_disabled": "Registrierung ist deaktiviert. Bitte kontaktiere einen Administrator.",
+ "auth.reset_password": "Passwort zurücksetzen",
+ "auth.failed": "Diese Anmeldedaten wurden nicht gefunden.",
+ "auth.failed.callback": "Fehlerhafte Verarbeitung der Antwort des Anmeldeanbieters.",
+ "auth.failed.password": "Das angegebene Passwort ist inkorrekt.",
+ "auth.failed.email": "Wir können keinen Benutzer mit dieser E-Mail Adresse finden.",
+ "auth.throttle": "Zu viele Anmeldeversuche. Bitte versuche es in :seconds Sekunden erneut.",
+ "input.name": "Name",
+ "input.email": "E-Mail",
+ "input.password": "Passwort",
+ "input.password.again": "Passwort wiederholen",
+ "input.code": "Einmalcode",
+ "input.recovery_code": "Wiederherstellungscode",
+ "button.save": "Speichern",
+ "repository.url": "Beispiele Für öffentliche Repositories benutze https://.... Für private Repositories benutze git@....
https://github.com/coollabsio/coolify-examples main Branch wird ausgewählt https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify nodejs-fastify Branch wird ausgewählt. https://gitea.com/sedlav/expressjs.git main Branch wird ausgewählt. https://gitlab.com/andrasbacsai/nodejs-example.git main Branch wird ausgewählt."
+}
diff --git a/lang/es.json b/lang/es.json
new file mode 100644
index 000000000..0d8c0c940
--- /dev/null
+++ b/lang/es.json
@@ -0,0 +1,30 @@
+{
+ "auth.login": "Iniciar Sesión",
+ "auth.login.azure": "Acceder con Microsoft",
+ "auth.login.bitbucket": "Acceder con Bitbucket",
+ "auth.login.github": "Acceder con GitHub",
+ "auth.login.gitlab": "Acceder con Gitlab",
+ "auth.login.google": "Acceder con Google",
+ "auth.already_registered": "¿Ya estás registrado?",
+ "auth.confirm_password": "Confirmar contraseña",
+ "auth.forgot_password": "¿Olvidaste tu contraseña?",
+ "auth.forgot_password_send_email": "Enviar correo de recuperación de contraseña",
+ "auth.register_now": "Registrar",
+ "auth.logout": "Cerrar sesión",
+ "auth.register": "Registrar",
+ "auth.registration_disabled": "El registro está desactivado. Por favor contacta con el administrador.",
+ "auth.reset_password": "Cambiar contraseña",
+ "auth.failed": "Las credenciales no coinciden con nuestro registro..",
+ "auth.failed.callback": "Falló el proceso de inicio de sesión con el proveedor.",
+ "auth.failed.password": "La contraseña es incorrecta.",
+ "auth.failed.email": "No encontramos un usuario con ese correo.",
+ "auth.throttle": "Demasiados intentos. Por favor inténtalo en :seconds segundos.",
+ "input.name": "Nombre",
+ "input.email": "Correo",
+ "input.password": "Contraseña",
+ "input.password.again": "Escribe la contraseña otra vez",
+ "input.code": "Código de único uso",
+ "input.recovery_code": "Código de recuperación",
+ "button.save": "Guardar",
+ "repository.url": "Examples Para repositorios públicos, usar https://.... Para repositorios privados, usar git@....
https://github.com/coollabsio/coolify-examples main la rama 'main' será seleccionada. https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify nodejs-fastify la rama 'nodejs-fastify' será seleccionada. https://gitea.com/sedlav/expressjs.git main la rama 'main' será seleccionada. https://gitlab.com/andrasbacsai/nodejs-example.git main la rama 'main' será seleccionada."
+}
\ No newline at end of file
diff --git a/lang/fr.json b/lang/fr.json
new file mode 100644
index 000000000..ae7fa0a03
--- /dev/null
+++ b/lang/fr.json
@@ -0,0 +1,30 @@
+{
+ "auth.login": "Connexion",
+ "auth.login.azure": "Connexion avec Microsoft",
+ "auth.login.bitbucket": "Connexion avec Bitbucket",
+ "auth.login.github": "Connexion avec GitHub",
+ "auth.login.gitlab": "Connexion avec Gitlab",
+ "auth.login.google": "Connexion avec Google",
+ "auth.already_registered": "Déjà enregistré ?",
+ "auth.confirm_password": "Confirmer le mot de passe",
+ "auth.forgot_password": "Mot de passe oublié",
+ "auth.forgot_password_send_email": "Envoyer l'email de réinitialisation de mot de passe",
+ "auth.register_now": "S'enregistrer",
+ "auth.logout": "Déconnexion",
+ "auth.register": "S'enregistrer",
+ "auth.registration_disabled": "L'enregistrement est désactivé. Merci de contacter l'administateur.",
+ "auth.reset_password": "Réinitialiser le mot de passe",
+ "auth.failed": "Aucune correspondance n'a été trouvé pour les informations d'identification renseignées.",
+ "auth.failed.callback": "Erreur lors du processus de retour de la plateforme de connexion.",
+ "auth.failed.password": "Le mot de passe renseigné est incorrect.",
+ "auth.failed.email": "Aucun utilisateur avec cette adresse email n'a été trouvé.",
+ "auth.throttle": "Trop de tentatives de connexion. Merci de réessayer dans :seconds secondes.",
+ "input.name": "Nom",
+ "input.email": "Email",
+ "input.password": "Mot de passe",
+ "input.password.again": "Mot de passe identique",
+ "input.code": "Code à usage unique",
+ "input.recovery_code": "Code de récupération",
+ "button.save": "Sauvegarder",
+ "repository.url": "Exemples Pour les dépôts publiques, utilisez https://.... Pour les dépôts privés, utilisez git@....
https://github.com/coollabsio/coolify-examples main sera la branche selectionnée https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify nodejs-fastify sera la branche selectionnée. https://gitea.com/sedlav/expressjs.git main sera la branche selectionnée. https://gitlab.com/andrasbacsai/nodejs-example.git main sera la branche selectionnée."
+}
diff --git a/lang/it.json b/lang/it.json
new file mode 100644
index 000000000..6e4feb9cc
--- /dev/null
+++ b/lang/it.json
@@ -0,0 +1,30 @@
+{
+ "auth.login": "Accedi",
+ "auth.login.azure": "Accedi con Microsoft",
+ "auth.login.bitbucket": "Accedi con Bitbucket",
+ "auth.login.github": "Accedi con GitHub",
+ "auth.login.gitlab": "Accedi con Gitlab",
+ "auth.login.google": "Accedi con Google",
+ "auth.already_registered": "Già registrato?",
+ "auth.confirm_password": "Conferma password",
+ "auth.forgot_password": "Password dimenticata",
+ "auth.forgot_password_send_email": "Invia email per reimpostare la password",
+ "auth.register_now": "Registrati",
+ "auth.logout": "Esci",
+ "auth.register": "Registrati",
+ "auth.registration_disabled": "La registrazione è disabilitata. Si prega di contattare l'amministratore.",
+ "auth.reset_password": "Reimposta password",
+ "auth.failed": "Queste credenziali non corrispondono ai nostri record.",
+ "auth.failed.callback": "Errore durante l'elaborazione del callback dal provider di accesso.",
+ "auth.failed.password": "La password fornita non è corretta.",
+ "auth.failed.email": "Non possiamo trovare un utente con questo indirizzo email.",
+ "auth.throttle": "Troppi tentativi di accesso. Per favore riprova tra :seconds secondi.",
+ "input.name": "Nome",
+ "input.email": "Email",
+ "input.password": "Password",
+ "input.password.again": "Ripeti password",
+ "input.code": "Codice monouso",
+ "input.recovery_code": "Codice di recupero",
+ "button.save": "Salva",
+ "repository.url": "Esempi Per i repository pubblici, utilizza https://.... Per i repository privati, utilizza git@....
https://github.com/coollabsio/coolify-examples mainブランチが選択されます https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify nodejs-fastifyブランチが選択されます。 https://gitea.com/sedlav/expressjs.git mainブランチが選択されます。 https://gitlab.com/andrasbacsai/nodejs-example.git mainブランチが選択されます。"
+}
diff --git a/lang/pt.json b/lang/pt.json
new file mode 100644
index 000000000..b5dd5c434
--- /dev/null
+++ b/lang/pt.json
@@ -0,0 +1,30 @@
+{
+ "auth.login": "Entrar",
+ "auth.login.azure": "Entrar com Microsoft",
+ "auth.login.bitbucket": "Entrar com Bitbucket",
+ "auth.login.github": "Entrar com GitHub",
+ "auth.login.gitlab": "Entrar com Gitlab",
+ "auth.login.google": "Entrar com Google",
+ "auth.already_registered": "Já tem uma conta?",
+ "auth.confirm_password": "Confirmar senha",
+ "auth.forgot_password": "Esqueceu a senha?",
+ "auth.forgot_password_send_email": "Enviar e-mail de redefinição de senha",
+ "auth.register_now": "Cadastrar-se",
+ "auth.logout": "Sair",
+ "auth.register": "Cadastrar",
+ "auth.registration_disabled": "Cadastro desativado. Por favor, entre em contato com o administrador.",
+ "auth.reset_password": "Redefinir senha",
+ "auth.failed": "Essas credenciais não correspondem aos nossos registros.",
+ "auth.failed.callback": "Falha ao processar o callback do provedor de login.",
+ "auth.failed.password": "A senha fornecida está incorreta.",
+ "auth.failed.email": "Não encontramos um usuário com esse endereço de e-mail.",
+ "auth.throttle": "Muitas tentativas de login. Por favor, tente novamente em :seconds segundos.",
+ "input.name": "Nome",
+ "input.email": "E-mail",
+ "input.password": "Senha",
+ "input.password.again": "Repetir senha",
+ "input.code": "Código único",
+ "input.recovery_code": "Código de recuperação",
+ "button.save": "Salvar",
+ "repository.url": "Exemplos Para repositórios públicos, use https://.... Para repositórios privados, use git@....
https://github.com/coollabsio/coolify-examples a branch main será selecionada https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify a branch nodejs-fastify será selecionada. https://gitea.com/sedlav/expressjs.git a branch main será selecionada. https://gitlab.com/andrasbacsai/nodejs-example.git a branch main será selecionada."
+}
diff --git a/lang/tr.json b/lang/tr.json
new file mode 100644
index 000000000..255b0d15b
--- /dev/null
+++ b/lang/tr.json
@@ -0,0 +1,30 @@
+{
+ "auth.login": "Giriş",
+ "auth.login.azure": "Microsoft ile Giriş Yap",
+ "auth.login.bitbucket": "Bitbucket ile Giriş Yap",
+ "auth.login.github": "GitHub ile Giriş Yap",
+ "auth.login.gitlab": "GitLab ile Giriş Yap",
+ "auth.login.google": "Google ile Giriş Yap",
+ "auth.already_registered": "Zaten kayıtlı mısınız?",
+ "auth.confirm_password": "Şifreyi Onayla",
+ "auth.forgot_password": "Şifremi Unuttum",
+ "auth.forgot_password_send_email": "Şifre sıfırlama e-postası gönder",
+ "auth.register_now": "Kayıt Ol",
+ "auth.logout": "Çıkış Yap",
+ "auth.register": "Kayıt Ol",
+ "auth.registration_disabled": "Kayıt devre dışı bırakıldı. Lütfen yöneticiyle iletişime geçin.",
+ "auth.reset_password": "Şifreyi Sıfırla",
+ "auth.failed": "Bu kimlik bilgileri kayıtlarımızla eşleşmiyor.",
+ "auth.failed.callback": "Giriş sağlayıcıdan gelen istek işlenemedi.",
+ "auth.failed.password": "Sağlanan şifre yanlış.",
+ "auth.failed.email": "Bu e-posta adresiyle bir kullanıcı bulamıyoruz.",
+ "auth.throttle": "Çok fazla giriş denemesi. Lütfen :seconds saniye sonra tekrar deneyin.",
+ "input.name": "İsim",
+ "input.email": "E-posta",
+ "input.password": "Şifre",
+ "input.password.again": "Şifreyi Tekrar Girin",
+ "input.code": "Tek Kullanımlık Kod",
+ "input.recovery_code": "Kurtarma Kodu",
+ "button.save": "Kaydet",
+ "repository.url": "Örnekler Halka açık depolar için https://... kullanın. Özel depolar için git@... kullanın.
https://github.com/coollabsio/coolify-examples main dalı seçilecek https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify nodejs-fastify dalı seçilecek. https://gitea.com/sedlav/expressjs.git main dalı seçilecek. https://gitlab.com/andrasbacsai/nodejs-example.git main dalı seçilecek."
+}
diff --git a/lang/vi.json b/lang/vi.json
new file mode 100644
index 000000000..548dbe8b7
--- /dev/null
+++ b/lang/vi.json
@@ -0,0 +1,30 @@
+{
+ "auth.login": "Đăng Nhập",
+ "auth.login.azure": "Đăng Nhập Bằng Microsoft",
+ "auth.login.bitbucket": "Đăng Nhập Bằng Bitbucket",
+ "auth.login.github": "Đăng Nhập Bằng GitHub",
+ "auth.login.gitlab": "Đăng Nhập Bằng Gitlab",
+ "auth.login.google": "Đăng Nhập Bằng Google",
+ "auth.already_registered": "Đã đăng ký?",
+ "auth.confirm_password": "Nhập lại mật khẩu",
+ "auth.forgot_password": "Quên mật khẩu",
+ "auth.forgot_password_send_email": "Gửi email đặt lại mật khẩu",
+ "auth.register_now": "Đăng ký ngay",
+ "auth.logout": "Đăng xuất",
+ "auth.register": "Đăng ký",
+ "auth.registration_disabled": "Đăng ký không khả dụng. Vui lòng liên hệ quản trị viên.",
+ "auth.reset_password": "Đặt lại mật khẩu",
+ "auth.failed": "Thông tin đăng nhập không khớp với bất kỳ tài khoản nào.",
+ "auth.failed.callback": "Xử lý thông tin từ nhà cung cấp đăng nhập thất bại.",
+ "auth.failed.password": "Mật khẩu bạn cung cấp không chính xác.",
+ "auth.failed.email": "Không có người dùng nào đã đăng ký với email đó.",
+ "auth.throttle": "Quá nhiều lần đăng nhập thất bại. Vui lòng thử lại sau :seconds giây.",
+ "input.name": "Tên",
+ "input.email": "Email",
+ "input.password": "Mật khẩu",
+ "input.password.again": "Mật khẩu lần nữa",
+ "input.code": "One-time code",
+ "input.recovery_code": "Mã khôi phục",
+ "button.save": "Lưu",
+ "repository.url": "Ví dụ Với repo công khai, sử dụng https://.... Với repo riêng tư, sử dụng git@....