Compare commits

...

97 Commits

Author SHA1 Message Date
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
Andras Bacsai
5f8b8bd730 Merge pull request #1480 from coollabsio/next
v4.0.0-beta.147
2023-11-28 13:39:34 +01:00
Andras Bacsai
44f6d93639 Update installation script to include curl and wget 2023-11-28 13:28:15 +01:00
Andras Bacsai
e35b8a0f96 Add Stringable interface to validateOS method 2023-11-28 13:21:32 +01:00
Andras Bacsai
b26e23e7c3 Fix validateOS() return type 2023-11-28 13:17:59 +01:00
Andras Bacsai
e6f7e32037 Add SUPPORTED_OS constant based on /etc/os-release 2023-11-28 13:12:42 +01:00
Andras Bacsai
1c386db41d Update Docker installation command and add support for SLES 2023-11-28 13:12:25 +01:00
Andras Bacsai
085b655d9f Update version to 1.1.0 and add support for Redhat and Sles based operating systems 2023-11-28 13:02:12 +01:00
Andras Bacsai
2788fcf4e1 Add Docker Compose based applications and preview deployments to proxy on restart 2023-11-28 12:48:55 +01:00
Andras Bacsai
d058e04213 Add fqdn attribute to InstanceSettings model 2023-11-28 12:11:03 +01:00
Andras Bacsai
066f171163 Add Docker Compose file for Formbricks service 2023-11-28 12:05:14 +01:00
Andras Bacsai
2001be07d0 refactor: env variable generator 2023-11-28 12:05:04 +01:00
Andras Bacsai
39552cc42f fix: double default password length 2023-11-28 12:04:21 +01:00
Andras Bacsai
7f5d7e0eb0 Refactor application submit method to handle dockercompose build pack 2023-11-28 11:10:48 +01:00
Andras Bacsai
0eda49b104 fix: pull request build variables 2023-11-28 11:10:42 +01:00
Andras Bacsai
636995d0e4 Refactor server delete view 2023-11-28 10:55:24 +01:00
Andras Bacsai
4c0623f022 Refactor server delete view to display defined resources as links 2023-11-28 10:54:46 +01:00
Andras Bacsai
3e2e1080f5 nothing to see here 2023-11-28 10:46:00 +01:00
Andras Bacsai
3f866a07d8 Fix docker compose PR location default value 2023-11-28 10:11:53 +01:00
Andras Bacsai
23571ae104 wip 2023-11-27 15:50:22 +01:00
Andras Bacsai
c1710c8f7b moar fixes 2023-11-27 15:25:15 +01:00
Andras Bacsai
d4d2cc71a0 fix: lots of regarding git + docker compose deployments 2023-11-27 14:28:21 +01:00
Andras Bacsai
8d86d53292 fix: new logging for deployment jobs
fix: git based docker compose files
2023-11-27 11:54:55 +01:00
Andras Bacsai
fae97e4dee Fix network connection issues in Server and Service models 2023-11-27 09:58:31 +01:00
Andras Bacsai
8d0c3abf2e Refactor server delete view to display defined
resources
2023-11-27 09:42:23 +01:00
Andras Bacsai
d396f649df fix: show defined resources in server tab, so you will know what you need to delete before you can delete the server. 2023-11-27 09:39:43 +01:00
Andras Bacsai
ec21155c9e Update rules for field validation in StackForm.php 2023-11-24 21:38:39 +01:00
Andras Bacsai
58111f53b9 test wire:ignore 2023-11-24 21:35:01 +01:00
Andras Bacsai
2cbe1e8489 Add SMTP mail transport option to Ghost compose
file
2023-11-24 21:23:48 +01:00
Andras Bacsai
10e5a58b9e Add extra fields for MinIO, Weblate, and Ghost services 2023-11-24 21:04:15 +01:00
Andras Bacsai
6f886e8b6f Update Ghost configuration with mail options 2023-11-24 21:03:59 +01:00
Andras Bacsai
f96a91eb31 wip: compose based apps 2023-11-24 15:48:23 +01:00
Andras Bacsai
65a1961722 Add environment variables for Horizon balance 2023-11-24 10:12:37 +01:00
Andras Bacsai
c5a932ab88 Add environment variables for GitHub
authentication and email configuration
2023-11-24 08:38:49 +01:00
Andras Bacsai
d1e10dacc0 wip 2023-11-23 21:02:30 +01:00
Andras Bacsai
96327af838 Update log-drains.blade.php and add
trigger-with-external-database.yaml and
service-templates.json
2023-11-23 12:44:08 +01:00
Andras Bacsai
1cb6d594d0 Fix service loading issue in project select page 2023-11-23 11:49:49 +01:00
Andras Bacsai
16261fc36e Remove unnecessary code and update services list
loading
2023-11-23 11:40:29 +01:00
Andras Bacsai
cff694b0c4 Update Weblate configuration 2023-11-23 11:35:19 +01:00
Andras Bacsai
97fd56b9e4 Update number of servers in pricing plans 2023-11-23 10:57:11 +01:00
Andras Bacsai
72cfa3e7b0 Update server limits using environment variables 2023-11-23 10:51:57 +01:00
Andras Bacsai
3cf41e1e23 Update server basic value 2023-11-23 10:47:25 +01:00
Andras Bacsai
2a7a63a672 Add trigger.dev service 2023-11-23 09:05:22 +01:00
Andras Bacsai
7fb9e672cf Fix server execution method parameter name 2023-11-22 20:56:25 +01:00
Andras Bacsai
9012f6b953 Fix GitHub App retrieval in webhooks.php 2023-11-22 16:40:49 +01:00
Andras Bacsai
407eba8b76 Fix DockerCleanupJob exception message 2023-11-22 16:39:16 +01:00
Andras Bacsai
68f6ab5796 wip 2023-11-22 15:18:49 +01:00
Andras Bacsai
3dd36a2271 Fix container status handling and notifications 2023-11-22 15:18:37 +01:00
Andras Bacsai
7f69eb3c2e Merge pull request #1479 from coollabsio/next
v4.0.0-beta.146 - quick fix before release
2023-11-22 14:27:04 +01:00
Andras Bacsai
6ccbf911b2 Fix condition for pushing to Docker registry 2023-11-22 14:25:55 +01:00
Andras Bacsai
5c77cec68f Merge pull request #1478 from coollabsio/next
v4.0.0-beta.146
2023-11-22 14:22:10 +01:00
Andras Bacsai
25a0489f7f Fix log drain issue in advanced and service application 2023-11-22 14:21:03 +01:00
Andras Bacsai
5e27b88bef Add new console commands for root email change, root password reset, and service deletion 2023-11-22 13:21:25 +01:00
Andras Bacsai
ec98afe707 Merge pull request #1474 from coollabsio/next
v4.0.0-beta.145
2023-11-22 08:45:00 +01:00
Andras Bacsai
ce26127705 wip: new deployment jobs 2023-11-21 22:17:35 +01:00
Andras Bacsai
ef7fc1b260 Refactor code and update destination component 2023-11-21 15:31:46 +01:00
Andras Bacsai
f58e6766e1 Update Docker Engine version check 2023-11-21 13:06:05 +01:00
Andras Bacsai
4a21102983 fix: server adding process 2023-11-21 12:07:06 +01:00
Andras Bacsai
e78b6758d8 feat: add docker engine support install script to rhel based systems 2023-11-21 11:39:19 +01:00
Andras Bacsai
16eb7f4fb4 Add tracing option to Sentry configuration 2023-11-21 09:01:52 +01:00
Andras Bacsai
4974ce6eda Update release version to 4.0.0-beta.145 2023-11-21 08:41:43 +01:00
Andras Bacsai
6cdba17aca Update token retrieval in reset-password.blade.php 2023-11-20 15:16:23 +01:00
Andras Bacsai
30f8e8f232 fix: handle different label formats in services 2023-11-20 15:01:35 +01:00
Andras Bacsai
608f0b7840 Refactor Docker image name generation and push to
registry
2023-11-20 14:23:11 +01:00
Andras Bacsai
d0366c4054 Update Docker Registry link in general.blade.php 2023-11-20 13:58:31 +01:00
Andras Bacsai
f88e3c5b29 feat: push locally built image to docker registry
ui: fixes here and there
2023-11-20 13:49:10 +01:00
Andras Bacsai
e33fec0e1a Refactor checkbox component and update GPU
settings helper links
2023-11-20 11:37:09 +01:00
Andras Bacsai
912b0a263e feat: gpu enabled containers
feat: move advanced settings to different view
2023-11-20 11:35:31 +01:00
Andras Bacsai
8f963adbd4 fix: only report nonruntime errors 2023-11-20 10:32:06 +01:00
Andras Bacsai
8f2c24d7e9 fix: reset password 2023-11-18 17:50:44 +01:00
Andras Bacsai
9f3dbc3cbb Merge pull request #1469 from coollabsio/next
v4.0.0-beta.144
2023-11-17 21:28:18 +01:00
Andras Bacsai
8a9ee84925 Fix log drain container notification bug 2023-11-17 21:24:22 +01:00
Andras Bacsai
689480003a feat: log drainer container check 2023-11-17 21:16:25 +01:00
Andras Bacsai
3b20eee909 feat: enable/disable log drain by service 2023-11-17 20:08:21 +01:00
Andras Bacsai
e8cadc176b Merge pull request #1468 from coollabsio/next
v4.0.0-beta.143
2023-11-17 15:21:29 +01:00
Andras Bacsai
b0c96e64c9 Fix server unreachable notification count 2023-11-17 15:18:08 +01:00
Andras Bacsai
9ce3b43e09 Add Team model and merge servers with own servers 2023-11-17 15:11:29 +01:00
Andras Bacsai
4c2b3df861 Update server runtime and comments 2023-11-17 14:56:39 +01:00
Andras Bacsai
467471f54a Fix server readiness check in ContainerStatusJob and ServerStatusJob 2023-11-17 14:46:04 +01:00
Andras Bacsai
60171093c5 Update version to 4.0.0-beta.143 2023-11-17 14:43:57 +01:00
141 changed files with 4712 additions and 1646 deletions

View File

@@ -11,19 +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));
}
// 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);
} }
} }
// TODO: make notification for application
// $application->environment->project->team->notify(new StatusChanged($application));
} }
} }
} }

View File

@@ -23,7 +23,7 @@ class StartMariadb
$this->configuration_dir = database_configuration_dir() . '/' . $container_name; $this->configuration_dir = database_configuration_dir() . '/' . $container_name;
$this->commands = [ $this->commands = [
"echo '####### Starting {$database->name}.'", "echo 'Starting {$database->name}.'",
"mkdir -p $this->configuration_dir", "mkdir -p $this->configuration_dir",
]; ];
@@ -69,7 +69,7 @@ class StartMariadb
] ]
] ]
]; ];
if ($this->database->destination->server->isDrainLogActivated()) { if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
$docker_compose['services'][$container_name]['logging'] = [ $docker_compose['services'][$container_name]['logging'] = [
'driver' => 'fluentd', 'driver' => 'fluentd',
'options' => [ 'options' => [
@@ -104,7 +104,7 @@ class StartMariadb
$this->commands[] = "echo 'Pulling {$database->image} image.'"; $this->commands[] = "echo 'Pulling {$database->image} image.'";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
$this->commands[] = "echo '####### {$database->name} started.'"; $this->commands[] = "echo '{$database->name} started.'";
return remote_process($this->commands, $database->destination->server); return remote_process($this->commands, $database->destination->server);
} }

View File

@@ -25,7 +25,7 @@ class StartMongodb
$this->configuration_dir = database_configuration_dir() . '/' . $container_name; $this->configuration_dir = database_configuration_dir() . '/' . $container_name;
$this->commands = [ $this->commands = [
"echo '####### Starting {$database->name}.'", "echo 'Starting {$database->name}.'",
"mkdir -p $this->configuration_dir", "mkdir -p $this->configuration_dir",
]; ];
@@ -76,7 +76,7 @@ class StartMongodb
] ]
] ]
]; ];
if ($this->database->destination->server->isDrainLogActivated()) { if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
$docker_compose['services'][$container_name]['logging'] = [ $docker_compose['services'][$container_name]['logging'] = [
'driver' => 'fluentd', 'driver' => 'fluentd',
'options' => [ 'options' => [
@@ -120,7 +120,7 @@ class StartMongodb
$this->commands[] = "echo 'Pulling {$database->image} image.'"; $this->commands[] = "echo 'Pulling {$database->image} image.'";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
$this->commands[] = "echo '####### {$database->name} started.'"; $this->commands[] = "echo '{$database->name} started.'";
return remote_process($this->commands, $database->destination->server); return remote_process($this->commands, $database->destination->server);
} }

View File

@@ -23,7 +23,7 @@ class StartMysql
$this->configuration_dir = database_configuration_dir() . '/' . $container_name; $this->configuration_dir = database_configuration_dir() . '/' . $container_name;
$this->commands = [ $this->commands = [
"echo '####### Starting {$database->name}.'", "echo 'Starting {$database->name}.'",
"mkdir -p $this->configuration_dir", "mkdir -p $this->configuration_dir",
]; ];
@@ -69,7 +69,7 @@ class StartMysql
] ]
] ]
]; ];
if ($this->database->destination->server->isDrainLogActivated()) { if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
$docker_compose['services'][$container_name]['logging'] = [ $docker_compose['services'][$container_name]['logging'] = [
'driver' => 'fluentd', 'driver' => 'fluentd',
'options' => [ 'options' => [
@@ -104,7 +104,7 @@ class StartMysql
$this->commands[] = "echo 'Pulling {$database->image} image.'"; $this->commands[] = "echo 'Pulling {$database->image} image.'";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
$this->commands[] = "echo '####### {$database->name} started.'"; $this->commands[] = "echo '{$database->name} started.'";
return remote_process($this->commands, $database->destination->server); return remote_process($this->commands, $database->destination->server);
} }

View File

@@ -23,7 +23,7 @@ class StartPostgresql
$this->configuration_dir = database_configuration_dir() . '/' . $container_name; $this->configuration_dir = database_configuration_dir() . '/' . $container_name;
$this->commands = [ $this->commands = [
"echo '####### Starting {$database->name}.'", "echo 'Starting {$database->name}.'",
"mkdir -p $this->configuration_dir", "mkdir -p $this->configuration_dir",
"mkdir -p $this->configuration_dir/docker-entrypoint-initdb.d/" "mkdir -p $this->configuration_dir/docker-entrypoint-initdb.d/"
]; ];
@@ -79,7 +79,8 @@ class StartPostgresql
] ]
] ]
]; ];
if ($this->database->destination->server->isDrainLogActivated()) { if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
ray('Log Drain Enabled');
$docker_compose['services'][$container_name]['logging'] = [ $docker_compose['services'][$container_name]['logging'] = [
'driver' => 'fluentd', 'driver' => 'fluentd',
'options' => [ 'options' => [
@@ -129,7 +130,7 @@ class StartPostgresql
$this->commands[] = "echo 'Pulling {$database->image} image.'"; $this->commands[] = "echo 'Pulling {$database->image} image.'";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
$this->commands[] = "echo '####### {$database->name} started.'"; $this->commands[] = "echo '{$database->name} started.'";
return remote_process($this->commands, $database->destination->server); return remote_process($this->commands, $database->destination->server);
} }

View File

@@ -26,7 +26,7 @@ class StartRedis
$this->configuration_dir = database_configuration_dir() . '/' . $container_name; $this->configuration_dir = database_configuration_dir() . '/' . $container_name;
$this->commands = [ $this->commands = [
"echo '####### Starting {$database->name}.'", "echo 'Starting {$database->name}.'",
"mkdir -p $this->configuration_dir", "mkdir -p $this->configuration_dir",
]; ];
@@ -78,7 +78,7 @@ class StartRedis
] ]
] ]
]; ];
if ($this->database->destination->server->isDrainLogActivated()) { if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
$docker_compose['services'][$container_name]['logging'] = [ $docker_compose['services'][$container_name]['logging'] = [
'driver' => 'fluentd', 'driver' => 'fluentd',
'options' => [ 'options' => [
@@ -114,7 +114,7 @@ class StartRedis
$this->commands[] = "echo 'Pulling {$database->image} image.'"; $this->commands[] = "echo 'Pulling {$database->image} image.'";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
$this->commands[] = "echo '####### {$database->name} started.'"; $this->commands[] = "echo '{$database->name} started.'";
return remote_process($this->commands, $database->destination->server); return remote_process($this->commands, $database->destination->server);
} }
@@ -166,6 +166,5 @@ class StartRedis
$content = $this->database->redis_conf; $content = $this->database->redis_conf;
$content_base64 = base64_encode($content); $content_base64 = base64_encode($content);
$this->commands[] = "echo '{$content_base64}' | base64 -d > $this->configuration_dir/{$filename}"; $this->commands[] = "echo '{$content_base64}' | base64 -d > $this->configuration_dir/{$filename}";
} }
} }

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

@@ -11,6 +11,11 @@ class InstallDocker
use AsAction; use AsAction;
public function handle(Server $server) public function handle(Server $server)
{ {
$supported_os_type = $server->validateOS();
if (!$supported_os_type) {
throw new \Exception('Server OS type is not supported for automated installation. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://coolify.io/docs/servers#install-docker-engine-manually">documentation</a>.');
}
ray('Installing Docker on server: ' . $server->name . ' (' . $server->ip . ')' . ' with OS type: ' . $supported_os_type);
$dockerVersion = '24.0'; $dockerVersion = '24.0';
$config = base64_encode('{ $config = base64_encode('{
"log-driver": "json-file", "log-driver": "json-file",
@@ -27,36 +32,65 @@ class InstallDocker
'server_id' => $server->id, 'server_id' => $server->id,
]); ]);
} }
$command = collect([]);
if (isDev() && $server->id === 0) { if (isDev() && $server->id === 0) {
$command = [ $command = $command->merge([
"echo '####### Installing Prerequisites...'", "echo 'Installing Prerequisites...'",
"sleep 1", "sleep 1",
"echo '####### Installing/updating Docker Engine...'", "echo 'Installing Docker Engine...'",
"echo '####### Configuring Docker Engine (merging existing configuration with the required)...'", "echo 'Configuring Docker Engine (merging existing configuration with the required)...'",
"sleep 4", "sleep 4",
"echo '####### Restarting Docker Engine...'", "echo 'Restarting Docker Engine...'",
"ls -l /tmp" "ls -l /tmp"
]; ]);
} else { } else {
$command = [ if ($supported_os_type->contains('debian')) {
"echo '####### Installing Prerequisites...'", $command = $command->merge([
"command -v jq >/dev/null || apt-get update", "echo 'Installing Prerequisites...'",
"command -v jq >/dev/null || apt install -y jq", "command -v jq >/dev/null || apt-get update -y",
"echo '####### Installing/updating Docker Engine...'", "command -v jq >/dev/null || apt install -y curl wget git jq",
]);
} else if ($supported_os_type->contains('rhel')) {
$command = $command->merge([
"echo 'Installing Prerequisites...'",
"command -v jq >/dev/null || dnf install -y curl wget git jq",
]);
} else if ($supported_os_type->contains('sles')) {
$command = $command->merge([
"echo 'Installing Prerequisites...'",
"command -v jq >/dev/null || zypper update -y",
"command -v jq >/dev/null || zypper install -y curl wget git jq",
]);
} else {
throw new \Exception('Unsupported OS');
}
$command = $command->merge([
"echo 'Installing Docker Engine...'",
"curl https://releases.rancher.com/install-docker/{$dockerVersion}.sh | sh", "curl https://releases.rancher.com/install-docker/{$dockerVersion}.sh | sh",
"echo '####### Configuring Docker Engine (merging existing configuration with the required)...'", "echo 'Configuring Docker Engine (merging existing configuration with the required)...'",
"test -s /etc/docker/daemon.json && cp /etc/docker/daemon.json \"/etc/docker/daemon.json.original-`date +\"%Y%m%d-%H%M%S\"`\" || echo '{$config}' | base64 -d > /etc/docker/daemon.json", "test -s /etc/docker/daemon.json && cp /etc/docker/daemon.json \"/etc/docker/daemon.json.original-`date +\"%Y%m%d-%H%M%S\"`\" || echo '{$config}' | base64 -d > /etc/docker/daemon.json",
"echo '{$config}' | base64 -d > /etc/docker/daemon.json.coolify", "echo '{$config}' | base64 -d > /etc/docker/daemon.json.coolify",
"cat <<< $(jq . /etc/docker/daemon.json.coolify) > /etc/docker/daemon.json.coolify", "cat <<< $(jq . /etc/docker/daemon.json.coolify) > /etc/docker/daemon.json.coolify",
"cat <<< $(jq -s '.[0] * .[1]' /etc/docker/daemon.json /etc/docker/daemon.json.coolify) > /etc/docker/daemon.json", "cat <<< $(jq -s '.[0] * .[1]' /etc/docker/daemon.json /etc/docker/daemon.json.coolify) > /etc/docker/daemon.json",
"echo '####### Restarting Docker Engine...'", "echo 'Restarting Docker Engine...'",
"systemctl enable docker >/dev/null 2>&1 || true",
"systemctl restart docker", "systemctl restart docker",
"echo '####### Creating default Docker network (coolify)...'", ]);
"docker network create --attachable coolify >/dev/null 2>&1 || true", if ($server->isSwarm()) {
"echo '####### Done!'" $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

@@ -8,8 +8,17 @@ use App\Models\Server;
class InstallLogDrain class InstallLogDrain
{ {
use AsAction; use AsAction;
public function handle(Server $server, string $type) public function handle(Server $server)
{ {
if ($server->settings->is_logdrain_newrelic_enabled) {
$type = 'newrelic';
} else if ($server->settings->is_logdrain_highlight_enabled) {
$type = 'highlight';
} else if ($server->settings->is_logdrain_axiom_enabled) {
$type = 'axiom';
} else {
$type = 'none';
}
try { try {
if ($type === 'none') { if ($type === 'none') {
$command = [ $command = [
@@ -127,6 +136,7 @@ services:
- ./parsers.conf:/parsers.conf - ./parsers.conf:/parsers.conf
ports: ports:
- 127.0.0.1:24224:24224 - 127.0.0.1:24224:24224
restart: unless-stopped
"); ");
$readme = base64_encode('# New Relic Log Drain $readme = base64_encode('# New Relic Log Drain
This log drain is based on [Fluent Bit](https://fluentbit.io/) and New Relic Log Forwarder. This log drain is based on [Fluent Bit](https://fluentbit.io/) and New Relic Log Forwarder.

View File

@@ -14,15 +14,15 @@ class StartService
$network = $service->destination->network; $network = $service->destination->network;
$service->saveComposeConfigs(); $service->saveComposeConfigs();
$commands[] = "cd " . $service->workdir(); $commands[] = "cd " . $service->workdir();
$commands[] = "echo '####### Saved configuration files to {$service->workdir()}.'"; $commands[] = "echo 'Saved configuration files to {$service->workdir()}.'";
$commands[] = "echo '####### Creating Docker network.'"; $commands[] = "echo 'Creating Docker network.'";
$commands[] = "docker network create --attachable '{$service->uuid}' >/dev/null || true"; $commands[] = "docker network create --attachable '{$service->uuid}' >/dev/null 2>&1 || true";
$commands[] = "echo '####### Starting service {$service->name} on {$service->server->name}.'"; $commands[] = "echo 'Starting service {$service->name} on {$service->server->name}.'";
$commands[] = "echo '####### Pulling images.'"; $commands[] = "echo 'Pulling images.'";
$commands[] = "docker compose pull"; $commands[] = "docker compose pull";
$commands[] = "echo '####### Starting containers.'"; $commands[] = "echo 'Starting containers.'";
$commands[] = "docker compose up -d --remove-orphans --force-recreate"; $commands[] = "docker compose up -d --remove-orphans --force-recreate";
$commands[] = "docker network connect $service->uuid coolify-proxy || true"; $commands[] = "docker network connect $service->uuid coolify-proxy >/dev/null 2>&1 || true";
$compose = data_get($service,'docker_compose',[]); $compose = data_get($service,'docker_compose',[]);
$serviceNames = data_get(Yaml::parse($compose),'services',[]); $serviceNames = data_get(Yaml::parse($compose),'services',[]);
foreach($serviceNames as $serviceName => $serviceConfig){ foreach($serviceNames as $serviceName => $serviceConfig){

View File

@@ -30,8 +30,9 @@ 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();
@@ -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
@@ -101,14 +104,14 @@ class Init extends Command
ray('Application without environment', $application->name); ray('Application without environment', $application->name);
$application->delete(); $application->delete();
} }
if (!data_get($application, 'destination.server')) {
ray('Application without server', $application->name);
$application->delete();
}
if (!$application->destination()) { if (!$application->destination()) {
ray('Application without destination', $application->name); ray('Application without destination', $application->name);
$application->delete(); $application->delete();
} }
if (!data_get($application, 'destination.server')) {
ray('Application without server', $application->name);
$application->delete();
}
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
echo "Error in application: {$e->getMessage()}\n"; echo "Error in application: {$e->getMessage()}\n";
@@ -120,14 +123,14 @@ class Init extends Command
ray('Postgresql without environment', $postgresql->name); ray('Postgresql without environment', $postgresql->name);
$postgresql->delete(); $postgresql->delete();
} }
if (!data_get($postgresql, 'destination.server')) {
ray('Postgresql without server', $postgresql->name);
$postgresql->delete();
}
if (!$postgresql->destination()) { if (!$postgresql->destination()) {
ray('Postgresql without destination', $postgresql->name); ray('Postgresql without destination', $postgresql->name);
$postgresql->delete(); $postgresql->delete();
} }
if (!data_get($postgresql, 'destination.server')) {
ray('Postgresql without server', $postgresql->name);
$postgresql->delete();
}
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
echo "Error in postgresql: {$e->getMessage()}\n"; echo "Error in postgresql: {$e->getMessage()}\n";
@@ -139,14 +142,14 @@ class Init extends Command
ray('Redis without environment', $redis->name); ray('Redis without environment', $redis->name);
$redis->delete(); $redis->delete();
} }
if (!data_get($redis, 'destination.server')) {
ray('Redis without server', $redis->name);
$redis->delete();
}
if (!$redis->destination()) { if (!$redis->destination()) {
ray('Redis without destination', $redis->name); ray('Redis without destination', $redis->name);
$redis->delete(); $redis->delete();
} }
if (!data_get($redis, 'destination.server')) {
ray('Redis without server', $redis->name);
$redis->delete();
}
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
echo "Error in redis: {$e->getMessage()}\n"; echo "Error in redis: {$e->getMessage()}\n";
@@ -159,14 +162,14 @@ class Init extends Command
ray('Mongodb without environment', $mongodb->name); ray('Mongodb without environment', $mongodb->name);
$mongodb->delete(); $mongodb->delete();
} }
if (!data_get($mongodb, 'destination.server')) {
ray('Mongodb without server', $mongodb->name);
$mongodb->delete();
}
if (!$mongodb->destination()) { if (!$mongodb->destination()) {
ray('Mongodb without destination', $mongodb->name); ray('Mongodb without destination', $mongodb->name);
$mongodb->delete(); $mongodb->delete();
} }
if (!data_get($mongodb, 'destination.server')) {
ray('Mongodb without server', $mongodb->name);
$mongodb->delete();
}
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
echo "Error in mongodb: {$e->getMessage()}\n"; echo "Error in mongodb: {$e->getMessage()}\n";
@@ -179,14 +182,14 @@ class Init extends Command
ray('Mysql without environment', $mysql->name); ray('Mysql without environment', $mysql->name);
$mysql->delete(); $mysql->delete();
} }
if (!data_get($mysql, 'destination.server')) {
ray('Mysql without server', $mysql->name);
$mysql->delete();
}
if (!$mysql->destination()) { if (!$mysql->destination()) {
ray('Mysql without destination', $mysql->name); ray('Mysql without destination', $mysql->name);
$mysql->delete(); $mysql->delete();
} }
if (!data_get($mysql, 'destination.server')) {
ray('Mysql without server', $mysql->name);
$mysql->delete();
}
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
echo "Error in mysql: {$e->getMessage()}\n"; echo "Error in mysql: {$e->getMessage()}\n";
@@ -199,14 +202,14 @@ class Init extends Command
ray('Mariadb without environment', $mariadb->name); ray('Mariadb without environment', $mariadb->name);
$mariadb->delete(); $mariadb->delete();
} }
if (!data_get($mariadb, 'destination.server')) {
ray('Mariadb without server', $mariadb->name);
$mariadb->delete();
}
if (!$mariadb->destination()) { if (!$mariadb->destination()) {
ray('Mariadb without destination', $mariadb->name); ray('Mariadb without destination', $mariadb->name);
$mariadb->delete(); $mariadb->delete();
} }
if (!data_get($mariadb, 'destination.server')) {
ray('Mariadb without server', $mariadb->name);
$mariadb->delete();
}
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
echo "Error in mariadb: {$e->getMessage()}\n"; echo "Error in mariadb: {$e->getMessage()}\n";
@@ -219,14 +222,14 @@ class Init extends Command
ray('Service without environment', $service->name); ray('Service without environment', $service->name);
$service->delete(); $service->delete();
} }
if (!data_get($service, 'server')) {
ray('Service without server', $service->name);
$service->delete();
}
if (!$service->destination()) { if (!$service->destination()) {
ray('Service without destination', $service->name); ray('Service without destination', $service->name);
$service->delete(); $service->delete();
} }
if (!data_get($service, 'server')) {
ray('Service without server', $service->name);
$service->delete();
}
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
echo "Error in service: {$e->getMessage()}\n"; echo "Error in service: {$e->getMessage()}\n";

View File

@@ -0,0 +1,41 @@
<?php
namespace App\Console\Commands;
use App\Models\User;
use Illuminate\Console\Command;
class RootChangeEmail extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'root:change-email';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Change Root Email';
/**
* Execute the console command.
*/
public function handle()
{
//
$this->info('You are about to change the root user\'s email.');
$email = $this->ask('Give me a new email for root user');
$this->info('Updating root email...');
try {
User::find(0)->update(['email' => $email]);
$this->info('Root user\'s email updated successfully.');
} catch (\Exception $e) {
$this->error('Failed to update root user\'s email.');
return;
}
}
}

View File

@@ -8,14 +8,14 @@ use Illuminate\Support\Facades\Hash;
use function Laravel\Prompts\password; use function Laravel\Prompts\password;
class UsersResetRoot extends Command class RootResetPassword extends Command
{ {
/** /**
* The name and signature of the console command. * The name and signature of the console command.
* *
* @var string * @var string
*/ */
protected $signature = 'users:reset-root'; protected $signature = 'root:reset-password';
/** /**
* The console command description. * The console command description.

View File

@@ -12,21 +12,21 @@ use function Laravel\Prompts\confirm;
use function Laravel\Prompts\multiselect; use function Laravel\Prompts\multiselect;
use function Laravel\Prompts\select; use function Laravel\Prompts\select;
class ResourcesDelete extends Command class ServicesDelete extends Command
{ {
/** /**
* The name and signature of the console command. * The name and signature of the console command.
* *
* @var string * @var string
*/ */
protected $signature = 'resources:delete'; protected $signature = 'services:delete';
/** /**
* The console command description. * The console command description.
* *
* @var string * @var string
*/ */
protected $description = 'Delete a resource from the database'; protected $description = 'Delete a service from the database';
/** /**
* Execute the console command. * Execute the console command.
@@ -34,7 +34,7 @@ class ResourcesDelete extends Command
public function handle() public function handle()
{ {
$resource = select( $resource = select(
'What resource do you want to delete?', 'What service do you want to delete?',
['Application', 'Database', 'Service', 'Server'], ['Application', 'Database', 'Service', 'Server'],
); );
if ($resource === 'Application') { if ($resource === 'Application') {

View File

@@ -26,7 +26,7 @@ class ServicesGenerate extends Command
*/ */
public function handle() public function handle()
{ {
ray()->clearAll(); // ray()->clearAll();
$files = array_diff(scandir(base_path('templates/compose')), ['.', '..']); $files = array_diff(scandir(base_path('templates/compose')), ['.', '..']);
$files = array_filter($files, function ($file) { $files = array_filter($files, function ($file) {
return strpos($file, '.yaml') !== false; return strpos($file, '.yaml') !== false;

View File

@@ -71,6 +71,15 @@ class SyncBunny extends Command
]); ]);
}); });
try { try {
if (!$only_template && !$only_version) {
$this->info('About to sync files (docker-compose.prod.yaml, upgrade.sh, install.sh, etc) to BunnyCDN.');
}
if ($only_template) {
$this->info('About to sync service-templates.json to BunnyCDN.');
}
if ($only_version) {
$this->info('About to sync versions.json to BunnyCDN.');
}
$confirmed = confirm('Are you sure you want to sync?'); $confirmed = confirm('Are you sure you want to sync?');
if (!$confirmed) { if (!$confirmed) {
return; return;

View File

@@ -2,6 +2,7 @@
namespace App\Console; namespace App\Console;
use App\Jobs\CheckLogDrainContainerJob;
use App\Jobs\CleanupInstanceStuffsJob; use App\Jobs\CleanupInstanceStuffsJob;
use App\Jobs\DatabaseBackupJob; use App\Jobs\DatabaseBackupJob;
use App\Jobs\InstanceAutoUpdateJob; use App\Jobs\InstanceAutoUpdateJob;
@@ -11,6 +12,7 @@ use App\Jobs\ServerStatusJob;
use App\Models\InstanceSettings; use App\Models\InstanceSettings;
use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledDatabaseBackup;
use App\Models\Server; use App\Models\Server;
use App\Models\Team;
use Illuminate\Console\Scheduling\Schedule; use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel; use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
@@ -52,12 +54,17 @@ class Kernel extends ConsoleKernel
{ {
if (isCloud()) { if (isCloud()) {
$servers = Server::all()->whereNotNull('team.subscription')->where('team.subscription.stripe_trial_already_ended', false)->where('ip', '!=', '1.2.3.4'); $servers = Server::all()->whereNotNull('team.subscription')->where('team.subscription.stripe_trial_already_ended', false)->where('ip', '!=', '1.2.3.4');
$own = Team::find(0)->servers;
$servers = $servers->merge($own);
} else { } else {
$servers = Server::all()->where('ip', '!=', '1.2.3.4'); $servers = Server::all()->where('ip', '!=', '1.2.3.4');
} }
foreach ($servers as $server) { foreach ($servers as $server) {
$schedule->job(new ServerStatusJob($server))->everyTenMinutes()->onOneServer(); $schedule->job(new ServerStatusJob($server))->everyTenMinutes()->onOneServer();
$schedule->job(new ContainerStatusJob($server))->everyMinute()->onOneServer(); $schedule->job(new ContainerStatusJob($server))->everyMinute()->onOneServer();
if ($server->isLogDrainEnabled()) {
$schedule->job(new CheckLogDrainContainerJob($server))->everyMinute()->onOneServer();
}
} }
} }
private function instance_auto_update($schedule) private function instance_auto_update($schedule)

View File

@@ -6,6 +6,7 @@ use App\Models\InstanceSettings;
use App\Models\User; use App\Models\User;
use Illuminate\Auth\AuthenticationException; use Illuminate\Auth\AuthenticationException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use RuntimeException;
use Sentry\Laravel\Integration; use Sentry\Laravel\Integration;
use Sentry\State\Scope; use Sentry\State\Scope;
use Throwable; use Throwable;
@@ -55,7 +56,9 @@ class Handler extends ExceptionHandler
{ {
$this->reportable(function (Throwable $e) { $this->reportable(function (Throwable $e) {
if (isDev()) { if (isDev()) {
ray($e); // return;
}
if ($e instanceof RuntimeException) {
return; return;
} }
$this->settings = InstanceSettings::get(); $this->settings = InstanceSettings::get();
@@ -74,6 +77,7 @@ class Handler extends ExceptionHandler
); );
} }
); );
ray('reporting to sentry');
Integration::captureUnhandledException($e); Integration::captureUnhandledException($e);
}); });
} }

