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 @@
@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 75f2441f0..98a2c772a 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/project/application/general.blade.php b/resources/views/livewire/project/application/general.blade.php index daeac9710..835c15766 100644 --- a/resources/views/livewire/project/application/general.blade.php +++ b/resources/views/livewire/project/application/general.blade.php @@ -69,21 +69,39 @@ @endif @if ($application->build_pack !== 'dockerimage' && $application->build_pack !== 'dockercompose')

Docker Registry

-
Push the built image to a docker registry. More info here.
+ @if ($application->destination->server->isSwarm()) +
Docker Swarm requires the image to be available in a registry. More info here.
+ @else +
Push the built image to a docker registry. More info here.
+ @endif
@if ($application->build_pack === 'dockerimage') - - + @if ($application->destination->server->isSwarm()) + + + @else + + + @endif @else - - + @if ($application->destination->server->isSwarm()) + + + @else + + + @endif + @endif
@@ -140,8 +158,8 @@ @endif @if ($application->build_pack === 'dockercompose') Reload Compose File - + {{-- --}} @endif diff --git a/resources/views/livewire/server/form.blade.php b/resources/views/livewire/server/form.blade.php index 4004dd096..b95d0e938 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..6ec358fdc 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.
@@ -25,6 +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') 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" diff --git a/versions.json b/versions.json index 02f132c2f..661a44a35 100644 --- a/versions.json +++ b/versions.json @@ -4,7 +4,7 @@ "version": "3.12.36" }, "v4": { - "version": "4.0.0-beta.147" + "version": "4.0.0-beta.148" } } }