Compare commits

...

23 Commits

Author SHA1 Message Date
Andras Bacsai
db4a4c74fc Merge pull request #1502 from coollabsio/next
v4.0.0-beta.149
2023-11-29 17:05:28 +01:00
Andras Bacsai
5c7ef80219 Fix container retrieval in CheckLogDrainContainerJob and ContainerStatusJob 2023-11-29 17:03:04 +01:00
Andras Bacsai
243d1c06fc cloud: disable trial 2023-11-29 16:34:31 +01:00
Andras Bacsai
ef25f7d800 Update Sentry DSN 2023-11-29 16:21:03 +01:00
Andras Bacsai
45640ffdb1 Update version numbers to 4.0.0-beta.149 2023-11-29 16:19:40 +01:00
Andras Bacsai
378291b209 Merge pull request #1501 from coollabsio/next
Commented out cleanup_ssh() function
2023-11-29 15:23:21 +01:00
Andras Bacsai
3583e552f1 Commented out cleanup_ssh() function 2023-11-29 15:23:03 +01:00
Andras Bacsai
d7dfeaf988 Merge pull request #1496 from coollabsio/next
v4.0.0-beta.148
2023-11-29 15:17:39 +01:00
Andras Bacsai
7fe5eca661 Add precheck for containers 2023-11-29 15:13:03 +01:00
Andras Bacsai
0dff57e69f Add cleanup option to app:init command 2023-11-29 15:03:21 +01:00
Andras Bacsai
f4803ad58b wip: swarm
fix: gitcompose deployments
2023-11-29 14:59:06 +01:00
Andras Bacsai
2d7bbbe300 wip: swarm 2023-11-29 10:06:52 +01:00
Andras Bacsai
928b68043b wip: swarm 2023-11-28 20:49:38 +01:00
Andras Bacsai
b21add0210 Update Swarm cluster label to Swarm Manager 2023-11-28 20:09:00 +01:00
Andras Bacsai
c41ffd6bfb wip: swarm 2023-11-28 18:42:09 +01:00
Andras Bacsai
b4874c7df3 wip: swarm 2023-11-28 18:31:04 +01:00
Andras Bacsai
c505a6ce9c wip 2023-11-28 15:49:24 +01:00
Andras Bacsai
706e4b13ee fix: sentry issue 2023-11-28 14:27:38 +01:00
Andras Bacsai
4af471ee31 fix: no container servers 2023-11-28 14:26:35 +01:00
Andras Bacsai
87062e4e22 Refactor application deployment job 2023-11-28 14:23:59 +01:00
Andras Bacsai
500ba0fab8 fix: do not remove deployment in case compose based failed 2023-11-28 14:08:42 +01:00
Andras Bacsai
1c72c127d5 Remove unused imports and fix import statement 2023-11-28 14:05:55 +01:00
Andras Bacsai
69bb4ae5ee Update release version to 4.0.0-beta.148 2023-11-28 13:40:33 +01:00
37 changed files with 768 additions and 418 deletions

View File

@@ -11,29 +11,34 @@ class StopApplication
public function handle(Application $application) public function handle(Application $application)
{ {
$server = $application->destination->server; $server = $application->destination->server;
$containers = getCurrentApplicationContainerStatus($server, $application->id, 0); if ($server->isSwarm()) {
if ($containers->count() > 0) { instant_remote_process(["docker stack rm {$application->uuid}" ], $server);
foreach ($containers as $container) { } else {
$containerName = data_get($container, 'Names'); $containers = getCurrentApplicationContainerStatus($server, $application->id, 0);
if ($containerName) { if ($containers->count() > 0) {
instant_remote_process( foreach ($containers as $container) {
["docker rm -f {$containerName}"], $containerName = data_get($container, 'Names');
$server 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 // Delete Preview Deployments
// $application->environment->project->team->notify(new StatusChanged($application)); $previewDeployments = $application->previews;
} foreach ($previewDeployments as $previewDeployment) {
// Delete Preview Deployments $containers = getCurrentApplicationContainerStatus($server, $application->id, $previewDeployment->pull_request_id);
$previewDeployments = $application->previews; foreach ($containers as $container) {
foreach ($previewDeployments as $previewDeployment) { $name = str_replace('/', '', $container['Names']);
$containers = getCurrentApplicationContainerStatus($server, $application->id, $previewDeployment->pull_request_id); instant_remote_process(["docker rm -f $name"], $application->destination->server, throwError: false);
foreach ($containers as $container) { }
$name = str_replace('/', '', $container['Names']);
instant_remote_process(["docker rm -f $name"], $application->destination->server, throwError: false);
} }
} }
} }
} }

View File

@@ -17,35 +17,42 @@ class CheckProxy
return false; return false;
} }
} }
$status = getContainerStatus($server, 'coolify-proxy'); if ($server->isSwarm()) {
if ($status === 'running') { $status = getContainerStatus($server, 'coolify-proxy_traefik');
$server->proxy->set('status', 'running'); $server->proxy->set('status', $status);
$server->save(); $server->save();
return false; return false;
} } else {
$ip = $server->ip; $status = getContainerStatus($server, 'coolify-proxy');
if ($server->id === 0) { if ($status === 'running') {
$ip = 'host.docker.internal'; $server->proxy->set('status', 'running');
} $server->save();
return false;
}
$ip = $server->ip;
if ($server->id === 0) {
$ip = 'host.docker.internal';
}
$connection80 = @fsockopen($ip, '80'); $connection80 = @fsockopen($ip, '80');
$connection443 = @fsockopen($ip, '443'); $connection443 = @fsockopen($ip, '443');
$port80 = is_resource($connection80) && fclose($connection80); $port80 = is_resource($connection80) && fclose($connection80);
$port443 = is_resource($connection443) && fclose($connection443); $port443 = is_resource($connection443) && fclose($connection443);
if ($port80) { if ($port80) {
if ($fromUI) { if ($fromUI) {
throw new \Exception("Port 80 is in use.<br>You must stop the process using this port.<br>Docs: <a target='_blank' href='https://coolify.io/docs'>https://coolify.io/docs</a> <br> Discord: <a target='_blank' href='https://coollabs.io/discord'>https://coollabs.io/discord</a>"); throw new \Exception("Port 80 is in use.<br>You must stop the process using this port.<br>Docs: <a target='_blank' href='https://coolify.io/docs'>https://coolify.io/docs</a> <br> Discord: <a target='_blank' href='https://coollabs.io/discord'>https://coollabs.io/discord</a>");
} else { } else {
return false; return false;
}
} }
} if ($port443) {
if ($port443) { if ($fromUI) {
if ($fromUI) { throw new \Exception("Port 443 is in use.<br>You must stop the process using this port.<br>Docs: <a target='_blank' href='https://coolify.io/docs'>https://coolify.io/docs</a> <br> Discord: <a target='_blank' href='https://coollabs.io/discord'>https://coollabs.io/discord</a>");
throw new \Exception("Port 443 is in use.<br>You must stop the process using this port.<br>Docs: <a target='_blank' href='https://coolify.io/docs'>https://coolify.io/docs</a> <br> Discord: <a target='_blank' href='https://coollabs.io/discord'>https://coollabs.io/discord</a>"); } else {
} else { return false;
return false; }
} }
return true;
} }
return true;
} }
} }

View File