View File

@@ -10,23 +10,6 @@ class ApplicationController extends Controller
{ {
use AuthorizesRequests, ValidatesRequests; use AuthorizesRequests, ValidatesRequests;
public function configuration()
{
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (!$project) {
return redirect()->route('dashboard');
}
$environment = $project->load(['environments'])->environments->where('name', request()->route('environment_name'))->first()->load(['applications']);
if (!$environment) {
return redirect()->route('dashboard');
}
$application = $environment->applications->where('uuid', request()->route('application_uuid'))->first();
if (!$application) {
return redirect()->route('dashboard');
}
return view('project.application.configuration', ['application' => $application]);
}
public function deployments() public function deployments()
{ {
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first(); $project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();

View File

@@ -67,7 +67,7 @@ class ProjectController extends Controller
$database = create_standalone_mongodb($environment->id, $destination_uuid); $database = create_standalone_mongodb($environment->id, $destination_uuid);
} else if ($type->value() === 'mysql') { } else if ($type->value() === 'mysql') {
$database = create_standalone_mysql($environment->id, $destination_uuid); $database = create_standalone_mysql($environment->id, $destination_uuid);
}else if ($type->value() === 'mariadb') { } else if ($type->value() === 'mariadb') {
$database = create_standalone_mariadb($environment->id, $destination_uuid); $database = create_standalone_mariadb($environment->id, $destination_uuid);
} }
return redirect()->route('project.database.configuration', [ return redirect()->route('project.database.configuration', [
@@ -104,27 +104,7 @@ class ProjectController extends Controller
$generatedValue = $value; $generatedValue = $value;
if ($value->contains('SERVICE_')) { if ($value->contains('SERVICE_')) {
$command = $value->after('SERVICE_')->beforeLast('_'); $command = $value->after('SERVICE_')->beforeLast('_');
// TODO: make it shared with Service.php $generatedValue = generateEnvValue($command->value());
switch ($command->value()) {
case 'PASSWORD':
$generatedValue = Str::password(symbols: false);
break;
case 'PASSWORD_64':
$generatedValue = Str::password(length: 64, symbols: false);
break;
case 'BASE64_64':
$generatedValue = Str::random(64);
break;
case 'BASE64_128':
$generatedValue = Str::random(128);
break;
case 'BASE64':
$generatedValue = Str::random(32);
break;
case 'USER':
$generatedValue = Str::random(16);
break;
}
} }
EnvironmentVariable::create([ EnvironmentVariable::create([
'key' => $key, 'key' => $key,
@@ -137,7 +117,7 @@ class ProjectController extends Controller
} }
$service->parse(isNew: true); $service->parse(isNew: true);
return redirect()->route('project.service', [ return redirect()->route('project.service.configuration', [
'service_uuid' => $service->uuid, 'service_uuid' => $service->uuid,
'environment_name' => $environment->name, 'environment_name' => $environment->name,
'project_uuid' => $project->uuid, 'project_uuid' => $project->uuid,

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,13 +183,14 @@ 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()
{ {
try { try {
$customErrorMessage = "Server is not reachable:";
config()->set('coolify.mux_enabled', false); config()->set('coolify.mux_enabled', false);
instant_remote_process(['uptime'], $this->createdServer, true); instant_remote_process(['uptime'], $this->createdServer, true);
@@ -198,7 +200,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
]); ]);
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->serverReachable = false; $this->serverReachable = false;
return handleError(error: $e, customErrorMessage: $customErrorMessage, livewire: $this); return handleError(error: $e, livewire: $this);
} }
try { try {
@@ -206,7 +208,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
$dockerVersion = checkMinimumDockerEngineVersion($dockerVersion); $dockerVersion = checkMinimumDockerEngineVersion($dockerVersion);
if (is_null($dockerVersion)) { if (is_null($dockerVersion)) {
$this->currentState = 'install-docker'; $this->currentState = 'install-docker';
throw new \Exception('Docker version is not supported or not installed.'); throw new \Exception('Docker not found or old version is installed.');
} }
$this->createdServer->settings()->update([ $this->createdServer->settings()->update([
'is_usable' => true, 'is_usable' => true,
@@ -214,14 +216,20 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
$this->getProxyType(); $this->getProxyType();
} catch (\Throwable $e) { } catch (\Throwable $e) {
// $this->dockerInstallationStarted = false; // $this->dockerInstallationStarted = false;
return handleError(error: $e, customErrorMessage: $customErrorMessage, livewire: $this); return handleError(error: $e, livewire: $this);
} }
} }
public function installDocker() public function installDocker()
{ {
$this->dockerInstallationStarted = true; try {
$activity = InstallDocker::run($this->createdServer); $this->dockerInstallationStarted = true;
$this->emit('newMonitorActivity', $activity->id); $activity = InstallDocker::run($this->createdServer);
$this->emit('installDocker');
$this->emit('newMonitorActivity', $activity->id);
} catch (\Throwable $e) {
$this->dockerInstallationStarted = false;
return handleError(error: $e, livewire: $this);
}
} }
public function dockerInstalledOrSkipped() public function dockerInstalledOrSkipped()
{ {

View File

@@ -3,7 +3,6 @@
namespace App\Http\Livewire; namespace App\Http\Livewire;
use App\Models\Project; use App\Models\Project;
use App\Models\S3Storage;
use App\Models\Server; use App\Models\Server;
use Livewire\Component; use Livewire\Component;

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Http\Livewire\Project\Application;
use App\Models\Application;
use Livewire\Component;
class Advanced extends Component
{
public Application $application;
protected $rules = [
'application.settings.is_git_submodules_enabled' => 'boolean|required',
'application.settings.is_git_lfs_enabled' => 'boolean|required',
'application.settings.is_preview_deployments_enabled' => 'boolean|required',
'application.settings.is_auto_deploy_enabled' => 'boolean|required',
'application.settings.is_force_https_enabled' => 'boolean|required',
'application.settings.is_log_drain_enabled' => 'boolean|required',
'application.settings.is_gpu_enabled' => 'boolean|required',
'application.settings.gpu_driver' => 'string|required',
'application.settings.gpu_count' => 'string|required',
'application.settings.gpu_device_ids' => 'string|required',
'application.settings.gpu_options' => 'string|required',
];
public function instantSave()
{
if ($this->application->isLogDrainEnabled()) {
if (!$this->application->destination->server->isLogDrainEnabled()) {
$this->application->settings->is_log_drain_enabled = false;
$this->emit('error', 'Log drain is not enabled on this server.');
return;
}
}
if ($this->application->settings->is_force_https_enabled) {
$this->emit('resetDefaultLabels', false);
}
$this->application->settings->save();
$this->emit('success', 'Settings saved.');
}
public function submit() {
if ($this->application->settings->gpu_count && $this->application->settings->gpu_device_ids) {
$this->emit('error', 'You cannot set both GPU count and GPU device IDs.');
$this->application->settings->gpu_count = null;
$this->application->settings->gpu_device_ids = null;
$this->application->settings->save();
return;
}
$this->application->settings->save();
$this->emit('success', 'Settings saved.');
}
public function render()
{
return view('livewire.project.application.advanced');
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Http\Livewire\Project\Application;
use App\Models\Application;
use App\Models\Server;
use App\Models\StandaloneDocker;
use Livewire\Component;
class Configuration extends Component
{
public Application $application;
public $servers;
public function mount()
{
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (!$project) {
return redirect()->route('dashboard');
}
$environment = $project->load(['environments'])->environments->where('name', request()->route('environment_name'))->first()->load(['applications']);
if (!$environment) {
return redirect()->route('dashboard');
}
$application = $environment->applications->where('uuid', request()->route('application_uuid'))->first();
if (!$application) {
return redirect()->route('dashboard');
}
$this->application = $application;
$mainServer = $application->destination->server;
$servers = Server::ownedByCurrentTeam()->get();
$this->servers = $servers->filter(function ($server) use ($mainServer) {
return $server->id != $mainServer->id;
});
}
public function render()
{
return view('livewire.project.application.configuration');
}
}

View File

@@ -6,6 +6,7 @@ use App\Models\Application;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Livewire\Component; use Livewire\Component;
use Visus\Cuid2\Cuid2;
class General extends Component class General extends Component
{ {
@@ -25,15 +26,17 @@ class General extends Component
public bool $labelsChanged = false; public bool $labelsChanged = false;
public bool $isConfigurationChanged = false; public bool $isConfigurationChanged = false;
public ?string $initialDockerComposeLocation = null;
public ?string $initialDockerComposePrLocation = null;
public bool $is_static; public bool $is_static;
public bool $is_git_submodules_enabled;
public bool $is_git_lfs_enabled;
public bool $is_debug_enabled;
public bool $is_preview_deployments_enabled;
public bool $is_auto_deploy_enabled;
public bool $is_force_https_enabled;
public $parsedServices = [];
public $parsedServiceDomains = [];
protected $listeners = [
'resetDefaultLabels'
];
protected $rules = [ protected $rules = [
'application.name' => 'required', 'application.name' => 'required',
'application.description' => 'nullable', 'application.description' => 'nullable',
@@ -54,8 +57,15 @@ class General extends Component
'application.docker_registry_image_name' => 'nullable', 'application.docker_registry_image_name' => 'nullable',
'application.docker_registry_image_tag' => 'nullable', 'application.docker_registry_image_tag' => 'nullable',
'application.dockerfile_location' => 'nullable', 'application.dockerfile_location' => 'nullable',
'application.docker_compose_location' => 'nullable',
'application.docker_compose_pr_location' => 'nullable',
'application.docker_compose' => 'nullable',
'application.docker_compose_pr' => 'nullable',
'application.docker_compose_raw' => 'nullable',
'application.docker_compose_pr_raw' => 'nullable',
'application.custom_labels' => 'nullable', 'application.custom_labels' => 'nullable',
'application.dockerfile_target_build' => 'nullable', 'application.dockerfile_target_build' => 'nullable',
'application.settings.is_static' => 'boolean|required',
]; ];
protected $validationAttributes = [ protected $validationAttributes = [
'application.name' => 'name', 'application.name' => 'name',
@@ -77,12 +87,26 @@ class General extends Component
'application.docker_registry_image_name' => 'Docker registry image name', 'application.docker_registry_image_name' => 'Docker registry image name',
'application.docker_registry_image_tag' => 'Docker registry image tag', 'application.docker_registry_image_tag' => 'Docker registry image tag',
'application.dockerfile_location' => 'Dockerfile location', 'application.dockerfile_location' => 'Dockerfile location',
'application.docker_compose_location' => 'Docker compose location',
'application.docker_compose_pr_location' => 'Docker compose location',
'application.docker_compose' => 'Docker compose',
'application.docker_compose_pr' => 'Docker compose',
'application.docker_compose_raw' => 'Docker compose raw',
'application.docker_compose_pr_raw' => 'Docker compose raw',
'application.custom_labels' => 'Custom labels', 'application.custom_labels' => 'Custom labels',
'application.dockerfile_target_build' => 'Dockerfile target build', 'application.dockerfile_target_build' => 'Dockerfile target build',
'application.settings.is_static' => 'Is static',
]; ];
public function mount() public function mount()
{ {
try {
$this->parsedServices = $this->application->parseCompose();
} catch (\Throwable $e) {
$this->emit('error', $e->getMessage());
}
$this->parsedServiceDomains = $this->application->docker_compose_domains ? json_decode($this->application->docker_compose_domains, true) : [];
$this->ports_exposes = $this->application->ports_exposes; $this->ports_exposes = $this->application->ports_exposes;
if (str($this->application->status)->startsWith('running') && is_null($this->application->config_hash)) { if (str($this->application->status)->startsWith('running') && is_null($this->application->config_hash)) {
$this->application->isConfigurationChanged(true); $this->application->isConfigurationChanged(true);
@@ -93,23 +117,52 @@ class General extends Component
} else { } else {
$this->customLabels = str($this->application->custom_labels)->replace(',', "\n"); $this->customLabels = str($this->application->custom_labels)->replace(',', "\n");
} }
if (data_get($this->application, 'settings')) { $this->initialDockerComposeLocation = $this->application->docker_compose_location;
$this->is_static = $this->application->settings->is_static;
$this->is_git_submodules_enabled = $this->application->settings->is_git_submodules_enabled;
$this->is_git_lfs_enabled = $this->application->settings->is_git_lfs_enabled;
$this->is_debug_enabled = $this->application->settings->is_debug_enabled;
$this->is_preview_deployments_enabled = $this->application->settings->is_preview_deployments_enabled;
$this->is_auto_deploy_enabled = $this->application->settings->is_auto_deploy_enabled;
$this->is_force_https_enabled = $this->application->settings->is_force_https_enabled;
}
$this->checkLabelUpdates(); $this->checkLabelUpdates();
} }
public function instantSave()
{
$this->application->settings->save();
$this->emit('success', 'Settings saved.');
}
public function loadComposeFile($isInit = false)
{
try {
if ($isInit && $this->application->docker_compose_raw) {
return;
}
['parsedServices' => $this->parsedServices, 'initialDockerComposeLocation' => $this->initialDockerComposeLocation, 'initialDockerComposePrLocation' => $this->initialDockerComposePrLocation] = $this->application->loadComposeFile($isInit);
$this->emit('success', 'Docker compose file loaded.');
} catch (\Throwable $e) {
$this->application->docker_compose_location = $this->initialDockerComposeLocation;
$this->application->docker_compose_pr_location = $this->initialDockerComposePrLocation;
$this->application->save();
return handleError($e, $this);
}
}
public function generateDomain(string $serviceName)
{
$domain = $this->parsedServiceDomains[$serviceName]['domain'] ?? null;
if (!$domain) {
$uuid = new Cuid2(7);
$domain = generateFqdn($this->application->destination->server, $uuid);
$this->parsedServiceDomains[$serviceName]['domain'] = $domain;
$this->application->docker_compose_domains = json_encode($this->parsedServiceDomains);
$this->application->save();
$this->emit('success', 'Domain generated.');
}
return $domain;
}
public function updatedApplicationBuildPack() public function updatedApplicationBuildPack()
{ {
if ($this->application->build_pack !== 'nixpacks') { if ($this->application->build_pack !== 'nixpacks') {
$this->application->settings->is_static = $this->is_static = false; $this->application->settings->is_static = $this->is_static = false;
$this->application->settings->save(); $this->application->settings->save();
} }
if ($this->application->build_pack === 'dockercompose') {
$this->application->fqdn = null;
$this->application->settings->save();
}
$this->submit(); $this->submit();
} }
public function checkLabelUpdates() public function checkLabelUpdates()
@@ -120,32 +173,6 @@ class General extends Component
$this->labelsChanged = false; $this->labelsChanged = false;
} }
} }
public function instantSave()
{
// @TODO: find another way - if possible
$force_https = $this->application->settings->is_force_https_enabled;
$this->application->settings->is_static = $this->is_static;
if ($this->is_static) {
$this->application->ports_exposes = 80;
} else {
$this->application->ports_exposes = 3000;
}
$this->application->settings->is_git_submodules_enabled = $this->is_git_submodules_enabled;
$this->application->settings->is_git_lfs_enabled = $this->is_git_lfs_enabled;
$this->application->settings->is_debug_enabled = $this->is_debug_enabled;
$this->application->settings->is_preview_deployments_enabled = $this->is_preview_deployments_enabled;
$this->application->settings->is_auto_deploy_enabled = $this->is_auto_deploy_enabled;
$this->application->settings->is_force_https_enabled = $this->is_force_https_enabled;
$this->application->settings->save();
$this->application->save();
$this->application->refresh();
$this->emit('success', 'Application settings updated!');
$this->checkLabelUpdates();
$this->isConfigurationChanged = $this->application->isConfigurationChanged();
if ($force_https !== $this->is_force_https_enabled) {
$this->resetDefaultLabels(false);
}
}
public function getWildcardDomain() public function getWildcardDomain()
{ {
@@ -172,6 +199,9 @@ class General extends Component
public function submit($showToaster = true) public function submit($showToaster = true)
{ {
try { try {
if ($this->application->build_pack === 'dockercompose' && ($this->initialDockerComposeLocation !== $this->application->docker_compose_location || $this->initialDockerComposePrLocation !== $this->application->docker_compose_pr_location)) {
$this->loadComposeFile();
}
$this->validate(); $this->validate();
if ($this->ports_exposes !== $this->application->ports_exposes) { if ($this->ports_exposes !== $this->application->ports_exposes) {
$this->resetDefaultLabels(false); $this->resetDefaultLabels(false);
@@ -204,6 +234,10 @@ class General extends Component
$this->customLabels = str($this->customLabels)->replace(',', "\n"); $this->customLabels = str($this->customLabels)->replace(',', "\n");
} }
$this->application->custom_labels = $this->customLabels->explode("\n")->implode(','); $this->application->custom_labels = $this->customLabels->explode("\n")->implode(',');
if ($this->application->build_pack === 'dockercompose') {
$this->application->docker_compose_domains = json_encode($this->parsedServiceDomains);
$this->parsedServices = $this->application->parseCompose();
}
$this->application->save(); $this->application->save();
$showToaster && $this->emit('success', 'Application settings updated!'); $showToaster && $this->emit('success', 'Application settings updated!');
} catch (\Throwable $e) { } catch (\Throwable $e) {

View File

@@ -41,6 +41,10 @@ class Heading extends Component
public function deploy(bool $force_rebuild = false) public function deploy(bool $force_rebuild = false)
{ {
if ($this->application->build_pack === 'dockercompose' && is_null($this->application->docker_compose_raw)) {
$this->emit('error', 'Please load a Compose file first.');
return;
}
$this->setDeploymentUuid(); $this->setDeploymentUuid();
queue_application_deployment( queue_application_deployment(
application_id: $this->application->id, application_id: $this->application->id,
@@ -68,7 +72,8 @@ class Heading extends Component
$this->application->save(); $this->application->save();
$this->application->refresh(); $this->application->refresh();
} }
public function restart() { public function restart()
{
$this->setDeploymentUuid(); $this->setDeploymentUuid();
queue_application_deployment( queue_application_deployment(
application_id: $this->application->id, application_id: $this->application->id,

View File

@@ -72,10 +72,12 @@ class Previews extends Component
public function stop(int $pull_request_id) public function stop(int $pull_request_id)
{ {
try { try {
$container_name = generateApplicationContainerName($this->application, $pull_request_id); $containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id, $pull_request_id);
foreach ($containers as $container) {
instant_remote_process(["docker rm -f $container_name"], $this->application->destination->server, throwError: false); $name = str_replace('/', '', $container['Names']);
ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->delete(); instant_remote_process(["docker rm -f $name"], $this->application->destination->server, throwError: false);
}
ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first()->delete();
$this->application->refresh(); $this->application->refresh();
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);

View File

@@ -38,10 +38,10 @@ class Rollback extends Component
]); ]);
} }
public function loadImages() public function loadImages($showToast = false)
{ {
try { try {
$image = $this->application->uuid; $image = $this->application->docker_registry_image_name ?? $this->application->uuid;
if ($this->application->destination->server->isFunctional()) { if ($this->application->destination->server->isFunctional()) {
$output = instant_remote_process([ $output = instant_remote_process([
"docker inspect --format='{{.Config.Image}}' {$this->application->uuid}", "docker inspect --format='{{.Config.Image}}' {$this->application->uuid}",
@@ -66,6 +66,7 @@ class Rollback extends Component
]; ];
})->toArray(); })->toArray();
} }
$showToast && $this->emit('success', 'Images loaded.');
return []; return [];
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);

View File

@@ -28,6 +28,7 @@ class General extends Component
'database.ports_mappings' => 'nullable', 'database.ports_mappings' => 'nullable',
'database.is_public' => 'nullable|boolean', 'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer', 'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
]; ];
protected $validationAttributes = [ protected $validationAttributes = [
'database.name' => 'Name', 'database.name' => 'Name',
@@ -50,6 +51,20 @@ class General extends Component
$this->db_url_public = $this->database->getDbUrl(); $this->db_url_public = $this->database->getDbUrl();
} }
} }
public function instantSaveAdvanced() {
try {
if (!$this->database->destination->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->emit('error', 'Log drain is not enabled on the server. Please enable it first.');
return;
}
$this->database->save();
$this->emit('success', 'Database updated successfully.');
$this->emit('success', 'You need to restart the service for the changes to take effect.');
} catch (Exception $e) {
return handleError($e, $this);
}
}
public function submit() public function submit()
{ {
try { try {

View File

@@ -27,6 +27,7 @@ class General extends Component
'database.ports_mappings' => 'nullable', 'database.ports_mappings' => 'nullable',
'database.is_public' => 'nullable|boolean', 'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer', 'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
]; ];
protected $validationAttributes = [ protected $validationAttributes = [
'database.name' => 'Name', 'database.name' => 'Name',
@@ -48,7 +49,21 @@ class General extends Component
$this->db_url_public = $this->database->getDbUrl(); $this->db_url_public = $this->database->getDbUrl();
} }
} }
public function instantSaveAdvanced()
{
try {
if (!$this->database->destination->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->emit('error', 'Log drain is not enabled on the server. Please enable it first.');
return;
}
$this->database->save();
$this->emit('success', 'Database updated successfully.');
$this->emit('success', 'You need to restart the service for the changes to take effect.');
} catch (Exception $e) {
return handleError($e, $this);
}
}
public function submit() public function submit()
{ {
try { try {

View File

@@ -28,6 +28,7 @@ class General extends Component
'database.ports_mappings' => 'nullable', 'database.ports_mappings' => 'nullable',
'database.is_public' => 'nullable|boolean', 'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer', 'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
]; ];
protected $validationAttributes = [ protected $validationAttributes = [
'database.name' => 'Name', 'database.name' => 'Name',
@@ -50,6 +51,21 @@ class General extends Component
$this->db_url_public = $this->database->getDbUrl(); $this->db_url_public = $this->database->getDbUrl();
} }
} }
public function instantSaveAdvanced()
{
try {
if (!$this->database->destination->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->emit('error', 'Log drain is not enabled on the server. Please enable it first.');
return;
}
$this->database->save();
$this->emit('success', 'Database updated successfully.');
$this->emit('success', 'You need to restart the service for the changes to take effect.');
} catch (Exception $e) {
return handleError($e, $this);
}
}
public function submit() public function submit()
{ {
try { try {

View File

@@ -34,6 +34,7 @@ class General extends Component
'database.ports_mappings' => 'nullable', 'database.ports_mappings' => 'nullable',
'database.is_public' => 'nullable|boolean', 'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer', 'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
]; ];
protected $validationAttributes = [ protected $validationAttributes = [
'database.name' => 'Name', 'database.name' => 'Name',
@@ -57,6 +58,20 @@ class General extends Component
$this->db_url_public = $this->database->getDbUrl(); $this->db_url_public = $this->database->getDbUrl();
} }
} }
public function instantSaveAdvanced() {
try {
if (!$this->database->destination->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->emit('error', 'Log drain is not enabled on the server. Please enable it first.');
return;
}
$this->database->save();
$this->emit('success', 'Database updated successfully.');
$this->emit('success', 'You need to restart the service for the changes to take effect.');
} catch (Exception $e) {
return handleError($e, $this);
}
}
public function instantSave() public function instantSave()
{ {
try { try {

View File

@@ -25,6 +25,7 @@ class General extends Component
'database.ports_mappings' => 'nullable', 'database.ports_mappings' => 'nullable',
'database.is_public' => 'nullable|boolean', 'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer', 'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
]; ];
protected $validationAttributes = [ protected $validationAttributes = [
'database.name' => 'Name', 'database.name' => 'Name',
@@ -43,6 +44,20 @@ class General extends Component
$this->db_url_public = $this->database->getDbUrl(); $this->db_url_public = $this->database->getDbUrl();
} }
} }
public function instantSaveAdvanced() {
try {
if (!$this->database->destination->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->emit('error', 'Log drain is not enabled on the server. Please enable it first.');
return;
}
$this->database->save();
$this->emit('success', 'Database updated successfully.');
$this->emit('success', 'You need to restart the service for the changes to take effect.');
} catch (Exception $e) {
return handleError($e, $this);
}
}
public function submit() public function submit()
{ {
try { try {

View File

@@ -129,7 +129,7 @@ class DockerCompose extends Component
$service->parse(isNew: true); $service->parse(isNew: true);
return redirect()->route('project.service', [ return redirect()->route('project.service.configuration', [
'service_uuid' => $service->uuid, 'service_uuid' => $service->uuid,
'environment_name' => $environment->name, 'environment_name' => $environment->name,
'project_uuid' => $project->uuid, 'project_uuid' => $project->uuid,

View File

@@ -47,7 +47,7 @@ class Select extends Component
} }
public function render() public function render()
{ {
$this->loadServices(); if ($this->search) $this->loadServices();
return view('livewire.project.new.select'); return view('livewire.project.new.select');
} }
@@ -69,10 +69,10 @@ class Select extends Component
// } // }
// } // }
public function loadServices(bool $force = false) public function loadServices()
{ {
try { try {
if (count($this->allServices) > 0 && !$force) { if (count($this->allServices) > 0) {
if (!$this->search) { if (!$this->search) {
$this->services = $this->allServices; $this->services = $this->allServices;
return; return;

View File

@@ -16,6 +16,7 @@ class Application extends Component
'application.image' => 'required', 'application.image' => 'required',
'application.exclude_from_status' => 'required|boolean', 'application.exclude_from_status' => 'required|boolean',
'application.required_fqdn' => 'required|boolean', 'application.required_fqdn' => 'required|boolean',
'application.is_log_drain_enabled' => 'nullable|boolean',
]; ];
public function render() public function render()
{ {
@@ -25,13 +26,22 @@ class Application extends Component
{ {
$this->submit(); $this->submit();
} }
public function instantSaveAdvanced()
{
if (!$this->application->service->destination->server->isLogDrainEnabled()) {
$this->application->is_log_drain_enabled = false;
$this->emit('error', 'Log drain is not enabled on the server. Please enable it first.');
return;
}
$this->application->save();
$this->emit('success', 'You need to restart the service for the changes to take effect.');
}
public function delete() public function delete()
{ {
try { try {
$this->application->delete(); $this->application->delete();
$this->emit('success', 'Application deleted successfully.'); $this->emit('success', 'Application deleted successfully.');
return redirect()->route('project.service', $this->parameters); return redirect()->route('project.service.configuration', $this->parameters);
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View File

@@ -21,18 +21,31 @@ class Database extends Component
'database.exclude_from_status' => 'required|boolean', 'database.exclude_from_status' => 'required|boolean',
'database.public_port' => 'nullable|integer', 'database.public_port' => 'nullable|integer',
'database.is_public' => 'required|boolean', 'database.is_public' => 'required|boolean',
'database.is_log_drain_enabled' => 'required|boolean',
]; ];
public function render() public function render()
{ {
return view('livewire.project.service.database'); return view('livewire.project.service.database');
} }
public function mount() { public function mount()
{
if ($this->database->is_public) { if ($this->database->is_public) {
$this->db_url_public = $this->database->getServiceDatabaseUrl(); $this->db_url_public = $this->database->getServiceDatabaseUrl();
} }
$this->refreshFileStorages(); $this->refreshFileStorages();
} }
public function instantSave() { public function instantSaveAdvanced()
{
if (!$this->database->service->destination->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->emit('error', 'Log drain is not enabled on the server. Please enable it first.');
return;
}
$this->submit();
$this->emit('success', 'You need to restart the service for the changes to take effect.');
}
public function instantSave()
{
if ($this->database->is_public && !$this->database->public_port) { if ($this->database->is_public && !$this->database->public_port) {
$this->emit('error', 'Public port is required.'); $this->emit('error', 'Public port is required.');
$this->database->is_public = false; $this->database->is_public = false;

View File

@@ -23,7 +23,7 @@ class StackForm extends Component
foreach ($fields as $fieldKey => $field) { foreach ($fields as $fieldKey => $field) {
$key = data_get($field, 'key'); $key = data_get($field, 'key');
$value = data_get($field, 'value'); $value = data_get($field, 'value');
$rules = data_get($field, 'rules'); $rules = data_get($field, 'rules', 'nullable');
$isPassword = data_get($field, 'isPassword'); $isPassword = data_get($field, 'isPassword');
$this->fields[$key] = [ $this->fields[$key] = [
"serviceName" => $serviceName, "serviceName" => $serviceName,
@@ -31,6 +31,7 @@ class StackForm extends Component
"name" => $fieldKey, "name" => $fieldKey,
"value" => $value, "value" => $value,
"isPassword" => $isPassword, "isPassword" => $isPassword,
"rules" => $rules
]; ];
$this->rules["fields.$key.value"] = $rules; $this->rules["fields.$key.value"] = $rules;
$this->validationAttributes["fields.$key.value"] = $fieldKey; $this->validationAttributes["fields.$key.value"] = $fieldKey;

View File

@@ -6,5 +6,7 @@ use Livewire\Component;
class Destination extends Component class Destination extends Component
{ {
public $destination; public $resource;
public $servers = [];
public $additionalServers = [];
} }

View File

@@ -59,6 +59,7 @@ class Show extends Component
{ {
$this->validate(); $this->validate();
$this->env->save(); $this->env->save();
ray($this->env);
$this->emit('success', 'Environment variable updated successfully.'); $this->emit('success', 'Environment variable updated successfully.');
$this->emit('refreshEnvs'); $this->emit('refreshEnvs');
} }

View File

@@ -17,13 +17,15 @@ class Logs extends Component
public ?string $type = null; public ?string $type = null;
public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb $resource; public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb $resource;
public Server $server; public Server $server;
public ?string $container = null; public $container = [];
public $containers;
public $parameters; public $parameters;
public $query; public $query;
public $status; public $status;
public function mount() public function mount()
{ {
$this->containers = collect();
$this->parameters = get_route_parameters(); $this->parameters = get_route_parameters();
$this->query = request()->query(); $this->query = request()->query();
if (data_get($this->parameters, 'application_uuid')) { if (data_get($this->parameters, 'application_uuid')) {
@@ -33,7 +35,9 @@ class Logs extends Component
$this->server = $this->resource->destination->server; $this->server = $this->resource->destination->server;
$containers = getCurrentApplicationContainerStatus($this->server, $this->resource->id, 0); $containers = getCurrentApplicationContainerStatus($this->server, $this->resource->id, 0);
if ($containers->count() > 0) { if ($containers->count() > 0) {
$this->container = data_get($containers[0], 'Names'); $containers->each(function ($container) {
$this->containers->push(str_replace('/', '', $container['Names']));
});
} }
} else if (data_get($this->parameters, 'database_uuid')) { } else if (data_get($this->parameters, 'database_uuid')) {
$this->type = 'database'; $this->type = 'database';

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()
@@ -43,15 +45,20 @@ class Form extends Component
$this->wildcard_domain = $this->server->settings->wildcard_domain; $this->wildcard_domain = $this->server->settings->wildcard_domain;
$this->cleanup_after_percentage = $this->server->settings->cleanup_after_percentage; $this->cleanup_after_percentage = $this->server->settings->cleanup_after_percentage;
} }
public function serverRefresh() public function serverRefresh($install = true)
{ {
$this->validateServer(); $this->validateServer($install);
} }
public function instantSave() 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()
{ {
@@ -77,12 +84,15 @@ class Form extends Component
{ {
try { try {
$uptime = $this->server->validateConnection(); $uptime = $this->server->validateConnection();
if ($uptime) { if (!$uptime) {
$install && $this->emit('success', 'Server is reachable.');
} else {
$install && $this->emit('error', 'Server is not reachable. Please check your connection and configuration.'); $install && $this->emit('error', 'Server is not reachable. Please check your connection and configuration.');
return; return;
} }
$supported_os_type = $this->server->validateOS();
if (!$supported_os_type) {
$install && $this->emit('error', 'Server OS type is not supported for automated installation. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://coolify.io/docs/servers#install-docker-engine-manually">documentation</a>.');
return;
}
$dockerInstalled = $this->server->validateDockerEngine(); $dockerInstalled = $this->server->validateDockerEngine();
if ($dockerInstalled) { if ($dockerInstalled) {
$install && $this->emit('success', 'Docker Engine is installed.<br> Checking version.'); $install && $this->emit('success', 'Docker Engine is installed.<br> Checking version.');
@@ -92,11 +102,17 @@ class Form extends Component
} }
$dockerVersion = $this->server->validateDockerEngineVersion(); $dockerVersion = $this->server->validateDockerEngineVersion();
if ($dockerVersion) { if ($dockerVersion) {
$install && $this->emit('success', 'Docker Engine version is 23+.'); $install && $this->emit('success', 'Docker Engine version is 22+.');
} else { } else {
$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

@@ -2,6 +2,7 @@
namespace App\Http\Livewire\Server; namespace App\Http\Livewire\Server;
use App\Actions\Server\InstallLogDrain;
use App\Models\Server; use App\Models\Server;
use Livewire\Component; use Livewire\Component;
@@ -46,14 +47,8 @@ class LogDrains extends Component
public function configureLogDrain() public function configureLogDrain()
{ {
try { try {
if ($this->server->settings->is_logdrain_newrelic_enabled) { InstallLogDrain::run($this->server);
$this->server->logDrain('newrelic'); if (!$this->server->isLogDrainEnabled()) {
} else if ($this->server->settings->is_logdrain_highlight_enabled) {
$this->server->logDrain('highlight');
} else if ($this->server->settings->is_logdrain_axiom_enabled) {
$this->server->logDrain('axiom');
} else {
$this->server->logDrain('none');
$this->emit('serverRefresh'); $this->emit('serverRefresh');
$this->emit('success', 'Log drain service stopped.'); $this->emit('success', 'Log drain service stopped.');
return; return;

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

@@ -19,13 +19,14 @@ class Show extends Component
if (is_null($this->server)) { if (is_null($this->server)) {
return redirect()->route('server.all'); return redirect()->route('server.all');
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }
} }
public function submit() public function submit()
{ {
$this->emit('serverRefresh'); $this->emit('serverRefresh',false);
} }
public function render() public function render()
{ {

View File

@@ -24,6 +24,7 @@ use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use RuntimeException;
use Spatie\Url\Url; use Spatie\Url\Url;
use Symfony\Component\Yaml\Yaml; use Symfony\Component\Yaml\Yaml;
use Throwable; use Throwable;
@@ -54,6 +55,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private GithubApp|GitlabApp|string $source = 'other'; private GithubApp|GitlabApp|string $source = 'other';
private StandaloneDocker|SwarmDocker $destination; private StandaloneDocker|SwarmDocker $destination;
private Server $server; private Server $server;
private Server $mainServer;
private ?ApplicationPreview $preview = null; private ?ApplicationPreview $preview = null;
private ?string $git_type = null; private ?string $git_type = null;
@@ -71,9 +73,9 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private $docker_compose; private $docker_compose;
private $docker_compose_base64; private $docker_compose_base64;
private string $dockerfile_location = '/Dockerfile'; private string $dockerfile_location = '/Dockerfile';
private string $docker_compose_location = '/docker-compose.yml';
private ?string $addHosts = null; private ?string $addHosts = null;
private ?string $buildTarget = null; private ?string $buildTarget = null;
private $log_model;
private Collection $saved_outputs; private Collection $saved_outputs;
private ?string $full_healthcheck_url = null; private ?string $full_healthcheck_url = null;
@@ -90,9 +92,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
public $tries = 1; public $tries = 1;
public function __construct(int $application_deployment_queue_id) public function __construct(int $application_deployment_queue_id)
{ {
// ray()->clearScreen();
$this->application_deployment_queue = ApplicationDeploymentQueue::find($application_deployment_queue_id); $this->application_deployment_queue = ApplicationDeploymentQueue::find($application_deployment_queue_id);
$this->log_model = $this->application_deployment_queue;
$this->application = Application::find($this->application_deployment_queue->application_id); $this->application = Application::find($this->application_deployment_queue->application_id);
$this->build_pack = data_get($this->application, 'build_pack'); $this->build_pack = data_get($this->application, 'build_pack');
@@ -110,9 +110,9 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->source = $source->getMorphClass()::where('id', $this->application->source->id)->first(); $this->source = $source->getMorphClass()::where('id', $this->application->source->id)->first();
} }
$this->destination = $this->application->destination->getMorphClass()::where('id', $this->application->destination->id)->first(); $this->destination = $this->application->destination->getMorphClass()::where('id', $this->application->destination->id)->first();
$this->server = $this->destination->server; $this->server = $this->mainServer = $this->destination->server;
$this->serverUser = $this->server->user; $this->serverUser = $this->server->user;
$this->basedir = "/artifacts/{$this->deployment_uuid}"; $this->basedir = $this->application->generateBaseDir($this->deployment_uuid);
$this->workdir = "{$this->basedir}" . rtrim($this->application->base_directory, '/'); $this->workdir = "{$this->basedir}" . rtrim($this->application->base_directory, '/');
$this->configuration_dir = application_configuration_dir() . "/{$this->application->uuid}"; $this->configuration_dir = application_configuration_dir() . "/{$this->application->uuid}";
$this->is_debug_enabled = $this->application->settings->is_debug_enabled; $this->is_debug_enabled = $this->application->settings->is_debug_enabled;
@@ -156,49 +156,48 @@ 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} ";
} }
// Get user home directory
$this->serverUserHomeDir = instant_remote_process(["echo \$HOME"], $this->server);
$this->dockerConfigFileExists = instant_remote_process(["test -f {$this->serverUserHomeDir}/.docker/config.json && echo 'OK' || echo 'NOK'"], $this->server);
// Check custom port // Check custom port
preg_match('/(?<=:)\d+(?=\/)/', $this->application->git_repository, $matches); ['repository' => $this->customRepository, 'port' => $this->customPort] = $this->application->customRepository();
if (count($matches) === 1) {
$this->customPort = $matches[0];
$gitHost = str($this->application->git_repository)->before(':');
$gitRepo = str($this->application->git_repository)->after('/');
$this->customRepository = "$gitHost:$gitRepo";
} else {
$this->customRepository = $this->application->git_repository;
}
try { try {
if ($this->restart_only && $this->application->build_pack !== 'dockerimage') { if ($this->restart_only && $this->application->build_pack !== 'dockerimage') {
$this->just_restart(); $this->just_restart();
if ($this->server->isProxyShouldRun()) {
dispatch(new ContainerStatusJob($this->server));
}
$this->next(ApplicationDeploymentStatus::FINISHED->value);
$this->application->isConfigurationChanged(true);
return;
} else if ($this->application->dockerfile) { } else if ($this->application->dockerfile) {
$this->deploy_simple_dockerfile(); $this->deploy_simple_dockerfile();
} else if ($this->application->build_pack === 'dockercompose') {
$this->deploy_docker_compose_buildpack();
} else if ($this->application->build_pack === 'dockerimage') { } else if ($this->application->build_pack === 'dockerimage') {
$this->deploy_dockerimage_buildpack(); $this->deploy_dockerimage_buildpack();
} else if ($this->application->build_pack === 'dockerfile') { } else if ($this->application->build_pack === 'dockerfile') {
@@ -215,10 +214,23 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if ($this->server->isProxyShouldRun()) { if ($this->server->isProxyShouldRun()) {
dispatch(new ContainerStatusJob($this->server)); dispatch(new ContainerStatusJob($this->server));
} }
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->next(ApplicationDeploymentStatus::FINISHED->value);
$this->application->isConfigurationChanged(true); $this->application->isConfigurationChanged(true);
} catch (Exception $e) { } catch (Exception $e) {
ray($e);
$this->fail($e); $this->fail($e);
throw $e; throw $e;
} finally { } finally {
@@ -256,60 +268,70 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
); );
} }
} }
private function push_to_docker_registry()
// private function deploy_docker_compose() {
// { try {
// $dockercompose_base64 = base64_encode($this->application->dockercompose); instant_remote_process(["docker images --format '{{json .}}' {$this->production_image_name}"], $this->server);
// $this->execute_remote_command( $this->execute_remote_command(
// [ [
// "echo 'Starting deployment of {$this->application->name}.'" "echo '\n----------------------------------------'",
// ], ],
// ); ["echo -n 'Pushing image to docker registry ({$this->production_image_name}).'"],
// $this->prepare_builder_image(); [
// $this->execute_remote_command( executeInDocker($this->deployment_uuid, "docker push {$this->production_image_name}"), 'hidden' => true
// [ ],
// executeInDocker($this->deployment_uuid, "echo '$dockercompose_base64' | base64 -d > $this->workdir/docker-compose.yaml") );
// ], if ($this->application->docker_registry_image_tag) {
// ); // Tag image with latest
// $this->build_image_name = Str::lower("{$this->customRepository}:build"); $this->execute_remote_command(
// $this->production_image_name = Str::lower("{$this->application->uuid}:latest"); ['echo -n "Tagging and pushing image with latest tag."'],
// $this->save_environment_variables(); [
// $containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id); executeInDocker($this->deployment_uuid, "docker tag {$this->production_image_name} {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"), 'ignore_errors' => true, 'hidden' => true
// ray($containers); ],
// if ($containers->count() > 0) { [
// foreach ($containers as $container) { executeInDocker($this->deployment_uuid, "docker push {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"), 'ignore_errors' => true, 'hidden' => true
// $containerName = data_get($container, 'Names'); ],
// if ($containerName) { );
// instant_remote_process( }
// ["docker rm -f {$containerName}"], $this->execute_remote_command([
// $this->application->destination->server "echo -n 'Image pushed to docker registry.'"
// ); ]);
// } } catch (Exception $e) {
// } $this->execute_remote_command(
// } ["echo -n 'Failed to push image to docker registry. Please check debug logs for more information.'"],
);
// $this->execute_remote_command( ray($e);
// ["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) {
$this->build_image_name = Str::lower("{$this->application->uuid}:build"); if ($this->application->docker_registry_image_name) {
$this->production_image_name = Str::lower("{$this->application->uuid}:latest"); $this->build_image_name = Str::lower("{$this->application->docker_registry_image_name}:build");
$this->production_image_name = Str::lower("{$this->application->docker_registry_image_name}:latest");
} else {
$this->build_image_name = Str::lower("{$this->application->uuid}:build");
$this->production_image_name = Str::lower("{$this->application->uuid}:latest");
}
} else if ($this->application->build_pack === 'dockerimage') { } else if ($this->application->build_pack === 'dockerimage') {
$this->production_image_name = Str::lower("{$this->dockerImage}:{$this->dockerImageTag}"); $this->production_image_name = Str::lower("{$this->dockerImage}:{$this->dockerImageTag}");
} else if ($this->pull_request_id !== 0) { } else if ($this->pull_request_id !== 0) {
$this->build_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}-build"); if ($this->application->docker_registry_image_name) {
$this->production_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}"); $this->build_image_name = Str::lower("{$this->application->docker_registry_image_name}:pr-{$this->pull_request_id}-build");
} else { $this->production_image_name = Str::lower("{$this->application->docker_registry_image_name}:pr-{$this->pull_request_id}");
$tag = Str::of("{$this->commit}-{$this->application->id}-{$this->pull_request_id}"); } else {
if (strlen($tag) > 128) { $this->build_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}-build");
$tag = $tag->substr(0, 128); $this->production_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}");
}
} else {
$this->dockerImageTag = str($this->commit)->substr(0, 128);
if ($this->application->docker_registry_image_name) {
$this->build_image_name = Str::lower("{$this->application->docker_registry_image_name}:{$this->dockerImageTag}-build");
$this->production_image_name = Str::lower("{$this->application->docker_registry_image_name}:{$this->dockerImageTag}");
} else {
$this->build_image_name = Str::lower("{$this->application->uuid}:{$this->dockerImageTag}-build");
$this->production_image_name = Str::lower("{$this->application->uuid}:{$this->dockerImageTag}");
} }
$this->build_image_name = Str::lower("{$this->application->uuid}:{$tag}-build");
$this->production_image_name = Str::lower("{$this->application->uuid}:{$tag}");
} }
} }
private function just_restart() private function just_restart()
@@ -323,23 +345,39 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->check_git_if_build_needed(); $this->check_git_if_build_needed();
$this->set_base_dir(); $this->set_base_dir();
$this->generate_image_names(); $this->generate_image_names();
$this->execute_remote_command([ $this->check_image_locally_or_remotely();
"docker images -q {$this->production_image_name} 2>/dev/null", "hidden" => true, "save" => "local_image_found"
]);
if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty()) { if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty()) {
$this->generate_compose_file(); $this->generate_compose_file();
$this->rolling_update(); $this->rolling_update();
return; return;
} }
throw new RuntimeException('Cannot find image anywhere. Please redeploy the application.');
}
private function check_image_locally_or_remotely()
{
$this->execute_remote_command([ $this->execute_remote_command([
"echo 'Cannot find image {$this->production_image_name} locally. Please redeploy the application.'", "docker images -q {$this->production_image_name} 2>/dev/null", "hidden" => true, "save" => "local_image_found"
]); ]);
if (str($this->saved_outputs->get('local_image_found'))->isEmpty() && $this->application->docker_registry_image_name) {
$this->execute_remote_command([
"docker pull {$this->production_image_name} 2>/dev/null", "ignore_errors" => true, "hidden" => true
]);
$this->execute_remote_command([
"docker images -q {$this->production_image_name} 2>/dev/null", "hidden" => true, "save" => "local_image_found"
]);
}
} }
private function save_environment_variables() private function save_environment_variables()
{ {
$envs = collect([]); $envs = collect([]);
foreach ($this->application->environment_variables as $env) { if ($this->pull_request_id !== 0) {
$envs->push($env->key . '=' . $env->value); foreach ($this->application->environment_variables_preview as $env) {
$envs->push($env->key . '=' . $env->value);
}
} else {
foreach ($this->application->environment_variables as $env) {
$envs->push($env->key . '=' . $env->value);
}
} }
$envs_base64 = base64_encode($envs->implode("\n")); $envs_base64 = base64_encode($envs->implode("\n"));
$this->execute_remote_command( $this->execute_remote_command(
@@ -348,6 +386,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
], ],
); );
} }
private function deploy_simple_dockerfile() private function deploy_simple_dockerfile()
{ {
$dockerfile_base64 = base64_encode($this->application->dockerfile); $dockerfile_base64 = base64_encode($this->application->dockerfile);
@@ -385,7 +424,54 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->generate_compose_file(); $this->generate_compose_file();
$this->rolling_update(); $this->rolling_update();
} }
private function deploy_docker_compose_buildpack()
{
if (data_get($this->application, 'docker_compose_location')) {
$this->docker_compose_location = $this->application->docker_compose_location;
}
if ($this->pull_request_id === 0) {
$this->application_deployment_queue->addLogEntry("Starting deployment of {$this->application->name}.");
} else {
$this->application_deployment_queue->addLogEntry("Starting pull request (#{$this->pull_request_id}) deployment of {$this->customRepository}:{$this->application->git_branch}.");
}
$this->server->executeRemoteCommand(
commands: $this->application->prepareHelperImage($this->deployment_uuid),
loggingModel: $this->application_deployment_queue
);
$this->check_git_if_build_needed();
$this->clone_repository();
$this->generate_image_names();
$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}{$this->docker_compose_location}"), "hidden" => true
]);
$this->save_environment_variables();
$this->stop_running_container(force: true);
ray($this->pull_request_id);
$networkId = $this->application->uuid;
if ($this->pull_request_id !== 0) {
$networkId = "{$this->application->uuid}-{$this->pull_request_id}";
}
ray($networkId);
if ($this->server->isSwarm()) {
// TODO
} else {
$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);
}
private function deploy_dockerfile_buildpack() private function deploy_dockerfile_buildpack()
{ {
if (data_get($this->application, 'dockerfile_location')) { if (data_get($this->application, 'dockerfile_location')) {
@@ -406,7 +492,12 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->generate_build_env_variables(); $this->generate_build_env_variables();
$this->add_build_env_variables_to_dockerfile(); $this->add_build_env_variables_to_dockerfile();
$this->build_image(); $this->build_image();
// if ($this->application->additional_destinations) {
// $this->push_to_docker_registry();
// $this->deploy_to_additional_destinations();
// } else {
$this->rolling_update(); $this->rolling_update();
// }
} }
private function deploy_nixpacks_buildpack() private function deploy_nixpacks_buildpack()
{ {
@@ -420,12 +511,10 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->set_base_dir(); $this->set_base_dir();
$this->generate_image_names(); $this->generate_image_names();
if (!$this->force_rebuild) { if (!$this->force_rebuild) {
$this->execute_remote_command([ $this->check_image_locally_or_remotely();
"docker images -q {$this->production_image_name} 2>/dev/null", "hidden" => true, "save" => "local_image_found" if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) {
]);
if (Str::of($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) {
$this->execute_remote_command([ $this->execute_remote_command([
"echo 'No configuration changed & Docker Image found locally with the same Git Commit SHA {$this->application->uuid}:{$this->commit}. Build step skipped.'", "echo 'No configuration changed & image found ({$this->production_image_name}) with the same Git Commit SHA. Build step skipped.'",
]); ]);
$this->generate_compose_file(); $this->generate_compose_file();
$this->rolling_update(); $this->rolling_update();
@@ -466,71 +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 '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) {
["echo -n 'Rolling update started.'"], $this->execute_remote_command(
); [
$this->start_by_compose_file(); "echo '\n----------------------------------------'",
$this->health_check(); ],
$this->stop_running_container(); ["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() 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 = 0; }
$this->execute_remote_command( // ray('New container name: ', $this->container_name);
[ if ($this->container_name) {
"echo 'Waiting for healthcheck to pass on the new version of your application.'" $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(
[
"echo 'Attempt {$counter} of {$this->application->health_check_retries}'"
],
[
"docker inspect --format='{{json .State.Health.Status}}' {$this->container_name}",
"hidden" => true,
"save" => "health_check"
],
);
$this->execute_remote_command(
[
"echo 'New version healthcheck status: {$this->saved_outputs->get('health_check')}'"
],
);
if (Str::of($this->saved_outputs->get('health_check'))->contains('healthy')) {
$this->newVersionIsHealthy = true;
$this->execute_remote_command( $this->execute_remote_command(
[ [
"echo 'Rolling update completed.'" "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')}'"
], ],
); );
$this->application->update(['status' => 'running']); if (Str::of($this->saved_outputs->get('health_check'))->contains('healthy')) {
break; $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);
} }
} }
} }
@@ -550,8 +651,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
} }
$this->generate_compose_file(); $this->generate_compose_file();
// Needs separate preview variables // Needs separate preview variables
// $this->generate_build_env_variables(); $this->generate_build_env_variables();
// $this->add_build_env_variables_to_dockerfile(); $this->add_build_env_variables_to_dockerfile();
$this->build_image(); $this->build_image();
$this->stop_running_container(); $this->stop_running_container();
$this->execute_remote_command( $this->execute_remote_command(
@@ -563,12 +664,15 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function prepare_builder_image() private function prepare_builder_image()
{ {
$helperImage = config('coolify.helper_image'); $helperImage = config('coolify.helper_image');
// Get user home directory
$this->serverUserHomeDir = instant_remote_process(["echo \$HOME"], $this->server);
$this->dockerConfigFileExists = instant_remote_process(["test -f {$this->serverUserHomeDir}/.docker/config.json && echo 'OK' || echo 'NOK'"], $this->server);
if ($this->dockerConfigFileExists === 'OK') { if ($this->dockerConfigFileExists === 'OK') {
$runCommand = "docker run -d --network {$this->destination->network} -v /:/host --name {$this->deployment_uuid} --rm -v {$this->serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}"; $runCommand = "docker run -d --network {$this->destination->network} -v /:/host --name {$this->deployment_uuid} --rm -v {$this->serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
} else { } else {
$runCommand = "docker run -d --network {$this->destination->network} -v /:/host --name {$this->deployment_uuid} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}"; $runCommand = "docker run -d --network {$this->destination->network} -v /:/host --name {$this->deployment_uuid} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
} }
$this->execute_remote_command( $this->execute_remote_command(
[ [
"echo -n 'Preparing container with helper image: $helperImage.'", "echo -n 'Preparing container with helper image: $helperImage.'",
@@ -582,7 +686,31 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
], ],
); );
} }
private function deploy_to_additional_destinations()
{
$destination_ids = collect(str($this->application->additional_destinations)->explode(','));
foreach ($destination_ids as $destination_id) {
$destination = StandaloneDocker::find($destination_id);
$server = $destination->server;
if ($server->team_id !== $this->mainServer->team_id) {
$this->execute_remote_command(
[
"echo -n 'Skipping deployment to {$server->name}. Not in the same team?!'",
],
);
continue;
}
$this->server = $server;
$this->execute_remote_command(
[
"echo -n 'Deploying to {$this->server->name}.'",
],
);
$this->prepare_builder_image();
$this->generate_image_names();
$this->rolling_update();
}
}
private function set_base_dir() private function set_base_dir()
{ {
$this->execute_remote_command( $this->execute_remote_command(
@@ -630,10 +758,12 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function clone_repository() private function clone_repository()
{ {
$importCommands = $this->generate_git_import_commands(); $importCommands = $this->generate_git_import_commands();
$this->application_deployment_queue->addLogEntry("\n----------------------------------------");
$this->application_deployment_queue->addLogEntry("Importing {$this->customRepository}:{$this->application->git_branch} (commit sha {$this->application->git_commit_sha}) to {$this->basedir}.");
if ($this->pull_request_id !== 0) {
$this->application_deployment_queue->addLogEntry("Checking out tag pull/{$this->pull_request_id}/head.");
}
$this->execute_remote_command( $this->execute_remote_command(
[
"echo -n 'Importing {$this->customRepository}:{$this->application->git_branch} (commit sha {$this->application->git_commit_sha}) to {$this->basedir}. '"
],
[ [
$importCommands, "hidden" => true $importCommands, "hidden" => true
] ]
@@ -642,90 +772,13 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function generate_git_import_commands() private function generate_git_import_commands()
{ {
$this->branch = $this->application->git_branch; ['commands' => $commands, 'branch' => $this->branch, 'fullRepoUrl' => $this->fullRepoUrl] = $this->application->generateGitImportCommands($this->deployment_uuid, $this->pull_request_id, $this->git_type);
$commands = collect([]); return $commands;
$git_clone_command = "git clone -q -b {$this->application->git_branch}";
if ($this->pull_request_id !== 0) {
$pr_branch_name = "pr-{$this->pull_request_id}-coolify";
}
if ($this->application->deploymentType() === 'source') {
$source_html_url = data_get($this->application, 'source.html_url');
$url = parse_url(filter_var($source_html_url, FILTER_SANITIZE_URL));
$source_html_url_host = $url['host'];
$source_html_url_scheme = $url['scheme'];
if ($this->source->getMorphClass() == 'App\Models\GithubApp') {
if ($this->source->is_public) {
$this->fullRepoUrl = "{$this->source->html_url}/{$this->customRepository}";
$git_clone_command = "{$git_clone_command} {$this->source->html_url}/{$this->customRepository} {$this->basedir}";
$git_clone_command = $this->set_git_import_settings($git_clone_command);
$commands->push(executeInDocker($this->deployment_uuid, $git_clone_command));
} else {
$github_access_token = generate_github_installation_token($this->source);
$commands->push(executeInDocker($this->deployment_uuid, "git clone -q -b {$this->application->git_branch} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$this->customRepository}.git {$this->basedir}"));
$this->fullRepoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$this->customRepository}.git";
}
if ($this->pull_request_id !== 0) {
$this->branch = "pull/{$this->pull_request_id}/head:$pr_branch_name";
$commands->push(executeInDocker($this->deployment_uuid, "cd {$this->basedir} && git fetch origin $this->branch && git checkout $pr_branch_name"));
}
return $commands->implode(' && ');
}
}
if ($this->application->deploymentType() === 'deploy_key') {
$this->fullRepoUrl = $this->customRepository;
$private_key = data_get($this->application, 'private_key.private_key');
if (is_null($private_key)) {
throw new Exception('Private key not found. Please add a private key to the application and try again.');
}
$private_key = base64_encode($private_key);
$git_clone_command_base = "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" {$git_clone_command} {$this->customRepository} {$this->basedir}";
$git_clone_command = $this->set_git_import_settings($git_clone_command_base);
$commands = collect([
executeInDocker($this->deployment_uuid, "mkdir -p /root/.ssh"),
executeInDocker($this->deployment_uuid, "echo '{$private_key}' | base64 -d > /root/.ssh/id_rsa"),
executeInDocker($this->deployment_uuid, "chmod 600 /root/.ssh/id_rsa"),
]);
if ($this->pull_request_id !== 0) {
ray($this->git_type);
if ($this->git_type === 'gitlab') {
$this->branch = "merge-requests/{$this->pull_request_id}/head:$pr_branch_name";
$commands->push(executeInDocker($this->deployment_uuid, "echo 'Checking out $this->branch'"));
$git_clone_command = "{$git_clone_command} && cd {$this->basedir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $this->branch && git checkout $pr_branch_name";
}
if ($this->git_type === 'github') {
$this->branch = "pull/{$this->pull_request_id}/head:$pr_branch_name";
$commands->push(executeInDocker($this->deployment_uuid, "echo 'Checking out $this->branch'"));
$git_clone_command = "{$git_clone_command} && cd {$this->basedir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $this->branch && git checkout $pr_branch_name";
}
}
$commands->push(executeInDocker($this->deployment_uuid, $git_clone_command));
return $commands->implode(' && ');
}
if ($this->application->deploymentType() === 'other') {
$this->fullRepoUrl = $this->customRepository;
$git_clone_command = "{$git_clone_command} {$this->customRepository} {$this->basedir}";
$git_clone_command = $this->set_git_import_settings($git_clone_command);
$commands->push(executeInDocker($this->deployment_uuid, $git_clone_command));
return $commands->implode(' && ');
}
} }
private function set_git_import_settings($git_clone_command) private function set_git_import_settings($git_clone_command)
{ {
if ($this->application->git_commit_sha !== 'HEAD') { return $this->application->setGitImportSettings($this->deployment_uuid, $git_clone_command);
$git_clone_command = "{$git_clone_command} && cd {$this->basedir} && git -c advice.detachedHead=false checkout {$this->application->git_commit_sha} >/dev/null 2>&1";
}
if ($this->application->settings->is_git_submodules_enabled) {
$git_clone_command = "{$git_clone_command} && cd {$this->basedir} && git submodule update --init --recursive";
}
if ($this->application->settings->is_git_lfs_enabled) {
$git_clone_command = "{$git_clone_command} && cd {$this->basedir} && git lfs pull";
}
return $git_clone_command;
} }
private function cleanup_git() private function cleanup_git()
@@ -737,16 +790,10 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function generate_nixpacks_confs() private function generate_nixpacks_confs()
{ {
$this->execute_remote_command(
[
"echo -n 'Generating nixpacks configuration.'",
]
);
$nixpacks_command = $this->nixpacks_build_cmd(); $nixpacks_command = $this->nixpacks_build_cmd();
$this->execute_remote_command( $this->execute_remote_command(
[ [
"echo -n Running: $nixpacks_command", "echo -n 'Generating nixpacks configuration with: $nixpacks_command'",
], ],
[executeInDocker($this->deployment_uuid, $nixpacks_command)], [executeInDocker($this->deployment_uuid, $nixpacks_command)],
[executeInDocker($this->deployment_uuid, "cp {$this->workdir}/.nixpacks/Dockerfile {$this->workdir}/Dockerfile")], [executeInDocker($this->deployment_uuid, "cp {$this->workdir}/.nixpacks/Dockerfile {$this->workdir}/Dockerfile")],
@@ -757,7 +804,11 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function nixpacks_build_cmd() private function nixpacks_build_cmd()
{ {
$this->generate_env_variables(); $this->generate_env_variables();
$nixpacks_command = "nixpacks build --cache-key '{$this->application->uuid}' -o {$this->workdir} {$this->env_args} --no-error-without-start"; $cacheKey = $this->application->uuid;
if ($this->pull_request_id !== 0) {
$cacheKey = "{$this->application->uuid}-pr-{$this->pull_request_id}";
}
$nixpacks_command = "nixpacks build --cache-key '{$cacheKey}' -o {$this->workdir} {$this->env_args} --no-error-without-start";
if ($this->application->build_command) { if ($this->application->build_command) {
$nixpacks_command .= " --build-cmd \"{$this->application->build_command}\""; $nixpacks_command .= " --build-cmd \"{$this->application->build_command}\"";
} }
@@ -807,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 = [
@@ -832,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,
@@ -864,7 +899,49 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
] ]
] ]
]; ];
if ($this->server->isDrainLogActivated()) { 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'] = [ $docker_compose['services'][$this->container_name]['logging'] = [
'driver' => 'fluentd', 'driver' => 'fluentd',
'options' => [ 'options' => [
@@ -874,6 +951,25 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
] ]
]; ];
} }
if ($this->application->settings->is_gpu_enabled) {
$docker_compose['services'][$this->container_name]['deploy']['resources']['reservations']['devices'] = [
[
'driver' => data_get($this->application, 'settings.gpu_driver', 'nvidia'),
'capabilities' => ['gpu'],
'options' => data_get($this->application, 'settings.gpu_options', [])
]
];
if (data_get($this->application, 'settings.gpu_count')) {
$count = data_get($this->application, 'settings.gpu_count');
if ($count === 'all') {
$docker_compose['services'][$this->container_name]['deploy']['resources']['reservations']['devices'][0]['count'] = $count;
} else {
$docker_compose['services'][$this->container_name]['deploy']['resources']['reservations']['devices'][0]['count'] = (int) $count;
}
} else if (data_get($this->application, 'settings.gpu_device_ids')) {
$docker_compose['services'][$this->container_name]['deploy']['resources']['reservations']['devices'][0]['ids'] = data_get($this->application, 'settings.gpu_device_ids');
}
}
if ($this->application->isHealthcheckDisabled()) { if ($this->application->isHealthcheckDisabled()) {
data_forget($docker_compose, 'services.' . $this->container_name . '.healthcheck'); data_forget($docker_compose, 'services.' . $this->container_name . '.healthcheck');
} }
@@ -892,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]);
@@ -1000,9 +1101,12 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
"echo -n 'Static deployment. Copying static assets to the image.'", "echo -n 'Static deployment. Copying static assets to the image.'",
]); ]);
} else { } else {
$this->execute_remote_command([ $this->execute_remote_command(
"echo -n 'Building docker image for your application. To check the current progress, click on Show Debug Logs.'", [
]); "echo -n 'Building docker image started.'",
],
["echo -n 'To check the current progress, click on Show Debug Logs.'"]
);
} }
if ($this->application->settings->is_static || $this->application->build_pack === 'static') { if ($this->application->settings->is_static || $this->application->build_pack === 'static') {
@@ -1084,19 +1188,17 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
]); ]);
} }
} }
$this->execute_remote_command([
"echo -n 'Building docker image completed.'",
]);
} }
private function stop_running_container(bool $force = false) private function stop_running_container(bool $force = false)
{ {
$this->execute_remote_command(["echo -n 'Removing old version of your application.'"]); $this->application_deployment_queue->addLogEntry("Removing old containers.");
if ($this->newVersionIsHealthy || $force) { if ($this->newVersionIsHealthy || $force) {
$containers = getCurrentApplicationContainerStatus($this->server, $this->application->id, $this->pull_request_id); $containers = getCurrentApplicationContainerStatus($this->server, $this->application->id, $this->pull_request_id);
if ($this->pull_request_id !== 0) { if ($this->pull_request_id === 0) {
$containers = $containers->filter(function ($container) {
return data_get($container, 'Names') === $this->container_name;
});
} else {
$containers = $containers->filter(function ($container) { $containers = $containers->filter(function ($container) {
return data_get($container, 'Names') !== $this->container_name; return data_get($container, 'Names') !== $this->container_name;
}); });
@@ -1108,8 +1210,8 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
); );
}); });
} else { } else {
$this->application_deployment_queue->addLogEntry("New container is not healthy, rolling back to the old container.");
$this->execute_remote_command( $this->execute_remote_command(
["echo -n 'New version is not healthy, rolling back to the old version.'"],
[executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true], [executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true],
); );
} }
@@ -1118,18 +1220,17 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
private function start_by_compose_file() private function start_by_compose_file()
{ {
if ($this->application->build_pack === 'dockerimage') { if ($this->application->build_pack === 'dockerimage') {
$this->application_deployment_queue->addLogEntry("Pulling latest images from the registry.");
$this->execute_remote_command( $this->execute_remote_command(
["echo -n 'Pulling latest images from the registry.'"],
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} pull"), "hidden" => true], [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} pull"), "hidden" => true],
["echo -n 'Starting application (could take a while).'"],
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true], [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true],
); );
} else { } else {
$this->execute_remote_command( $this->execute_remote_command(
["echo -n 'Starting application (could take a while).'"],
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true], [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()
@@ -1154,10 +1255,16 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"), "hidden" => true, "save" => 'dockerfile' executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"), "hidden" => true, "save" => 'dockerfile'
]); ]);
$dockerfile = collect(Str::of($this->saved_outputs->get('dockerfile'))->trim()->explode("\n")); $dockerfile = collect(Str::of($this->saved_outputs->get('dockerfile'))->trim()->explode("\n"));
if ($this->pull_request_id === 0) {
foreach ($this->application->build_environment_variables as $env) { foreach ($this->application->build_environment_variables as $env) {
$dockerfile->splice(1, 0, "ARG {$env->key}={$env->value}"); $dockerfile->splice(1, 0, "ARG {$env->key}={$env->value}");
}
} else {
foreach ($this->application->build_environment_variables_preview as $env) {
$dockerfile->splice(1, 0, "ARG {$env->key}={$env->value}");
}
} }
ray($dockerfile->implode("\n"));
$dockerfile_base64 = base64_encode($dockerfile->implode("\n")); $dockerfile_base64 = base64_encode($dockerfile->implode("\n"));
$this->execute_remote_command([ $this->execute_remote_command([
executeInDocker($this->deployment_uuid, "echo '{$dockerfile_base64}' | base64 -d > {$this->workdir}{$this->dockerfile_location}"), executeInDocker($this->deployment_uuid, "echo '{$dockerfile_base64}' | base64 -d > {$this->workdir}{$this->dockerfile_location}"),
@@ -1185,11 +1292,15 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
public function failed(Throwable $exception): void public function failed(Throwable $exception): void
{ {
$this->execute_remote_command( $this->execute_remote_command(
["echo 'Oops something is not okay, are you okay? 😢'"], ["echo 'Oops something is not okay, are you okay? 😢'", 'type' => 'err'],
["echo '{$exception->getMessage()}'"], ["echo '{$exception->getMessage()}'", 'type' => 'err'],
["echo -n 'Deployment failed. Removing the new version of your application.'"],
[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

@@ -0,0 +1,28 @@
<?php
namespace App\Jobs;
use App\Traits\ExecuteRemoteCommand;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ApplicationRestartJob implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, ExecuteRemoteCommand;
public $timeout = 3600;
public $tries = 1;
public string $applicationDeploymentQueueId;
public function __construct(string $applicationDeploymentQueueId)
{
$this->applicationDeploymentQueueId = $applicationDeploymentQueueId;
}
public function handle() {
ray('Restarting application');
}
}

View File

@@ -0,0 +1,88 @@
<?php
namespace App\Jobs;
use App\Actions\Server\InstallLogDrain;
use App\Models\Server;
use App\Notifications\Container\ContainerRestarted;
use App\Notifications\Container\ContainerStopped;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Sleep;
class CheckLogDrainContainerJob implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(public Server $server)
{
}
public function middleware(): array
{
return [(new WithoutOverlapping($this->server->id))->dontRelease()];
}
public function uniqueId(): int
{
return $this->server->id;
}
public function healthcheck()
{
$status = instant_remote_process(["docker inspect --format='{{json .State.Status}}' coolify-log-drain"], $this->server, false);
if (str($status)->contains('running')) {
return true;
} else {
return false;
}
}
public function handle(): void
{
// ray("checking log drain statuses for {$this->server->id}");
try {
if (!$this->server->isServerReady()) {
return;
};
$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);
$containers = format_docker_command_output_to_json($containers);
$foundLogDrainContainer = $containers->filter(function ($value, $key) {
return data_get($value, 'Name') === '/coolify-log-drain';
})->first();
if (!$foundLogDrainContainer || !$this->healthcheck()) {
ray('Log drain container not found or unhealthy. Restarting...');
InstallLogDrain::run($this->server);
Sleep::for(10)->seconds();
if ($this->healthcheck()) {
if ($this->server->log_drain_notification_sent) {
$this->server->team->notify(new ContainerRestarted('Coolify Log Drainer', $this->server));
$this->server->update(['log_drain_notification_sent' => false]);
}
return;
}
if (!$this->server->log_drain_notification_sent) {
ray('Log drain container still unhealthy. Sending notification...');
$this->server->team->notify(new ContainerStopped('Coolify Log Drainer', $this->server, null));
$this->server->update(['log_drain_notification_sent' => true]);
}
} else {
if ($this->server->log_drain_notification_sent) {
$this->server->team->notify(new ContainerRestarted('Coolify Log Drainer', $this->server));
$this->server->update(['log_drain_notification_sent' => false]);
}
}
} catch (\Throwable $e) {
send_internal_notification("CheckLogDrainContainerJob failed on ({$this->server->id}) with: " . $e->getMessage());
ray($e->getMessage());
handleError($e);
}
}
}

View File

@@ -35,54 +35,71 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
return $this->server->id; return $this->server->id;
} }
public function handle(): void public function handle()
{ {
ray("checking container statuses for {$this->server->id}"); // ray("checking container statuses for {$this->server->id}");
try { try {
$this->server->checkServerRediness(); if (!$this->server->isServerReady()) {
$containers = instant_remote_process(["docker container ls -q"], $this->server); return;
if (!$containers) { };
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; return;
} }
$containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this->server);
$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();
$previews = $this->server->previews(); $previews = $this->server->previews();
$this->server->proxyType();
/// Check if proxy is running
$foundProxyContainer = $containers->filter(function ($value, $key) {
return data_get($value, 'Name') === '/coolify-proxy';
})->first();
if (!$foundProxyContainer) {
try {
$shouldStart = CheckProxy::run($this->server);
if ($shouldStart) {
StartProxy::run($this->server, false);
$this->server->team->notify(new ContainerRestarted('coolify-proxy', $this->server));
} else {
ray('Proxy could not be started.');
}
} catch (\Throwable $e) {
ray($e);
}
} else {
$this->server->proxy->status = data_get($foundProxyContainer, 'State.Status');
$this->server->save();
$connectProxyToDockerNetworks = connectProxyToNetworks($this->server);
instant_remote_process($connectProxyToDockerNetworks, $this->server, false);
}
$foundApplications = []; $foundApplications = [];
$foundApplicationPreviews = []; $foundApplicationPreviews = [];
$foundDatabases = []; $foundDatabases = [];
$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');
$uuid = data_get($labels, 'com.docker.compose.service');
}
$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) {
@@ -114,7 +131,6 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
} }
} }
} else { } else {
$uuid = data_get($labels, 'com.docker.compose.service');
if ($uuid) { if ($uuid) {
$database = $databases->where('uuid', $uuid)->first(); $database = $databases->where('uuid', $uuid)->first();
if ($database) { if ($database) {
@@ -187,7 +203,7 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
} else { } else {
$url = null; $url = null;
} }
$this->server->team->notify(new ContainerStopped($containerName, $this->server, $url)); $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
$exitedService->update(['status' => 'exited']); $exitedService->update(['status' => 'exited']);
} }
@@ -214,7 +230,7 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
$url = null; $url = null;
} }
$this->server->team->notify(new ContainerStopped($containerName, $this->server, $url)); $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
} }
$notRunningApplicationPreviews = $previews->pluck('id')->diff($foundApplicationPreviews); $notRunningApplicationPreviews = $previews->pluck('id')->diff($foundApplicationPreviews);
foreach ($notRunningApplicationPreviews as $previewId) { foreach ($notRunningApplicationPreviews as $previewId) {
@@ -239,7 +255,7 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
$url = null; $url = null;
} }
$this->server->team->notify(new ContainerStopped($containerName, $this->server, $url)); $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
} }
$notRunningDatabases = $databases->pluck('id')->diff($foundDatabases); $notRunningDatabases = $databases->pluck('id')->diff($foundDatabases);
foreach ($notRunningDatabases as $database) { foreach ($notRunningDatabases as $database) {
@@ -263,12 +279,40 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
} else { } else {
$url = null; $url = null;
} }
$this->server->team->notify(new ContainerStopped($containerName, $this->server, $url)); $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
}
// Check if proxy is running
$this->server->proxyType();
$foundProxyContainer = $containers->filter(function ($value, $key) {
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 {
$shouldStart = CheckProxy::run($this->server);
if ($shouldStart) {
StartProxy::run($this->server, false);
$this->server->team?->notify(new ContainerRestarted('coolify-proxy', $this->server));
} else {
ray('Proxy could not be started.');
}
} catch (\Throwable $e) {
ray($e);
}
} else {
$this->server->proxy->status = data_get($foundProxyContainer, 'State.Status');
$this->server->save();
$connectProxyToDockerNetworks = connectProxyToNetworks($this->server);
instant_remote_process($connectProxyToDockerNetworks, $this->server, false);
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
send_internal_notification('ContainerStatusJob failed with: ' . $e->getMessage()); send_internal_notification("ContainerStatusJob failed on ({$this->server->id}) with: " . $e->getMessage());
ray($e->getMessage()); ray($e->getMessage());
handleError($e); return handleError($e);
} }
} }
} }

View File

@@ -3,8 +3,6 @@
namespace App\Jobs; namespace App\Jobs;
use App\Models\Server; use App\Models\Server;
use App\Notifications\Server\HighDiskUsage;
use Exception;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
@@ -13,6 +11,7 @@ use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use RuntimeException;
class DockerCleanupJob implements ShouldQueue, ShouldBeEncrypted class DockerCleanupJob implements ShouldQueue, ShouldBeEncrypted
{ {
@@ -35,7 +34,7 @@ class DockerCleanupJob implements ShouldQueue, ShouldBeEncrypted
} }
}); });
if ($isInprogress) { if ($isInprogress) {
throw new Exception('DockerCleanupJob: ApplicationDeploymentQueue is not empty, skipping...'); throw new RuntimeException('DockerCleanupJob: ApplicationDeploymentQueue is not empty, skipping...');
} }
if (!$this->server->isFunctional()) { if (!$this->server->isFunctional()) {
return; return;

View File

@@ -34,8 +34,9 @@ class ServerStatusJob implements ShouldQueue, ShouldBeEncrypted
{ {
ray("checking server status for {$this->server->id}"); ray("checking server status for {$this->server->id}");
try { try {
$this->server->checkServerRediness(); if ($this->server->isServerReady()) {
$this->cleanup(notify: false); $this->cleanup(notify: false);
}
} catch (\Throwable $e) { } catch (\Throwable $e) {
send_internal_notification('ServerStatusJob failed with: ' . $e->getMessage()); send_internal_notification('ServerStatusJob failed with: ' . $e->getMessage());
ray($e->getMessage()); ray($e->getMessage());

View File

@@ -6,6 +6,9 @@ use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Spatie\Activitylog\Models\Activity; use Spatie\Activitylog\Models\Activity;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use RuntimeException;
use Symfony\Component\Yaml\Yaml;
use Visus\Cuid2\Cuid2;
class Application extends BaseModel class Application extends BaseModel
{ {
@@ -45,7 +48,14 @@ class Application extends BaseModel
$application->environment_variables_preview()->delete(); $application->environment_variables_preview()->delete();
}); });
} }
public function link()
{
return route('project.application.configuration', [
'project_uuid' => $this->environment->project->uuid,
'environment_name' => $this->environment->name,
'application_uuid' => $this->uuid
]);
}
public function settings() public function settings()
{ {
return $this->hasOne(ApplicationSetting::class); return $this->hasOne(ApplicationSetting::class);
@@ -123,6 +133,36 @@ class Application extends BaseModel
} }
); );
} }
public function dockerComposeLocation(): Attribute
{
return Attribute::make(
set: function ($value) {
if (is_null($value) || $value === '') {
return '/docker-compose.yaml';
} else {
if ($value !== '/') {
return Str::start(Str::replaceEnd('/', '', $value), '/');
}
return Str::start($value, '/');
}
}
);
}
public function dockerComposePrLocation(): Attribute
{
return Attribute::make(
set: function ($value) {
if (is_null($value) || $value === '') {
return '/docker-compose.yaml';
} else {
if ($value !== '/') {
return Str::start(Str::replaceEnd('/', '', $value), '/');
}
return Str::start($value, '/');
}
}
);
}
public function baseDirectory(): Attribute public function baseDirectory(): Attribute
{ {
return Attribute::make( return Attribute::make(
@@ -157,7 +197,16 @@ class Application extends BaseModel
: explode(',', $this->ports_exposes) : explode(',', $this->ports_exposes)
); );
} }
public function serviceType()
{
$found = str(collect(SPECIFIC_SERVICES)->filter(function ($service) {
return str($this->image)->before(':')->value() === $service;
})->first());
if ($found->isNotEmpty()) {
return $found;
}
return null;
}
public function environment_variables(): HasMany public function environment_variables(): HasMany
{ {
return $this->hasMany(EnvironmentVariable::class)->where('is_preview', false)->orderBy('key', 'asc'); return $this->hasMany(EnvironmentVariable::class)->where('is_preview', false)->orderBy('key', 'asc');
@@ -224,8 +273,8 @@ class Application extends BaseModel
{ {
return $this->morphTo(); return $this->morphTo();
} }
public function isDeploymentInprogress()
public function isDeploymentInprogress() { {
$deployments = ApplicationDeploymentQueue::where('application_id', $this->id)->where('status', 'in_progress')->count(); $deployments = ApplicationDeploymentQueue::where('application_id', $this->id)->where('status', 'in_progress')->count();
if ($deployments > 0) { if ($deployments > 0) {
return true; return true;
@@ -300,6 +349,10 @@ class Application extends BaseModel
} }
return false; return false;
} }
public function isLogDrainEnabled()
{
return data_get($this, 'settings.is_log_drain_enabled', false);
}
public function isConfigurationChanged($save = false) public function isConfigurationChanged($save = false)
{ {
$newConfigHash = $this->fqdn . $this->git_repository . $this->git_branch . $this->git_commit_sha . $this->build_pack . $this->static_image . $this->install_command . $this->build_command . $this->start_command . $this->port_exposes . $this->port_mappings . $this->base_directory . $this->publish_directory . $this->health_check_path . $this->health_check_port . $this->health_check_host . $this->health_check_method . $this->health_check_return_code . $this->health_check_scheme . $this->health_check_response_text . $this->health_check_interval . $this->health_check_timeout . $this->health_check_retries . $this->health_check_start_period . $this->health_check_enabled . $this->limits_memory . $this->limits_swap . $this->limits_swappiness . $this->limits_reservation . $this->limits_cpus . $this->limits_cpuset . $this->limits_cpu_shares . $this->dockerfile . $this->dockerfile_location . $this->custom_labels; $newConfigHash = $this->fqdn . $this->git_repository . $this->git_branch . $this->git_commit_sha . $this->build_pack . $this->static_image . $this->install_command . $this->build_command . $this->start_command . $this->port_exposes . $this->port_mappings . $this->base_directory . $this->publish_directory . $this->health_check_path . $this->health_check_port . $this->health_check_host . $this->health_check_method . $this->health_check_return_code . $this->health_check_scheme . $this->health_check_response_text . $this->health_check_interval . $this->health_check_timeout . $this->health_check_retries . $this->health_check_start_period . $this->health_check_enabled . $this->limits_memory . $this->limits_swap . $this->limits_swappiness . $this->limits_reservation . $this->limits_cpus . $this->limits_cpuset . $this->limits_cpu_shares . $this->dockerfile . $this->dockerfile_location . $this->custom_labels;
@@ -327,4 +380,300 @@ class Application extends BaseModel
return true; return true;
} }
} }
public function isMultipleServerDeployment()
{
if (isDev()) {
return true;
}
if (data_get($this, 'additional_destinations') && data_get($this, 'docker_registry_image_name')) {
return true;
}
return false;
}
public function healthCheckUrl()
{
if ($this->dockerfile || $this->build_pack === 'dockerfile' || $this->build_pack === 'dockerimage') {
return null;
}
if (!$this->health_check_port) {
$health_check_port = $this->ports_exposes_array[0];
} else {
$health_check_port = $this->health_check_port;
}
if ($this->health_check_path) {
$full_healthcheck_url = "{$this->health_check_scheme}://{$this->health_check_host}:{$health_check_port}{$this->health_check_path}";
} else {
$full_healthcheck_url = "{$this->health_check_scheme}://{$this->health_check_host}:{$health_check_port}/";
}
return $full_healthcheck_url;
}
function customRepository()
{
preg_match('/(?<=:)\d+(?=\/)/', $this->git_repository, $matches);
$port = 22;
if (count($matches) === 1) {
$port = $matches[0];
$gitHost = str($this->git_repository)->before(':');
$gitRepo = str($this->git_repository)->after('/');
$repository = "$gitHost:$gitRepo";
} else {
$repository = $this->git_repository;
}
return [
'repository' => $repository,
'port' => $port
];
}
function generateBaseDir(string $uuid)
{
return "/artifacts/{$uuid}";
}
function setGitImportSettings(string $deployment_uuid, string $git_clone_command)
{
$baseDir = $this->generateBaseDir($deployment_uuid);
if ($this->git_commit_sha !== 'HEAD') {
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && git -c advice.detachedHead=false checkout {$this->git_commit_sha} >/dev/null 2>&1";
}
if ($this->settings->is_git_submodules_enabled) {
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && git submodule update --init --recursive";
}
if ($this->settings->is_git_lfs_enabled) {
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && git lfs pull";
}
return $git_clone_command;
}
function generateGitImportCommands(string $deployment_uuid, int $pull_request_id = 0, ?string $git_type = null, bool $exec_in_docker = true, bool $only_checkout = false, ?string $custom_base_dir = null)
{
$branch = $this->git_branch;
['repository' => $customRepository, 'port' => $customPort] = $this->customRepository();
$baseDir = $custom_base_dir ?? $this->generateBaseDir($deployment_uuid);
$commands = collect([]);
$git_clone_command = "git clone -b {$this->git_branch}";
if ($only_checkout) {
$git_clone_command = "git clone --no-checkout -b {$this->git_branch}";
}
if ($pull_request_id !== 0) {
$pr_branch_name = "pr-{$pull_request_id}-coolify";
}
if ($this->deploymentType() === 'source') {
$source_html_url = data_get($this, 'source.html_url');
$url = parse_url(filter_var($source_html_url, FILTER_SANITIZE_URL));
$source_html_url_host = $url['host'];
$source_html_url_scheme = $url['scheme'];
if ($this->source->getMorphClass() == 'App\Models\GithubApp') {
if ($this->source->is_public) {
$fullRepoUrl = "{$this->source->html_url}/{$customRepository}";
$git_clone_command = "{$git_clone_command} {$this->source->html_url}/{$customRepository} {$baseDir}";
if (!$only_checkout) {
$git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command);
}
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, $git_clone_command));
} else {
$commands->push($git_clone_command);
}
} else {
$github_access_token = generate_github_installation_token($this->source);
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, "{$git_clone_command} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}.git {$baseDir}"));
$fullRepoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}.git";
} else {
$commands->push("{$git_clone_command} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository} {$baseDir}");
$fullRepoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}";
}
}
if ($pull_request_id !== 0) {
$branch = "pull/{$pull_request_id}/head:$pr_branch_name";
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, "cd {$baseDir} && git fetch origin {$branch} && git checkout $pr_branch_name"));
} else {
$commands->push("cd {$baseDir} && git fetch origin {$branch} && git checkout $pr_branch_name");
}
}
return [
'commands' => $commands->implode(' && '),
'branch' => $branch,
'fullRepoUrl' => $fullRepoUrl
];
}
}
if ($this->deploymentType() === 'deploy_key') {
$fullRepoUrl = $customRepository;
$private_key = data_get($this, 'private_key.private_key');
if (is_null($private_key)) {
throw new RuntimeException('Private key not found. Please add a private key to the application and try again.');
}
$private_key = base64_encode($private_key);
$git_clone_command_base = "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" {$git_clone_command} {$customRepository} {$baseDir}";
if (!$only_checkout) {
$git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command_base);
}
if ($exec_in_docker) {
$commands = collect([
executeInDocker($deployment_uuid, "mkdir -p /root/.ssh"),
executeInDocker($deployment_uuid, "echo '{$private_key}' | base64 -d > /root/.ssh/id_rsa"),
executeInDocker($deployment_uuid, "chmod 600 /root/.ssh/id_rsa"),
]);
} else {
$commands = collect([
"mkdir -p /root/.ssh",
"echo '{$private_key}' | base64 -d > /root/.ssh/id_rsa",
"chmod 600 /root/.ssh/id_rsa",
]);
}
if ($pull_request_id !== 0) {
if ($git_type === 'gitlab') {
$branch = "merge-requests/{$pull_request_id}/head:$pr_branch_name";
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
} else {
$commands->push("echo 'Checking out $branch'");
}
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && git checkout $pr_branch_name";
}
if ($git_type === 'github') {
$branch = "pull/{$pull_request_id}/head:$pr_branch_name";
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
} else {
$commands->push("echo 'Checking out $branch'");
}
$git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && git checkout $pr_branch_name";
}
}
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, $git_clone_command));
} else {
$commands->push($git_clone_command);
}
return [
'commands' => $commands->implode(' && '),
'branch' => $branch,
'fullRepoUrl' => $fullRepoUrl
];
}
if ($this->deploymentType() === 'other') {
$fullRepoUrl = $customRepository;
$git_clone_command = "{$git_clone_command} {$customRepository} {$baseDir}";
$git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command);
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, $git_clone_command));
} else {
$commands->push($git_clone_command);
}
return [
'commands' => $commands->implode(' && '),
'branch' => $branch,
'fullRepoUrl' => $fullRepoUrl
];
}
}
public function prepareHelperImage(string $deploymentUuid)
{
$basedir = $this->generateBaseDir($deploymentUuid);
$helperImage = config('coolify.helper_image');
$server = data_get($this, 'destination.server');
$network = data_get($this, 'destination.network');
$serverUserHomeDir = instant_remote_process(["echo \$HOME"], $server);
$dockerConfigFileExists = instant_remote_process(["test -f {$serverUserHomeDir}/.docker/config.json && echo 'OK' || echo 'NOK'"], $server);
$commands = collect([]);
if ($dockerConfigFileExists === 'OK') {
$commands->push([
"command" => "docker run -d --network $network -v /:/host --name $deploymentUuid --rm -v {$serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock $helperImage",
"hidden" => true,
]);
} else {
$commands->push([
"command" => "docker run -d --network {$network} -v /:/host --name {$deploymentUuid} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}",
"hidden" => true,
]);
}
$commands->push([
"command" => executeInDocker($deploymentUuid, "mkdir -p {$basedir}"),
"hidden" => true,
]);
return $commands;
}
function parseCompose(int $pull_request_id = 0)
{
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);
}
return $mainCompose;
} else {
return collect([]);
}
}
function loadComposeFile($isInit = false)
{
$initialDockerComposeLocation = $this->docker_compose_location;
// $initialDockerComposePrLocation = $this->docker_compose_pr_location;
if ($this->build_pack === 'dockercompose') {
if ($isInit && $this->docker_compose_raw) {
return;
}
$uuid = new Cuid2();
['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([".$workdir$composeFile"]);
// if ($composeFile !== $prComposeFile) {
// $fileList->push(".$prComposeFile");
// }
$commands = collect([
"mkdir -p /tmp/{$uuid} && cd /tmp/{$uuid}",
$cloneCommand,
"git sparse-checkout init --cone",
"git sparse-checkout set {$fileList->implode(' ')}",
"git read-tree -mu HEAD",
"cat .$workdir$composeFile",
]);
$composeFileContent = instant_remote_process($commands, $this->destination->server, false);
if (!$composeFileContent) {
$this->docker_compose_location = $initialDockerComposeLocation;
$this->save();
throw new \Exception("Could not load base compose file from $workdir$composeFile");
} else {
$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();
// }
// }
$commands = collect([
"rm -rf /tmp/{$uuid}",
]);
instant_remote_process($commands, $this->destination->server, false);
return [
'parsedServices' => $this->parseCompose(),
'initialDockerComposeLocation' => $this->docker_compose_location,
'initialDockerComposePrLocation' => $this->docker_compose_pr_location,
];
}
}
} }

