Compare commits

...

101 Commits

Author SHA1 Message Date
Andras Bacsai
53975fcf61 Merge pull request #1516 from coollabsio/next
v4.0.0-beta.152
2023-12-04 11:26:17 +01:00
Andras Bacsai
76296c1f19 fix: prevent autorefresh of proxy status 2023-12-04 11:25:24 +01:00
Andras Bacsai
c25baf69e1 fix: workdir issue for basedir
fix: remove / mount on helpers image
2023-12-04 11:20:50 +01:00
Andras Bacsai
f952512615 fix: add cf tunnel to boarding server view 2023-12-04 09:29:55 +01:00
Andras Bacsai
c6557eada8 service: meilisearch 2023-12-03 12:16:33 +01:00
Andras Bacsai
2c2d74c0d6 Update release version to 4.0.0-beta.152 2023-12-01 22:16:48 +01:00
Andras Bacsai
028a2eb275 Fix Docker compose build command and remove debug statements 2023-12-01 22:16:27 +01:00
Andras Bacsai
ce7fad5bef Merge pull request #1511 from coollabsio/next
fix: use official install script with rancher (one will work for sure)
2023-12-01 14:02:30 +01:00
Andras Bacsai
cd7852e4f9 fix: use official install script with rancher (one will work for sure) 2023-12-01 14:02:11 +01:00
Andras Bacsai
7b022a2482 Merge pull request #1510 from coollabsio/next
v4.0.0-beta.151
2023-12-01 13:03:42 +01:00
Andras Bacsai
12d9b6538b Fix environment variable parsing in Docker Compose file 2023-12-01 12:34:23 +01:00
Andras Bacsai
335788c2d6 fix: default value do not overwrite existing env value 2023-12-01 12:14:23 +01:00
Andras Bacsai
2352e4a71d Fix directory creation inApplicationDeploymentJob.php 2023-12-01 12:13:55 +01:00
Andras Bacsai
dc03179bd1 feat: auto-restart tcp proxies for databases 2023-12-01 11:37:00 +01:00
Andras Bacsai
cc72f416e8 feat: custom log drain endpoints 2023-12-01 11:13:58 +01:00
Andras Bacsai
3b67d0a8de feat: save timestamp configuration for logs 2023-12-01 10:34:30 +01:00
Andras Bacsai
0135ba7e89 Delete docker-compose.prod.standalone.yml 2023-11-30 13:38:52 +01:00
Andras Bacsai
a28a28cd23 Add docker-compose.prod.standalone.yml
configuration file
2023-11-30 13:17:43 +01:00
Andras Bacsai
b52680a2d8 Fix dispatch_sync issue in ContainerStatusJob 2023-11-30 12:55:31 +01:00
Andras Bacsai
0670e6c1d6 fix: server view for link() 2023-11-30 12:21:53 +01:00
Andras Bacsai
c3882b75c1 Update release version to 4.0.0-beta.151 2023-11-30 12:21:42 +01:00
Andras Bacsai
64b6f86a36 Update PostgreSQL image version to 16-alpine for services 2023-11-30 12:21:21 +01:00
Andras Bacsai
b9efc22253 Merge pull request #1504 from coollabsio/next
v4.0.0-beta.150 - quick fix
2023-11-29 18:44:25 +01:00
Andras Bacsai
e3d9eb0154 Add hidden flag to docker compose command 2023-11-29 18:43:02 +01:00
Andras Bacsai
66f3967479 Merge pull request #1503 from coollabsio/next
v4.0.0-beta.150
2023-11-29 18:41:41 +01:00
Andras Bacsai
c54439e84c fix: dockercompose save ./ volumes under /data/coolify 2023-11-29 18:40:41 +01:00
Andras Bacsai
db4a4c74fc Merge pull request #1502 from coollabsio/next
v4.0.0-beta.149
2023-11-29 17:05:28 +01:00
Andras Bacsai
5c7ef80219 Fix container retrieval in CheckLogDrainContainerJob and ContainerStatusJob 2023-11-29 17:03:04 +01:00
Andras Bacsai
243d1c06fc cloud: disable trial 2023-11-29 16:34:31 +01:00
Andras Bacsai
ef25f7d800 Update Sentry DSN 2023-11-29 16:21:03 +01:00
Andras Bacsai
45640ffdb1 Update version numbers to 4.0.0-beta.149 2023-11-29 16:19:40 +01:00
Andras Bacsai
378291b209 Merge pull request #1501 from coollabsio/next
Commented out cleanup_ssh() function
2023-11-29 15:23:21 +01:00
Andras Bacsai
3583e552f1 Commented out cleanup_ssh() function 2023-11-29 15:23:03 +01:00
Andras Bacsai
d7dfeaf988 Merge pull request #1496 from coollabsio/next
v4.0.0-beta.148
2023-11-29 15:17:39 +01:00
Andras Bacsai
7fe5eca661 Add precheck for containers 2023-11-29 15:13:03 +01:00
Andras Bacsai
0dff57e69f Add cleanup option to app:init command 2023-11-29 15:03:21 +01:00
Andras Bacsai
f4803ad58b wip: swarm
fix: gitcompose deployments
2023-11-29 14:59:06 +01:00
Andras Bacsai
2d7bbbe300 wip: swarm 2023-11-29 10:06:52 +01:00
Andras Bacsai
928b68043b wip: swarm 2023-11-28 20:49:38 +01:00
Andras Bacsai
b21add0210 Update Swarm cluster label to Swarm Manager 2023-11-28 20:09:00 +01:00
Andras Bacsai
c41ffd6bfb wip: swarm 2023-11-28 18:42:09 +01:00
Andras Bacsai
b4874c7df3 wip: swarm 2023-11-28 18:31:04 +01:00
Andras Bacsai
c505a6ce9c wip 2023-11-28 15:49:24 +01:00
Andras Bacsai
706e4b13ee fix: sentry issue 2023-11-28 14:27:38 +01:00
Andras Bacsai
4af471ee31 fix: no container servers 2023-11-28 14:26:35 +01:00
Andras Bacsai
87062e4e22 Refactor application deployment job 2023-11-28 14:23:59 +01:00
Andras Bacsai
500ba0fab8 fix: do not remove deployment in case compose based failed 2023-11-28 14:08:42 +01:00
Andras Bacsai
1c72c127d5 Remove unused imports and fix import statement 2023-11-28 14:05:55 +01:00
Andras Bacsai
69bb4ae5ee Update release version to 4.0.0-beta.148 2023-11-28 13:40:33 +01:00
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
113 changed files with 3820 additions and 2849 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

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

@@ -15,7 +15,7 @@ class InstallDocker
if (!$supported_os_type) { 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>.'); 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: ' . $supported_os_type); 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",
@@ -44,24 +44,30 @@ class InstallDocker
"ls -l /tmp" "ls -l /tmp"
]); ]);
} else { } else {
if ($supported_os_type === 'debian') { if ($supported_os_type->contains('debian')) {
$command = $command->merge([ $command = $command->merge([
"echo 'Installing Prerequisites...'", "echo 'Installing Prerequisites...'",
"command -v jq >/dev/null || apt-get update", "command -v jq >/dev/null || apt-get update -y",
"command -v jq >/dev/null || apt install -y jq", "command -v jq >/dev/null || apt install -y curl wget git jq",
]); ]);
} else if ($supported_os_type === 'rhel') { } else if ($supported_os_type->contains('rhel')) {
$command = $command->merge([ $command = $command->merge([
"echo 'Installing Prerequisites...'", "echo 'Installing Prerequisites...'",
"command -v jq >/dev/null || dnf install -y jq", "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 { } else {
throw new \Exception('Unsupported OS'); throw new \Exception('Unsupported OS');
} }
$command = $command->merge([ $command = $command->merge([
"echo 'Installing Docker Engine...'", "echo 'Installing Docker Engine...'",
"curl https://releases.rancher.com/install-docker/{$dockerVersion}.sh | sh", "curl https://releases.rancher.com/install-docker/{$dockerVersion}.sh | sh || curl https://get.docker.com | sh -s -- --version {$dockerVersion}",
"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",
@@ -70,10 +76,20 @@ class InstallDocker
"echo 'Restarting Docker Engine...'", "echo 'Restarting Docker Engine...'",
"systemctl enable docker >/dev/null 2>&1 || true", "systemctl enable docker >/dev/null 2>&1 || true",
"systemctl restart docker", "systemctl restart docker",
"echo 'Creating default Docker network (coolify)...'",
"docker network create --attachable coolify >/dev/null 2>&1 || true",
"echo 'Done!'"
]); ]);
if ($server->isSwarm()) {
$command = $command->merge([
"docker network create --attachable --driver overlay coolify-overlay >/dev/null 2>&1 || true",
]);
} else {
$command = $command->merge([
"docker network create --attachable coolify >/dev/null 2>&1 || true",
]);
$command = $command->merge([
"echo 'Done!'",
]);
}
return remote_process($command, $server); return remote_process($command, $server);
} }
} }

View File

@@ -16,6 +16,8 @@ class InstallLogDrain
$type = 'highlight'; $type = 'highlight';
} else if ($server->settings->is_logdrain_axiom_enabled) { } else if ($server->settings->is_logdrain_axiom_enabled) {
$type = 'axiom'; $type = 'axiom';
} else if ($server->settings->is_logdrain_custom_enabled) {
$type = 'custom';
} else { } else {
$type = 'none'; $type = 'none';
} }
@@ -114,15 +116,23 @@ class InstallLogDrain
json_date_format iso8601 json_date_format iso8601
tls On tls On
"); ");
} else if ($type === 'custom') {
if (!$server->settings->is_logdrain_custom_enabled) {
throw new \Exception('Custom log drain is not enabled.');
}
$config = base64_encode($server->settings->logdrain_custom_config);
$parsers = base64_encode($server->settings->logdrain_custom_config_parser);
} else { } else {
throw new \Exception('Unknown log drain type.'); throw new \Exception('Unknown log drain type.');
} }
$parsers = base64_encode(" if ($type !== 'custom') {
$parsers = base64_encode("
[PARSER] [PARSER]
Name empty_line_skipper Name empty_line_skipper
Format regex Format regex
Regex /^(?!\s*$).+/ Regex /^(?!\s*$).+/
"); ");
}
$compose = base64_encode(" $compose = base64_encode("
services: services:
coolify-log-drain: coolify-log-drain:
@@ -179,6 +189,12 @@ Files:
"echo AXIOM_DATASET_NAME={$server->settings->logdrain_axiom_dataset_name} >> $config_path/.env", "echo AXIOM_DATASET_NAME={$server->settings->logdrain_axiom_dataset_name} >> $config_path/.env",
"echo AXIOM_API_KEY={$server->settings->logdrain_axiom_api_key} >> $config_path/.env", "echo AXIOM_API_KEY={$server->settings->logdrain_axiom_api_key} >> $config_path/.env",
]; ];
} else if ($type === 'custom') {
$add_envs_command = [
"touch $config_path/.env"
];
} else {
throw new \Exception('Unknown log drain type.');
} }
$restart_command = [ $restart_command = [
"echo 'Stopping old Fluent Bit'", "echo 'Stopping old Fluent Bit'",

View File

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

View File

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

@@ -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,8 @@ 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 bool $isCloudflareTunnel = false;
public ?Server $createdServer = null; public ?Server $createdServer = null;
public Collection $projects; public Collection $projects;
@@ -182,7 +184,10 @@ 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->is_cloudflare_tunnel = $this->isCloudflareTunnel;
$this->createdServer->settings->save();
$this->createdServer->addInitialNetwork();
$this->validateServer(); $this->validateServer();
} }
public function validateServer() public function validateServer()
@@ -197,6 +202,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
]); ]);
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->serverReachable = false; $this->serverReachable = false;
$this->createdServer->delete();
return handleError(error: $e, livewire: $this); return handleError(error: $e, livewire: $this);
} }

View File

@@ -23,7 +23,7 @@ class Advanced extends Component
]; ];
public function instantSave() public function instantSave()
{ {
if ($this->application->settings->is_log_drain_enabled) { if ($this->application->isLogDrainEnabled()) {
if (!$this->application->destination->server->isLogDrainEnabled()) { if (!$this->application->destination->server->isLogDrainEnabled()) {
$this->application->settings->is_log_drain_enabled = false; $this->application->settings->is_log_drain_enabled = false;
$this->emit('error', 'Log drain is not enabled on this server.'); $this->emit('error', 'Log drain is not enabled on this server.');

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,8 +26,14 @@ 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 $parsedServices = [];
public $parsedServiceDomains = [];
protected $listeners = [ protected $listeners = [
'resetDefaultLabels' 'resetDefaultLabels'
]; ];
@@ -50,6 +57,12 @@ 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', 'application.settings.is_static' => 'boolean|required',
@@ -74,6 +87,12 @@ 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', 'application.settings.is_static' => 'Is static',
@@ -81,6 +100,13 @@ class General extends Component
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);
@@ -91,6 +117,7 @@ class General extends Component
} else { } else {
$this->customLabels = str($this->application->custom_labels)->replace(',', "\n"); $this->customLabels = str($this->application->custom_labels)->replace(',', "\n");
} }
$this->initialDockerComposeLocation = $this->application->docker_compose_location;
$this->checkLabelUpdates(); $this->checkLabelUpdates();
} }
public function instantSave() public function instantSave()
@@ -98,12 +125,44 @@ class General extends Component
$this->application->settings->save(); $this->application->settings->save();
$this->emit('success', 'Settings saved.'); $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()
@@ -140,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);
@@ -172,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

