diff --git a/app/Actions/Application/StopApplication.php b/app/Actions/Application/StopApplication.php
index 1d09f0daf..d32248042 100644
--- a/app/Actions/Application/StopApplication.php
+++ b/app/Actions/Application/StopApplication.php
@@ -11,29 +11,34 @@ class StopApplication
public function handle(Application $application)
{
$server = $application->destination->server;
- $containers = getCurrentApplicationContainerStatus($server, $application->id, 0);
- if ($containers->count() > 0) {
- foreach ($containers as $container) {
- $containerName = data_get($container, 'Names');
- if ($containerName) {
- instant_remote_process(
- ["docker rm -f {$containerName}"],
- $server
- );
+ if ($server->isSwarm()) {
+ instant_remote_process(["docker stack rm {$application->uuid}" ], $server);
+ } else {
+ $containers = getCurrentApplicationContainerStatus($server, $application->id, 0);
+ if ($containers->count() > 0) {
+ foreach ($containers as $container) {
+ $containerName = data_get($container, 'Names');
+ if ($containerName) {
+ instant_remote_process(
+ ["docker rm -f {$containerName}"],
+ $server
+ );
+ }
}
+ // TODO: make notification for application
+ // $application->environment->project->team->notify(new StatusChanged($application));
}
- // TODO: make notification for application
- // $application->environment->project->team->notify(new StatusChanged($application));
- }
- // Delete Preview Deployments
- $previewDeployments = $application->previews;
- foreach ($previewDeployments as $previewDeployment) {
- $containers = getCurrentApplicationContainerStatus($server, $application->id, $previewDeployment->pull_request_id);
- foreach ($containers as $container) {
- $name = str_replace('/', '', $container['Names']);
- instant_remote_process(["docker rm -f $name"], $application->destination->server, throwError: false);
+ // Delete Preview Deployments
+ $previewDeployments = $application->previews;
+ foreach ($previewDeployments as $previewDeployment) {
+ $containers = getCurrentApplicationContainerStatus($server, $application->id, $previewDeployment->pull_request_id);
+ foreach ($containers as $container) {
+ $name = str_replace('/', '', $container['Names']);
+ instant_remote_process(["docker rm -f $name"], $application->destination->server, throwError: false);
+ }
}
}
+
}
}
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/Actions/Proxy/StartProxy.php b/app/Actions/Proxy/StartProxy.php
index 6a6738177..a99e47b25 100644
--- a/app/Actions/Proxy/StartProxy.php
+++ b/app/Actions/Proxy/StartProxy.php
@@ -13,6 +13,7 @@ class StartProxy
public function handle(Server $server, bool $async = true): string|Activity
{
try {
+
$proxyType = $server->proxyType();
$commands = collect([]);
$proxy_path = get_proxy_path();
@@ -24,18 +25,29 @@ class StartProxy
$docker_compose_yml_base64 = base64_encode($configuration);
$server->proxy->last_applied_settings = Str::of($docker_compose_yml_base64)->pipe('md5')->value;
$server->save();
- $commands = $commands->merge([
- "mkdir -p $proxy_path && cd $proxy_path",
- "echo 'Creating required Docker Compose file.'",
- "echo 'Pulling docker image.'",
- 'docker compose pull',
- "echo 'Stopping existing coolify-proxy.'",
- "docker compose down -v --remove-orphans > /dev/null 2>&1",
- "echo 'Starting coolify-proxy.'",
- 'docker compose up -d --remove-orphans',
- "echo 'Proxy started successfully.'"
- ]);
- $commands = $commands->merge(connectProxyToNetworks($server));
+ if ($server->isSwarm()) {
+ $commands = $commands->merge([
+ "mkdir -p $proxy_path && cd $proxy_path",
+ "echo 'Creating required Docker Compose file.'",
+ "echo 'Starting coolify-proxy.'",
+ "cd $proxy_path && docker stack deploy -c docker-compose.yml coolify-proxy",
+ "echo 'Proxy started successfully.'"
+ ]);
+ } else {
+ $commands = $commands->merge([
+ "mkdir -p $proxy_path && cd $proxy_path",
+ "echo 'Creating required Docker Compose file.'",
+ "echo 'Pulling docker image.'",
+ 'docker compose pull',
+ "echo 'Stopping existing coolify-proxy.'",
+ "docker compose down -v --remove-orphans > /dev/null 2>&1",
+ "echo 'Starting coolify-proxy.'",
+ 'docker compose up -d --remove-orphans',
+ "echo 'Proxy started successfully.'"
+ ]);
+ $commands = $commands->merge(connectProxyToNetworks($server));
+ }
+
if ($async) {
$activity = remote_process($commands, $server);
return $activity;
@@ -46,11 +58,9 @@ class StartProxy
$server->save();
return 'OK';
}
- } catch(\Throwable $e) {
+ } catch (\Throwable $e) {
ray($e);
throw $e;
}
-
-
}
}
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/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/app/Http/Livewire/Boarding/Index.php b/app/Http/Livewire/Boarding/Index.php
index 7f53708ad..67221d0c6 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 $isSwarmManager = 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_swarm_manager = $this->isSwarmManager;
+ $this->createdServer->settings->save();
+ $this->createdServer->addInitialNetwork();
$this->validateServer();
}
public function validateServer()
diff --git a/app/Http/Livewire/Server/Form.php b/app/Http/Livewire/Server/Form.php
index dacb8faad..efca800c2 100644
--- a/app/Http/Livewire/Server/Form.php
+++ b/app/Http/Livewire/Server/Form.php
@@ -17,14 +17,15 @@ 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_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()
@@ -49,9 +51,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 +107,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..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,6 +29,7 @@ class ByIp extends Component
'ip' => 'required',
'user' => 'required|string',
'port' => 'required|integer',
+ 'is_swarm_manager' => 'required|boolean',
];
protected $validationAttributes = [
'name' => 'Name',
@@ -36,6 +37,7 @@ class ByIp extends Component
'ip' => 'IP Address/Domain',
'user' => 'User',
'port' => 'Port',
+ 'is_swarm_manager' => 'Swarm Manager',
];
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->is_swarm_manager = $this->is_swarm_manager;
$server->settings->save();
+ $server->addInitialNetwork();
return redirect()->route('server.show', $server->uuid);
} catch (\Throwable $e) {
return handleError($e, $this);
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 7d229e879..d0f18d00c 100644
--- a/app/Jobs/ApplicationDeploymentJob.php
+++ b/app/Jobs/ApplicationDeploymentJob.php
@@ -156,25 +156,27 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
// Generate custom host<->ip mapping
$allContainers = instant_remote_process(["docker network inspect {$this->destination->network} -f '{{json .Containers}}' "], $this->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());
+ if (!is_null($allContainers)) {
+ $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());
+ }
}
}
+ $this->addHosts = $ips->map(function ($ip, $name) {
+ return "--add-host $name:$ip";
+ })->implode(' ');
}
- $this->addHosts = $ips->map(function ($ip, $name) {
- return "--add-host $name:$ip";
- })->implode(' ');
if ($this->application->dockerfile_target_build) {
$this->buildTarget = " --target {$this->application->dockerfile_target_build} ";
@@ -214,6 +216,17 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
}
if ($this->application->docker_registry_image_name && $this->application->build_pack !== 'dockerimage') {
$this->push_to_docker_registry();
+ if ($this->server->isSwarm()) {
+ $this->application_deployment_queue->addLogEntry("Creating / updating stack.");
+ $this->execute_remote_command(
+ [
+ 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.'"
+ ]
+ );
+ }
}
$this->next(ApplicationDeploymentStatus::FINISHED->value);
$this->application->isConfigurationChanged(true);
@@ -290,42 +303,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) {
@@ -402,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(
[
@@ -468,22 +444,31 @@ 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}";
}
- $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
- ]);
+ ray($networkId);
+ 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);
}
@@ -570,74 +555,83 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
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();
+ if ($this->server->isSwarm()) {
+ // Skip this.
} else {
- $this->execute_remote_command(
- [
- "echo '\n----------------------------------------'",
- ],
- ["echo -n 'Rolling update started.'"],
- );
- $this->start_by_compose_file();
- $this->health_check();
- $this->stop_running_container();
+ 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();
+ $this->application_deployment_queue->addLogEntry("Rolling update completed.");
+ }
}
}
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) {
+ if ($this->server->isSwarm()) {
+ // Implement healthcheck for swarm
+ } else {
+ 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 'Healthcheck URL (inside the container): {$this->full_healthcheck_url}'"
+ "echo 'Waiting for healthcheck to pass on the new container.'"
]
);
- }
- 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']);
+ if ($this->full_healthcheck_url) {
$this->execute_remote_command(
[
- "echo 'New container is healthy.'"
+ "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')}'"
],
);
- break;
+ 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);
}
- $counter++;
- sleep($this->application->health_check_interval);
}
}
}
@@ -844,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;
@@ -884,21 +858,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
}
if ($this->pull_request_id !== 0) {
$labels = collect(generateLabelsApplication($this->application, $this->preview));
-
- // $newHostLabel = $newLabels->filter(function ($label) {
- // return str($label)->contains('Host');
- // });
- // $labels = $labels->reject(function ($label) {
- // return str($label)->contains('Host');
- // });
- // ray($labels,$newLabels);
- // $labels = $labels->map(function ($label) {
- // $pattern = '/([a-zA-Z0-9]+)-(\d+)-(http|https)/';
- // $replacement = "$1-pr-{$this->pull_request_id}-$2-$3";
- // $newLabel = preg_replace($pattern, $replacement, $label);
- // return $newLabel;
- // });
- // $labels = $labels->merge($newHostLabel);
}
$labels = $labels->merge(defaultLabels($this->application->id, $this->application->uuid, $this->pull_request_id))->toArray();
$docker_compose = [
@@ -909,7 +868,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,
@@ -941,6 +899,48 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
]
]
];
+ if ($this->server->isSwarm()) {
+ data_forget($docker_compose, 'services.' . $this->container_name . '.container_name');
+ data_forget($docker_compose, 'services.' . $this->container_name . '.expose');
+ data_forget($docker_compose, 'services.' . $this->container_name . '.restart');
+
+ data_forget($docker_compose, 'services.' . $this->container_name . '.mem_limit');
+ data_forget($docker_compose, 'services.' . $this->container_name . '.memswap_limit');
+ data_forget($docker_compose, 'services.' . $this->container_name . '.mem_swappiness');
+ data_forget($docker_compose, 'services.' . $this->container_name . '.mem_reservation');
+ 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'] = [
'driver' => 'fluentd',
@@ -988,6 +988,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]);
@@ -1204,7 +1209,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 +1230,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()
@@ -1289,9 +1294,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);
}
diff --git a/app/Jobs/ContainerStatusJob.php b/app/Jobs/ContainerStatusJob.php
index 79265f7a1..b99681455 100644
--- a/app/Jobs/ContainerStatusJob.php
+++ b/app/Jobs/ContainerStatusJob.php
@@ -42,8 +42,43 @@ 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);
+ 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 {
+ // 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;
+ }
+ 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();
@@ -55,10 +90,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) {
@@ -90,7 +131,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) {
@@ -245,7 +285,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/app/Models/Server.php b/app/Models/Server.php
index 07714caa7..408106107 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
{
@@ -37,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) {
@@ -84,7 +67,7 @@ class Server extends BaseModel
{
$teamId = currentTeam()->id;
$selectArray = collect($select)->concat(['id']);
- return Server::whereTeamId($teamId)->with('settings')->select($selectArray->all())->orderBy('name');
+ return Server::whereTeamId($teamId)->with('settings','swarmDockers','standaloneDockers')->select($selectArray->all())->orderBy('name');
}
static public function isUsable()
@@ -103,7 +86,41 @@ class Server extends BaseModel
{
return $this->hasOne(ServerSetting::class);
}
+ public function addInitialNetwork() {
+ ray($this->id);
+ 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-overlay',
+ 'network' => 'coolify-overlay',
+ 'server_id' => $this->id,
+ ]);
+ } else {
+ StandaloneDocker::create([
+ 'name' => 'coolify-overlay',
+ 'network' => 'coolify',
+ 'server_id' => $this->id,
+ ]);
+ }
+ }
+ }
public function proxyType()
{
$proxyType = $this->proxy->get('type');
@@ -359,12 +376,16 @@ class Server extends BaseModel
return false;
}
}
+ public function isSwarm()
+ {
+ return data_get($this, 'settings.is_swarm_manager') || data_get($this, 'settings.is_swarm_worker');
+ }
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([
@@ -375,14 +396,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;
@@ -400,7 +421,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()
@@ -417,9 +451,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 --attachable --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/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php
index 030aa6aa7..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)
@@ -275,8 +291,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);
diff --git a/bootstrap/helpers/proxy.php b/bootstrap/helpers/proxy.php
index 2f595d1c6..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(),
@@ -102,7 +118,6 @@ function generate_default_proxy_configuration(Server $server)
"--entrypoints.https.address=:443",
"--entrypoints.http.http.encodequerysemicolons=true",
"--entrypoints.https.http.encodequerysemicolons=true",
- "--providers.docker=true",
"--providers.docker.exposedbydefault=false",
"--providers.file.directory=/traefik/dynamic/",
"--providers.file.watch=true",
@@ -110,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,7 +134,24 @@ function generate_default_proxy_configuration(Server $server)
$config['services']['traefik']['command'][] = "--accesslog.filepath=/traefik/access.log";
$config['services']['traefik']['command'][] = "--accesslog.bufferingsize=100";
}
- $config = Yaml::dump($config, 4, 2);
+ 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",
+ ],
+ ],
+ ];
+ } else {
+ $config['services']['traefik']['command'][] = "--providers.docker=true";
+ }
+ $config = Yaml::dump($config, 12, 2);
SaveConfiguration::run($server, $config);
return $config;
}
diff --git a/bootstrap/helpers/remoteProcess.php b/bootstrap/helpers/remoteProcess.php
index 45c8ae0e5..2fe90415e 100644
--- a/bootstrap/helpers/remoteProcess.php
+++ b/bootstrap/helpers/remoteProcess.php
@@ -123,6 +123,9 @@ function instant_remote_process(Collection|array $command, Server $server, $thro
}
return excludeCertainErrors($process->errorOutput(), $exitCode);
}
+ if ($output === 'null') {
+ $output = null;
+ }
return $output;
}
function excludeCertainErrors(string $errorOutput, ?int $exitCode = null)
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/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 @@
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/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/database/seeders/SwarmDockerSeeder.php b/database/seeders/SwarmDockerSeeder.php
index 906e1bccc..8a204e159 100644
--- a/database/seeders/SwarmDockerSeeder.php
+++ b/database/seeders/SwarmDockerSeeder.php
@@ -13,9 +13,9 @@ class SwarmDockerSeeder extends Seeder
*/
public function run(): void
{
- SwarmDocker::create([
- 'name' => 'Swarm Docker 1',
- 'server_id' => 1,
- ]);
+ // SwarmDocker::create([
+ // 'name' => 'Swarm Docker 1',
+ // 'server_id' => 1,
+ // ]);
}
}
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
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 @@
Private Keys are used to connect to a remote server through a secure shell, called SSH.
@@ -200,14 +200,17 @@ label="Description" id="remoteServerDescription" />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.