View File

@@ -3,8 +3,48 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Carbon;
class ApplicationDeploymentQueue extends Model class ApplicationDeploymentQueue extends Model
{ {
protected $guarded = []; protected $guarded = [];
public function getOutput($name)
{
if (!$this->logs) {
return null;
}
return collect(json_decode($this->logs))->where('name', $name)->first()?->output ?? null;
}
public function addLogEntry(string $message, string $type = 'stdout', bool $hidden = false)
{
if ($type === 'error') {
$type = 'stderr';
}
$message = str($message)->trim();
if ($message->startsWith('╔')) {
$message = "\n" . $message;
}
$newLogEntry = [
'command' => null,
'output' => remove_iip($message),
'type' => $type,
'timestamp' => Carbon::now('UTC'),
'hidden' => $hidden,
'batch' => 1,
];
if ($this->logs) {
$previousLogs = json_decode($this->logs, associative: true, flags: JSON_THROW_ON_ERROR);
$newLogEntry['order'] = count($previousLogs) + 1;
$previousLogs[] = $newLogEntry;
$this->update([
'logs' => json_encode($previousLogs, flags: JSON_THROW_ON_ERROR),
]);
} else {
$this->update([
'logs' => json_encode([$newLogEntry], flags: JSON_THROW_ON_ERROR),
]);
}
}
} }

