From 69bb4ae5ee8fe4a725dd812080614ebafe0fde32 Mon Sep 17 00:00:00 2001
From: Andras Bacsai
Date: Tue, 28 Nov 2023 13:40:33 +0100
Subject: [PATCH 01/15] Update release version to 4.0.0-beta.148
---
config/sentry.php | 2 +-
config/version.php | 2 +-
versions.json | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/config/sentry.php b/config/sentry.php
index c07fda49b..8acf3d588 100644
--- a/config/sentry.php
+++ b/config/sentry.php
@@ -7,7 +7,7 @@ return [
// The release version of your application
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
- 'release' => '4.0.0-beta.147',
+ 'release' => '4.0.0-beta.148',
// When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'),
diff --git a/config/version.php b/config/version.php
index cf8ab7438..b2be1df08 100644
--- a/config/version.php
+++ b/config/version.php
@@ -1,3 +1,3 @@
Date: Tue, 28 Nov 2023 14:05:55 +0100
Subject: [PATCH 02/15] Remove unused imports and fix import statement
---
app/Models/Server.php | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/app/Models/Server.php b/app/Models/Server.php
index 07714caa7..ca5bd9559 100644
--- a/app/Models/Server.php
+++ b/app/Models/Server.php
@@ -2,8 +2,6 @@
namespace App\Models;
-use App\Actions\Server\InstallLogDrain;
-use App\Actions\Server\InstallNewRelic;
use App\Enums\ApplicationDeploymentStatus;
use App\Enums\ProxyStatus;
use App\Enums\ProxyTypes;
@@ -18,7 +16,7 @@ use Illuminate\Support\Sleep;
use Spatie\SchemalessAttributes\Casts\SchemalessAttributes;
use Spatie\SchemalessAttributes\SchemalessAttributesTrait;
use Illuminate\Support\Str;
-use Stringable;
+use Illuminate\Support\Stringable;
class Server extends BaseModel
{
From 500ba0fab8ba567fb2fe8f043d604044c893c8a3 Mon Sep 17 00:00:00 2001
From: Andras Bacsai
Date: Tue, 28 Nov 2023 14:08:42 +0100
Subject: [PATCH 03/15] fix: do not remove deployment in case compose based
failed
---
app/Jobs/ApplicationDeploymentJob.php | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php
index 7d229e879..153404c72 100644
--- a/app/Jobs/ApplicationDeploymentJob.php
+++ b/app/Jobs/ApplicationDeploymentJob.php
@@ -1289,9 +1289,13 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$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]
);
+ if ($this->application->build_pack !== 'dockercompose') {
+ $this->execute_remote_command(
+ ["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);
}
From 87062e4e226da1d6a0ee95280695adc099d5184d Mon Sep 17 00:00:00 2001
From: Andras Bacsai
Date: Tue, 28 Nov 2023 14:23:59 +0100
Subject: [PATCH 04/15] Refactor application deployment job
---
app/Jobs/ApplicationDeploymentJob.php | 39 ++-------------------------
1 file changed, 2 insertions(+), 37 deletions(-)
diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php
index 153404c72..dae4c0730 100644
--- a/app/Jobs/ApplicationDeploymentJob.php
+++ b/app/Jobs/ApplicationDeploymentJob.php
@@ -290,42 +290,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
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) {
@@ -589,6 +553,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->start_by_compose_file();
$this->health_check();
$this->stop_running_container();
+ $this->application_deployment_queue->addLogEntry("Rolling update completed.");
}
}
private function health_check()
@@ -1204,7 +1169,6 @@ 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->application_deployment_queue->addLogEntry("Rolling update completed.");
} else {
$this->application_deployment_queue->addLogEntry("New container is not healthy, rolling back to the old container.");
$this->execute_remote_command(
@@ -1226,6 +1190,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true],
);
}
+ $this->application_deployment_queue->addLogEntry("New container started.");
}
private function generate_build_env_variables()
From 4af471ee3161558b35c16b04cdc8539ad09c3c19 Mon Sep 17 00:00:00 2001
From: Andras Bacsai
Date: Tue, 28 Nov 2023 14:26:35 +0100
Subject: [PATCH 05/15] fix: no container servers
---
app/Jobs/ContainerStatusJob.php | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/app/Jobs/ContainerStatusJob.php b/app/Jobs/ContainerStatusJob.php
index 79265f7a1..e30729299 100644
--- a/app/Jobs/ContainerStatusJob.php
+++ b/app/Jobs/ContainerStatusJob.php
@@ -42,7 +42,10 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
if (!$this->server->isServerReady()) {
return;
};
- $containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this->server);
+ $containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this->server, false);
+ if (is_null($containers)) {
+ return;
+ }
$containers = format_docker_command_output_to_json($containers);
$applications = $this->server->applications();
$databases = $this->server->databases();
From 706e4b13ee30fe7eff6b162a52eb5e1150d39909 Mon Sep 17 00:00:00 2001
From: Andras Bacsai
Date: Tue, 28 Nov 2023 14:27:38 +0100
Subject: [PATCH 06/15] fix: sentry issue
---
bootstrap/helpers/docker.php | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php
index 030aa6aa7..d643cce6b 100644
--- a/bootstrap/helpers/docker.php
+++ b/bootstrap/helpers/docker.php
@@ -275,8 +275,11 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
return $labels->all();
}
-function isDatabaseImage(string $image)
+function isDatabaseImage(?string $image = null)
{
+ if (is_null($image)) {
+ return false;
+ }
$image = str($image);
if ($image->contains(':')) {
$image = str($image);
From c505a6ce9cfd5e729cf64e11dfcdac9dc5dc497e Mon Sep 17 00:00:00 2001
From: Andras Bacsai
Date: Tue, 28 Nov 2023 15:49:24 +0100
Subject: [PATCH 07/15] wip
---
app/Actions/Proxy/StartProxy.php | 7 +-
app/Http/Livewire/Boarding/Index.php | 5 +-
app/Http/Livewire/Dashboard.php | 2 +
app/Http/Livewire/Project/New/Select.php | 1 +
app/Http/Livewire/Server/Form.php | 23 ++++--
app/Http/Livewire/Server/New/ByIp.php | 6 +-
app/Models/Server.php | 82 ++++++++++++++-----
app/Models/SwarmDocker.php | 44 ++++++++++
..._28_143533_add_fields_to_swarm_dockers.php | 30 +++++++
.../views/livewire/boarding/index.blade.php | 34 ++++----
.../views/livewire/server/form.blade.php | 13 +--
.../views/livewire/server/new/by-ip.blade.php | 6 +-
12 files changed, 198 insertions(+), 55 deletions(-)
create mode 100644 database/migrations/2023_11_28_143533_add_fields_to_swarm_dockers.php
diff --git a/app/Actions/Proxy/StartProxy.php b/app/Actions/Proxy/StartProxy.php
index 6a6738177..b90508c5a 100644
--- a/app/Actions/Proxy/StartProxy.php
+++ b/app/Actions/Proxy/StartProxy.php
@@ -13,6 +13,9 @@ class StartProxy
public function handle(Server $server, bool $async = true): string|Activity
{
try {
+ if ($server->isSwarm()) {
+ throw new \Exception("Server is part of swarm, not implemented yet.");
+ }
$proxyType = $server->proxyType();
$commands = collect([]);
$proxy_path = get_proxy_path();
@@ -46,11 +49,9 @@ class StartProxy
$server->save();
return 'OK';
}
- } catch(\Throwable $e) {
+ } catch (\Throwable $e) {
ray($e);
throw $e;
}
-
-
}
}
diff --git a/app/Http/Livewire/Boarding/Index.php b/app/Http/Livewire/Boarding/Index.php
index 7f53708ad..645b0da6a 100644
--- a/app/Http/Livewire/Boarding/Index.php
+++ b/app/Http/Livewire/Boarding/Index.php
@@ -31,6 +31,7 @@ class Index extends Component
public ?string $remoteServerHost = null;
public ?int $remoteServerPort = 22;
public ?string $remoteServerUser = 'root';
+ public bool $isPartOfSwarm = false;
public ?Server $createdServer = null;
public Collection $projects;
@@ -182,7 +183,9 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
'private_key_id' => $this->createdPrivateKey->id,
'team_id' => currentTeam()->id,
]);
- $this->createdServer->save();
+ $this->createdServer->settings->is_part_of_swarm = $this->isPartOfSwarm;
+ $this->createdServer->settings->save();
+ $this->createdServer->addInitialNetwork();
$this->validateServer();
}
public function validateServer()
diff --git a/app/Http/Livewire/Dashboard.php b/app/Http/Livewire/Dashboard.php
index b7219864d..2052ded86 100644
--- a/app/Http/Livewire/Dashboard.php
+++ b/app/Http/Livewire/Dashboard.php
@@ -14,6 +14,8 @@ class Dashboard extends Component
public function mount()
{
$this->servers = Server::ownedByCurrentTeam()->get();
+ ray($this->servers[1]);
+ ray($this->servers[1]->standaloneDockers);
$this->projects = Project::ownedByCurrentTeam()->get();
}
// public function getIptables()
diff --git a/app/Http/Livewire/Project/New/Select.php b/app/Http/Livewire/Project/New/Select.php
index 13fbb8886..a23209768 100644
--- a/app/Http/Livewire/Project/New/Select.php
+++ b/app/Http/Livewire/Project/New/Select.php
@@ -126,6 +126,7 @@ class Select extends Component
{
$this->server_id = $server->id;
$this->standaloneDockers = $server->standaloneDockers;
+ ray($server);
$this->swarmDockers = $server->swarmDockers;
$this->current_step = 'destinations';
}
diff --git a/app/Http/Livewire/Server/Form.php b/app/Http/Livewire/Server/Form.php
index dacb8faad..27dfaa8e2 100644
--- a/app/Http/Livewire/Server/Form.php
+++ b/app/Http/Livewire/Server/Form.php
@@ -17,14 +17,14 @@ class Form extends Component
protected $listeners = ['serverRefresh'];
protected $rules = [
- 'server.name' => 'required|min:6',
+ 'server.name' => 'required',
'server.description' => 'nullable',
'server.ip' => 'required',
'server.user' => 'required',
'server.port' => 'required',
- 'server.settings.is_cloudflare_tunnel' => 'required',
+ 'server.settings.is_cloudflare_tunnel' => 'required|boolean',
'server.settings.is_reachable' => 'required',
- 'server.settings.is_part_of_swarm' => 'required',
+ 'server.settings.is_part_of_swarm' => 'required|boolean',
'wildcard_domain' => 'nullable|url',
];
protected $validationAttributes = [
@@ -49,9 +49,14 @@ class Form extends Component
}
public function instantSave()
{
- refresh_server_connection($this->server->privateKey);
- $this->validateServer();
- $this->server->settings->save();
+ try {
+ refresh_server_connection($this->server->privateKey);
+ $this->validateServer(false);
+ $this->server->settings->save();
+ $this->emit('success', 'Server updated successfully.');
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
}
public function installDocker()
{
@@ -100,6 +105,12 @@ class Form extends Component
$install && $this->installDocker();
return;
}
+ if ($this->server->isSwarm()) {
+ $swarmInstalled = $this->server->validateDockerSwarm();
+ if ($swarmInstalled) {
+ $install && $this->emit('success', 'Docker Swarm is initiated.');
+ }
+ }
} catch (\Throwable $e) {
return handleError($e, $this);
} finally {
diff --git a/app/Http/Livewire/Server/New/ByIp.php b/app/Http/Livewire/Server/New/ByIp.php
index 66750db28..139705d94 100644
--- a/app/Http/Livewire/Server/New/ByIp.php
+++ b/app/Http/Livewire/Server/New/ByIp.php
@@ -29,6 +29,7 @@ class ByIp extends Component
'ip' => 'required',
'user' => 'required|string',
'port' => 'required|integer',
+ 'is_part_of_swarm' => 'required|boolean',
];
protected $validationAttributes = [
'name' => 'Name',
@@ -36,6 +37,7 @@ class ByIp extends Component
'ip' => 'IP Address/Domain',
'user' => 'User',
'port' => 'Port',
+ 'is_part_of_swarm' => 'Is part of swarm',
];
public function mount()
@@ -72,11 +74,11 @@ class ByIp extends Component
'proxy' => [
"type" => ProxyTypes::TRAEFIK_V2->value,
"status" => ProxyStatus::EXITED->value,
- ]
-
+ ],
]);
$server->settings->is_part_of_swarm = $this->is_part_of_swarm;
$server->settings->save();
+ $server->addInitialNetwork();
return redirect()->route('server.show', $server->uuid);
} catch (\Throwable $e) {
return handleError($e, $this);
diff --git a/app/Models/Server.php b/app/Models/Server.php
index ca5bd9559..9a0c4ce7d 100644
--- a/app/Models/Server.php
+++ b/app/Models/Server.php
@@ -35,25 +35,10 @@ class Server extends BaseModel
}
$server->forceFill($payload);
});
-
static::created(function ($server) {
ServerSetting::create([
'server_id' => $server->id,
]);
- if ($server->id === 0) {
- StandaloneDocker::create([
- 'id' => 0,
- 'name' => 'coolify',
- 'network' => 'coolify',
- 'server_id' => $server->id,
- ]);
- } else {
- StandaloneDocker::create([
- 'name' => 'coolify',
- 'network' => 'coolify',
- 'server_id' => $server->id,
- ]);
- }
});
static::deleting(function ($server) {
$server->destinations()->each(function ($destination) {
@@ -101,7 +86,39 @@ class Server extends BaseModel
{
return $this->hasOne(ServerSetting::class);
}
-
+ public function addInitialNetwork() {
+ if ($this->id === 0) {
+ if ($this->isSwarm()) {
+ SwarmDocker::create([
+ 'id' => 0,
+ 'name' => 'coolify',
+ 'network' => 'coolify-overlay',
+ 'server_id' => $this->id,
+ ]);
+ } else {
+ StandaloneDocker::create([
+ 'id' => 0,
+ 'name' => 'coolify',
+ 'network' => 'coolify',
+ 'server_id' => $this->id,
+ ]);
+ }
+ } else {
+ if ($this->isSwarm()) {
+ SwarmDocker::create([
+ 'name' => 'coolify',
+ 'network' => 'coolify-overlay',
+ 'server_id' => $this->id,
+ ]);
+ } else {
+ StandaloneDocker::create([
+ 'name' => 'coolify',
+ 'network' => 'coolify',
+ 'server_id' => $this->id,
+ ]);
+ }
+ }
+ }
public function proxyType()
{
$proxyType = $this->proxy->get('type');
@@ -357,12 +374,16 @@ class Server extends BaseModel
return false;
}
}
+ public function isSwarm()
+ {
+ return data_get($this, 'settings.is_part_of_swarm');
+ }
public function validateConnection()
{
+ $server = Server::find($this->id);
if ($this->skipServer()) {
return false;
}
-
$uptime = instant_remote_process(['uptime'], $this, false);
if (!$uptime) {
$this->settings()->update([
@@ -373,14 +394,14 @@ class Server extends BaseModel
$this->settings()->update([
'is_reachable' => true,
]);
- $this->update([
+ $server->update([
'unreachable_count' => 0,
]);
}
if (data_get($this, 'unreachable_notification_sent') === true) {
$this->team->notify(new Revived($this));
- $this->update(['unreachable_notification_sent' => false]);
+ $server->update(['unreachable_notification_sent' => false]);
}
return true;
@@ -398,7 +419,20 @@ class Server extends BaseModel
}
$this->settings->is_usable = true;
$this->settings->save();
- $this->validateCoolifyNetwork();
+ $this->validateCoolifyNetwork(isSwarm: false);
+ return true;
+ }
+ public function validateDockerSwarm()
+ {
+ $swarmStatus = instant_remote_process(["docker info|grep -i swarm"], $this, false);
+ $swarmStatus = str($swarmStatus)->trim()->after(':')->trim();
+ if ($swarmStatus === 'inactive') {
+ throw new \Exception('Docker Swarm is not initiated. Please join the server to a swarm before continuing.');
+ return false;
+ }
+ $this->settings->is_usable = true;
+ $this->settings->save();
+ $this->validateCoolifyNetwork(isSwarm: true);
return true;
}
public function validateDockerEngineVersion()
@@ -415,9 +449,13 @@ class Server extends BaseModel
$this->settings->save();
return true;
}
- public function validateCoolifyNetwork()
+ public function validateCoolifyNetwork($isSwarm = false)
{
- return instant_remote_process(["docker network create coolify --attachable >/dev/null 2>&1 || true"], $this, false);
+ if ($isSwarm) {
+ return instant_remote_process(["docker network create --driver overlay coolify-overlay >/dev/null 2>&1 || true"], $this, false);
+ } else {
+ return instant_remote_process(["docker network create coolify --attachable >/dev/null 2>&1 || true"], $this, false);
+ }
}
public function executeRemoteCommand(Collection $commands, ?ApplicationDeploymentQueue $loggingModel = null)
{
diff --git a/app/Models/SwarmDocker.php b/app/Models/SwarmDocker.php
index ea56f85bc..9f0973db5 100644
--- a/app/Models/SwarmDocker.php
+++ b/app/Models/SwarmDocker.php
@@ -4,13 +4,57 @@ namespace App\Models;
class SwarmDocker extends BaseModel
{
+ protected $guarded = [];
+
public function applications()
{
return $this->morphMany(Application::class, 'destination');
}
+ public function postgresqls()
+ {
+ return $this->morphMany(StandalonePostgresql::class, 'destination');
+ }
+
+ public function redis()
+ {
+ return $this->morphMany(StandaloneRedis::class, 'destination');
+ }
+ public function mongodbs()
+ {
+ return $this->morphMany(StandaloneMongodb::class, 'destination');
+ }
+ public function mysqls()
+ {
+ return $this->morphMany(StandaloneMysql::class, 'destination');
+ }
+ public function mariadbs()
+ {
+ return $this->morphMany(StandaloneMariadb::class, 'destination');
+ }
+
public function server()
{
return $this->belongsTo(Server::class);
}
+
+ public function services()
+ {
+ return $this->morphMany(Service::class, 'destination');
+ }
+
+ public function databases()
+ {
+ $postgresqls = $this->postgresqls;
+ $redis = $this->redis;
+ $mongodbs = $this->mongodbs;
+ $mysqls = $this->mysqls;
+ $mariadbs = $this->mariadbs;
+ return $postgresqls->concat($redis)->concat($mongodbs)->concat($mysqls)->concat($mariadbs);
+ }
+
+ public function attachedTo()
+ {
+ return $this->applications?->count() > 0 || $this->databases()->count() > 0;
+ }
}
diff --git a/database/migrations/2023_11_28_143533_add_fields_to_swarm_dockers.php b/database/migrations/2023_11_28_143533_add_fields_to_swarm_dockers.php
new file mode 100644
index 000000000..7ca398b90
--- /dev/null
+++ b/database/migrations/2023_11_28_143533_add_fields_to_swarm_dockers.php
@@ -0,0 +1,30 @@
+string('network');
+
+ $table->unique(['server_id', 'network']);
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('swarm_dockers', function (Blueprint $table) {
+ $table->dropColumn('network');
+ });
+ }
+};
diff --git a/resources/views/livewire/boarding/index.blade.php b/resources/views/livewire/boarding/index.blade.php
index 75f2441f0..389633b2d 100644
--- a/resources/views/livewire/boarding/index.blade.php
+++ b/resources/views/livewire/boarding/index.blade.php
@@ -131,16 +131,16 @@
@if (!$serverReachable)
- This server is not reachable with the following public key.
-
- Please make sure you have the correct public key in your ~/.ssh/authorized_keys file for user
- 'root' or skip the boarding process and add a new private key manually to Coolify and to the
- server.
-
- Check again
-
- @endif
+ This server is not reachable with the following public key.
+
+ Please make sure you have the correct public key in your ~/.ssh/authorized_keys file for user
+ 'root' or skip the boarding process and add a new private key manually to Coolify and to the
+ server.
+
+ Check
+ again
+
+ @endif
Private Keys are used to connect to a remote server through a secure shell, called SSH.
@@ -200,14 +200,17 @@
label="Description" id="remoteServerDescription" />
-
+
+
+
+
Check Connection
@@ -226,7 +229,7 @@
- Let's do it!
+ Let's do it!
@if ($dockerInstallationStarted)
Validate Server & Continue
@@ -234,7 +237,10 @@
This will install the latest Docker Engine on your server, configure a few things to be able
- to run optimal.
Minimum Docker Engine version is: 22
To manually install Docker Engine, check this documentation.
+ to run optimal.
Minimum Docker Engine version is: 22
To manually install Docker
+ Engine, check this
+ documentation.
diff --git a/resources/views/livewire/server/form.blade.php b/resources/views/livewire/server/form.blade.php
index 4004dd096..0a98128cf 100644
--- a/resources/views/livewire/server/form.blade.php
+++ b/resources/views/livewire/server/form.blade.php
@@ -38,8 +38,7 @@
- {{-- --}}
+
- @if (!$server->isLocalhost())
-
+
+ @if (!$server->isLocalhost())
-
- @endif
+ @endif
+
+
@if ($server->isFunctional())
diff --git a/resources/views/livewire/server/new/by-ip.blade.php b/resources/views/livewire/server/new/by-ip.blade.php
index 7dfb3a5b0..9503a36a4 100644
--- a/resources/views/livewire/server/new/by-ip.blade.php
+++ b/resources/views/livewire/server/new/by-ip.blade.php
@@ -3,7 +3,7 @@
@else
Create a new Server
- Servers are the main blocks of your infrastructure.
+ Servers are the main blocks of your infrastructure.
diff --git a/resources/views/livewire/server/form.blade.php b/resources/views/livewire/server/form.blade.php
index 0a98128cf..c124a6b25 100644
--- a/resources/views/livewire/server/form.blade.php
+++ b/resources/views/livewire/server/form.blade.php
@@ -55,7 +55,7 @@
id="server.settings.is_cloudflare_tunnel" label="Cloudflare Tunnel" />
@endif
+ label="Is it a Swarm Manager?" />
diff --git a/resources/views/livewire/server/new/by-ip.blade.php b/resources/views/livewire/server/new/by-ip.blade.php
index 9503a36a4..410f27fa7 100644
--- a/resources/views/livewire/server/new/by-ip.blade.php
+++ b/resources/views/livewire/server/new/by-ip.blade.php
@@ -27,7 +27,7 @@
+ label="Is it a Swarm Manager?" />
Save New Server
From 928b68043b85be34e41587c67da4f1838470898f Mon Sep 17 00:00:00 2001
From: Andras Bacsai
Date: Tue, 28 Nov 2023 20:49:38 +0100
Subject: [PATCH 11/15] wip: swarm
---
app/Jobs/ApplicationDeploymentJob.php | 36 ++++++++++++++++++++++++++-
1 file changed, 35 insertions(+), 1 deletion(-)
diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php
index 17d9b2c9b..02473e0a5 100644
--- a/app/Jobs/ApplicationDeploymentJob.php
+++ b/app/Jobs/ApplicationDeploymentJob.php
@@ -877,7 +877,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
'container_name' => $this->container_name,
'restart' => RESTART_MODE,
'environment' => $environment_variables,
- 'labels' => $labels,
'expose' => $ports,
'networks' => [
$this->destination->network,
@@ -921,7 +920,37 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
data_forget($docker_compose, 'services.' . $this->container_name . '.cpus');
data_forget($docker_compose, 'services.' . $this->container_name . '.cpuset');
data_forget($docker_compose, 'services.' . $this->container_name . '.cpu_shares');
+
+ $docker_compose['services'][$this->container_name]['deploy'] = [
+ 'placement' => [
+ 'constraints' => [
+ 'node.role == worker'
+ ]
+ ],
+ 'mode' => 'replicated',
+ 'replicas' => 1,
+ 'update_config' => [
+ 'order' => 'start-first'
+ ],
+ 'rollback_config' => [
+ 'order' => 'start-first'
+ ],
+ 'labels' => $labels,
+ 'resources' => [
+ 'limits' => [
+ 'cpus' => $this->application->limits_cpus,
+ 'memory' => $this->application->limits_memory,
+ ],
+ 'reservations' => [
+ 'cpus' => $this->application->limits_cpus,
+ 'memory' => $this->application->limits_memory,
+ ]
+ ]
+ ];
+
} else {
+ $docker_compose['services'][$this->container_name]['labels'] = $labels;
+
}
if ($this->server->isLogDrainEnabled() && $this->application->isLogDrainEnabled()) {
$docker_compose['services'][$this->container_name]['logging'] = [
@@ -970,6 +999,11 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
// 'dockerfile' => $this->workdir . $this->dockerfile_location,
// ];
// }
+
+ $docker_compose['services'][$this->application->uuid] = $docker_compose['services'][$this->container_name];
+
+ data_forget($docker_compose, 'services.' . $this->container_name);
+
$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]);
From 2d7bbbe30089b8b75aa3ae51a544aac8f6e27827 Mon Sep 17 00:00:00 2001
From: Andras Bacsai
Date: Wed, 29 Nov 2023 10:06:52 +0100
Subject: [PATCH 12/15] wip: swarm
---
app/Actions/Proxy/StartProxy.php | 2 +-
app/Actions/Server/InstallDocker.php | 16 ++++++--
app/Http/Livewire/Boarding/Index.php | 4 +-
app/Http/Livewire/Server/Form.php | 8 ++--
app/Http/Livewire/Server/New/ByIp.php | 8 ++--
app/Jobs/ApplicationDeploymentJob.php | 15 ++++---
app/Jobs/ContainerStatusJob.php | 5 +++
app/Models/Server.php | 2 +-
bootstrap/helpers/proxy.php | 40 ++++++++++++-------
...3_11_29_075937_change_swarm_properties.php | 30 ++++++++++++++
.../components/applications/navbar.blade.php | 2 +
.../views/livewire/boarding/index.blade.php | 2 +-
.../views/livewire/server/form.blade.php | 4 +-
.../views/livewire/server/new/by-ip.blade.php | 2 +-
scripts/upgrade.sh | 3 +-
15 files changed, 106 insertions(+), 37 deletions(-)
create mode 100644 database/migrations/2023_11_29_075937_change_swarm_properties.php
diff --git a/app/Actions/Proxy/StartProxy.php b/app/Actions/Proxy/StartProxy.php
index b6f657b35..a99e47b25 100644
--- a/app/Actions/Proxy/StartProxy.php
+++ b/app/Actions/Proxy/StartProxy.php
@@ -30,7 +30,7 @@ class StartProxy
"mkdir -p $proxy_path && cd $proxy_path",
"echo 'Creating required Docker Compose file.'",
"echo 'Starting coolify-proxy.'",
- // "docker stack deploy -c docker-compose.yaml coolify-proxy",
+ "cd $proxy_path && docker stack deploy -c docker-compose.yml coolify-proxy",
"echo 'Proxy started successfully.'"
]);
} else {
diff --git a/app/Actions/Server/InstallDocker.php b/app/Actions/Server/InstallDocker.php
index a580c3473..79863d989 100644
--- a/app/Actions/Server/InstallDocker.php
+++ b/app/Actions/Server/InstallDocker.php
@@ -76,10 +76,20 @@ class InstallDocker
"echo 'Restarting Docker Engine...'",
"systemctl enable docker >/dev/null 2>&1 || true",
"systemctl restart docker",
- "echo 'Creating default Docker network (coolify)...'",
- "docker network create --attachable coolify >/dev/null 2>&1 || true",
- "echo 'Done!'"
]);
+ if ($server->isSwarm()) {
+ $command = $command->merge([
+ "docker network create --attachable --driver overlay coolify-overlay >/dev/null 2>&1 || true",
+ ]);
+ } else {
+ $command = $command->merge([
+ "docker network create --attachable coolify >/dev/null 2>&1 || true",
+ ]);
+ $command = $command->merge([
+ "echo 'Done!'",
+ ]);
+ }
+
return remote_process($command, $server);
}
}
diff --git a/app/Http/Livewire/Boarding/Index.php b/app/Http/Livewire/Boarding/Index.php
index 645b0da6a..67221d0c6 100644
--- a/app/Http/Livewire/Boarding/Index.php
+++ b/app/Http/Livewire/Boarding/Index.php
@@ -31,7 +31,7 @@ class Index extends Component
public ?string $remoteServerHost = null;
public ?int $remoteServerPort = 22;
public ?string $remoteServerUser = 'root';
- public bool $isPartOfSwarm = false;
+ public bool $isSwarmManager = false;
public ?Server $createdServer = null;
public Collection $projects;
@@ -183,7 +183,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
'private_key_id' => $this->createdPrivateKey->id,
'team_id' => currentTeam()->id,
]);
- $this->createdServer->settings->is_part_of_swarm = $this->isPartOfSwarm;
+ $this->createdServer->settings->is_swarm_manager = $this->isSwarmManager;
$this->createdServer->settings->save();
$this->createdServer->addInitialNetwork();
$this->validateServer();
diff --git a/app/Http/Livewire/Server/Form.php b/app/Http/Livewire/Server/Form.php
index 27dfaa8e2..efca800c2 100644
--- a/app/Http/Livewire/Server/Form.php
+++ b/app/Http/Livewire/Server/Form.php
@@ -24,7 +24,8 @@ class Form extends Component
'server.port' => 'required',
'server.settings.is_cloudflare_tunnel' => 'required|boolean',
'server.settings.is_reachable' => 'required',
- 'server.settings.is_part_of_swarm' => 'required|boolean',
+ 'server.settings.is_swarm_manager' => 'required|boolean',
+ // 'server.settings.is_swarm_worker' => 'required|boolean',
'wildcard_domain' => 'nullable|url',
];
protected $validationAttributes = [
@@ -34,8 +35,9 @@ class Form extends Component
'server.user' => 'User',
'server.port' => 'Port',
'server.settings.is_cloudflare_tunnel' => 'Cloudflare Tunnel',
- 'server.settings.is_reachable' => 'is reachable',
- 'server.settings.is_part_of_swarm' => 'is part of swarm'
+ 'server.settings.is_reachable' => 'Is reachable',
+ 'server.settings.is_swarm_manager' => 'Swarm Manager',
+ // 'server.settings.is_swarm_worker' => 'Swarm Worker',
];
public function mount()
diff --git a/app/Http/Livewire/Server/New/ByIp.php b/app/Http/Livewire/Server/New/ByIp.php
index 139705d94..858f4ffa1 100644
--- a/app/Http/Livewire/Server/New/ByIp.php
+++ b/app/Http/Livewire/Server/New/ByIp.php
@@ -21,7 +21,7 @@ class ByIp extends Component
public string $ip;
public string $user = 'root';
public int $port = 22;
- public bool $is_part_of_swarm = false;
+ public bool $is_swarm_manager = false;
protected $rules = [
'name' => 'required|string',
@@ -29,7 +29,7 @@ class ByIp extends Component
'ip' => 'required',
'user' => 'required|string',
'port' => 'required|integer',
- 'is_part_of_swarm' => 'required|boolean',
+ 'is_swarm_manager' => 'required|boolean',
];
protected $validationAttributes = [
'name' => 'Name',
@@ -37,7 +37,7 @@ class ByIp extends Component
'ip' => 'IP Address/Domain',
'user' => 'User',
'port' => 'Port',
- 'is_part_of_swarm' => 'Is part of swarm',
+ 'is_swarm_manager' => 'Swarm Manager',
];
public function mount()
@@ -76,7 +76,7 @@ class ByIp extends Component
"status" => ProxyStatus::EXITED->value,
],
]);
- $server->settings->is_part_of_swarm = $this->is_part_of_swarm;
+ $server->settings->is_swarm_manager = $this->is_swarm_manager;
$server->settings->save();
$server->addInitialNetwork();
return redirect()->route('server.show', $server->uuid);
diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php
index 02473e0a5..bd6d7436a 100644
--- a/app/Jobs/ApplicationDeploymentJob.php
+++ b/app/Jobs/ApplicationDeploymentJob.php
@@ -453,11 +453,16 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if ($this->pull_request_id !== 0) {
$networkId = "{$this->application->uuid}-{$this->pull_request_id}";
}
- $this->execute_remote_command([
- "docker network create --attachable '{$networkId}' >/dev/null || true", "hidden" => true, "ignore_errors" => true
- ], [
- "docker network connect {$networkId} coolify-proxy || true", "hidden" => true, "ignore_errors" => true
- ]);
+ if ($this->server->isSwarm()) {
+ // TODO
+ } else {
+ $this->execute_remote_command([
+ "docker network create --attachable '{$networkId}' >/dev/null || true", "hidden" => true, "ignore_errors" => true
+ ], [
+ "docker network connect {$networkId} coolify-proxy || true", "hidden" => true, "ignore_errors" => true
+ ]);
+ }
+
$this->start_by_compose_file();
$this->application->loadComposeFile(isInit: false);
}
diff --git a/app/Jobs/ContainerStatusJob.php b/app/Jobs/ContainerStatusJob.php
index e30729299..30324baab 100644
--- a/app/Jobs/ContainerStatusJob.php
+++ b/app/Jobs/ContainerStatusJob.php
@@ -42,6 +42,11 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
if (!$this->server->isServerReady()) {
return;
};
+ if ($this->server->isSwarm()) {
+
+ } else {
+
+ }
$containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this->server, false);
if (is_null($containers)) {
return;
diff --git a/app/Models/Server.php b/app/Models/Server.php
index 5e3762971..408106107 100644
--- a/app/Models/Server.php
+++ b/app/Models/Server.php
@@ -378,7 +378,7 @@ class Server extends BaseModel
}
public function isSwarm()
{
- return data_get($this, 'settings.is_part_of_swarm');
+ return data_get($this, 'settings.is_swarm_manager') || data_get($this, 'settings.is_swarm_worker');
}
public function validateConnection()
{
diff --git a/bootstrap/helpers/proxy.php b/bootstrap/helpers/proxy.php
index 614988eca..76fa9cc5a 100644
--- a/bootstrap/helpers/proxy.php
+++ b/bootstrap/helpers/proxy.php
@@ -54,9 +54,15 @@ function connectProxyToNetworks(Server $server)
function generate_default_proxy_configuration(Server $server)
{
$proxy_path = get_proxy_path();
- $networks = collect($server->standaloneDockers)->map(function ($docker) {
- return $docker['network'];
- })->unique();
+ if ($server->isSwarm()) {
+ $networks = collect($server->swarmDockers)->map(function ($docker) {
+ return $docker['network'];
+ })->unique();
+ } else {
+ $networks = collect($server->standaloneDockers)->map(function ($docker) {
+ return $docker['network'];
+ })->unique();
+ }
if ($networks->count() === 0) {
$networks = collect(['coolify']);
}
@@ -66,6 +72,16 @@ function generate_default_proxy_configuration(Server $server)
"external" => true,
];
});
+ $labels = [
+ "traefik.enable=true",
+ "traefik.http.routers.traefik.entrypoints=http",
+ "traefik.http.routers.traefik.middlewares=traefik-basic-auth@file",
+ "traefik.http.routers.traefik.service=api@internal",
+ "traefik.http.services.traefik.loadbalancer.server.port=8080",
+ // Global Middlewares
+ "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https",
+ "traefik.http.middlewares.gzip.compress=true",
+ ];
$config = [
"version" => "3.8",
"networks" => $array_of_networks->toArray(),
@@ -109,16 +125,7 @@ function generate_default_proxy_configuration(Server $server)
"--certificatesresolvers.letsencrypt.acme.storage=/traefik/acme.json",
"--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=http",
],
- "labels" => [
- "traefik.enable=true",
- "traefik.http.routers.traefik.entrypoints=http",
- "traefik.http.routers.traefik.middlewares=traefik-basic-auth@file",
- "traefik.http.routers.traefik.service=api@internal",
- "traefik.http.services.traefik.loadbalancer.server.port=8080",
- // Global Middlewares
- "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https",
- "traefik.http.middlewares.gzip.compress=true",
- ],
+ "labels" => $labels,
],
],
];
@@ -128,8 +135,13 @@ function generate_default_proxy_configuration(Server $server)
$config['services']['traefik']['command'][] = "--accesslog.bufferingsize=100";
}
if ($server->isSwarm()) {
+ data_forget($config, 'services.traefik.container_name');
+ data_forget($config, 'services.traefik.restart');
+ data_forget($config, 'services.traefik.labels');
+
$config['services']['traefik']['command'][] = "--providers.docker.swarmMode=true";
$config['services']['traefik']['deploy'] = [
+ "labels" => $labels,
"placement" => [
"constraints" => [
"node.role==manager",
@@ -139,7 +151,7 @@ function generate_default_proxy_configuration(Server $server)
} else {
$config['services']['traefik']['command'][] = "--providers.docker=true";
}
- $config = Yaml::dump($config, 4, 2);
+ $config = Yaml::dump($config, 12, 2);
SaveConfiguration::run($server, $config);
return $config;
}
diff --git a/database/migrations/2023_11_29_075937_change_swarm_properties.php b/database/migrations/2023_11_29_075937_change_swarm_properties.php
new file mode 100644
index 000000000..6c0edc432
--- /dev/null
+++ b/database/migrations/2023_11_29_075937_change_swarm_properties.php
@@ -0,0 +1,30 @@
+renameColumn('is_part_of_swarm', 'is_swarm_manager');
+ $table->boolean('is_swarm_worker')->default(false);
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('server_settings', function (Blueprint $table) {
+ $table->renameColumn('is_swarm_manager', 'is_part_of_swarm');
+ $table->dropColumn('is_swarm_worker');
+ });
+ }
+};
diff --git a/resources/views/components/applications/navbar.blade.php b/resources/views/components/applications/navbar.blade.php
index d0cd565dc..9bd048e26 100644
--- a/resources/views/components/applications/navbar.blade.php
+++ b/resources/views/components/applications/navbar.blade.php
@@ -15,6 +15,8 @@
@if ($application->build_pack === 'dockercompose' && is_null($application->docker_compose_raw))
Please load a Compose file.
+ @elseif ($application->destination->server->isSwarm() && str($application->docker_registry_image_name)->isEmpty())
+ Swarm Deployments requires a Docker Image in a Registry.
@else
@if ($application->status !== 'exited')
diff --git a/resources/views/livewire/boarding/index.blade.php b/resources/views/livewire/boarding/index.blade.php
index 9f8e47961..fb102c7c5 100644
--- a/resources/views/livewire/boarding/index.blade.php
+++ b/resources/views/livewire/boarding/index.blade.php
@@ -208,7 +208,7 @@
id="remoteServerUser" />
-
Check Connection
diff --git a/resources/views/livewire/server/form.blade.php b/resources/views/livewire/server/form.blade.php
index c124a6b25..528fd32c5 100644
--- a/resources/views/livewire/server/form.blade.php
+++ b/resources/views/livewire/server/form.blade.php
@@ -54,8 +54,10 @@
helper="If you are using Cloudflare Tunnels, enable this. It will proxy all ssh requests to your server through Cloudflare.Coolify does not install/setup Cloudflare (cloudflared) on your server."
id="server.settings.is_cloudflare_tunnel" label="Cloudflare Tunnel" />
@endif
-
+ {{-- --}}
diff --git a/resources/views/livewire/server/new/by-ip.blade.php b/resources/views/livewire/server/new/by-ip.blade.php
index 410f27fa7..62268f2fe 100644
--- a/resources/views/livewire/server/new/by-ip.blade.php
+++ b/resources/views/livewire/server/new/by-ip.blade.php
@@ -26,7 +26,7 @@
@endforeach
-
diff --git a/scripts/upgrade.sh b/scripts/upgrade.sh
index 3c44308a0..af6463869 100644
--- a/scripts/upgrade.sh
+++ b/scripts/upgrade.sh
@@ -16,6 +16,7 @@ curl -fsSL $CDN/.env.production -o /data/coolify/source/.env.production
sort -u -t '=' -k 1,1 /data/coolify/source/.env /data/coolify/source/.env.production | sed '/^$/d' > /data/coolify/source/.env.temp && mv /data/coolify/source/.env.temp /data/coolify/source/.env
# Make sure coolify network exists
-docker network create coolify 2>/dev/null
+docker network create --attachable coolify 2>/dev/null
+# docker network create --attachable --driver=overlay coolify-overlay 2>/dev/null
docker run --pull always -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock --rm ghcr.io/coollabsio/coolify-helper bash -c "LATEST_IMAGE=${1:-} docker compose --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml up -d --pull always --remove-orphans --force-recreate"
From f4803ad58bfb8f7cb12a7c296338767b06f3c66a Mon Sep 17 00:00:00 2001
From: Andras Bacsai
Date: Wed, 29 Nov 2023 14:59:06 +0100
Subject: [PATCH 13/15] wip: swarm fix: gitcompose deployments
---
app/Actions/Proxy/CheckProxy.php | 55 +++++++++++--------
app/Http/Livewire/Server/Proxy/Deploy.php | 26 +++++++--
app/Http/Livewire/Server/Proxy/Status.php | 2 +-
app/Jobs/ApplicationDeploymentJob.php | 34 +++---------
app/Jobs/ContainerStatusJob.php | 43 +++++++++++++--
app/Models/Application.php | 49 +++++++++--------
bootstrap/helpers/docker.php | 20 ++++++-
bootstrap/helpers/shared.php | 2 +-
.../views/livewire/boarding/index.blade.php | 4 +-
.../views/livewire/server/form.blade.php | 4 +-
.../views/livewire/server/new/by-ip.blade.php | 4 +-
.../livewire/server/proxy/status.blade.php | 2 +-
12 files changed, 149 insertions(+), 96 deletions(-)
diff --git a/app/Actions/Proxy/CheckProxy.php b/app/Actions/Proxy/CheckProxy.php
index 279fac20e..32673abc9 100644
--- a/app/Actions/Proxy/CheckProxy.php
+++ b/app/Actions/Proxy/CheckProxy.php
@@ -17,35 +17,42 @@ class CheckProxy
return false;
}
}
- $status = getContainerStatus($server, 'coolify-proxy');
- if ($status === 'running') {
- $server->proxy->set('status', 'running');
+ if ($server->isSwarm()) {
+ $status = getContainerStatus($server, 'coolify-proxy_traefik');
+ $server->proxy->set('status', $status);
$server->save();
return false;
- }
- $ip = $server->ip;
- if ($server->id === 0) {
- $ip = 'host.docker.internal';
- }
+ } else {
+ $status = getContainerStatus($server, 'coolify-proxy');
+ if ($status === 'running') {
+ $server->proxy->set('status', 'running');
+ $server->save();
+ return false;
+ }
+ $ip = $server->ip;
+ if ($server->id === 0) {
+ $ip = 'host.docker.internal';
+ }
- $connection80 = @fsockopen($ip, '80');
- $connection443 = @fsockopen($ip, '443');
- $port80 = is_resource($connection80) && fclose($connection80);
- $port443 = is_resource($connection443) && fclose($connection443);
- if ($port80) {
- if ($fromUI) {
- throw new \Exception("Port 80 is in use.
You must stop the process using this port.
Docs: https://coolify.io/docs
Discord: https://coollabs.io/discord");
- } else {
- return false;
+ $connection80 = @fsockopen($ip, '80');
+ $connection443 = @fsockopen($ip, '443');
+ $port80 = is_resource($connection80) && fclose($connection80);
+ $port443 = is_resource($connection443) && fclose($connection443);
+ if ($port80) {
+ if ($fromUI) {
+ throw new \Exception("Port 80 is in use.
You must stop the process using this port.
Docs: https://coolify.io/docs
Discord: https://coollabs.io/discord");
+ } else {
+ return false;
+ }
}
- }
- if ($port443) {
- if ($fromUI) {
- throw new \Exception("Port 443 is in use.
You must stop the process using this port.
Docs: https://coolify.io/docs
Discord: https://coollabs.io/discord");
- } else {
- return false;
+ if ($port443) {
+ if ($fromUI) {
+ throw new \Exception("Port 443 is in use.
You must stop the process using this port.
Docs: https://coolify.io/docs
Discord: https://coollabs.io/discord");
+ } else {
+ return false;
+ }
}
+ return true;
}
- return true;
}
}
diff --git a/app/Http/Livewire/Server/Proxy/Deploy.php b/app/Http/Livewire/Server/Proxy/Deploy.php
index 7e828b092..9612eccc7 100644
--- a/app/Http/Livewire/Server/Proxy/Deploy.php
+++ b/app/Http/Livewire/Server/Proxy/Deploy.php
@@ -58,11 +58,25 @@ class Deploy extends Component
public function stop()
{
- instant_remote_process([
- "docker rm -f coolify-proxy",
- ], $this->server);
- $this->server->proxy->status = 'exited';
- $this->server->save();
- $this->emit('proxyStatusUpdated');
+ try {
+ if ($this->server->isSwarm()) {
+ instant_remote_process([
+ "docker service rm coolify-proxy_traefik",
+ ], $this->server);
+ $this->server->proxy->status = 'exited';
+ $this->server->save();
+ $this->emit('proxyStatusUpdated');
+ } else {
+ instant_remote_process([
+ "docker rm -f coolify-proxy",
+ ], $this->server);
+ $this->server->proxy->status = 'exited';
+ $this->server->save();
+ $this->emit('proxyStatusUpdated');
+ }
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+
}
}
diff --git a/app/Http/Livewire/Server/Proxy/Status.php b/app/Http/Livewire/Server/Proxy/Status.php
index 8df8f10cd..0c5f274b5 100644
--- a/app/Http/Livewire/Server/Proxy/Status.php
+++ b/app/Http/Livewire/Server/Proxy/Status.php
@@ -16,7 +16,7 @@ class Status extends Component
protected $listeners = ['proxyStatusUpdated', 'startProxyPolling'];
public function startProxyPolling()
{
- $this->polling = true;
+ $this->checkProxy();
}
public function proxyStatusUpdated()
{
diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php
index bd6d7436a..d0f18d00c 100644
--- a/app/Jobs/ApplicationDeploymentJob.php
+++ b/app/Jobs/ApplicationDeploymentJob.php
@@ -220,8 +220,11 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->application_deployment_queue->addLogEntry("Creating / updating stack.");
$this->execute_remote_command(
[
- "docker stack deploy --with-registry-auth --prune --compose-file {$this->configuration_dir}/docker-compose.yml {$this->application->uuid}"
+ executeInDocker($this->deployment_uuid, "cd {$this->workdir} && docker stack deploy --with-registry-auth -c docker-compose.yml {$this->application->uuid}")
],
+ [
+ "echo 'Stack deployed. It may take a few minutes to fully available in your swarm.'"
+ ]
);
}
}
@@ -376,7 +379,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$envs->push($env->key . '=' . $env->value);
}
}
- ray($envs);
$envs_base64 = base64_encode($envs->implode("\n"));
$this->execute_remote_command(
[
@@ -442,17 +444,21 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->cleanup_git();
$composeFile = $this->application->parseCompose(pull_request_id: $this->pull_request_id);
$yaml = Yaml::dump($composeFile->toArray(), 10);
+ ray($composeFile);
+ ray($this->container_name);
$this->docker_compose_base64 = base64_encode($yaml);
$this->execute_remote_command([
- executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d > {$this->workdir}/docker-compose.yaml"), "hidden" => true
+ executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d > {$this->workdir}{$this->docker_compose_location}"), "hidden" => true
]);
$this->save_environment_variables();
$this->stop_running_container(force: true);
+ ray($this->pull_request_id);
$networkId = $this->application->uuid;
if ($this->pull_request_id !== 0) {
$networkId = "{$this->application->uuid}-{$this->pull_request_id}";
}
+ ray($networkId);
if ($this->server->isSwarm()) {
// TODO
} else {
@@ -832,26 +838,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->env_args = $this->env_args->implode(' ');
}
- private function modify_compose_file()
- {
- // ray("{$this->workdir}{$this->docker_compose_location}");
- $this->execute_remote_command([executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->docker_compose_location}"), "hidden" => true, "save" => 'compose_file']);
- if ($this->saved_outputs->get('compose_file')) {
- $compose = $this->saved_outputs->get('compose_file');
- }
- try {
- $yaml = Yaml::parse($compose);
- } catch (\Exception $e) {
- throw new \Exception($e->getMessage());
- }
- $services = data_get($yaml, 'services');
- $topLevelNetworks = collect(data_get($yaml, 'networks', []));
- $definedNetwork = collect([$this->application->uuid]);
-
- $services = collect($services)->map(function ($service, $serviceName) use ($topLevelNetworks, $definedNetwork) {
- $serviceNetworks = collect(data_get($service, 'networks', []));
- });
- }
private function generate_compose_file()
{
$ports = $this->application->settings->is_static ? [80] : $this->application->ports_exposes_array;
@@ -952,10 +938,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
]
]
];
-
} else {
$docker_compose['services'][$this->container_name]['labels'] = $labels;
-
}
if ($this->server->isLogDrainEnabled() && $this->application->isLogDrainEnabled()) {
$docker_compose['services'][$this->container_name]['logging'] = [
diff --git a/app/Jobs/ContainerStatusJob.php b/app/Jobs/ContainerStatusJob.php
index 30324baab..7063e01cd 100644
--- a/app/Jobs/ContainerStatusJob.php
+++ b/app/Jobs/ContainerStatusJob.php
@@ -43,15 +43,37 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
return;
};
if ($this->server->isSwarm()) {
-
+ $containers = instant_remote_process(["docker service inspect $(docker service ls -q) --format '{{json .}}'"], $this->server, false);
+ $containerReplicase = instant_remote_process(["docker service ls --format '{{json .}}'"], $this->server, false);
} else {
-
+ $containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this->server, false);
+ $containerReplicase = null;
}
- $containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this->server, false);
if (is_null($containers)) {
return;
}
$containers = format_docker_command_output_to_json($containers);
+ if ($containerReplicase) {
+ $containerReplicase = format_docker_command_output_to_json($containerReplicase);
+ foreach ($containerReplicase as $containerReplica) {
+ $name = data_get($containerReplica, 'Name');
+ $containers = $containers->map(function ($container) use ($name, $containerReplica) {
+ if (data_get($container, 'Spec.Name') === $name) {
+ $replicas = data_get($containerReplica, 'Replicas');
+ $running = str($replicas)->explode('/')[0];
+ $total = str($replicas)->explode('/')[1];
+ if ($running === $total) {
+ data_set($container, 'State.Status', 'running');
+ data_set($container, 'State.Health.Status', 'healthy');
+ } else {
+ data_set($container, 'State.Status', 'starting');
+ data_set($container, 'State.Health.Status', 'unhealthy');
+ }
+ }
+ return $container;
+ });
+ }
+ }
$applications = $this->server->applications();
$databases = $this->server->databases();
$services = $this->server->services()->get();
@@ -63,10 +85,16 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
$foundServices = [];
foreach ($containers as $container) {
+ if ($this->server->isSwarm()) {
+ $labels = data_get($container, 'Spec.Labels');
+ $uuid = data_get($labels, 'coolify.name');
+ } else {
+ $labels = data_get($container, 'Config.Labels');
+ $uuid = data_get($labels, 'com.docker.compose.service');
+ }
$containerStatus = data_get($container, 'State.Status');
$containerHealth = data_get($container, 'State.Health.Status', 'unhealthy');
$containerStatus = "$containerStatus ($containerHealth)";
- $labels = data_get($container, 'Config.Labels');
$labels = Arr::undot(format_docker_labels_to_json($labels));
$applicationId = data_get($labels, 'coolify.applicationId');
if ($applicationId) {
@@ -98,7 +126,6 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
}
}
} else {
- $uuid = data_get($labels, 'com.docker.compose.service');
if ($uuid) {
$database = $databases->where('uuid', $uuid)->first();
if ($database) {
@@ -253,7 +280,11 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
// Check if proxy is running
$this->server->proxyType();
$foundProxyContainer = $containers->filter(function ($value, $key) {
- return data_get($value, 'Name') === '/coolify-proxy';
+ if ($this->server->isSwarm()) {
+ return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik';
+ } else {
+ return data_get($value, 'Name') === '/coolify-proxy';
+ }
})->first();
if (!$foundProxyContainer) {
try {
diff --git a/app/Models/Application.php b/app/Models/Application.php
index ea7e0a930..e2d93c7a1 100644
--- a/app/Models/Application.php
+++ b/app/Models/Application.php
@@ -603,6 +603,7 @@ class Application extends BaseModel
{
if ($this->docker_compose_raw) {
$mainCompose = parseDockerComposeFile(resource: $this, isNew: false, pull_request_id: $pull_request_id);
+ ray($this->docker_compose_pr_raw);
if ($this->getMorphClass() === 'App\Models\Application' && $this->docker_compose_pr_raw) {
parseDockerComposeFile(resource: $this, isNew: false, pull_request_id: $pull_request_id, is_pr: true);
}
@@ -614,7 +615,7 @@ class Application extends BaseModel
function loadComposeFile($isInit = false)
{
$initialDockerComposeLocation = $this->docker_compose_location;
- $initialDockerComposePrLocation = $this->docker_compose_pr_location;
+ // $initialDockerComposePrLocation = $this->docker_compose_pr_location;
if ($this->build_pack === 'dockercompose') {
if ($isInit && $this->docker_compose_raw) {
return;
@@ -623,11 +624,11 @@ class Application extends BaseModel
['commands' => $cloneCommand] = $this->generateGitImportCommands(deployment_uuid: $uuid, only_checkout: true, exec_in_docker: false, custom_base_dir: '.');
$workdir = rtrim($this->base_directory, '/');
$composeFile = $this->docker_compose_location;
- $prComposeFile = $this->docker_compose_pr_location;
- $fileList = collect([".$composeFile"]);
- if ($composeFile !== $prComposeFile) {
- $fileList->push(".$prComposeFile");
- }
+ // $prComposeFile = $this->docker_compose_pr_location;
+ $fileList = collect([".$workdir$composeFile"]);
+ // if ($composeFile !== $prComposeFile) {
+ // $fileList->push(".$prComposeFile");
+ // }
$commands = collect([
"mkdir -p /tmp/{$uuid} && cd /tmp/{$uuid}",
$cloneCommand,
@@ -645,24 +646,24 @@ class Application extends BaseModel
$this->docker_compose_raw = $composeFileContent;
$this->save();
}
- if ($composeFile === $prComposeFile) {
- $this->docker_compose_pr_raw = $composeFileContent;
- $this->save();
- } else {
- $commands = collect([
- "cd /tmp/{$uuid}",
- "cat .$workdir$prComposeFile",
- ]);
- $composePrFileContent = instant_remote_process($commands, $this->destination->server, false);
- if (!$composePrFileContent) {
- $this->docker_compose_pr_location = $initialDockerComposePrLocation;
- $this->save();
- throw new \Exception("Could not load compose file from $workdir$prComposeFile");
- } else {
- $this->docker_compose_pr_raw = $composePrFileContent;
- $this->save();
- }
- }
+ // if ($composeFile === $prComposeFile) {
+ // $this->docker_compose_pr_raw = $composeFileContent;
+ // $this->save();
+ // } else {
+ // $commands = collect([
+ // "cd /tmp/{$uuid}",
+ // "cat .$workdir$prComposeFile",
+ // ]);
+ // $composePrFileContent = instant_remote_process($commands, $this->destination->server, false);
+ // if (!$composePrFileContent) {
+ // $this->docker_compose_pr_location = $initialDockerComposePrLocation;
+ // $this->save();
+ // throw new \Exception("Could not load compose file from $workdir$prComposeFile");
+ // } else {
+ // $this->docker_compose_pr_raw = $composePrFileContent;
+ // $this->save();
+ // }
+ // }
$commands = collect([
"rm -rf /tmp/{$uuid}",
diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php
index d643cce6b..c46c4d558 100644
--- a/bootstrap/helpers/docker.php
+++ b/bootstrap/helpers/docker.php
@@ -93,7 +93,11 @@ function executeInDocker(string $containerId, string $command)
function getContainerStatus(Server $server, string $container_id, bool $all_data = false, bool $throwError = false)
{
- $container = instant_remote_process(["docker inspect --format '{{json .}}' {$container_id}"], $server, $throwError);
+ if ($server->isSwarm()) {
+ $container = instant_remote_process(["docker service ls --filter 'name={$container_id}' --format '{{json .}}' "], $server, $throwError);
+ } else {
+ $container = instant_remote_process(["docker inspect --format '{{json .}}' {$container_id}"], $server, $throwError);
+ }
if (!$container) {
return 'exited';
}
@@ -101,7 +105,19 @@ function getContainerStatus(Server $server, string $container_id, bool $all_data
if ($all_data) {
return $container[0];
}
- return data_get($container[0], 'State.Status', 'exited');
+ if ($server->isSwarm()) {
+ $replicas = data_get($container[0], 'Replicas');
+ $replicas = explode('/', $replicas);
+ $active = (int)$replicas[0];
+ $total = (int)$replicas[1];
+ if ($active === $total) {
+ return 'running';
+ } else {
+ return 'starting';
+ }
+ } else {
+ return data_get($container[0], 'State.Status', 'exited');
+ }
}
function generateApplicationContainerName(Application $application, $pull_request_id = 0)
diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php
index 8eb1fd443..3681a6c0c 100644
--- a/bootstrap/helpers/shared.php
+++ b/bootstrap/helpers/shared.php
@@ -863,7 +863,6 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$key = Str::of($variableName);
$value = Str::of($variable);
}
- // TODO: here is the problem
if ($key->startsWith('SERVICE_FQDN')) {
if ($isNew || $savedService->fqdn === null) {
$name = $key->after('SERVICE_FQDN_')->beforeLast('_')->lower();
@@ -1145,6 +1144,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
data_set($service, 'volumes', $serviceVolumes->toArray());
}
} else {
+ // TODO
}
// Decide if the service is a database
$isDatabase = isDatabaseImage(data_get_str($service, 'image'));
diff --git a/resources/views/livewire/boarding/index.blade.php b/resources/views/livewire/boarding/index.blade.php
index fb102c7c5..98a2c772a 100644
--- a/resources/views/livewire/boarding/index.blade.php
+++ b/resources/views/livewire/boarding/index.blade.php
@@ -207,10 +207,10 @@
placeholder="Username to connect to your server. Default is root." label="Username"
id="remoteServerUser" />
- --}}
Check Connection
diff --git a/resources/views/livewire/server/form.blade.php b/resources/views/livewire/server/form.blade.php
index 528fd32c5..b95d0e938 100644
--- a/resources/views/livewire/server/form.blade.php
+++ b/resources/views/livewire/server/form.blade.php
@@ -54,8 +54,8 @@
helper="If you are using Cloudflare Tunnels, enable this. It will proxy all ssh requests to your server through Cloudflare.Coolify does not install/setup Cloudflare (cloudflared) on your server."
id="server.settings.is_cloudflare_tunnel" label="Cloudflare Tunnel" />
@endif
-
+ {{-- --}}
{{-- --}}
diff --git a/resources/views/livewire/server/new/by-ip.blade.php b/resources/views/livewire/server/new/by-ip.blade.php
index 62268f2fe..6ec358fdc 100644
--- a/resources/views/livewire/server/new/by-ip.blade.php
+++ b/resources/views/livewire/server/new/by-ip.blade.php
@@ -25,10 +25,10 @@
@endif
@endforeach
- --}}
Save New Server
diff --git a/resources/views/livewire/server/proxy/status.blade.php b/resources/views/livewire/server/proxy/status.blade.php
index 65eebe037..e3184d061 100644
--- a/resources/views/livewire/server/proxy/status.blade.php
+++ b/resources/views/livewire/server/proxy/status.blade.php
@@ -1,6 +1,6 @@
@if ($server->isFunctional())
-
+
@if (data_get($server, 'proxy.status') === 'running')
@elseif (data_get($server, 'proxy.status') === 'restarting')
From 0dff57e69feade4f93a9a2d9d839761b92220017 Mon Sep 17 00:00:00 2001
From: Andras Bacsai
Date: Wed, 29 Nov 2023 15:03:21 +0100
Subject: [PATCH 14/15] Add cleanup option to app:init command
---
app/Console/Commands/Init.php | 57 ++++++++++---------
.../etc/s6-overlay/s6-rc.d/init-script/up | 2 +-
2 files changed, 30 insertions(+), 29 deletions(-)
diff --git a/app/Console/Commands/Init.php b/app/Console/Commands/Init.php
index 4cf0f21bf..862fb7625 100644
--- a/app/Console/Commands/Init.php
+++ b/app/Console/Commands/Init.php
@@ -30,6 +30,7 @@ class Init extends Command
$this->alive();
$cleanup = $this->option('cleanup');
if ($cleanup) {
+ echo "Running cleanup\n";
$this->cleanup_stucked_resources();
$this->cleanup_ssh();
}
@@ -101,14 +102,14 @@ class Init extends Command
ray('Application without environment', $application->name);
$application->delete();
}
- if (!data_get($application, 'destination.server')) {
- ray('Application without server', $application->name);
- $application->delete();
- }
if (!$application->destination()) {
ray('Application without destination', $application->name);
$application->delete();
}
+ if (!data_get($application, 'destination.server')) {
+ ray('Application without server', $application->name);
+ $application->delete();
+ }
}
} catch (\Throwable $e) {
echo "Error in application: {$e->getMessage()}\n";
@@ -120,14 +121,14 @@ class Init extends Command
ray('Postgresql without environment', $postgresql->name);
$postgresql->delete();
}
- if (!data_get($postgresql, 'destination.server')) {
- ray('Postgresql without server', $postgresql->name);
- $postgresql->delete();
- }
if (!$postgresql->destination()) {
ray('Postgresql without destination', $postgresql->name);
$postgresql->delete();
}
+ if (!data_get($postgresql, 'destination.server')) {
+ ray('Postgresql without server', $postgresql->name);
+ $postgresql->delete();
+ }
}
} catch (\Throwable $e) {
echo "Error in postgresql: {$e->getMessage()}\n";
@@ -139,14 +140,14 @@ class Init extends Command
ray('Redis without environment', $redis->name);
$redis->delete();
}
- if (!data_get($redis, 'destination.server')) {
- ray('Redis without server', $redis->name);
- $redis->delete();
- }
if (!$redis->destination()) {
ray('Redis without destination', $redis->name);
$redis->delete();
}
+ if (!data_get($redis, 'destination.server')) {
+ ray('Redis without server', $redis->name);
+ $redis->delete();
+ }
}
} catch (\Throwable $e) {
echo "Error in redis: {$e->getMessage()}\n";
@@ -159,14 +160,14 @@ class Init extends Command
ray('Mongodb without environment', $mongodb->name);
$mongodb->delete();
}
- if (!data_get($mongodb, 'destination.server')) {
- ray('Mongodb without server', $mongodb->name);
- $mongodb->delete();
- }
if (!$mongodb->destination()) {
ray('Mongodb without destination', $mongodb->name);
$mongodb->delete();
}
+ if (!data_get($mongodb, 'destination.server')) {
+ ray('Mongodb without server', $mongodb->name);
+ $mongodb->delete();
+ }
}
} catch (\Throwable $e) {
echo "Error in mongodb: {$e->getMessage()}\n";
@@ -179,14 +180,14 @@ class Init extends Command
ray('Mysql without environment', $mysql->name);
$mysql->delete();
}
- if (!data_get($mysql, 'destination.server')) {
- ray('Mysql without server', $mysql->name);
- $mysql->delete();
- }
if (!$mysql->destination()) {
ray('Mysql without destination', $mysql->name);
$mysql->delete();
}
+ if (!data_get($mysql, 'destination.server')) {
+ ray('Mysql without server', $mysql->name);
+ $mysql->delete();
+ }
}
} catch (\Throwable $e) {
echo "Error in mysql: {$e->getMessage()}\n";
@@ -199,14 +200,14 @@ class Init extends Command
ray('Mariadb without environment', $mariadb->name);
$mariadb->delete();
}
- if (!data_get($mariadb, 'destination.server')) {
- ray('Mariadb without server', $mariadb->name);
- $mariadb->delete();
- }
if (!$mariadb->destination()) {
ray('Mariadb without destination', $mariadb->name);
$mariadb->delete();
}
+ if (!data_get($mariadb, 'destination.server')) {
+ ray('Mariadb without server', $mariadb->name);
+ $mariadb->delete();
+ }
}
} catch (\Throwable $e) {
echo "Error in mariadb: {$e->getMessage()}\n";
@@ -219,14 +220,14 @@ class Init extends Command
ray('Service without environment', $service->name);
$service->delete();
}
- if (!data_get($service, 'server')) {
- ray('Service without server', $service->name);
- $service->delete();
- }
if (!$service->destination()) {
ray('Service without destination', $service->name);
$service->delete();
}
+ if (!data_get($service, 'server')) {
+ ray('Service without server', $service->name);
+ $service->delete();
+ }
}
} catch (\Throwable $e) {
echo "Error in service: {$e->getMessage()}\n";
diff --git a/docker/prod-ssu/etc/s6-overlay/s6-rc.d/init-script/up b/docker/prod-ssu/etc/s6-overlay/s6-rc.d/init-script/up
index 09595f708..32492f6b7 100644
--- a/docker/prod-ssu/etc/s6-overlay/s6-rc.d/init-script/up
+++ b/docker/prod-ssu/etc/s6-overlay/s6-rc.d/init-script/up
@@ -1,2 +1,2 @@
#!/command/execlineb -P
-php /var/www/html/artisan app:init
+php /var/www/html/artisan app:init --cleanup
From 7fe5eca66113918c7a2f5a1b3a61125b8742f653 Mon Sep 17 00:00:00 2001
From: Andras Bacsai
Date: Wed, 29 Nov 2023 15:13:03 +0100
Subject: [PATCH 15/15] Add precheck for containers
---
app/Jobs/ContainerStatusJob.php | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/app/Jobs/ContainerStatusJob.php b/app/Jobs/ContainerStatusJob.php
index 7063e01cd..b99681455 100644
--- a/app/Jobs/ContainerStatusJob.php
+++ b/app/Jobs/ContainerStatusJob.php
@@ -46,6 +46,11 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
$containers = instant_remote_process(["docker service inspect $(docker service ls -q) --format '{{json .}}'"], $this->server, false);
$containerReplicase = instant_remote_process(["docker service ls --format '{{json .}}'"], $this->server, false);
} else {
+ // Precheck for containers
+ $containers = instant_remote_process(["docker container ls -q"], $this->server);
+ if (!$containers) {
+ return;
+ }
$containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this->server, false);
$containerReplicase = null;
}