diff --git a/app/Actions/Database/StartMariadb.php b/app/Actions/Database/StartMariadb.php
index c6b243381..21fcdb8a5 100644
--- a/app/Actions/Database/StartMariadb.php
+++ b/app/Actions/Database/StartMariadb.php
@@ -23,7 +23,7 @@ class StartMariadb
$this->configuration_dir = database_configuration_dir() . '/' . $container_name;
$this->commands = [
- "echo '####### Starting {$database->name}.'",
+ "echo 'Starting {$database->name}.'",
"mkdir -p $this->configuration_dir",
];
@@ -104,7 +104,7 @@ class StartMariadb
$this->commands[] = "echo 'Pulling {$database->image} image.'";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
- $this->commands[] = "echo '####### {$database->name} started.'";
+ $this->commands[] = "echo '{$database->name} started.'";
return remote_process($this->commands, $database->destination->server);
}
diff --git a/app/Actions/Database/StartMongodb.php b/app/Actions/Database/StartMongodb.php
index 9eb884dbe..e0197a7bc 100644
--- a/app/Actions/Database/StartMongodb.php
+++ b/app/Actions/Database/StartMongodb.php
@@ -25,7 +25,7 @@ class StartMongodb
$this->configuration_dir = database_configuration_dir() . '/' . $container_name;
$this->commands = [
- "echo '####### Starting {$database->name}.'",
+ "echo 'Starting {$database->name}.'",
"mkdir -p $this->configuration_dir",
];
@@ -120,7 +120,7 @@ class StartMongodb
$this->commands[] = "echo 'Pulling {$database->image} image.'";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
- $this->commands[] = "echo '####### {$database->name} started.'";
+ $this->commands[] = "echo '{$database->name} started.'";
return remote_process($this->commands, $database->destination->server);
}
diff --git a/app/Actions/Database/StartMysql.php b/app/Actions/Database/StartMysql.php
index 761832525..76e8af619 100644
--- a/app/Actions/Database/StartMysql.php
+++ b/app/Actions/Database/StartMysql.php
@@ -23,7 +23,7 @@ class StartMysql
$this->configuration_dir = database_configuration_dir() . '/' . $container_name;
$this->commands = [
- "echo '####### Starting {$database->name}.'",
+ "echo 'Starting {$database->name}.'",
"mkdir -p $this->configuration_dir",
];
@@ -104,7 +104,7 @@ class StartMysql
$this->commands[] = "echo 'Pulling {$database->image} image.'";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
- $this->commands[] = "echo '####### {$database->name} started.'";
+ $this->commands[] = "echo '{$database->name} started.'";
return remote_process($this->commands, $database->destination->server);
}
diff --git a/app/Actions/Database/StartPostgresql.php b/app/Actions/Database/StartPostgresql.php
index b88da5e4f..97ae9da0e 100644
--- a/app/Actions/Database/StartPostgresql.php
+++ b/app/Actions/Database/StartPostgresql.php
@@ -23,7 +23,7 @@ class StartPostgresql
$this->configuration_dir = database_configuration_dir() . '/' . $container_name;
$this->commands = [
- "echo '####### Starting {$database->name}.'",
+ "echo 'Starting {$database->name}.'",
"mkdir -p $this->configuration_dir",
"mkdir -p $this->configuration_dir/docker-entrypoint-initdb.d/"
];
@@ -130,7 +130,7 @@ class StartPostgresql
$this->commands[] = "echo 'Pulling {$database->image} image.'";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
- $this->commands[] = "echo '####### {$database->name} started.'";
+ $this->commands[] = "echo '{$database->name} started.'";
return remote_process($this->commands, $database->destination->server);
}
diff --git a/app/Actions/Database/StartRedis.php b/app/Actions/Database/StartRedis.php
index fab055f20..fcb87b891 100644
--- a/app/Actions/Database/StartRedis.php
+++ b/app/Actions/Database/StartRedis.php
@@ -26,7 +26,7 @@ class StartRedis
$this->configuration_dir = database_configuration_dir() . '/' . $container_name;
$this->commands = [
- "echo '####### Starting {$database->name}.'",
+ "echo 'Starting {$database->name}.'",
"mkdir -p $this->configuration_dir",
];
@@ -114,7 +114,7 @@ class StartRedis
$this->commands[] = "echo 'Pulling {$database->image} image.'";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
- $this->commands[] = "echo '####### {$database->name} started.'";
+ $this->commands[] = "echo '{$database->name} started.'";
return remote_process($this->commands, $database->destination->server);
}
diff --git a/app/Actions/Server/InstallDocker.php b/app/Actions/Server/InstallDocker.php
index e99e8a11b..0713ed086 100644
--- a/app/Actions/Server/InstallDocker.php
+++ b/app/Actions/Server/InstallDocker.php
@@ -11,6 +11,11 @@ class InstallDocker
use AsAction;
public function handle(Server $server)
{
+ $supported_os_type = $server->validateOS();
+ if (!$supported_os_type) {
+ throw new \Exception('Server OS type is not supported for automated installation. Please install Docker manually before continuing: documentation.');
+ }
+ ray('Installing Docker on server: ' . $server->name . ' (' . $server->ip . ')' . ' with OS: ' . $supported_os_type);
$dockerVersion = '24.0';
$config = base64_encode('{
"log-driver": "json-file",
@@ -27,36 +32,49 @@ class InstallDocker
'server_id' => $server->id,
]);
}
-
+ $command = collect([]);
if (isDev() && $server->id === 0) {
- $command = [
- "echo '####### Installing Prerequisites...'",
+ $command = $command->merge([
+ "echo 'Installing Prerequisites...'",
"sleep 1",
- "echo '####### Installing/updating Docker Engine...'",
- "echo '####### Configuring Docker Engine (merging existing configuration with the required)...'",
+ "echo 'Installing Docker Engine...'",
+ "echo 'Configuring Docker Engine (merging existing configuration with the required)...'",
"sleep 4",
- "echo '####### Restarting Docker Engine...'",
+ "echo 'Restarting Docker Engine...'",
"ls -l /tmp"
- ];
+ ]);
} else {
- $command = [
- "echo '####### Installing Prerequisites...'",
- "command -v jq >/dev/null || apt-get update",
- "command -v jq >/dev/null || apt install -y jq",
- "echo '####### Installing/updating Docker Engine...'",
+ if ($supported_os_type === 'debian') {
+ $command = $command->merge([
+ "echo 'Installing Prerequisites...'",
+ "command -v jq >/dev/null || apt-get update",
+ "command -v jq >/dev/null || apt install -y jq",
+
+ ]);
+ } else if ($supported_os_type === 'rhel') {
+ $command = $command->merge([
+ "echo 'Installing Prerequisites...'",
+ "command -v jq >/dev/null || dnf install -y jq",
+ ]);
+ } else {
+ throw new \Exception('Unsupported OS');
+ }
+ $command = $command->merge([
+ "echo 'Installing Docker Engine...'",
"curl https://releases.rancher.com/install-docker/{$dockerVersion}.sh | sh",
- "echo '####### Configuring Docker Engine (merging existing configuration with the required)...'",
+ "echo 'Configuring Docker Engine (merging existing configuration with the required)...'",
"test -s /etc/docker/daemon.json && cp /etc/docker/daemon.json \"/etc/docker/daemon.json.original-`date +\"%Y%m%d-%H%M%S\"`\" || echo '{$config}' | base64 -d > /etc/docker/daemon.json",
"echo '{$config}' | base64 -d > /etc/docker/daemon.json.coolify",
"cat <<< $(jq . /etc/docker/daemon.json.coolify) > /etc/docker/daemon.json.coolify",
"cat <<< $(jq -s '.[0] * .[1]' /etc/docker/daemon.json /etc/docker/daemon.json.coolify) > /etc/docker/daemon.json",
- "echo '####### Restarting Docker Engine...'",
+ "echo 'Restarting Docker Engine...'",
+ "systemctl enable docker >/dev/null 2>&1 || true",
"systemctl restart docker",
- "echo '####### Creating default Docker network (coolify)...'",
+ "echo 'Creating default Docker network (coolify)...'",
"docker network create --attachable coolify >/dev/null 2>&1 || true",
- "echo '####### Done!'"
- ];
+ "echo 'Done!'"
+ ]);
+ return remote_process($command, $server);
}
- return remote_process($command, $server);
}
}
diff --git a/app/Actions/Service/StartService.php b/app/Actions/Service/StartService.php
index 74bdd81cb..ef473e578 100644
--- a/app/Actions/Service/StartService.php
+++ b/app/Actions/Service/StartService.php
@@ -14,13 +14,13 @@ class StartService
$network = $service->destination->network;
$service->saveComposeConfigs();
$commands[] = "cd " . $service->workdir();
- $commands[] = "echo '####### Saved configuration files to {$service->workdir()}.'";
- $commands[] = "echo '####### Creating Docker network.'";
+ $commands[] = "echo 'Saved configuration files to {$service->workdir()}.'";
+ $commands[] = "echo 'Creating Docker network.'";
$commands[] = "docker network create --attachable '{$service->uuid}' >/dev/null || true";
- $commands[] = "echo '####### Starting service {$service->name} on {$service->server->name}.'";
- $commands[] = "echo '####### Pulling images.'";
+ $commands[] = "echo 'Starting service {$service->name} on {$service->server->name}.'";
+ $commands[] = "echo 'Pulling images.'";
$commands[] = "docker compose pull";
- $commands[] = "echo '####### Starting containers.'";
+ $commands[] = "echo 'Starting containers.'";
$commands[] = "docker compose up -d --remove-orphans --force-recreate";
$commands[] = "docker network connect $service->uuid coolify-proxy || true";
$compose = data_get($service,'docker_compose',[]);
diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php
index b4f63661c..e78849a07 100644
--- a/app/Exceptions/Handler.php
+++ b/app/Exceptions/Handler.php
@@ -6,6 +6,7 @@ use App\Models\InstanceSettings;
use App\Models\User;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
+use RuntimeException;
use Sentry\Laravel\Integration;
use Sentry\State\Scope;
use Throwable;
@@ -55,7 +56,9 @@ class Handler extends ExceptionHandler
{
$this->reportable(function (Throwable $e) {
if (isDev()) {
- ray($e);
+ // return;
+ }
+ if ($e instanceof RuntimeException) {
return;
}
$this->settings = InstanceSettings::get();
@@ -74,6 +77,7 @@ class Handler extends ExceptionHandler
);
}
);
+ ray('reporting to sentry');
Integration::captureUnhandledException($e);
});
}
diff --git a/app/Http/Controllers/ApplicationController.php b/app/Http/Controllers/ApplicationController.php
index 6f58c71e6..12411d3fd 100644
--- a/app/Http/Controllers/ApplicationController.php
+++ b/app/Http/Controllers/ApplicationController.php
@@ -10,23 +10,6 @@ class ApplicationController extends Controller
{
use AuthorizesRequests, ValidatesRequests;
- public function configuration()
- {
- $project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
- if (!$project) {
- return redirect()->route('dashboard');
- }
- $environment = $project->load(['environments'])->environments->where('name', request()->route('environment_name'))->first()->load(['applications']);
- if (!$environment) {
- return redirect()->route('dashboard');
- }
- $application = $environment->applications->where('uuid', request()->route('application_uuid'))->first();
- if (!$application) {
- return redirect()->route('dashboard');
- }
- return view('project.application.configuration', ['application' => $application]);
- }
-
public function deployments()
{
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
diff --git a/app/Http/Livewire/Boarding/Index.php b/app/Http/Livewire/Boarding/Index.php
index d710b9e6f..7f53708ad 100644
--- a/app/Http/Livewire/Boarding/Index.php
+++ b/app/Http/Livewire/Boarding/Index.php
@@ -188,7 +188,6 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
public function validateServer()
{
try {
- $customErrorMessage = "Server is not reachable:";
config()->set('coolify.mux_enabled', false);
instant_remote_process(['uptime'], $this->createdServer, true);
@@ -198,7 +197,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
]);
} catch (\Throwable $e) {
$this->serverReachable = false;
- return handleError(error: $e, customErrorMessage: $customErrorMessage, livewire: $this);
+ return handleError(error: $e, livewire: $this);
}
try {
@@ -206,7 +205,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
$dockerVersion = checkMinimumDockerEngineVersion($dockerVersion);
if (is_null($dockerVersion)) {
$this->currentState = 'install-docker';
- throw new \Exception('Docker version is not supported or not installed.');
+ throw new \Exception('Docker not found or old version is installed.');
}
$this->createdServer->settings()->update([
'is_usable' => true,
@@ -214,14 +213,20 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
$this->getProxyType();
} catch (\Throwable $e) {
// $this->dockerInstallationStarted = false;
- return handleError(error: $e, customErrorMessage: $customErrorMessage, livewire: $this);
+ return handleError(error: $e, livewire: $this);
}
}
public function installDocker()
{
- $this->dockerInstallationStarted = true;
- $activity = InstallDocker::run($this->createdServer);
- $this->emit('newMonitorActivity', $activity->id);
+ try {
+ $this->dockerInstallationStarted = true;
+ $activity = InstallDocker::run($this->createdServer);
+ $this->emit('installDocker');
+ $this->emit('newMonitorActivity', $activity->id);
+ } catch (\Throwable $e) {
+ $this->dockerInstallationStarted = false;
+ return handleError(error: $e, livewire: $this);
+ }
}
public function dockerInstalledOrSkipped()
{
diff --git a/app/Http/Livewire/Dashboard.php b/app/Http/Livewire/Dashboard.php
index 723b00f7f..b7219864d 100644
--- a/app/Http/Livewire/Dashboard.php
+++ b/app/Http/Livewire/Dashboard.php
@@ -3,7 +3,6 @@
namespace App\Http\Livewire;
use App\Models\Project;
-use App\Models\S3Storage;
use App\Models\Server;
use Livewire\Component;
diff --git a/app/Http/Livewire/Project/Application/Advanced.php b/app/Http/Livewire/Project/Application/Advanced.php
new file mode 100644
index 000000000..552a3873a
--- /dev/null
+++ b/app/Http/Livewire/Project/Application/Advanced.php
@@ -0,0 +1,54 @@
+ 'boolean|required',
+ 'application.settings.is_git_lfs_enabled' => 'boolean|required',
+ 'application.settings.is_preview_deployments_enabled' => 'boolean|required',
+ 'application.settings.is_auto_deploy_enabled' => 'boolean|required',
+ 'application.settings.is_force_https_enabled' => 'boolean|required',
+ 'application.settings.is_log_drain_enabled' => 'boolean|required',
+ 'application.settings.is_gpu_enabled' => 'boolean|required',
+ 'application.settings.gpu_driver' => 'string|required',
+ 'application.settings.gpu_count' => 'string|required',
+ 'application.settings.gpu_device_ids' => 'string|required',
+ 'application.settings.gpu_options' => 'string|required',
+ ];
+ public function instantSave()
+ {
+ if ($this->application->settings->is_log_drain_enabled) {
+ if (!$this->application->destination->server->isLogDrainEnabled()) {
+ $this->application->settings->is_log_drain_enabled = false;
+ $this->emit('error', 'Log drain is not enabled on this server.');
+ return;
+ }
+ }
+ if ($this->application->settings->is_force_https_enabled) {
+ $this->emit('resetDefaultLabels', false);
+ }
+ $this->application->settings->save();
+ $this->emit('success', 'Settings saved.');
+ }
+ public function submit() {
+ if ($this->application->settings->gpu_count && $this->application->settings->gpu_device_ids) {
+ $this->emit('error', 'You cannot set both GPU count and GPU device IDs.');
+ $this->application->settings->gpu_count = null;
+ $this->application->settings->gpu_device_ids = null;
+ $this->application->settings->save();
+ return;
+ }
+ $this->application->settings->save();
+ $this->emit('success', 'Settings saved.');
+ }
+ public function render()
+ {
+ return view('livewire.project.application.advanced');
+ }
+}
diff --git a/app/Http/Livewire/Project/Application/Configuration.php b/app/Http/Livewire/Project/Application/Configuration.php
new file mode 100644
index 000000000..7cb2939f2
--- /dev/null
+++ b/app/Http/Livewire/Project/Application/Configuration.php
@@ -0,0 +1,39 @@
+load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
+ if (!$project) {
+ return redirect()->route('dashboard');
+ }
+ $environment = $project->load(['environments'])->environments->where('name', request()->route('environment_name'))->first()->load(['applications']);
+ if (!$environment) {
+ return redirect()->route('dashboard');
+ }
+ $application = $environment->applications->where('uuid', request()->route('application_uuid'))->first();
+ if (!$application) {
+ return redirect()->route('dashboard');
+ }
+ $this->application = $application;
+ $mainServer = $application->destination->server;
+ $servers = Server::ownedByCurrentTeam()->get();
+ $this->servers = $servers->filter(function ($server) use ($mainServer) {
+ return $server->id != $mainServer->id;
+ });
+ }
+ public function render()
+ {
+ return view('livewire.project.application.configuration');
+ }
+}
diff --git a/app/Http/Livewire/Project/Application/General.php b/app/Http/Livewire/Project/Application/General.php
index 63dbeba23..27c5023c3 100644
--- a/app/Http/Livewire/Project/Application/General.php
+++ b/app/Http/Livewire/Project/Application/General.php
@@ -26,14 +26,10 @@ class General extends Component
public bool $isConfigurationChanged = false;
public bool $is_static;
- public bool $is_git_submodules_enabled;
- public bool $is_git_lfs_enabled;
- public bool $is_debug_enabled;
- public bool $is_preview_deployments_enabled;
- public bool $is_auto_deploy_enabled;
- public bool $is_force_https_enabled;
- public bool $is_log_drain_enabled;
+ protected $listeners = [
+ 'resetDefaultLabels'
+ ];
protected $rules = [
'application.name' => 'required',
'application.description' => 'nullable',
@@ -56,6 +52,7 @@ class General extends Component
'application.dockerfile_location' => 'nullable',
'application.custom_labels' => 'nullable',
'application.dockerfile_target_build' => 'nullable',
+ 'application.settings.is_static' => 'boolean|required',
];
protected $validationAttributes = [
'application.name' => 'name',
@@ -79,6 +76,7 @@ class General extends Component
'application.dockerfile_location' => 'Dockerfile location',
'application.custom_labels' => 'Custom labels',
'application.dockerfile_target_build' => 'Dockerfile target build',
+ 'application.settings.is_static' => 'Is static',
];
public function mount()
@@ -93,18 +91,13 @@ class General extends Component
} else {
$this->customLabels = str($this->application->custom_labels)->replace(',', "\n");
}
- if (data_get($this->application, 'settings')) {
- $this->is_static = $this->application->settings->is_static;
- $this->is_git_submodules_enabled = $this->application->settings->is_git_submodules_enabled;
- $this->is_git_lfs_enabled = $this->application->settings->is_git_lfs_enabled;
- $this->is_debug_enabled = $this->application->settings->is_debug_enabled;
- $this->is_preview_deployments_enabled = $this->application->settings->is_preview_deployments_enabled;
- $this->is_auto_deploy_enabled = $this->application->settings->is_auto_deploy_enabled;
- $this->is_force_https_enabled = $this->application->settings->is_force_https_enabled;
- $this->is_log_drain_enabled = $this->application->settings->is_log_drain_enabled;
- }
$this->checkLabelUpdates();
}
+ public function instantSave()
+ {
+ $this->application->settings->save();
+ $this->emit('success', 'Settings saved.');
+ }
public function updatedApplicationBuildPack()
{
if ($this->application->build_pack !== 'nixpacks') {
@@ -121,40 +114,6 @@ class General extends Component
$this->labelsChanged = false;
}
}
- public function instantSave()
- {
- // @TODO: find another way - if possible
- $force_https = $this->application->settings->is_force_https_enabled;
- $this->application->settings->is_static = $this->is_static;
- if ($this->is_static) {
- $this->application->ports_exposes = 80;
- } else {
- $this->application->ports_exposes = 3000;
- }
- $this->application->settings->is_git_submodules_enabled = $this->is_git_submodules_enabled;
- $this->application->settings->is_git_lfs_enabled = $this->is_git_lfs_enabled;
- $this->application->settings->is_debug_enabled = $this->is_debug_enabled;
- $this->application->settings->is_preview_deployments_enabled = $this->is_preview_deployments_enabled;
- $this->application->settings->is_auto_deploy_enabled = $this->is_auto_deploy_enabled;
- $this->application->settings->is_force_https_enabled = $this->is_force_https_enabled;
- $this->application->settings->is_log_drain_enabled = $this->is_log_drain_enabled;
- if ($this->is_log_drain_enabled) {
- if (!$this->application->destination->server->isLogDrainEnabled()) {
- $this->application->settings->is_log_drain_enabled = $this->is_log_drain_enabled = false;
- $this->emit('error', 'Log drain is not enabled on the server. Please enable it first.');
- return;
- }
- }
- $this->application->settings->save();
- $this->application->save();
- $this->application->refresh();
- $this->emit('success', 'Application settings updated!');
- $this->checkLabelUpdates();
- $this->isConfigurationChanged = $this->application->isConfigurationChanged();
- if ($force_https !== $this->is_force_https_enabled) {
- $this->resetDefaultLabels(false);
- }
- }
public function getWildcardDomain()
{
diff --git a/app/Http/Livewire/Project/Application/Rollback.php b/app/Http/Livewire/Project/Application/Rollback.php
index dcebd4d93..4c363421d 100644
--- a/app/Http/Livewire/Project/Application/Rollback.php
+++ b/app/Http/Livewire/Project/Application/Rollback.php
@@ -38,10 +38,10 @@ class Rollback extends Component
]);
}
- public function loadImages()
+ public function loadImages($showToast = false)
{
try {
- $image = $this->application->uuid;
+ $image = $this->application->docker_registry_image_name ?? $this->application->uuid;
if ($this->application->destination->server->isFunctional()) {
$output = instant_remote_process([
"docker inspect --format='{{.Config.Image}}' {$this->application->uuid}",
@@ -66,6 +66,7 @@ class Rollback extends Component
];
})->toArray();
}
+ $showToast && $this->emit('success', 'Images loaded.');
return [];
} catch (\Throwable $e) {
return handleError($e, $this);
diff --git a/app/Http/Livewire/Project/Shared/Destination.php b/app/Http/Livewire/Project/Shared/Destination.php
index 3bdb48af6..a946c013f 100644
--- a/app/Http/Livewire/Project/Shared/Destination.php
+++ b/app/Http/Livewire/Project/Shared/Destination.php
@@ -6,5 +6,7 @@ use Livewire\Component;
class Destination extends Component
{
- public $destination;
+ public $resource;
+ public $servers = [];
+ public $additionalServers = [];
}
diff --git a/app/Http/Livewire/Server/Form.php b/app/Http/Livewire/Server/Form.php
index 4caa15b3f..dacb8faad 100644
--- a/app/Http/Livewire/Server/Form.php
+++ b/app/Http/Livewire/Server/Form.php
@@ -43,9 +43,9 @@ class Form extends Component
$this->wildcard_domain = $this->server->settings->wildcard_domain;
$this->cleanup_after_percentage = $this->server->settings->cleanup_after_percentage;
}
- public function serverRefresh()
+ public function serverRefresh($install = true)
{
- $this->validateServer();
+ $this->validateServer($install);
}
public function instantSave()
{
@@ -77,12 +77,15 @@ class Form extends Component
{
try {
$uptime = $this->server->validateConnection();
- if ($uptime) {
- $install && $this->emit('success', 'Server is reachable.');
- } else {
+ if (!$uptime) {
$install && $this->emit('error', 'Server is not reachable. Please check your connection and configuration.');
return;
}
+ $supported_os_type = $this->server->validateOS();
+ if (!$supported_os_type) {
+ $install && $this->emit('error', 'Server OS type is not supported for automated installation. Please install Docker manually before continuing: documentation.');
+ return;
+ }
$dockerInstalled = $this->server->validateDockerEngine();
if ($dockerInstalled) {
$install && $this->emit('success', 'Docker Engine is installed.
Checking version.');
@@ -92,7 +95,7 @@ class Form extends Component
}
$dockerVersion = $this->server->validateDockerEngineVersion();
if ($dockerVersion) {
- $install && $this->emit('success', 'Docker Engine version is 23+.');
+ $install && $this->emit('success', 'Docker Engine version is 22+.');
} else {
$install && $this->installDocker();
return;
diff --git a/app/Http/Livewire/Server/Show.php b/app/Http/Livewire/Server/Show.php
index 77ae447d7..3863381b2 100644
--- a/app/Http/Livewire/Server/Show.php
+++ b/app/Http/Livewire/Server/Show.php
@@ -25,7 +25,7 @@ class Show extends Component
}
public function submit()
{
- $this->emit('serverRefresh');
+ $this->emit('serverRefresh',false);
}
public function render()
{
diff --git a/app/Jobs/ApplicationDeployDockerImageJob.php b/app/Jobs/ApplicationDeployDockerImageJob.php
new file mode 100644
index 000000000..dda5629c8
--- /dev/null
+++ b/app/Jobs/ApplicationDeployDockerImageJob.php
@@ -0,0 +1,111 @@
+applicationDeploymentQueueId = $applicationDeploymentQueueId;
+ }
+ public function handle()
+ {
+ ray()->clearAll();
+ ray('Deploying Docker Image');
+ try {
+ $applicationDeploymentQueue = ApplicationDeploymentQueue::find($this->applicationDeploymentQueueId);
+ $application = Application::find($applicationDeploymentQueue->application_id);
+
+ $deploymentUuid = data_get($applicationDeploymentQueue, 'deployment_uuid');
+ $dockerImage = data_get($application, 'docker_registry_image_name');
+ $dockerImageTag = data_get($application, 'docker_registry_image_tag');
+ $productionImageName = str("{$dockerImage}:{$dockerImageTag}");
+ $destination = $application->destination->getMorphClass()::where('id', $application->destination->id)->first();
+ $pullRequestId = data_get($applicationDeploymentQueue, 'pull_request_id');
+
+ $server = data_get($destination, 'server');
+ $network = data_get($destination, 'network');
+
+ $containerName = generateApplicationContainerName($application, $pullRequestId);
+ savePrivateKeyToFs($server);
+
+ ray("echo 'Starting deployment of {$productionImageName}.'");
+
+ $applicationDeploymentQueue->update([
+ 'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
+ ]);
+ $this->executeRemoteCommand(
+ server: $server,
+ logModel: $applicationDeploymentQueue,
+ commands: prepareHelperContainer($server, $network, $deploymentUuid)
+ );
+
+ $this->executeRemoteCommand(
+ server: $server,
+ logModel: $applicationDeploymentQueue,
+ commands: generateComposeFile(
+ deploymentUuid: $deploymentUuid,
+ server: $server,
+ network: $network,
+ application: $application,
+ containerName: $containerName,
+ imageName: $productionImageName,
+ pullRequestId: $pullRequestId
+ )
+ );
+ $this->executeRemoteCommand(
+ server: $server,
+ logModel: $applicationDeploymentQueue,
+ commands: rollingUpdate(application: $application, deploymentUuid: $deploymentUuid)
+ );
+ } catch (Throwable $e) {
+ $this->executeRemoteCommand(
+ server: $server,
+ logModel: $applicationDeploymentQueue,
+ commands: [
+ "echo 'Oops something is not okay, are you okay? 😢'",
+ "echo '{$e->getMessage()}'",
+ "echo -n 'Deployment failed. Removing the new version of your application.'",
+ executeInDocker($deploymentUuid, "docker rm -f $containerName >/dev/null 2>&1"),
+ ]
+ );
+ // $this->next(ApplicationDeploymentStatus::FAILED->value);
+ throw $e;
+ }
+ }
+ // private function next(string $status)
+ // {
+ // // If the deployment is cancelled by the user, don't update the status
+ // if ($this->application_deployment_queue->status !== ApplicationDeploymentStatus::CANCELLED_BY_USER->value) {
+ // $this->application_deployment_queue->update([
+ // 'status' => $status,
+ // ]);
+ // }
+ // queue_next_deployment($this->application);
+ // if ($status === ApplicationDeploymentStatus::FINISHED->value) {
+ // $this->application->environment->project->team->notify(new DeploymentSuccess($this->application, $this->deployment_uuid, $this->preview));
+ // }
+ // if ($status === ApplicationDeploymentStatus::FAILED->value) {
+ // $this->application->environment->project->team->notify(new DeploymentFailed($this->application, $this->deployment_uuid, $this->preview));
+ // }
+ // }
+}
diff --git a/app/Jobs/ApplicationDeploySimpleDockerfileJob.php b/app/Jobs/ApplicationDeploySimpleDockerfileJob.php
new file mode 100644
index 000000000..a9e17bc80
--- /dev/null
+++ b/app/Jobs/ApplicationDeploySimpleDockerfileJob.php
@@ -0,0 +1,29 @@
+applicationDeploymentQueueId = $applicationDeploymentQueueId;
+ }
+ public function handle() {
+ ray('Deploying Simple Dockerfile');
+ }
+}
diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php
index dacdbf89b..77c8bd13f 100644
--- a/app/Jobs/ApplicationDeploymentJob.php
+++ b/app/Jobs/ApplicationDeploymentJob.php
@@ -24,6 +24,7 @@ use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
+use RuntimeException;
use Spatie\Url\Url;
use Symfony\Component\Yaml\Yaml;
use Throwable;
@@ -54,6 +55,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private GithubApp|GitlabApp|string $source = 'other';
private StandaloneDocker|SwarmDocker $destination;
private Server $server;
+ private Server $mainServer;
private ?ApplicationPreview $preview = null;
private ?string $git_type = null;
@@ -110,7 +112,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->source = $source->getMorphClass()::where('id', $this->application->source->id)->first();
}
$this->destination = $this->application->destination->getMorphClass()::where('id', $this->application->destination->id)->first();
- $this->server = $this->destination->server;
+ $this->server = $this->mainServer = $this->destination->server;
$this->serverUser = $this->server->user;
$this->basedir = "/artifacts/{$this->deployment_uuid}";
$this->workdir = "{$this->basedir}" . rtrim($this->application->base_directory, '/');
@@ -180,10 +182,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->buildTarget = " --target {$this->application->dockerfile_target_build} ";
}
- // Get user home directory
- $this->serverUserHomeDir = instant_remote_process(["echo \$HOME"], $this->server);
- $this->dockerConfigFileExists = instant_remote_process(["test -f {$this->serverUserHomeDir}/.docker/config.json && echo 'OK' || echo 'NOK'"], $this->server);
-
// Check custom port
preg_match('/(?<=:)\d+(?=\/)/', $this->application->git_repository, $matches);
if (count($matches) === 1) {
@@ -197,6 +195,12 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
try {
if ($this->restart_only && $this->application->build_pack !== 'dockerimage') {
$this->just_restart();
+ if ($this->server->isProxyShouldRun()) {
+ dispatch(new ContainerStatusJob($this->server));
+ }
+ $this->next(ApplicationDeploymentStatus::FINISHED->value);
+ $this->application->isConfigurationChanged(true);
+ return;
} else if ($this->application->dockerfile) {
$this->deploy_simple_dockerfile();
} else if ($this->application->build_pack === 'dockerimage') {
@@ -215,10 +219,12 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if ($this->server->isProxyShouldRun()) {
dispatch(new ContainerStatusJob($this->server));
}
+ if ($this->application->docker_registry_image_name) {
+ $this->push_to_docker_registry();
+ }
$this->next(ApplicationDeploymentStatus::FINISHED->value);
$this->application->isConfigurationChanged(true);
} catch (Exception $e) {
- ray($e);
$this->fail($e);
throw $e;
} finally {
@@ -256,7 +262,41 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
);
}
}
-
+ private function push_to_docker_registry()
+ {
+ try {
+ instant_remote_process(["docker images --format '{{json .}}' {$this->production_image_name}"], $this->server);
+ $this->execute_remote_command(
+ [
+ "echo '\n----------------------------------------'",
+ ],
+ ["echo -n 'Pushing image to docker registry ({$this->production_image_name}).'"],
+ [
+ executeInDocker($this->deployment_uuid, "docker push {$this->production_image_name}"), 'hidden' => true
+ ],
+ );
+ if ($this->application->docker_registry_image_tag) {
+ // Tag image with latest
+ $this->execute_remote_command(
+ ['echo -n "Tagging and pushing image with latest tag."'],
+ [
+ executeInDocker($this->deployment_uuid, "docker tag {$this->production_image_name} {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"), 'ignore_errors' => true, 'hidden' => true
+ ],
+ [
+ executeInDocker($this->deployment_uuid, "docker push {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"), 'ignore_errors' => true, 'hidden' => true
+ ],
+ );
+ }
+ $this->execute_remote_command([
+ "echo -n 'Image pushed to docker registry.'"
+ ]);
+ } catch (Exception $e) {
+ $this->execute_remote_command(
+ ["echo -n 'Failed to push image to docker registry. Please check debug logs for more information.'"],
+ );
+ ray($e);
+ }
+ }
// private function deploy_docker_compose()
// {
// $dockercompose_base64 = base64_encode($this->application->dockercompose);
@@ -296,20 +336,32 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function generate_image_names()
{
if ($this->application->dockerfile) {
- $this->build_image_name = Str::lower("{$this->application->uuid}:build");
- $this->production_image_name = Str::lower("{$this->application->uuid}:latest");
+ if ($this->application->docker_registry_image_name) {
+ $this->build_image_name = Str::lower("{$this->application->docker_registry_image_name}:build");
+ $this->production_image_name = Str::lower("{$this->application->docker_registry_image_name}:latest");
+ } else {
+ $this->build_image_name = Str::lower("{$this->application->uuid}:build");
+ $this->production_image_name = Str::lower("{$this->application->uuid}:latest");
+ }
} else if ($this->application->build_pack === 'dockerimage') {
$this->production_image_name = Str::lower("{$this->dockerImage}:{$this->dockerImageTag}");
} else if ($this->pull_request_id !== 0) {
- $this->build_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}-build");
- $this->production_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}");
- } else {
- $tag = Str::of("{$this->commit}-{$this->application->id}-{$this->pull_request_id}");
- if (strlen($tag) > 128) {
- $tag = $tag->substr(0, 128);
+ if ($this->application->docker_registry_image_name) {
+ $this->build_image_name = Str::lower("{$this->application->docker_registry_image_name}:pr-{$this->pull_request_id}-build");
+ $this->production_image_name = Str::lower("{$this->application->docker_registry_image_name}:pr-{$this->pull_request_id}");
+ } else {
+ $this->build_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}-build");
+ $this->production_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}");
+ }
+ } else {
+ $this->dockerImageTag = str($this->commit)->substr(0, 128);
+ if ($this->application->docker_registry_image_name) {
+ $this->build_image_name = Str::lower("{$this->application->docker_registry_image_name}:{$this->dockerImageTag}-build");
+ $this->production_image_name = Str::lower("{$this->application->docker_registry_image_name}:{$this->dockerImageTag}");
+ } else {
+ $this->build_image_name = Str::lower("{$this->application->uuid}:{$this->dockerImageTag}-build");
+ $this->production_image_name = Str::lower("{$this->application->uuid}:{$this->dockerImageTag}");
}
- $this->build_image_name = Str::lower("{$this->application->uuid}:{$tag}-build");
- $this->production_image_name = Str::lower("{$this->application->uuid}:{$tag}");
}
}
private function just_restart()
@@ -323,31 +375,41 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->check_git_if_build_needed();
$this->set_base_dir();
$this->generate_image_names();
- $this->execute_remote_command([
- "docker images -q {$this->production_image_name} 2>/dev/null", "hidden" => true, "save" => "local_image_found"
- ]);
+ $this->check_image_locally_or_remotely();
if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty()) {
$this->generate_compose_file();
$this->rolling_update();
return;
}
- $this->execute_remote_command([
- "echo 'Cannot find image {$this->production_image_name} locally. Please redeploy the application.'",
- ]);
+ throw new RuntimeException('Cannot find image anywhere. Please redeploy the application.');
}
- private function save_environment_variables()
+ private function check_image_locally_or_remotely()
{
- $envs = collect([]);
- foreach ($this->application->environment_variables as $env) {
- $envs->push($env->key . '=' . $env->value);
+ $this->execute_remote_command([
+ "docker images -q {$this->production_image_name} 2>/dev/null", "hidden" => true, "save" => "local_image_found"
+ ]);
+ if (str($this->saved_outputs->get('local_image_found'))->isEmpty() && $this->application->docker_registry_image_name) {
+ $this->execute_remote_command([
+ "docker pull {$this->production_image_name} 2>/dev/null", "ignore_errors" => true, "hidden" => true
+ ]);
+ $this->execute_remote_command([
+ "docker images -q {$this->production_image_name} 2>/dev/null", "hidden" => true, "save" => "local_image_found"
+ ]);
}
- $envs_base64 = base64_encode($envs->implode("\n"));
- $this->execute_remote_command(
- [
- executeInDocker($this->deployment_uuid, "echo '$envs_base64' | base64 -d > $this->workdir/.env")
- ],
- );
}
+ // private function save_environment_variables()
+ // {
+ // $envs = collect([]);
+ // foreach ($this->application->environment_variables as $env) {
+ // $envs->push($env->key . '=' . $env->value);
+ // }
+ // $envs_base64 = base64_encode($envs->implode("\n"));
+ // $this->execute_remote_command(
+ // [
+ // executeInDocker($this->deployment_uuid, "echo '$envs_base64' | base64 -d > $this->workdir/.env")
+ // ],
+ // );
+ // }
private function deploy_simple_dockerfile()
{
$dockerfile_base64 = base64_encode($this->application->dockerfile);
@@ -406,7 +468,12 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->generate_build_env_variables();
$this->add_build_env_variables_to_dockerfile();
$this->build_image();
- $this->rolling_update();
+ // if ($this->application->additional_destinations) {
+ // $this->push_to_docker_registry();
+ // $this->deploy_to_additional_destinations();
+ // } else {
+ $this->rolling_update();
+ // }
}
private function deploy_nixpacks_buildpack()
{
@@ -420,12 +487,10 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->set_base_dir();
$this->generate_image_names();
if (!$this->force_rebuild) {
- $this->execute_remote_command([
- "docker images -q {$this->production_image_name} 2>/dev/null", "hidden" => true, "save" => "local_image_found"
- ]);
- if (Str::of($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) {
+ $this->check_image_locally_or_remotely();
+ if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) {
$this->execute_remote_command([
- "echo 'No configuration changed & Docker Image found locally with the same Git Commit SHA {$this->application->uuid}:{$this->commit}. Build step skipped.'",
+ "echo 'No configuration changed & image found ({$this->production_image_name}) with the same Git Commit SHA. Build step skipped.'",
]);
$this->generate_compose_file();
$this->rolling_update();
@@ -468,12 +533,18 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
{
if (count($this->application->ports_mappings_array) > 0) {
$this->execute_remote_command(
+ [
+ "echo '\n----------------------------------------'",
+ ],
["echo -n 'Application has ports mapped to the host system, rolling update is not supported.'"],
);
$this->stop_running_container(force: true);
$this->start_by_compose_file();
} else {
$this->execute_remote_command(
+ [
+ "echo '\n----------------------------------------'",
+ ],
["echo -n 'Rolling update started.'"],
);
$this->start_by_compose_file();
@@ -489,10 +560,10 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
}
// ray('New container name: ', $this->container_name);
if ($this->container_name) {
- $counter = 0;
+ $counter = 1;
$this->execute_remote_command(
[
- "echo 'Waiting for healthcheck to pass on the new version of your application.'"
+ "echo 'Waiting for healthcheck to pass on the new container.'"
]
);
if ($this->full_healthcheck_url) {
@@ -504,9 +575,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
}
while ($counter < $this->application->health_check_retries) {
$this->execute_remote_command(
- [
- "echo 'Attempt {$counter} of {$this->application->health_check_retries}'"
- ],
[
"docker inspect --format='{{json .State.Health.Status}}' {$this->container_name}",
"hidden" => true,
@@ -516,17 +584,17 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
);
$this->execute_remote_command(
[
- "echo 'New version healthcheck status: {$this->saved_outputs->get('health_check')}'"
+ "echo 'Attempt {$counter} of {$this->application->health_check_retries} | Healthcheck status: {$this->saved_outputs->get('health_check')}'"
],
);
if (Str::of($this->saved_outputs->get('health_check'))->contains('healthy')) {
$this->newVersionIsHealthy = true;
+ $this->application->update(['status' => 'running']);
$this->execute_remote_command(
[
- "echo 'Rolling update completed.'"
+ "echo 'New container is healthy.'"
],
);
- $this->application->update(['status' => 'running']);
break;
}
$counter++;
@@ -563,12 +631,15 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function prepare_builder_image()
{
$helperImage = config('coolify.helper_image');
+ // Get user home directory
+ $this->serverUserHomeDir = instant_remote_process(["echo \$HOME"], $this->server);
+ $this->dockerConfigFileExists = instant_remote_process(["test -f {$this->serverUserHomeDir}/.docker/config.json && echo 'OK' || echo 'NOK'"], $this->server);
+
if ($this->dockerConfigFileExists === 'OK') {
$runCommand = "docker run -d --network {$this->destination->network} -v /:/host --name {$this->deployment_uuid} --rm -v {$this->serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
} else {
$runCommand = "docker run -d --network {$this->destination->network} -v /:/host --name {$this->deployment_uuid} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
}
-
$this->execute_remote_command(
[
"echo -n 'Preparing container with helper image: $helperImage.'",
@@ -582,7 +653,31 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
],
);
}
-
+ private function deploy_to_additional_destinations()
+ {
+ $destination_ids = collect(str($this->application->additional_destinations)->explode(','));
+ foreach ($destination_ids as $destination_id) {
+ $destination = StandaloneDocker::find($destination_id);
+ $server = $destination->server;
+ if ($server->team_id !== $this->mainServer->team_id) {
+ $this->execute_remote_command(
+ [
+ "echo -n 'Skipping deployment to {$server->name}. Not in the same team?!'",
+ ],
+ );
+ continue;
+ }
+ $this->server = $server;
+ $this->execute_remote_command(
+ [
+ "echo -n 'Deploying to {$this->server->name}.'",
+ ],
+ );
+ $this->prepare_builder_image();
+ $this->generate_image_names();
+ $this->rolling_update();
+ }
+ }
private function set_base_dir()
{
$this->execute_remote_command(
@@ -631,6 +726,9 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
{
$importCommands = $this->generate_git_import_commands();
$this->execute_remote_command(
+ [
+ "echo '\n----------------------------------------'",
+ ],
[
"echo -n 'Importing {$this->customRepository}:{$this->application->git_branch} (commit sha {$this->application->git_commit_sha}) to {$this->basedir}. '"
],
@@ -678,7 +776,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->fullRepoUrl = $this->customRepository;
$private_key = data_get($this->application, 'private_key.private_key');
if (is_null($private_key)) {
- throw new Exception('Private key not found. Please add a private key to the application and try again.');
+ throw new RuntimeException('Private key not found. Please add a private key to the application and try again.');
}
$private_key = base64_encode($private_key);
$git_clone_command_base = "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" {$git_clone_command} {$this->customRepository} {$this->basedir}";
@@ -737,16 +835,10 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function generate_nixpacks_confs()
{
-
- $this->execute_remote_command(
- [
- "echo -n 'Generating nixpacks configuration.'",
- ]
- );
$nixpacks_command = $this->nixpacks_build_cmd();
$this->execute_remote_command(
[
- "echo -n Running: $nixpacks_command",
+ "echo -n 'Generating nixpacks configuration with: $nixpacks_command'",
],
[executeInDocker($this->deployment_uuid, $nixpacks_command)],
[executeInDocker($this->deployment_uuid, "cp {$this->workdir}/.nixpacks/Dockerfile {$this->workdir}/Dockerfile")],
@@ -874,6 +966,26 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
]
];
}
+ if ($this->application->settings->is_gpu_enabled) {
+ ray('asd');
+ $docker_compose['services'][$this->container_name]['deploy']['resources']['reservations']['devices'] = [
+ [
+ 'driver' => data_get($this->application, 'settings.gpu_driver', 'nvidia'),
+ 'capabilities' => ['gpu'],
+ 'options' => data_get($this->application, 'settings.gpu_options', [])
+ ]
+ ];
+ if (data_get($this->application, 'settings.gpu_count')) {
+ $count = data_get($this->application, 'settings.gpu_count');
+ if ($count === 'all') {
+ $docker_compose['services'][$this->container_name]['deploy']['resources']['reservations']['devices'][0]['count'] = $count;
+ } else {
+ $docker_compose['services'][$this->container_name]['deploy']['resources']['reservations']['devices'][0]['count'] = (int) $count;
+ }
+ } else if (data_get($this->application, 'settings.gpu_device_ids')) {
+ $docker_compose['services'][$this->container_name]['deploy']['resources']['reservations']['devices'][0]['ids'] = data_get($this->application, 'settings.gpu_device_ids');
+ }
+ }
if ($this->application->isHealthcheckDisabled()) {
data_forget($docker_compose, 'services.' . $this->container_name . '.healthcheck');
}
@@ -1000,9 +1112,12 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
"echo -n 'Static deployment. Copying static assets to the image.'",
]);
} else {
- $this->execute_remote_command([
- "echo -n 'Building docker image for your application. To check the current progress, click on Show Debug Logs.'",
- ]);
+ $this->execute_remote_command(
+ [
+ "echo -n 'Building docker image started.'",
+ ],
+ ["echo -n 'To check the current progress, click on Show Debug Logs.'"]
+ );
}
if ($this->application->settings->is_static || $this->application->build_pack === 'static') {
@@ -1084,12 +1199,14 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
]);
}
}
+ $this->execute_remote_command([
+ "echo -n 'Building docker image completed.'",
+ ]);
}
private function stop_running_container(bool $force = false)
{
- $this->execute_remote_command(["echo -n 'Removing old version of your application.'"]);
-
+ $this->execute_remote_command(["echo -n 'Removing old container.'"]);
if ($this->newVersionIsHealthy || $force) {
$containers = getCurrentApplicationContainerStatus($this->server, $this->application->id, $this->pull_request_id);
if ($this->pull_request_id !== 0) {
@@ -1107,9 +1224,14 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
[executeInDocker($this->deployment_uuid, "docker rm -f $containerName >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true],
);
});
+ $this->execute_remote_command(
+ [
+ "echo 'Rolling update completed.'"
+ ],
+ );
} else {
$this->execute_remote_command(
- ["echo -n 'New version is not healthy, rolling back to the old version.'"],
+ ["echo -n 'New container is not healthy, rolling back to the old container.'"],
[executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true],
);
}
@@ -1121,12 +1243,10 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$this->execute_remote_command(
["echo -n 'Pulling latest images from the registry.'"],
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} pull"), "hidden" => true],
- ["echo -n 'Starting application (could take a while).'"],
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true],
);
} else {
$this->execute_remote_command(
- ["echo -n 'Starting application (could take a while).'"],
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true],
);
}
@@ -1185,9 +1305,9 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
public function failed(Throwable $exception): void
{
$this->execute_remote_command(
- ["echo 'Oops something is not okay, are you okay? 😢'"],
- ["echo '{$exception->getMessage()}'"],
- ["echo -n 'Deployment failed. Removing the new version of your application.'"],
+ ["echo 'Oops something is not okay, are you okay? 😢'", 'type' => 'err'],
+ ["echo '{$exception->getMessage()}'", 'type' => 'err'],
+ ["echo -n 'Deployment failed. Removing the new version of your application.'", 'type' => 'err'],
[executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true]
);
diff --git a/app/Jobs/ApplicationRestartJob.php b/app/Jobs/ApplicationRestartJob.php
new file mode 100644
index 000000000..3216baa5a
--- /dev/null
+++ b/app/Jobs/ApplicationRestartJob.php
@@ -0,0 +1,28 @@
+applicationDeploymentQueueId = $applicationDeploymentQueueId;
+ }
+ public function handle() {
+ ray('Restarting application');
+ }
+}
diff --git a/app/Jobs/ContainerStatusJob.php b/app/Jobs/ContainerStatusJob.php
index 34678b18a..a45bebf8e 100644
--- a/app/Jobs/ContainerStatusJob.php
+++ b/app/Jobs/ContainerStatusJob.php
@@ -37,7 +37,7 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
public function handle(): void
{
- ray("checking container statuses for {$this->server->id}");
+ // ray("checking container statuses for {$this->server->id}");
try {
if (!$this->server->isServerReady()) {
return;
diff --git a/app/Jobs/MultipleApplicationDeploymentJob.php b/app/Jobs/MultipleApplicationDeploymentJob.php
new file mode 100644
index 000000000..32c98d3b0
--- /dev/null
+++ b/app/Jobs/MultipleApplicationDeploymentJob.php
@@ -0,0 +1,1165 @@
+clearScreen();
+ $this->application_deployment_queue = ApplicationDeploymentQueue::find($application_deployment_queue_id);
+ $this->log_model = $this->application_deployment_queue;
+ $this->application = Application::find($this->application_deployment_queue->application_id);
+ $this->build_pack = data_get($this->application, 'build_pack');
+
+ $this->application_deployment_queue_id = $application_deployment_queue_id;
+ $this->deployment_uuid = $this->application_deployment_queue->deployment_uuid;
+ $this->commit = $this->application_deployment_queue->commit;
+ $this->force_rebuild = $this->application_deployment_queue->force_rebuild;
+ $this->restart_only = $this->application_deployment_queue->restart_only;
+
+ $this->git_type = data_get($this->application_deployment_queue, 'git_type');
+
+ $source = data_get($this->application, 'source');
+ if ($source) {
+ $this->source = $source->getMorphClass()::where('id', $this->application->source->id)->first();
+ }
+ $this->destination = $this->application->destination->getMorphClass()::where('id', $this->application->destination->id)->first();
+ $this->server = $this->mainServer = $this->destination->server;
+ $this->serverUser = $this->server->user;
+ $this->basedir = generateBaseDir($this->deployment_uuid);
+ $this->workdir = "{$this->basedir}" . rtrim($this->application->base_directory, '/');
+ $this->configuration_dir = application_configuration_dir() . "/{$this->application->uuid}";
+ $this->is_debug_enabled = $this->application->settings->is_debug_enabled;
+ $this->saved_outputs = collect();
+ $this->container_name = generateApplicationContainerName($this->application, 0);
+ }
+
+ public function handle(): void
+ {
+ savePrivateKeyToFs($this->server);
+ $this->application_deployment_queue->update([
+ 'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
+ ]);
+
+ $this->addHosts = generateHostIpMapping($this->server, $this->destination->network);
+
+ if ($this->application->dockerfile_target_build) {
+ $this->buildTarget = " --target {$this->application->dockerfile_target_build} ";
+ }
+
+ // Check custom port
+ preg_match('/(?<=:)\d+(?=\/)/', $this->application->git_repository, $matches);
+ if (count($matches) === 1) {
+ $this->customPort = $matches[0];
+ $gitHost = str($this->application->git_repository)->before(':');
+ $gitRepo = str($this->application->git_repository)->after('/');
+ $this->customRepository = "$gitHost:$gitRepo";
+ } else {
+ $this->customRepository = $this->application->git_repository;
+ }
+ try {
+ if ($this->application->isMultipleServerDeployment()) {
+ if ($this->application->build_pack === 'dockerimage') {
+ $this->dockerImage = $this->application->docker_registry_image_name;
+ $this->dockerImageTag = $this->application->docker_registry_image_tag;
+ $this->execute_remote_command(
+ [
+ "echo 'Starting deployment of {$this->dockerImage}:{$this->dockerImageTag}.'"
+ ],
+ );
+ $this->production_image_name = Str::lower("{$this->dockerImage}:{$this->dockerImageTag}");
+ ray(prepareHelperContainer($this->server, $this->deployment_uuid));
+ $this->execute_remote_command(
+ [prepareHelperContainer($this->server, $this->deployment_uuid)]
+ );
+ }
+ } else {
+ throw new RuntimeException('Missing configuration for multiple server deployment.');
+ }
+ // if ($this->restart_only && $this->application->build_pack !== 'dockerimage') {
+ // $this->just_restart();
+ // if ($this->server->isProxyShouldRun()) {
+ // dispatch(new ContainerStatusJob($this->server));
+ // }
+ // $this->next(ApplicationDeploymentStatus::FINISHED->value);
+ // $this->application->isConfigurationChanged(true);
+ // return;
+ // } else if ($this->application->dockerfile) {
+ // $this->deploy_simple_dockerfile();
+ // } else if ($this->application->build_pack === 'dockerimage') {
+ // $this->deploy_dockerimage_buildpack();
+ // } else if ($this->application->build_pack === 'dockerfile') {
+ // $this->deploy_dockerfile_buildpack();
+ // } else if ($this->application->build_pack === 'static') {
+ // $this->deploy_static_buildpack();
+ // } else {
+ // $this->deploy_nixpacks_buildpack();
+ // }
+ // if ($this->server->isProxyShouldRun()) {
+ // dispatch(new ContainerStatusJob($this->server));
+ // }
+ // if ($this->application->docker_registry_image_name) {
+ // $this->push_to_docker_registry();
+ // }
+ // $this->next(ApplicationDeploymentStatus::FINISHED->value);
+ // $this->application->isConfigurationChanged(true);
+ } catch (Exception $e) {
+ $this->fail($e);
+ throw $e;
+ } finally {
+ // if (isset($this->docker_compose_base64)) {
+ // $readme = generate_readme_file($this->application->name, $this->application_deployment_queue->updated_at);
+ // $composeFileName = "$this->configuration_dir/docker-compose.yml";
+ // $this->execute_remote_command(
+ // [
+ // "mkdir -p $this->configuration_dir"
+ // ],
+ // [
+ // "echo '{$this->docker_compose_base64}' | base64 -d > $composeFileName",
+ // ],
+ // [
+ // "echo '{$readme}' > $this->configuration_dir/README.md",
+ // ]
+ // );
+ // }
+ // $this->execute_remote_command(
+ // [
+ // "docker rm -f {$this->deployment_uuid} >/dev/null 2>&1",
+ // "hidden" => true,
+ // "ignore_errors" => true,
+ // ]
+ // );
+ // $this->execute_remote_command(
+ // [
+ // "docker image prune -f >/dev/null 2>&1",
+ // "hidden" => true,
+ // "ignore_errors" => true,
+ // ]
+ // );
+ }
+ }
+ private function push_to_docker_registry()
+ {
+ try {
+ instant_remote_process(["docker images --format '{{json .}}' {$this->production_image_name}"], $this->server);
+ $this->execute_remote_command(
+ [
+ "echo '\n----------------------------------------'",
+ ],
+ ["echo -n 'Pushing image to docker registry ({$this->production_image_name}).'"],
+ [
+ executeInDocker($this->deployment_uuid, "docker push {$this->production_image_name}"), 'hidden' => true
+ ],
+ );
+ if ($this->application->docker_registry_image_tag) {
+ // Tag image with latest
+ $this->execute_remote_command(
+ ['echo -n "Tagging and pushing image with latest tag."'],
+ [
+ executeInDocker($this->deployment_uuid, "docker tag {$this->production_image_name} {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"), 'ignore_errors' => true, 'hidden' => true
+ ],
+ [
+ executeInDocker($this->deployment_uuid, "docker push {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"), 'ignore_errors' => true, 'hidden' => true
+ ],
+ );
+ }
+ $this->execute_remote_command([
+ "echo -n 'Image pushed to docker registry.'"
+ ]);
+ } catch (Exception $e) {
+ $this->execute_remote_command(
+ ["echo -n 'Failed to push image to docker registry. Please check debug logs for more information.'"],
+ );
+ ray($e);
+ }
+ }
+ // private function deploy_docker_compose()
+ // {
+ // $dockercompose_base64 = base64_encode($this->application->dockercompose);
+ // $this->execute_remote_command(
+ // [
+ // "echo 'Starting deployment of {$this->application->name}.'"
+ // ],
+ // );
+ // $this->prepare_builder_image();
+ // $this->execute_remote_command(
+ // [
+ // executeInDocker($this->deployment_uuid, "echo '$dockercompose_base64' | base64 -d > $this->workdir/docker-compose.yaml")
+ // ],
+ // );
+ // $this->build_image_name = Str::lower("{$this->customRepository}:build");
+ // $this->production_image_name = Str::lower("{$this->application->uuid}:latest");
+ // $this->save_environment_variables();
+ // $containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id);
+ // ray($containers);
+ // if ($containers->count() > 0) {
+ // foreach ($containers as $container) {
+ // $containerName = data_get($container, 'Names');
+ // if ($containerName) {
+ // instant_remote_process(
+ // ["docker rm -f {$containerName}"],
+ // $this->application->destination->server
+ // );
+ // }
+ // }
+ // }
+
+ // $this->execute_remote_command(
+ // ["echo -n 'Starting services (could take a while)...'"],
+ // [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up -d"), "hidden" => true],
+ // );
+ // }
+ private function generate_image_names()
+ {
+ if ($this->application->dockerfile) {
+ if ($this->application->docker_registry_image_name) {
+ $this->build_image_name = Str::lower("{$this->application->docker_registry_image_name}:build");
+ $this->production_image_name = Str::lower("{$this->application->docker_registry_image_name}:latest");
+ } else {
+ $this->build_image_name = Str::lower("{$this->application->uuid}:build");
+ $this->production_image_name = Str::lower("{$this->application->uuid}:latest");
+ }
+ } else if ($this->application->build_pack === 'dockerimage') {
+ $this->production_image_name = Str::lower("{$this->dockerImage}:{$this->dockerImageTag}");
+ } else {
+ $this->dockerImageTag = str($this->commit)->substr(0, 128);
+ if ($this->application->docker_registry_image_name) {
+ $this->build_image_name = Str::lower("{$this->application->docker_registry_image_name}:{$this->dockerImageTag}-build");
+ $this->production_image_name = Str::lower("{$this->application->docker_registry_image_name}:{$this->dockerImageTag}");
+ } else {
+ $this->build_image_name = Str::lower("{$this->application->uuid}:{$this->dockerImageTag}-build");
+ $this->production_image_name = Str::lower("{$this->application->uuid}:{$this->dockerImageTag}");
+ }
+ }
+ }
+ private function just_restart()
+ {
+ $this->execute_remote_command(
+ [
+ "echo 'Starting deployment of {$this->customRepository}:{$this->application->git_branch}.'"
+ ],
+ );
+ $this->prepare_builder_image();
+ $this->check_git_if_build_needed();
+ $this->set_base_dir();
+ $this->generate_image_names();
+ $this->check_image_locally_or_remotely();
+ if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty()) {
+ $this->generate_compose_file();
+ $this->rolling_update();
+ return;
+ }
+ throw new RuntimeException('Cannot find image anywhere. Please redeploy the application.');
+ }
+ private function check_image_locally_or_remotely()
+ {
+ $this->execute_remote_command([
+ "docker images -q {$this->production_image_name} 2>/dev/null", "hidden" => true, "save" => "local_image_found"
+ ]);
+ if (str($this->saved_outputs->get('local_image_found'))->isEmpty() && $this->application->docker_registry_image_name) {
+ $this->execute_remote_command([
+ "docker pull {$this->production_image_name} 2>/dev/null", "ignore_errors" => true, "hidden" => true
+ ]);
+ $this->execute_remote_command([
+ "docker images -q {$this->production_image_name} 2>/dev/null", "hidden" => true, "save" => "local_image_found"
+ ]);
+ }
+ }
+ // private function save_environment_variables()
+ // {
+ // $envs = collect([]);
+ // foreach ($this->application->environment_variables as $env) {
+ // $envs->push($env->key . '=' . $env->value);
+ // }
+ // $envs_base64 = base64_encode($envs->implode("\n"));
+ // $this->execute_remote_command(
+ // [
+ // executeInDocker($this->deployment_uuid, "echo '$envs_base64' | base64 -d > $this->workdir/.env")
+ // ],
+ // );
+ // }
+ private function deploy_simple_dockerfile()
+ {
+ $dockerfile_base64 = base64_encode($this->application->dockerfile);
+ $this->execute_remote_command(
+ [
+ "echo 'Starting deployment of {$this->application->name}.'"
+ ],
+ );
+ $this->prepare_builder_image();
+ $this->execute_remote_command(
+ [
+ executeInDocker($this->deployment_uuid, "echo '$dockerfile_base64' | base64 -d > $this->workdir$this->dockerfile_location")
+ ],
+ );
+ $this->generate_image_names();
+ $this->generate_compose_file();
+ $this->generate_build_env_variables();
+ $this->add_build_env_variables_to_dockerfile();
+ $this->build_image();
+ $this->rolling_update();
+ }
+
+ private function deploy_dockerimage_buildpack()
+ {
+ // $this->dockerImage = $this->application->docker_registry_image_name;
+ // $this->dockerImageTag = $this->application->docker_registry_image_tag;
+ // ray("echo 'Starting deployment of {$this->dockerImage}:{$this->dockerImageTag}.'");
+ // $this->execute_remote_command(
+ // [
+ // "echo 'Starting deployment of {$this->dockerImage}:{$this->dockerImageTag}.'"
+ // ],
+ // );
+ // $this->generate_image_names();
+ // $this->prepare_builder_image();
+ $this->generate_compose_file();
+ $this->rolling_update();
+ }
+
+ private function deploy_dockerfile_buildpack()
+ {
+ if (data_get($this->application, 'dockerfile_location')) {
+ $this->dockerfile_location = $this->application->dockerfile_location;
+ }
+ $this->execute_remote_command(
+ [
+ "echo 'Starting deployment of {$this->customRepository}:{$this->application->git_branch}.'"
+ ],
+ );
+ $this->prepare_builder_image();
+ $this->check_git_if_build_needed();
+ $this->clone_repository();
+ $this->set_base_dir();
+ $this->generate_image_names();
+ $this->cleanup_git();
+ $this->generate_compose_file();
+ $this->generate_build_env_variables();
+ $this->add_build_env_variables_to_dockerfile();
+ $this->build_image();
+ // if ($this->application->additional_destinations) {
+ // $this->push_to_docker_registry();
+ // $this->deploy_to_additional_destinations();
+ // } else {
+ $this->rolling_update();
+ // }
+ }
+ private function deploy_nixpacks_buildpack()
+ {
+ $this->execute_remote_command(
+ [
+ "echo 'Starting deployment of {$this->customRepository}:{$this->application->git_branch}.'"
+ ],
+ );
+ $this->prepare_builder_image();
+ $this->check_git_if_build_needed();
+ $this->set_base_dir();
+ $this->generate_image_names();
+ if (!$this->force_rebuild) {
+ $this->check_image_locally_or_remotely();
+ if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) {
+ $this->execute_remote_command([
+ "echo 'No configuration changed & image found ({$this->production_image_name}) with the same Git Commit SHA. Build step skipped.'",
+ ]);
+ $this->generate_compose_file();
+ $this->rolling_update();
+ return;
+ }
+ if ($this->application->isConfigurationChanged()) {
+ $this->execute_remote_command([
+ "echo 'Configuration changed. Rebuilding image.'",
+ ]);
+ }
+ }
+ $this->clone_repository();
+ $this->cleanup_git();
+ $this->generate_nixpacks_confs();
+ $this->generate_compose_file();
+ $this->generate_build_env_variables();
+ $this->add_build_env_variables_to_dockerfile();
+ $this->build_image();
+ $this->rolling_update();
+ }
+ private function deploy_static_buildpack()
+ {
+ $this->execute_remote_command(
+ [
+ "echo 'Starting deployment of {$this->customRepository}:{$this->application->git_branch}.'"
+ ],
+ );
+ $this->prepare_builder_image();
+ $this->check_git_if_build_needed();
+ $this->set_base_dir();
+ $this->generate_image_names();
+ $this->clone_repository();
+ $this->cleanup_git();
+ $this->build_image();
+ $this->generate_compose_file();
+ $this->rolling_update();
+ }
+
+ private function rolling_update()
+ {
+ if (count($this->application->ports_mappings_array) > 0) {
+ $this->execute_remote_command(
+ [
+ "echo '\n----------------------------------------'",
+ ],
+ ["echo -n 'Application has ports mapped to the host system, rolling update is not supported.'"],
+ );
+ $this->stop_running_container(force: true);
+ $this->start_by_compose_file();
+ } else {
+ $this->execute_remote_command(
+ [
+ "echo '\n----------------------------------------'",
+ ],
+ ["echo -n 'Rolling update started.'"],
+ );
+ $this->start_by_compose_file();
+ $this->health_check();
+ $this->stop_running_container();
+ }
+ }
+ private function health_check()
+ {
+ if ($this->application->isHealthcheckDisabled()) {
+ $this->newVersionIsHealthy = true;
+ return;
+ }
+ // ray('New container name: ', $this->container_name);
+ if ($this->container_name) {
+ $counter = 1;
+ $this->execute_remote_command(
+ [
+ "echo 'Waiting for healthcheck to pass on the new container.'"
+ ]
+ );
+ if ($this->full_healthcheck_url) {
+ $this->execute_remote_command(
+ [
+ "echo 'Healthcheck URL (inside the container): {$this->full_healthcheck_url}'"
+ ]
+ );
+ }
+ while ($counter < $this->application->health_check_retries) {
+ $this->execute_remote_command(
+ [
+ "docker inspect --format='{{json .State.Health.Status}}' {$this->container_name}",
+ "hidden" => true,
+ "save" => "health_check"
+ ],
+
+ );
+ $this->execute_remote_command(
+ [
+ "echo 'Attempt {$counter} of {$this->application->health_check_retries} | Healthcheck status: {$this->saved_outputs->get('health_check')}'"
+ ],
+ );
+ if (Str::of($this->saved_outputs->get('health_check'))->contains('healthy')) {
+ $this->newVersionIsHealthy = true;
+ $this->application->update(['status' => 'running']);
+ $this->execute_remote_command(
+ [
+ "echo 'New container is healthy.'"
+ ],
+ );
+ break;
+ }
+ $counter++;
+ sleep($this->application->health_check_interval);
+ }
+ }
+ }
+
+ private function prepare_builder_image()
+ {
+ $helperImage = config('coolify.helper_image');
+ // Get user home directory
+ $this->serverUserHomeDir = instant_remote_process(["echo \$HOME"], $this->server);
+ $this->dockerConfigFileExists = instant_remote_process(["test -f {$this->serverUserHomeDir}/.docker/config.json && echo 'OK' || echo 'NOK'"], $this->server);
+
+ if ($this->dockerConfigFileExists === 'OK') {
+ $runCommand = "docker run -d --network {$this->destination->network} -v /:/host --name {$this->deployment_uuid} --rm -v {$this->serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
+ } else {
+ $runCommand = "docker run -d --network {$this->destination->network} -v /:/host --name {$this->deployment_uuid} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
+ }
+ $this->execute_remote_command(
+ [
+ "echo -n 'Preparing container with helper image: $helperImage.'",
+ ],
+ [
+ $runCommand,
+ "hidden" => true,
+ ],
+ [
+ "command" => executeInDocker($this->deployment_uuid, "mkdir -p {$this->basedir}")
+ ],
+ );
+ }
+ private function deploy_to_additional_destinations()
+ {
+ $destination_ids = collect(str($this->application->additional_destinations)->explode(','));
+ foreach ($destination_ids as $destination_id) {
+ $destination = StandaloneDocker::find($destination_id);
+ $server = $destination->server;
+ if ($server->team_id !== $this->mainServer->team_id) {
+ $this->execute_remote_command(
+ [
+ "echo -n 'Skipping deployment to {$server->name}. Not in the same team?!'",
+ ],
+ );
+ continue;
+ }
+ $this->server = $server;
+ $this->execute_remote_command(
+ [
+ "echo -n 'Deploying to {$this->server->name}.'",
+ ],
+ );
+ $this->prepare_builder_image();
+ $this->generate_image_names();
+ $this->rolling_update();
+ }
+ }
+ private function set_base_dir()
+ {
+ $this->execute_remote_command(
+ [
+ "echo -n 'Setting base directory to {$this->workdir}.'"
+ ],
+ );
+ }
+ private function check_git_if_build_needed()
+ {
+ $this->generate_git_import_commands();
+ $private_key = data_get($this->application, 'private_key.private_key');
+ if ($private_key) {
+ $private_key = base64_encode($private_key);
+ $this->execute_remote_command(
+ [
+ executeInDocker($this->deployment_uuid, "mkdir -p /root/.ssh")
+ ],
+ [
+ executeInDocker($this->deployment_uuid, "echo '{$private_key}' | base64 -d > /root/.ssh/id_rsa")
+ ],
+ [
+ executeInDocker($this->deployment_uuid, "chmod 600 /root/.ssh/id_rsa")
+ ],
+ [
+ executeInDocker($this->deployment_uuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git ls-remote {$this->fullRepoUrl} {$this->branch}"),
+ "hidden" => true,
+ "save" => "git_commit_sha"
+ ],
+ );
+ } else {
+ $this->execute_remote_command(
+ [
+ executeInDocker($this->deployment_uuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git ls-remote {$this->fullRepoUrl} {$this->branch}"),
+ "hidden" => true,
+ "save" => "git_commit_sha"
+ ],
+ );
+ }
+
+ if ($this->saved_outputs->get('git_commit_sha')) {
+ $this->commit = $this->saved_outputs->get('git_commit_sha')->before("\t");
+ }
+ }
+ private function clone_repository()
+ {
+ $importCommands = $this->generate_git_import_commands();
+ $this->execute_remote_command(
+ [
+ "echo '\n----------------------------------------'",
+ ],
+ [
+ "echo -n 'Importing {$this->customRepository}:{$this->application->git_branch} (commit sha {$this->application->git_commit_sha}) to {$this->basedir}. '"
+ ],
+ [
+ $importCommands, "hidden" => true
+ ]
+ );
+ }
+
+ private function generate_git_import_commands()
+ {
+ $this->branch = $this->application->git_branch;
+ $commands = collect([]);
+ $git_clone_command = "git clone -q -b {$this->application->git_branch}";
+
+ if ($this->application->deploymentType() === 'source') {
+ $source_html_url = data_get($this->application, 'source.html_url');
+ $url = parse_url(filter_var($source_html_url, FILTER_SANITIZE_URL));
+ $source_html_url_host = $url['host'];
+ $source_html_url_scheme = $url['scheme'];
+
+ if ($this->source->getMorphClass() == 'App\Models\GithubApp') {
+ if ($this->source->is_public) {
+ $this->fullRepoUrl = "{$this->source->html_url}/{$this->customRepository}";
+ $git_clone_command = "{$git_clone_command} {$this->source->html_url}/{$this->customRepository} {$this->basedir}";
+ $git_clone_command = $this->set_git_import_settings($git_clone_command);
+
+ $commands->push(executeInDocker($this->deployment_uuid, $git_clone_command));
+ } else {
+ $github_access_token = generate_github_installation_token($this->source);
+ $commands->push(executeInDocker($this->deployment_uuid, "git clone -q -b {$this->application->git_branch} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$this->customRepository}.git {$this->basedir}"));
+ $this->fullRepoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$this->customRepository}.git";
+ }
+ return $commands->implode(' && ');
+ }
+ }
+ if ($this->application->deploymentType() === 'deploy_key') {
+ $this->fullRepoUrl = $this->customRepository;
+ $private_key = data_get($this->application, 'private_key.private_key');
+ if (is_null($private_key)) {
+ throw new RuntimeException('Private key not found. Please add a private key to the application and try again.');
+ }
+ $private_key = base64_encode($private_key);
+ $git_clone_command_base = "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" {$git_clone_command} {$this->customRepository} {$this->basedir}";
+ $git_clone_command = $this->set_git_import_settings($git_clone_command_base);
+ $commands = collect([
+ executeInDocker($this->deployment_uuid, "mkdir -p /root/.ssh"),
+ executeInDocker($this->deployment_uuid, "echo '{$private_key}' | base64 -d > /root/.ssh/id_rsa"),
+ executeInDocker($this->deployment_uuid, "chmod 600 /root/.ssh/id_rsa"),
+ ]);
+
+ $commands->push(executeInDocker($this->deployment_uuid, $git_clone_command));
+ return $commands->implode(' && ');
+ }
+ if ($this->application->deploymentType() === 'other') {
+ $this->fullRepoUrl = $this->customRepository;
+ $git_clone_command = "{$git_clone_command} {$this->customRepository} {$this->basedir}";
+ $git_clone_command = $this->set_git_import_settings($git_clone_command);
+ $commands->push(executeInDocker($this->deployment_uuid, $git_clone_command));
+ return $commands->implode(' && ');
+ }
+ }
+
+ private function set_git_import_settings($git_clone_command)
+ {
+ if ($this->application->git_commit_sha !== 'HEAD') {
+ $git_clone_command = "{$git_clone_command} && cd {$this->basedir} && git -c advice.detachedHead=false checkout {$this->application->git_commit_sha} >/dev/null 2>&1";
+ }
+ if ($this->application->settings->is_git_submodules_enabled) {
+ $git_clone_command = "{$git_clone_command} && cd {$this->basedir} && git submodule update --init --recursive";
+ }
+ if ($this->application->settings->is_git_lfs_enabled) {
+ $git_clone_command = "{$git_clone_command} && cd {$this->basedir} && git lfs pull";
+ }
+ return $git_clone_command;
+ }
+
+ private function cleanup_git()
+ {
+ $this->execute_remote_command(
+ [executeInDocker($this->deployment_uuid, "rm -fr {$this->basedir}/.git")],
+ );
+ }
+
+ private function generate_nixpacks_confs()
+ {
+ $nixpacks_command = $this->nixpacks_build_cmd();
+ $this->execute_remote_command(
+ [
+ "echo -n 'Generating nixpacks configuration with: $nixpacks_command'",
+ ],
+ [executeInDocker($this->deployment_uuid, $nixpacks_command)],
+ [executeInDocker($this->deployment_uuid, "cp {$this->workdir}/.nixpacks/Dockerfile {$this->workdir}/Dockerfile")],
+ [executeInDocker($this->deployment_uuid, "rm -f {$this->workdir}/.nixpacks/Dockerfile")]
+ );
+ }
+
+ private function nixpacks_build_cmd()
+ {
+ $this->generate_env_variables();
+ $nixpacks_command = "nixpacks build --cache-key '{$this->application->uuid}' -o {$this->workdir} {$this->env_args} --no-error-without-start";
+ if ($this->application->build_command) {
+ $nixpacks_command .= " --build-cmd \"{$this->application->build_command}\"";
+ }
+ if ($this->application->start_command) {
+ $nixpacks_command .= " --start-cmd \"{$this->application->start_command}\"";
+ }
+ if ($this->application->install_command) {
+ $nixpacks_command .= " --install-cmd \"{$this->application->install_command}\"";
+ }
+ $nixpacks_command .= " {$this->workdir}";
+ return $nixpacks_command;
+ }
+
+ private function generate_env_variables()
+ {
+ $this->env_args = collect([]);
+ foreach ($this->application->nixpacks_environment_variables_preview as $env) {
+ $this->env_args->push("--env {$env->key}={$env->value}");
+ }
+ $this->env_args = $this->env_args->implode(' ');
+ }
+
+ private function generate_compose_file()
+ {
+ $ports = $this->application->settings->is_static ? [80] : $this->application->ports_exposes_array;
+
+ $persistent_storages = $this->generate_local_persistent_volumes();
+ $volume_names = $this->generate_local_persistent_volumes_only_volume_names();
+ $environment_variables = $this->generate_environment_variables($ports);
+
+ if (data_get($this->application, 'custom_labels')) {
+ $labels = collect(str($this->application->custom_labels)->explode(','));
+ $labels = $labels->filter(function ($value, $key) {
+ return !Str::startsWith($value, 'coolify.');
+ });
+ $this->application->custom_labels = $labels->implode(',');
+ $this->application->save();
+ } else {
+ $labels = collect(generateLabelsApplication($this->application, $this->preview));
+ }
+
+ $labels = $labels->merge(defaultLabels($this->application->id, $this->application->uuid, 0))->toArray();
+ $docker_compose = [
+ 'version' => '3.8',
+ 'services' => [
+ $this->container_name => [
+ 'image' => $this->production_image_name,
+ 'container_name' => $this->container_name,
+ 'restart' => RESTART_MODE,
+ 'environment' => $environment_variables,
+ 'labels' => $labels,
+ 'expose' => $ports,
+ 'networks' => [
+ $this->destination->network,
+ ],
+ 'healthcheck' => [
+ 'test' => [
+ 'CMD-SHELL',
+ $this->generate_healthcheck_commands()
+ ],
+ 'interval' => $this->application->health_check_interval . 's',
+ 'timeout' => $this->application->health_check_timeout . 's',
+ 'retries' => $this->application->health_check_retries,
+ 'start_period' => $this->application->health_check_start_period . 's'
+ ],
+ 'mem_limit' => $this->application->limits_memory,
+ 'memswap_limit' => $this->application->limits_memory_swap,
+ 'mem_swappiness' => $this->application->limits_memory_swappiness,
+ 'mem_reservation' => $this->application->limits_memory_reservation,
+ 'cpus' => (int) $this->application->limits_cpus,
+ 'cpuset' => $this->application->limits_cpuset,
+ 'cpu_shares' => $this->application->limits_cpu_shares,
+ ]
+ ],
+ 'networks' => [
+ $this->destination->network => [
+ 'external' => true,
+ 'name' => $this->destination->network,
+ 'attachable' => true
+ ]
+ ]
+ ];
+ if ($this->server->isLogDrainEnabled() && $this->application->isLogDrainEnabled()) {
+ $docker_compose['services'][$this->container_name]['logging'] = [
+ 'driver' => 'fluentd',
+ 'options' => [
+ 'fluentd-address' => "tcp://127.0.0.1:24224",
+ 'fluentd-async' => "true",
+ 'fluentd-sub-second-precision' => "true",
+ ]
+ ];
+ }
+ if ($this->application->settings->is_gpu_enabled) {
+ ray('asd');
+ $docker_compose['services'][$this->container_name]['deploy']['resources']['reservations']['devices'] = [
+ [
+ 'driver' => data_get($this->application, 'settings.gpu_driver', 'nvidia'),
+ 'capabilities' => ['gpu'],
+ 'options' => data_get($this->application, 'settings.gpu_options', [])
+ ]
+ ];
+ if (data_get($this->application, 'settings.gpu_count')) {
+ $count = data_get($this->application, 'settings.gpu_count');
+ if ($count === 'all') {
+ $docker_compose['services'][$this->container_name]['deploy']['resources']['reservations']['devices'][0]['count'] = $count;
+ } else {
+ $docker_compose['services'][$this->container_name]['deploy']['resources']['reservations']['devices'][0]['count'] = (int) $count;
+ }
+ } else if (data_get($this->application, 'settings.gpu_device_ids')) {
+ $docker_compose['services'][$this->container_name]['deploy']['resources']['reservations']['devices'][0]['ids'] = data_get($this->application, 'settings.gpu_device_ids');
+ }
+ }
+ if ($this->application->isHealthcheckDisabled()) {
+ data_forget($docker_compose, 'services.' . $this->container_name . '.healthcheck');
+ }
+ if (count($persistent_storages) > 0) {
+ $docker_compose['services'][$this->container_name]['volumes'] = $persistent_storages;
+ }
+ if (count($volume_names) > 0) {
+ $docker_compose['volumes'] = $volume_names;
+ }
+ // if ($this->build_pack === 'dockerfile') {
+ // $docker_compose['services'][$this->container_name]['build'] = [
+ // 'context' => $this->workdir,
+ // 'dockerfile' => $this->workdir . $this->dockerfile_location,
+ // ];
+ // }
+ $this->docker_compose = Yaml::dump($docker_compose, 10);
+ $this->docker_compose_base64 = base64_encode($this->docker_compose);
+ $this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d > {$this->workdir}/docker-compose.yml"), "hidden" => true]);
+ }
+
+ private function generate_local_persistent_volumes()
+ {
+ $local_persistent_volumes = [];
+ foreach ($this->application->persistentStorages as $persistentStorage) {
+ $volume_name = $persistentStorage->host_path ?? $persistentStorage->name;
+ $local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
+ }
+ return $local_persistent_volumes;
+ }
+
+ private function generate_local_persistent_volumes_only_volume_names()
+ {
+ $local_persistent_volumes_names = [];
+ foreach ($this->application->persistentStorages as $persistentStorage) {
+ if ($persistentStorage->host_path) {
+ continue;
+ }
+ $name = $persistentStorage->name;
+ $local_persistent_volumes_names[$name] = [
+ 'name' => $name,
+ 'external' => false,
+ ];
+ }
+ return $local_persistent_volumes_names;
+ }
+
+ private function generate_environment_variables($ports)
+ {
+ $environment_variables = collect();
+ foreach ($this->application->runtime_environment_variables_preview as $env) {
+ $environment_variables->push("$env->key=$env->value");
+ }
+ foreach ($this->application->nixpacks_environment_variables_preview as $env) {
+ $environment_variables->push("$env->key=$env->value");
+ }
+ // Add PORT if not exists, use the first port as default
+ if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('PORT'))->isEmpty()) {
+ $environment_variables->push("PORT={$ports[0]}");
+ }
+ return $environment_variables->all();
+ }
+
+ private function generate_healthcheck_commands()
+ {
+ if ($this->application->dockerfile || $this->application->build_pack === 'dockerfile' || $this->application->build_pack === 'dockerimage') {
+ // TODO: disabled HC because there are several ways to hc a simple docker image, hard to figure out a good way. Like some docker images (pocketbase) does not have curl.
+ return 'exit 0';
+ }
+ if (!$this->application->health_check_port) {
+ $health_check_port = $this->application->ports_exposes_array[0];
+ } else {
+ $health_check_port = $this->application->health_check_port;
+ }
+ if ($this->application->health_check_path) {
+ $this->full_healthcheck_url = "{$this->application->health_check_method}: {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}{$this->application->health_check_path}";
+ $generated_healthchecks_commands = [
+ "curl -s -X {$this->application->health_check_method} -f {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}{$this->application->health_check_path} > /dev/null"
+ ];
+ } else {
+ $this->full_healthcheck_url = "{$this->application->health_check_method}: {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}/";
+ $generated_healthchecks_commands = [
+ "curl -s -X {$this->application->health_check_method} -f {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}/"
+ ];
+ }
+ return implode(' ', $generated_healthchecks_commands);
+ }
+ private function pull_latest_image($image)
+ {
+ $this->execute_remote_command(
+ ["echo -n 'Pulling latest image ($image) from the registry.'"],
+
+ [
+ executeInDocker($this->deployment_uuid, "docker pull {$image}"), "hidden" => true
+ ]
+ );
+ }
+ private function build_image()
+ {
+ if ($this->application->build_pack === 'static') {
+ $this->execute_remote_command([
+ "echo -n 'Static deployment. Copying static assets to the image.'",
+ ]);
+ } else {
+ $this->execute_remote_command(
+ [
+ "echo -n 'Building docker image started.'",
+ ],
+ ["echo -n 'To check the current progress, click on Show Debug Logs.'"]
+ );
+ }
+
+ if ($this->application->settings->is_static || $this->application->build_pack === 'static') {
+ if ($this->application->static_image) {
+ $this->pull_latest_image($this->application->static_image);
+ }
+ if ($this->application->build_pack === 'static') {
+ $dockerfile = base64_encode("FROM {$this->application->static_image}
+WORKDIR /usr/share/nginx/html/
+LABEL coolify.deploymentId={$this->deployment_uuid}
+COPY . .
+RUN rm -f /usr/share/nginx/html/nginx.conf
+RUN rm -f /usr/share/nginx/html/Dockerfile
+COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
+ $nginx_config = base64_encode("server {
+ listen 80;
+ listen [::]:80;
+ server_name localhost;
+
+ location / {
+ root /usr/share/nginx/html;
+ index index.html;
+ try_files \$uri \$uri.html \$uri/index.html \$uri/ /index.html =404;
+ }
+
+ error_page 500 502 503 504 /50x.html;
+ location = /50x.html {
+ root /usr/share/nginx/html;
+ }
+ }");
+ } else {
+ $this->execute_remote_command([
+ executeInDocker($this->deployment_uuid, "docker build $this->buildTarget $this->addHosts --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->build_image_name {$this->workdir}"), "hidden" => true
+ ]);
+
+ $dockerfile = base64_encode("FROM {$this->application->static_image}
+WORKDIR /usr/share/nginx/html/
+LABEL coolify.deploymentId={$this->deployment_uuid}
+COPY --from=$this->build_image_name /app/{$this->application->publish_directory} .
+COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
+
+ $nginx_config = base64_encode("server {
+ listen 80;
+ listen [::]:80;
+ server_name localhost;
+
+ location / {
+ root /usr/share/nginx/html;
+ index index.html;
+ try_files \$uri \$uri.html \$uri/index.html \$uri/ /index.html =404;
+ }
+
+ error_page 500 502 503 504 /50x.html;
+ location = /50x.html {
+ root /usr/share/nginx/html;
+ }
+ }");
+ }
+ $this->execute_remote_command(
+ [
+ executeInDocker($this->deployment_uuid, "echo '{$dockerfile}' | base64 -d > {$this->workdir}/Dockerfile")
+ ],
+ [
+ executeInDocker($this->deployment_uuid, "echo '{$nginx_config}' | base64 -d > {$this->workdir}/nginx.conf")
+ ],
+ [
+ executeInDocker($this->deployment_uuid, "docker build $this->addHosts --network host -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true
+ ]
+ );
+ } else {
+ // Pure Dockerfile based deployment
+ if ($this->application->dockerfile) {
+ $this->execute_remote_command([
+ executeInDocker($this->deployment_uuid, "docker build --pull $this->buildTarget $this->addHosts --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true
+ ]);
+ } else {
+ $this->execute_remote_command([
+ executeInDocker($this->deployment_uuid, "docker build $this->buildTarget $this->addHosts --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true
+ ]);
+ }
+ }
+ $this->execute_remote_command([
+ "echo -n 'Building docker image completed.'",
+ ]);
+ }
+
+ private function stop_running_container(bool $force = false)
+ {
+ $this->execute_remote_command(["echo -n 'Removing old container.'"]);
+ if ($this->newVersionIsHealthy || $force) {
+ $containers = getCurrentApplicationContainerStatus($this->server, $this->application->id, 0);
+ $containers = $containers->filter(function ($container) {
+ return data_get($container, 'Names') !== $this->container_name;
+ });
+ $containers->each(function ($container) {
+ $containerName = data_get($container, 'Names');
+ $this->execute_remote_command(
+ [executeInDocker($this->deployment_uuid, "docker rm -f $containerName >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true],
+ );
+ });
+ $this->execute_remote_command(
+ [
+ "echo 'Rolling update completed.'"
+ ],
+ );
+ } else {
+ $this->execute_remote_command(
+ ["echo -n 'New container is not healthy, rolling back to the old container.'"],
+ [executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true],
+ );
+ }
+ }
+
+ private function start_by_compose_file()
+ {
+ if ($this->application->build_pack === 'dockerimage') {
+ $this->execute_remote_command(
+ ["echo -n 'Pulling latest images from the registry.'"],
+ [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} pull"), "hidden" => true],
+ [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true],
+ );
+ } else {
+ $this->execute_remote_command(
+ [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true],
+ );
+ }
+ }
+
+ private function generate_build_env_variables()
+ {
+ $this->build_args = collect(["--build-arg SOURCE_COMMIT=\"{$this->commit}\""]);
+ foreach ($this->application->build_environment_variables_preview as $env) {
+ $this->build_args->push("--build-arg {$env->key}=\"{$env->value}\"");
+ }
+ $this->build_args = $this->build_args->implode(' ');
+ }
+
+ private function add_build_env_variables_to_dockerfile()
+ {
+ $this->execute_remote_command([
+ executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"), "hidden" => true, "save" => 'dockerfile'
+ ]);
+ $dockerfile = collect(Str::of($this->saved_outputs->get('dockerfile'))->trim()->explode("\n"));
+
+ foreach ($this->application->build_environment_variables as $env) {
+ $dockerfile->splice(1, 0, "ARG {$env->key}={$env->value}");
+ }
+ $dockerfile_base64 = base64_encode($dockerfile->implode("\n"));
+ $this->execute_remote_command([
+ executeInDocker($this->deployment_uuid, "echo '{$dockerfile_base64}' | base64 -d > {$this->workdir}{$this->dockerfile_location}"),
+ "hidden" => true
+ ]);
+ }
+
+ private function next(string $status)
+ {
+ // If the deployment is cancelled by the user, don't update the status
+ if ($this->application_deployment_queue->status !== ApplicationDeploymentStatus::CANCELLED_BY_USER->value) {
+ $this->application_deployment_queue->update([
+ 'status' => $status,
+ ]);
+ }
+ queue_next_deployment($this->application);
+ if ($status === ApplicationDeploymentStatus::FINISHED->value) {
+ $this->application->environment->project->team->notify(new DeploymentSuccess($this->application, $this->deployment_uuid, $this->preview));
+ }
+ if ($status === ApplicationDeploymentStatus::FAILED->value) {
+ $this->application->environment->project->team->notify(new DeploymentFailed($this->application, $this->deployment_uuid, $this->preview));
+ }
+ }
+
+ public function failed(Throwable $exception): void
+ {
+ $this->execute_remote_command(
+ ["echo 'Oops something is not okay, are you okay? 😢'", 'type' => 'err'],
+ ["echo '{$exception->getMessage()}'", 'type' => 'err'],
+ ["echo -n 'Deployment failed. Removing the new version of your application.'", 'type' => 'err'],
+ [executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true]
+ );
+
+ $this->next(ApplicationDeploymentStatus::FAILED->value);
+ }
+}
diff --git a/app/Models/Application.php b/app/Models/Application.php
index cb3d85f94..785ef3040 100644
--- a/app/Models/Application.php
+++ b/app/Models/Application.php
@@ -225,7 +225,8 @@ class Application extends BaseModel
return $this->morphTo();
}
- public function isDeploymentInprogress() {
+ public function isDeploymentInprogress()
+ {
$deployments = ApplicationDeploymentQueue::where('application_id', $this->id)->where('status', 'in_progress')->count();
if ($deployments > 0) {
return true;
@@ -300,8 +301,9 @@ class Application extends BaseModel
}
return false;
}
- public function isLogDrainEnabled() {
- return data_get($this, 'settings.is_log_drain_enabled', false);
+ public function isLogDrainEnabled()
+ {
+ return data_get($this, 'settings.is_log_drain_enabled', false);
}
public function isConfigurationChanged($save = false)
{
@@ -330,4 +332,14 @@ class Application extends BaseModel
return true;
}
}
+ public function isMultipleServerDeployment()
+ {
+ if (isDev()) {
+ return true;
+ }
+ if (data_get($this, 'additional_destinations') && data_get($this, 'docker_registry_image_name')) {
+ return true;
+ }
+ return false;
+ }
}
diff --git a/app/Models/Server.php b/app/Models/Server.php
index 02c3186c6..30e07c1d8 100644
--- a/app/Models/Server.php
+++ b/app/Models/Server.php
@@ -171,7 +171,7 @@ class Server extends BaseModel
break;
}
$result = $this->validateConnection();
- ray('validateConnection: ' . $result);
+ // ray('validateConnection: ' . $result);
if (!$result) {
$serverUptimeCheckNumber++;
$this->update([
@@ -304,6 +304,27 @@ class Server extends BaseModel
{
return $this->settings->is_logdrain_newrelic_enabled || $this->settings->is_logdrain_highlight_enabled || $this->settings->is_logdrain_axiom_enabled;
}
+ public function validateOS()
+ {
+ $os_release = instant_remote_process(['cat /etc/os-release'], $this);
+ $datas = collect(explode("\n", $os_release));
+ $collectedData = collect([]);
+ foreach ($datas as $data) {
+ $item = Str::of($data)->trim();
+ $collectedData->put($item->before('=')->value(), $item->after('=')->lower()->replace('"', '')->value());
+ }
+ $ID = data_get($collectedData, 'ID');
+ $ID_LIKE = data_get($collectedData, 'ID_LIKE');
+ $VERSION_ID = data_get($collectedData, 'VERSION_ID');
+ // ray($ID, $ID_LIKE, $VERSION_ID);
+ if (collect(SUPPORTED_OS)->contains($ID_LIKE)) {
+ ray('supported');
+ return str($ID_LIKE)->explode(' ')->first();
+ } else {
+ ray('not supported');
+ return false;
+ }
+ }
public function validateConnection()
{
if ($this->skipServer()) {
@@ -314,27 +335,22 @@ class Server extends BaseModel
if (!$uptime) {
$this->settings()->update([
'is_reachable' => false,
- 'is_usable' => false
]);
return false;
+ } else {
+ $this->settings()->update([
+ 'is_reachable' => true,
+ ]);
+ $this->update([
+ 'unreachable_count' => 0,
+ ]);
}
if (data_get($this, 'unreachable_notification_sent') === true) {
$this->team->notify(new Revived($this));
$this->update(['unreachable_notification_sent' => false]);
}
- if (
- data_get($this, 'settings.is_reachable') === false ||
- data_get($this, 'settings.is_usable') === false
- ) {
- $this->settings()->update([
- 'is_reachable' => true,
- 'is_usable' => true
- ]);
- }
- $this->update([
- 'unreachable_count' => 0,
- ]);
+
return true;
}
public function validateDockerEngine($throwError = false)
@@ -344,7 +360,7 @@ class Server extends BaseModel
$this->settings->is_usable = false;
$this->settings->save();
if ($throwError) {
- throw new \Exception('Server is not usable.');
+ throw new \Exception('Server is not usable. Docker Engine is not installed.');
}
return false;
}
@@ -362,6 +378,7 @@ class Server extends BaseModel
$this->settings->save();
return false;
}
+ $this->settings->is_reachable = true;
$this->settings->is_usable = true;
$this->settings->save();
return true;
diff --git a/app/Models/Service.php b/app/Models/Service.php
index cf78e8260..8cd195bce 100644
--- a/app/Models/Service.php
+++ b/app/Models/Service.php
@@ -53,50 +53,83 @@ class Service extends BaseModel
$image = str($application->image)->before(':')->value();
switch ($image) {
case str($image)->contains('minio'):
+ $data = collect([]);
$console_url = $this->environment_variables()->where('key', 'MINIO_BROWSER_REDIRECT_URL')->first();
$s3_api_url = $this->environment_variables()->where('key', 'MINIO_SERVER_URL')->first();
$admin_user = $this->environment_variables()->where('key', 'SERVICE_USER_MINIO')->first();
+ if (is_null($admin_user)) {
+ $admin_user = $this->environment_variables()->where('key', 'MINIO_ROOT_USER')->first();
+ }
$admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_MINIO')->first();
- $fields->put('MinIO', [
- 'Console URL' => [
- 'key' => data_get($console_url, 'key'),
- 'value' => data_get($console_url, 'value'),
- 'rules' => 'required|url',
- ],
- 'S3 API URL' => [
- 'key' => data_get($s3_api_url, 'key'),
- 'value' => data_get($s3_api_url, 'value'),
- 'rules' => 'required|url',
- ],
- 'Admin User' => [
- 'key' => data_get($admin_user, 'key'),
- 'value' => data_get($admin_user, 'value'),
- 'rules' => 'required',
- ],
- 'Admin Password' => [
- 'key' => data_get($admin_password, 'key'),
- 'value' => data_get($admin_password, 'value'),
- 'rules' => 'required',
- 'isPassword' => true,
- ],
- ]);
+ if (is_null($admin_password)) {
+ $admin_password = $this->environment_variables()->where('key', 'MINIO_ROOT_PASSWORD')->first();
+ }
+
+ if ($console_url) {
+ $data = $data->merge([
+ 'Console URL' => [
+ 'key' => data_get($console_url, 'key'),
+ 'value' => data_get($console_url, 'value'),
+ 'rules' => 'required|url',
+ ],
+ ]);
+ }
+ if ($s3_api_url) {
+ $data = $data->merge([
+ 'S3 API URL' => [
+ 'key' => data_get($s3_api_url, 'key'),
+ 'value' => data_get($s3_api_url, 'value'),
+ 'rules' => 'required|url',
+ ],
+ ]);
+ }
+ if ($admin_user) {
+ $data = $data->merge([
+ 'Admin User' => [
+ 'key' => data_get($admin_user, 'key'),
+ 'value' => data_get($admin_user, 'value'),
+ 'rules' => 'required',
+ ],
+ ]);
+ }
+ if ($admin_password) {
+ $data = $data->merge([
+ 'Admin Password' => [
+ 'key' => data_get($admin_password, 'key'),
+ 'value' => data_get($admin_password, 'value'),
+ 'rules' => 'required',
+ 'isPassword' => true,
+ ],
+ ]);
+ }
+
+ $fields->put('MinIO', $data->toArray());
break;
case str($image)->contains('weblate'):
+ $data = collect([]);
$admin_email = $this->environment_variables()->where('key', 'WEBLATE_ADMIN_EMAIL')->first();
$admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_WEBLATE')->first();
- $fields->put('Weblate', [
- 'Admin Email' => [
- 'key' => data_get($admin_email, 'key'),
- 'value' => data_get($admin_email, 'value'),
- 'rules' => 'required|email',
- ],
- 'Admin Password' => [
- 'key' => data_get($admin_password, 'key'),
- 'value' => data_get($admin_password, 'value'),
- 'rules' => 'required',
- 'isPassword' => true,
- ],
- ]);
+
+ if ($admin_email) {
+ $data = $data->merge([
+ 'Admin Email' => [
+ 'key' => data_get($admin_email, 'key'),
+ 'value' => data_get($admin_email, 'value'),
+ 'rules' => 'required|email',
+ ],
+ ]);
+ }
+ if ($admin_password) {
+ $data = $data->merge([
+ 'Admin Password' => [
+ 'key' => data_get($admin_password, 'key'),
+ 'value' => data_get($admin_password, 'value'),
+ 'rules' => 'required',
+ 'isPassword' => true,
+ ],
+ ]);
+ }
+ $fields->put('Weblate', $data);
}
}
$databases = $this->databases()->get();
@@ -367,6 +400,19 @@ class Service extends BaseModel
$serviceNetworks = collect(data_get($service, 'networks', []));
$serviceVariables = collect(data_get($service, 'environment', []));
$serviceLabels = collect(data_get($service, 'labels', []));
+ if ($serviceLabels->count() > 0) {
+ $removedLabels = collect([]);
+ $serviceLabels = $serviceLabels->filter(function ($serviceLabel, $serviceLabelName) use ($removedLabels) {
+ if (!str($serviceLabel)->contains('=')) {
+ $removedLabels->put($serviceLabelName, $serviceLabel);
+ return false;
+ }
+ return $serviceLabel;
+ });
+ foreach($removedLabels as $removedLabelName =>$removedLabel) {
+ $serviceLabels->push("$removedLabelName=$removedLabel");
+ }
+ }
$containerName = "$serviceName-{$this->uuid}";
diff --git a/app/Traits/ExecuteRemoteCommand.php b/app/Traits/ExecuteRemoteCommand.php
index 1244fde28..1306f645c 100644
--- a/app/Traits/ExecuteRemoteCommand.php
+++ b/app/Traits/ExecuteRemoteCommand.php
@@ -12,7 +12,6 @@ use Illuminate\Support\Str;
trait ExecuteRemoteCommand
{
public ?string $save = null;
-
public function execute_remote_command(...$commands)
{
static::$batch_counter++;
@@ -32,16 +31,20 @@ trait ExecuteRemoteCommand
throw new \RuntimeException('Command is not set');
}
$hidden = data_get($single_command, 'hidden', false);
+ $customType = data_get($single_command, 'type');
$ignore_errors = data_get($single_command, 'ignore_errors', false);
$this->save = data_get($single_command, 'save');
$remote_command = generateSshCommand($this->server, $command);
- $process = Process::timeout(3600)->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $hidden) {
+ $process = Process::timeout(3600)->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $hidden, $customType) {
$output = Str::of($output)->trim();
+ if ($output->startsWith('â•”')) {
+ $output = "\n" . $output;
+ }
$new_log_entry = [
- 'command' => $command,
- 'output' => $output,
- 'type' => $type === 'err' ? 'stderr' : 'stdout',
+ 'command' => remove_iip($command),
+ 'output' => remove_iip($output),
+ 'type' => $customType ?? $type === 'err' ? 'stderr' : 'stdout',
'timestamp' => Carbon::now('UTC'),
'hidden' => $hidden,
'batch' => static::$batch_counter,
diff --git a/app/Traits/ExecuteRemoteCommandNew.php b/app/Traits/ExecuteRemoteCommandNew.php
new file mode 100644
index 000000000..ca56d9e50
--- /dev/null
+++ b/app/Traits/ExecuteRemoteCommandNew.php
@@ -0,0 +1,77 @@
+each(function ($singleCommand) use ($server, $logModel) {
+ $command = data_get($singleCommand, 'command') ?? $singleCommand[0] ?? null;
+ if ($command === null) {
+ throw new \RuntimeException('Command is not set');
+ }
+ $hidden = data_get($singleCommand, 'hidden', false);
+ $customType = data_get($singleCommand, 'type');
+ $ignoreErrors = data_get($singleCommand, 'ignore_errors', false);
+ $save = data_get($singleCommand, 'save');
+
+ $remote_command = generateSshCommand($server, $command);
+ $process = Process::timeout(3600)->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $hidden, $customType, $logModel, $save) {
+ $output = str($output)->trim();
+ if ($output->startsWith('â•”')) {
+ $output = "\n" . $output;
+ }
+ $newLogEntry = [
+ 'command' => remove_iip($command),
+ 'output' => remove_iip($output),
+ 'type' => $customType ?? $type === 'err' ? 'stderr' : 'stdout',
+ 'timestamp' => Carbon::now('UTC'),
+ 'hidden' => $hidden,
+ 'batch' => static::$batch_counter,
+ ];
+
+ if (!$logModel->logs) {
+ $newLogEntry['order'] = 1;
+ } else {
+ $previousLogs = json_decode($logModel->logs, associative: true, flags: JSON_THROW_ON_ERROR);
+ $newLogEntry['order'] = count($previousLogs) + 1;
+ }
+
+ $previousLogs[] = $newLogEntry;
+ $logModel->logs = json_encode($previousLogs, flags: JSON_THROW_ON_ERROR);
+ $logModel->save();
+
+ if ($save) {
+ $this->remoteCommandOutputs[$save] = str($output)->trim();
+ }
+ });
+ $logModel->update([
+ 'current_process_id' => $process->id(),
+ ]);
+
+ $processResult = $process->wait();
+ if ($processResult->exitCode() !== 0) {
+ if (!$ignoreErrors) {
+ $status = ApplicationDeploymentStatus::FAILED->value;
+ $logModel->status = $status;
+ $logModel->save();
+ throw new \RuntimeException($processResult->errorOutput());
+ }
+ }
+ });
+ }
+}
diff --git a/app/View/Components/Forms/Textarea.php b/app/View/Components/Forms/Textarea.php
index 69c618fb4..50ffe77f7 100644
--- a/app/View/Components/Forms/Textarea.php
+++ b/app/View/Components/Forms/Textarea.php
@@ -38,7 +38,7 @@ class Textarea extends Component
if (is_null($this->id)) $this->id = new Cuid2(7);
if (is_null($this->name)) $this->name = $this->id;
- $this->label = Str::title($this->label);
+ // $this->label = Str::title($this->label);
return view('components.forms.textarea');
}
}
diff --git a/bootstrap/helpers/applications.php b/bootstrap/helpers/applications.php
index fce225dc5..1cead8a54 100644
--- a/bootstrap/helpers/applications.php
+++ b/bootstrap/helpers/applications.php
@@ -1,8 +1,15 @@
count() > 0) {
return;
}
+ // New deployment
+ // dispatchDeploymentJob($deployment->id);
dispatch(new ApplicationDeploymentJob(
application_deployment_queue_id: $deployment->id,
))->onConnection('long-running')->onQueue('long-running');
+
}
function queue_next_deployment(Application $application)
{
$next_found = ApplicationDeploymentQueue::where('application_id', $application->id)->where('status', 'queued')->first();
if ($next_found) {
+ // New deployment
+ // dispatchDeploymentJob($next_found->id);
dispatch(new ApplicationDeploymentJob(
application_deployment_queue_id: $next_found->id,
))->onConnection('long-running')->onQueue('long-running');
+
+ }
+}
+function dispatchDeploymentJob($id)
+{
+ $applicationQueue = ApplicationDeploymentQueue::find($id);
+ $application = Application::find($applicationQueue->application_id);
+
+ $isRestartOnly = data_get($applicationQueue, 'restart_only');
+ $isSimpleDockerFile = data_get($application, 'dockerfile');
+ $isDockerImage = data_get($application, 'build_pack') === 'dockerimage';
+
+ if ($isRestartOnly) {
+ ApplicationRestartJob::dispatch(applicationDeploymentQueueId: $id)->onConnection('long-running')->onQueue('long-running');
+ } else if ($isSimpleDockerFile) {
+ ApplicationDeploySimpleDockerfileJob::dispatch(applicationDeploymentQueueId: $id)->onConnection('long-running')->onQueue('long-running');
+ } else if ($isDockerImage) {
+ ApplicationDeployDockerImageJob::dispatch(applicationDeploymentQueueId: $id)->onConnection('long-running')->onQueue('long-running');
+ } else {
+ throw new Exception('Unknown build pack');
+ }
+}
+
+// Deployment things
+function generateHostIpMapping(Server $server, string $network)
+{
+ // Generate custom host<->ip hostnames
+ $allContainers = instant_remote_process(["docker network inspect {$network} -f '{{json .Containers}}' "], $server);
+ $allContainers = format_docker_command_output_to_json($allContainers);
+ $ips = collect([]);
+ if (count($allContainers) > 0) {
+ $allContainers = $allContainers[0];
+ foreach ($allContainers as $container) {
+ $containerName = data_get($container, 'Name');
+ if ($containerName === 'coolify-proxy') {
+ continue;
+ }
+ $containerIp = data_get($container, 'IPv4Address');
+ if ($containerName && $containerIp) {
+ $containerIp = str($containerIp)->before('/');
+ $ips->put($containerName, $containerIp->value());
+ }
+ }
+ }
+ return $ips->map(function ($ip, $name) {
+ return "--add-host $name:$ip";
+ })->implode(' ');
+}
+
+function generateBaseDir(string $deplyomentUuid)
+{
+ return "/artifacts/$deplyomentUuid";
+}
+function generateWorkdir(string $deplyomentUuid, Application $application)
+{
+ return generateBaseDir($deplyomentUuid) . rtrim($application->base_directory, '/');
+}
+
+function prepareHelperContainer(Server $server, string $network, string $deploymentUuid)
+{
+ $basedir = generateBaseDir($deploymentUuid);
+ $helperImage = config('coolify.helper_image');
+
+ $serverUserHomeDir = instant_remote_process(["echo \$HOME"], $server);
+ $dockerConfigFileExists = instant_remote_process(["test -f {$serverUserHomeDir}/.docker/config.json && echo 'OK' || echo 'NOK'"], $server);
+
+ $commands = collect([]);
+ if ($dockerConfigFileExists === 'OK') {
+ $commands->push([
+ "command" => "docker run -d --network $network -v /:/host --name $deploymentUuid --rm -v {$serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock $helperImage",
+ "hidden" => true,
+ ]);
+ } else {
+ $commands->push([
+ "command" => "docker run -d --network {$network} -v /:/host --name {$deploymentUuid} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}",
+ "hidden" => true,
+ ]);
+ }
+ $commands->push([
+ "command" => executeInDocker($deploymentUuid, "mkdir -p {$basedir}"),
+ "hidden" => true,
+ ]);
+ return $commands;
+}
+
+function generateComposeFile(string $deploymentUuid, Server $server, string $network, Application $application, string $containerName, string $imageName, ?ApplicationPreview $preview = null, int $pullRequestId = 0)
+{
+ $ports = $application->settings->is_static ? [80] : $application->ports_exposes_array;
+ $workDir = generateWorkdir($deploymentUuid, $application);
+ $persistent_storages = generateLocalPersistentVolumes($application, $pullRequestId);
+ $volume_names = generateLocalPersistentVolumesOnlyVolumeNames($application, $pullRequestId);
+ $environment_variables = generateEnvironmentVariables($application, $ports, $pullRequestId);
+
+ if (data_get($application, 'custom_labels')) {
+ $labels = collect(str($application->custom_labels)->explode(','));
+ $labels = $labels->filter(function ($value, $key) {
+ return !str($value)->startsWith('coolify.');
+ });
+ $application->custom_labels = $labels->implode(',');
+ $application->save();
+ } else {
+ $labels = collect(generateLabelsApplication($application, $preview));
+ }
+ if ($pullRequestId !== 0) {
+ $labels = collect(generateLabelsApplication($application, $preview));
+ }
+ $labels = $labels->merge(defaultLabels($application->id, $application->uuid, 0))->toArray();
+ $docker_compose = [
+ 'version' => '3.8',
+ 'services' => [
+ $containerName => [
+ 'image' => $imageName,
+ 'container_name' => $containerName,
+ 'restart' => RESTART_MODE,
+ 'environment' => $environment_variables,
+ 'labels' => $labels,
+ 'expose' => $ports,
+ 'networks' => [
+ $network,
+ ],
+ 'mem_limit' => $application->limits_memory,
+ 'memswap_limit' => $application->limits_memory_swap,
+ 'mem_swappiness' => $application->limits_memory_swappiness,
+ 'mem_reservation' => $application->limits_memory_reservation,
+ 'cpus' => (int) $application->limits_cpus,
+ 'cpuset' => $application->limits_cpuset,
+ 'cpu_shares' => $application->limits_cpu_shares,
+ ]
+ ],
+ 'networks' => [
+ $network => [
+ 'external' => true,
+ 'name' => $network,
+ 'attachable' => true
+ ]
+ ]
+ ];
+ if ($server->isLogDrainEnabled() && $application->isLogDrainEnabled()) {
+ $docker_compose['services'][$containerName]['logging'] = [
+ 'driver' => 'fluentd',
+ 'options' => [
+ 'fluentd-address' => "tcp://127.0.0.1:24224",
+ 'fluentd-async' => "true",
+ 'fluentd-sub-second-precision' => "true",
+ ]
+ ];
+ }
+ if ($application->settings->is_gpu_enabled) {
+ ray('asd');
+ $docker_compose['services'][$containerName]['deploy']['resources']['reservations']['devices'] = [
+ [
+ 'driver' => data_get($application, 'settings.gpu_driver', 'nvidia'),
+ 'capabilities' => ['gpu'],
+ 'options' => data_get($application, 'settings.gpu_options', [])
+ ]
+ ];
+ if (data_get($application, 'settings.gpu_count')) {
+ $count = data_get($application, 'settings.gpu_count');
+ if ($count === 'all') {
+ $docker_compose['services'][$containerName]['deploy']['resources']['reservations']['devices'][0]['count'] = $count;
+ } else {
+ $docker_compose['services'][$containerName]['deploy']['resources']['reservations']['devices'][0]['count'] = (int) $count;
+ }
+ } else if (data_get($application, 'settings.gpu_device_ids')) {
+ $docker_compose['services'][$containerName]['deploy']['resources']['reservations']['devices'][0]['ids'] = data_get($application, 'settings.gpu_device_ids');
+ }
+ }
+ if ($application->isHealthcheckDisabled()) {
+ data_forget($docker_compose, 'services.' . $containerName . '.healthcheck');
+ }
+ if (count($application->ports_mappings_array) > 0 && $pullRequestId === 0) {
+ $docker_compose['services'][$containerName]['ports'] = $application->ports_mappings_array;
+ }
+ if (count($persistent_storages) > 0) {
+ $docker_compose['services'][$containerName]['volumes'] = $persistent_storages;
+ }
+ if (count($volume_names) > 0) {
+ $docker_compose['volumes'] = $volume_names;
+ }
+ $docker_compose = Yaml::dump($docker_compose, 10);
+ $docker_compose_base64 = base64_encode($docker_compose);
+ $commands = collect([]);
+ $commands->push([
+ "command" => executeInDocker($deploymentUuid, "echo '{$docker_compose_base64}' | base64 -d > {$workDir}/docker-compose.yml"),
+ "hidden" => true,
+ ]);
+ return $commands;
+}
+function generateLocalPersistentVolumes(Application $application, int $pullRequestId = 0)
+{
+ $local_persistent_volumes = [];
+ foreach ($application->persistentStorages as $persistentStorage) {
+ $volume_name = $persistentStorage->host_path ?? $persistentStorage->name;
+ if ($pullRequestId !== 0) {
+ $volume_name = $volume_name . '-pr-' . $pullRequestId;
+ }
+ $local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
+ }
+ return $local_persistent_volumes;
+}
+
+function generateLocalPersistentVolumesOnlyVolumeNames(Application $application, int $pullRequestId = 0)
+{
+ $local_persistent_volumes_names = [];
+ foreach ($application->persistentStorages as $persistentStorage) {
+ if ($persistentStorage->host_path) {
+ continue;
+ }
+ $name = $persistentStorage->name;
+
+ if ($pullRequestId !== 0) {
+ $name = $name . '-pr-' . $pullRequestId;
+ }
+
+ $local_persistent_volumes_names[$name] = [
+ 'name' => $name,
+ 'external' => false,
+ ];
+ }
+ return $local_persistent_volumes_names;
+}
+function generateEnvironmentVariables(Application $application, $ports, int $pullRequestId = 0)
+{
+ $environment_variables = collect();
+ // ray('Generate Environment Variables')->green();
+ if ($pullRequestId === 0) {
+ // ray($this->application->runtime_environment_variables)->green();
+ foreach ($application->runtime_environment_variables as $env) {
+ $environment_variables->push("$env->key=$env->value");
+ }
+ foreach ($application->nixpacks_environment_variables as $env) {
+ $environment_variables->push("$env->key=$env->value");
+ }
+ } else {
+ // ray($this->application->runtime_environment_variables_preview)->green();
+ foreach ($application->runtime_environment_variables_preview as $env) {
+ $environment_variables->push("$env->key=$env->value");
+ }
+ foreach ($application->nixpacks_environment_variables_preview as $env) {
+ $environment_variables->push("$env->key=$env->value");
+ }
+ }
+ // Add PORT if not exists, use the first port as default
+ if ($environment_variables->filter(fn ($env) => str($env)->contains('PORT'))->isEmpty()) {
+ $environment_variables->push("PORT={$ports[0]}");
+ }
+ return $environment_variables->all();
+}
+
+function rollingUpdate(Application $application, string $deploymentUuid)
+{
+ $commands = collect([]);
+ $workDir = generateWorkdir($deploymentUuid, $application);
+ if (count($application->ports_mappings_array) > 0) {
+ // $this->execute_remote_command(
+ // [
+ // "echo '\n----------------------------------------'",
+ // ],
+ // ["echo -n 'Application has ports mapped to the host system, rolling update is not supported.'"],
+ // );
+ // $this->stop_running_container(force: true);
+ // $this->start_by_compose_file();
+ } else {
+ $commands->push(
+ [
+ "command" => "echo '\n----------------------------------------'"
+ ],
+ [
+ "command" => "echo -n 'Rolling update started.'"
+ ]
+ );
+ if ($application->build_pack === 'dockerimage') {
+ $commands->push(
+ ["echo -n 'Pulling latest images from the registry.'"],
+ [executeInDocker($deploymentUuid, "docker compose --project-directory {$workDir} pull"), "hidden" => true],
+ [executeInDocker($deploymentUuid, "docker compose --project-directory {$workDir} up --build -d"), "hidden" => true],
+ );
+ } else {
+ $commands->push(
+ [executeInDocker($deploymentUuid, "docker compose --project-directory {$workDir} up --build -d"), "hidden" => true],
+ );
+ }
+ return $commands;
}
}
diff --git a/bootstrap/helpers/constants.php b/bootstrap/helpers/constants.php
index e844efea9..299d3acb9 100644
--- a/bootstrap/helpers/constants.php
+++ b/bootstrap/helpers/constants.php
@@ -1,5 +1,6 @@
';
const DATABASE_TYPES = ['postgresql', 'redis', 'mongodb', 'mysql', 'mariadb'];
const VALID_CRON_STRINGS = [
'every_minute' => '* * * * *',
@@ -26,3 +27,8 @@ const DATABASE_DOCKER_IMAGES = [
const SPECIFIC_SERVICES = [
'quay.io/minio/minio',
];
+
+const SUPPORTED_OS = [
+ 'debian',
+ 'rhel centos fedora'
+];
diff --git a/bootstrap/helpers/remoteProcess.php b/bootstrap/helpers/remoteProcess.php
index c1ed577b5..948e47329 100644
--- a/bootstrap/helpers/remoteProcess.php
+++ b/bootstrap/helpers/remoteProcess.php
@@ -170,10 +170,13 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d
$i['timestamp'] = Carbon::parse($i['timestamp'])->format('Y-M-d H:i:s.u');
return $i;
});
-
return $formatted;
}
-
+function remove_iip($text)
+{
+ $text = preg_replace('/x-access-token:.*?(?=@)/', "x-access-token:" . REDACTED, $text);
+ return preg_replace('/\x1b\[[0-9;]*m/', '', $text);
+}
function refresh_server_connection(?PrivateKey $private_key = null)
{
if (is_null($private_key)) {
diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php
index 6e3c6ca4f..071c252ff 100644
--- a/bootstrap/helpers/shared.php
+++ b/bootstrap/helpers/shared.php
@@ -93,8 +93,13 @@ function refreshSession(?Team $team = null): void
}
function handleError(?Throwable $error = null, ?Livewire\Component $livewire = null, ?string $customErrorMessage = null)
{
- ray('handleError');
- ray($error);
+ if ($error instanceof TooManyRequestsException) {
+ if (isset($livewire)) {
+ return $livewire->emit('error', "Too many requests. Please try again in {$error->secondsUntilAvailable} seconds.");
+ }
+ return "Too many requests. Please try again in {$error->secondsUntilAvailable} seconds.";
+ }
+
if ($error instanceof Throwable) {
$message = $error->getMessage();
} else {
@@ -103,55 +108,12 @@ function handleError(?Throwable $error = null, ?Livewire\Component $livewire = n
if ($customErrorMessage) {
$message = $customErrorMessage . ' ' . $message;
}
- if ($error instanceof TooManyRequestsException) {
- if (isset($livewire)) {
- return $livewire->emit('error', "Too many requests. Please try again in {$error->secondsUntilAvailable} seconds.");
- }
- return "Too many requests. Please try again in {$error->secondsUntilAvailable} seconds.";
- }
+
if (isset($livewire)) {
return $livewire->emit('error', $message);
}
-
- throw new RuntimeException($message);
+ throw new Exception($message);
}
-function general_error_handler(Throwable $err, Livewire\Component $that = null, $isJson = false, $customErrorMessage = null): mixed
-{
- try {
- ray($err);
- ray('ERROR OCCURRED: ' . $err->getMessage());
- if ($err instanceof QueryException) {
- if ($err->errorInfo[0] === '23505') {
- throw new Exception($customErrorMessage ?? 'Duplicate entry found.', '23505');
- } else if (count($err->errorInfo) === 4) {
- throw new Exception($customErrorMessage ?? $err->errorInfo[3]);
- } else {
- throw new Exception($customErrorMessage ?? $err->errorInfo[2]);
- }
- } elseif ($err instanceof TooManyRequestsException) {
- throw new Exception($customErrorMessage ?? "Too many requests. Please try again in {$err->secondsUntilAvailable} seconds.");
- } else {
- if ($err->getMessage() === 'This action is unauthorized.') {
- return redirect()->route('dashboard')->with('error', $customErrorMessage ?? $err->getMessage());
- }
- throw new Exception($customErrorMessage ?? $err->getMessage());
- }
- } catch (\Throwable $e) {
- if ($that) {
- return $that->emit('error', $customErrorMessage ?? $e->getMessage());
- } elseif ($isJson) {
- return response()->json([
- 'code' => $e->getCode(),
- 'error' => $e->getMessage(),
- ]);
- } else {
- ray($customErrorMessage);
- ray($e);
- return $customErrorMessage ?? $e->getMessage();
- }
- }
-}
-
function get_route_parameters(): array
{
return Route::current()->parameters();
diff --git a/config/sentry.php b/config/sentry.php
index 0c8ab1b30..a09025958 100644
--- a/config/sentry.php
+++ b/config/sentry.php
@@ -3,11 +3,11 @@
return [
// @see https://docs.sentry.io/product/sentry-basics/dsn-explainer/
- 'dsn' => 'https://c35fe90ee56e18b220bb55e8217d4839@o1082494.ingest.sentry.io/4505347448045568',
+ 'dsn' => 'https://396748153b19c469f5ceff50f1664323@o1082494.ingest.sentry.io/4505347448045568',
// 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.144',
+ 'release' => '4.0.0-beta.145',
// When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'),
@@ -76,6 +76,7 @@ return [
'send_default_pii' => env('SENTRY_SEND_DEFAULT_PII', false),
// @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#traces-sample-rate
+ 'enable_tracing' => env('SENTRY_ENABLE_TRACING', false),
'traces_sample_rate' => 0.2,
'profiles_sample_rate' => env('SENTRY_PROFILES_SAMPLE_RATE') === null ? null : (float)env('SENTRY_PROFILES_SAMPLE_RATE'),
diff --git a/config/version.php b/config/version.php
index a4a00caab..9bbb31a9f 100644
--- a/config/version.php
+++ b/config/version.php
@@ -1,3 +1,3 @@
boolean('is_gpu_enabled')->default(false);
+ $table->string('gpu_driver')->default('nvidia');
+ $table->string('gpu_count')->nullable();
+ $table->string('gpu_device_ids')->nullable();
+ $table->longText('gpu_options')->nullable();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('application_settings', function (Blueprint $table) {
+ $table->dropColumn('is_gpu_enabled');
+ $table->dropColumn('gpu_driver');
+ $table->dropColumn('gpu_count');
+ $table->dropColumn('gpu_device_ids');
+ $table->dropColumn('gpu_options');
+ });
+ }
+};
diff --git a/database/migrations/2023_11_21_121920_add_additional_destinations_to_apps.php b/database/migrations/2023_11_21_121920_add_additional_destinations_to_apps.php
new file mode 100644
index 000000000..4852007b4
--- /dev/null
+++ b/database/migrations/2023_11_21_121920_add_additional_destinations_to_apps.php
@@ -0,0 +1,28 @@
+string('additional_destinations')->nullable()->after('destination');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('applications', function (Blueprint $table) {
+ $table->dropColumn('additional_destinations');
+ });
+ }
+};
diff --git a/resources/views/components/forms/checkbox.blade.php b/resources/views/components/forms/checkbox.blade.php
index de4abb87a..6041eb793 100644
--- a/resources/views/components/forms/checkbox.blade.php
+++ b/resources/views/components/forms/checkbox.blade.php
@@ -1,4 +1,4 @@
-
You could lost a lot of functionalities if you change the server details of the server where Coolify is @@ -64,9 +64,4 @@ helper="Disk cleanup job will be executed if disk usage is more than this number." /> @endif -