View File

@@ -5,7 +5,26 @@ namespace App\Models;
class ApplicationPreview extends BaseModel class ApplicationPreview extends BaseModel
{ {
protected $guarded = []; protected $guarded = [];
protected static function booted()
{
static::deleting(function ($preview) {
if ($preview->application->build_pack === 'dockercompose') {
$server = $preview->application->destination->server;
$composeFile = $preview->application->parseCompose(pull_request_id: $preview->pull_request_id);
$volumes = data_get($composeFile, 'volumes');
$networks = data_get($composeFile, 'networks');
$networkKeys = collect($networks)->keys();
$volumeKeys = collect($volumes)->keys();
$volumeKeys->each(function ($key) use ($server) {
instant_remote_process(["docker volume rm -f $key"], $server, false);
});
$networkKeys->each(function ($key) use ($server) {
instant_remote_process(["docker network disconnect $key coolify-proxy"], $server, false);
instant_remote_process(["docker network rm $key"], $server, false);
});
}
});
}
static function findPreviewByApplicationAndPullId(int $application_id, int $pull_request_id) static function findPreviewByApplicationAndPullId(int $application_id, int $pull_request_id)
{ {
return self::where('application_id', $application_id)->where('pull_request_id', $pull_request_id)->firstOrFail(); return self::where('application_id', $application_id)->where('pull_request_id', $pull_request_id)->firstOrFail();

View File

@@ -3,8 +3,10 @@
namespace App\Models; namespace App\Models;
use App\Notifications\Channels\SendsEmail; use App\Notifications\Channels\SendsEmail;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
use Spatie\Url\Url;
class InstanceSettings extends Model implements SendsEmail class InstanceSettings extends Model implements SendsEmail
{ {
@@ -16,6 +18,18 @@ class InstanceSettings extends Model implements SendsEmail
'smtp_password' => 'encrypted', 'smtp_password' => 'encrypted',
]; ];
public function fqdn(): Attribute
{
return Attribute::make(
set: function ($value) {
if ($value) {
$url = Url::fromString($value);
$host = $url->getHost();
return $url->getScheme() . '://' . $host;
}
}
);
}
public static function get() public static function get()
{ {
return InstanceSettings::findOrFail(0); return InstanceSettings::findOrFail(0);

View File

@@ -2,22 +2,26 @@
namespace App\Models; namespace App\Models;
use App\Actions\Server\InstallLogDrain; use App\Enums\ApplicationDeploymentStatus;
use App\Actions\Server\InstallNewRelic;
use App\Enums\ProxyStatus; use App\Enums\ProxyStatus;
use App\Enums\ProxyTypes; use App\Enums\ProxyTypes;
use App\Notifications\Server\Revived; use App\Notifications\Server\Revived;
use App\Notifications\Server\Unreachable; use App\Notifications\Server\Unreachable;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Sleep; 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 Illuminate\Support\Stringable;
class Server extends BaseModel class Server extends BaseModel
{ {
use SchemalessAttributesTrait; use SchemalessAttributesTrait;
public static $batch_counter = 0;
protected static function booted() protected static function booted()
{ {
@@ -31,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) {
@@ -78,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()
@@ -97,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');
@@ -128,15 +151,17 @@ class Server extends BaseModel
} }
return false; return false;
} }
public function checkServerRediness() public function isServerReady()
{ {
$serverUptimeCheckNumber = $this->unreachable_count; $serverUptimeCheckNumber = $this->unreachable_count;
$serverUptimeCheckNumberMax = 3; $serverUptimeCheckNumberMax = 3;
$currentTime = now()->timestamp; $currentTime = now()->timestamp;
$runtime5Minutes = 1 * 60; $runtime = 30;
// Run for 1 minutes max and check every 5 seconds for 3 times
while ($currentTime + $runtime5Minutes > now()->timestamp) { $isReady = false;
// Run for 30 seconds max and check every 5 seconds for 3 times
while ($currentTime + $runtime > now()->timestamp) {
if ($serverUptimeCheckNumber >= $serverUptimeCheckNumberMax) { if ($serverUptimeCheckNumber >= $serverUptimeCheckNumberMax) {
if ($this->unreachable_notification_sent === false) { if ($this->unreachable_notification_sent === false) {
ray('Server unreachable, sending notification...'); ray('Server unreachable, sending notification...');
@@ -165,10 +190,11 @@ class Server extends BaseModel
$db->update(['status' => 'exited']); $db->update(['status' => 'exited']);
} }
} }
throw new \Exception('Server is not reachable.'); $isReady = false;
break;
} }
$result = $this->validateConnection(); $result = $this->validateConnection();
ray('validateConnection: ' . $result); // ray('validateConnection: ' . $result);
if (!$result) { if (!$result) {
$serverUptimeCheckNumber++; $serverUptimeCheckNumber++;
$this->update([ $this->update([
@@ -177,14 +203,22 @@ class Server extends BaseModel
Sleep::for(5)->seconds(); Sleep::for(5)->seconds();
return; return;
} }
$isReady = true;
break; break;
} }
return true; return $isReady;
} }
public function getDiskUsage() public function getDiskUsage()
{ {
return instant_remote_process(["df /| tail -1 | awk '{ print $5}' | sed 's/%//g'"], $this, false); return instant_remote_process(["df /| tail -1 | awk '{ print $5}' | sed 's/%//g'"], $this, false);
} }
public function definedResources()
{
$applications = $this->applications();
$databases = $this->databases();
$services = $this->services();
return $applications->concat($databases)->concat($services->get());
}
public function hasDefinedResources() public function hasDefinedResources()
{ {
$applications = $this->applications()->count() > 0; $applications = $this->applications()->count() > 0;
@@ -213,6 +247,23 @@ class Server extends BaseModel
return $standaloneDocker->applications; return $standaloneDocker->applications;
})->flatten(); })->flatten();
} }
public function dockerComposeBasedApplications()
{
return $this->applications()->filter(function ($application) {
return data_get($application, 'build_pack') === 'dockercompose';
});
}
public function dockerComposeBasedPreviewDeployments()
{
return $this->previews()->filter(function ($preview) {
$applicationId = data_get($preview, 'application_id');
$application = Application::find($applicationId);
if (!$application) {
return false;
}
return data_get($application, 'build_pack') === 'dockercompose';
});
}
public function services() public function services()
{ {
return $this->hasMany(Service::class); return $this->hasMany(Service::class);
@@ -292,49 +343,69 @@ class Server extends BaseModel
// } // }
return true; return true;
} }
public function logDrain($type)
{
InstallLogDrain::run($this, $type);
}
public function isFunctional() public function isFunctional()
{ {
return $this->settings->is_reachable && $this->settings->is_usable; return $this->settings->is_reachable && $this->settings->is_usable;
} }
public function isDrainLogActivated() public function isLogDrainEnabled()
{ {
return $this->settings->is_logdrain_newrelic_enabled || $this->settings->is_logdrain_highlight_enabled || $this->settings->is_logdrain_axiom_enabled; return $this->settings->is_logdrain_newrelic_enabled || $this->settings->is_logdrain_highlight_enabled || $this->settings->is_logdrain_axiom_enabled;
} }
public function validateOS(): bool | Stringable
{
$os_release = instant_remote_process(['cat /etc/os-release'], $this);
$datas = collect(explode("\n", $os_release));
$collectedData = collect([]);
foreach ($datas as $data) {
$item = Str::of($data)->trim();
$collectedData->put($item->before('=')->value(), $item->after('=')->lower()->replace('"', '')->value());
}
$ID = data_get($collectedData, 'ID');
// $ID_LIKE = data_get($collectedData, 'ID_LIKE');
// $VERSION_ID = data_get($collectedData, 'VERSION_ID');
$supported = collect(SUPPORTED_OS)->filter(function ($supportedOs) use ($ID) {
if (str($supportedOs)->contains($ID)) {
return str($ID);
}
});
if ($supported->count() === 1) {
ray('supported');
return str($supported->first());
} else {
ray('not supported');
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([
'is_reachable' => false, 'is_reachable' => false,
'is_usable' => false
]); ]);
return false; return false;
} else {
$this->settings()->update([
'is_reachable' => true,
]);
$server->update([
'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]);
} }
if (
data_get($this, 'settings.is_reachable') === false ||
data_get($this, 'settings.is_usable') === false
) {
$this->settings()->update([
'is_reachable' => true,
'is_usable' => true
]);
}
$this->update([
'unreachable_count' => 0,
]);
return true; return true;
} }
public function validateDockerEngine($throwError = false) public function validateDockerEngine($throwError = false)
@@ -344,13 +415,26 @@ class Server extends BaseModel
$this->settings->is_usable = false; $this->settings->is_usable = false;
$this->settings->save(); $this->settings->save();
if ($throwError) { if ($throwError) {
throw new \Exception('Server is not usable.'); throw new \Exception('Server is not usable. Docker Engine is not installed.');
} }
return false; return false;
} }
$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()
@@ -362,12 +446,96 @@ class Server extends BaseModel
$this->settings->save(); $this->settings->save();
return false; return false;
} }
$this->settings->is_reachable = true;
$this->settings->is_usable = true; $this->settings->is_usable = true;
$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)
{
static::$batch_counter++;
foreach ($commands as $command) {
$realCommand = data_get($command, 'command');
if (is_null($realCommand)) {
throw new \RuntimeException('Command is not set');
}
$hidden = data_get($command, 'hidden', false);
$ignoreErrors = data_get($command, 'ignoreErrors', false);
$customOutputType = data_get($command, 'customOutputType');
$name = data_get($command, 'name');
$remoteCommand = generateSshCommand($this, $realCommand);
$process = Process::timeout(3600)->idleTimeout(3600)->start($remoteCommand, function (string $type, string $output) use ($realCommand, $hidden, $customOutputType, $loggingModel, $name) {
$output = str($output)->trim();
if ($output->startsWith('╔')) {
$output = "\n" . $output;
}
$newLogEntry = [
'command' => remove_iip($realCommand),
'output' => remove_iip($output),
'type' => $customOutputType ?? $type === 'err' ? 'stderr' : 'stdout',
'timestamp' => Carbon::now('UTC'),
'hidden' => $hidden,
'batch' => static::$batch_counter,
];
if ($loggingModel) {
if (!$loggingModel->logs) {
$newLogEntry['order'] = 1;
} else {
$previousLogs = json_decode($loggingModel->logs, associative: true, flags: JSON_THROW_ON_ERROR);
$newLogEntry['order'] = count($previousLogs) + 1;
}
if ($name) {
$newLogEntry['name'] = $name;
}
$previousLogs[] = $newLogEntry;
$loggingModel->logs = json_encode($previousLogs, flags: JSON_THROW_ON_ERROR);
$loggingModel->save();
}
});
if ($loggingModel) {
$loggingModel->update([
'current_process_id' => $process->id(),
]);
}
$processResult = $process->wait();
if ($processResult->exitCode() !== 0) {
if (!$ignoreErrors) {
if ($loggingModel) {
$status = ApplicationDeploymentStatus::FAILED->value;
$loggingModel->status = $status;
$loggingModel->save();
}
throw new \RuntimeException($processResult->errorOutput());
}
}
}
}
public function stopApplicationRelatedRunningContainers(string $applicationId, string $containerName)
{
$containers = getCurrentApplicationContainerStatus($this, $applicationId, 0);
$containers = $containers->filter(function ($container) use ($containerName) {
return data_get($container, 'Names') !== $containerName;
});
$containers->each(function ($container) {
$removableContainer = data_get($container, 'Names');
$this->server->executeRemoteCommand(
commands: collect([
'command' => "docker rm -f $removableContainer >/dev/null 2>&1",
'hidden' => true,
'ignoreErrors' => true
]),
loggingModel: $this->deploymentQueueEntry
);
});
} }
} }