@@ -22,79 +22,19 @@ class DockerCompose extends Component
$this->query = request()->query(); $this->query = request()->query();
if (isDev()) { if (isDev()) {
$this->dockerComposeRaw = 'services: $this->dockerComposeRaw = 'services:
ghost: appsmith:
image: ghost:5 build:
volumes: context: .
- ~/configs:/etc/configs/:ro dockerfile_inline: |
- ./var/lib/ghost/content:/tmp/ghost2/content:ro FROM nginx
- /var/lib/ghost/content:/tmp/ghost/content:rw ARG GIT_COMMIT
- ghost-content-data:/var/lib/ghost/content ARG GIT_BRANCH
- type: volume RUN echo "Hello World ${GIT_COMMIT} ${GIT_BRANCH}"
source: mydata args:
target: /data - GIT_COMMIT=cdc3b19
- type: bind - GIT_BRANCH=${GIT_BRANCH}
source: ./var/lib/ghost/data
target: /data
- type: bind
source: /tmp
target: /tmp
labels:
- "test.label=true"
ports:
- "3000"
- "3000-3005"
- "8000:8000"
- "9090-9091:8080-8081"
- "49100:22"
- "127.0.0.1:8001:8001"
- "127.0.0.1:5000-5010:5000-5010"
- "127.0.0.1::5000"
- "6060:6060/udp"
- "12400-12500:1240"
- target: 80
published: 8080
protocol: tcp
mode: host
networks:
- some-network
- other-network
environment: environment:
- database__client=${DATABASE_CLIENT:-mysql} - APPSMITH_MAIL_ENABLED=${APPSMITH_MAIL_ENABLED}
- database__connection__database=${MYSQL_DATABASE:-ghost}
- database__connection__host=${DATABASE_CONNECTION_HOST:-mysql}
- test=${TEST:?true}
- url=$SERVICE_FQDN_GHOST
- database__connection__user=$SERVICE_USER_MYSQL
- database__connection__password=$SERVICE_PASSWORD_MYSQL
depends_on:
- mysql
mysql:
image: mysql:8.0
volumes:
- ghost-mysql-data:/var/lib/mysql
environment:
- MYSQL_USER=${SERVICE_USER_MYSQL}
- MYSQL_PASSWORD=${SERVICE_PASSWORD_MYSQL}
- MYSQL_DATABASE=$MYSQL_DATABASE
- MYSQL_ROOT_PASSWORD=${SERVICE_PASSWORD_MYSQLROOT}
- SESSION_SECRET
minio:
image: minio/minio
environment:
RACK_ENV: development
A: $A
SHOW: ${SHOW}
SHOW1: ${SHOW2-show1}
SHOW2: ${SHOW3:-show2}
SHOW3: ${SHOW4?show3}
SHOW4: ${SHOW5:?show4}
SHOW5: ${SERVICE_USER_MINIO}
SHOW6: ${SERVICE_PASSWORD_MINIO}
SHOW7: ${SERVICE_PASSWORD_64_MINIO}
SHOW8: ${SERVICE_BASE64_64_MINIO}
SHOW9: ${SERVICE_BASE64_128_MINIO}
SHOW10: ${SERVICE_BASE64_MINIO}
SHOW11:
'; ';
} }
} }
@@ -129,7 +69,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