@@ -13,6 +13,7 @@ class StartProxy
public function handle(Server $server, bool $async = true): string|Activity public function handle(Server $server, bool $async = true): string|Activity
{ {
try { try {
$proxyType = $server->proxyType(); $proxyType = $server->proxyType();
$commands = collect([]); $commands = collect([]);
$proxy_path = get_proxy_path(); $proxy_path = get_proxy_path();
@@ -24,18 +25,29 @@ class StartProxy
$docker_compose_yml_base64 = base64_encode($configuration); $docker_compose_yml_base64 = base64_encode($configuration);
$server->proxy->last_applied_settings = Str::of($docker_compose_yml_base64)->pipe('md5')->value; $server->proxy->last_applied_settings = Str::of($docker_compose_yml_base64)->pipe('md5')->value;
$server->save(); $server->save();
$commands = $commands->merge([ if ($server->isSwarm()) {
"mkdir -p $proxy_path && cd $proxy_path", $commands = $commands->merge([
"echo 'Creating required Docker Compose file.'", "mkdir -p $proxy_path && cd $proxy_path",
"echo 'Pulling docker image.'", "echo 'Creating required Docker Compose file.'",
'docker compose pull', "echo 'Starting coolify-proxy.'",
"echo 'Stopping existing coolify-proxy.'", "cd $proxy_path && docker stack deploy -c docker-compose.yml coolify-proxy",
"docker compose down -v --remove-orphans > /dev/null 2>&1", "echo 'Proxy started successfully.'"
"echo 'Starting coolify-proxy.'", ]);
'docker compose up -d --remove-orphans', } else {
"echo 'Proxy started successfully.'" $commands = $commands->merge([
]); "mkdir -p $proxy_path && cd $proxy_path",
$commands = $commands->merge(connectProxyToNetworks($server)); "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) { if ($async) {
$activity = remote_process($commands, $server); $activity = remote_process($commands, $server);
return $activity; return $activity;
@@ -46,11 +58,9 @@ class StartProxy
$server->save(); $server->save();
return 'OK'; return 'OK';
} }
} catch(\Throwable $e) { } catch (\Throwable $e) {
ray($e); ray($e);
throw $e; throw $e;
} }
} }
} }

View File

@@ -76,10 +76,20 @@ class InstallDocker
"echo 'Restarting Docker Engine...'", "echo 'Restarting Docker Engine...'",
"systemctl enable docker >/dev/null 2>&1 || true", "systemctl enable docker >/dev/null 2>&1 || true",
"systemctl restart docker", "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); return remote_process($command, $server);
} }
} }

View File

@@ -30,20 +30,21 @@ class Init extends Command
$this->alive(); $this->alive();
$cleanup = $this->option('cleanup'); $cleanup = $this->option('cleanup');
if ($cleanup) { if ($cleanup) {
echo "Running cleanup\n";
$this->cleanup_stucked_resources(); $this->cleanup_stucked_resources();
$this->cleanup_ssh(); // $this->cleanup_ssh();
} }
$this->cleanup_in_progress_application_deployments(); $this->cleanup_in_progress_application_deployments();
$this->cleanup_stucked_helper_containers(); $this->cleanup_stucked_helper_containers();
} }
private function cleanup_stucked_helper_containers() { private function cleanup_stucked_helper_containers()
{
$servers = Server::all(); $servers = Server::all();
foreach ($servers as $server) { foreach ($servers as $server) {
if ($server->isFunctional()) { if ($server->isFunctional()) {
CleanupHelperContainersJob::dispatch($server); CleanupHelperContainersJob::dispatch($server);
} }
} }
} }
private function alive() private function alive()
{ {
@@ -62,21 +63,23 @@ class Init extends Command
echo "Error in alive: {$e->getMessage()}\n"; echo "Error in alive: {$e->getMessage()}\n";
} }
} }
private function cleanup_ssh() // private function cleanup_ssh()
{ // {
try {
$files = Storage::allFiles('ssh/keys'); // TODO: it will cleanup id.root@host.docker.internal
foreach ($files as $file) { // try {
Storage::delete($file); // $files = Storage::allFiles('ssh/keys');
} // foreach ($files as $file) {
$files = Storage::allFiles('ssh/mux'); // Storage::delete($file);
foreach ($files as $file) { // }
Storage::delete($file); // $files = Storage::allFiles('ssh/mux');
} // foreach ($files as $file) {
} catch (\Throwable $e) { // Storage::delete($file);
echo "Error in cleaning ssh: {$e->getMessage()}\n"; // }
} // } catch (\Throwable $e) {
} // echo "Error in cleaning ssh: {$e->getMessage()}\n";
// }
// }
private function cleanup_in_progress_application_deployments() private function cleanup_in_progress_application_deployments()
{ {
// Cleanup any failed deployments // Cleanup any failed deployments
@@ -98,15 +101,15 @@ class Init extends Command
$applications = Application::all(); $applications = Application::all();
foreach ($applications as $application) { foreach ($applications as $application) {
if (!data_get($application, 'environment')) { if (!data_get($application, 'environment')) {
ray('Application without environment', $application->name); echo 'Application without environment' . $application->name . 'deleting\n';
$application->delete();
}
if (!data_get($application, 'destination.server')) {
ray('Application without server', $application->name);
$application->delete(); $application->delete();
} }
if (!$application->destination()) { if (!$application->destination()) {
ray('Application without destination', $application->name); echo 'Application without destination' . $application->name . 'deleting\n';
$application->delete();
}
if (!data_get($application, 'destination.server')) {
echo 'Application without server' . $application->name . 'deleting\n';
$application->delete(); $application->delete();
} }
} }
@@ -117,15 +120,15 @@ class Init extends Command
$postgresqls = StandalonePostgresql::all(); $postgresqls = StandalonePostgresql::all();
foreach ($postgresqls as $postgresql) { foreach ($postgresqls as $postgresql) {
if (!data_get($postgresql, 'environment')) { if (!data_get($postgresql, 'environment')) {
ray('Postgresql without environment', $postgresql->name); echo 'Postgresql without environment' . $postgresql->name . 'deleting\n';
$postgresql->delete();
}
if (!data_get($postgresql, 'destination.server')) {
ray('Postgresql without server', $postgresql->name);
$postgresql->delete(); $postgresql->delete();
} }
if (!$postgresql->destination()) { if (!$postgresql->destination()) {
ray('Postgresql without destination', $postgresql->name); echo 'Postgresql without destination' . $postgresql->name . 'deleting\n';
$postgresql->delete();
}
if (!data_get($postgresql, 'destination.server')) {
echo 'Postgresql without server' . $postgresql->name . 'deleting\n';
$postgresql->delete(); $postgresql->delete();
} }
} }
@@ -136,15 +139,15 @@ class Init extends Command
$redis = StandaloneRedis::all(); $redis = StandaloneRedis::all();
foreach ($redis as $redis) { foreach ($redis as $redis) {
if (!data_get($redis, 'environment')) { if (!data_get($redis, 'environment')) {
ray('Redis without environment', $redis->name); echo 'Redis without environment' . $redis->name . 'deleting\n';
$redis->delete();
}
if (!data_get($redis, 'destination.server')) {
ray('Redis without server', $redis->name);
$redis->delete(); $redis->delete();
} }
if (!$redis->destination()) { if (!$redis->destination()) {
ray('Redis without destination', $redis->name); echo 'Redis without destination' . $redis->name . 'deleting\n';
$redis->delete();
}
if (!data_get($redis, 'destination.server')) {
echo 'Redis without server' . $redis->name . 'deleting\n';
$redis->delete(); $redis->delete();
} }
} }
@@ -156,15 +159,15 @@ class Init extends Command
$mongodbs = StandaloneMongodb::all(); $mongodbs = StandaloneMongodb::all();
foreach ($mongodbs as $mongodb) { foreach ($mongodbs as $mongodb) {
if (!data_get($mongodb, 'environment')) { if (!data_get($mongodb, 'environment')) {
ray('Mongodb without environment', $mongodb->name); echo 'Mongodb without environment' . $mongodb->name . 'deleting\n';
$mongodb->delete();
}
if (!data_get($mongodb, 'destination.server')) {
ray('Mongodb without server', $mongodb->name);
$mongodb->delete(); $mongodb->delete();
} }
if (!$mongodb->destination()) { if (!$mongodb->destination()) {
ray('Mongodb without destination', $mongodb->name); echo 'Mongodb without destination' . $mongodb->name . 'deleting\n';
$mongodb->delete();
}
if (!data_get($mongodb, 'destination.server')) {
echo 'Mongodb without server' . $mongodb->name . 'deleting\n';
$mongodb->delete(); $mongodb->delete();
} }
} }
@@ -176,15 +179,15 @@ class Init extends Command
$mysqls = StandaloneMysql::all(); $mysqls = StandaloneMysql::all();
foreach ($mysqls as $mysql) { foreach ($mysqls as $mysql) {
if (!data_get($mysql, 'environment')) { if (!data_get($mysql, 'environment')) {
ray('Mysql without environment', $mysql->name); echo 'Mysql without environment' . $mysql->name . 'deleting\n';
$mysql->delete();
}
if (!data_get($mysql, 'destination.server')) {
ray('Mysql without server', $mysql->name);
$mysql->delete(); $mysql->delete();
} }
if (!$mysql->destination()) { if (!$mysql->destination()) {
ray('Mysql without destination', $mysql->name); echo 'Mysql without destination' . $mysql->name . 'deleting\n';
$mysql->delete();
}
if (!data_get($mysql, 'destination.server')) {
echo 'Mysql without server' . $mysql->name . 'deleting\n';
$mysql->delete(); $mysql->delete();
} }
} }
@@ -196,15 +199,15 @@ class Init extends Command
$mariadbs = StandaloneMariadb::all(); $mariadbs = StandaloneMariadb::all();
foreach ($mariadbs as $mariadb) { foreach ($mariadbs as $mariadb) {
if (!data_get($mariadb, 'environment')) { if (!data_get($mariadb, 'environment')) {
ray('Mariadb without environment', $mariadb->name); echo 'Mariadb without environment' . $mariadb->name . 'deleting\n';
$mariadb->delete();
}
if (!data_get($mariadb, 'destination.server')) {
ray('Mariadb without server', $mariadb->name);
$mariadb->delete(); $mariadb->delete();
} }
if (!$mariadb->destination()) { if (!$mariadb->destination()) {
ray('Mariadb without destination', $mariadb->name); echo 'Mariadb without destination' . $mariadb->name . 'deleting\n';
$mariadb->delete();
}
if (!data_get($mariadb, 'destination.server')) {
echo 'Mariadb without server' . $mariadb->name . 'deleting\n';
$mariadb->delete(); $mariadb->delete();
} }
} }
@@ -216,15 +219,15 @@ class Init extends Command
$services = Service::all(); $services = Service::all();
foreach ($services as $service) { foreach ($services as $service) {
if (!data_get($service, 'environment')) { if (!data_get($service, 'environment')) {
ray('Service without environment', $service->name); echo 'Service without environment' . $service->name . 'deleting\n';
$service->delete();
}
if (!data_get($service, 'server')) {
ray('Service without server', $service->name);
$service->delete(); $service->delete();
} }
if (!$service->destination()) { if (!$service->destination()) {
ray('Service without destination', $service->name); echo 'Service without destination' . $service->name . 'deleting\n';
$service->delete();
}
if (!data_get($service, 'server')) {
echo 'Service without server' . $service->name . 'deleting\n';
$service->delete(); $service->delete();
} }
} }
@@ -235,7 +238,7 @@ class Init extends Command
$serviceApplications = ServiceApplication::all(); $serviceApplications = ServiceApplication::all();
foreach ($serviceApplications as $service) { foreach ($serviceApplications as $service) {
if (!data_get($service, 'service')) { if (!data_get($service, 'service')) {
ray('ServiceApplication without service', $service->name); echo 'ServiceApplication without service' . $service->name . 'deleting\n';
$service->delete(); $service->delete();
} }
} }
@@ -246,7 +249,7 @@ class Init extends Command
$serviceDatabases = ServiceDatabase::all(); $serviceDatabases = ServiceDatabase::all();
foreach ($serviceDatabases as $service) { foreach ($serviceDatabases as $service) {
if (!data_get($service, 'service')) { if (!data_get($service, 'service')) {
ray('ServiceDatabase without service', $service->name); echo 'ServiceDatabase without service' . $service->name . 'deleting\n';
$service->delete(); $service->delete();
} }
} }

View File

@@ -31,6 +31,7 @@ class Index extends Component
public ?string $remoteServerHost = null; public ?string $remoteServerHost = null;
public ?int $remoteServerPort = 22; public ?int $remoteServerPort = 22;
public ?string $remoteServerUser = 'root'; public ?string $remoteServerUser = 'root';
public bool $isSwarmManager = false;
public ?Server $createdServer = null; public ?Server $createdServer = null;
public Collection $projects; public Collection $projects;
@@ -182,7 +183,9 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
'private_key_id' => $this->createdPrivateKey->id, 'private_key_id' => $this->createdPrivateKey->id,
'team_id' => currentTeam()->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(); $this->validateServer();
} }
public function validateServer() public function validateServer()