View File

@@ -52,51 +52,145 @@ class Service extends BaseModel
foreach ($applications as $application) { foreach ($applications as $application) {
$image = str($application->image)->before(':')->value(); $image = str($application->image)->before(':')->value();
switch ($image) { switch ($image) {
case str($image)->contains('minio'): case str($image)?->contains('minio'):
$data = collect([]);
$console_url = $this->environment_variables()->where('key', 'MINIO_BROWSER_REDIRECT_URL')->first(); $console_url = $this->environment_variables()->where('key', 'MINIO_BROWSER_REDIRECT_URL')->first();
$s3_api_url = $this->environment_variables()->where('key', 'MINIO_SERVER_URL')->first(); $s3_api_url = $this->environment_variables()->where('key', 'MINIO_SERVER_URL')->first();
$admin_user = $this->environment_variables()->where('key', 'SERVICE_USER_MINIO')->first(); $admin_user = $this->environment_variables()->where('key', 'SERVICE_USER_MINIO')->first();
if (is_null($admin_user)) {
$admin_user = $this->environment_variables()->where('key', 'MINIO_ROOT_USER')->first();
}
$admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_MINIO')->first(); $admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_MINIO')->first();
$fields->put('MinIO', [ if (is_null($admin_password)) {
'Console URL' => [ $admin_password = $this->environment_variables()->where('key', 'MINIO_ROOT_PASSWORD')->first();
'key' => data_get($console_url, 'key'), }
'value' => data_get($console_url, 'value'),
'rules' => 'required|url', if ($console_url) {
], $data = $data->merge([
'S3 API URL' => [ 'Console URL' => [
'key' => data_get($s3_api_url, 'key'), 'key' => data_get($console_url, 'key'),
'value' => data_get($s3_api_url, 'value'), 'value' => data_get($console_url, 'value'),
'rules' => 'required|url', 'rules' => 'required|url',
], ],
'Admin User' => [ ]);
'key' => data_get($admin_user, 'key'), }
'value' => data_get($admin_user, 'value'), if ($s3_api_url) {
'rules' => 'required', $data = $data->merge([
], 'S3 API URL' => [
'Admin Password' => [ 'key' => data_get($s3_api_url, 'key'),
'key' => data_get($admin_password, 'key'), 'value' => data_get($s3_api_url, 'value'),
'value' => data_get($admin_password, 'value'), 'rules' => 'required|url',
'rules' => 'required', ],
'isPassword' => true, ]);
], }
]); if ($admin_user) {
$data = $data->merge([
'Admin User' => [
'key' => data_get($admin_user, 'key'),
'value' => data_get($admin_user, 'value'),
'rules' => 'required',
],
]);
}
if ($admin_password) {
$data = $data->merge([
'Admin Password' => [
'key' => data_get($admin_password, 'key'),
'value' => data_get($admin_password, 'value'),
'rules' => 'required',
'isPassword' => true,
],
]);
}
$fields->put('MinIO', $data->toArray());
break; break;
case str($image)->contains('weblate'): case str($image)?->contains('weblate'):
$data = collect([]);
$admin_email = $this->environment_variables()->where('key', 'WEBLATE_ADMIN_EMAIL')->first(); $admin_email = $this->environment_variables()->where('key', 'WEBLATE_ADMIN_EMAIL')->first();
$admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_WEBLATE')->first(); $admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_WEBLATE')->first();
$fields->put('Weblate', [
'Admin Email' => [ if ($admin_email) {
'key' => data_get($admin_email, 'key'), $data = $data->merge([
'value' => data_get($admin_email, 'value'), 'Admin Email' => [
'rules' => 'required|email', 'key' => data_get($admin_email, 'key'),
], 'value' => data_get($admin_email, 'value'),
'Admin Password' => [ 'rules' => 'required|email',
'key' => data_get($admin_password, 'key'), ],
'value' => data_get($admin_password, 'value'), ]);
'rules' => 'required', }
'isPassword' => true, if ($admin_password) {
], $data = $data->merge([
]); 'Admin Password' => [
'key' => data_get($admin_password, 'key'),
'value' => data_get($admin_password, 'value'),
'rules' => 'required',
'isPassword' => true,
],
]);
}
$fields->put('Weblate', $data);
break;
case str($image)?->contains('ghost'):
$data = collect([]);
$MAIL_OPTIONS_AUTH_PASS = $this->environment_variables()->where('key', 'MAIL_OPTIONS_AUTH_PASS')->first();
$MAIL_OPTIONS_AUTH_USER = $this->environment_variables()->where('key', 'MAIL_OPTIONS_AUTH_USER')->first();
$MAIL_OPTIONS_SECURE = $this->environment_variables()->where('key', 'MAIL_OPTIONS_SECURE')->first();
$MAIL_OPTIONS_PORT = $this->environment_variables()->where('key', 'MAIL_OPTIONS_PORT')->first();
$MAIL_OPTIONS_SERVICE = $this->environment_variables()->where('key', 'MAIL_OPTIONS_SERVICE')->first();
$MAIL_OPTIONS_HOST = $this->environment_variables()->where('key', 'MAIL_OPTIONS_HOST')->first();
if ($MAIL_OPTIONS_AUTH_PASS) {
$data = $data->merge([
'Mail Password' => [
'key' => data_get($MAIL_OPTIONS_AUTH_PASS, 'key'),
'value' => data_get($MAIL_OPTIONS_AUTH_PASS, 'value'),
'isPassword' => true,
],
]);
}
if ($MAIL_OPTIONS_AUTH_USER) {
$data = $data->merge([
'Mail User' => [
'key' => data_get($MAIL_OPTIONS_AUTH_USER, 'key'),
'value' => data_get($MAIL_OPTIONS_AUTH_USER, 'value'),
],
]);
}
if ($MAIL_OPTIONS_SECURE) {
$data = $data->merge([
'Mail Secure' => [
'key' => data_get($MAIL_OPTIONS_SECURE, 'key'),
'value' => data_get($MAIL_OPTIONS_SECURE, 'value'),
],
]);
}
if ($MAIL_OPTIONS_PORT) {
$data = $data->merge([
'Mail Port' => [
'key' => data_get($MAIL_OPTIONS_PORT, 'key'),
'value' => data_get($MAIL_OPTIONS_PORT, 'value'),
],
]);
}
if ($MAIL_OPTIONS_SERVICE) {
$data = $data->merge([
'Mail Service' => [
'key' => data_get($MAIL_OPTIONS_SERVICE, 'key'),
'value' => data_get($MAIL_OPTIONS_SERVICE, 'value'),
],
]);
}
if ($MAIL_OPTIONS_HOST) {
$data = $data->merge([
'Mail Host' => [
'key' => data_get($MAIL_OPTIONS_HOST, 'key'),
'value' => data_get($MAIL_OPTIONS_HOST, 'value'),
],
]);
}
$fields->put('Ghost', $data);
break;
} }
} }
$databases = $this->databases()->get(); $databases = $this->databases()->get();
@@ -267,6 +361,14 @@ class Service extends BaseModel
} }
} }
} }
public function link()
{
return route('project.service.configuration', [
'project_uuid' => $this->environment->project->uuid,
'environment_name' => $this->environment->name,
'service_uuid' => $this->uuid
]);
}
public function documentation() public function documentation()
{ {
$services = getServiceTemplates(); $services = getServiceTemplates();
@@ -338,508 +440,12 @@ class Service extends BaseModel
public function parse(bool $isNew = false): Collection public function parse(bool $isNew = false): Collection
{ {
// ray()->clearAll(); return parseDockerComposeFile($this, $isNew);
if ($this->docker_compose_raw) { }
try { public function networks()
$yaml = Yaml::parse($this->docker_compose_raw); {
} catch (\Exception $e) { $networks = getTopLevelNetworks($this);
throw new \Exception($e->getMessage()); // ray($networks);
} return $networks;
$topLevelVolumes = collect(data_get($yaml, 'volumes', []));
$topLevelNetworks = collect(data_get($yaml, 'networks', []));
$dockerComposeVersion = data_get($yaml, 'version') ?? '3.8';
$services = data_get($yaml, 'services');
$generatedServiceFQDNS = collect([]);
if (is_null($this->destination)) {
$destination = $this->server->destinations()->first();
if ($destination) {
$this->destination()->associate($destination);
$this->save();
}
}
$definedNetwork = collect([$this->uuid]);
$services = collect($services)->map(function ($service, $serviceName) use ($topLevelVolumes, $topLevelNetworks, $definedNetwork, $isNew, $generatedServiceFQDNS) {
$serviceVolumes = collect(data_get($service, 'volumes', []));
$servicePorts = collect(data_get($service, 'ports', []));
$serviceNetworks = collect(data_get($service, 'networks', []));
$serviceVariables = collect(data_get($service, 'environment', []));
$serviceLabels = collect(data_get($service, 'labels', []));
$containerName = "$serviceName-{$this->uuid}";
// Decide if the service is a database
$isDatabase = false;
$image = data_get_str($service, 'image');
if ($image->contains(':')) {
$image = Str::of($image);
} else {
$image = Str::of($image)->append(':latest');
}
$imageName = $image->before(':');
if (collect(DATABASE_DOCKER_IMAGES)->contains($imageName)) {
$isDatabase = true;
}
data_set($service, 'is_database', $isDatabase);
// Create new serviceApplication or serviceDatabase
if ($isDatabase) {
if ($isNew) {
$savedService = ServiceDatabase::create([
'name' => $serviceName,
'image' => $image,
'service_id' => $this->id
]);
} else {
$savedService = ServiceDatabase::where([
'name' => $serviceName,
'service_id' => $this->id
])->first();
}
} else {
if ($isNew) {
$savedService = ServiceApplication::create([
'name' => $serviceName,
'image' => $image,
'service_id' => $this->id
]);
} else {
$savedService = ServiceApplication::where([
'name' => $serviceName,
'service_id' => $this->id
])->first();
}
}
if (is_null($savedService)) {
if ($isDatabase) {
$savedService = ServiceDatabase::create([
'name' => $serviceName,
'image' => $image,
'service_id' => $this->id
]);
} else {
$savedService = ServiceApplication::create([
'name' => $serviceName,
'image' => $image,
'service_id' => $this->id
]);
}
}
// Check if image changed
if ($savedService->image !== $image) {
$savedService->image = $image;
$savedService->save();
}
// Collect/create/update networks
if ($serviceNetworks->count() > 0) {
foreach ($serviceNetworks as $networkName => $networkDetails) {
$networkExists = $topLevelNetworks->contains(function ($value, $key) use ($networkName) {
return $value == $networkName || $key == $networkName;
});
if (!$networkExists) {
$topLevelNetworks->put($networkDetails, null);
}
}
}
// Collect/create/update ports
$collectedPorts = collect([]);
if ($servicePorts->count() > 0) {
foreach ($servicePorts as $sport) {
if (is_string($sport) || is_numeric($sport)) {
$collectedPorts->push($sport);
}
if (is_array($sport)) {
$target = data_get($sport, 'target');
$published = data_get($sport, 'published');
$protocol = data_get($sport, 'protocol');
$collectedPorts->push("$target:$published/$protocol");
}
}
}
$savedService->ports = $collectedPorts->implode(',');
$savedService->save();
// Add Coolify specific networks
$definedNetworkExists = $topLevelNetworks->contains(function ($value, $_) use ($definedNetwork) {
return $value == $definedNetwork;
});
if (!$definedNetworkExists) {
foreach ($definedNetwork as $network) {
$topLevelNetworks->put($network, [
'name' => $network,
'external' => true
]);
}
}
$networks = collect();
foreach ($serviceNetworks as $key => $serviceNetwork) {
if (gettype($serviceNetwork) === 'string') {
// networks:
// - appwrite
$networks->put($serviceNetwork, null);
} else if (gettype($serviceNetwork) === 'array') {
// networks:
// default:
// ipv4_address: 192.168.203.254
// $networks->put($serviceNetwork, null);
ray($key);
$networks->put($key, $serviceNetwork);
}
}
foreach ($definedNetwork as $key => $network) {
$networks->put($network, null);
}
data_set($service, 'networks', $networks->toArray());
// Collect/create/update volumes
if ($serviceVolumes->count() > 0) {
$serviceVolumes = $serviceVolumes->map(function ($volume) use ($savedService, $topLevelVolumes) {
$type = null;
$source = null;
$target = null;
$content = null;
$isDirectory = false;
if (is_string($volume)) {
$source = Str::of($volume)->before(':');
$target = Str::of($volume)->after(':')->beforeLast(':');
if ($source->startsWith('./') || $source->startsWith('/') || $source->startsWith('~')) {
$type = Str::of('bind');
} else {
$type = Str::of('volume');
}
} else if (is_array($volume)) {
$type = data_get_str($volume, 'type');
$source = data_get_str($volume, 'source');
$target = data_get_str($volume, 'target');
$content = data_get($volume, 'content');
$isDirectory = (bool) data_get($volume, 'isDirectory', false);
$foundConfig = $savedService->fileStorages()->whereMountPath($target)->first();
if ($foundConfig) {
$contentNotNull = data_get($foundConfig, 'content');
if ($contentNotNull) {
$content = $contentNotNull;
}
$isDirectory = (bool) data_get($foundConfig, 'is_directory');
}
}
if ($type->value() === 'bind') {
if ($source->value() === "/var/run/docker.sock") {
return $volume;
}
if ($source->value() === '/tmp' || $source->value() === '/tmp/') {
return $volume;
}
LocalFileVolume::updateOrCreate(
[
'mount_path' => $target,
'resource_id' => $savedService->id,
'resource_type' => get_class($savedService)
],
[
'fs_path' => $source,
'mount_path' => $target,
'content' => $content,
'is_directory' => $isDirectory,
'resource_id' => $savedService->id,
'resource_type' => get_class($savedService)
]
);
} else if ($type->value() === 'volume') {
$slugWithoutUuid = Str::slug($source, '-');
$name = "{$savedService->service->uuid}_{$slugWithoutUuid}";
if (is_string($volume)) {
$source = Str::of($volume)->before(':');
$target = Str::of($volume)->after(':')->beforeLast(':');
$source = $name;
$volume = "$source:$target";
} else if (is_array($volume)) {
data_set($volume, 'source', $name);
}
$topLevelVolumes->put($name, [
'name' => $name,
]);
LocalPersistentVolume::updateOrCreate(
[
'mount_path' => $target,
'resource_id' => $savedService->id,
'resource_type' => get_class($savedService)
],
[
'name' => $name,
'mount_path' => $target,
'resource_id' => $savedService->id,
'resource_type' => get_class($savedService)
]
);
}
$savedService->getFilesFromServer(isInit: true);
return $volume;
});
data_set($service, 'volumes', $serviceVolumes->toArray());
}
// Add env_file with at least .env to the service
// $envFile = collect(data_get($service, 'env_file', []));
// if ($envFile->count() > 0) {
// if (!$envFile->contains('.env')) {
// $envFile->push('.env');
// }
// } else {
// $envFile = collect(['.env']);
// }
// data_set($service, 'env_file', $envFile->toArray());
// Get variables from the service
foreach ($serviceVariables as $variableName => $variable) {
if (is_numeric($variableName)) {
$variable = Str::of($variable);
if ($variable->contains('=')) {
// - SESSION_SECRET=123
// - SESSION_SECRET=
$key = $variable->before('=');
$value = $variable->after('=');
} else {
// - SESSION_SECRET
$key = $variable;
$value = null;
}
} else {
// SESSION_SECRET: 123
// SESSION_SECRET:
$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();
$fqdn = generateFqdn($this->server, "{$name->value()}-{$this->uuid}");
if (substr_count($key->value(), '_') === 3) {
// SERVICE_FQDN_UMAMI_1000
$port = $key->afterLast('_');
} else {
// SERVICE_FQDN_UMAMI
$port = null;
}
if ($port) {
$fqdn = "$fqdn:$port";
}
if (substr_count($key->value(), '_') >= 2) {
if (is_null($value)) {
$value = Str::of('/');
}
$path = $value->value();
if ($generatedServiceFQDNS->count() > 0) {
$alreadyGenerated = $generatedServiceFQDNS->has($key->value());
if ($alreadyGenerated) {
$fqdn = $generatedServiceFQDNS->get($key->value());
} else {
$generatedServiceFQDNS->put($key->value(), $fqdn);
}
} else {
$generatedServiceFQDNS->put($key->value(), $fqdn);
}
$fqdn = "$fqdn$path";
}
if (!$isDatabase) {
if ($savedService->fqdn) {
$fqdn = $savedService->fqdn . ',' . $fqdn;
} else {
$fqdn = $fqdn;
}
$savedService->fqdn = $fqdn;
$savedService->save();
}
}
// data_forget($service, "environment.$variableName");
// $yaml = data_forget($yaml, "services.$serviceName.environment.$variableName");
// if (count(data_get($yaml, 'services.' . $serviceName . '.environment')) === 0) {
// $yaml = data_forget($yaml, "services.$serviceName.environment");
// }
continue;
}
if ($value?->startsWith('$')) {
$value = Str::of(replaceVariables($value));
$key = $value;
$foundEnv = EnvironmentVariable::where([
'key' => $key,
'service_id' => $this->id,
])->first();
if ($value->startsWith('SERVICE_')) {
// Count _ in $value
$count = substr_count($value->value(), '_');
if ($count === 2) {
// SERVICE_FQDN_UMAMI
$command = $value->after('SERVICE_')->beforeLast('_');
$forService = $value->afterLast('_');
$generatedValue = null;
$port = null;
}
if ($count === 3) {
// SERVICE_FQDN_UMAMI_1000
$command = $value->after('SERVICE_')->before('_');
$forService = $value->after('SERVICE_')->after('_')->before('_');
$generatedValue = null;
$port = $value->afterLast('_');
}
if ($command->value() === 'FQDN' || $command->value() === 'URL') {
if (Str::lower($forService) === $serviceName) {
$fqdn = generateFqdn($this->server, $containerName);
} else {
$fqdn = generateFqdn($this->server, Str::lower($forService) . '-' . $this->uuid);
}
if ($port) {
$fqdn = "$fqdn:$port";
}
if ($foundEnv) {
$fqdn = data_get($foundEnv, 'value');
} else {
if ($command->value() === 'URL') {
$fqdn = Str::of($fqdn)->after('://')->value();
}
EnvironmentVariable::create([
'key' => $key,
'value' => $fqdn,
'is_build_time' => false,
'service_id' => $this->id,
'is_preview' => false,
]);
}
if (!$isDatabase) {
if ($command->value() === 'FQDN' && is_null($savedService->fqdn)) {
$savedService->fqdn = $fqdn;
$savedService->save();
}
}
} else {
switch ($command) {
case 'PASSWORD':
$generatedValue = Str::password(symbols: false);
break;
case 'PASSWORD_64':
$generatedValue = Str::password(length: 64, symbols: false);
break;
case 'BASE64_64':
$generatedValue = Str::random(64);
break;
case 'BASE64_128':
$generatedValue = Str::random(128);
break;
case 'BASE64':
$generatedValue = Str::random(32);
break;
case 'USER':
$generatedValue = Str::random(16);
break;
}
if (!$foundEnv) {
EnvironmentVariable::create([
'key' => $key,
'value' => $generatedValue,
'is_build_time' => false,
'service_id' => $this->id,
'is_preview' => false,
]);
}
}
} else {
if ($value->contains(':-')) {
$key = $value->before(':');
$defaultValue = $value->after(':-');
} else if ($value->contains('-')) {
$key = $value->before('-');
$defaultValue = $value->after('-');
} else if ($value->contains(':?')) {
$key = $value->before(':');
$defaultValue = $value->after(':?');
} else if ($value->contains('?')) {
$key = $value->before('?');
$defaultValue = $value->after('?');
} else {
$key = $value;
$defaultValue = null;
}
if ($foundEnv) {
$defaultValue = data_get($foundEnv, 'value');
}
EnvironmentVariable::updateOrCreate([
'key' => $key,
'service_id' => $this->id,
], [
'value' => $defaultValue,
'is_build_time' => false,
'service_id' => $this->id,
'is_preview' => false,
]);
}
}
}
// Add labels to the service
if ($savedService->serviceType()) {
$fqdns = generateServiceSpecificFqdns($savedService, forTraefik: true);
} else {
$fqdns = collect(data_get($savedService, 'fqdns'));
}
$defaultLabels = defaultLabels($this->id, $containerName, type: 'service', subType: $isDatabase ? 'database' : 'application', subId: $savedService->id);
$serviceLabels = $serviceLabels->merge($defaultLabels);
if (!$isDatabase && $fqdns->count() > 0) {
if ($fqdns) {
$serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik($this->uuid, $fqdns, true));
}
}
if ($this->server->isDrainLogActivated()) {
data_set($service, 'logging', [
'driver' => 'fluentd',
'options' => [
'fluentd-address' => "tcp://127.0.0.1:24224",
'fluentd-async' => "true",
'fluentd-sub-second-precision' => "true",
]
]);
}
data_set($service, 'labels', $serviceLabels->toArray());
data_forget($service, 'is_database');
data_set($service, 'restart', RESTART_MODE);
data_set($service, 'container_name', $containerName);
data_forget($service, 'volumes.*.content');
data_forget($service, 'volumes.*.isDirectory');
// Remove unnecessary variables from service.environment
// $withoutServiceEnvs = collect([]);
// collect(data_get($service, 'environment'))->each(function ($value, $key) use ($withoutServiceEnvs) {
// ray($key, $value);
// if (!Str::of($key)->startsWith('$SERVICE_') && !Str::of($value)->startsWith('SERVICE_')) {
// $k = Str::of($value)->before("=");
// $v = Str::of($value)->after("=");
// $withoutServiceEnvs->put($k->value(), $v->value());
// }
// });
// ray($withoutServiceEnvs);
// data_set($service, 'environment', $withoutServiceEnvs->toArray());
return $service;
});
$finalServices = [
'version' => $dockerComposeVersion,
'services' => $services->toArray(),
'volumes' => $topLevelVolumes->toArray(),
'networks' => $topLevelNetworks->toArray(),
];
$this->docker_compose_raw = Yaml::dump($yaml, 10, 2);
$this->docker_compose = Yaml::dump($finalServices, 10, 2);
$this->save();
$this->saveComposeConfigs();
return collect([]);
} else {
return collect([]);
}
} }
} }