@@ -28,7 +28,12 @@ class Application extends Component
} }
public function instantSaveAdvanced() public function instantSaveAdvanced()
{ {
$this->submit(); 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.'); $this->emit('success', 'You need to restart the service for the changes to take effect.');
} }
public function delete() public function delete()
@@ -36,7 +41,7 @@ class Application extends Component
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);
} }
@@ -49,11 +54,6 @@ class Application extends Component
{ {
try { try {
$this->validate(); $this->validate();
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->application->save();
updateCompose($this->application); updateCompose($this->application);
$this->emit('success', 'Application saved successfully.'); $this->emit('success', 'Application saved successfully.');

View File

@@ -28,7 +28,7 @@ class Index extends Component
} }
public function checkStatus() public function checkStatus()
{ {
dispatch_sync(new ContainerStatusJob($this->service->server)); dispatch(new ContainerStatusJob($this->service->server));
$this->refreshStacks(); $this->refreshStacks();
} }
public function refreshStacks() public function refreshStacks()

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

@@ -2,7 +2,16 @@
namespace App\Http\Livewire\Project\Shared; namespace App\Http\Livewire\Project\Shared;
use App\Models\Application;
use App\Models\Server; use App\Models\Server;
use App\Models\Service;
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use Illuminate\Support\Facades\Process; use Illuminate\Support\Facades\Process;
use Livewire\Component; use Livewire\Component;
@@ -10,17 +19,44 @@ class GetLogs extends Component
{ {
public string $outputs = ''; public string $outputs = '';
public string $errors = ''; public string $errors = '';
public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb $resource;
public ServiceApplication|ServiceDatabase|null $servicesubtype = null;
public Server $server; public Server $server;
public ?string $container = null; public ?string $container = null;
public ?bool $streamLogs = false; public ?bool $streamLogs = false;
public ?bool $showTimeStamps = true; public ?bool $showTimeStamps = true;
public int $numberOfLines = 100; public int $numberOfLines = 100;
public function mount()
{
if ($this->resource->getMorphClass() === 'App\Models\Application') {
$this->showTimeStamps = $this->resource->settings->is_include_timestamps;
} else {
if ($this->servicesubtype) {
$this->showTimeStamps = $this->servicesubtype->is_include_timestamps;
} else {
$this->showTimeStamps = $this->resource->is_include_timestamps;
}
}
}
public function doSomethingWithThisChunkOfOutput($output) public function doSomethingWithThisChunkOfOutput($output)
{ {
$this->outputs .= removeAnsiColors($output); $this->outputs .= removeAnsiColors($output);
} }
public function instantSave() public function instantSave()
{ {
if ($this->resource->getMorphClass() === 'App\Models\Application') {
$this->resource->settings->is_include_timestamps = $this->showTimeStamps;
$this->resource->settings->save();
} else {
if ($this->servicesubtype) {
$this->servicesubtype->is_include_timestamps = $this->showTimeStamps;
$this->servicesubtype->save();
} else {
$this->resource->is_include_timestamps = $this->showTimeStamps;
$this->resource->save();
}
}
} }
public function getLogs($refresh = false) public function getLogs($refresh = false)
{ {

View File

@@ -17,13 +17,16 @@ 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 $serviceSubType;
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 +36,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';
@@ -60,6 +65,11 @@ class Logs extends Component
} else if (data_get($this->parameters, 'service_uuid')) { } else if (data_get($this->parameters, 'service_uuid')) {
$this->type = 'service'; $this->type = 'service';
$this->resource = Service::where('uuid', $this->parameters['service_uuid'])->firstOrFail(); $this->resource = Service::where('uuid', $this->parameters['service_uuid'])->firstOrFail();
$service_name = data_get($this->parameters, 'service_name');
$this->serviceSubType = $this->resource->applications()->where('name', $service_name)->first();
if (!$this->serviceSubType) {
$this->serviceSubType = $this->resource->databases()->where('name', $service_name)->first();
}
$this->status = $this->resource->status; $this->status = $this->resource->status;
$this->server = $this->resource->server; $this->server = $this->resource->server;
$this->container = data_get($this->parameters, 'service_name') . '-' . $this->resource->uuid; $this->container = data_get($this->parameters, 'service_name') . '-' . $this->resource->uuid;

View File

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

View File

@@ -19,6 +19,9 @@ class LogDrains extends Component
'server.settings.is_logdrain_axiom_enabled' => 'required|boolean', 'server.settings.is_logdrain_axiom_enabled' => 'required|boolean',
'server.settings.logdrain_axiom_dataset_name' => 'required|string', 'server.settings.logdrain_axiom_dataset_name' => 'required|string',
'server.settings.logdrain_axiom_api_key' => 'required|string', 'server.settings.logdrain_axiom_api_key' => 'required|string',
'server.settings.is_logdrain_custom_enabled' => 'required|boolean',
'server.settings.logdrain_custom_config' => 'required|string',
'server.settings.logdrain_custom_config_parser' => 'nullable',
]; ];
protected $validationAttributes = [ protected $validationAttributes = [
'server.settings.is_logdrain_newrelic_enabled' => 'New Relic log drain', 'server.settings.is_logdrain_newrelic_enabled' => 'New Relic log drain',
@@ -29,6 +32,9 @@ class LogDrains extends Component
'server.settings.is_logdrain_axiom_enabled' => 'Axiom log drain', 'server.settings.is_logdrain_axiom_enabled' => 'Axiom log drain',
'server.settings.logdrain_axiom_dataset_name' => 'Axiom dataset name', 'server.settings.logdrain_axiom_dataset_name' => 'Axiom dataset name',
'server.settings.logdrain_axiom_api_key' => 'Axiom API key', 'server.settings.logdrain_axiom_api_key' => 'Axiom API key',
'server.settings.is_logdrain_custom_enabled' => 'Custom log drain',
'server.settings.logdrain_custom_config' => 'Custom log drain configuration',
'server.settings.logdrain_custom_config_parser' => 'Custom log drain configuration parser',
]; ];
public function mount() public function mount()
@@ -84,6 +90,7 @@ class LogDrains extends Component
$this->server->settings->update([ $this->server->settings->update([
'is_logdrain_highlight_enabled' => false, 'is_logdrain_highlight_enabled' => false,
'is_logdrain_axiom_enabled' => false, 'is_logdrain_axiom_enabled' => false,
'is_logdrain_custom_enabled' => false,
]); ]);
} else if ($type === 'highlight') { } else if ($type === 'highlight') {
$this->validate([ $this->validate([
@@ -93,6 +100,7 @@ class LogDrains extends Component
$this->server->settings->update([ $this->server->settings->update([
'is_logdrain_newrelic_enabled' => false, 'is_logdrain_newrelic_enabled' => false,
'is_logdrain_axiom_enabled' => false, 'is_logdrain_axiom_enabled' => false,
'is_logdrain_custom_enabled' => false,
]); ]);
} else if ($type === 'axiom') { } else if ($type === 'axiom') {
$this->validate([ $this->validate([
@@ -103,6 +111,18 @@ class LogDrains extends Component
$this->server->settings->update([ $this->server->settings->update([
'is_logdrain_newrelic_enabled' => false, 'is_logdrain_newrelic_enabled' => false,
'is_logdrain_highlight_enabled' => false, 'is_logdrain_highlight_enabled' => false,
'is_logdrain_custom_enabled' => false,
]);
} else if ($type === 'custom') {
$this->validate([
'server.settings.is_logdrain_custom_enabled' => 'required|boolean',
'server.settings.logdrain_custom_config' => 'required|string',
'server.settings.logdrain_custom_config_parser' => 'nullable',
]);
$this->server->settings->update([
'is_logdrain_newrelic_enabled' => false,
'is_logdrain_highlight_enabled' => false,
'is_logdrain_axiom_enabled' => false,
]); ]);
} }
$this->server->settings->save(); $this->server->settings->save();
@@ -121,6 +141,10 @@ class LogDrains extends Component
$this->server->settings->update([ $this->server->settings->update([
'is_logdrain_axiom_enabled' => false, 'is_logdrain_axiom_enabled' => false,
]); ]);
} else if ($type === 'custom') {
$this->server->settings->update([
'is_logdrain_custom_enabled' => false,
]);
} }
handleError($e, $this); handleError($e, $this);
return false; return false;

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

@@ -14,9 +14,12 @@ class Status extends Component
public int $numberOfPolls = 0; public int $numberOfPolls = 0;
protected $listeners = ['proxyStatusUpdated', 'startProxyPolling']; protected $listeners = ['proxyStatusUpdated', 'startProxyPolling'];
public function mount() {
$this->checkProxy();
}
public function startProxyPolling() public function startProxyPolling()
{ {
$this->polling = true; $this->checkProxy();
} }
public function proxyStatusUpdated() public function proxyStatusUpdated()
{ {

View File

@@ -19,6 +19,7 @@ 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);
} }

View File

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

View File

@@ -1,111 +0,0 @@
<?php
namespace App\Jobs;
use App\Enums\ApplicationDeploymentStatus;
use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
use App\Traits\ExecuteRemoteCommandNew;
use Exception;
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;
use Throwable;
class ApplicationDeployDockerImageJob implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, ExecuteRemoteCommandNew;
public $timeout = 3600;
public $tries = 1;
public string $applicationDeploymentQueueId;
public function __construct(string $applicationDeploymentQueueId)
{
$this->applicationDeploymentQueueId = $applicationDeploymentQueueId;
}
public function handle()
{
ray()->clearAll();
ray('Deploying Docker Image');
try {
$applicationDeploymentQueue = ApplicationDeploymentQueue::find($this->applicationDeploymentQueueId);
$application = Application::find($applicationDeploymentQueue->application_id);
$deploymentUuid = data_get($applicationDeploymentQueue, 'deployment_uuid');
$dockerImage = data_get($application, 'docker_registry_image_name');
$dockerImageTag = data_get($application, 'docker_registry_image_tag');
$productionImageName = str("{$dockerImage}:{$dockerImageTag}");
$destination = $application->destination->getMorphClass()::where('id', $application->destination->id)->first();
$pullRequestId = data_get($applicationDeploymentQueue, 'pull_request_id');
$server = data_get($destination, 'server');
$network = data_get($destination, 'network');
$containerName = generateApplicationContainerName($application, $pullRequestId);
savePrivateKeyToFs($server);
ray("echo 'Starting deployment of {$productionImageName}.'");
$applicationDeploymentQueue->update([
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
]);
$this->executeRemoteCommand(
server: $server,
logModel: $applicationDeploymentQueue,
commands: prepareHelperContainer($server, $network, $deploymentUuid)
);
$this->executeRemoteCommand(
server: $server,
logModel: $applicationDeploymentQueue,
commands: generateComposeFile(
deploymentUuid: $deploymentUuid,
server: $server,
network: $network,
application: $application,
containerName: $containerName,
imageName: $productionImageName,
pullRequestId: $pullRequestId
)
);
$this->executeRemoteCommand(
server: $server,
logModel: $applicationDeploymentQueue,
commands: rollingUpdate(application: $application, deploymentUuid: $deploymentUuid)
);
} catch (Throwable $e) {
$this->executeRemoteCommand(
server: $server,
logModel: $applicationDeploymentQueue,
commands: [
"echo 'Oops something is not okay, are you okay? 😢'",
"echo '{$e->getMessage()}'",
"echo -n 'Deployment failed. Removing the new version of your application.'",
executeInDocker($deploymentUuid, "docker rm -f $containerName >/dev/null 2>&1"),
]
);
// $this->next(ApplicationDeploymentStatus::FAILED->value);
throw $e;
}
}
// private function next(string $status)
// {
// // If the deployment is cancelled by the user, don't update the status
// if ($this->application_deployment_queue->status !== ApplicationDeploymentStatus::CANCELLED_BY_USER->value) {
// $this->application_deployment_queue->update([
// 'status' => $status,
// ]);
// }
// queue_next_deployment($this->application);
// if ($status === ApplicationDeploymentStatus::FINISHED->value) {
// $this->application->environment->project->team->notify(new DeploymentSuccess($this->application, $this->deployment_uuid, $this->preview));
// }
// if ($status === ApplicationDeploymentStatus::FAILED->value) {
// $this->application->environment->project->team->notify(new DeploymentFailed($this->application, $this->deployment_uuid, $this->preview));
// }
// }
}

View File

@@ -1,29 +0,0 @@
<?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 ApplicationDeploySimpleDockerfileJob 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('Deploying Simple Dockerfile');
}
}

View File

@@ -73,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;
@@ -92,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');
@@ -114,7 +112,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$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->mainServer = $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;
@@ -158,40 +156,35 @@ 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} ";
} }
// 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();
@@ -203,6 +196,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
return; 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') {
@@ -219,8 +214,19 @@ 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) { if ($this->application->docker_registry_image_name && $this->application->build_pack !== 'dockerimage') {
$this->push_to_docker_registry(); $this->push_to_docker_registry();
if ($this->server->isSwarm()) {
$this->application_deployment_queue->addLogEntry("Creating / updating stack.");
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "cd {$this->workdir} && docker stack deploy --with-registry-auth -c docker-compose.yml {$this->application->uuid}")
],
[
"echo 'Stack deployed. It may take a few minutes to fully available in your swarm.'"
]
);
}
} }
$this->next(ApplicationDeploymentStatus::FINISHED->value); $this->next(ApplicationDeploymentStatus::FINISHED->value);
$this->application->isConfigurationChanged(true); $this->application->isConfigurationChanged(true);
@@ -297,42 +303,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
ray($e); ray($e);
} }
} }
// private function deploy_docker_compose()
// {
// $dockercompose_base64 = base64_encode($this->application->dockercompose);
// $this->execute_remote_command(
// [
// "echo 'Starting deployment of {$this->application->name}.'"
// ],
// );
// $this->prepare_builder_image();
// $this->execute_remote_command(
// [
// executeInDocker($this->deployment_uuid, "echo '$dockercompose_base64' | base64 -d > $this->workdir/docker-compose.yaml")
// ],
// );
// $this->build_image_name = Str::lower("{$this->customRepository}:build");
// $this->production_image_name = Str::lower("{$this->application->uuid}:latest");
// $this->save_environment_variables();
// $containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id);
// ray($containers);
// if ($containers->count() > 0) {
// foreach ($containers as $container) {
// $containerName = data_get($container, 'Names');
// if ($containerName) {
// instant_remote_process(
// ["docker rm -f {$containerName}"],
// $this->application->destination->server
// );
// }
// }
// }
// $this->execute_remote_command(
// ["echo -n 'Starting services (could take a while)...'"],
// [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up -d"), "hidden" => true],
// );
// }
private function generate_image_names() private function generate_image_names()
{ {
if ($this->application->dockerfile) { if ($this->application->dockerfile) {
@@ -377,6 +347,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->generate_image_names(); $this->generate_image_names();
$this->check_image_locally_or_remotely(); $this->check_image_locally_or_remotely();
if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty()) { if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty()) {
$this->create_workdir();
$this->generate_compose_file(); $this->generate_compose_file();
$this->rolling_update(); $this->rolling_update();
return; return;
@@ -397,19 +368,26 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
]); ]);
} }
} }
// 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);
// $envs_base64 = base64_encode($envs->implode("\n")); }
// $this->execute_remote_command( } else {
// [ foreach ($this->application->environment_variables as $env) {
// executeInDocker($this->deployment_uuid, "echo '$envs_base64' | base64 -d > $this->workdir/.env") $envs->push($env->key . '=' . $env->value);
// ], }
// ); }
// } $envs_base64 = base64_encode($envs->implode("\n"));
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "echo '$envs_base64' | base64 -d > $this->workdir/.env")
],
);
}
private function deploy_simple_dockerfile() private function deploy_simple_dockerfile()
{ {
$dockerfile_base64 = base64_encode($this->application->dockerfile); $dockerfile_base64 = base64_encode($this->application->dockerfile);
@@ -447,7 +425,67 @@ 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();
$this->application->loadComposeFile(isInit: false);
$composeFile = $this->application->parseCompose(pull_request_id: $this->pull_request_id);
$yaml = Yaml::dump($composeFile->toArray(), 10);
$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);
$networkId = $this->application->uuid;
if ($this->pull_request_id !== 0) {
$networkId = "{$this->application->uuid}-{$this->pull_request_id}";
}
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
]);
}
if (isset($this->docker_compose_base64)) {
$readme = generate_readme_file($this->application->name, $this->application_deployment_queue->updated_at);
$composeFileName = "$this->configuration_dir/docker-compose.yml";
if ($this->pull_request_id !== 0) {
$composeFileName = "$this->configuration_dir/docker-compose-pr-{$this->pull_request_id}.yml";
}
$this->execute_remote_command(
[
"mkdir -p $this->configuration_dir"
],
[
"echo '{$this->docker_compose_base64}' | base64 -d > $composeFileName",
],
[
"echo '{$readme}' > $this->configuration_dir/README.md",
]
);
}
$this->start_by_compose_file();
}
private function deploy_dockerfile_buildpack() private function deploy_dockerfile_buildpack()
{ {
if (data_get($this->application, 'dockerfile_location')) { if (data_get($this->application, 'dockerfile_location')) {
@@ -472,7 +510,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
// $this->push_to_docker_registry(); // $this->push_to_docker_registry();
// $this->deploy_to_additional_destinations(); // $this->deploy_to_additional_destinations();
// } else { // } else {
$this->rolling_update(); $this->rolling_update();
// } // }
} }
private function deploy_nixpacks_buildpack() private function deploy_nixpacks_buildpack()
@@ -489,6 +527,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if (!$this->force_rebuild) { if (!$this->force_rebuild) {
$this->check_image_locally_or_remotely(); $this->check_image_locally_or_remotely();
if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) { if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) {
$this->create_workdir();
$this->execute_remote_command([ $this->execute_remote_command([
"echo 'No configuration changed & image found ({$this->production_image_name}) with the same Git Commit SHA. Build step skipped.'", "echo 'No configuration changed & image found ({$this->production_image_name}) with the same Git Commit SHA. Build step skipped.'",
]); ]);
@@ -531,74 +570,83 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function rolling_update() private function rolling_update()
{ {
if (count($this->application->ports_mappings_array) > 0) { if ($this->server->isSwarm()) {
$this->execute_remote_command( // Skip this.
[
"echo '\n----------------------------------------'",
],
["echo -n 'Application has ports mapped to the host system, rolling update is not supported.'"],
);
$this->stop_running_container(force: true);
$this->start_by_compose_file();
} else { } else {
$this->execute_remote_command( if (count($this->application->ports_mappings_array) > 0) {
[ $this->execute_remote_command(
"echo '\n----------------------------------------'", [
], "echo '\n----------------------------------------'",
["echo -n 'Rolling update started.'"], ],
); ["echo -n 'Application has ports mapped to the host system, rolling update is not supported.'"],
$this->start_by_compose_file(); );
$this->health_check(); $this->stop_running_container(force: true);
$this->stop_running_container(); $this->start_by_compose_file();
} else {
$this->execute_remote_command(
[
"echo '\n----------------------------------------'",
],
["echo -n 'Rolling update started.'"],
);
$this->start_by_compose_file();
$this->health_check();
$this->stop_running_container();
$this->application_deployment_queue->addLogEntry("Rolling update completed.");
}
} }
} }
private function health_check() private function health_check()
{ {
if ($this->application->isHealthcheckDisabled()) { if ($this->server->isSwarm()) {
$this->newVersionIsHealthy = true; // Implement healthcheck for swarm
return; } else {
} if ($this->application->isHealthcheckDisabled()) {
// ray('New container name: ', $this->container_name); $this->newVersionIsHealthy = true;
if ($this->container_name) { return;
$counter = 1; }
$this->execute_remote_command( // ray('New container name: ', $this->container_name);
[ if ($this->container_name) {
"echo 'Waiting for healthcheck to pass on the new container.'" $counter = 1;
]
);
if ($this->full_healthcheck_url) {
$this->execute_remote_command( $this->execute_remote_command(
[ [
"echo 'Healthcheck URL (inside the container): {$this->full_healthcheck_url}'" "echo 'Waiting for healthcheck to pass on the new container.'"
] ]
); );
} if ($this->full_healthcheck_url) {
while ($counter < $this->application->health_check_retries) {
$this->execute_remote_command(
[
"docker inspect --format='{{json .State.Health.Status}}' {$this->container_name}",
"hidden" => true,
"save" => "health_check"
],
);
$this->execute_remote_command(
[
"echo 'Attempt {$counter} of {$this->application->health_check_retries} | Healthcheck status: {$this->saved_outputs->get('health_check')}'"
],
);
if (Str::of($this->saved_outputs->get('health_check'))->contains('healthy')) {
$this->newVersionIsHealthy = true;
$this->application->update(['status' => 'running']);
$this->execute_remote_command( $this->execute_remote_command(
[ [
"echo 'New container is healthy.'" "echo 'Healthcheck URL (inside the container): {$this->full_healthcheck_url}'"
]
);
}
while ($counter < $this->application->health_check_retries) {
$this->execute_remote_command(
[
"docker inspect --format='{{json .State.Health.Status}}' {$this->container_name}",
"hidden" => true,
"save" => "health_check"
],
);
$this->execute_remote_command(
[
"echo 'Attempt {$counter} of {$this->application->health_check_retries} | Healthcheck status: {$this->saved_outputs->get('health_check')}'"
], ],
); );
break; if (Str::of($this->saved_outputs->get('health_check'))->contains('healthy')) {
$this->newVersionIsHealthy = true;
$this->application->update(['status' => 'running']);
$this->execute_remote_command(
[
"echo 'New container is healthy.'"
],
);
break;
}
$counter++;
sleep($this->application->health_check_interval);
} }
$counter++;
sleep($this->application->health_check_interval);
} }
} }
} }
@@ -618,8 +666,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(
@@ -627,7 +675,14 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up -d"), "hidden" => true], [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up -d"), "hidden" => true],
); );
} }
private function create_workdir()
{
$this->execute_remote_command(
[
"command" => executeInDocker($this->deployment_uuid, "mkdir -p {$this->workdir}")
],
);
}
private function prepare_builder_image() private function prepare_builder_image()
{ {
$helperImage = config('coolify.helper_image'); $helperImage = config('coolify.helper_image');
@@ -636,9 +691,9 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->dockerConfigFileExists = instant_remote_process(["test -f {$this->serverUserHomeDir}/.docker/config.json && echo 'OK' || echo 'NOK'"], $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} --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} --name {$this->deployment_uuid} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
} }
$this->execute_remote_command( $this->execute_remote_command(
[ [
@@ -651,6 +706,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
[ [
"command" => executeInDocker($this->deployment_uuid, "mkdir -p {$this->basedir}") "command" => executeInDocker($this->deployment_uuid, "mkdir -p {$this->basedir}")
], ],
); );
} }
private function deploy_to_additional_destinations() private function deploy_to_additional_destinations()
@@ -725,13 +781,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----------------------------------------'",
],
[
"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
] ]
@@ -740,90 +795,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 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 {$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()
@@ -849,7 +827,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}\"";
} }
@@ -899,21 +881,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 = [
@@ -924,7 +891,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,
@@ -956,6 +922,48 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
] ]
] ]
]; ];
if ($this->server->isSwarm()) {
data_forget($docker_compose, 'services.' . $this->container_name . '.container_name');
data_forget($docker_compose, 'services.' . $this->container_name . '.expose');
data_forget($docker_compose, 'services.' . $this->container_name . '.restart');
data_forget($docker_compose, 'services.' . $this->container_name . '.mem_limit');
data_forget($docker_compose, 'services.' . $this->container_name . '.memswap_limit');
data_forget($docker_compose, 'services.' . $this->container_name . '.mem_swappiness');
data_forget($docker_compose, 'services.' . $this->container_name . '.mem_reservation');
data_forget($docker_compose, 'services.' . $this->container_name . '.cpus');
data_forget($docker_compose, 'services.' . $this->container_name . '.cpuset');
data_forget($docker_compose, 'services.' . $this->container_name . '.cpu_shares');
$docker_compose['services'][$this->container_name]['deploy'] = [
'placement' => [
'constraints' => [
'node.role == worker'
]
],
'mode' => 'replicated',
'replicas' => 1,
'update_config' => [
'order' => 'start-first'
],
'rollback_config' => [
'order' => 'start-first'
],
'labels' => $labels,
'resources' => [
'limits' => [
'cpus' => $this->application->limits_cpus,
'memory' => $this->application->limits_memory,
],
'reservations' => [
'cpus' => $this->application->limits_cpus,
'memory' => $this->application->limits_memory,
]
]
];
} else {
$docker_compose['services'][$this->container_name]['labels'] = $labels;
}
if ($this->server->isLogDrainEnabled() && $this->application->isLogDrainEnabled()) { if ($this->server->isLogDrainEnabled() && $this->application->isLogDrainEnabled()) {
$docker_compose['services'][$this->container_name]['logging'] = [ $docker_compose['services'][$this->container_name]['logging'] = [
'driver' => 'fluentd', 'driver' => 'fluentd',
@@ -967,7 +975,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
]; ];
} }
if ($this->application->settings->is_gpu_enabled) { if ($this->application->settings->is_gpu_enabled) {
ray('asd');
$docker_compose['services'][$this->container_name]['deploy']['resources']['reservations']['devices'] = [ $docker_compose['services'][$this->container_name]['deploy']['resources']['reservations']['devices'] = [
[ [
'driver' => data_get($this->application, 'settings.gpu_driver', 'nvidia'), 'driver' => data_get($this->application, 'settings.gpu_driver', 'nvidia'),
@@ -1004,6 +1011,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]);
@@ -1206,14 +1218,10 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
private function stop_running_container(bool $force = false) private function stop_running_container(bool $force = false)
{ {
$this->execute_remote_command(["echo -n 'Removing old container.'"]); $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;
}); });
@@ -1224,14 +1232,9 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
[executeInDocker($this->deployment_uuid, "docker rm -f $containerName >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true], [executeInDocker($this->deployment_uuid, "docker rm -f $containerName >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true],
); );
}); });
$this->execute_remote_command(
[
"echo 'Rolling update completed.'"
],
);
} 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 container is not healthy, rolling back to the old container.'"],
[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],
); );
} }
@@ -1240,8 +1243,8 @@ 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],
[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],
); );
@@ -1250,6 +1253,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true], [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true],
); );
} }
$this->application_deployment_queue->addLogEntry("New container started.");
} }
private function generate_build_env_variables() private function generate_build_env_variables()
@@ -1274,9 +1278,14 @@ 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}");
}
} }
$dockerfile_base64 = base64_encode($dockerfile->implode("\n")); $dockerfile_base64 = base64_encode($dockerfile->implode("\n"));
$this->execute_remote_command([ $this->execute_remote_command([
@@ -1307,9 +1316,13 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$this->execute_remote_command( $this->execute_remote_command(
["echo 'Oops something is not okay, are you okay? 😢'", 'type' => 'err'], ["echo 'Oops something is not okay, are you okay? 😢'", 'type' => 'err'],
["echo '{$exception->getMessage()}'", 'type' => 'err'], ["echo '{$exception->getMessage()}'", 'type' => 'err'],
["echo -n 'Deployment failed. Removing the new version of your application.'", 'type' => 'err'],
[executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true]
); );
if ($this->application->build_pack !== 'dockercompose') {
$this->execute_remote_command(
["echo -n 'Deployment failed. Removing the new version of your application.'", 'type' => 'err'],
[executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true]
);
}
$this->next(ApplicationDeploymentStatus::FAILED->value); $this->next(ApplicationDeploymentStatus::FAILED->value);
} }

View File

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

View File

@@ -2,6 +2,7 @@
namespace App\Jobs; namespace App\Jobs;
use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Proxy\CheckProxy; use App\Actions\Proxy\CheckProxy;
use App\Actions\Proxy\StartProxy; use App\Actions\Proxy\StartProxy;
use App\Models\ApplicationPreview; use App\Models\ApplicationPreview;
@@ -21,10 +22,6 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(public Server $server)
{
$this->handle();
}
public function middleware(): array public function middleware(): array
{ {
return [(new WithoutOverlapping($this->server->id))->dontRelease()]; return [(new WithoutOverlapping($this->server->id))->dontRelease()];
@@ -35,34 +32,75 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
return $this->server->id; return $this->server->id;
} }
public function handle(): void public function __construct(public Server $server)
{
if (isDev()) $this->handle();
}
public function handle()
{ {
// ray("checking container statuses for {$this->server->id}"); // ray("checking container statuses for {$this->server->id}");
try { try {
if (!$this->server->isServerReady()) { if (!$this->server->isServerReady()) {
return; return;
}; };
$containers = instant_remote_process(["docker container ls -q"], $this->server); if ($this->server->isSwarm()) {
if (!$containers) { $containers = instant_remote_process(["docker service inspect $(docker service ls -q) --format '{{json .}}'"], $this->server, false);
$containerReplicase = instant_remote_process(["docker service ls --format '{{json .}}'"], $this->server, false);
} else {
// Precheck for containers
$containers = instant_remote_process(["docker container ls -q"], $this->server, false);
if (!$containers) {
return;
}
$containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this->server, false);
$containerReplicase = null;
}
if (is_null($containers)) {
return; 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();
$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');
}
$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) {
@@ -98,11 +136,25 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
if ($uuid) { if ($uuid) {
$database = $databases->where('uuid', $uuid)->first(); $database = $databases->where('uuid', $uuid)->first();
if ($database) { if ($database) {
$isPublic = data_get($database, 'is_public');
$foundDatabases[] = $database->id; $foundDatabases[] = $database->id;
$statusFromDb = $database->status; $statusFromDb = $database->status;
if ($statusFromDb !== $containerStatus) { if ($statusFromDb !== $containerStatus) {
$database->update(['status' => $containerStatus]); $database->update(['status' => $containerStatus]);
} }
if ($isPublic) {
$foundTcpProxy = $containers->filter(function ($value, $key) use ($uuid) {
if ($this->server->isSwarm()) {
return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid";
} else {
return data_get($value, 'Name') === "/$uuid-proxy";
}
})->first();
if (!$foundTcpProxy) {
StartDatabaseProxy::run($database);
$this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server));
}
}
} else { } else {
// Notify user that this container should not be there. // Notify user that this container should not be there.
} }
@@ -167,7 +219,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']);
} }
@@ -194,7 +246,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) {
@@ -219,7 +271,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) {
@@ -243,22 +295,24 @@ 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 // Check if proxy is running
$this->server->proxyType(); $this->server->proxyType();
$foundProxyContainer = $containers->filter(function ($value, $key) { $foundProxyContainer = $containers->filter(function ($value, $key) {
return data_get($value, 'Name') === '/coolify-proxy'; if ($this->server->isSwarm()) {
return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik';
} else {
return data_get($value, 'Name') === '/coolify-proxy';
}
})->first(); })->first();
if (!$foundProxyContainer) { if (!$foundProxyContainer) {
try { try {
$shouldStart = CheckProxy::run($this->server); $shouldStart = CheckProxy::run($this->server);
if ($shouldStart) { if ($shouldStart) {
StartProxy::run($this->server, false); StartProxy::run($this->server, false);
$this->server->team->notify(new ContainerRestarted('coolify-proxy', $this->server)); $this->server->team?->notify(new ContainerRestarted('coolify-proxy', $this->server));
} else {
ray('Proxy could not be started.');
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
ray($e); ray($e);
@@ -272,7 +326,7 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
} catch (\Throwable $e) { } catch (\Throwable $e) {
send_internal_notification("ContainerStatusJob failed on ({$this->server->id}) 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;

File diff suppressed because it is too large Load Diff

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,17 @@ class Application extends BaseModel
$application->environment_variables_preview()->delete(); $application->environment_variables_preview()->delete();
}); });
} }
public function link()
{
if (data_get($this, 'environment.project.uuid')) {
return route('project.application.configuration', [
'project_uuid' => data_get($this, 'environment.project.uuid'),
'environment_name' => data_get($this, 'environment.name'),
'application_uuid' => data_get($this, 'uuid')
]);
}
return null;
}
public function settings() public function settings()
{ {
return $this->hasOne(ApplicationSetting::class); return $this->hasOne(ApplicationSetting::class);
@@ -123,6 +136,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 +200,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,7 +276,6 @@ 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();
@@ -342,4 +393,289 @@ class Application extends BaseModel
} }
return false; 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 --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} --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);
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,39 @@ class Server extends BaseModel
{ {
return $this->hasOne(ServerSetting::class); return $this->hasOne(ServerSetting::class);
} }
public function addInitialNetwork() {
if ($this->id === 0) {
if ($this->isSwarm()) {
SwarmDocker::create([
'id' => 0,
'name' => 'coolify',
'network' => 'coolify-overlay',
'server_id' => $this->id,
]);
} else {
StandaloneDocker::create([
'id' => 0,
'name' => 'coolify',
'network' => 'coolify',
'server_id' => $this->id,
]);
}
} else {
if ($this->isSwarm()) {
SwarmDocker::create([
'name' => 'coolify-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');
@@ -189,6 +210,13 @@ class Server extends BaseModel
{ {
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;
@@ -217,6 +245,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);
@@ -302,9 +347,9 @@ class Server extends BaseModel
} }
public function isLogDrainEnabled() 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 || $this->settings->is_logdrain_custom_enabled;
} }
public function validateOS() public function validateOS(): bool | Stringable
{ {
$os_release = instant_remote_process(['cat /etc/os-release'], $this); $os_release = instant_remote_process(['cat /etc/os-release'], $this);
$datas = collect(explode("\n", $os_release)); $datas = collect(explode("\n", $os_release));
@@ -314,23 +359,31 @@ class Server extends BaseModel
$collectedData->put($item->before('=')->value(), $item->after('=')->lower()->replace('"', '')->value()); $collectedData->put($item->before('=')->value(), $item->after('=')->lower()->replace('"', '')->value());
} }
$ID = data_get($collectedData, 'ID'); $ID = data_get($collectedData, 'ID');
$ID_LIKE = data_get($collectedData, 'ID_LIKE'); // $ID_LIKE = data_get($collectedData, 'ID_LIKE');
$VERSION_ID = data_get($collectedData, 'VERSION_ID'); // $VERSION_ID = data_get($collectedData, 'VERSION_ID');
// ray($ID, $ID_LIKE, $VERSION_ID); $supported = collect(SUPPORTED_OS)->filter(function ($supportedOs) use ($ID) {
if (collect(SUPPORTED_OS)->contains($ID_LIKE)) { if (str($supportedOs)->contains($ID)) {
return str($ID);
}
});
if ($supported->count() === 1) {
ray('supported'); ray('supported');
return str($ID_LIKE)->explode(' ')->first(); return str($supported->first());
} else { } else {
ray('not supported'); ray('not supported');
return false; return false;
} }
} }
public function isSwarm()
{
return data_get($this, 'settings.is_swarm_manager') || data_get($this, 'settings.is_swarm_worker');
}
public function validateConnection() public function validateConnection()
{ {
$server = Server::find($this->id);
if ($this->skipServer()) { if ($this->skipServer()) {
return false; return false;
} }
$uptime = instant_remote_process(['uptime'], $this, false); $uptime = instant_remote_process(['uptime'], $this, false);
if (!$uptime) { if (!$uptime) {
$this->settings()->update([ $this->settings()->update([
@@ -341,14 +394,14 @@ class Server extends BaseModel
$this->settings()->update([ $this->settings()->update([
'is_reachable' => true, 'is_reachable' => true,
]); ]);
$this->update([ $server->update([
'unreachable_count' => 0, 'unreachable_count' => 0,
]); ]);
} }
if (data_get($this, 'unreachable_notification_sent') === true) { if (data_get($this, 'unreachable_notification_sent') === true) {
$this->team->notify(new Revived($this)); $this->team->notify(new Revived($this));
$this->update(['unreachable_notification_sent' => false]); $server->update(['unreachable_notification_sent' => false]);
} }
return true; return true;
@@ -366,7 +419,20 @@ class Server extends BaseModel
} }
$this->settings->is_usable = true; $this->settings->is_usable = true;
$this->settings->save(); $this->settings->save();
$this->validateCoolifyNetwork(); $this->validateCoolifyNetwork(isSwarm: false);
return true;
}
public function validateDockerSwarm()
{
$swarmStatus = instant_remote_process(["docker info|grep -i swarm"], $this, false);
$swarmStatus = str($swarmStatus)->trim()->after(':')->trim();
if ($swarmStatus === 'inactive') {
throw new \Exception('Docker Swarm is not initiated. Please join the server to a swarm before continuing.');
return false;
}
$this->settings->is_usable = true;
$this->settings->save();
$this->validateCoolifyNetwork(isSwarm: true);
return true; return true;
} }
public function validateDockerEngineVersion() public function validateDockerEngineVersion()
@@ -383,8 +449,91 @@ class Server extends BaseModel
$this->settings->save(); $this->settings->save();
return true; return true;
} }
public function validateCoolifyNetwork() public function validateCoolifyNetwork($isSwarm = false)
{ {
return instant_remote_process(["docker network create coolify --attachable >/dev/null 2>&1 || true"], $this, false); if ($isSwarm) {
return instant_remote_process(["docker network create --attachable --driver overlay coolify-overlay >/dev/null 2>&1 || true"], $this, false);
} else {
return instant_remote_process(["docker network create coolify --attachable >/dev/null 2>&1 || true"], $this, false);
}
}
public function executeRemoteCommand(Collection $commands, ?ApplicationDeploymentQueue $loggingModel = null)
{
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,7 +52,7 @@ 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([]); $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();
@@ -105,7 +105,7 @@ class Service extends BaseModel
$fields->put('MinIO', $data->toArray()); $fields->put('MinIO', $data->toArray());
break; break;
case str($image)->contains('weblate'): case str($image)?->contains('weblate'):
$data = collect([]); $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();
@@ -130,8 +130,84 @@ class Service extends BaseModel
]); ]);
} }
$fields->put('Weblate', $data); $fields->put('Weblate', $data);
break;
case str($image)?->contains('meilisearch'):
$data = collect([]);
$SERVICE_PASSWORD_MEILISEARCH = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_MEILISEARCH')->first();
if ($SERVICE_PASSWORD_MEILISEARCH) {
$data = $data->merge([
'API Key' => [
'key' => data_get($SERVICE_PASSWORD_MEILISEARCH, 'key'),
'value' => data_get($SERVICE_PASSWORD_MEILISEARCH, 'value'),
'isPassword' => true,
],
]);
}
$fields->put('Meilisearch', $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;
} }
} }
ray($fields);
$databases = $this->databases()->get(); $databases = $this->databases()->get();
foreach ($databases as $database) { foreach ($databases as $database) {
@@ -300,6 +376,17 @@ class Service extends BaseModel
} }
} }
} }
public function link()
{
if (data_get($this, 'environment.project.uuid')) {
return route('project.service.configuration', [
'project_uuid' => data_get($this, 'environment.project.uuid'),
'environment_name' => data_get($this, 'environment.name'),
'service_uuid' => data_get($this, 'uuid')
]);
}
return null;
}
public function documentation() public function documentation()
{ {
$services = getServiceTemplates(); $services = getServiceTemplates();
@@ -371,521 +458,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', []));
if ($serviceLabels->count() > 0) {
$removedLabels = collect([]);
$serviceLabels = $serviceLabels->filter(function ($serviceLabel, $serviceLabelName) use ($removedLabels) {
if (!str($serviceLabel)->contains('=')) {
$removedLabels->put($serviceLabelName, $serviceLabel);
return false;
}
return $serviceLabel;
});
foreach($removedLabels as $removedLabelName =>$removedLabel) {
$serviceLabels->push("$removedLabelName=$removedLabel");
}
}
$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->isLogDrainEnabled() && $savedService->isLogDrainEnabled()) {
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

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

View File

@@ -48,6 +48,17 @@ class StandaloneMongodb extends BaseModel
{ {
return data_get($this, 'is_log_drain_enabled', false); return data_get($this, 'is_log_drain_enabled', false);
} }
public function link()
{
if (data_get($this, 'environment.project.uuid')) {
return route('project.database.configuration', [
'project_uuid' => data_get($this, 'environment.project.uuid'),
'environment_name' => data_get($this, 'environment.name'),
'database_uuid' => data_get($this, 'uuid')
]);
}
return null;
}
public function mongoInitdbRootPassword(): Attribute public function mongoInitdbRootPassword(): Attribute
{ {
return Attribute::make( return Attribute::make(

View File

@@ -41,6 +41,17 @@ class StandaloneMysql extends BaseModel
$database->environment_variables()->delete(); $database->environment_variables()->delete();
}); });
} }
public function link()
{
if (data_get($this, 'environment.project.uuid')) {
return route('project.database.configuration', [
'project_uuid' => data_get($this, 'environment.project.uuid'),
'environment_name' => data_get($this, 'environment.name'),
'database_uuid' => data_get($this, 'uuid')
]);
}
return null;
}
public function type(): string public function type(): string
{ {
return 'standalone-mysql'; return 'standalone-mysql';

View File

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

View File

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

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

@@ -12,6 +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++;
@@ -23,8 +24,6 @@ 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) {
@@ -49,32 +48,29 @@ trait ExecuteRemoteCommand
'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

@@ -36,8 +36,6 @@ function queue_application_deployment(int $application_id, string $deployment_uu
if ($running_deployments->count() > 0) { if ($running_deployments->count() > 0) {
return; return;
} }
// New deployment
// dispatchDeploymentJob($deployment->id);
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');
@@ -48,34 +46,11 @@ function queue_next_deployment(Application $application)
{ {
$next_found = ApplicationDeploymentQueue::where('application_id', $application->id)->where('status', 'queued')->first(); $next_found = ApplicationDeploymentQueue::where('application_id', $application->id)->where('status', 'queued')->first();
if ($next_found) { if ($next_found) {
// New deployment
// dispatchDeploymentJob($next_found->id);
dispatch(new ApplicationDeploymentJob( dispatch(new ApplicationDeploymentJob(
application_deployment_queue_id: $next_found->id, application_deployment_queue_id: $next_found->id,
))->onConnection('long-running')->onQueue('long-running'); ))->onConnection('long-running')->onQueue('long-running');
} }
} }
function dispatchDeploymentJob($id)
{
$applicationQueue = ApplicationDeploymentQueue::find($id);
$application = Application::find($applicationQueue->application_id);
$isRestartOnly = data_get($applicationQueue, 'restart_only');
$isSimpleDockerFile = data_get($application, 'dockerfile');
$isDockerImage = data_get($application, 'build_pack') === 'dockerimage';
if ($isRestartOnly) {
ApplicationRestartJob::dispatch(applicationDeploymentQueueId: $id)->onConnection('long-running')->onQueue('long-running');
} else if ($isSimpleDockerFile) {
ApplicationDeploySimpleDockerfileJob::dispatch(applicationDeploymentQueueId: $id)->onConnection('long-running')->onQueue('long-running');
} else if ($isDockerImage) {
ApplicationDeployDockerImageJob::dispatch(applicationDeploymentQueueId: $id)->onConnection('long-running')->onQueue('long-running');
} else {
throw new Exception('Unknown build pack');
}
}
// Deployment things // Deployment things
function generateHostIpMapping(Server $server, string $network) function generateHostIpMapping(Server $server, string $network)
{ {
@@ -122,12 +97,12 @@ function prepareHelperContainer(Server $server, string $network, string $deploym
$commands = collect([]); $commands = collect([]);
if ($dockerConfigFileExists === 'OK') { if ($dockerConfigFileExists === 'OK') {
$commands->push([ $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", "command" => "docker run -d --network $network --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, "hidden" => true,
]); ]);
} else { } else {
$commands->push([ $commands->push([
"command" => "docker run -d --network {$network} -v /:/host --name {$deploymentUuid} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}", "command" => "docker run -d --network {$network} --name {$deploymentUuid} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}",
"hidden" => true, "hidden" => true,
]); ]);
} }
@@ -201,7 +176,6 @@ function generateComposeFile(string $deploymentUuid, Server $server, string $net
]; ];
} }
if ($application->settings->is_gpu_enabled) { if ($application->settings->is_gpu_enabled) {
ray('asd');
$docker_compose['services'][$containerName]['deploy']['resources']['reservations']['devices'] = [ $docker_compose['services'][$containerName]['deploy']['resources']['reservations']['devices'] = [
[ [
'driver' => data_get($application, 'settings.gpu_driver', 'nvidia'), 'driver' => data_get($application, 'settings.gpu_driver', 'nvidia'),
@@ -302,39 +276,37 @@ function generateEnvironmentVariables(Application $application, $ports, int $pul
return $environment_variables->all(); return $environment_variables->all();
} }
function rollingUpdate(Application $application, string $deploymentUuid) function startNewApplication(Application $application, string $deploymentUuid, ApplicationDeploymentQueue $loggingModel)
{ {
$commands = collect([]); $commands = collect([]);
$workDir = generateWorkdir($deploymentUuid, $application); $workDir = generateWorkdir($deploymentUuid, $application);
if (count($application->ports_mappings_array) > 0) { if ($application->build_pack === 'dockerimage') {
// $this->execute_remote_command( $loggingModel->addLogEntry('Pulling latest images from the registry.');
// [ $commands->push(
// "echo '\n----------------------------------------'", [
// ], "command" => executeInDocker($deploymentUuid, "docker compose --project-directory {$workDir} pull"),
// ["echo -n 'Application has ports mapped to the host system, rolling update is not supported.'"], "hidden" => true
// ); ],
// $this->stop_running_container(force: true); [
// $this->start_by_compose_file(); "command" => executeInDocker($deploymentUuid, "docker compose --project-directory {$workDir} up --build -d"),
"hidden" => true
],
);
} else { } else {
$commands->push( $commands->push(
[ [
"command" => "echo '\n----------------------------------------'" "command" => executeInDocker($deploymentUuid, "docker compose --project-directory {$workDir} up --build -d"),
"hidden" => true
], ],
[
"command" => "echo -n 'Rolling update started.'"
]
); );
if ($application->build_pack === 'dockerimage') {
$commands->push(
["echo -n 'Pulling latest images from the registry.'"],
[executeInDocker($deploymentUuid, "docker compose --project-directory {$workDir} pull"), "hidden" => true],
[executeInDocker($deploymentUuid, "docker compose --project-directory {$workDir} up --build -d"), "hidden" => true],
);
} else {
$commands->push(
[executeInDocker($deploymentUuid, "docker compose --project-directory {$workDir} up --build -d"), "hidden" => true],
);
}
return $commands;
} }
return $commands;
}
function removeOldDeployment(string $containerName)
{
$commands = collect([]);
$commands->push(
["docker rm -f $containerName >/dev/null 2>&1"],
);
return $commands;
} }

View File

@@ -28,7 +28,9 @@ const SPECIFIC_SERVICES = [
'quay.io/minio/minio', 'quay.io/minio/minio',
]; ];
// Based on /etc/os-release
const SUPPORTED_OS = [ const SUPPORTED_OS = [
'debian', 'ubuntu debian raspbian',
'rhel centos fedora' '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,14 +164,15 @@ 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;

File diff suppressed because it is too large Load Diff

View File

@@ -22,13 +22,13 @@ return [
'official' => 'https://cdn.coollabs.io/coolify/service-templates.json', 'official' => 'https://cdn.coollabs.io/coolify/service-templates.json',
], ],
'limits' => [ 'limits' => [
'trial_period' => 7, 'trial_period' => 0,
'server' => [ 'server' => [
'zero' => 0, 'zero' => 0,
'self-hosted' => 999999999999, 'self-hosted' => 999999999999,
'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://396748153b19c469f5ceff50f1664323@o1082494.ingest.sentry.io/4505347448045568', 'dsn' => 'https://bea22abf110618b07252032aa2e07859@o1082494.ingest.sentry.io/4505347448045568',
// The release version of your application // The release version of your application
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')) // Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
'release' => '4.0.0-beta.145', 'release' => '4.0.0-beta.152',
// When left empty or `null` the Laravel environment will be used // When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'), 'environment' => config('app.env'),

View File

@@ -1,3 +1,3 @@
<?php <?php
return '4.0.0-beta.145'; return '4.0.0-beta.152';

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

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

View File

@@ -0,0 +1,32 @@
<?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->boolean('is_logdrain_custom_enabled')->default(false);
$table->text('logdrain_custom_config')->nullable();
$table->text('logdrain_custom_config_parser')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('server_settings', function (Blueprint $table) {
$table->dropColumn('is_logdrain_custom_enabled');
$table->dropColumn('logdrain_custom_config');
$table->dropColumn('logdrain_custom_config_parser');
});
}
};

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

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

View File

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

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

View File

@@ -24,13 +24,16 @@
helper="Allow Git LFS during build process." /> helper="Allow Git LFS during build process." />
@endif @endif
<form wire:submit.prevent="submit"> <form wire:submit.prevent="submit">
<div class="flex gap-2"> @if ($application->build_pack !== 'dockercompose')
<x-forms.checkbox helper="Enable GPU usage for this application. More info <a href='https://docs.docker.com/compose/gpu-support/' class='text-white underline' target='_blank'>here</a>." instantSave <div class="flex gap-2">
id="application.settings.is_gpu_enabled" label="GPU Enabled Application" /> <x-forms.checkbox
@if ($application->settings->is_gpu_enabled) helper="Enable GPU usage for this application. More info <a href='https://docs.docker.com/compose/gpu-support/' class='text-white underline' target='_blank'>here</a>."
<x-forms.button type="submiot">Save</x-forms.button> instantSave id="application.settings.is_gpu_enabled" label="GPU Enabled Application" />
@endif @if ($application->settings->is_gpu_enabled)
</div> <x-forms.button type="submiot">Save</x-forms.button>
@endif
</div>
@endif
@if ($application->settings->is_gpu_enabled) @if ($application->settings->is_gpu_enabled)
<div class="flex flex-col w-full gap-2 p-2 xl:flex-row"> <div class="flex flex-col w-full gap-2 p-2 xl:flex-row">
<x-forms.input label="GPU Driver" id="application.settings.gpu_driver"> </x-forms.input> <x-forms.input label="GPU Driver" id="application.settings.gpu_driver"> </x-forms.input>

View File

@@ -20,7 +20,7 @@
<a :class="activeTab === 'server' && 'text-white'" <a :class="activeTab === 'server' && 'text-white'"
@click.prevent="activeTab = 'server'; window.location.hash = 'server'" href="#">Server @click.prevent="activeTab = 'server'; window.location.hash = 'server'" href="#">Server
</a> </a>
@if ($application->build_pack !== 'static') @if ($application->build_pack !== 'static' && $application->build_pack !== 'dockercompose')
<a :class="activeTab === 'storages' && 'text-white'" <a :class="activeTab === 'storages' && 'text-white'"
@click.prevent="activeTab = 'storages'; window.location.hash = 'storages'" href="#">Storages @click.prevent="activeTab = 'storages'; window.location.hash = 'storages'" href="#">Storages
</a> </a>
@@ -34,7 +34,7 @@
Deployments Deployments
</a> </a>
@endif @endif
@if ($application->build_pack !== 'static') @if ($application->build_pack !== 'static' && $application->build_pack !== 'dockercompose')
<a :class="activeTab === 'health' && 'text-white'" <a :class="activeTab === 'health' && 'text-white'"
@click.prevent="activeTab = 'health'; window.location.hash = 'health'" href="#">Healthchecks @click.prevent="activeTab = 'health'; window.location.hash = 'health'" href="#">Healthchecks
</a> </a>
@@ -42,10 +42,12 @@
<a :class="activeTab === 'rollback' && 'text-white'" <a :class="activeTab === 'rollback' && 'text-white'"
@click.prevent="activeTab = 'rollback'; window.location.hash = 'rollback'" href="#">Rollback @click.prevent="activeTab = 'rollback'; window.location.hash = 'rollback'" href="#">Rollback
</a> </a>
<a :class="activeTab === 'resource-limits' && 'text-white'" @if ($application->build_pack !== 'dockercompose')
@click.prevent="activeTab = 'resource-limits'; window.location.hash = 'resource-limits'" <a :class="activeTab === 'resource-limits' && 'text-white'"
href="#">Resource Limits @click.prevent="activeTab = 'resource-limits'; window.location.hash = 'resource-limits'"
</a> href="#">Resource Limits
</a>
@endif
<a :class="activeTab === 'danger' && 'text-white'" <a :class="activeTab === 'danger' && 'text-white'"
@click.prevent="activeTab = 'danger'; window.location.hash = 'danger'" href="#">Danger Zone @click.prevent="activeTab = 'danger'; window.location.hash = 'danger'" href="#">Danger Zone
</a> </a>

View File

@@ -49,7 +49,7 @@
<div @class([ <div @class([
'font-mono whitespace-pre-line', 'font-mono whitespace-pre-line',
'text-warning' => $line['hidden'], 'text-warning' => $line['hidden'],
'text-error' => $line['type'] == 'stderr', 'text-red-500' => $line['type'] == 'stderr',
])>[{{ $line['timestamp'] }}] @if ($line['hidden']) ])>[{{ $line['timestamp'] }}] @if ($line['hidden'])
<br>COMMAND: <br>{{ $line['command'] }} <br><br>OUTPUT: <br>COMMAND: <br>{{ $line['command'] }} <br><br>OUTPUT:
@endif{{ $line['output'] }}@if ($line['hidden']) @endif{{ $line['output'] }}@if ($line['hidden'])

View File

@@ -1,5 +1,5 @@
<div class="flex items-center gap-2 pb-4"> <div class="flex items-center gap-2 pb-4">
<h2>Logs</h2> <h2>Deployment Log</h2>
@if ($is_debug_enabled) @if ($is_debug_enabled)
<x-forms.button wire:click.prevent="show_debug">Hide Debug Logs</x-forms.button> <x-forms.button wire:click.prevent="show_debug">Hide Debug Logs</x-forms.button>
@else @else

View File

@@ -16,20 +16,22 @@
<x-forms.input id="application.name" label="Name" required /> <x-forms.input id="application.name" label="Name" required />
<x-forms.input id="application.description" label="Description" /> <x-forms.input id="application.description" label="Description" />
</div> </div>
<div class="flex items-end gap-2"> @if ($application->build_pack !== 'dockercompose')
<x-forms.input placeholder="https://coolify.io" id="application.fqdn" label="Domains" <div class="flex items-end gap-2">
helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io, https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. " /> <x-forms.input placeholder="https://coolify.io" id="application.fqdn" label="Domains"
<x-forms.button wire:click="getWildcardDomain">Generate Domain helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io, https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. " />
</x-forms.button> <x-forms.button wire:click="getWildcardDomain">Generate Domain
</div> </x-forms.button>
@if (!$application->dockerfile) </div>
@endif
@if (!$application->dockerfile && $application->build_pack !== 'dockerimage')
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<div class="flex gap-2"> <div class="flex gap-2">
<x-forms.select wire:model="application.build_pack" label="Build Pack" required> <x-forms.select wire:model="application.build_pack" label="Build Pack" required>
<option value="nixpacks">Nixpacks</option> <option value="nixpacks">Nixpacks</option>
<option value="static">Static</option> <option value="static">Static</option>
<option value="dockerfile">Dockerfile</option> <option value="dockerfile">Dockerfile</option>
<option value="dockerimage">Docker Image</option> <option value="dockercompose">Docker Compose</option>
</x-forms.select> </x-forms.select>
@if ($application->settings->is_static || $application->build_pack === 'static') @if ($application->settings->is_static || $application->build_pack === 'static')
<x-forms.select id="application.static_image" label="Static Image" required> <x-forms.select id="application.static_image" label="Static Image" required>
@@ -45,28 +47,65 @@
helper="If your application is a static site or the final build assets should be served as a static site, enable this." /> helper="If your application is a static site or the final build assets should be served as a static site, enable this." />
</div> </div>
@endif @endif
@if ($application->build_pack === 'dockercompose')
@if (count($parsedServices) > 0)
@foreach (data_get($parsedServices, 'services') as $serviceName => $service)
@if (!isDatabaseImage(data_get($service, 'image')))
<div class="flex items-end gap-2">
<x-forms.input
helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io, https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. "
label="Domains for {{ str($serviceName)->headline() }}"
id="parsedServiceDomains.{{ $serviceName }}.domain"></x-forms.input>
@if (!data_get($parsedServiceDomains, "$serviceName.domain"))
<x-forms.button wire:click="generateDomain('{{ $serviceName }}')">Generate
Domain</x-forms.button>
@endif
</div>
@endif
@endforeach
@endif
@endif
</div> </div>
@endif @endif
<h3>Docker Registry</h3> @if ($application->build_pack !== 'dockerimage' && $application->build_pack !== 'dockercompose')
@if ($application->build_pack !== 'dockerimage') <h3>Docker Registry</h3>
<div>Push the built image to a docker registry. More info <a class="underline" @if ($application->destination->server->isSwarm())
href="https://coolify.io/docs/docker-registries" target="_blank">here</a>.</div> <div>Docker Swarm requires the image to be available in a registry. More info <a class="underline"
@endif href="https://coolify.io/docs/docker-registries" target="_blank">here</a>.</div>
<div class="flex flex-col gap-2 xl:flex-row"> @else
@if ($application->build_pack === 'dockerimage') <div>Push the built image to a docker registry. More info <a class="underline"
<x-forms.input id="application.docker_registry_image_name" label="Docker Image" /> href="https://coolify.io/docs/docker-registries" target="_blank">here</a>.</div>
<x-forms.input id="application.docker_registry_image_tag" label="Docker Image Tag" />
@else
<x-forms.input id="application.docker_registry_image_name"
helper="Empty means it won't push the image to a docker registry."
placeholder="Empty means it won't push the image to a docker registry." label="Docker Image" />
<x-forms.input id="application.docker_registry_image_tag"
placeholder="Empty means only push commit sha tag."
helper="If set, it will tag the built image with this tag too. <br><br>Example: If you set it to 'latest', it will push the image with the commit sha tag + with the latest tag."
label="Docker Image Tag" />
@endif @endif
<div class="flex flex-col gap-2 xl:flex-row">
@if ($application->build_pack === 'dockerimage')
@if ($application->destination->server->isSwarm())
<x-forms.input required id="application.docker_registry_image_name" label="Docker Image" />
<x-forms.input id="application.docker_registry_image_tag" label="Docker Image Tag" />
@else
<x-forms.input id="application.docker_registry_image_name" label="Docker Image" />
<x-forms.input id="application.docker_registry_image_tag" label="Docker Image Tag" />
@endif
@else
@if ($application->destination->server->isSwarm())
<x-forms.input id="application.docker_registry_image_name" required label="Docker Image" />
<x-forms.input id="application.docker_registry_image_tag"
helper="If set, it will tag the built image with this tag too. <br><br>Example: If you set it to 'latest', it will push the image with the commit sha tag + with the latest tag."
label="Docker Image Tag" />
@else
<x-forms.input id="application.docker_registry_image_name"
helper="Empty means it won't push the image to a docker registry."
placeholder="Empty means it won't push the image to a docker registry."
label="Docker Image" />
<x-forms.input id="application.docker_registry_image_tag"
placeholder="Empty means only push commit sha tag."
helper="If set, it will tag the built image with this tag too. <br><br>Example: If you set it to 'latest', it will push the image with the commit sha tag + with the latest tag."
label="Docker Image Tag" />
@endif
</div> @endif
</div>
@endif
@if ($application->build_pack !== 'dockerimage') @if ($application->build_pack !== 'dockerimage')
<h3>Build</h3> <h3>Build</h3>
@@ -91,7 +130,16 @@
@if ($application->build_pack === 'dockerfile' && !$application->dockerfile) @if ($application->build_pack === 'dockerfile' && !$application->dockerfile)
<x-forms.input placeholder="/Dockerfile" id="application.dockerfile_location" <x-forms.input placeholder="/Dockerfile" id="application.dockerfile_location"
label="Dockerfile Location" label="Dockerfile Location"
helper="It is calculated together with the Base Directory: {{ Str::start($application->base_directory . $application->dockerfile_location, '/') }}" /> helper="It is calculated together with the Base Directory:<br><span class='text-warning'>{{ Str::start($application->base_directory . $application->dockerfile_location, '/') }}</span>" />
@endif
@if ($application->build_pack === 'dockercompose')
<span wire:init='loadComposeFile(true)'></span>
<x-forms.input placeholder="/docker-compose.yaml" id="application.docker_compose_location"
label="Docker Compose Location"
helper="It is calculated together with the Base Directory:<br><span class='text-warning'>{{ Str::start($application->base_directory . $application->docker_compose_location, '/') }}</span>" />
{{-- <x-forms.input placeholder="/docker-compose.yaml" id="application.docker_compose_pr_location"
label="Docker Compose Location For Pull Requests"
helper="It is calculated together with the Base Directory:<br><span class='text-warning'>{{ Str::start($application->base_directory . $application->docker_compose_pr_location, '/') }}</span>" /> --}}
@endif @endif
@if ($application->build_pack === 'dockerfile') @if ($application->build_pack === 'dockerfile')
<x-forms.input id="application.dockerfile_target_build" label="Docker Build Stage Target" <x-forms.input id="application.dockerfile_target_build" label="Docker Build Stage Target"
@@ -108,23 +156,33 @@
@endif @endif
</div> </div>
@endif @endif
@if ($application->build_pack === 'dockercompose')
<x-forms.button wire:click="loadComposeFile">Reload Compose File</x-forms.button>
<x-forms.textarea rows="10" readonly id="application.docker_compose"
label="Docker Compose Content" helper="You need to modify the docker compose file." />
{{-- <x-forms.textarea rows="10" readonly id="application.docker_compose_pr"
label="Docker PR Compose Content" helper="You need to modify the docker compose file." /> --}}
@endif
@if ($application->dockerfile) @if ($application->dockerfile)
<x-forms.textarea label="Dockerfile" id="application.dockerfile" rows="6"> </x-forms.textarea> <x-forms.textarea label="Dockerfile" id="application.dockerfile" rows="6"> </x-forms.textarea>
@endif @endif
<h3>Network</h3> @if ($application->build_pack !== 'dockercompose')
<div class="flex flex-col gap-2 xl:flex-row"> <h3>Network</h3>
@if ($application->settings->is_static || $application->build_pack === 'static') <div class="flex flex-col gap-2 xl:flex-row">
<x-forms.input id="application.ports_exposes" label="Ports Exposes" readonly /> @if ($application->settings->is_static || $application->build_pack === 'static')
@else <x-forms.input id="application.ports_exposes" label="Ports Exposes" readonly />
<x-forms.input placeholder="3000,3001" id="application.ports_exposes" label="Ports Exposes" required @else
helper="A comma separated list of ports your application uses. The first port will be used as default healthcheck port if nothing defined in the Healthcheck menu. Be sure to set this correctly." /> <x-forms.input placeholder="3000,3001" id="application.ports_exposes" label="Ports Exposes"
@endif required
<x-forms.input placeholder="3000:3000" id="application.ports_mappings" label="Ports Mappings" helper="A comma separated list of ports your application uses. The first port will be used as default healthcheck port if nothing defined in the Healthcheck menu. Be sure to set this correctly." />
helper="A comma separated list of ports you would like to map to the host system. Useful when you do not want to use domains.<br><br><span class='inline-block font-bold text-warning'>Example:</span><br>3000:3000,3002:3002<br><br>Rolling update is not supported if you have a port mapped to the host." /> @endif
</div> <x-forms.input placeholder="3000:3000" id="application.ports_mappings" label="Ports Mappings"
<x-forms.textarea label="Container Labels" rows="15" id="customLabels"></x-forms.textarea> helper="A comma separated list of ports you would like to map to the host system. Useful when you do not want to use domains.<br><br><span class='inline-block font-bold text-warning'>Example:</span><br>3000:3000,3002:3002<br><br>Rolling update is not supported if you have a port mapped to the host." />
<x-forms.button wire:click="resetDefaultLabels">Reset to Coolify Generated Labels</x-forms.button> </div>
<x-forms.textarea label="Container Labels" rows="15" id="customLabels"></x-forms.textarea>
<x-forms.button wire:click="resetDefaultLabels">Reset to Coolify Generated Labels</x-forms.button>
@endif
</div> </div>
</form> </form>
</div> </div>

View File

@@ -71,15 +71,16 @@
</a> </a>
</div> </div>
<div class="flex items-center gap-2 pt-6"> <div class="flex items-center gap-2 pt-6">
<x-forms.button class="bg-coolgray-500" wire:click="deploy({{ data_get($preview, 'pull_request_id') }})"> <x-forms.button class="bg-coolgray-500"
wire:click="deploy({{ data_get($preview, 'pull_request_id') }})">
@if (data_get($preview, 'status') === 'exited') @if (data_get($preview, 'status') === 'exited')
Deploy Deploy
@else @else
Redeploy Redeploy
@endif @endif
</x-forms.button> </x-forms.button>
<x-forms.button class="bg-coolgray-500" wire:click="stop({{ data_get($preview, 'pull_request_id') }})">Remove <x-forms.button class="bg-coolgray-500"
Preview wire:click="stop({{ data_get($preview, 'pull_request_id') }})">Remove Preview
</x-forms.button> </x-forms.button>
<a <a
href="{{ route('project.application.deployments', [...$parameters, 'pull_request_id' => data_get($preview, 'pull_request_id')]) }}"> href="{{ route('project.application.deployments', [...$parameters, 'pull_request_id' => data_get($preview, 'pull_request_id')]) }}">

View File

@@ -67,7 +67,7 @@
Based on a Docker Compose Based on a Docker Compose
</div> </div>
<div class="description"> <div class="description">
You can deploy complex application easily with Docker Compose. You can deploy complex application easily with Docker Compose, without Git.
</div> </div>
</div> </div>
</div> </div>
@@ -77,7 +77,7 @@
Based on an existing Docker Image Based on an existing Docker Image
</div> </div>
<div class="description"> <div class="description">
You can deploy an existing Docker Image form any Registry. You can deploy an existing Docker Image form any Registry, without Git.
</div> </div>
</div> </div>
</div> </div>
@@ -145,9 +145,9 @@
</div> </div>
</div> --}} </div> --}}
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2" wire:init='loadServices'>
<h2 class="py-4">Services</h2> <h2 class="py-4">Services</h2>
<x-forms.button wire:click='loadServices(true)'>Reload Services List</x-forms.button> <x-forms.button wire:click='loadServices'>Reload Services List</x-forms.button>
<input <input
class="w-full text-white rounded input input-sm bg-coolgray-200 disabled:bg-coolgray-200/50 disabled:border-none placeholder:text-coolgray-500 read-only:text-neutral-500 read-only:bg-coolgray-200/50" class="w-full text-white rounded input input-sm bg-coolgray-200 disabled:bg-coolgray-200/50 disabled:border-none placeholder:text-coolgray-500 read-only:text-neutral-500 read-only:bg-coolgray-200/50"
wire:model.debounce.200ms="search" placeholder="Search..."></input> wire:model.debounce.200ms="search" placeholder="Search..."></input>

View File

@@ -1,4 +1,4 @@
<dialog id="composeModal" class="modal" x-data="{ raw: true }"> <dialog id="composeModal" class="modal" x-data="{ raw: true }" wire:ignore.self>
<form method="dialog" class="flex flex-col gap-2 rounded max-w-7xl modal-box" wire:submit.prevent='submit'> <form method="dialog" class="flex flex-col gap-2 rounded max-w-7xl modal-box" wire:submit.prevent='submit'>
<div class="flex items-end gap-2"> <div class="flex items-end gap-2">
<h1>Docker Compose</h1> <h1>Docker Compose</h1>

View File

@@ -2,8 +2,8 @@
<livewire:project.service.navbar :service="$service" :parameters="$parameters" :query="$query" /> <livewire:project.service.navbar :service="$service" :parameters="$parameters" :query="$query" />
<div class="flex h-full pt-6"> <div class="flex h-full pt-6">
<div class="flex flex-col gap-4 min-w-fit"> <div class="flex flex-col gap-4 min-w-fit">
<a class="{{ request()->routeIs('project.service') ? 'text-white' : '' }}" <a class="{{ request()->routeIs('project.service.configuration') ? 'text-white' : '' }}"
href="{{ route('project.service', [...$parameters, 'service_name' => null]) }}"> href="{{ route('project.service.configuration', [...$parameters, 'service_name' => null]) }}">
<button><- Back</button> <button><- Back</button>
</a> </a>
<a :class="activeTab === 'general' && 'text-white'" <a :class="activeTab === 'general' && 'text-white'"

View File

@@ -18,7 +18,8 @@
</div> </div>
<div class="grid grid-cols-2 gap-2"> <div class="grid grid-cols-2 gap-2">
@foreach ($fields as $serviceName => $field) @foreach ($fields as $serviceName => $field)
<x-forms.input type="{{ data_get($field, 'isPassword') ? 'password' : 'text' }}" required <x-forms.input type="{{ data_get($field, 'isPassword') ? 'password' : 'text' }}"
required="{{ str(data_get($field, 'rules'))?->contains('required') }}"
helper="Variable name: {{ $serviceName }}" helper="Variable name: {{ $serviceName }}"
label="{{ data_get($field, 'serviceName') }} {{ data_get($field, 'name') }}" label="{{ data_get($field, 'serviceName') }} {{ data_get($field, 'name') }}"
id="fields.{{ $serviceName }}.value"></x-forms.input> id="fields.{{ $serviceName }}.value"></x-forms.input>

View File

@@ -1,6 +1,6 @@
<div x-init="$wire.getLogs"> <div x-init="$wire.getLogs">
<div class="flex gap-2"> <div class="flex gap-2">
<h2>Logs</h2> <h4>Container: {{$container}}</h4>
@if ($streamLogs) @if ($streamLogs)
<span wire:poll.2000ms='getLogs(true)' class="loading loading-xs text-warning loading-spinner"></span> <span wire:poll.2000ms='getLogs(true)' class="loading loading-xs text-warning loading-spinner"></span>
@endif @endif
@@ -13,7 +13,7 @@
<x-forms.input label="Only Show Number of Lines" placeholder="1000" required id="numberOfLines"></x-forms.input> <x-forms.input label="Only Show Number of Lines" placeholder="1000" required id="numberOfLines"></x-forms.input>
<x-forms.button type="submit">Refresh</x-forms.button> <x-forms.button type="submit">Refresh</x-forms.button>
</form> </form>
<div id="screen" x-data="{ fullscreen: false, alwaysScroll: false, intervalId: null }" :class="fullscreen ? 'fullscreen' : 'container w-full pt-4 mx-auto'"> <div id="screen" x-data="{ fullscreen: false, alwaysScroll: false, intervalId: null }" :class="fullscreen ? 'fullscreen' : 'container w-full py-4 mx-auto'">
<div class="relative flex flex-col-reverse w-full p-4 pt-6 overflow-y-auto text-white bg-coolgray-100 scrollbar border-coolgray-300" <div class="relative flex flex-col-reverse w-full p-4 pt-6 overflow-y-auto text-white bg-coolgray-100 scrollbar border-coolgray-300"
:class="fullscreen ? '' : 'max-h-[40rem] border border-solid rounded'"> :class="fullscreen ? '' : 'max-h-[40rem] border border-solid rounded'">
<button title="Minimize" x-show="fullscreen" class="fixed top-4 right-4" x-on:click="makeFullscreen"><svg <button title="Minimize" x-show="fullscreen" class="fixed top-4 right-4" x-on:click="makeFullscreen"><svg

View File

@@ -3,13 +3,20 @@
<h1>Logs</h1> <h1>Logs</h1>
<livewire:project.application.heading :application="$resource" /> <livewire:project.application.heading :application="$resource" />
<div class="pt-4"> <div class="pt-4">
<livewire:project.shared.get-logs :server="$server" :container="$container" /> @forelse ($containers as $container)
@if ($loop->first)
<h2 class="pb-4">Logs</h2>
@endif
<livewire:project.shared.get-logs :server="$server" :resource="$resource" :container="$container" />
@empty
<div>No containers are not running.</div>
@endforelse
</div> </div>
@elseif ($type === 'database') @elseif ($type === 'database')
<h1>Logs</h1> <h1>Logs</h1>
<livewire:project.database.heading :database="$resource" /> <livewire:project.database.heading :database="$resource" />
<div class="pt-4"> <div class="pt-4">
<livewire:project.shared.get-logs :server="$server" :container="$container" /> <livewire:project.shared.get-logs :resource="$resource" :server="$server" :container="$container" />
</div> </div>
@elseif ($type === 'service') @elseif ($type === 'service')
<livewire:project.service.navbar :service="$resource" :parameters="$parameters" :query="$query" /> <livewire:project.service.navbar :service="$resource" :parameters="$parameters" :query="$query" />
@@ -21,7 +28,7 @@
</a> </a>
</div> </div>
<div class="flex-1 pl-8"> <div class="flex-1 pl-8">
<livewire:project.shared.get-logs :server="$server" :container="$container" /> <livewire:project.shared.get-logs :server="$server" :resource="$resource" :servicesubtype="$serviceSubType" :container="$container" />
</div> </div>
</div> </div>
@endif @endif

View File

@@ -9,33 +9,36 @@
helper="See details in our <a target='_blank' class='text-white underline' href='https://coolify.io/docs/api-authentication'>documentation</a>." helper="See details in our <a target='_blank' class='text-white underline' href='https://coolify.io/docs/api-authentication'>documentation</a>."
label="Deploy Webhook (auth required)" id="deploywebhook"></x-forms.input> label="Deploy Webhook (auth required)" id="deploywebhook"></x-forms.input>
</div> </div>
<div> @if ($resource->type() !== 'service')
<h3>Manual Git Webhooks</h3> <div>
@if ($githubManualWebhook && $gitlabManualWebhook) <h3>Manual Git Webhooks</h3>
<form wire:submit.prevent='saveSecret' class="flex flex-col gap-2"> @if ($githubManualWebhook && $gitlabManualWebhook)
<div class="flex items-end gap-2"> <form wire:submit.prevent='saveSecret' class="flex flex-col gap-2">
<x-forms.input helper="Content Type in GitHub configuration could be json or form-urlencoded." <div class="flex items-end gap-2">
readonly label="GitHub" id="githubManualWebhook"></x-forms.input> <x-forms.input helper="Content Type in GitHub configuration could be json or form-urlencoded."
<x-forms.input type="password" readonly label="GitHub" id="githubManualWebhook"></x-forms.input>
helper="Need to set a secret to be able to use this webhook. It should match with the secret in GitHub." <x-forms.input type="password"
label="GitHub Webhook Secret" id="resource.manual_webhook_secret_github"></x-forms.input> helper="Need to set a secret to be able to use this webhook. It should match with the secret in GitHub."
label="GitHub Webhook Secret" id="resource.manual_webhook_secret_github"></x-forms.input>
</div>
<a target="_blank" class="flex hover:no-underline" href="{{ $resource?->gitWebhook }}">
<x-forms.button>Webhook Configuration on GitHub
<x-external-link />
</x-forms.button>
</a>
<div class="flex gap-2">
<x-forms.input readonly label="GitLab" id="gitlabManualWebhook"></x-forms.input>
<x-forms.input type="password"
helper="Need to set a secret to be able to use this webhook. It should match with the secret in GitLab."
label="GitLab Webhook Secret" id="resource.manual_webhook_secret_gitlab"></x-forms.input>
</div>
<x-forms.button type="submit">Save</x-forms.button>
</form>
@else
You are using an official Git App. You do not need manual webhooks.
@endif
</div>
@endif
</div>
<a target="_blank" class="flex hover:no-underline" href="{{ $resource?->gitWebhook }}">
<x-forms.button>Webhook Configuration on GitHub
<x-external-link />
</x-forms.button>
</a>
<div class="flex gap-2">
<x-forms.input readonly label="GitLab" id="gitlabManualWebhook"></x-forms.input>
<x-forms.input type="password"
helper="Need to set a secret to be able to use this webhook. It should match with the secret in GitLab."
label="GitLab Webhook Secret" id="resource.manual_webhook_secret_gitlab"></x-forms.input>
</div>
<x-forms.button type="submit">Save</x-forms.button>
</form>
@else
You are using an official Git App. You do not need manual webhooks.
@endif
</div>
</div> </div>

View File

@@ -11,12 +11,53 @@
<div class="pb-4">This will remove this server from Coolify. Beware! There is no coming <div class="pb-4">This will remove this server from Coolify. Beware! There is no coming
back! back!
</div> </div>
@if ($server->hasDefinedResources()) @if ($server->definedResources()->count() > 0)
<div class="text-warning">Please delete all resources before deleting this server.</div> <x-forms.button disabled isError isModal modalId="deleteServer">
Delete
</x-forms.button>
@else @else
<x-forms.button isError isModal modalId="deleteServer"> <x-forms.button isError isModal modalId="deleteServer">
Delete Delete
</x-forms.button> </x-forms.button>
@endif @endif
<div class="flex flex-col">
@forelse ($server->definedResources() as $resource)
@if ($loop->first)
<h3 class="pt-4">Defined resources</h3>
@endif
@if ($resource->link())
<a class="flex gap-2 p-1 hover:bg-coolgray-100 hover:no-underline" href="{{ $resource->link() }}">
<div class="w-64">{{ str($resource->type())->headline() }}</div>
<div>{{ $resource->name }}</div>
</a>
@else
<div class="flex gap-2 p-1 hover:bg-coolgray-100 hover:no-underline">
<div class="w-64">{{ str($resource->type())->headline() }}</div>
<div>{{ $resource->name }}</div>
</div>
@endif
@empty
@endforelse
</div>
@else
<div class="flex flex-col">
@forelse ($server->definedResources() as $resource)
@if ($loop->first)
<h3 class="pt-4">Defined resources</h3>
@endif
@if ($resource->link())
<a class="flex gap-2 p-1 hover:bg-coolgray-100 hover:no-underline" href="{{ $resource->link() }}">
<div class="w-64">{{ str($resource->type())->headline() }}</div>
<div>{{ $resource->name }}</div>
</a>
@else
<div class="flex gap-2 p-1 hover:bg-coolgray-100 hover:no-underline">
<div class="w-64">{{ str($resource->type())->headline() }}</div>
<div>{{ $resource->name }}</div>
</div>
@endif
@empty
@endforelse
</div>
@endif @endif
</div> </div>

View File

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

View File

@@ -24,24 +24,7 @@
</x-forms.button> </x-forms.button>
</div> </div>
</form> </form>
{{-- <h3>Highlight.io</h3>
<div class="w-32">
<x-forms.checkbox instantSave='instantSave("highlight")'
id="server.settings.is_logdrain_highlight_enabled" label="Enabled" />
</div>
<form wire:submit.prevent='submit("highlight")' class="flex flex-col">
<div class="flex flex-col gap-4">
<div class="flex flex-col w-full gap-2 xl:flex-row">
<x-forms.input type="password" required id="server.settings.logdrain_highlight_project_id"
label="Project Id" />
</div>
</div>
<div class="flex justify-end gap-4 pt-6">
<x-forms.button type="submit">
Save
</x-forms.button>
</div>
</form> --}}
<h3>Axiom</h3> <h3>Axiom</h3>
<div class="w-32"> <div class="w-32">
<x-forms.checkbox instantSave='instantSave("axiom")' id="server.settings.is_logdrain_axiom_enabled" <x-forms.checkbox instantSave='instantSave("axiom")' id="server.settings.is_logdrain_axiom_enabled"
@@ -61,6 +44,43 @@
</x-forms.button> </x-forms.button>
</div> </div>
</form> </form>
{{-- <h3>Highlight.io</h3>
<div class="w-32">
<x-forms.checkbox instantSave='instantSave("highlight")'
id="server.settings.is_logdrain_highlight_enabled" label="Enabled" />
</div>
<form wire:submit.prevent='submit("highlight")' class="flex flex-col">
<div class="flex flex-col gap-4">
<div class="flex flex-col w-full gap-2 xl:flex-row">
<x-forms.input type="password" required id="server.settings.logdrain_highlight_project_id"
label="Project Id" />
</div>
</div>
<div class="flex justify-end gap-4 pt-6">
<x-forms.button type="submit">
Save
</x-forms.button>
</div>
</form> --}}
<h3>Custom FluentBit configuration</h3>
<div class="w-32">
<x-forms.checkbox instantSave='instantSave("custom")' id="server.settings.is_logdrain_custom_enabled"
label="Enabled" />
</div>
<form wire:submit.prevent='submit("custom")' class="flex flex-col">
<div class="flex flex-col gap-4">
<x-forms.textarea rows="6" required id="server.settings.logdrain_custom_config"
label="Custom FluentBit Configuration" />
<x-forms.textarea id="server.settings.logdrain_custom_config_parser"
label="Custom Parser Configuration" />
</div>
<div class="flex justify-end gap-4 pt-6">
<x-forms.button type="submit">
Save
</x-forms.button>
</div>
</form>
</div> </div>
</div> </div>
</div> </div>

View File

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

View File

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

View File

@@ -79,7 +79,7 @@
@endforeach @endforeach
@foreach ($environment->services->sortBy('name') as $service) @foreach ($environment->services->sortBy('name') as $service)
<a class="relative box group" <a class="relative box group"
href="{{ route('project.service', [$project->uuid, $environment->name, $service->uuid]) }}"> href="{{ route('project.service.configuration', [$project->uuid, $environment->name, $service->uuid]) }}">
<div class="flex flex-col mx-6"> <div class="flex flex-col mx-6">
<div class="font-bold text-white">{{ $service->name }}</div> <div class="font-bold text-white">{{ $service->name }}</div>
<div class="description">{{ $service->description }}</div> <div class="description">{{ $service->description }}</div>

View File

@@ -120,7 +120,7 @@ Route::middleware(['auth', 'verified'])->group(function () {
// Services // Services
Route::get('/project/{project_uuid}/{environment_name}/service/{service_uuid}', ServiceIndex::class)->name('project.service'); Route::get('/project/{project_uuid}/{environment_name}/service/{service_uuid}', ServiceIndex::class)->name('project.service.configuration');
Route::get('/project/{project_uuid}/{environment_name}/service/{service_uuid}/{service_name}', ServiceShow::class)->name('project.service.show'); Route::get('/project/{project_uuid}/{environment_name}/service/{service_uuid}/{service_name}', ServiceShow::class)->name('project.service.show');
Route::get('/project/{project_uuid}/{environment_name}/service/{service_uuid}/{service_name}/logs', Logs::class)->name('project.service.logs'); Route::get('/project/{project_uuid}/{environment_name}/service/{service_uuid}/{service_name}/logs', Logs::class)->name('project.service.logs');
}); });