View File

@@ -17,14 +17,15 @@ class Form extends Component
protected $listeners = ['serverRefresh']; protected $listeners = ['serverRefresh'];
protected $rules = [ protected $rules = [
'server.name' => 'required|min:6', 'server.name' => 'required',
'server.description' => 'nullable', 'server.description' => 'nullable',
'server.ip' => 'required', 'server.ip' => 'required',
'server.user' => 'required', 'server.user' => 'required',
'server.port' => '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_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', 'wildcard_domain' => 'nullable|url',
]; ];
protected $validationAttributes = [ protected $validationAttributes = [
@@ -34,8 +35,9 @@ class Form extends Component
'server.user' => 'User', 'server.user' => 'User',
'server.port' => 'Port', 'server.port' => 'Port',
'server.settings.is_cloudflare_tunnel' => 'Cloudflare Tunnel', 'server.settings.is_cloudflare_tunnel' => 'Cloudflare Tunnel',
'server.settings.is_reachable' => 'is reachable', 'server.settings.is_reachable' => 'Is reachable',
'server.settings.is_part_of_swarm' => 'is part of swarm' 'server.settings.is_swarm_manager' => 'Swarm Manager',
// 'server.settings.is_swarm_worker' => 'Swarm Worker',
]; ];
public function mount() public function mount()
@@ -49,9 +51,14 @@ class Form extends Component
} }
public function instantSave() public function instantSave()
{ {
refresh_server_connection($this->server->privateKey); try {
$this->validateServer(); refresh_server_connection($this->server->privateKey);
$this->server->settings->save(); $this->validateServer(false);
$this->server->settings->save();
$this->emit('success', 'Server updated successfully.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
} }
public function installDocker() public function installDocker()
{ {
@@ -100,6 +107,12 @@ class Form extends Component
$install && $this->installDocker(); $install && $this->installDocker();
return; return;
} }
if ($this->server->isSwarm()) {
$swarmInstalled = $this->server->validateDockerSwarm();
if ($swarmInstalled) {
$install && $this->emit('success', 'Docker Swarm is initiated.');
}
}
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} finally { } finally {

View File

@@ -21,7 +21,7 @@ class ByIp extends Component
public string $ip; public string $ip;
public string $user = 'root'; public string $user = 'root';
public int $port = 22; public int $port = 22;
public bool $is_part_of_swarm = false; public bool $is_swarm_manager = false;
protected $rules = [ protected $rules = [
'name' => 'required|string', 'name' => 'required|string',
@@ -29,6 +29,7 @@ class ByIp extends Component
'ip' => 'required', 'ip' => 'required',
'user' => 'required|string', 'user' => 'required|string',
'port' => 'required|integer', 'port' => 'required|integer',
'is_swarm_manager' => 'required|boolean',
]; ];
protected $validationAttributes = [ protected $validationAttributes = [
'name' => 'Name', 'name' => 'Name',
@@ -36,6 +37,7 @@ class ByIp extends Component
'ip' => 'IP Address/Domain', 'ip' => 'IP Address/Domain',
'user' => 'User', 'user' => 'User',
'port' => 'Port', 'port' => 'Port',
'is_swarm_manager' => 'Swarm Manager',
]; ];
public function mount() public function mount()
@@ -72,11 +74,11 @@ class ByIp extends Component
'proxy' => [ 'proxy' => [
"type" => ProxyTypes::TRAEFIK_V2->value, "type" => ProxyTypes::TRAEFIK_V2->value,
"status" => ProxyStatus::EXITED->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->settings->save();
$server->addInitialNetwork();
return redirect()->route('server.show', $server->uuid); return redirect()->route('server.show', $server->uuid);
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);

View File

@@ -58,11 +58,25 @@ class Deploy extends Component
public function stop() public function stop()
{ {
instant_remote_process([ try {
"docker rm -f coolify-proxy", if ($this->server->isSwarm()) {
], $this->server); instant_remote_process([
$this->server->proxy->status = 'exited'; "docker service rm coolify-proxy_traefik",
$this->server->save(); ], $this->server);
$this->emit('proxyStatusUpdated'); $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);
}
} }
} }

View File

@@ -16,7 +16,7 @@ class Status extends Component
protected $listeners = ['proxyStatusUpdated', 'startProxyPolling']; protected $listeners = ['proxyStatusUpdated', 'startProxyPolling'];
public function startProxyPolling() public function startProxyPolling()
{ {
$this->polling = true; $this->checkProxy();
} }
public function proxyStatusUpdated() public function proxyStatusUpdated()
{ {

View File

@@ -11,6 +11,9 @@ class PricingPlans extends Component
public bool $isTrial = false; public bool $isTrial = false;
public function mount() { public function mount() {
$this->isTrial = !data_get(currentTeam(),'subscription.stripe_trial_already_ended'); $this->isTrial = !data_get(currentTeam(),'subscription.stripe_trial_already_ended');
if (config('constants.limits.trial_period') == 0) {
$this->isTrial = false;
}
} }
public function subscribeStripe($type) public function subscribeStripe($type)
{ {
@@ -63,6 +66,7 @@ class PricingPlans extends Component
]; ];
if (!data_get($team,'subscription.stripe_trial_already_ended')) { if (!data_get($team,'subscription.stripe_trial_already_ended')) {
if (config('constants.limits.trial_period') > 0) {
$payload['subscription_data'] = [ $payload['subscription_data'] = [
'trial_period_days' => config('constants.limits.trial_period'), 'trial_period_days' => config('constants.limits.trial_period'),
'trial_settings' => [ 'trial_settings' => [
@@ -71,6 +75,7 @@ class PricingPlans extends Component
] ]
], ],
]; ];
}
$payload['payment_method_collection'] = 'if_required'; $payload['payment_method_collection'] = 'if_required';
} }
$customer = currentTeam()->subscription?->stripe_customer_id ?? null; $customer = currentTeam()->subscription?->stripe_customer_id ?? null;

View File

@@ -156,25 +156,27 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
// Generate custom host<->ip mapping // Generate custom host<->ip mapping
$allContainers = instant_remote_process(["docker network inspect {$this->destination->network} -f '{{json .Containers}}' "], $this->server); $allContainers = instant_remote_process(["docker network inspect {$this->destination->network} -f '{{json .Containers}}' "], $this->server);
$allContainers = format_docker_command_output_to_json($allContainers); if (!is_null($allContainers)) {
$ips = collect([]); $allContainers = format_docker_command_output_to_json($allContainers);
if (count($allContainers) > 0) { $ips = collect([]);
$allContainers = $allContainers[0]; if (count($allContainers) > 0) {
foreach ($allContainers as $container) { $allContainers = $allContainers[0];
$containerName = data_get($container, 'Name'); foreach ($allContainers as $container) {
if ($containerName === 'coolify-proxy') { $containerName = data_get($container, 'Name');
continue; if ($containerName === 'coolify-proxy') {
} continue;
$containerIp = data_get($container, 'IPv4Address'); }
if ($containerName && $containerIp) { $containerIp = data_get($container, 'IPv4Address');
$containerIp = str($containerIp)->before('/'); if ($containerName && $containerIp) {
$ips->put($containerName, $containerIp->value()); $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) { if ($this->application->dockerfile_target_build) {
$this->buildTarget = " --target {$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') { if ($this->application->docker_registry_image_name && $this->application->build_pack !== 'dockerimage') {
$this->push_to_docker_registry(); $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->next(ApplicationDeploymentStatus::FINISHED->value);
$this->application->isConfigurationChanged(true); $this->application->isConfigurationChanged(true);
@@ -290,42 +303,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
ray($e); 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() private function generate_image_names()
{ {
if ($this->application->dockerfile) { if ($this->application->dockerfile) {
@@ -402,7 +379,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$envs->push($env->key . '=' . $env->value); $envs->push($env->key . '=' . $env->value);
} }
} }
ray($envs);
$envs_base64 = base64_encode($envs->implode("\n")); $envs_base64 = base64_encode($envs->implode("\n"));
$this->execute_remote_command( $this->execute_remote_command(
[ [
@@ -468,22 +444,31 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->cleanup_git(); $this->cleanup_git();
$composeFile = $this->application->parseCompose(pull_request_id: $this->pull_request_id); $composeFile = $this->application->parseCompose(pull_request_id: $this->pull_request_id);
$yaml = Yaml::dump($composeFile->toArray(), 10); $yaml = Yaml::dump($composeFile->toArray(), 10);
ray($composeFile);
ray($this->container_name);
$this->docker_compose_base64 = base64_encode($yaml); $this->docker_compose_base64 = base64_encode($yaml);
$this->execute_remote_command([ $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->save_environment_variables();
$this->stop_running_container(force: true); $this->stop_running_container(force: true);
ray($this->pull_request_id);
$networkId = $this->application->uuid; $networkId = $this->application->uuid;
if ($this->pull_request_id !== 0) { if ($this->pull_request_id !== 0) {
$networkId = "{$this->application->uuid}-{$this->pull_request_id}"; $networkId = "{$this->application->uuid}-{$this->pull_request_id}";
} }
$this->execute_remote_command([ ray($networkId);
"docker network create --attachable '{$networkId}' >/dev/null || true", "hidden" => true, "ignore_errors" => true if ($this->server->isSwarm()) {
], [ // TODO
"docker network connect {$networkId} coolify-proxy || true", "hidden" => true, "ignore_errors" => true } 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->start_by_compose_file();
$this->application->loadComposeFile(isInit: false); $this->application->loadComposeFile(isInit: false);
} }
@@ -570,74 +555,83 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function rolling_update() private function rolling_update()
{ {
if (count($this->application->ports_mappings_array) > 0) { if ($this->server->isSwarm()) {
$this->execute_remote_command( // Skip this.
[
"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 { } else {
$this->execute_remote_command( if (count($this->application->ports_mappings_array) > 0) {
[ $this->execute_remote_command(
"echo '\n----------------------------------------'", [
], "echo '\n----------------------------------------'",
["echo -n 'Rolling update started.'"], ],
); ["echo -n 'Application has ports mapped to the host system, rolling update is not supported.'"],
$this->start_by_compose_file(); );
$this->health_check(); $this->stop_running_container(force: true);
$this->stop_running_container(); $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() private function health_check()
{ {
if ($this->application->isHealthcheckDisabled()) { if ($this->server->isSwarm()) {
$this->newVersionIsHealthy = true; // Implement healthcheck for swarm
return; } else {
} if ($this->application->isHealthcheckDisabled()) {
// ray('New container name: ', $this->container_name); $this->newVersionIsHealthy = true;
if ($this->container_name) { return;
$counter = 1; }
$this->execute_remote_command( // ray('New container name: ', $this->container_name);
[ if ($this->container_name) {
"echo 'Waiting for healthcheck to pass on the new container.'" $counter = 1;
]
);
if ($this->full_healthcheck_url) {
$this->execute_remote_command( $this->execute_remote_command(
[ [
"echo 'Healthcheck URL (inside the container): {$this->full_healthcheck_url}'" "echo 'Waiting for healthcheck to pass on the new container.'"
] ]
); );
} if ($this->full_healthcheck_url) {
while ($counter < $this->application->health_check_retries) {
$this->execute_remote_command(
[
"docker inspect --format='{{json .State.Health.Status}}' {$this->container_name}",
"hidden" => true,
"save" => "health_check"
],
);
$this->execute_remote_command(
[
"echo 'Attempt {$counter} of {$this->application->health_check_retries} | Healthcheck status: {$this->saved_outputs->get('health_check')}'"
],
);
if (Str::of($this->saved_outputs->get('health_check'))->contains('healthy')) {
$this->newVersionIsHealthy = true;
$this->application->update(['status' => 'running']);
$this->execute_remote_command( $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(' '); $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() private function generate_compose_file()
{ {
$ports = $this->application->settings->is_static ? [80] : $this->application->ports_exposes_array; $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) { if ($this->pull_request_id !== 0) {
$labels = collect(generateLabelsApplication($this->application, $this->preview)); $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(); $labels = $labels->merge(defaultLabels($this->application->id, $this->application->uuid, $this->pull_request_id))->toArray();
$docker_compose = [ $docker_compose = [
@@ -909,7 +868,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
'container_name' => $this->container_name, 'container_name' => $this->container_name,
'restart' => RESTART_MODE, 'restart' => RESTART_MODE,
'environment' => $environment_variables, 'environment' => $environment_variables,
'labels' => $labels,
'expose' => $ports, 'expose' => $ports,
'networks' => [ 'networks' => [
$this->destination->network, $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()) { if ($this->server->isLogDrainEnabled() && $this->application->isLogDrainEnabled()) {
$docker_compose['services'][$this->container_name]['logging'] = [ $docker_compose['services'][$this->container_name]['logging'] = [
'driver' => 'fluentd', 'driver' => 'fluentd',
@@ -988,6 +988,11 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
// 'dockerfile' => $this->workdir . $this->dockerfile_location, // '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 = Yaml::dump($docker_compose, 10);
$this->docker_compose_base64 = base64_encode($this->docker_compose); $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]); $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], [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 { } else {
$this->application_deployment_queue->addLogEntry("New container is not healthy, rolling back to the old container."); $this->application_deployment_queue->addLogEntry("New container is not healthy, rolling back to the old container.");
$this->execute_remote_command( $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], [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() private function generate_build_env_variables()
@@ -1289,9 +1294,13 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$this->execute_remote_command( $this->execute_remote_command(
["echo 'Oops something is not okay, are you okay? 😢'", 'type' => 'err'], ["echo 'Oops something is not okay, are you okay? 😢'", 'type' => 'err'],
["echo '{$exception->getMessage()}'", '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); $this->next(ApplicationDeploymentStatus::FAILED->value);
} }

View File

@@ -47,7 +47,7 @@ class CheckLogDrainContainerJob implements ShouldQueue, ShouldBeEncrypted
if (!$this->server->isServerReady()) { if (!$this->server->isServerReady()) {
return; return;
}; };
$containers = instant_remote_process(["docker container ls -q"], $this->server); $containers = instant_remote_process(["docker container ls -q"], $this->server, false);
if (!$containers) { if (!$containers) {
return; return;
} }

View File

@@ -42,8 +42,43 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
if (!$this->server->isServerReady()) { if (!$this->server->isServerReady()) {
return; 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, false);
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); $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(); $applications = $this->server->applications();
$databases = $this->server->databases(); $databases = $this->server->databases();
$services = $this->server->services()->get(); $services = $this->server->services()->get();
@@ -55,10 +90,15 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
$foundServices = []; $foundServices = [];
foreach ($containers as $container) { 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');
}
$containerStatus = data_get($container, 'State.Status'); $containerStatus = data_get($container, 'State.Status');
$containerHealth = data_get($container, 'State.Health.Status', 'unhealthy'); $containerHealth = data_get($container, 'State.Health.Status', 'unhealthy');
$containerStatus = "$containerStatus ($containerHealth)"; $containerStatus = "$containerStatus ($containerHealth)";
$labels = data_get($container, 'Config.Labels');
$labels = Arr::undot(format_docker_labels_to_json($labels)); $labels = Arr::undot(format_docker_labels_to_json($labels));
$applicationId = data_get($labels, 'coolify.applicationId'); $applicationId = data_get($labels, 'coolify.applicationId');
if ($applicationId) { if ($applicationId) {
@@ -245,7 +285,11 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
// Check if proxy is running // Check if proxy is running
$this->server->proxyType(); $this->server->proxyType();
$foundProxyContainer = $containers->filter(function ($value, $key) { $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(); })->first();
if (!$foundProxyContainer) { if (!$foundProxyContainer) {
try { try {

View File

@@ -603,6 +603,7 @@ class Application extends BaseModel
{ {
if ($this->docker_compose_raw) { if ($this->docker_compose_raw) {
$mainCompose = parseDockerComposeFile(resource: $this, isNew: false, pull_request_id: $pull_request_id); $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) { 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); 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) function loadComposeFile($isInit = false)
{ {
$initialDockerComposeLocation = $this->docker_compose_location; $initialDockerComposeLocation = $this->docker_compose_location;
$initialDockerComposePrLocation = $this->docker_compose_pr_location; // $initialDockerComposePrLocation = $this->docker_compose_pr_location;
if ($this->build_pack === 'dockercompose') { if ($this->build_pack === 'dockercompose') {
if ($isInit && $this->docker_compose_raw) { if ($isInit && $this->docker_compose_raw) {
return; 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: '.'); ['commands' => $cloneCommand] = $this->generateGitImportCommands(deployment_uuid: $uuid, only_checkout: true, exec_in_docker: false, custom_base_dir: '.');
$workdir = rtrim($this->base_directory, '/'); $workdir = rtrim($this->base_directory, '/');
$composeFile = $this->docker_compose_location; $composeFile = $this->docker_compose_location;
$prComposeFile = $this->docker_compose_pr_location; // $prComposeFile = $this->docker_compose_pr_location;
$fileList = collect([".$composeFile"]); $fileList = collect([".$workdir$composeFile"]);
if ($composeFile !== $prComposeFile) { // if ($composeFile !== $prComposeFile) {
$fileList->push(".$prComposeFile"); // $fileList->push(".$prComposeFile");
} // }
$commands = collect([ $commands = collect([
"mkdir -p /tmp/{$uuid} && cd /tmp/{$uuid}", "mkdir -p /tmp/{$uuid} && cd /tmp/{$uuid}",
$cloneCommand, $cloneCommand,
@@ -645,24 +646,24 @@ class Application extends BaseModel
$this->docker_compose_raw = $composeFileContent; $this->docker_compose_raw = $composeFileContent;
$this->save(); $this->save();
} }
if ($composeFile === $prComposeFile) { // if ($composeFile === $prComposeFile) {
$this->docker_compose_pr_raw = $composeFileContent; // $this->docker_compose_pr_raw = $composeFileContent;
$this->save(); // $this->save();
} else { // } else {
$commands = collect([ // $commands = collect([
"cd /tmp/{$uuid}", // "cd /tmp/{$uuid}",
"cat .$workdir$prComposeFile", // "cat .$workdir$prComposeFile",
]); // ]);
$composePrFileContent = instant_remote_process($commands, $this->destination->server, false); // $composePrFileContent = instant_remote_process($commands, $this->destination->server, false);
if (!$composePrFileContent) { // if (!$composePrFileContent) {
$this->docker_compose_pr_location = $initialDockerComposePrLocation; // $this->docker_compose_pr_location = $initialDockerComposePrLocation;
$this->save(); // $this->save();
throw new \Exception("Could not load compose file from $workdir$prComposeFile"); // throw new \Exception("Could not load compose file from $workdir$prComposeFile");
} else { // } else {
$this->docker_compose_pr_raw = $composePrFileContent; // $this->docker_compose_pr_raw = $composePrFileContent;
$this->save(); // $this->save();
} // }
} // }
$commands = collect([ $commands = collect([
"rm -rf /tmp/{$uuid}", "rm -rf /tmp/{$uuid}",

View File

@@ -2,8 +2,6 @@
namespace App\Models; namespace App\Models;
use App\Actions\Server\InstallLogDrain;
use App\Actions\Server\InstallNewRelic;
use App\Enums\ApplicationDeploymentStatus; use App\Enums\ApplicationDeploymentStatus;
use App\Enums\ProxyStatus; use App\Enums\ProxyStatus;
use App\Enums\ProxyTypes; use App\Enums\ProxyTypes;
@@ -18,7 +16,7 @@ use Illuminate\Support\Sleep;
use Spatie\SchemalessAttributes\Casts\SchemalessAttributes; use Spatie\SchemalessAttributes\Casts\SchemalessAttributes;
use Spatie\SchemalessAttributes\SchemalessAttributesTrait; use Spatie\SchemalessAttributes\SchemalessAttributesTrait;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Stringable; use Illuminate\Support\Stringable;
class Server extends BaseModel class Server extends BaseModel
{ {
@@ -37,25 +35,10 @@ class Server extends BaseModel
} }
$server->forceFill($payload); $server->forceFill($payload);
}); });
static::created(function ($server) { static::created(function ($server) {
ServerSetting::create([ ServerSetting::create([
'server_id' => $server->id, '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) { static::deleting(function ($server) {
$server->destinations()->each(function ($destination) { $server->destinations()->each(function ($destination) {
@@ -84,7 +67,7 @@ class Server extends BaseModel
{ {
$teamId = currentTeam()->id; $teamId = currentTeam()->id;
$selectArray = collect($select)->concat(['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() static public function isUsable()
@@ -103,7 +86,41 @@ class Server extends BaseModel
{ {
return $this->hasOne(ServerSetting::class); 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() public function proxyType()
{ {
$proxyType = $this->proxy->get('type'); $proxyType = $this->proxy->get('type');
@@ -359,12 +376,16 @@ class Server extends BaseModel
return false; return false;
} }
} }
public function isSwarm()
{
return data_get($this, 'settings.is_swarm_manager') || data_get($this, 'settings.is_swarm_worker');
}
public function validateConnection() public function validateConnection()
{ {
$server = Server::find($this->id);
if ($this->skipServer()) { if ($this->skipServer()) {
return false; return false;
} }
$uptime = instant_remote_process(['uptime'], $this, false); $uptime = instant_remote_process(['uptime'], $this, false);
if (!$uptime) { if (!$uptime) {
$this->settings()->update([ $this->settings()->update([
@@ -375,14 +396,14 @@ class Server extends BaseModel
$this->settings()->update([ $this->settings()->update([
'is_reachable' => true, 'is_reachable' => true,
]); ]);
$this->update([ $server->update([
'unreachable_count' => 0, 'unreachable_count' => 0,
]); ]);
} }
if (data_get($this, 'unreachable_notification_sent') === true) { if (data_get($this, 'unreachable_notification_sent') === true) {
$this->team->notify(new Revived($this)); $this->team->notify(new Revived($this));
$this->update(['unreachable_notification_sent' => false]); $server->update(['unreachable_notification_sent' => false]);
} }
return true; return true;
@@ -400,7 +421,20 @@ class Server extends BaseModel
} }
$this->settings->is_usable = true; $this->settings->is_usable = true;
$this->settings->save(); $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; return true;
} }
public function validateDockerEngineVersion() public function validateDockerEngineVersion()
@@ -417,9 +451,13 @@ class Server extends BaseModel
$this->settings->save(); $this->settings->save();
return true; 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) public function executeRemoteCommand(Collection $commands, ?ApplicationDeploymentQueue $loggingModel = null)
{ {

View File

@@ -4,13 +4,57 @@ namespace App\Models;
class SwarmDocker extends BaseModel class SwarmDocker extends BaseModel
{ {
protected $guarded = [];
public function applications() public function applications()
{ {
return $this->morphMany(Application::class, 'destination'); 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() public function server()
{ {
return $this->belongsTo(Server::class); 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;
}
} }

View File

@@ -93,7 +93,11 @@ function executeInDocker(string $containerId, string $command)
function getContainerStatus(Server $server, string $container_id, bool $all_data = false, bool $throwError = false) 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) { if (!$container) {
return 'exited'; return 'exited';
} }
@@ -101,7 +105,19 @@ function getContainerStatus(Server $server, string $container_id, bool $all_data
if ($all_data) { if ($all_data) {
return $container[0]; 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) function generateApplicationContainerName(Application $application, $pull_request_id = 0)
@@ -275,8 +291,11 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
return $labels->all(); return $labels->all();
} }
function isDatabaseImage(string $image) function isDatabaseImage(?string $image = null)
{ {
if (is_null($image)) {
return false;
}
$image = str($image); $image = str($image);
if ($image->contains(':')) { if ($image->contains(':')) {
$image = str($image); $image = str($image);

View File

@@ -54,9 +54,15 @@ function connectProxyToNetworks(Server $server)
function generate_default_proxy_configuration(Server $server) function generate_default_proxy_configuration(Server $server)
{ {
$proxy_path = get_proxy_path(); $proxy_path = get_proxy_path();
$networks = collect($server->standaloneDockers)->map(function ($docker) { if ($server->isSwarm()) {
return $docker['network']; $networks = collect($server->swarmDockers)->map(function ($docker) {
})->unique(); return $docker['network'];
})->unique();
} else {
$networks = collect($server->standaloneDockers)->map(function ($docker) {
return $docker['network'];
})->unique();
}
if ($networks->count() === 0) { if ($networks->count() === 0) {
$networks = collect(['coolify']); $networks = collect(['coolify']);
} }
@@ -66,6 +72,16 @@ function generate_default_proxy_configuration(Server $server)
"external" => true, "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 = [ $config = [
"version" => "3.8", "version" => "3.8",
"networks" => $array_of_networks->toArray(), "networks" => $array_of_networks->toArray(),
@@ -102,7 +118,6 @@ function generate_default_proxy_configuration(Server $server)
"--entrypoints.https.address=:443", "--entrypoints.https.address=:443",
"--entrypoints.http.http.encodequerysemicolons=true", "--entrypoints.http.http.encodequerysemicolons=true",
"--entrypoints.https.http.encodequerysemicolons=true", "--entrypoints.https.http.encodequerysemicolons=true",
"--providers.docker=true",
"--providers.docker.exposedbydefault=false", "--providers.docker.exposedbydefault=false",
"--providers.file.directory=/traefik/dynamic/", "--providers.file.directory=/traefik/dynamic/",
"--providers.file.watch=true", "--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.storage=/traefik/acme.json",
"--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=http", "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=http",
], ],
"labels" => [ "labels" => $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",
],
], ],
], ],
]; ];
@@ -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.filepath=/traefik/access.log";
$config['services']['traefik']['command'][] = "--accesslog.bufferingsize=100"; $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); SaveConfiguration::run($server, $config);
return $config; return $config;
} }

View File

@@ -123,6 +123,9 @@ function instant_remote_process(Collection|array $command, Server $server, $thro
} }
return excludeCertainErrors($process->errorOutput(), $exitCode); return excludeCertainErrors($process->errorOutput(), $exitCode);
} }
if ($output === 'null') {
$output = null;
}
return $output; return $output;
} }
function excludeCertainErrors(string $errorOutput, ?int $exitCode = null) function excludeCertainErrors(string $errorOutput, ?int $exitCode = null)

View File

@@ -863,7 +863,6 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$key = Str::of($variableName); $key = Str::of($variableName);
$value = Str::of($variable); $value = Str::of($variable);
} }
// TODO: here is the problem
if ($key->startsWith('SERVICE_FQDN')) { if ($key->startsWith('SERVICE_FQDN')) {
if ($isNew || $savedService->fqdn === null) { if ($isNew || $savedService->fqdn === null) {
$name = $key->after('SERVICE_FQDN_')->beforeLast('_')->lower(); $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()); data_set($service, 'volumes', $serviceVolumes->toArray());
} }
} else { } else {
// TODO
} }
// Decide if the service is a database // Decide if the service is a database
$isDatabase = isDatabaseImage(data_get_str($service, 'image')); $isDatabase = isDatabaseImage(data_get_str($service, 'image'));

View File

@@ -22,7 +22,7 @@ return [
'official' => 'https://cdn.coollabs.io/coolify/service-templates.json', 'official' => 'https://cdn.coollabs.io/coolify/service-templates.json',
], ],
'limits' => [ 'limits' => [
'trial_period' => 7, 'trial_period' => 0,
'server' => [ 'server' => [
'zero' => 0, 'zero' => 0,
'self-hosted' => 999999999999, 'self-hosted' => 999999999999,

View File

@@ -3,11 +3,11 @@
return [ return [
// @see https://docs.sentry.io/product/sentry-basics/dsn-explainer/ // @see https://docs.sentry.io/product/sentry-basics/dsn-explainer/
'dsn' => 'https://396748153b19c469f5ceff50f1664323@o1082494.ingest.sentry.io/4505347448045568', 'dsn' => 'https://bea22abf110618b07252032aa2e07859@o1082494.ingest.sentry.io/4505347448045568',
// The release version of your application // The release version of your application
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')) // 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.149',
// When left empty or `null` the Laravel environment will be used // When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'), 'environment' => config('app.env'),

View File

@@ -1,3 +1,3 @@
<?php <?php
return '4.0.0-beta.147'; return '4.0.0-beta.149';

View File

@@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('swarm_dockers', function (Blueprint $table) {
$table->string('network');
$table->unique(['server_id', 'network']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('swarm_dockers', function (Blueprint $table) {
$table->dropColumn('network');
});
}
};

View File

@@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('server_settings', function (Blueprint $table) {
$table->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');
});
}
};

View File

@@ -13,9 +13,9 @@ class SwarmDockerSeeder extends Seeder
*/ */
public function run(): void public function run(): void
{ {
SwarmDocker::create([ // SwarmDocker::create([
'name' => 'Swarm Docker 1', // 'name' => 'Swarm Docker 1',
'server_id' => 1, // 'server_id' => 1,
]); // ]);
} }
} }

View File

@@ -1,2 +1,2 @@
#!/command/execlineb -P #!/command/execlineb -P
php /var/www/html/artisan app:init php /var/www/html/artisan app:init --cleanup

View File

@@ -15,6 +15,8 @@
<div class="flex-1"></div> <div class="flex-1"></div>
@if ($application->build_pack === 'dockercompose' && is_null($application->docker_compose_raw)) @if ($application->build_pack === 'dockercompose' && is_null($application->docker_compose_raw))
<div>Please load a Compose file.</div> <div>Please load a Compose file.</div>
@elseif ($application->destination->server->isSwarm() && str($application->docker_registry_image_name)->isEmpty())
Swarm Deployments requires a Docker Image in a Registry.
@else @else
<x-applications.advanced :application="$application" /> <x-applications.advanced :application="$application" />
@if ($application->status !== 'exited') @if ($application->status !== 'exited')

View File

@@ -21,8 +21,11 @@
</label> </label>
</fieldset> </fieldset>
</div> </div>
<div class="py-2 text-center"><span class="font-bold text-warning">{{ config('constants.limits.trial_period') }} @if (config('constants.limits.trial_period') > 0)
days trial</span> included on all plans, without credit card details.</div> <div class="py-2 text-center"><span
class="font-bold text-warning">{{ config('constants.limits.trial_period') }}
days trial</span> included on all plans, without credit card details.</div>
@endif
<div x-show="selected === 'monthly'" class="flex justify-center h-10 mt-3 text-sm leading-6 "> <div x-show="selected === 'monthly'" class="flex justify-center h-10 mt-3 text-sm leading-6 ">
<div>Save <span class="font-bold text-warning">10%</span> annually with the yearly plans. <div>Save <span class="font-bold text-warning">10%</span> annually with the yearly plans.
</div> </div>
@@ -255,8 +258,8 @@
<div class="flex items-start gap-4 text-xl tracking-tight">Need official support for <div class="flex items-start gap-4 text-xl tracking-tight">Need official support for
your self-hosted instance? your self-hosted instance?
<x-forms.button> <x-forms.button>
<a class="font-bold text-white hover:no-underline" <a class="font-bold text-white hover:no-underline" href="{{ config('coolify.docs') }}">Contact
href="{{ config('coolify.docs') }}">Contact Us</a> Us</a>
</x-forms.button> </x-forms.button>
</div> </div>
</div> </div>

View File

@@ -131,16 +131,16 @@
</form> </form>
</div> </div>
@if (!$serverReachable) @if (!$serverReachable)
This server is not reachable with the following public key. This server is not reachable with the following public key.
<br /> <br /> <br /> <br />
Please make sure you have the correct public key in your ~/.ssh/authorized_keys file for user 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 'root' or skip the boarding process and add a new private key manually to Coolify and to the
server. server.
<x-forms.input readonly id="serverPublicKey"></x-forms.input> <x-forms.input readonly id="serverPublicKey"></x-forms.input>
<x-forms.button class="box" wire:target="validateServer" <x-forms.button class="box" wire:target="validateServer" wire:click="validateServer">Check
wire:click="validateServer">Check again again
</x-forms.button> </x-forms.button>
@endif @endif
</x-slot:actions> </x-slot:actions>
<x-slot:explanation> <x-slot:explanation>
<p>Private Keys are used to connect to a remote server through a secure shell, called SSH.</p> <p>Private Keys are used to connect to a remote server through a secure shell, called SSH.</p>
@@ -200,14 +200,17 @@
label="Description" id="remoteServerDescription" /> label="Description" id="remoteServerDescription" />
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
<x-forms.input required placeholder="127.0.0.1" label="IP Address" <x-forms.input required placeholder="127.0.0.1" label="IP Address" id="remoteServerHost" />
id="remoteServerHost" />
<x-forms.input required placeholder="Port number of your server. Default is 22." <x-forms.input required placeholder="Port number of your server. Default is 22."
label="Port" id="remoteServerPort" /> label="Port" id="remoteServerPort" />
<x-forms.input required readonly <x-forms.input required readonly
placeholder="Username to connect to your server. Default is root." label="Username" placeholder="Username to connect to your server. Default is root." label="Username"
id="remoteServerUser" /> id="remoteServerUser" />
</div> </div>
{{-- <div class="w-64">
<x-forms.checkbox type="checkbox" id="isSwarmManager"
label="Is it a Swarm Manager?" />
</div> --}}
<x-forms.button type="submit">Check Connection</x-forms.button> <x-forms.button type="submit">Check Connection</x-forms.button>
</form> </form>
</x-slot:actions> </x-slot:actions>
@@ -226,7 +229,7 @@
</x-slot:question> </x-slot:question>
<x-slot:actions> <x-slot:actions>
<x-forms.button class="justify-center box" wire:click="installDocker"> <x-forms.button class="justify-center box" wire:click="installDocker">
Let's do it!</x-forms.button> Let's do it!</x-forms.button>
@if ($dockerInstallationStarted) @if ($dockerInstallationStarted)
<x-forms.button class="justify-center box" wire:click="dockerInstalledOrSkipped"> <x-forms.button class="justify-center box" wire:click="dockerInstalledOrSkipped">
Validate Server & Continue</x-forms.button> Validate Server & Continue</x-forms.button>
@@ -234,7 +237,10 @@
</x-slot:actions> </x-slot:actions>
<x-slot:explanation> <x-slot:explanation>
<p>This will install the latest Docker Engine on your server, configure a few things to be able <p>This will install the latest Docker Engine on your server, configure a few things to be able
to run optimal.<br><br>Minimum Docker Engine version is: 22<br><br>To manually install Docker Engine, check <a target="_blank" class="underline text-warning" href="https://coolify.io/docs/servers#install-docker-engine-manually">this documentation</a>.</p> to run optimal.<br><br>Minimum Docker Engine version is: 22<br><br>To manually install Docker
Engine, check <a target="_blank" class="underline text-warning"
href="https://coolify.io/docs/servers#install-docker-engine-manually">this
documentation</a>.</p>
</x-slot:explanation> </x-slot:explanation>
</x-boarding-step> </x-boarding-step>

View File

@@ -69,21 +69,39 @@
@endif @endif
@if ($application->build_pack !== 'dockerimage' && $application->build_pack !== 'dockercompose') @if ($application->build_pack !== 'dockerimage' && $application->build_pack !== 'dockercompose')
<h3>Docker Registry</h3> <h3>Docker Registry</h3>
<div>Push the built image to a docker registry. More info <a class="underline" @if ($application->destination->server->isSwarm())
href="https://coolify.io/docs/docker-registries" target="_blank">here</a>.</div> <div>Docker Swarm requires the image to be available in a registry. More info <a class="underline"
href="https://coolify.io/docs/docker-registries" target="_blank">here</a>.</div>
@else
<div>Push the built image to a docker registry. More info <a class="underline"
href="https://coolify.io/docs/docker-registries" target="_blank">here</a>.</div>
@endif
<div class="flex flex-col gap-2 xl:flex-row"> <div class="flex flex-col gap-2 xl:flex-row">
@if ($application->build_pack === 'dockerimage') @if ($application->build_pack === 'dockerimage')
<x-forms.input id="application.docker_registry_image_name" label="Docker Image" /> @if ($application->destination->server->isSwarm())
<x-forms.input id="application.docker_registry_image_tag" label="Docker Image Tag" /> <x-forms.input required id="application.docker_registry_image_name" label="Docker Image" />
<x-forms.input id="application.docker_registry_image_tag" label="Docker Image Tag" />
@else
<x-forms.input id="application.docker_registry_image_name" label="Docker Image" />
<x-forms.input id="application.docker_registry_image_tag" label="Docker Image Tag" />
@endif
@else @else
<x-forms.input id="application.docker_registry_image_name" @if ($application->destination->server->isSwarm())
helper="Empty means it won't push the image to a docker registry." <x-forms.input id="application.docker_registry_image_name" required label="Docker Image" />
placeholder="Empty means it won't push the image to a docker registry." <x-forms.input id="application.docker_registry_image_tag"
label="Docker Image" /> helper="If set, it will tag the built image with this tag too. <br><br>Example: If you set it to 'latest', it will push the image with the commit sha tag + with the latest tag."
<x-forms.input id="application.docker_registry_image_tag" label="Docker Image Tag" />
placeholder="Empty means only push commit sha tag." @else
helper="If set, it will tag the built image with this tag too. <br><br>Example: If you set it to 'latest', it will push the image with the commit sha tag + with the latest tag." <x-forms.input id="application.docker_registry_image_name"
label="Docker Image Tag" /> helper="Empty means it won't push the image to a docker registry."
placeholder="Empty means it won't push the image to a docker registry."
label="Docker Image" />
<x-forms.input id="application.docker_registry_image_tag"
placeholder="Empty means only push commit sha tag."
helper="If set, it will tag the built image with this tag too. <br><br>Example: If you set it to 'latest', it will push the image with the commit sha tag + with the latest tag."
label="Docker Image Tag" />
@endif
@endif @endif
</div> </div>
@@ -140,8 +158,8 @@
@endif @endif
@if ($application->build_pack === 'dockercompose') @if ($application->build_pack === 'dockercompose')
<x-forms.button wire:click="loadComposeFile">Reload Compose File</x-forms.button> <x-forms.button wire:click="loadComposeFile">Reload Compose File</x-forms.button>
<x-forms.textarea rows="10" readonly id="application.docker_compose" label="Docker Compose Content" <x-forms.textarea rows="10" readonly id="application.docker_compose"
helper="You need to modify the docker compose file." /> label="Docker Compose Content" helper="You need to modify the docker compose file." />
{{-- <x-forms.textarea rows="10" readonly id="application.docker_compose_pr" {{-- <x-forms.textarea rows="10" readonly id="application.docker_compose_pr"
label="Docker PR Compose Content" helper="You need to modify the docker compose file." /> --}} label="Docker PR Compose Content" helper="You need to modify the docker compose file." /> --}}
@endif @endif

View File

@@ -38,8 +38,7 @@
<x-forms.input id="server.description" label="Description" /> <x-forms.input id="server.description" label="Description" />
<x-forms.input placeholder="https://example.com" id="wildcard_domain" label="Wildcard Domain" <x-forms.input placeholder="https://example.com" id="wildcard_domain" label="Wildcard Domain"
helper="Wildcard domain for your applications. If you set this, you will get a random generated domain for your new applications.<br><span class='font-bold text-white'>Example:</span><br>In case you set:<span class='text-helper'>https://example.com</span> your applications will get:<br> <span class='text-helper'>https://randomId.example.com</span>" /> helper="Wildcard domain for your applications. If you set this, you will get a random generated domain for your new applications.<br><span class='font-bold text-white'>Example:</span><br>In case you set:<span class='text-helper'>https://example.com</span> your applications will get:<br> <span class='text-helper'>https://randomId.example.com</span>" />
{{-- <x-forms.checkbox disabled type="checkbox" id="server.settings.is_part_of_swarm"
label="Is it part of a Swarm cluster?" /> --}}
</div> </div>
<div class="flex flex-col w-full gap-2 lg:flex-row"> <div class="flex flex-col w-full gap-2 lg:flex-row">
<x-forms.input id="server.ip" label="IP Address/Domain" <x-forms.input id="server.ip" label="IP Address/Domain"
@@ -49,13 +48,17 @@
<x-forms.input type="number" id="server.port" label="Port" required /> <x-forms.input type="number" id="server.port" label="Port" required />
</div> </div>
</div> </div>
@if (!$server->isLocalhost()) <div class="w-64">
<div class="w-64"> @if (!$server->isLocalhost())
<x-forms.checkbox instantSave <x-forms.checkbox instantSave
helper="If you are using Cloudflare Tunnels, enable this. It will proxy all ssh requests to your server through Cloudflare.<span class='text-warning'>Coolify does not install/setup Cloudflare (cloudflared) on your server.</span>" helper="If you are using Cloudflare Tunnels, enable this. It will proxy all ssh requests to your server through Cloudflare.<span class='text-warning'>Coolify does not install/setup Cloudflare (cloudflared) on your server.</span>"
id="server.settings.is_cloudflare_tunnel" label="Cloudflare Tunnel" /> id="server.settings.is_cloudflare_tunnel" label="Cloudflare Tunnel" />
</div> @endif
@endif {{-- <x-forms.checkbox instantSave type="checkbox" id="server.settings.is_swarm_manager"
label="Is it a Swarm Manager?" /> --}}
{{-- <x-forms.checkbox instantSave type="checkbox" id="server.settings.is_swarm_worker"
label="Is it a Swarm Worker?" /> --}}
</div>
</div> </div>
@if ($server->isFunctional()) @if ($server->isFunctional())

View File

@@ -3,7 +3,7 @@
<x-limit-reached name="servers" /> <x-limit-reached name="servers" />
@else @else
<h1>Create a new Server</h1> <h1>Create a new Server</h1>
<div class="subtitle ">Servers are the main blocks of your infrastructure.</div> <div class="subtitle">Servers are the main blocks of your infrastructure.</div>
<form class="flex flex-col gap-2" wire:submit.prevent='submit'> <form class="flex flex-col gap-2" wire:submit.prevent='submit'>
<div class="flex gap-2"> <div class="flex gap-2">
<x-forms.input id="name" label="Name" required /> <x-forms.input id="name" label="Name" required />
@@ -25,6 +25,10 @@
@endif @endif
@endforeach @endforeach
</x-forms.select> </x-forms.select>
{{-- <div class="w-64">
<x-forms.checkbox type="checkbox" id="is_swarm_manager"
label="Is it a Swarm Manager?" />
</div> --}}
<x-forms.button type="submit"> <x-forms.button type="submit">
Save New Server Save New Server
</x-forms.button> </x-forms.button>

View File

@@ -1,6 +1,6 @@
<div> <div>
@if ($server->isFunctional()) @if ($server->isFunctional())
<div class="flex gap-2" @if ($polling) wire:poll.2000ms='checkProxy' @endif> <div class="flex gap-2" wire:poll.5000ms='checkProxy'>
@if (data_get($server, 'proxy.status') === 'running') @if (data_get($server, 'proxy.status') === 'running')
<x-status.running status="Proxy Running" /> <x-status.running status="Proxy Running" />
@elseif (data_get($server, 'proxy.status') === 'restarting') @elseif (data_get($server, 'proxy.status') === 'restarting')

View File

@@ -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 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 # 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" 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"

View File

@@ -4,7 +4,7 @@
"version": "3.12.36" "version": "3.12.36"
}, },
"v4": { "v4": {
"version": "4.0.0-beta.147" "version": "4.0.0-beta.149"
} }
} }
} }