View File

@@ -18,6 +18,10 @@ class ServiceApplication extends BaseModel
$service->fileStorages()->delete(); $service->fileStorages()->delete();
}); });
} }
public function isLogDrainEnabled()
{
return data_get($this, 'is_log_drain_enabled', false);
}
public function type() public function type()
{ {
return 'service'; return 'service';

View File

@@ -16,6 +16,10 @@ class ServiceDatabase extends BaseModel
$service->fileStorages()->delete(); $service->fileStorages()->delete();
}); });
} }
public function isLogDrainEnabled()
{
return data_get($this, 'is_log_drain_enabled', false);
}
public function type() public function type()
{ {
return 'service'; return 'service';

View File

@@ -41,6 +41,18 @@ class StandaloneMariadb extends BaseModel
$database->environment_variables()->delete(); $database->environment_variables()->delete();
}); });
} }
public function link()
{
return route('project.database.configuration', [
'project_uuid' => $this->environment->project->uuid,
'environment_name' => $this->environment->name,
'database_uuid' => $this->uuid
]);
}
public function isLogDrainEnabled()
{
return data_get($this, 'is_log_drain_enabled', false);
}
public function type(): string public function type(): string
{ {
return 'standalone-mariadb'; return 'standalone-mariadb';

View File

@@ -44,7 +44,18 @@ class StandaloneMongodb extends BaseModel
$database->environment_variables()->delete(); $database->environment_variables()->delete();
}); });
} }
public function isLogDrainEnabled()
{
return data_get($this, 'is_log_drain_enabled', false);
}
public function link()
{
return route('project.database.configuration', [
'project_uuid' => $this->environment->project->uuid,
'environment_name' => $this->environment->name,
'database_uuid' => $this->uuid
]);
}
public function mongoInitdbRootPassword(): Attribute public function mongoInitdbRootPassword(): Attribute
{ {
return Attribute::make( return Attribute::make(

View File

@@ -41,11 +41,24 @@ class StandaloneMysql extends BaseModel
$database->environment_variables()->delete(); $database->environment_variables()->delete();
}); });
} }
public function link()
{
return route('project.database.configuration', [
'project_uuid' => $this->environment->project->uuid,
'environment_name' => $this->environment->name,
'database_uuid' => $this->uuid
]);
}
public function type(): string public function type(): string
{ {
return 'standalone-mysql'; return 'standalone-mysql';
} }
public function isLogDrainEnabled()
{
return data_get($this, 'is_log_drain_enabled', false);
}
public function portsMappings(): Attribute public function portsMappings(): Attribute
{ {
return Attribute::make( return Attribute::make(

View File

@@ -41,6 +41,18 @@ class StandalonePostgresql extends BaseModel
$database->environment_variables()->delete(); $database->environment_variables()->delete();
}); });
} }
public function link()
{
return route('project.database.configuration', [
'project_uuid' => $this->environment->project->uuid,
'environment_name' => $this->environment->name,
'database_uuid' => $this->uuid
]);
}
public function isLogDrainEnabled()
{
return data_get($this, 'is_log_drain_enabled', false);
}
public function portsMappings(): Attribute public function portsMappings(): Attribute
{ {

View File

@@ -36,6 +36,18 @@ class StandaloneRedis extends BaseModel
$database->environment_variables()->delete(); $database->environment_variables()->delete();
}); });
} }
public function link()
{
return route('project.database.configuration', [
'project_uuid' => $this->environment->project->uuid,
'environment_name' => $this->environment->name,
'database_uuid' => $this->uuid
]);
}
public function isLogDrainEnabled()
{
return data_get($this, 'is_log_drain_enabled', false);
}
public function portsMappings(): Attribute public function portsMappings(): Attribute
{ {

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

@@ -27,7 +27,7 @@ class ContainerRestarted extends Notification implements ShouldQueue
public function toMail(): MailMessage public function toMail(): MailMessage
{ {
$mail = new MailMessage(); $mail = new MailMessage();
$mail->subject("Coolify: Container ({$this->name}) has been restarted automatically on {$this->server->name}"); $mail->subject("Coolify: A service ({$this->name}) has been restarted automatically on {$this->server->name}");
$mail->view('emails.container-restarted', [ $mail->view('emails.container-restarted', [
'containerName' => $this->name, 'containerName' => $this->name,
'serverName' => $this->server->name, 'serverName' => $this->server->name,
@@ -38,12 +38,12 @@ class ContainerRestarted extends Notification implements ShouldQueue
public function toDiscord(): string public function toDiscord(): string
{ {
$message = "Coolify: Container ({$this->name}) has been restarted automatically on {$this->server->name}"; $message = "Coolify: A service ({$this->name}) has been restarted automatically on {$this->server->name}";
return $message; return $message;
} }
public function toTelegram(): array public function toTelegram(): array
{ {
$message = "Coolify: Container ({$this->name}) has been restarted automatically on {$this->server->name}"; $message = "Coolify: A service ({$this->name}) has been restarted automatically on {$this->server->name}";
$payload = [ $payload = [
"message" => $message, "message" => $message,
]; ];

View File

@@ -26,7 +26,7 @@ class ContainerStopped extends Notification implements ShouldQueue
public function toMail(): MailMessage public function toMail(): MailMessage
{ {
$mail = new MailMessage(); $mail = new MailMessage();
$mail->subject("Coolify: Container ({$this->name}) has been stopped on {$this->server->name}"); $mail->subject("Coolify: A service ({$this->name}) has been stopped on {$this->server->name}");
$mail->view('emails.container-stopped', [ $mail->view('emails.container-stopped', [
'containerName' => $this->name, 'containerName' => $this->name,
'serverName' => $this->server->name, 'serverName' => $this->server->name,
@@ -37,12 +37,12 @@ class ContainerStopped extends Notification implements ShouldQueue
public function toDiscord(): string public function toDiscord(): string
{ {
$message = "Coolify: Container ({$this->name}) has been stopped on {$this->server->name}"; $message = "Coolify: A service ({$this->name}) has been stopped on {$this->server->name}";
return $message; return $message;
} }
public function toTelegram(): array public function toTelegram(): array
{ {
$message = "Coolify: Container ($this->name} has been stopped on {$this->server->name}"; $message = "Coolify: A service ($this->name} has been stopped on {$this->server->name}";
$payload = [ $payload = [
"message" => $message, "message" => $message,
]; ];

View File

@@ -43,7 +43,7 @@ class Unreachable extends Notification implements ShouldQueue
public function toMail(): MailMessage public function toMail(): MailMessage
{ {
$mail = new MailMessage(); $mail = new MailMessage();
$mail->subject("Coolify: Server ({$this->server->name}) is unreachable after trying to connect to it 5 times"); $mail->subject("Coolify: Server ({$this->server->name}) is unreachable after trying to connect to it 3 times");
$mail->view('emails.server-lost-connection', [ $mail->view('emails.server-lost-connection', [
'name' => $this->server->name, 'name' => $this->server->name,
]); ]);
@@ -52,13 +52,13 @@ class Unreachable extends Notification implements ShouldQueue
public function toDiscord(): string public function toDiscord(): string
{ {
$message = "Coolify: Server '{$this->server->name}' is unreachable after trying to connect to it 5 times. All automations & integrations are turned off! Please check your server! IMPORTANT: We automatically try to revive your server. If your server is back online, we will automatically turn on all automations & integrations."; $message = "Coolify: Server '{$this->server->name}' is unreachable after trying to connect to it 3 times. All automations & integrations are turned off! Please check your server! IMPORTANT: We automatically try to revive your server. If your server is back online, we will automatically turn on all automations & integrations.";
return $message; return $message;
} }
public function toTelegram(): array public function toTelegram(): array
{ {
return [ return [
"message" => "Coolify: Server '{$this->server->name}' is unreachable after trying to connect to it 5 times. All automations & integrations are turned off! Please check your server! IMPORTANT: We automatically try to revive your server. If your server is back online, we will automatically turn on all automations & integrations." "message" => "Coolify: Server '{$this->server->name}' is unreachable after trying to connect to it 3 times. All automations & integrations are turned off! Please check your server! IMPORTANT: We automatically try to revive your server. If your server is back online, we will automatically turn on all automations & integrations."
]; ];
} }
} }

View File

@@ -12,7 +12,7 @@ use Illuminate\Support\Str;
trait ExecuteRemoteCommand trait ExecuteRemoteCommand
{ {
public ?string $save = null; public ?string $save = null;
public static int $batch_counter = 0;
public function execute_remote_command(...$commands) public function execute_remote_command(...$commands)
{ {
static::$batch_counter++; static::$batch_counter++;
@@ -24,54 +24,53 @@ trait ExecuteRemoteCommand
if ($this->server instanceof Server === false) { if ($this->server instanceof Server === false) {
throw new \RuntimeException('Server is not set or is not an instance of Server model'); throw new \RuntimeException('Server is not set or is not an instance of Server model');
} }
$commandsText->each(function ($single_command) { $commandsText->each(function ($single_command) {
$command = data_get($single_command, 'command') ?? $single_command[0] ?? null; $command = data_get($single_command, 'command') ?? $single_command[0] ?? null;
if ($command === null) { if ($command === null) {
throw new \RuntimeException('Command is not set'); throw new \RuntimeException('Command is not set');
} }
$hidden = data_get($single_command, 'hidden', false); $hidden = data_get($single_command, 'hidden', false);
$customType = data_get($single_command, 'type');
$ignore_errors = data_get($single_command, 'ignore_errors', false); $ignore_errors = data_get($single_command, 'ignore_errors', false);
$this->save = data_get($single_command, 'save'); $this->save = data_get($single_command, 'save');
$remote_command = generateSshCommand($this->server, $command); $remote_command = generateSshCommand($this->server, $command);
$process = Process::timeout(3600)->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $hidden) { $process = Process::timeout(3600)->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $hidden, $customType) {
$output = Str::of($output)->trim(); $output = Str::of($output)->trim();
if ($output->startsWith('╔')) {
$output = "\n" . $output;
}
$new_log_entry = [ $new_log_entry = [
'command' => $command, 'command' => remove_iip($command),
'output' => $output, 'output' => remove_iip($output),
'type' => $type === 'err' ? 'stderr' : 'stdout', 'type' => $customType ?? $type === 'err' ? 'stderr' : 'stdout',
'timestamp' => Carbon::now('UTC'), 'timestamp' => Carbon::now('UTC'),
'hidden' => $hidden, 'hidden' => $hidden,
'batch' => static::$batch_counter, 'batch' => static::$batch_counter,
]; ];
if (!$this->application_deployment_queue->logs) {
if (!$this->log_model->logs) {
$new_log_entry['order'] = 1; $new_log_entry['order'] = 1;
} else { } else {
$previous_logs = json_decode($this->log_model->logs, associative: true, flags: JSON_THROW_ON_ERROR); $previous_logs = json_decode($this->application_deployment_queue->logs, associative: true, flags: JSON_THROW_ON_ERROR);
$new_log_entry['order'] = count($previous_logs) + 1; $new_log_entry['order'] = count($previous_logs) + 1;
} }
$previous_logs[] = $new_log_entry; $previous_logs[] = $new_log_entry;
$this->log_model->logs = json_encode($previous_logs, flags: JSON_THROW_ON_ERROR); $this->application_deployment_queue->logs = json_encode($previous_logs, flags: JSON_THROW_ON_ERROR);
$this->log_model->save(); $this->application_deployment_queue->save();
if ($this->save) { if ($this->save) {
$this->saved_outputs[$this->save] = Str::of($output)->trim(); $this->saved_outputs[$this->save] = Str::of($output)->trim();
} }
}); });
$this->log_model->update([ $this->application_deployment_queue->update([
'current_process_id' => $process->id(), 'current_process_id' => $process->id(),
]); ]);
$process_result = $process->wait(); $process_result = $process->wait();
if ($process_result->exitCode() !== 0) { if ($process_result->exitCode() !== 0) {
if (!$ignore_errors) { if (!$ignore_errors) {
$status = ApplicationDeploymentStatus::FAILED->value; $this->application_deployment_queue->status = ApplicationDeploymentStatus::FAILED->value;
$this->log_model->status = $status; $this->application_deployment_queue->save();
$this->log_model->save();
throw new \RuntimeException($process_result->errorOutput()); throw new \RuntimeException($process_result->errorOutput());
} }
} }

View File

@@ -0,0 +1,77 @@
<?php
namespace App\Traits;
use App\Enums\ApplicationDeploymentStatus;
use App\Models\Server;
use Carbon\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Process;
trait ExecuteRemoteCommandNew
{
public static $batch_counter = 0;
public function executeRemoteCommand(Server $server, $logModel, $commands)
{
static::$batch_counter++;
if ($commands instanceof Collection) {
$commandsText = $commands;
} else {
$commandsText = collect($commands);
}
$commandsText->each(function ($singleCommand) use ($server, $logModel) {
$command = data_get($singleCommand, 'command') ?? $singleCommand[0] ?? null;
if ($command === null) {
throw new \RuntimeException('Command is not set');
}
$hidden = data_get($singleCommand, 'hidden', false);
$customType = data_get($singleCommand, 'type');
$ignoreErrors = data_get($singleCommand, 'ignore_errors', false);
$save = data_get($singleCommand, 'save');
$remote_command = generateSshCommand($server, $command);
$process = Process::timeout(3600)->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $hidden, $customType, $logModel, $save) {
$output = str($output)->trim();
if ($output->startsWith('╔')) {
$output = "\n" . $output;
}
$newLogEntry = [
'command' => remove_iip($command),
'output' => remove_iip($output),
'type' => $customType ?? $type === 'err' ? 'stderr' : 'stdout',
'timestamp' => Carbon::now('UTC'),
'hidden' => $hidden,
'batch' => static::$batch_counter,
];
if (!$logModel->logs) {
$newLogEntry['order'] = 1;
} else {
$previousLogs = json_decode($logModel->logs, associative: true, flags: JSON_THROW_ON_ERROR);
$newLogEntry['order'] = count($previousLogs) + 1;
}
$previousLogs[] = $newLogEntry;
$logModel->logs = json_encode($previousLogs, flags: JSON_THROW_ON_ERROR);
$logModel->save();
if ($save) {
$this->remoteCommandOutputs[$save] = str($output)->trim();
}
});
$logModel->update([
'current_process_id' => $process->id(),
]);
$processResult = $process->wait();
if ($processResult->exitCode() !== 0) {
if (!$ignoreErrors) {
$status = ApplicationDeploymentStatus::FAILED->value;
$logModel->status = $status;
$logModel->save();
throw new \RuntimeException($processResult->errorOutput());
}
}
});
}
}

View File

@@ -38,7 +38,7 @@ class Textarea extends Component
if (is_null($this->id)) $this->id = new Cuid2(7); if (is_null($this->id)) $this->id = new Cuid2(7);
if (is_null($this->name)) $this->name = $this->id; if (is_null($this->name)) $this->name = $this->id;
$this->label = Str::title($this->label); // $this->label = Str::title($this->label);
return view('components.forms.textarea'); return view('components.forms.textarea');
} }
} }

View File

@@ -1,8 +1,15 @@
<?php <?php
use App\Jobs\ApplicationDeployDockerImageJob;
use App\Jobs\ApplicationDeploymentJob; use App\Jobs\ApplicationDeploymentJob;
use App\Jobs\ApplicationDeploySimpleDockerfileJob;
use App\Jobs\ApplicationRestartJob;
use App\Jobs\MultipleApplicationDeploymentJob;
use App\Models\Application; use App\Models\Application;
use App\Models\ApplicationDeploymentQueue; use App\Models\ApplicationDeploymentQueue;
use App\Models\ApplicationPreview;
use App\Models\Server;
use Symfony\Component\Yaml\Yaml;
function queue_application_deployment(int $application_id, string $deployment_uuid, int | null $pull_request_id = 0, string $commit = 'HEAD', bool $force_rebuild = false, bool $is_webhook = false, bool $restart_only = false, ?string $git_type = null) function queue_application_deployment(int $application_id, string $deployment_uuid, int | null $pull_request_id = 0, string $commit = 'HEAD', bool $force_rebuild = false, bool $is_webhook = false, bool $restart_only = false, ?string $git_type = null)
{ {
@@ -32,6 +39,7 @@ function queue_application_deployment(int $application_id, string $deployment_uu
dispatch(new ApplicationDeploymentJob( dispatch(new ApplicationDeploymentJob(
application_deployment_queue_id: $deployment->id, application_deployment_queue_id: $deployment->id,
))->onConnection('long-running')->onQueue('long-running'); ))->onConnection('long-running')->onQueue('long-running');
} }
function queue_next_deployment(Application $application) function queue_next_deployment(Application $application)
@@ -43,3 +51,262 @@ function queue_next_deployment(Application $application)
))->onConnection('long-running')->onQueue('long-running'); ))->onConnection('long-running')->onQueue('long-running');
} }
} }
// Deployment things
function generateHostIpMapping(Server $server, string $network)
{
// Generate custom host<->ip hostnames
$allContainers = instant_remote_process(["docker network inspect {$network} -f '{{json .Containers}}' "], $server);
$allContainers = format_docker_command_output_to_json($allContainers);
$ips = collect([]);
if (count($allContainers) > 0) {
$allContainers = $allContainers[0];
foreach ($allContainers as $container) {
$containerName = data_get($container, 'Name');
if ($containerName === 'coolify-proxy') {
continue;
}
$containerIp = data_get($container, 'IPv4Address');
if ($containerName && $containerIp) {
$containerIp = str($containerIp)->before('/');
$ips->put($containerName, $containerIp->value());
}
}
}
return $ips->map(function ($ip, $name) {
return "--add-host $name:$ip";
})->implode(' ');
}
function generateBaseDir(string $deplyomentUuid)
{
return "/artifacts/$deplyomentUuid";
}
function generateWorkdir(string $deplyomentUuid, Application $application)
{
return generateBaseDir($deplyomentUuid) . rtrim($application->base_directory, '/');
}
function prepareHelperContainer(Server $server, string $network, string $deploymentUuid)
{
$basedir = generateBaseDir($deploymentUuid);
$helperImage = config('coolify.helper_image');
$serverUserHomeDir = instant_remote_process(["echo \$HOME"], $server);
$dockerConfigFileExists = instant_remote_process(["test -f {$serverUserHomeDir}/.docker/config.json && echo 'OK' || echo 'NOK'"], $server);
$commands = collect([]);
if ($dockerConfigFileExists === 'OK') {
$commands->push([
"command" => "docker run -d --network $network -v /:/host --name $deploymentUuid --rm -v {$serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock $helperImage",
"hidden" => true,
]);
} else {
$commands->push([
"command" => "docker run -d --network {$network} -v /:/host --name {$deploymentUuid} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}",
"hidden" => true,
]);
}
$commands->push([
"command" => executeInDocker($deploymentUuid, "mkdir -p {$basedir}"),
"hidden" => true,
]);
return $commands;
}
function generateComposeFile(string $deploymentUuid, Server $server, string $network, Application $application, string $containerName, string $imageName, ?ApplicationPreview $preview = null, int $pullRequestId = 0)
{
$ports = $application->settings->is_static ? [80] : $application->ports_exposes_array;
$workDir = generateWorkdir($deploymentUuid, $application);
$persistent_storages = generateLocalPersistentVolumes($application, $pullRequestId);
$volume_names = generateLocalPersistentVolumesOnlyVolumeNames($application, $pullRequestId);
$environment_variables = generateEnvironmentVariables($application, $ports, $pullRequestId);
if (data_get($application, 'custom_labels')) {
$labels = collect(str($application->custom_labels)->explode(','));
$labels = $labels->filter(function ($value, $key) {
return !str($value)->startsWith('coolify.');
});
$application->custom_labels = $labels->implode(',');
$application->save();
} else {
$labels = collect(generateLabelsApplication($application, $preview));
}
if ($pullRequestId !== 0) {
$labels = collect(generateLabelsApplication($application, $preview));
}
$labels = $labels->merge(defaultLabels($application->id, $application->uuid, 0))->toArray();
$docker_compose = [
'version' => '3.8',
'services' => [
$containerName => [
'image' => $imageName,
'container_name' => $containerName,
'restart' => RESTART_MODE,
'environment' => $environment_variables,
'labels' => $labels,
'expose' => $ports,
'networks' => [
$network,
],
'mem_limit' => $application->limits_memory,
'memswap_limit' => $application->limits_memory_swap,
'mem_swappiness' => $application->limits_memory_swappiness,
'mem_reservation' => $application->limits_memory_reservation,
'cpus' => (int) $application->limits_cpus,
'cpuset' => $application->limits_cpuset,
'cpu_shares' => $application->limits_cpu_shares,
]
],
'networks' => [
$network => [
'external' => true,
'name' => $network,
'attachable' => true
]
]
];
if ($server->isLogDrainEnabled() && $application->isLogDrainEnabled()) {
$docker_compose['services'][$containerName]['logging'] = [
'driver' => 'fluentd',
'options' => [
'fluentd-address' => "tcp://127.0.0.1:24224",
'fluentd-async' => "true",
'fluentd-sub-second-precision' => "true",
]
];
}
if ($application->settings->is_gpu_enabled) {
$docker_compose['services'][$containerName]['deploy']['resources']['reservations']['devices'] = [
[
'driver' => data_get($application, 'settings.gpu_driver', 'nvidia'),
'capabilities' => ['gpu'],
'options' => data_get($application, 'settings.gpu_options', [])
]
];
if (data_get($application, 'settings.gpu_count')) {
$count = data_get($application, 'settings.gpu_count');
if ($count === 'all') {
$docker_compose['services'][$containerName]['deploy']['resources']['reservations']['devices'][0]['count'] = $count;
} else {
$docker_compose['services'][$containerName]['deploy']['resources']['reservations']['devices'][0]['count'] = (int) $count;
}
} else if (data_get($application, 'settings.gpu_device_ids')) {
$docker_compose['services'][$containerName]['deploy']['resources']['reservations']['devices'][0]['ids'] = data_get($application, 'settings.gpu_device_ids');
}
}
if ($application->isHealthcheckDisabled()) {
data_forget($docker_compose, 'services.' . $containerName . '.healthcheck');
}
if (count($application->ports_mappings_array) > 0 && $pullRequestId === 0) {
$docker_compose['services'][$containerName]['ports'] = $application->ports_mappings_array;
}
if (count($persistent_storages) > 0) {
$docker_compose['services'][$containerName]['volumes'] = $persistent_storages;
}
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$commands = collect([]);
$commands->push([
"command" => executeInDocker($deploymentUuid, "echo '{$docker_compose_base64}' | base64 -d > {$workDir}/docker-compose.yml"),
"hidden" => true,
]);
return $commands;
}
function generateLocalPersistentVolumes(Application $application, int $pullRequestId = 0)
{
$local_persistent_volumes = [];
foreach ($application->persistentStorages as $persistentStorage) {
$volume_name = $persistentStorage->host_path ?? $persistentStorage->name;
if ($pullRequestId !== 0) {
$volume_name = $volume_name . '-pr-' . $pullRequestId;
}
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
}
return $local_persistent_volumes;
}
function generateLocalPersistentVolumesOnlyVolumeNames(Application $application, int $pullRequestId = 0)
{
$local_persistent_volumes_names = [];
foreach ($application->persistentStorages as $persistentStorage) {
if ($persistentStorage->host_path) {
continue;
}
$name = $persistentStorage->name;
if ($pullRequestId !== 0) {
$name = $name . '-pr-' . $pullRequestId;
}
$local_persistent_volumes_names[$name] = [
'name' => $name,
'external' => false,
];
}
return $local_persistent_volumes_names;
}
function generateEnvironmentVariables(Application $application, $ports, int $pullRequestId = 0)
{
$environment_variables = collect();
// ray('Generate Environment Variables')->green();
if ($pullRequestId === 0) {
// ray($this->application->runtime_environment_variables)->green();
foreach ($application->runtime_environment_variables as $env) {
$environment_variables->push("$env->key=$env->value");
}
foreach ($application->nixpacks_environment_variables as $env) {
$environment_variables->push("$env->key=$env->value");
}
} else {
// ray($this->application->runtime_environment_variables_preview)->green();
foreach ($application->runtime_environment_variables_preview as $env) {
$environment_variables->push("$env->key=$env->value");
}
foreach ($application->nixpacks_environment_variables_preview as $env) {
$environment_variables->push("$env->key=$env->value");
}
}
// Add PORT if not exists, use the first port as default
if ($environment_variables->filter(fn ($env) => str($env)->contains('PORT'))->isEmpty()) {
$environment_variables->push("PORT={$ports[0]}");
}
return $environment_variables->all();
}
function startNewApplication(Application $application, string $deploymentUuid, ApplicationDeploymentQueue $loggingModel)
{
$commands = collect([]);
$workDir = generateWorkdir($deploymentUuid, $application);
if ($application->build_pack === 'dockerimage') {
$loggingModel->addLogEntry('Pulling latest images from the registry.');
$commands->push(
[
"command" => executeInDocker($deploymentUuid, "docker compose --project-directory {$workDir} pull"),
"hidden" => true
],
[
"command" => executeInDocker($deploymentUuid, "docker compose --project-directory {$workDir} up --build -d"),
"hidden" => true
],
);
} else {
$commands->push(
[
"command" => executeInDocker($deploymentUuid, "docker compose --project-directory {$workDir} up --build -d"),
"hidden" => true
],
);
}
return $commands;
}
function removeOldDeployment(string $containerName)
{
$commands = collect([]);
$commands->push(
["docker rm -f $containerName >/dev/null 2>&1"],
);
return $commands;
}

View File

@@ -1,5 +1,6 @@
<?php <?php
const REDACTED = '<REDACTED>';
const DATABASE_TYPES = ['postgresql', 'redis', 'mongodb', 'mysql', 'mariadb']; const DATABASE_TYPES = ['postgresql', 'redis', 'mongodb', 'mysql', 'mariadb'];
const VALID_CRON_STRINGS = [ const VALID_CRON_STRINGS = [
'every_minute' => '* * * * *', 'every_minute' => '* * * * *',
@@ -26,3 +27,10 @@ const DATABASE_DOCKER_IMAGES = [
const SPECIFIC_SERVICES = [ const SPECIFIC_SERVICES = [
'quay.io/minio/minio', 'quay.io/minio/minio',
]; ];
// Based on /etc/os-release
const SUPPORTED_OS = [
'ubuntu debian raspbian',
'centos fedora rhel ol rocky',
'sles opensuse-leap opensuse-tumbleweed'
];

View File

@@ -24,7 +24,7 @@ function create_standalone_postgresql($environment_id, $destination_uuid): Stand
} }
return StandalonePostgresql::create([ return StandalonePostgresql::create([
'name' => generate_database_name('postgresql'), 'name' => generate_database_name('postgresql'),
'postgres_password' => \Illuminate\Support\Str::password(symbols: false), 'postgres_password' => \Illuminate\Support\Str::password(length: 64, symbols: false),
'environment_id' => $environment_id, 'environment_id' => $environment_id,
'destination_id' => $destination->id, 'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(), 'destination_type' => $destination->getMorphClass(),
@@ -39,7 +39,7 @@ function create_standalone_redis($environment_id, $destination_uuid): Standalone
} }
return StandaloneRedis::create([ return StandaloneRedis::create([
'name' => generate_database_name('redis'), 'name' => generate_database_name('redis'),
'redis_password' => \Illuminate\Support\Str::password(symbols: false), 'redis_password' => \Illuminate\Support\Str::password(length: 64, symbols: false),
'environment_id' => $environment_id, 'environment_id' => $environment_id,
'destination_id' => $destination->id, 'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(), 'destination_type' => $destination->getMorphClass(),
@@ -54,7 +54,7 @@ function create_standalone_mongodb($environment_id, $destination_uuid): Standalo
} }
return StandaloneMongodb::create([ return StandaloneMongodb::create([
'name' => generate_database_name('mongodb'), 'name' => generate_database_name('mongodb'),
'mongo_initdb_root_password' => \Illuminate\Support\Str::password(symbols: false), 'mongo_initdb_root_password' => \Illuminate\Support\Str::password(length: 64, symbols: false),
'environment_id' => $environment_id, 'environment_id' => $environment_id,
'destination_id' => $destination->id, 'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(), 'destination_type' => $destination->getMorphClass(),
@@ -68,8 +68,8 @@ function create_standalone_mysql($environment_id, $destination_uuid): Standalone
} }
return StandaloneMysql::create([ return StandaloneMysql::create([
'name' => generate_database_name('mysql'), 'name' => generate_database_name('mysql'),
'mysql_root_password' => \Illuminate\Support\Str::password(symbols: false), 'mysql_root_password' => \Illuminate\Support\Str::password(length: 64, symbols: false),
'mysql_password' => \Illuminate\Support\Str::password(symbols: false), 'mysql_password' => \Illuminate\Support\Str::password(length: 64, symbols: false),
'environment_id' => $environment_id, 'environment_id' => $environment_id,
'destination_id' => $destination->id, 'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(), 'destination_type' => $destination->getMorphClass(),
@@ -83,8 +83,8 @@ function create_standalone_mariadb($environment_id, $destination_uuid): Standalo
} }
return StandaloneMariadb::create([ return StandaloneMariadb::create([
'name' => generate_database_name('mariadb'), 'name' => generate_database_name('mariadb'),
'mariadb_root_password' => \Illuminate\Support\Str::password(symbols: false), 'mariadb_root_password' => \Illuminate\Support\Str::password(length: 64, symbols: false),
'mariadb_password' => \Illuminate\Support\Str::password(symbols: false), 'mariadb_password' => \Illuminate\Support\Str::password(length: 64, symbols: false),
'environment_id' => $environment_id, 'environment_id' => $environment_id,
'destination_id' => $destination->id, 'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(), 'destination_type' => $destination->getMorphClass(),

View File

@@ -3,6 +3,9 @@
use App\Models\Application; use App\Models\Application;
use App\Models\ApplicationPreview; use App\Models\ApplicationPreview;
use App\Models\Server; use App\Models\Server;
use App\Models\Service;
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Spatie\Url\Url; use Spatie\Url\Url;
@@ -90,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';
} }
@@ -98,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)
@@ -137,18 +156,28 @@ function defaultLabels($id, $name, $pull_request_id = 0, string $type = 'applica
$labels->push('coolify.name=' . $name); $labels->push('coolify.name=' . $name);
$labels->push('coolify.pullRequestId=' . $pull_request_id); $labels->push('coolify.pullRequestId=' . $pull_request_id);
if ($type === 'service') { if ($type === 'service') {
$labels->push('coolify.service.subId=' . $subId); $subId && $labels->push('coolify.service.subId=' . $subId);
$labels->push('coolify.service.subType=' . $subType); $subType && $labels->push('coolify.service.subType=' . $subType);
} }
return $labels; return $labels;
} }
function generateServiceSpecificFqdns($service, $forTraefik = false) function generateServiceSpecificFqdns(ServiceApplication|Application $resource, $forTraefik = false)
{ {
$variables = collect($service->service->environment_variables); if ($resource->getMorphClass() === 'App\Models\ServiceApplication') {
$type = $service->serviceType(); $uuid = $resource->uuid;
$server = $resource->service->server;
$environment_variables = $resource->service->environment_variables;
$type = $resource->serviceType();
} else if ($resource->getMorphClass() === 'App\Models\Application') {
$uuid = $resource->uuid;
$server = $resource->destination->server;
$environment_variables = $resource->environment_variables;
$type = $resource->serviceType();
}
$variables = collect($environment_variables);
$payload = collect([]); $payload = collect([]);
switch ($type) { switch ($type) {
case $type->contains('minio'): case $type?->contains('minio'):
$MINIO_BROWSER_REDIRECT_URL = $variables->where('key', 'MINIO_BROWSER_REDIRECT_URL')->first(); $MINIO_BROWSER_REDIRECT_URL = $variables->where('key', 'MINIO_BROWSER_REDIRECT_URL')->first();
$MINIO_SERVER_URL = $variables->where('key', 'MINIO_SERVER_URL')->first(); $MINIO_SERVER_URL = $variables->where('key', 'MINIO_SERVER_URL')->first();
if (is_null($MINIO_BROWSER_REDIRECT_URL) || is_null($MINIO_SERVER_URL)) { if (is_null($MINIO_BROWSER_REDIRECT_URL) || is_null($MINIO_SERVER_URL)) {
@@ -156,12 +185,12 @@ function generateServiceSpecificFqdns($service, $forTraefik = false)
} }
if (is_null($MINIO_BROWSER_REDIRECT_URL?->value)) { if (is_null($MINIO_BROWSER_REDIRECT_URL?->value)) {
$MINIO_BROWSER_REDIRECT_URL?->update([ $MINIO_BROWSER_REDIRECT_URL?->update([
"value" => generateFqdn($service->service->server, 'console-' . $service->uuid) "value" => generateFqdn($server, 'console-' . $uuid)
]); ]);
} }
if (is_null($MINIO_SERVER_URL?->value)) { if (is_null($MINIO_SERVER_URL?->value)) {
$MINIO_SERVER_URL?->update([ $MINIO_SERVER_URL?->update([
"value" => generateFqdn($service->service->server, 'minio-' . $service->uuid) "value" => generateFqdn($server, 'minio-' . $uuid)
]); ]);
} }
if ($forTraefik) { if ($forTraefik) {
@@ -175,10 +204,11 @@ function generateServiceSpecificFqdns($service, $forTraefik = false)
$MINIO_SERVER_URL->value, $MINIO_SERVER_URL->value,
]); ]);
} }
break;
} }
return $payload; return $payload;
} }
function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled, $onlyPort = null) function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null)
{ {
$labels = collect([]); $labels = collect([]);
$labels->push('traefik.enable=true'); $labels->push('traefik.enable=true');
@@ -260,3 +290,21 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
} }
return $labels->all(); return $labels->all();
} }
function isDatabaseImage(?string $image = null)
{
if (is_null($image)) {
return false;
}
$image = str($image);
if ($image->contains(':')) {
$image = str($image);
} else {
$image = str($image)->append(':latest');
}
$imageName = $image->before(':');
if (collect(DATABASE_DOCKER_IMAGES)->contains($imageName)) {
return true;
}
return false;
}

View File

@@ -1,6 +1,7 @@
<?php <?php
use App\Actions\Proxy\SaveConfiguration; use App\Actions\Proxy\SaveConfiguration;
use App\Models\Application;
use App\Models\Server; use App\Models\Server;
use Symfony\Component\Yaml\Yaml; use Symfony\Component\Yaml\Yaml;
@@ -12,9 +13,32 @@ function get_proxy_path()
} }
function connectProxyToNetworks(Server $server) function connectProxyToNetworks(Server $server)
{ {
// Standalone networks
$networks = collect($server->standaloneDockers)->map(function ($docker) { $networks = collect($server->standaloneDockers)->map(function ($docker) {
return $docker['network']; return $docker['network'];
})->unique(); });
// Service networks
foreach ($server->services()->get() as $service) {
$networks->push($service->networks());
}
// Docker compose based apps
$docker_compose_apps = $server->dockerComposeBasedApplications();
foreach ($docker_compose_apps as $app) {
$networks->push($app->uuid);
}
// Docker compose based preview deployments
$docker_compose_previews = $server->dockerComposeBasedPreviewDeployments();
foreach ($docker_compose_previews as $preview) {
$pullRequestId = $preview->pull_request_id;
$applicationId = $preview->application_id;
$application = Application::find($applicationId);
if (!$application) {
continue;
}
$network = "{$application->uuid}-{$pullRequestId}";
$networks->push($network);
}
$networks = collect($networks)->flatten()->unique();
if ($networks->count() === 0) { if ($networks->count() === 0) {
$networks = collect(['coolify']); $networks = collect(['coolify']);
} }
@@ -30,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']);
} }
@@ -42,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(),
@@ -78,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",
@@ -86,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",
],
], ],
], ],
]; ];
@@ -104,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)
@@ -151,6 +154,7 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d
if (is_null($application_deployment_queue)) { if (is_null($application_deployment_queue)) {
return collect([]); return collect([]);
} }
// ray(data_get($application_deployment_queue, 'logs'));
try { try {
$decoded = json_decode( $decoded = json_decode(
data_get($application_deployment_queue, 'logs'), data_get($application_deployment_queue, 'logs'),
@@ -160,20 +164,24 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d
} catch (\JsonException $exception) { } catch (\JsonException $exception) {
return collect([]); return collect([]);
} }
// ray($decoded );
$formatted = collect($decoded); $formatted = collect($decoded);
if (!$is_debug_enabled) { if (!$is_debug_enabled) {
$formatted = $formatted->filter(fn ($i) => $i['hidden'] === false ?? false); $formatted = $formatted->filter(fn ($i) => $i['hidden'] === false ?? false);
} }
$formatted = $formatted $formatted = $formatted
->sortBy(fn ($i) => $i['order']) ->sortBy(fn ($i) => data_get($i, 'order'))
->map(function ($i) { ->map(function ($i) {
$i['timestamp'] = Carbon::parse($i['timestamp'])->format('Y-M-d H:i:s.u'); data_set($i, 'timestamp', Carbon::parse(data_get($i, 'timestamp'))->format('Y-M-d H:i:s.u'));
return $i; return $i;
}); });
return $formatted; return $formatted;
} }
function remove_iip($text)
{
$text = preg_replace('/x-access-token:.*?(?=@)/', "x-access-token:" . REDACTED, $text);
return preg_replace('/\x1b\[[0-9;]*m/', '', $text);
}
function refresh_server_connection(?PrivateKey $private_key = null) function refresh_server_connection(?PrivateKey $private_key = null)
{ {
if (is_null($private_key)) { if (is_null($private_key)) {

File diff suppressed because it is too large Load Diff

View File

@@ -26,9 +26,9 @@ return [
'server' => [ 'server' => [
'zero' => 0, 'zero' => 0,
'self-hosted' => 999999999999, 'self-hosted' => 999999999999,
'basic' => 1, 'basic' => env('LIMIT_SERVER_BASIC', 2),
'pro' => 10, 'pro' => env('LIMIT_SERVER_PRO', 10),
'ultimate' => 25, 'ultimate' => env('LIMIT_SERVER_ULTIMATE', 25),
], ],
'email' => [ 'email' => [
'zero' => true, 'zero' => true,

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://c35fe90ee56e18b220bb55e8217d4839@o1082494.ingest.sentry.io/4505347448045568', 'dsn' => 'https://396748153b19c469f5ceff50f1664323@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.142', 'release' => '4.0.0-beta.148',
// 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'),
@@ -76,6 +76,7 @@ return [
'send_default_pii' => env('SENTRY_SEND_DEFAULT_PII', false), 'send_default_pii' => env('SENTRY_SEND_DEFAULT_PII', false),
// @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#traces-sample-rate // @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#traces-sample-rate
'enable_tracing' => env('SENTRY_ENABLE_TRACING', false),
'traces_sample_rate' => 0.2, 'traces_sample_rate' => 0.2,
'profiles_sample_rate' => env('SENTRY_PROFILES_SAMPLE_RATE') === null ? null : (float)env('SENTRY_PROFILES_SAMPLE_RATE'), 'profiles_sample_rate' => env('SENTRY_PROFILES_SAMPLE_RATE') === null ? null : (float)env('SENTRY_PROFILES_SAMPLE_RATE'),

View File

@@ -1,3 +1,3 @@
<?php <?php
return '4.0.0-beta.142'; return '4.0.0-beta.148';

View File

@@ -0,0 +1,76 @@
<?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('application_settings', function (Blueprint $table) {
$table->boolean('is_log_drain_enabled')->default(false);
});
Schema::table('standalone_redis', function (Blueprint $table) {
$table->boolean('is_log_drain_enabled')->default(false);
});
Schema::table('standalone_mysqls', function (Blueprint $table) {
$table->boolean('is_log_drain_enabled')->default(false);
});
Schema::table('standalone_mariadbs', function (Blueprint $table) {
$table->boolean('is_log_drain_enabled')->default(false);
});
Schema::table('standalone_postgresqls', function (Blueprint $table) {
$table->boolean('is_log_drain_enabled')->default(false);
});
Schema::table('standalone_mongodbs', function (Blueprint $table) {
$table->boolean('is_log_drain_enabled')->default(false);
});
Schema::table('service_applications', function (Blueprint $table) {
$table->boolean('is_log_drain_enabled')->default(false);
});
Schema::table('service_databases', function (Blueprint $table) {
$table->boolean('is_log_drain_enabled')->default(false);
});
Schema::table('servers', function (Blueprint $table) {
$table->boolean('log_drain_notification_sent')->default(false);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('application_settings', function (Blueprint $table) {
$table->dropColumn('is_log_drain_enabled');
});
Schema::table('standalone_redis', function (Blueprint $table) {
$table->dropColumn('is_log_drain_enabled');
});
Schema::table('standalone_mysqls', function (Blueprint $table) {
$table->dropColumn('is_log_drain_enabled');
});
Schema::table('standalone_mariadbs', function (Blueprint $table) {
$table->dropColumn('is_log_drain_enabled');
});
Schema::table('standalone_postgresqls', function (Blueprint $table) {
$table->dropColumn('is_log_drain_enabled');
});
Schema::table('standalone_mongodbs', function (Blueprint $table) {
$table->dropColumn('is_log_drain_enabled');
});
Schema::table('service_applications', function (Blueprint $table) {
$table->dropColumn('is_log_drain_enabled');
});
Schema::table('service_databases', function (Blueprint $table) {
$table->dropColumn('is_log_drain_enabled');
});
Schema::table('servers', function (Blueprint $table) {
$table->dropColumn('log_drain_notification_sent');
});
}
};

View File

@@ -0,0 +1,36 @@
<?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('application_settings', function (Blueprint $table) {
$table->boolean('is_gpu_enabled')->default(false);
$table->string('gpu_driver')->default('nvidia');
$table->string('gpu_count')->nullable();
$table->string('gpu_device_ids')->nullable();
$table->longText('gpu_options')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('application_settings', function (Blueprint $table) {
$table->dropColumn('is_gpu_enabled');
$table->dropColumn('gpu_driver');
$table->dropColumn('gpu_count');
$table->dropColumn('gpu_device_ids');
$table->dropColumn('gpu_options');
});
}
};

View File

@@ -0,0 +1,28 @@
<?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('applications', function (Blueprint $table) {
$table->string('additional_destinations')->nullable()->after('destination');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('applications', function (Blueprint $table) {
$table->dropColumn('additional_destinations');
});
}
};

View File

@@ -0,0 +1,42 @@
<?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('applications', function (Blueprint $table) {
$table->string('docker_compose_location')->nullable()->default('/docker-compose.yaml')->after('dockerfile_location');
$table->string('docker_compose_pr_location')->nullable()->default('/docker-compose.yaml')->after('docker_compose_location');
$table->longText('docker_compose')->nullable()->after('docker_compose_location');
$table->longText('docker_compose_pr')->nullable()->after('docker_compose_location');
$table->longText('docker_compose_raw')->nullable()->after('docker_compose');
$table->longText('docker_compose_pr_raw')->nullable()->after('docker_compose');
$table->text('docker_compose_domains')->nullable()->after('docker_compose_raw');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('applications', function (Blueprint $table) {
$table->dropColumn('docker_compose_location');
$table->dropColumn('docker_compose_pr_location');
$table->dropColumn('docker_compose');
$table->dropColumn('docker_compose_pr');
$table->dropColumn('docker_compose_raw');
$table->dropColumn('docker_compose_pr_raw');
$table->dropColumn('docker_compose_domains');
});
}
};

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

@@ -28,6 +28,8 @@ services:
- REDIS_HOST - REDIS_HOST
- REDIS_PASSWORD - REDIS_PASSWORD
- HORIZON_MAX_PROCESSES - HORIZON_MAX_PROCESSES
- HORIZON_BALANCE_MAX_SHIFT
- HORIZON_BALANCE_COOLDOWN
- SSL_MODE=off - SSL_MODE=off
- PHP_PM_CONTROL=dynamic - PHP_PM_CONTROL=dynamic
- PHP_PM_START_SERVERS=1 - PHP_PM_START_SERVERS=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

@@ -1,44 +1,50 @@
<div class="group"> <div class="group">
<label tabindex="0" class="flex items-center gap-2 cursor-pointer hover:text-white"> Open Application @if (data_get($application, 'fqdn') ||
<x-chevron-down /> collect(json_decode($this->application->docker_compose_domains))->count() > 0 ||
</label> data_get($application, 'previews', collect([]))->count() > 0 ||
data_get($application, 'ports_mappings_array'))
<label tabindex="0" class="flex items-center gap-2 cursor-pointer hover:text-white"> Open Application
<x-chevron-down />
</label>
<div class="absolute z-50 hidden group-hover:block"> <div class="absolute z-50 hidden group-hover:block">
<ul tabindex="0" class="relative -ml-24 text-xs text-white normal-case rounded min-w-max menu bg-coolgray-200"> <ul tabindex="0"
@if (data_get($application, 'gitBrancLocation')) class="relative -ml-24 text-xs text-white normal-case rounded min-w-max menu bg-coolgray-200">
<li> @if (data_get($application, 'gitBrancLocation'))
<a target="_blank"
class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs hover:text-white"
href="{{ $application->gitBranchLocation }}">
<x-git-icon git="{{ $application->source?->getMorphClass() }}" />
Git Repository
</a>
</li>
@endif
@if (data_get($application, 'fqdn'))
@foreach (Str::of(data_get($application, 'fqdn'))->explode(',') as $fqdn)
<li> <li>
<a class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs hover:text-white" <a target="_blank"
target="_blank" href="{{ getFqdnWithoutPort($fqdn) }}"> class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs hover:text-white"
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" href="{{ $application->gitBranchLocation }}">
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" <x-git-icon git="{{ $application->source?->getMorphClass() }}" />
stroke-linejoin="round"> Git Repository
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M9 15l6 -6" />
<path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" />
<path
d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" />
</svg>{{ getFqdnWithoutPort($fqdn) }}
</a> </a>
</li> </li>
@endforeach @endif
@endif @if (data_get($application, 'build_pack') === 'dockercompose')
@if (data_get($application, 'previews', collect([]))->count() > 0) @foreach (collect(json_decode($this->application->docker_compose_domains)) as $fqdn)
@foreach (data_get($application, 'previews') as $preview) @if (data_get($fqdn, 'domain'))
@if (data_get($preview, 'fqdn')) <li>
<a class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs hover:text-white"
target="_blank" href="{{ getFqdnWithoutPort(data_get($fqdn, 'domain')) }}">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M9 15l6 -6" />
<path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" />
<path
d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" />
</svg>{{ getFqdnWithoutPort(data_get($fqdn, 'domain')) }}
</a>
</li>
@endif
@endforeach
@endif
@if (data_get($application, 'fqdn'))
@foreach (str(data_get($application, 'fqdn'))->explode(',') as $fqdn)
<li> <li>
<a class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs hover:text-white" <a class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs hover:text-white"
target="_blank" href="{{ getFqdnWithoutPort(data_get($preview, 'fqdn')) }}"> target="_blank" href="{{ getFqdnWithoutPort($fqdn) }}">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" <svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round"> stroke-linejoin="round">
@@ -47,52 +53,72 @@
<path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" /> <path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" />
<path <path
d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" /> d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" />
</svg> </svg>{{ getFqdnWithoutPort($fqdn) }}
PR{{ data_get($preview, 'pull_request_id') }} |
{{ data_get($preview, 'fqdn') }}
</a> </a>
</li> </li>
@endif @endforeach
@endforeach @endif
@endif @if (data_get($application, 'previews', collect([]))->count() > 0)
@if (data_get($application, 'ports_mappings_array')) @foreach (data_get($application, 'previews') as $preview)
@foreach ($application->ports_mappings_array as $port) @if (data_get($preview, 'fqdn'))
@if (isDev()) <li>
<li> <a class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs hover:text-white"
<a class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs hover:text-white" target="_blank" href="{{ getFqdnWithoutPort(data_get($preview, 'fqdn')) }}">
target="_blank" href="http://localhost:{{ explode(':', $port)[0] }}"> <svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24"
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> <path d="M9 15l6 -6" />
<path d="M9 15l6 -6" /> <path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" />
<path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" /> <path
<path d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" />
d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" /> </svg>
</svg> PR{{ data_get($preview, 'pull_request_id') }} |
Port {{ $port }} {{ data_get($preview, 'fqdn') }}
</a> </a>
</li> </li>
@else @endif
<li> @endforeach
<a class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs hover:text-white" @endif
target="_blank" @if (data_get($application, 'ports_mappings_array'))
href="http://{{ $application->destination->server->ip }}:{{ explode(':', $port)[0] }}"> @foreach ($application->ports_mappings_array as $port)
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" @if (isDev())
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" <li>
stroke-linejoin="round"> <a class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs hover:text-white"
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> target="_blank" href="http://localhost:{{ explode(':', $port)[0] }}">
<path d="M9 15l6 -6" /> <svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24"
<path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" /> stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
<path stroke-linejoin="round">
d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" /> <path stroke="none" d="M0 0h24v24H0z" fill="none" />
</svg> <path d="M9 15l6 -6" />
Port {{ $port }} <path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" />
</a> <path
</li> d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" />
@endif </svg>
@endforeach Port {{ $port }}
@endif </a>
</ul> </li>
</div> @else
<li>
<a class="text-xs text-white rounded-none hover:no-underline hover:bg-coollabs hover:text-white"
target="_blank"
href="http://{{ $application->destination->server->ip }}:{{ explode(':', $port)[0] }}">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M9 15l6 -6" />
<path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" />
<path
d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" />
</svg>
Port {{ $port }}
</a>
</li>
@endif
@endforeach
@endif
</ul>
</div>
@endif
</div> </div>

View File

@@ -13,46 +13,59 @@
</a> </a>
<x-applications.links :application="$application" /> <x-applications.links :application="$application" />
<div class="flex-1"></div> <div class="flex-1"></div>
<x-applications.advanced :application="$application" /> @if ($application->build_pack === 'dockercompose' && is_null($application->docker_compose_raw))
<div>Please load a Compose file.</div>
@if ($application->status !== 'exited') @elseif ($application->destination->server->isSwarm() && str($application->docker_registry_image_name)->isEmpty())
<button title="With rolling update if possible" wire:click='deploy' class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400"> Swarm Deployments requires a Docker Image in a Registry.
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-orange-400" viewBox="0 0 24 24" stroke-width="2"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path
d="M10.09 4.01l.496 -.495a2 2 0 0 1 2.828 0l7.071 7.07a2 2 0 0 1 0 2.83l-7.07 7.07a2 2 0 0 1 -2.83 0l-7.07 -7.07a2 2 0 0 1 0 -2.83l3.535 -3.535h-3.988">
</path>
<path d="M7.05 11.038v-3.988"></path>
</svg>
Redeploy
</button>
<button title="Restart without rebuilding" wire:click='restart' class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg class="w-5 h-5 text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
<path d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747"/>
<path d="M20 4v5h-5"/>
</g>
</svg>
Restart
</button>
<button wire:click='stop' class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24" stroke-width="2"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path>
<path d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path>
</svg>
Stop
</button>
@else @else
<button wire:click='deploy' class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400"> <x-applications.advanced :application="$application" />
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-warning" viewBox="0 0 24 24" stroke-width="1.5" @if ($application->status !== 'exited')
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <button title="With rolling update if possible" wire:click='deploy'
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<path d="M7 4v16l13 -8z" /> <svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-orange-400" viewBox="0 0 24 24"
</svg> stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
Deploy stroke-linejoin="round">
</button> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path
d="M10.09 4.01l.496 -.495a2 2 0 0 1 2.828 0l7.071 7.07a2 2 0 0 1 0 2.83l-7.07 7.07a2 2 0 0 1 -2.83 0l-7.07 -7.07a2 2 0 0 1 0 -2.83l3.535 -3.535h-3.988">
</path>
<path d="M7.05 11.038v-3.988"></path>
</svg>
Redeploy
</button>
@if ($application->build_pack !== 'dockercompose')
<button title="Restart without rebuilding" wire:click='restart'
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg class="w-5 h-5 text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
stroke-width="2">
<path d="M19.933 13.041a8 8 0 1 1-9.925-8.788c3.899-1 7.935 1.007 9.425 4.747" />
<path d="M20 4v5h-5" />
</g>
</svg>
Restart
</button>
@endif
<button wire:click='stop' class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24" stroke-width="2"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path>
<path d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path>
</svg>
Stop
</button>
@else
<button wire:click='deploy'
class="flex items-center gap-2 cursor-pointer hover:text-white text-neutral-400">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-warning" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round"
stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 4v16l13 -8z" />
</svg>
Deploy
</button>
@endif
@endif @endif
</div> </div>

View File

@@ -1,4 +1,4 @@
<div class="form-control min-w-fit"> <div class="px-2 form-control min-w-fit hover:bg-coolgray-100">
<label class="flex gap-4 px-0 cursor-pointer label"> <label class="flex gap-4 px-0 cursor-pointer label">
<span class="flex gap-2 label-text min-w-fit"> <span class="flex gap-2 label-text min-w-fit">
@if ($label) @if ($label)
@@ -7,20 +7,7 @@
{{ $id }} {{ $id }}
@endif @endif
@if ($helper) @if ($helper)
<div class="group w-fit"> <x-helper :helper="$helper" />
<div class="cursor-pointer text-warning">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
class="w-4 h-4 stroke-current">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
</div>
<div class="absolute hidden text-xs group-hover:block border-coolgray-400 bg-coolgray-500">
<div class="p-4 card-body">
{!! $helper !!}
</div>
</div>
</div>
@endif @endif
</span> </span>
<input @disabled($disabled) type="checkbox" {{ $attributes->merge(['class' => $defaultClass]) }} <input @disabled($disabled) type="checkbox" {{ $attributes->merge(['class' => $defaultClass]) }}

View File

@@ -81,7 +81,7 @@
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
clip-rule="evenodd" /> clip-rule="evenodd" />
</svg> </svg>
1 server <x-helper helper="Bring Your Own Server." /> 2 servers <x-helper helper="Bring Your Own Server." />
</li> </li>
<li class="flex gap-x-3"> <li class="flex gap-x-3">
<svg class="flex-none w-5 h-6 text-warning" viewBox="0 0 20 20" fill="currentColor" <svg class="flex-none w-5 h-6 text-warning" viewBox="0 0 20 20" fill="currentColor"

View File

@@ -1,6 +1,6 @@
<div class="navbar-main"> <div class="navbar-main">
<a class="{{ request()->routeIs('project.service') ? 'text-white' : '' }}" <a class="{{ request()->routeIs('project.service.configuration') ? 'text-white' : '' }}"
href="{{ route('project.service', $parameters) }}"> href="{{ route('project.service.configuration', $parameters) }}">
<button>Configuration</button> <button>Configuration</button>
</a> </a>
<x-services.links /> <x-services.links />

View File

@@ -1,6 +1,6 @@
<x-emails.layout> <x-emails.layout>
Container ({{ $containerName }}) has been restarted automatically on {{$serverName}}, because it was stopped unexpectedly. A service ({{ $containerName }}) has been restarted automatically on {{$serverName}}, because it was stopped unexpectedly.
@if ($containerName === 'coolify-proxy') @if ($containerName === 'coolify-proxy')
Coolify Proxy should run on your server as you have FQDNs set up in one of your resources. Coolify Proxy should run on your server as you have FQDNs set up in one of your resources.

View File

@@ -1,6 +1,6 @@
<x-emails.layout> <x-emails.layout>
Container {{ $containerName }} has been stopped unexpectedly on {{$serverName}}. A service ({{ $containerName }}) has been stopped unexpectedly on {{$serverName}}.
@if ($url) @if ($url)
Please check what is going on [here]({{ $url }}). Please check what is going on [here]({{ $url }}).

Some files were not shown because too many files have changed in this diff Show More