View File

@@ -225,13 +225,9 @@ Route::post('/source/github/events/manual', function () {
return response("Nothing to do. No applications found with branch '$base_branch'."); return response("Nothing to do. No applications found with branch '$base_branch'.");
} }
} }
ray($applications);
foreach ($applications as $application) { foreach ($applications as $application) {
ray($application);
$webhook_secret = data_get($application, 'manual_webhook_secret_github'); $webhook_secret = data_get($application, 'manual_webhook_secret_github');
ray($webhook_secret);
$hmac = hash_hmac('sha256', request()->getContent(), $webhook_secret); $hmac = hash_hmac('sha256', request()->getContent(), $webhook_secret);
ray($hmac, $x_hub_signature_256);
if (!hash_equals($x_hub_signature_256, $hmac)) { if (!hash_equals($x_hub_signature_256, $hmac)) {
ray('Invalid signature'); ray('Invalid signature');
continue; continue;
@@ -317,11 +313,13 @@ Route::post('/source/github/events', function () {
// Installation handled by setup redirect url. Repositories queried on-demand. // Installation handled by setup redirect url. Repositories queried on-demand.
return response('cool'); return response('cool');
} }
$github_app = GithubApp::where('app_id', $x_github_hook_installation_target_id)->firstOrFail(); $github_app = GithubApp::where('app_id', $x_github_hook_installation_target_id)->first();
if (is_null($github_app)) {
return response('Nothing to do. No GitHub App found.');
}
$webhook_secret = data_get($github_app, 'webhook_secret'); $webhook_secret = data_get($github_app, 'webhook_secret');
$hmac = hash_hmac('sha256', request()->getContent(), $webhook_secret); $hmac = hash_hmac('sha256', request()->getContent(), $webhook_secret);
ray($hmac, $x_hub_signature_256)->blue();
if (config('app.env') !== 'local') { if (config('app.env') !== 'local') {
if (!hash_equals($x_hub_signature_256, $hmac)) { if (!hash_equals($x_hub_signature_256, $hmac)) {
return response('not cool'); return response('not cool');
@@ -658,12 +656,10 @@ Route::post('/payments/paddle/events', function () {
$h1 = Str::of($signature)->after('h1='); $h1 = Str::of($signature)->after('h1=');
$signedPayload = $ts->value . ':' . request()->getContent(); $signedPayload = $ts->value . ':' . request()->getContent();
$verify = hash_hmac('sha256', $signedPayload, config('subscription.paddle_webhook_secret')); $verify = hash_hmac('sha256', $signedPayload, config('subscription.paddle_webhook_secret'));
ray($verify, $h1->value, hash_equals($verify, $h1->value));
if (!hash_equals($verify, $h1->value)) { if (!hash_equals($verify, $h1->value)) {
return response('Invalid signature.', 400); return response('Invalid signature.', 400);
} }
$eventType = data_get($payload, 'event_type'); $eventType = data_get($payload, 'event_type');
ray($eventType);
$webhook = Webhook::create([ $webhook = Webhook::create([
'type' => 'paddle', 'type' => 'paddle',
'payload' => $payload, 'payload' => $payload,

View File

@@ -1,11 +1,7 @@
#!/bin/bash #!/bin/bash
## Do not modify this file. You will lose the ability to install and auto-update! ## Do not modify this file. You will lose the ability to install and auto-update!
########### VERSION="1.1.0"
## Always run "php artisan app:sync-to-bunny-cdn --env=secrets" or "scripts/run sync-bunny" if you update this file.
###########
VERSION="1.0.3"
DOCKER_VERSION="24.0" DOCKER_VERSION="24.0"
CDN="https://cdn.coollabs.io/coolify" CDN="https://cdn.coollabs.io/coolify"
@@ -18,10 +14,14 @@ if [ $EUID != 0 ]; then
echo "Please run as root" echo "Please run as root"
exit exit
fi fi
if [ $OS_TYPE != "ubuntu" ] && [ $OS_TYPE != "debian" ] && [ $OS_TYPE != "raspbian" ]; then
echo "This script only supports Ubuntu and Debian for now." case "$OS_TYPE" in
ubuntu | debian | raspbian | centos | fedora | rhel | ol | rocky | sles | opensuse-leap | opensuse-tumbleweed) ;;
*)
echo "This script only supports Debian, Redhat or Sles based operating systems for now."
exit exit
fi ;;
esac
# Ovewrite LATEST_VERSION if user pass a version number # Ovewrite LATEST_VERSION if user pass a version number
if [ "$1" != "" ]; then if [ "$1" != "" ]; then
@@ -40,20 +40,41 @@ echo "Coolify version: $LATEST_VERSION"
echo -e "-------------" echo -e "-------------"
echo "Installing required packages..." echo "Installing required packages..."
apt update -y >/dev/null 2>&1 case "$OS_TYPE" in
apt install -y curl wget git jq jc >/dev/null 2>&1 ubuntu | debian | raspbian)
apt update -y >/dev/null 2>&1
apt install -y curl wget git jq >/dev/null 2>&1
;;
centos | fedora | rhel | ol | rocky)
dnf install -y curl wget git jq >/dev/null 2>&1
;;
sles | opensuse-leap | opensuse-tumbleweed)
zypper refresh >/dev/null 2>&1
zypper install -y curl wget git jq >/dev/null 2>&1
;;
*)
echo "This script only supports Debian, Redhat or Sles based operating systems for now."
exit
;;
esac
if ! [ -x "$(command -v docker)" ]; then if ! [ -x "$(command -v docker)" ]; then
echo "Docker is not installed. Installing Docker..." echo "Docker is not installed. Installing Docker."
curl https://releases.rancher.com/install-docker/${DOCKER_VERSION}.sh | sh curl https://releases.rancher.com/install-docker/${DOCKER_VERSION}.sh | sh
if [ -x "$(command -v docker)" ]; then if [ -x "$(command -v docker)" ]; then
echo "Docker installed successfully." echo "Docker installed successfully."
else else
echo "Docker installation failed." echo "Docker installation failed with Rancher script. Trying with official script."
echo "Maybe your OS is not supported." curl https://get.docker.com | sh -s -- --version ${DOCKER_VERSION}
echo "Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue." if [ -x "$(command -v docker)" ]; then
exit 1 echo "Docker installed successfully."
fi else
echo "Docker installation failed with official script."
echo "Maybe your OS is not supported."
echo "Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
exit 1
fi
fi
fi fi
echo -e "-------------" echo -e "-------------"
echo -e "Check Docker Configuration..." echo -e "Check Docker Configuration..."
@@ -93,7 +114,6 @@ else
systemctl restart docker systemctl restart docker
fi fi
echo -e "-------------" echo -e "-------------"
mkdir -p /data/coolify/ssh/keys mkdir -p /data/coolify/ssh/keys

View File

@@ -16,6 +16,7 @@ curl -fsSL $CDN/.env.production -o /data/coolify/source/.env.production
sort -u -t '=' -k 1,1 /data/coolify/source/.env /data/coolify/source/.env.production | sed '/^$/d' > /data/coolify/source/.env.temp && mv /data/coolify/source/.env.temp /data/coolify/source/.env sort -u -t '=' -k 1,1 /data/coolify/source/.env /data/coolify/source/.env.production | sed '/^$/d' > /data/coolify/source/.env.temp && mv /data/coolify/source/.env.temp /data/coolify/source/.env
# Make sure coolify network exists # Make sure coolify network exists
docker network create coolify 2>/dev/null docker network create --attachable coolify 2>/dev/null
# docker network create --attachable --driver=overlay coolify-overlay 2>/dev/null
docker run --pull always -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock --rm ghcr.io/coollabsio/coolify-helper bash -c "LATEST_IMAGE=${1:-} docker compose --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml up -d --pull always --remove-orphans --force-recreate" docker run --pull always -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock --rm ghcr.io/coollabsio/coolify-helper bash -c "LATEST_IMAGE=${1:-} docker compose --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml up -d --pull always --remove-orphans --force-recreate"

View File

@@ -24,7 +24,7 @@ services:
- REDIS_PORT=6379 - REDIS_PORT=6379
- WEBSOCKETS_ENABLED=true - WEBSOCKETS_ENABLED=true
postgresql: postgresql:
image: postgres:15-alpine image: postgres:16-alpine
volumes: volumes:
- directus-postgresql-data:/var/lib/postgresql/data - directus-postgresql-data:/var/lib/postgresql/data
environment: environment:

View File

@@ -0,0 +1,60 @@
# documentation: https://formbricks.com/docs/self-hosting/docker
# slogan: Open Source Experience Management
# tags: form, builder, forms, open source, experience, management, self-hosted, docker
services:
formbricks:
image: formbricks/formbricks:latest
environment:
- SERVICE_FQDN_FORMBRICKS
- WEBAPP_URL=$SERVICE_FQDN_FORMBRICKS
- DATABASE_URL=postgres://$SERVICE_USER_POSTGRESQL:$SERVICE_PASSWORD_POSTGRESQL@postgresql:5432/${POSTGRESQL_DATABASE:-formbricks}
- NEXTAUTH_SECRET=$SERVICE_BASE64_64_NEXTAUTH
- NEXTAUTH_URL=$SERVICE_FQDN_FORMBRICKS
- ENCRYPTION_KEY=$SERVICE_BASE64_64_ENCRYPTION
- POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRESQL}
- MAIL_FROM=${MAIL_FROM:-test@example.com}
- SMTP_HOST=${SMTP_HOST:-test.example.com}
- SMTP_PORT=${SMTP_PORT:-587}
- SMTP_USER=${SMTP_USER:-test}
- SMTP_PASSWORD=${SMTP_PASSWORD:-test}
- SMTP_SECURE_ENABLED=${SMTP_SECURE_ENABLED:-0}
- SHORT_URL_BASE=${SHORT_URL_BASE}
- EMAIL_VERIFICATION_DISABLED=${EMAIL_VERIFICATION_DISABLED:-1}
- PASSWORD_RESET_DISABLED=${PASSWORD_RESET_DISABLED:-1}
- SIGNUP_DISABLED=${SIGNUP_DISABLED:-0}
- INVITE_DISABLED=${INVITE_DISABLED:-0}
- PRIVACY_URL=${PRIVACY_URL}
- TERMS_URL=${TERMS_URL}
- IMPRINT_URL=${IMPRINT_URL}
- GITHUB_AUTH_ENABLED=${GITHUB_AUTH_ENABLED:-0}
- GITHUB_ID=${GITHUB_ID}
- GITHUB_SECRET=${GITHUB_SECRET}
- GOOGLE_AUTH_ENABLED=${GOOGLE_AUTH_ENABLED:-0}
- GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID}
- GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET}
- ASSET_PREFIX_URL=${ASSET_PREFIX_URL}
volumes:
- formbricks-uploads:/apps/web/uploads/
depends_on:
postgresql:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000"]
interval: 2s
timeout: 10s
retries: 15
postgresql:
image: postgres:16-alpine
volumes:
- formbricks-postgresql-data:/var/lib/postgresql/data
environment:
- POSTGRES_USER=${SERVICE_USER_POSTGRESQL}
- POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRESQL}
- POSTGRES_DB=${POSTGRESQL_DATABASE:-formbricks}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 20s
retries: 10

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