Compare commits

...

91 Commits

Author SHA1 Message Date
Andras Bacsai
b528a0f4ec Merge pull request #2164 from coollabsio/next
v4.0.0-beta.277
2024-05-10 10:50:40 +02:00
Andras Bacsai
7f265a6692 Merge pull request #2178 from wutangpaul/main
Fix silent failing of install script on manjaro, make it behave as arch
2024-05-10 10:01:25 +02:00
Andras Bacsai
55e00e35c1 Merge pull request #2175 from chz/fix/2174
Fix: Resource Operations page: incorrect color for server and project name
2024-05-10 09:59:18 +02:00
Andras Bacsai
d94e1ba55b ui: fix a few boxes here and there 2024-05-10 09:56:39 +02:00
Andras Bacsai
00e7167174 Merge pull request #2173 from maurobender/fix_one_shot_commands_executing_using_host_environment
Fix one-shot commands for a specific container being executed using host environment variables
2024-05-10 09:45:54 +02:00
Andras Bacsai
9d7b69fc0e Merge pull request #2159 from snekROmonoro/patch-1
"Inprogress" -> "In progress"
2024-05-10 09:43:59 +02:00
Andras Bacsai
db9a68e9c9 make sentinel enabled env var 2024-05-10 09:21:19 +02:00
Andras Bacsai
b5d9d6e268 chore: Comment out server sentinel check in ServerStatusJob 2024-05-10 09:12:19 +02:00
Andras Bacsai
5ff0c563ec disable sentinel for now 2024-05-10 08:42:17 +02:00
Paul McClean
e2131523ec Force manjaro to be arch 2024-05-10 00:30:31 +01:00
Andras Bacsai
1f5f51e3e5 Refactor metrics retrieval and update chart visualization 2024-05-09 15:44:32 +02:00
Andras Bacsai
1bb0d54dce Update image pull schedule to every five minutes 2024-05-09 15:27:14 +02:00
Andras Bacsai
094bb37049 Update sentinel version to 0.0.4 2024-05-09 15:26:39 +02:00
Andras Bacsai
58601db5ef feat: init metrics 2024-05-09 13:25:18 +02:00
Andras Bacsai
b5bef98a9b chore: Update permissions on metrics and logs directories 2024-05-09 12:35:49 +02:00
Andras Bacsai
1026f1efa5 refactor: add SCHEDULER environment variable to StartSentinel.php 2024-05-09 12:30:51 +02:00
Andras Bacsai
0c673fb524 chore: update sentinel version to 0.0.2 in versions.json 2024-05-09 12:23:01 +02:00
Andras Bacsai
7ee2c9478d chore: add metrics and logs directories to installation script 2024-05-09 12:22:54 +02:00
Andras Bacsai
4b51f8251b fix: change permissions on newly created dirs 2024-05-09 12:22:49 +02:00
Andras Bacsai
e91a64b1cc Refactor GetContainersStatus.php for improved readability and maintainability 2024-05-09 12:10:06 +02:00
Chingiz Mammadov
8920762fc5 fix: Color for resource operation server and project name 2024-05-09 01:48:44 +04:00
Andras Bacsai
ba40f93386 do not use sentinel for container details for now 2024-05-08 20:59:58 +02:00
Andras Bacsai
781bf52a8e Refactor PullSentinelImageJob.php to only start Sentinel if local version is outdated 2024-05-08 19:32:13 +02:00
Mauro E. Bender
cca1a9832e Fix one-shot commands for a specific container being executed using host environment variables 2024-05-08 19:20:21 +02:00
Andras Bacsai
1a152a5597 feat: pull new sentinel image and restart container 2024-05-08 19:19:32 +02:00
Andras Bacsai
76a5290351 Refactor StartSentinel.php for improved readability and maintainability 2024-05-08 15:29:59 +02:00
Andras Bacsai
829e17ef2b Refactor GetContainersStatus.php for improved readability and maintainability 2024-05-08 15:19:12 +02:00
Andras Bacsai
bc5d3bea14 Refactor GetContainersStatus.php for improved readability and maintainability 2024-05-08 15:04:13 +02:00
Andras Bacsai
edf2a2e68d Refactor ContainerStatusJob.php for improved readability and maintainability 2024-05-08 14:53:48 +02:00
Andras Bacsai
5df443a016 Refactor StartSentinel.php for improved readability and maintainability 2024-05-08 14:48:04 +02:00
Andras Bacsai
f6396f2e74 fix: turn off hc for dockerimage/docker base deployments by default
fix: loading github app
2024-05-08 14:42:45 +02:00
Andras Bacsai
c618e58a11 feat: start Sentinel on servers. 2024-05-08 14:22:35 +02:00
Andras Bacsai
2ea27acdde add cloud scripts 2024-05-08 12:29:36 +02:00
Andras Bacsai
331cad276e chore: Refactor ApplicationDeploymentJob.php for improved readability and maintainability 2024-05-08 10:36:38 +02:00
Andras Bacsai
b74eab8377 ui: fix tag view 2024-05-08 10:36:30 +02:00
Andras Bacsai
fb80318553 Refactor docker-compose files to remove version numbers 2024-05-08 09:23:36 +02:00
Andras Bacsai
3eb4aed867 chore: Refactor GetContainersStatus.php for improved readability and maintainability 2024-05-08 09:23:32 +02:00
Andras Bacsai
f6f959a897 feat: experimental sentinel 2024-05-07 15:41:50 +02:00
Andras Bacsai
2b422a542a fix: empty db conf
feat: add listen_addresses to postgresql if its missing in the custom conf
2024-05-07 12:35:24 +02:00
Andras Bacsai
d0e9d58a43 chore: Add Listmonk service template and logo 2024-05-07 10:22:02 +02:00
Andras Bacsai
8a1933b9b2 chore: remove docker compose versions 2024-05-07 09:43:51 +02:00
Andras Bacsai
96a587f343 Update version to 4.0.0-beta.277 2024-05-07 09:43:41 +02:00
snekROmonoro
2bb6a71874 "Inprogress" -> "In progress" 2024-05-06 15:42:01 +02:00
Andras Bacsai
94acd12f1c Merge pull request #2158 from coollabsio/next
v4.0.0-beta.276
2024-05-06 14:34:30 +02:00
Andras Bacsai
5e44a61068 chore: Improve menu item styling and spacing in project index and show views 2024-05-06 14:33:45 +02:00
Andras Bacsai
a54f0ed94d chore: Improve menu item styling and spacing in service configuration and index views 2024-05-06 14:30:50 +02:00
Andras Bacsai
662c6f3cc2 chore: Improve menu item styling and spacing in service configuration and index views 2024-05-06 14:28:16 +02:00
Andras Bacsai
d93c635a0a Update version to 4.0.0-beta.276 2024-05-06 14:28:13 +02:00
Andras Bacsai
17b73aaf91 Merge pull request #2155 from coollabsio/next
v4.0.0-beta.275
2024-05-06 14:03:15 +02:00
Andras Bacsai
c194911458 revert 2024-05-06 14:01:07 +02:00
Andras Bacsai
92e99e3fb4 chore: dark mode should be the default 2024-05-06 14:00:20 +02:00
Andras Bacsai
848e6102a1 fix css here and there 2024-05-06 13:58:19 +02:00
Andras Bacsai
ef37bf9b1a Merge branch 'next' of github.com:coollabsio/coolify into next 2024-05-06 13:31:13 +02:00
Andras Bacsai
eb41e023c7 feat: Add lazy loading for images in General.php and improve Docker Compose file handling in Application.php 2024-05-06 13:03:55 +02:00
Andras Bacsai
6b4987bf39 fix: confirmation for custom labels 2024-05-06 12:52:06 +02:00
Andras Bacsai
d46ff76887 feat: Add container name to network aliases in ApplicationDeploymentJob 2024-05-06 12:47:49 +02:00
Andras Bacsai
d53a9e672c fix: Comment out internal notification in email_verify method 2024-05-06 12:39:47 +02:00
Andras Bacsai
80ef99a24b Merge branch 'next' of github.com:coollabsio/coolify into next 2024-05-06 12:33:24 +02:00
Andras Bacsai
6062a1f8c7 chore: Update DNS server validation helper text 2024-05-06 12:33:22 +02:00
Andras Bacsai
d974bd9a07 Merge pull request #2120 from bitsnaps/gitpod
fixt: env file example
2024-05-06 12:31:43 +02:00
Andras Bacsai
fb8c9566d5 Merge pull request #2148 from SIPC/Add-Chinese-Simplified
Add Chinese Simplified lang
2024-05-06 12:31:24 +02:00
Andras Bacsai
7431bd69e9 Merge pull request #2149 from SIPC/fix-install.sh-error
fix: install.sh error
2024-05-06 12:31:06 +02:00
Andras Bacsai
5d7f393e94 Merge pull request #2139 from eltociear/patch-3
fix: typo in tags.blade.php
2024-05-06 12:29:59 +02:00
Andras Bacsai
ef25d100e1 Merge pull request #2138 from chikof/patch-1
Change of wording
2024-05-06 12:29:45 +02:00
Andras Bacsai
17f82c8972 Merge pull request #2137 from LucianoLaratelli/main
Don't append '.git' for sr.ht repositories
2024-05-06 12:29:13 +02:00
Andras Bacsai
05c937743c feat: custom internal container names
fix: add warning if hc fails with dockerfile/dockerimage based deployments
2024-05-06 11:45:22 +02:00
Andras Bacsai
bf2e7ff130 chore: update version to 4.0.0-beta.275 2024-05-06 11:44:27 +02:00
Francesco Bruno
1b30ee606f improved responsivness in case the service/app/database is deployed 2024-05-05 18:14:14 +00:00
Francesco Bruno
3235907266 Refactor settings page layout for better responsiveness 2024-05-05 17:18:48 +00:00
Francesco Bruno
99c7e417d6 Global setting's input text uses flex wrap for better spacing on small screens 2024-05-05 17:17:54 +00:00
Francesco Bruno
d81906d348 assing a min height to navbar list 2024-05-05 17:09:22 +00:00
Francesco Bruno
296872d2e4 database configuration input boxes use wrap in small screens 2024-05-05 17:04:59 +00:00
Francesco Bruno
7cd02b4916 moved width class on parent 2024-05-05 16:58:55 +00:00
Francesco Bruno
0e217f48be Improved input text spacing with small screens 2024-05-05 16:58:12 +00:00
Francesco Bruno
61fdf4b6c7 use muni-item-active for submenus for services and database 2024-05-05 16:41:15 +00:00
Francesco Bruno
91dbf1f01a navbar changed in order top move as first element the button for start/redeploy 2024-05-05 16:33:52 +00:00
Francesco Bruno
d52aac76c0 Changed the breadcrumbs for let it using flex-wrap 2024-05-05 15:55:58 +00:00
Francesco Bruno
6102e441d6 In small screen the submenu is moved up, under the navbar-main 2024-05-05 15:23:25 +00:00
sipc.ink
1a5fec39c0 fix: install.sh error 2024-05-05 21:11:44 +08:00
sipc.ink
efa5091b98 Create zh-cn.json 2024-05-05 21:07:49 +08:00
Ikko Eltociear Ashimine
be4386658a fix: typo in tags.blade.php
seperated -> separated
2024-05-04 01:23:31 +09:00
Francesco Bruno
0cddce7a37 Changed the navbar-main class to utilize overflow-x-scroll, preventing the page from overflowing entirely. 2024-05-03 14:51:08 +00:00
Francesco Bruno
a86d13632e Format foar a correct tailwind positioning of classes and eliminated usless spaces. 2024-05-03 14:49:41 +00:00
Chiko
d71682a3f7 Change of wording 2024-05-03 15:06:06 +01:00
Luciano Laratelli
c901ace21a don't append '.git' for sr.ht repositories 2024-05-03 09:31:05 -04:00
Andras Bacsai
418398a870 Merge pull request #2133 from coollabsio/next
Fix server status check in ServerStatusJob.php
2024-05-03 13:46:06 +02:00
Andras Bacsai
baca57062e Fix server status check in ServerStatusJob.php 2024-05-03 13:45:42 +02:00
Andras Bacsai
52df8e6e8b Fix server status check in ServerStatusJob.php 2024-05-03 13:43:54 +02:00
Andras Bacsai
b51747378a Merge pull request #2132 from coollabsio/next
v4.0.0-beta.274
2024-05-03 13:35:26 +02:00
Andras Bacsai
424a6b0428 revert server checking fn 2024-05-03 13:31:35 +02:00
Ibrahim H
692047e4c8 fixt: env file 2024-05-02 19:11:46 +01:00
95 changed files with 1979 additions and 888 deletions

View File

@@ -3,7 +3,7 @@ tasks:
# Fix because of https://github.com/gitpod-io/gitpod/issues/16614 # Fix because of https://github.com/gitpod-io/gitpod/issues/16614
before: sudo curl -o /usr/local/bin/docker-compose -fsSL https://github.com/docker/compose/releases/download/v2.16.0/docker-compose-linux-$(uname -m) before: sudo curl -o /usr/local/bin/docker-compose -fsSL https://github.com/docker/compose/releases/download/v2.16.0/docker-compose-linux-$(uname -m)
init: | init: |
cp .env.example .env && cp .env.development.example .env &&
sed -i "s#APP_URL=http://localhost#APP_URL=$(gp url 8000)#g" .env sed -i "s#APP_URL=http://localhost#APP_URL=$(gp url 8000)#g" .env
sed -i "s#USERID=#USERID=33333#g" .env sed -i "s#USERID=#USERID=33333#g" .env
sed -i "s#GROUPID=#GROUPID=33333#g" .env sed -i "s#GROUPID=#GROUPID=33333#g" .env
@@ -20,7 +20,7 @@ tasks:
echo "Waiting for Sail environment to boot up." echo "Waiting for Sail environment to boot up."
gp sync-await spin-is-ready gp sync-await spin-is-ready
./vendor/bin/spin exec vite npm install ./vendor/bin/spin exec vite npm install
./vendor/bin/spin exec vite npm run dev ./vendor/bin/spin exec vite npm run dev -- --host
- name: Laravel Queue Worker, listening to code changes - name: Laravel Queue Worker, listening to code changes
command: | command: |

View File

@@ -33,7 +33,6 @@ class StartClickhouse
$environment_variables = $this->generate_environment_variables(); $environment_variables = $this->generate_environment_variables();
$docker_compose = [ $docker_compose = [
'version' => '3.8',
'services' => [ 'services' => [
$container_name => [ $container_name => [
'image' => $this->database->image, 'image' => $this->database->image,

View File

@@ -107,7 +107,6 @@ class StartDatabaseProxy
COPY nginx.conf /etc/nginx/nginx.conf COPY nginx.conf /etc/nginx/nginx.conf
EOF; EOF;
$docker_compose = [ $docker_compose = [
'version' => '3.8',
'services' => [ 'services' => [
$proxyContainerName => [ $proxyContainerName => [
'build' => [ 'build' => [

View File

@@ -36,7 +36,6 @@ class StartDragonfly
$environment_variables = $this->generate_environment_variables(); $environment_variables = $this->generate_environment_variables();
$docker_compose = [ $docker_compose = [
'version' => '3.8',
'services' => [ 'services' => [
$container_name => [ $container_name => [
'image' => $this->database->image, 'image' => $this->database->image,

View File

@@ -37,7 +37,6 @@ class StartKeydb
$this->add_custom_keydb(); $this->add_custom_keydb();
$docker_compose = [ $docker_compose = [
'version' => '3.8',
'services' => [ 'services' => [
$container_name => [ $container_name => [
'image' => $this->database->image, 'image' => $this->database->image,
@@ -96,7 +95,7 @@ class StartKeydb
if (count($volume_names) > 0) { if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names; $docker_compose['volumes'] = $volume_names;
} }
if (!is_null($this->database->keydb_conf)) { if (!is_null($this->database->keydb_conf) || !empty($this->database->keydb_conf)) {
$docker_compose['services'][$container_name]['volumes'][] = [ $docker_compose['services'][$container_name]['volumes'][] = [
'type' => 'bind', 'type' => 'bind',
'source' => $this->configuration_dir . '/keydb.conf', 'source' => $this->configuration_dir . '/keydb.conf',
@@ -162,7 +161,7 @@ class StartKeydb
} }
private function add_custom_keydb() private function add_custom_keydb()
{ {
if (is_null($this->database->keydb_conf)) { if (is_null($this->database->keydb_conf) || empty($this->database->keydb_conf)) {
return; return;
} }
$filename = 'keydb.conf'; $filename = 'keydb.conf';

View File

@@ -32,7 +32,6 @@ class StartMariadb
$environment_variables = $this->generate_environment_variables(); $environment_variables = $this->generate_environment_variables();
$this->add_custom_mysql(); $this->add_custom_mysql();
$docker_compose = [ $docker_compose = [
'version' => '3.8',
'services' => [ 'services' => [
$container_name => [ $container_name => [
'image' => $this->database->image, 'image' => $this->database->image,
@@ -90,7 +89,7 @@ class StartMariadb
if (count($volume_names) > 0) { if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names; $docker_compose['volumes'] = $volume_names;
} }
if (!is_null($this->database->mariadb_conf)) { if (!is_null($this->database->mariadb_conf) || !empty($this->database->mariadb_conf)) {
$docker_compose['services'][$container_name]['volumes'][] = [ $docker_compose['services'][$container_name]['volumes'][] = [
'type' => 'bind', 'type' => 'bind',
'source' => $this->configuration_dir . '/custom-config.cnf', 'source' => $this->configuration_dir . '/custom-config.cnf',
@@ -165,7 +164,7 @@ class StartMariadb
} }
private function add_custom_mysql() private function add_custom_mysql()
{ {
if (is_null($this->database->mariadb_conf)) { if (is_null($this->database->mariadb_conf) || empty($this->database->mariadb_conf)) {
return; return;
} }
$filename = 'custom-config.cnf'; $filename = 'custom-config.cnf';

View File

@@ -35,7 +35,6 @@ class StartMongodb
$this->add_custom_mongo_conf(); $this->add_custom_mongo_conf();
$docker_compose = [ $docker_compose = [
'version' => '3.8',
'services' => [ 'services' => [
$container_name => [ $container_name => [
'image' => $this->database->image, 'image' => $this->database->image,
@@ -97,7 +96,7 @@ class StartMongodb
if (count($volume_names) > 0) { if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names; $docker_compose['volumes'] = $volume_names;
} }
if (!is_null($this->database->mongo_conf)) { if (!is_null($this->database->mongo_conf) || !empty($this->database->mongo_conf)) {
$docker_compose['services'][$container_name]['volumes'][] = [ $docker_compose['services'][$container_name]['volumes'][] = [
'type' => 'bind', 'type' => 'bind',
'source' => $this->configuration_dir . '/mongod.conf', 'source' => $this->configuration_dir . '/mongod.conf',
@@ -178,7 +177,7 @@ class StartMongodb
} }
private function add_custom_mongo_conf() private function add_custom_mongo_conf()
{ {
if (is_null($this->database->mongo_conf)) { if (is_null($this->database->mongo_conf) || empty($this->database->mongo_conf)) {
return; return;
} }
$filename = 'mongod.conf'; $filename = 'mongod.conf';

View File

@@ -32,7 +32,6 @@ class StartMysql
$environment_variables = $this->generate_environment_variables(); $environment_variables = $this->generate_environment_variables();
$this->add_custom_mysql(); $this->add_custom_mysql();
$docker_compose = [ $docker_compose = [
'version' => '3.8',
'services' => [ 'services' => [
$container_name => [ $container_name => [
'image' => $this->database->image, 'image' => $this->database->image,
@@ -90,7 +89,7 @@ class StartMysql
if (count($volume_names) > 0) { if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names; $docker_compose['volumes'] = $volume_names;
} }
if (!is_null($this->database->mysql_conf)) { if (!is_null($this->database->mysql_conf) || !empty($this->database->mysql_conf)) {
$docker_compose['services'][$container_name]['volumes'][] = [ $docker_compose['services'][$container_name]['volumes'][] = [
'type' => 'bind', 'type' => 'bind',
'source' => $this->configuration_dir . '/custom-config.cnf', 'source' => $this->configuration_dir . '/custom-config.cnf',
@@ -165,7 +164,7 @@ class StartMysql
} }
private function add_custom_mysql() private function add_custom_mysql()
{ {
if (is_null($this->database->mysql_conf)) { if (is_null($this->database->mysql_conf) || empty($this->database->mysql_conf)) {
return; return;
} }
$filename = 'custom-config.cnf'; $filename = 'custom-config.cnf';

View File

@@ -35,7 +35,6 @@ class StartPostgresql
$this->add_custom_conf(); $this->add_custom_conf();
$docker_compose = [ $docker_compose = [
'version' => '3.8',
'services' => [ 'services' => [
$container_name => [ $container_name => [
'image' => $this->database->image, 'image' => $this->database->image,
@@ -78,7 +77,6 @@ class StartPostgresql
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset); data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
} }
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) { if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
ray('Log Drain Enabled');
$docker_compose['services'][$container_name]['logging'] = [ $docker_compose['services'][$container_name]['logging'] = [
'driver' => 'fluentd', 'driver' => 'fluentd',
'options' => [ 'options' => [
@@ -107,7 +105,7 @@ class StartPostgresql
]; ];
} }
} }
if (!is_null($this->database->postgres_conf)) { if (!is_null($this->database->postgres_conf) && !empty($this->database->postgres_conf)) {
$docker_compose['services'][$container_name]['volumes'][] = [ $docker_compose['services'][$container_name]['volumes'][] = [
'type' => 'bind', 'type' => 'bind',
'source' => $this->configuration_dir . '/custom-postgres.conf', 'source' => $this->configuration_dir . '/custom-postgres.conf',
@@ -165,8 +163,6 @@ class StartPostgresql
private function generate_environment_variables() private function generate_environment_variables()
{ {
$environment_variables = collect(); $environment_variables = collect();
ray('Generate Environment Variables')->green();
ray($this->database->runtime_environment_variables)->green();
foreach ($this->database->runtime_environment_variables as $env) { foreach ($this->database->runtime_environment_variables as $env) {
$environment_variables->push("$env->key=$env->real_value"); $environment_variables->push("$env->key=$env->real_value");
} }
@@ -203,11 +199,16 @@ class StartPostgresql
} }
private function add_custom_conf() private function add_custom_conf()
{ {
if (is_null($this->database->postgres_conf)) { if (is_null($this->database->postgres_conf) || empty($this->database->postgres_conf)) {
return; return;
} }
$filename = 'custom-postgres.conf'; $filename = 'custom-postgres.conf';
$content = $this->database->postgres_conf; $content = $this->database->postgres_conf;
if (!str($content)->contains('listen_addresses')) {
$content .= "\nlisten_addresses = '*'";
$this->database->postgres_conf = $content;
$this->database->save();
}
$content_base64 = base64_encode($content); $content_base64 = base64_encode($content);
$this->commands[] = "echo '{$content_base64}' | base64 -d | tee $this->configuration_dir/{$filename} > /dev/null"; $this->commands[] = "echo '{$content_base64}' | base64 -d | tee $this->configuration_dir/{$filename} > /dev/null";
} }

View File

@@ -37,7 +37,6 @@ class StartRedis
$this->add_custom_redis(); $this->add_custom_redis();
$docker_compose = [ $docker_compose = [
'version' => '3.8',
'services' => [ 'services' => [
$container_name => [ $container_name => [
'image' => $this->database->image, 'image' => $this->database->image,
@@ -100,7 +99,7 @@ class StartRedis
if (count($volume_names) > 0) { if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names; $docker_compose['volumes'] = $volume_names;
} }
if (!is_null($this->database->redis_conf)) { if (!is_null($this->database->redis_conf) || !empty($this->database->redis_conf)) {
$docker_compose['services'][$container_name]['volumes'][] = [ $docker_compose['services'][$container_name]['volumes'][] = [
'type' => 'bind', 'type' => 'bind',
'source' => $this->configuration_dir . '/redis.conf', 'source' => $this->configuration_dir . '/redis.conf',
@@ -166,7 +165,7 @@ class StartRedis
} }
private function add_custom_redis() private function add_custom_redis()
{ {
if (is_null($this->database->redis_conf)) { if (is_null($this->database->redis_conf) || empty($this->database->redis_conf)) {
return; return;
} }
$filename = 'redis.conf'; $filename = 'redis.conf';

View File

@@ -0,0 +1,657 @@
<?php
namespace App\Actions\Docker;
use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Proxy\CheckProxy;
use App\Actions\Proxy\StartProxy;
use App\Actions\Shared\ComplexStatusCheck;
use App\Models\Application;
use App\Models\ApplicationPreview;
use App\Models\Server;
use App\Models\ServiceDatabase;
use App\Notifications\Container\ContainerRestarted;
use App\Notifications\Container\ContainerStopped;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Log;
use Lorisleiva\Actions\Concerns\AsAction;
class GetContainersStatus
{
use AsAction;
public $applications;
public $server;
public function handle(Server $server)
{
if (isDev()) {
$server = Server::find(0);
}
$this->server = $server;
if (!$this->server->isFunctional()) {
return 'Server is not ready.';
};
$this->applications = $this->server->applications();
$skip_these_applications = collect([]);
foreach ($this->applications as $application) {
if ($application->additional_servers->count() > 0) {
$skip_these_applications->push($application);
ComplexStatusCheck::run($application);
$this->applications = $this->applications->filter(function ($value, $key) use ($application) {
return $value->id !== $application->id;
});
}
}
$this->applications = $this->applications->filter(function ($value, $key) use ($skip_these_applications) {
return !$skip_these_applications->pluck('id')->contains($value->id);
});
$this->old_way();
// if ($this->server->isSwarm()) {
// $this->old_way();
// } else {
// if (!$this->server->is_metrics_enabled) {
// $this->old_way();
// return;
// }
// $sentinel_found = instant_remote_process(["docker inspect coolify-sentinel"], $this->server, false);
// $sentinel_found = json_decode($sentinel_found, true);
// $status = data_get($sentinel_found, '0.State.Status', 'exited');
// if ($status === 'running') {
// ray('Checking with Sentinel');
// $this->sentinel();
// } else {
// ray('Checking the Old way');
// $this->old_way();
// }
// }
}
private function sentinel()
{
try {
$containers = $this->server->getContainers();
if ($containers->count() === 0) {
return;
}
$databases = $this->server->databases();
$services = $this->server->services()->get();
$previews = $this->server->previews();
$foundApplications = [];
$foundApplicationPreviews = [];
$foundDatabases = [];
$foundServices = [];
foreach ($containers as $container) {
$labels = Arr::undot(data_get($container, 'labels'));
$containerStatus = data_get($container, 'state');
$containerHealth = data_get($container, 'health_status', 'unhealthy');
$containerStatus = "$containerStatus ($containerHealth)";
$applicationId = data_get($labels, 'coolify.applicationId');
if ($applicationId) {
$pullRequestId = data_get($labels, 'coolify.pullRequestId');
if ($pullRequestId) {
if (str($applicationId)->contains('-')) {
$applicationId = str($applicationId)->before('-');
}
$preview = ApplicationPreview::where('application_id', $applicationId)->where('pull_request_id', $pullRequestId)->first();
if ($preview) {
$foundApplicationPreviews[] = $preview->id;
$statusFromDb = $preview->status;
if ($statusFromDb !== $containerStatus) {
$preview->update(['status' => $containerStatus]);
}
} else {
//Notify user that this container should not be there.
}
} else {
$application = $this->applications->where('id', $applicationId)->first();
if ($application) {
$foundApplications[] = $application->id;
$statusFromDb = $application->status;
if ($statusFromDb !== $containerStatus) {
$application->update(['status' => $containerStatus]);
}
} else {
//Notify user that this container should not be there.
}
}
} else {
$uuid = data_get($labels, 'com.docker.compose.service');
$type = data_get($labels, 'coolify.type');
if ($uuid) {
if ($type === 'service') {
$database_id = data_get($labels, 'coolify.service.subId');
if ($database_id) {
$service_db = ServiceDatabase::where('id', $database_id)->first();
if ($service_db) {
$uuid = $service_db->service->uuid;
$isPublic = data_get($service_db, 'is_public');
if ($isPublic) {
$foundTcpProxy = $containers->filter(function ($value, $key) use ($uuid) {
if ($this->server->isSwarm()) {
// TODO: fix this with sentinel
return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid";
} else {
return data_get($value, 'name') === "$uuid-proxy";
}
})->first();
if (!$foundTcpProxy) {
StartDatabaseProxy::run($service_db);
// $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$service_db->service->name}", $this->server));
}
}
}
}
} else {
$database = $databases->where('uuid', $uuid)->first();
if ($database) {
$isPublic = data_get($database, 'is_public');
$foundDatabases[] = $database->id;
$statusFromDb = $database->status;
if ($statusFromDb !== $containerStatus) {
$database->update(['status' => $containerStatus]);
}
if ($isPublic) {
$foundTcpProxy = $containers->filter(function ($value, $key) use ($uuid) {
if ($this->server->isSwarm()) {
// TODO: fix this with sentinel
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 {
// Notify user that this container should not be there.
}
}
}
if (data_get($container, 'name') === 'coolify-db') {
$foundDatabases[] = 0;
}
}
$serviceLabelId = data_get($labels, 'coolify.serviceId');
if ($serviceLabelId) {
$subType = data_get($labels, 'coolify.service.subType');
$subId = data_get($labels, 'coolify.service.subId');
$service = $services->where('id', $serviceLabelId)->first();
if (!$service) {
continue;
}
if ($subType === 'application') {
$service = $service->applications()->where('id', $subId)->first();
} else {
$service = $service->databases()->where('id', $subId)->first();
}
if ($service) {
$foundServices[] = "$service->id-$service->name";
$statusFromDb = $service->status;
if ($statusFromDb !== $containerStatus) {
// ray('Updating status: ' . $containerStatus);
$service->update(['status' => $containerStatus]);
}
}
}
}
$exitedServices = collect([]);
foreach ($services as $service) {
$apps = $service->applications()->get();
$dbs = $service->databases()->get();
foreach ($apps as $app) {
if (in_array("$app->id-$app->name", $foundServices)) {
continue;
} else {
$exitedServices->push($app);
}
}
foreach ($dbs as $db) {
if (in_array("$db->id-$db->name", $foundServices)) {
continue;
} else {
$exitedServices->push($db);
}
}
}
$exitedServices = $exitedServices->unique('id');
foreach ($exitedServices as $exitedService) {
if (str($exitedService->status)->startsWith('exited')) {
continue;
}
$name = data_get($exitedService, 'name');
$fqdn = data_get($exitedService, 'fqdn');
$containerName = $name ? "$name, available at $fqdn" : $fqdn;
$projectUuid = data_get($service, 'environment.project.uuid');
$serviceUuid = data_get($service, 'uuid');
$environmentName = data_get($service, 'environment.name');
if ($projectUuid && $serviceUuid && $environmentName) {
$url = base_url() . '/project/' . $projectUuid . "/" . $environmentName . "/service/" . $serviceUuid;
} else {
$url = null;
}
$this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
$exitedService->update(['status' => 'exited']);
}
$notRunningApplications = $this->applications->pluck('id')->diff($foundApplications);
foreach ($notRunningApplications as $applicationId) {
$application = $this->applications->where('id', $applicationId)->first();
if (str($application->status)->startsWith('exited')) {
continue;
}
$application->update(['status' => 'exited']);
$name = data_get($application, 'name');
$fqdn = data_get($application, 'fqdn');
$containerName = $name ? "$name ($fqdn)" : $fqdn;
$projectUuid = data_get($application, 'environment.project.uuid');
$applicationUuid = data_get($application, 'uuid');
$environment = data_get($application, 'environment.name');
if ($projectUuid && $applicationUuid && $environment) {
$url = base_url() . '/project/' . $projectUuid . "/" . $environment . "/application/" . $applicationUuid;
} else {
$url = null;
}
$this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
}
$notRunningApplicationPreviews = $previews->pluck('id')->diff($foundApplicationPreviews);
foreach ($notRunningApplicationPreviews as $previewId) {
$preview = $previews->where('id', $previewId)->first();
if (str($preview->status)->startsWith('exited')) {
continue;
}
$preview->update(['status' => 'exited']);
$name = data_get($preview, 'name');
$fqdn = data_get($preview, 'fqdn');
$containerName = $name ? "$name ($fqdn)" : $fqdn;
$projectUuid = data_get($preview, 'application.environment.project.uuid');
$environmentName = data_get($preview, 'application.environment.name');
$applicationUuid = data_get($preview, 'application.uuid');
if ($projectUuid && $applicationUuid && $environmentName) {
$url = base_url() . '/project/' . $projectUuid . "/" . $environmentName . "/application/" . $applicationUuid;
} else {
$url = null;
}
$this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
}
$notRunningDatabases = $databases->pluck('id')->diff($foundDatabases);
foreach ($notRunningDatabases as $database) {
$database = $databases->where('id', $database)->first();
if (str($database->status)->startsWith('exited')) {
continue;
}
$database->update(['status' => 'exited']);
$name = data_get($database, 'name');
$fqdn = data_get($database, 'fqdn');
$containerName = $name;
$projectUuid = data_get($database, 'environment.project.uuid');
$environmentName = data_get($database, 'environment.name');
$databaseUuid = data_get($database, 'uuid');
if ($projectUuid && $databaseUuid && $environmentName) {
$url = base_url() . '/project/' . $projectUuid . "/" . $environmentName . "/database/" . $databaseUuid;
} else {
$url = null;
}
$this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
}
// Check if proxy is running
$this->server->proxyType();
$foundProxyContainer = $containers->filter(function ($value, $key) {
if ($this->server->isSwarm()) {
// TODO: fix this with sentinel
return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik';
} else {
return data_get($value, 'name') === 'coolify-proxy';
}
})->first();
if (!$foundProxyContainer) {
try {
$shouldStart = CheckProxy::run($this->server);
if ($shouldStart) {
StartProxy::run($this->server, false);
$this->server->team?->notify(new ContainerRestarted('coolify-proxy', $this->server));
}
} catch (\Throwable $e) {
ray($e);
}
} else {
$this->server->proxy->status = data_get($foundProxyContainer, 'state');
$this->server->save();
$connectProxyToDockerNetworks = connectProxyToNetworks($this->server);
instant_remote_process($connectProxyToDockerNetworks, $this->server, false);
}
} catch (\Exception $e) {
send_internal_notification("ContainerStatusJob failed on ({$this->server->id}) with: " . $e->getMessage());
ray($e->getMessage());
return handleError($e);
}
}
private function old_way()
{
if ($this->server->isSwarm()) {
$containers = instant_remote_process(["docker service inspect $(docker service ls -q) --format '{{json .}}'"], $this->server, false);
$containerReplicates = 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);
$containerReplicates = null;
}
if (is_null($containers)) {
return;
}
$containers = format_docker_command_output_to_json($containers);
if ($containerReplicates) {
$containerReplicates = format_docker_command_output_to_json($containerReplicates);
foreach ($containerReplicates 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;
});
}
}
$databases = $this->server->databases();
$services = $this->server->services()->get();
$previews = $this->server->previews();
$foundApplications = [];
$foundApplicationPreviews = [];
$foundDatabases = [];
$foundServices = [];
foreach ($containers as $container) {
if ($this->server->isSwarm()) {
$labels = data_get($container, 'Spec.Labels');
$uuid = data_get($labels, 'coolify.name');
} else {
$labels = data_get($container, 'Config.Labels');
}
$containerStatus = data_get($container, 'State.Status');
$containerHealth = data_get($container, 'State.Health.Status', 'unhealthy');
$containerStatus = "$containerStatus ($containerHealth)";
$labels = Arr::undot(format_docker_labels_to_json($labels));
$applicationId = data_get($labels, 'coolify.applicationId');
if ($applicationId) {
$pullRequestId = data_get($labels, 'coolify.pullRequestId');
if ($pullRequestId) {
if (str($applicationId)->contains('-')) {
$applicationId = str($applicationId)->before('-');
}
$preview = ApplicationPreview::where('application_id', $applicationId)->where('pull_request_id', $pullRequestId)->first();
if ($preview) {
$foundApplicationPreviews[] = $preview->id;
$statusFromDb = $preview->status;
if ($statusFromDb !== $containerStatus) {
$preview->update(['status' => $containerStatus]);
}
} else {
//Notify user that this container should not be there.
}
} else {
$application = $this->applications->where('id', $applicationId)->first();
if ($application) {
$foundApplications[] = $application->id;
$statusFromDb = $application->status;
if ($statusFromDb !== $containerStatus) {
$application->update(['status' => $containerStatus]);
}
} else {
//Notify user that this container should not be there.
}
}
} else {
$uuid = data_get($labels, 'com.docker.compose.service');
$type = data_get($labels, 'coolify.type');
if ($uuid) {
if ($type === 'service') {
$database_id = data_get($labels, 'coolify.service.subId');
if ($database_id) {
$service_db = ServiceDatabase::where('id', $database_id)->first();
if ($service_db) {
$uuid = $service_db->service->uuid;
$isPublic = data_get($service_db, 'is_public');
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($service_db);
// $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$service_db->service->name}", $this->server));
}
}
}
}
} else {
$database = $databases->where('uuid', $uuid)->first();
if ($database) {
$isPublic = data_get($database, 'is_public');
$foundDatabases[] = $database->id;
$statusFromDb = $database->status;
if ($statusFromDb !== $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 {
// Notify user that this container should not be there.
}
}
}
if (data_get($container, 'Name') === '/coolify-db') {
$foundDatabases[] = 0;
}
}
$serviceLabelId = data_get($labels, 'coolify.serviceId');
if ($serviceLabelId) {
$subType = data_get($labels, 'coolify.service.subType');
$subId = data_get($labels, 'coolify.service.subId');
$service = $services->where('id', $serviceLabelId)->first();
if (!$service) {
continue;
}
if ($subType === 'application') {
$service = $service->applications()->where('id', $subId)->first();
} else {
$service = $service->databases()->where('id', $subId)->first();
}
if ($service) {
$foundServices[] = "$service->id-$service->name";
$statusFromDb = $service->status;
if ($statusFromDb !== $containerStatus) {
// ray('Updating status: ' . $containerStatus);
$service->update(['status' => $containerStatus]);
}
}
}
}
$exitedServices = collect([]);
foreach ($services as $service) {
$apps = $service->applications()->get();
$dbs = $service->databases()->get();
foreach ($apps as $app) {
if (in_array("$app->id-$app->name", $foundServices)) {
continue;
} else {
$exitedServices->push($app);
}
}
foreach ($dbs as $db) {
if (in_array("$db->id-$db->name", $foundServices)) {
continue;
} else {
$exitedServices->push($db);
}
}
}
$exitedServices = $exitedServices->unique('id');
foreach ($exitedServices as $exitedService) {
if (str($exitedService->status)->startsWith('exited')) {
continue;
}
$name = data_get($exitedService, 'name');
$fqdn = data_get($exitedService, 'fqdn');
$containerName = $name ? "$name, available at $fqdn" : $fqdn;
$projectUuid = data_get($service, 'environment.project.uuid');
$serviceUuid = data_get($service, 'uuid');
$environmentName = data_get($service, 'environment.name');
if ($projectUuid && $serviceUuid && $environmentName) {
$url = base_url() . '/project/' . $projectUuid . "/" . $environmentName . "/service/" . $serviceUuid;
} else {
$url = null;
}
$this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
$exitedService->update(['status' => 'exited']);
}
$notRunningApplications = $this->applications->pluck('id')->diff($foundApplications);
foreach ($notRunningApplications as $applicationId) {
$application = $this->applications->where('id', $applicationId)->first();
if (str($application->status)->startsWith('exited')) {
continue;
}
$application->update(['status' => 'exited']);
$name = data_get($application, 'name');
$fqdn = data_get($application, 'fqdn');
$containerName = $name ? "$name ($fqdn)" : $fqdn;
$projectUuid = data_get($application, 'environment.project.uuid');
$applicationUuid = data_get($application, 'uuid');
$environment = data_get($application, 'environment.name');
if ($projectUuid && $applicationUuid && $environment) {
$url = base_url() . '/project/' . $projectUuid . "/" . $environment . "/application/" . $applicationUuid;
} else {
$url = null;
}
$this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
}
$notRunningApplicationPreviews = $previews->pluck('id')->diff($foundApplicationPreviews);
foreach ($notRunningApplicationPreviews as $previewId) {
$preview = $previews->where('id', $previewId)->first();
if (str($preview->status)->startsWith('exited')) {
continue;
}
$preview->update(['status' => 'exited']);
$name = data_get($preview, 'name');
$fqdn = data_get($preview, 'fqdn');
$containerName = $name ? "$name ($fqdn)" : $fqdn;
$projectUuid = data_get($preview, 'application.environment.project.uuid');
$environmentName = data_get($preview, 'application.environment.name');
$applicationUuid = data_get($preview, 'application.uuid');
if ($projectUuid && $applicationUuid && $environmentName) {
$url = base_url() . '/project/' . $projectUuid . "/" . $environmentName . "/application/" . $applicationUuid;
} else {
$url = null;
}
$this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
}
$notRunningDatabases = $databases->pluck('id')->diff($foundDatabases);
foreach ($notRunningDatabases as $database) {
$database = $databases->where('id', $database)->first();
if (str($database->status)->startsWith('exited')) {
continue;
}
$database->update(['status' => 'exited']);
$name = data_get($database, 'name');
$fqdn = data_get($database, 'fqdn');
$containerName = $name;
$projectUuid = data_get($database, 'environment.project.uuid');
$environmentName = data_get($database, 'environment.name');
$databaseUuid = data_get($database, 'uuid');
if ($projectUuid && $databaseUuid && $environmentName) {
$url = base_url() . '/project/' . $projectUuid . "/" . $environmentName . "/database/" . $databaseUuid;
} else {
$url = null;
}
$this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
}
// Check if proxy is running
$this->server->proxyType();
$foundProxyContainer = $containers->filter(function ($value, $key) {
if ($this->server->isSwarm()) {
return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik';
} else {
return data_get($value, 'Name') === '/coolify-proxy';
}
})->first();
if (!$foundProxyContainer) {
try {
$shouldStart = CheckProxy::run($this->server);
if ($shouldStart) {
StartProxy::run($this->server, false);
$this->server->team?->notify(new ContainerRestarted('coolify-proxy', $this->server));
}
} catch (\Throwable $e) {
ray($e);
}
} else {
$this->server->proxy->status = data_get($foundProxyContainer, 'State.Status');
$this->server->save();
$connectProxyToDockerNetworks = connectProxyToNetworks($this->server);
instant_remote_process($connectProxyToDockerNetworks, $this->server, false);
}
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Actions\Server;
use Lorisleiva\Actions\Concerns\AsAction;
use App\Models\Server;
class StartSentinel
{
use AsAction;
public function handle(Server $server, $version = 'latest', bool $restart = false)
{
if ($restart) {
instant_remote_process(['docker rm -f coolify-sentinel'], $server, false);
}
instant_remote_process([
"docker run --rm --pull always -d -e \"SCHEDULER=true\" --name coolify-sentinel -v /var/run/docker.sock:/var/run/docker.sock -v /data/coolify/metrics:/app/metrics -v /data/coolify/logs:/app/logs --pid host --health-cmd \"curl --fail http://127.0.0.1:8888/api/health || exit 1\" --health-interval 10s --health-retries 3 ghcr.io/coollabsio/sentinel:$version",
"chown -R 9999:root /data/coolify/metrics /data/coolify/logs",
"chmod -R 700 /data/coolify/metrics /data/coolify/logs"
], $server, false);
}
}

View File

@@ -9,6 +9,7 @@ use App\Jobs\ScheduledTaskJob;
use App\Jobs\InstanceAutoUpdateJob; use App\Jobs\InstanceAutoUpdateJob;
use App\Jobs\ContainerStatusJob; use App\Jobs\ContainerStatusJob;
use App\Jobs\PullHelperImageJob; use App\Jobs\PullHelperImageJob;
use App\Jobs\PullSentinelImageJob;
use App\Jobs\ServerStatusJob; use App\Jobs\ServerStatusJob;
use App\Models\InstanceSettings; use App\Models\InstanceSettings;
use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledDatabaseBackup;
@@ -57,7 +58,10 @@ class Kernel extends ConsoleKernel
{ {
$servers = Server::all()->where('settings.is_usable', true)->where('settings.is_reachable', true)->where('ip', '!=', '1.2.3.4'); $servers = Server::all()->where('settings.is_usable', true)->where('settings.is_reachable', true)->where('ip', '!=', '1.2.3.4');
foreach ($servers as $server) { foreach ($servers as $server) {
$schedule->job(new PullHelperImageJob($server))->everyTenMinutes()->onOneServer(); if (config('coolify.is_sentinel_enabled')) {
$schedule->job(new PullSentinelImageJob($server))->everyFiveMinutes()->onOneServer();
}
$schedule->job(new PullHelperImageJob($server))->everyFiveMinutes()->onOneServer();
} }
} }
private function check_resources($schedule) private function check_resources($schedule)

View File

@@ -37,7 +37,7 @@ class Controller extends BaseController
public function email_verify(EmailVerificationRequest $request) { public function email_verify(EmailVerificationRequest $request) {
$request->fulfill(); $request->fulfill();
$name = request()->user()?->name; $name = request()->user()?->name;
send_internal_notification("User {$name} verified their email address."); // send_internal_notification("User {$name} verified their email address.");
return redirect(RouteServiceProvider::HOME); return redirect(RouteServiceProvider::HOME);
} }
public function forgot_password(Request $request) { public function forgot_password(Request $request) {

View File

@@ -2,6 +2,7 @@
namespace App\Jobs; namespace App\Jobs;
use App\Actions\Docker\GetContainersStatus;
use App\Enums\ApplicationDeploymentStatus; use App\Enums\ApplicationDeploymentStatus;
use App\Enums\ProcessStatus; use App\Enums\ProcessStatus;
use App\Events\ApplicationStatusChanged; use App\Events\ApplicationStatusChanged;
@@ -302,7 +303,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
{ {
if ($this->server->isProxyShouldRun()) { if ($this->server->isProxyShouldRun()) {
dispatch(new ContainerStatusJob($this->server)); GetContainersStatus::dispatch($this->server);
// dispatch(new ContainerStatusJob($this->server));
} }
$this->next(ApplicationDeploymentStatus::FINISHED->value); $this->next(ApplicationDeploymentStatus::FINISHED->value);
if ($this->pull_request_id !== 0) { if ($this->pull_request_id !== 0) {
@@ -869,7 +871,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->write_deployment_configurations(); $this->write_deployment_configurations();
$this->server = $this->original_server; $this->server = $this->original_server;
} }
if (count($this->application->ports_mappings_array) > 0 || (bool) $this->application->settings->is_consistent_container_name_enabled || $this->pull_request_id !== 0 || str($this->application->custom_docker_run_options)->contains('--ip') || str($this->application->custom_docker_run_options)->contains('--ip6')) { if (count($this->application->ports_mappings_array) > 0 || (bool) $this->application->settings->is_consistent_container_name_enabled || isset($this->application->settings->custom_internal_name) || $this->pull_request_id !== 0 || str($this->application->custom_docker_run_options)->contains('--ip') || str($this->application->custom_docker_run_options)->contains('--ip6')) {
$this->application_deployment_queue->addLogEntry("----------------------------------------"); $this->application_deployment_queue->addLogEntry("----------------------------------------");
if (count($this->application->ports_mappings_array) > 0) { if (count($this->application->ports_mappings_array) > 0) {
$this->application_deployment_queue->addLogEntry("Application has ports mapped to the host system, rolling update is not supported."); $this->application_deployment_queue->addLogEntry("Application has ports mapped to the host system, rolling update is not supported.");
@@ -877,6 +879,9 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if ((bool) $this->application->settings->is_consistent_container_name_enabled) { if ((bool) $this->application->settings->is_consistent_container_name_enabled) {
$this->application_deployment_queue->addLogEntry("Consistent container name feature enabled, rolling update is not supported."); $this->application_deployment_queue->addLogEntry("Consistent container name feature enabled, rolling update is not supported.");
} }
if (isset($this->application->settings->custom_internal_name)) {
$this->application_deployment_queue->addLogEntry("Custom internal name is set, rolling update is not supported.");
}
if ($this->pull_request_id !== 0) { if ($this->pull_request_id !== 0) {
$this->application->settings->is_consistent_container_name_enabled = true; $this->application->settings->is_consistent_container_name_enabled = true;
$this->application_deployment_queue->addLogEntry("Pull request deployment, rolling update is not supported."); $this->application_deployment_queue->addLogEntry("Pull request deployment, rolling update is not supported.");
@@ -1017,7 +1022,9 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
"command" => "docker rm -f {$this->deployment_uuid}", "command" => "docker rm -f {$this->deployment_uuid}",
"ignore_errors" => true, "ignore_errors" => true,
"hidden" => true "hidden" => true
], ]
);
$this->execute_remote_command(
[ [
$runCommand, $runCommand,
"hidden" => true, "hidden" => true,
@@ -1284,7 +1291,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->application->parseHealthcheckFromDockerfile($dockerfile); $this->application->parseHealthcheckFromDockerfile($dockerfile);
} }
$docker_compose = [ $docker_compose = [
'version' => '3.8',
'services' => [ 'services' => [
$this->container_name => [ $this->container_name => [
'image' => $this->production_image_name, 'image' => $this->production_image_name,
@@ -1292,7 +1298,11 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
'restart' => RESTART_MODE, 'restart' => RESTART_MODE,
'expose' => $ports, 'expose' => $ports,
'networks' => [ 'networks' => [
$this->destination->network, $this->destination->network => [
'aliases' => [
$this->container_name
]
]
], ],
'mem_limit' => $this->application->limits_memory, 'mem_limit' => $this->application->limits_memory,
'memswap_limit' => $this->application->limits_memory_swap, 'memswap_limit' => $this->application->limits_memory_swap,
@@ -1310,6 +1320,9 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
] ]
] ]
]; ];
if (isset($this->application->settings->custom_internal_name)) {
$docker_compose['services'][$this->container_name]['networks'][$this->destination->network]['aliases'][] = $this->application->settings->custom_internal_name;
}
// if (str($this->saved_outputs->get('dotenv'))->isNotEmpty()) { // if (str($this->saved_outputs->get('dotenv'))->isNotEmpty()) {
// if (data_get($docker_compose, "services.{$this->container_name}.env_file")) { // if (data_get($docker_compose, "services.{$this->container_name}.env_file")) {
// $docker_compose['services'][$this->container_name]['env_file'][] = '.env'; // $docker_compose['services'][$this->container_name]['env_file'][] = '.env';
@@ -1519,95 +1532,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
return $local_persistent_volumes_names; return $local_persistent_volumes_names;
} }
/*private function generate_environment_variables($ports)
{
$environment_variables = collect();
if ($this->pull_request_id === 0) {
foreach ($this->application->runtime_environment_variables as $env) {
// This is necessary because we have to escape the value of the environment variable
// but only if the environment variable is created after 4.0.0-beta.240
// when I implemented the escaping feature.
// Old environment variables are not escaped, because it could break the application
// as the application could expect the unescaped value.
if ($env->version === '4.0.0-beta.239') {
$real_value = $env->real_value;
} else {
$real_value = escapeEnvVariables($env->real_value);
}
if ($env->is_literal) {
$real_value = escapeDollarSign($real_value);
$environment_variables->push("$env->key='$real_value'");
} else {
$environment_variables->push("$env->key=$real_value");
}
}
foreach ($this->application->nixpacks_environment_variables as $env) {
if ($env->version === '4.0.0-beta.239') {
$real_value = $env->real_value;
} else {
$real_value = escapeEnvVariables($env->real_value);
}
if ($env->is_literal) {
$real_value = escapeDollarSign($real_value);
$environment_variables->push("$env->key='$real_value'");
} else {
$environment_variables->push("$env->key=$real_value");
}
}
} else {
foreach ($this->application->runtime_environment_variables_preview as $env) {
if ($env->version === '4.0.0-beta.239') {
$real_value = $env->real_value;
} else {
$real_value = escapeEnvVariables($env->real_value);
}
if ($env->is_literal) {
$real_value = escapeDollarSign($real_value);
$environment_variables->push("$env->key='$real_value'");
} else {
$environment_variables->push("$env->key=$real_value");
}
}
foreach ($this->application->nixpacks_environment_variables_preview as $env) {
if ($env->version === '4.0.0-beta.239') {
$real_value = $env->real_value;
} else {
$real_value = escapeEnvVariables($env->real_value);
}
if ($env->is_literal) {
$real_value = escapeDollarSign($real_value);
$environment_variables->push("$env->key='$real_value'");
} else {
$environment_variables->push("$env->key=$real_value");
}
}
}
// Add PORT if not exists, use the first port as default
if ($environment_variables->filter(fn ($env) => Str::of($env)->startsWith('PORT'))->isEmpty()) {
$environment_variables->push("PORT={$ports[0]}");
}
// Add HOST if not exists
if ($environment_variables->filter(fn ($env) => Str::of($env)->startsWith('HOST'))->isEmpty()) {
$environment_variables->push("HOST=0.0.0.0");
}
if ($environment_variables->filter(fn ($env) => Str::of($env)->startsWith('SOURCE_COMMIT'))->isEmpty()) {
if (!is_null($this->commit)) {
$environment_variables->push("SOURCE_COMMIT={$this->commit}");
} else {
$environment_variables->push("SOURCE_COMMIT=unknown");
}
}
ray($environment_variables->all());
return $environment_variables->all();
}*/
private function generate_healthcheck_commands() private function generate_healthcheck_commands()
{ {
// if ($this->application->dockerfile || $this->application->build_pack === 'dockerfile' || $this->application->build_pack === 'dockerimage') {
// // TODO: disabled HC because there are several ways to hc a simple docker image, hard to figure out a good way. Like some docker images (pocketbase) does not have curl.
// return 'exit 0';
// }
if (!$this->application->health_check_port) { if (!$this->application->health_check_port) {
$health_check_port = $this->application->ports_exposes_array[0]; $health_check_port = $this->application->ports_exposes_array[0];
} else { } else {
@@ -1619,12 +1545,12 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if ($this->application->health_check_path) { if ($this->application->health_check_path) {
$this->full_healthcheck_url = "{$this->application->health_check_method}: {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}{$this->application->health_check_path}"; $this->full_healthcheck_url = "{$this->application->health_check_method}: {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}{$this->application->health_check_path}";
$generated_healthchecks_commands = [ $generated_healthchecks_commands = [
"curl -s -X {$this->application->health_check_method} -f {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}{$this->application->health_check_path} > /dev/null" "curl -s -X {$this->application->health_check_method} -f {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}{$this->application->health_check_path} > /dev/null || wget -q -O- {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}{$this->application->health_check_path} > /dev/null || exit 1"
]; ];
} else { } else {
$this->full_healthcheck_url = "{$this->application->health_check_method}: {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}/"; $this->full_healthcheck_url = "{$this->application->health_check_method}: {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}/";
$generated_healthchecks_commands = [ $generated_healthchecks_commands = [
"curl -s -X {$this->application->health_check_method} -f {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}/" "curl -s -X {$this->application->health_check_method} -f {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}/ > /dev/null || wget -q -O- {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}/ > /dev/null || exit 1"
]; ];
} }
return implode(' ', $generated_healthchecks_commands); return implode(' ', $generated_healthchecks_commands);
@@ -1813,12 +1739,17 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
["docker rm -f $containerName >/dev/null 2>&1", "hidden" => true, "ignore_errors" => true], ["docker rm -f $containerName >/dev/null 2>&1", "hidden" => true, "ignore_errors" => true],
); );
}); });
if ($this->application->settings->is_consistent_container_name_enabled) { if ($this->application->settings->is_consistent_container_name_enabled || isset($this->application->settings->custom_internal_name)) {
$this->execute_remote_command( $this->execute_remote_command(
["docker rm -f $this->container_name >/dev/null 2>&1", "hidden" => true, "ignore_errors" => true], ["docker rm -f $this->container_name >/dev/null 2>&1", "hidden" => true, "ignore_errors" => true],
); );
} }
} else { } else {
if ($this->application->dockerfile || $this->application->build_pack === 'dockerfile' || $this->application->build_pack === 'dockerimage') {
$this->application_deployment_queue->addLogEntry("----------------------------------------");
$this->application_deployment_queue->addLogEntry("WARNING: Dockerfile or Docker Image based deployment detected. The healthcheck needs a curl or wget command to check the health of the application. Please make sure that it is available in the image or turn off healthcheck on Coolify's UI.");
$this->application_deployment_queue->addLogEntry("----------------------------------------");
}
$this->application_deployment_queue->addLogEntry("New container is not healthy, rolling back to the old container."); $this->application_deployment_queue->addLogEntry("New container is not healthy, rolling back to the old container.");
$this->application_deployment_queue->update([ $this->application_deployment_queue->update([
'status' => ApplicationDeploymentStatus::FAILED->value, 'status' => ApplicationDeploymentStatus::FAILED->value,

View File

@@ -2,15 +2,8 @@
namespace App\Jobs; namespace App\Jobs;
use App\Actions\Database\StartDatabaseProxy; use App\Actions\Docker\GetContainersStatus;
use App\Actions\Proxy\CheckProxy;
use App\Actions\Proxy\StartProxy;
use App\Actions\Shared\ComplexStatusCheck;
use App\Models\ApplicationPreview;
use App\Models\Server; use App\Models\Server;
use App\Models\ServiceDatabase;
use App\Notifications\Container\ContainerRestarted;
use App\Notifications\Container\ContainerStopped;
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;
@@ -18,7 +11,6 @@ use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; 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\Arr;
class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
{ {
@@ -44,335 +36,337 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
public function handle() public function handle()
{ {
if (!$this->server->isFunctional()) { GetContainersStatus::run($this->server);
return 'Server is not ready.'; return;
}; // if (!$this->server->isFunctional()) {
$applications = $this->server->applications(); // return 'Server is not ready.';
$skip_these_applications = collect([]); // };
foreach ($applications as $application) { // $applications = $this->server->applications();
if ($application->additional_servers->count() > 0) { // $skip_these_applications = collect([]);
$skip_these_applications->push($application); // foreach ($applications as $application) {
ComplexStatusCheck::run($application); // if ($application->additional_servers->count() > 0) {
$applications = $applications->filter(function ($value, $key) use ($application) { // $skip_these_applications->push($application);
return $value->id !== $application->id; // ComplexStatusCheck::run($application);
}); // $applications = $applications->filter(function ($value, $key) use ($application) {
} // return $value->id !== $application->id;
} // });
$applications = $applications->filter(function ($value, $key) use ($skip_these_applications) { // }
return !$skip_these_applications->pluck('id')->contains($value->id); // }
}); // $applications = $applications->filter(function ($value, $key) use ($skip_these_applications) {
try { // return !$skip_these_applications->pluck('id')->contains($value->id);
if ($this->server->isSwarm()) { // });
$containers = instant_remote_process(["docker service inspect $(docker service ls -q) --format '{{json .}}'"], $this->server, false); // try {
$containerReplicates = instant_remote_process(["docker service ls --format '{{json .}}'"], $this->server, false); // if ($this->server->isSwarm()) {
} else { // $containers = instant_remote_process(["docker service inspect $(docker service ls -q) --format '{{json .}}'"], $this->server, false);
// Precheck for containers // $containerReplicates = instant_remote_process(["docker service ls --format '{{json .}}'"], $this->server, false);
$containers = instant_remote_process(["docker container ls -q"], $this->server, false); // } else {
if (!$containers) { // // Precheck for containers
return; // $containers = instant_remote_process(["docker container ls -q"], $this->server, false);
} // if (!$containers) {
$containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this->server, false); // return;
$containerReplicates = null; // }
} // $containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this->server, false);
if (is_null($containers)) { // $containerReplicates = null;
return; // }
} // if (is_null($containers)) {
// return;
// }
$containers = format_docker_command_output_to_json($containers); // $containers = format_docker_command_output_to_json($containers);
if ($containerReplicates) { // if ($containerReplicates) {
$containerReplicates = format_docker_command_output_to_json($containerReplicates); // $containerReplicates = format_docker_command_output_to_json($containerReplicates);
foreach ($containerReplicates as $containerReplica) { // foreach ($containerReplicates as $containerReplica) {
$name = data_get($containerReplica, 'Name'); // $name = data_get($containerReplica, 'Name');
$containers = $containers->map(function ($container) use ($name, $containerReplica) { // $containers = $containers->map(function ($container) use ($name, $containerReplica) {
if (data_get($container, 'Spec.Name') === $name) { // if (data_get($container, 'Spec.Name') === $name) {
$replicas = data_get($containerReplica, 'Replicas'); // $replicas = data_get($containerReplica, 'Replicas');
$running = str($replicas)->explode('/')[0]; // $running = str($replicas)->explode('/')[0];
$total = str($replicas)->explode('/')[1]; // $total = str($replicas)->explode('/')[1];
if ($running === $total) { // if ($running === $total) {
data_set($container, 'State.Status', 'running'); // data_set($container, 'State.Status', 'running');
data_set($container, 'State.Health.Status', 'healthy'); // data_set($container, 'State.Health.Status', 'healthy');
} else { // } else {
data_set($container, 'State.Status', 'starting'); // data_set($container, 'State.Status', 'starting');
data_set($container, 'State.Health.Status', 'unhealthy'); // data_set($container, 'State.Health.Status', 'unhealthy');
} // }
} // }
return $container; // return $container;
}); // });
} // }
} // }
$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()) { // if ($this->server->isSwarm()) {
$labels = data_get($container, 'Spec.Labels'); // $labels = data_get($container, 'Spec.Labels');
$uuid = data_get($labels, 'coolify.name'); // $uuid = data_get($labels, 'coolify.name');
} else { // } else {
$labels = data_get($container, 'Config.Labels'); // $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 = 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) {
$pullRequestId = data_get($labels, 'coolify.pullRequestId'); // $pullRequestId = data_get($labels, 'coolify.pullRequestId');
if ($pullRequestId) { // if ($pullRequestId) {
if (str($applicationId)->contains('-')) { // if (str($applicationId)->contains('-')) {
$applicationId = str($applicationId)->before('-'); // $applicationId = str($applicationId)->before('-');
} // }
$preview = ApplicationPreview::where('application_id', $applicationId)->where('pull_request_id', $pullRequestId)->first(); // $preview = ApplicationPreview::where('application_id', $applicationId)->where('pull_request_id', $pullRequestId)->first();
if ($preview) { // if ($preview) {
$foundApplicationPreviews[] = $preview->id; // $foundApplicationPreviews[] = $preview->id;
$statusFromDb = $preview->status; // $statusFromDb = $preview->status;
if ($statusFromDb !== $containerStatus) { // if ($statusFromDb !== $containerStatus) {
$preview->update(['status' => $containerStatus]); // $preview->update(['status' => $containerStatus]);
} // }
} else { // } else {
//Notify user that this container should not be there. // //Notify user that this container should not be there.
} // }
} else { // } else {
$application = $applications->where('id', $applicationId)->first(); // $application = $applications->where('id', $applicationId)->first();
if ($application) { // if ($application) {
$foundApplications[] = $application->id; // $foundApplications[] = $application->id;
$statusFromDb = $application->status; // $statusFromDb = $application->status;
if ($statusFromDb !== $containerStatus) { // if ($statusFromDb !== $containerStatus) {
$application->update(['status' => $containerStatus]); // $application->update(['status' => $containerStatus]);
} // }
} else { // } else {
//Notify user that this container should not be there. // //Notify user that this container should not be there.
} // }
} // }
} else { // } else {
$uuid = data_get($labels, 'com.docker.compose.service'); // $uuid = data_get($labels, 'com.docker.compose.service');
$type = data_get($labels, 'coolify.type'); // $type = data_get($labels, 'coolify.type');
if ($uuid) { // if ($uuid) {
if ($type === 'service') { // if ($type === 'service') {
$database_id = data_get($labels, 'coolify.service.subId'); // $database_id = data_get($labels, 'coolify.service.subId');
if ($database_id) { // if ($database_id) {
$service_db = ServiceDatabase::where('id', $database_id)->first(); // $service_db = ServiceDatabase::where('id', $database_id)->first();
if ($service_db) { // if ($service_db) {
$uuid = $service_db->service->uuid; // $uuid = $service_db->service->uuid;
$isPublic = data_get($service_db, 'is_public'); // $isPublic = data_get($service_db, 'is_public');
if ($isPublic) { // if ($isPublic) {
$foundTcpProxy = $containers->filter(function ($value, $key) use ($uuid) { // $foundTcpProxy = $containers->filter(function ($value, $key) use ($uuid) {
if ($this->server->isSwarm()) { // if ($this->server->isSwarm()) {
return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid"; // return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid";
} else { // } else {
return data_get($value, 'Name') === "/$uuid-proxy"; // return data_get($value, 'Name') === "/$uuid-proxy";
} // }
})->first(); // })->first();
if (!$foundTcpProxy) { // if (!$foundTcpProxy) {
StartDatabaseProxy::run($service_db); // StartDatabaseProxy::run($service_db);
// $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$service_db->service->name}", $this->server)); // // $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$service_db->service->name}", $this->server));
} // }
} // }
} // }
} // }
} else { // } else {
$database = $databases->where('uuid', $uuid)->first(); // $database = $databases->where('uuid', $uuid)->first();
if ($database) { // if ($database) {
$isPublic = data_get($database, 'is_public'); // $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) { // if ($isPublic) {
$foundTcpProxy = $containers->filter(function ($value, $key) use ($uuid) { // $foundTcpProxy = $containers->filter(function ($value, $key) use ($uuid) {
if ($this->server->isSwarm()) { // if ($this->server->isSwarm()) {
return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid"; // return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid";
} else { // } else {
return data_get($value, 'Name') === "/$uuid-proxy"; // return data_get($value, 'Name') === "/$uuid-proxy";
} // }
})->first(); // })->first();
if (!$foundTcpProxy) { // if (!$foundTcpProxy) {
StartDatabaseProxy::run($database); // StartDatabaseProxy::run($database);
$this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server)); // $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.
} // }
} // }
} // }
if (data_get($container, 'Name') === '/coolify-db') { // if (data_get($container, 'Name') === '/coolify-db') {
$foundDatabases[] = 0; // $foundDatabases[] = 0;
} // }
} // }
$serviceLabelId = data_get($labels, 'coolify.serviceId'); // $serviceLabelId = data_get($labels, 'coolify.serviceId');
if ($serviceLabelId) { // if ($serviceLabelId) {
$subType = data_get($labels, 'coolify.service.subType'); // $subType = data_get($labels, 'coolify.service.subType');
$subId = data_get($labels, 'coolify.service.subId'); // $subId = data_get($labels, 'coolify.service.subId');
$service = $services->where('id', $serviceLabelId)->first(); // $service = $services->where('id', $serviceLabelId)->first();
if (!$service) { // if (!$service) {
continue; // continue;
} // }
if ($subType === 'application') { // if ($subType === 'application') {
$service = $service->applications()->where('id', $subId)->first(); // $service = $service->applications()->where('id', $subId)->first();
} else { // } else {
$service = $service->databases()->where('id', $subId)->first(); // $service = $service->databases()->where('id', $subId)->first();
} // }
if ($service) { // if ($service) {
$foundServices[] = "$service->id-$service->name"; // $foundServices[] = "$service->id-$service->name";
$statusFromDb = $service->status; // $statusFromDb = $service->status;
if ($statusFromDb !== $containerStatus) { // if ($statusFromDb !== $containerStatus) {
// ray('Updating status: ' . $containerStatus); // // ray('Updating status: ' . $containerStatus);
$service->update(['status' => $containerStatus]); // $service->update(['status' => $containerStatus]);
} // }
} // }
} // }
} // }
$exitedServices = collect([]); // $exitedServices = collect([]);
foreach ($services as $service) { // foreach ($services as $service) {
$apps = $service->applications()->get(); // $apps = $service->applications()->get();
$dbs = $service->databases()->get(); // $dbs = $service->databases()->get();
foreach ($apps as $app) { // foreach ($apps as $app) {
if (in_array("$app->id-$app->name", $foundServices)) { // if (in_array("$app->id-$app->name", $foundServices)) {
continue; // continue;
} else { // } else {
$exitedServices->push($app); // $exitedServices->push($app);
} // }
} // }
foreach ($dbs as $db) { // foreach ($dbs as $db) {
if (in_array("$db->id-$db->name", $foundServices)) { // if (in_array("$db->id-$db->name", $foundServices)) {
continue; // continue;
} else { // } else {
$exitedServices->push($db); // $exitedServices->push($db);
} // }
} // }
} // }
$exitedServices = $exitedServices->unique('id'); // $exitedServices = $exitedServices->unique('id');
foreach ($exitedServices as $exitedService) { // foreach ($exitedServices as $exitedService) {
if (str($exitedService->status)->startsWith('exited')) { // if (str($exitedService->status)->startsWith('exited')) {
continue; // continue;
} // }
$name = data_get($exitedService, 'name'); // $name = data_get($exitedService, 'name');
$fqdn = data_get($exitedService, 'fqdn'); // $fqdn = data_get($exitedService, 'fqdn');
$containerName = $name ? "$name, available at $fqdn" : $fqdn; // $containerName = $name ? "$name, available at $fqdn" : $fqdn;
$projectUuid = data_get($service, 'environment.project.uuid'); // $projectUuid = data_get($service, 'environment.project.uuid');
$serviceUuid = data_get($service, 'uuid'); // $serviceUuid = data_get($service, 'uuid');
$environmentName = data_get($service, 'environment.name'); // $environmentName = data_get($service, 'environment.name');
if ($projectUuid && $serviceUuid && $environmentName) { // if ($projectUuid && $serviceUuid && $environmentName) {
$url = base_url() . '/project/' . $projectUuid . "/" . $environmentName . "/service/" . $serviceUuid; // $url = base_url() . '/project/' . $projectUuid . "/" . $environmentName . "/service/" . $serviceUuid;
} 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']);
} // }
$notRunningApplications = $applications->pluck('id')->diff($foundApplications); // $notRunningApplications = $applications->pluck('id')->diff($foundApplications);
foreach ($notRunningApplications as $applicationId) { // foreach ($notRunningApplications as $applicationId) {
$application = $applications->where('id', $applicationId)->first(); // $application = $applications->where('id', $applicationId)->first();
if (str($application->status)->startsWith('exited')) { // if (str($application->status)->startsWith('exited')) {
continue; // continue;
} // }
$application->update(['status' => 'exited']); // $application->update(['status' => 'exited']);
$name = data_get($application, 'name'); // $name = data_get($application, 'name');
$fqdn = data_get($application, 'fqdn'); // $fqdn = data_get($application, 'fqdn');
$containerName = $name ? "$name ($fqdn)" : $fqdn; // $containerName = $name ? "$name ($fqdn)" : $fqdn;
$projectUuid = data_get($application, 'environment.project.uuid'); // $projectUuid = data_get($application, 'environment.project.uuid');
$applicationUuid = data_get($application, 'uuid'); // $applicationUuid = data_get($application, 'uuid');
$environment = data_get($application, 'environment.name'); // $environment = data_get($application, 'environment.name');
if ($projectUuid && $applicationUuid && $environment) { // if ($projectUuid && $applicationUuid && $environment) {
$url = base_url() . '/project/' . $projectUuid . "/" . $environment . "/application/" . $applicationUuid; // $url = base_url() . '/project/' . $projectUuid . "/" . $environment . "/application/" . $applicationUuid;
} 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));
} // }
$notRunningApplicationPreviews = $previews->pluck('id')->diff($foundApplicationPreviews); // $notRunningApplicationPreviews = $previews->pluck('id')->diff($foundApplicationPreviews);
foreach ($notRunningApplicationPreviews as $previewId) { // foreach ($notRunningApplicationPreviews as $previewId) {
$preview = $previews->where('id', $previewId)->first(); // $preview = $previews->where('id', $previewId)->first();
if (str($preview->status)->startsWith('exited')) { // if (str($preview->status)->startsWith('exited')) {
continue; // continue;
} // }
$preview->update(['status' => 'exited']); // $preview->update(['status' => 'exited']);
$name = data_get($preview, 'name'); // $name = data_get($preview, 'name');
$fqdn = data_get($preview, 'fqdn'); // $fqdn = data_get($preview, 'fqdn');
$containerName = $name ? "$name ($fqdn)" : $fqdn; // $containerName = $name ? "$name ($fqdn)" : $fqdn;
$projectUuid = data_get($preview, 'application.environment.project.uuid'); // $projectUuid = data_get($preview, 'application.environment.project.uuid');
$environmentName = data_get($preview, 'application.environment.name'); // $environmentName = data_get($preview, 'application.environment.name');
$applicationUuid = data_get($preview, 'application.uuid'); // $applicationUuid = data_get($preview, 'application.uuid');
if ($projectUuid && $applicationUuid && $environmentName) { // if ($projectUuid && $applicationUuid && $environmentName) {
$url = base_url() . '/project/' . $projectUuid . "/" . $environmentName . "/application/" . $applicationUuid; // $url = base_url() . '/project/' . $projectUuid . "/" . $environmentName . "/application/" . $applicationUuid;
} 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));
} // }
$notRunningDatabases = $databases->pluck('id')->diff($foundDatabases); // $notRunningDatabases = $databases->pluck('id')->diff($foundDatabases);
foreach ($notRunningDatabases as $database) { // foreach ($notRunningDatabases as $database) {
$database = $databases->where('id', $database)->first(); // $database = $databases->where('id', $database)->first();
if (str($database->status)->startsWith('exited')) { // if (str($database->status)->startsWith('exited')) {
continue; // continue;
} // }
$database->update(['status' => 'exited']); // $database->update(['status' => 'exited']);
$name = data_get($database, 'name'); // $name = data_get($database, 'name');
$fqdn = data_get($database, 'fqdn'); // $fqdn = data_get($database, 'fqdn');
$containerName = $name; // $containerName = $name;
$projectUuid = data_get($database, 'environment.project.uuid'); // $projectUuid = data_get($database, 'environment.project.uuid');
$environmentName = data_get($database, 'environment.name'); // $environmentName = data_get($database, 'environment.name');
$databaseUuid = data_get($database, 'uuid'); // $databaseUuid = data_get($database, 'uuid');
if ($projectUuid && $databaseUuid && $environmentName) { // if ($projectUuid && $databaseUuid && $environmentName) {
$url = base_url() . '/project/' . $projectUuid . "/" . $environmentName . "/database/" . $databaseUuid; // $url = base_url() . '/project/' . $projectUuid . "/" . $environmentName . "/database/" . $databaseUuid;
} 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) {
if ($this->server->isSwarm()) { // if ($this->server->isSwarm()) {
return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik'; // return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik';
} else { // } else {
return data_get($value, 'Name') === '/coolify-proxy'; // 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));
} // }
} catch (\Throwable $e) { // } catch (\Throwable $e) {
ray($e); // ray($e);
} // }
} else { // } else {
$this->server->proxy->status = data_get($foundProxyContainer, 'State.Status'); // $this->server->proxy->status = data_get($foundProxyContainer, 'State.Status');
$this->server->save(); // $this->server->save();
$connectProxyToDockerNetworks = connectProxyToNetworks($this->server); // $connectProxyToDockerNetworks = connectProxyToNetworks($this->server);
instant_remote_process($connectProxyToDockerNetworks, $this->server, false); // instant_remote_process($connectProxyToDockerNetworks, $this->server, false);
} // }
} 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());
return handleError($e); // return handleError($e);
} // }
} }
} }

View File

@@ -0,0 +1,56 @@
<?php
namespace App\Jobs;
use App\Actions\Server\StartSentinel;
use App\Models\Server;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
class PullSentinelImageJob implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $timeout = 1000;
public function middleware(): array
{
return [(new WithoutOverlapping($this->server->uuid))];
}
public function uniqueId(): string
{
return $this->server->uuid;
}
public function __construct(public Server $server)
{
}
public function handle(): void
{
try {
$version = get_latest_sentinel_version();
if (!$version) {
ray('Failed to get latest Sentinel version');
return;
}
$local_version = instant_remote_process(['docker exec coolify-sentinel sh -c "curl http://127.0.0.1:8888/api/version"'], $this->server, false);
if (empty($local_version)) {
$local_version = '0.0.0';
}
if (version_compare($local_version, $version, '<')) {
StartSentinel::run($this->server, $version, true);
return;
}
ray('Sentinel image is up to date');
} catch (\Throwable $e) {
send_internal_notification('PullSentinelImageJob failed with: ' . $e->getMessage());
ray($e->getMessage());
throw $e;
}
}
}

View File

@@ -17,7 +17,7 @@ class ServerStatusJob implements ShouldQueue, ShouldBeEncrypted
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int|string|null $disk_usage = null; public int|string|null $disk_usage = null;
public $tries = 4; public $tries = 3;
public function backoff(): int public function backoff(): int
{ {
return isDev() ? 1 : 3; return isDev() ? 1 : 3;
@@ -38,12 +38,15 @@ class ServerStatusJob implements ShouldQueue, ShouldBeEncrypted
public function handle() public function handle()
{ {
if (!$this->server->isServerReady($this->tries)) { if (!$this->server->isServerReady($this->tries)) {
return "Server is not ready yet."; throw new \RuntimeException('Server is not ready.');
}; };
try { try {
if ($this->server->isFunctional()) { if ($this->server->isFunctional()) {
$this->cleanup(notify: false); $this->cleanup(notify: false);
$this->removeCoolifyYaml(); $this->removeCoolifyYaml();
if (config('coolify.is_sentinel_enabled')) {
$this->server->checkSentinel();
}
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
send_internal_notification('ServerStatusJob failed with: ' . $e->getMessage()); send_internal_notification('ServerStatusJob failed with: ' . $e->getMessage());

View File

@@ -21,6 +21,7 @@ class Advanced extends Component
'application.settings.is_gpu_enabled' => 'boolean|required', 'application.settings.is_gpu_enabled' => 'boolean|required',
'application.settings.is_build_server_enabled' => 'boolean|required', 'application.settings.is_build_server_enabled' => 'boolean|required',
'application.settings.is_consistent_container_name_enabled' => 'boolean|required', 'application.settings.is_consistent_container_name_enabled' => 'boolean|required',
'application.settings.custom_internal_name' => 'string|nullable',
'application.settings.is_gzip_enabled' => 'boolean|required', 'application.settings.is_gzip_enabled' => 'boolean|required',
'application.settings.is_stripprefix_enabled' => 'boolean|required', 'application.settings.is_stripprefix_enabled' => 'boolean|required',
'application.settings.gpu_driver' => 'string|required', 'application.settings.gpu_driver' => 'string|required',
@@ -30,7 +31,8 @@ class Advanced extends Component
'application.settings.is_raw_compose_deployment_enabled' => 'boolean|required', 'application.settings.is_raw_compose_deployment_enabled' => 'boolean|required',
'application.settings.connect_to_docker_network' => 'boolean|required', 'application.settings.connect_to_docker_network' => 'boolean|required',
]; ];
public function mount() { public function mount()
{
$this->is_force_https_enabled = $this->application->isForceHttpsEnabled(); $this->is_force_https_enabled = $this->application->isForceHttpsEnabled();
$this->is_gzip_enabled = $this->application->isGzipEnabled(); $this->is_gzip_enabled = $this->application->isGzipEnabled();
$this->is_stripprefix_enabled = $this->application->isStripprefixEnabled(); $this->is_stripprefix_enabled = $this->application->isStripprefixEnabled();
@@ -65,7 +67,8 @@ class Advanced extends Component
$this->dispatch('success', 'Settings saved.'); $this->dispatch('success', 'Settings saved.');
$this->dispatch('configurationChanged'); $this->dispatch('configurationChanged');
} }
public function submit() { public function submit()
{
if ($this->application->settings->gpu_count && $this->application->settings->gpu_device_ids) { if ($this->application->settings->gpu_count && $this->application->settings->gpu_device_ids) {
$this->dispatch('error', 'You cannot set both GPU count and GPU device IDs.'); $this->dispatch('error', 'You cannot set both GPU count and GPU device IDs.');
$this->application->settings->gpu_count = null; $this->application->settings->gpu_count = null;
@@ -76,6 +79,16 @@ class Advanced extends Component
$this->application->settings->save(); $this->application->settings->save();
$this->dispatch('success', 'Settings saved.'); $this->dispatch('success', 'Settings saved.');
} }
public function saveCustomName()
{
if (isset($this->application->settings->custom_internal_name)) {
$this->application->settings->custom_internal_name = str($this->application->settings->custom_internal_name)->slug()->value();
} else {
$this->application->settings->custom_internal_name = null;
}
$this->application->settings->save();
$this->dispatch('success', 'Custom name saved.');
}
public function render() public function render()
{ {
return view('livewire.project.application.advanced'); return view('livewire.project.application.advanced');

View File

@@ -255,7 +255,6 @@ class General extends Component
} }
public function resetDefaultLabels() public function resetDefaultLabels()
{ {
ray('resetDefaultLabels');
$this->customLabels = str(implode("|", generateLabelsApplication($this->application)))->replace("|", "\n"); $this->customLabels = str(implode("|", generateLabelsApplication($this->application)))->replace("|", "\n");
$this->ports_exposes = $this->application->ports_exposes; $this->ports_exposes = $this->application->ports_exposes;
@@ -299,7 +298,10 @@ class General extends Component
} }
if ($this->application->build_pack === 'dockercompose' && $this->initialDockerComposeLocation !== $this->application->docker_compose_location) { if ($this->application->build_pack === 'dockercompose' && $this->initialDockerComposeLocation !== $this->application->docker_compose_location) {
$this->loadComposeFile(); $compose_return = $this->loadComposeFile();
if ($compose_return instanceof \Livewire\Features\SupportEvents\Event) {
return;
}
} }
$this->validate(); $this->validate();
if ($this->ports_exposes !== $this->application->ports_exposes) { if ($this->ports_exposes !== $this->application->ports_exposes) {

View File

@@ -3,9 +3,9 @@
namespace App\Livewire\Project\Application; namespace App\Livewire\Project\Application;
use App\Actions\Application\StopApplication; use App\Actions\Application\StopApplication;
use App\Actions\Docker\GetContainersStatus;
use App\Events\ApplicationStatusChanged; use App\Events\ApplicationStatusChanged;
use App\Jobs\ComplexContainerStatusJob; use App\Jobs\ContainerStatusJob;
use App\Jobs\ContainerStatusJob;
use App\Jobs\ServerStatusJob; use App\Jobs\ServerStatusJob;
use App\Models\Application; use App\Models\Application;
use Livewire\Component; use Livewire\Component;
@@ -33,7 +33,8 @@ class Heading extends Component
public function check_status($showNotification = false) public function check_status($showNotification = false)
{ {
if ($this->application->destination->server->isFunctional()) { if ($this->application->destination->server->isFunctional()) {
dispatch(new ContainerStatusJob($this->application->destination->server)); GetContainersStatus::dispatch($this->application->destination->server);
// dispatch(new ContainerStatusJob($this->application->destination->server));
} else { } else {
dispatch(new ServerStatusJob($this->application->destination->server)); dispatch(new ServerStatusJob($this->application->destination->server));
} }

View File

@@ -11,6 +11,7 @@ use App\Actions\Database\StartMysql;
use App\Actions\Database\StartPostgresql; use App\Actions\Database\StartPostgresql;
use App\Actions\Database\StartRedis; use App\Actions\Database\StartRedis;
use App\Actions\Database\StopDatabase; use App\Actions\Database\StopDatabase;
use App\Actions\Docker\GetContainersStatus;
use App\Jobs\ContainerStatusJob; use App\Jobs\ContainerStatusJob;
use Livewire\Component; use Livewire\Component;
@@ -44,7 +45,8 @@ class Heading extends Component
public function check_status($showNotification = false) public function check_status($showNotification = false)
{ {
dispatch_sync(new ContainerStatusJob($this->database->destination->server)); GetContainersStatus::run($this->database->destination->server);
// dispatch_sync(new ContainerStatusJob($this->database->destination->server));
$this->database->refresh(); $this->database->refresh();
if ($showNotification) $this->dispatch('success', 'Database status updated.'); if ($showNotification) $this->dispatch('success', 'Database status updated.');
} }

View File

@@ -150,7 +150,7 @@ class GithubPrivateRepository extends Component
'repository_project_id' => $this->selected_repository_id, 'repository_project_id' => $this->selected_repository_id,
'git_repository' => "{$this->selected_repository_owner}/{$this->selected_repository_repo}", 'git_repository' => "{$this->selected_repository_owner}/{$this->selected_repository_repo}",
'git_branch' => $this->selected_branch_name, 'git_branch' => $this->selected_branch_name,
'build_pack' => 'nixpacks', 'build_pack' => $this->build_pack,
'ports_exposes' => $this->port, 'ports_exposes' => $this->port,
'publish_directory' => $this->publish_directory, 'publish_directory' => $this->publish_directory,
'environment_id' => $environment->id, 'environment_id' => $environment->id,
@@ -162,6 +162,9 @@ class GithubPrivateRepository extends Component
$application->settings->is_static = $this->is_static; $application->settings->is_static = $this->is_static;
$application->settings->save(); $application->settings->save();
if ($this->build_pack === 'dockerfile' || $this->build_pack === 'dockerimage') {
$application->health_check_enabled = false;
}
$fqdn = generateFqdn($destination->server, $application->uuid); $fqdn = generateFqdn($destination->server, $application->uuid);
$application->fqdn = $fqdn; $application->fqdn = $fqdn;

View File

@@ -19,7 +19,7 @@ class GithubPrivateRepositoryDeployKey extends Component
public $current_step = 'private_keys'; public $current_step = 'private_keys';
public $parameters; public $parameters;
public $query; public $query;
public $private_keys =[]; public $private_keys = [];
public int $private_key_id; public int $private_key_id;
public int $port = 3000; public int $port = 3000;
@@ -125,7 +125,7 @@ class GithubPrivateRepositoryDeployKey extends Component
'name' => generate_random_name(), 'name' => generate_random_name(),
'git_repository' => $this->git_repository, 'git_repository' => $this->git_repository,
'git_branch' => $this->branch, 'git_branch' => $this->branch,
'build_pack' => 'nixpacks', 'build_pack' => $this->build_pack,
'ports_exposes' => $this->port, 'ports_exposes' => $this->port,
'publish_directory' => $this->publish_directory, 'publish_directory' => $this->publish_directory,
'environment_id' => $environment->id, 'environment_id' => $environment->id,
@@ -138,7 +138,7 @@ class GithubPrivateRepositoryDeployKey extends Component
'name' => generate_random_name(), 'name' => generate_random_name(),
'git_repository' => $this->git_repository, 'git_repository' => $this->git_repository,
'git_branch' => $this->branch, 'git_branch' => $this->branch,
'build_pack' => 'nixpacks', 'build_pack' => $this->build_pack,
'ports_exposes' => $this->port, 'ports_exposes' => $this->port,
'publish_directory' => $this->publish_directory, 'publish_directory' => $this->publish_directory,
'environment_id' => $environment->id, 'environment_id' => $environment->id,
@@ -149,7 +149,9 @@ class GithubPrivateRepositoryDeployKey extends Component
'source_type' => $this->git_source->getMorphClass() 'source_type' => $this->git_source->getMorphClass()
]; ];
} }
if ($this->build_pack === 'dockerfile' || $this->build_pack === 'dockerimage') {
$application_init['health_check_enabled'] = false;
}
$application = Application::create($application_init); $application = Application::create($application_init);
$application->settings->is_static = $this->is_static; $application->settings->is_static = $this->is_static;
$application->settings->save(); $application->settings->save();

View File

@@ -98,7 +98,8 @@ class PublicGitRepository extends Component
(str($this->repository_url)->startsWith('https://') || (str($this->repository_url)->startsWith('https://') ||
str($this->repository_url)->startsWith('http://')) && str($this->repository_url)->startsWith('http://')) &&
!str($this->repository_url)->endsWith('.git') && !str($this->repository_url)->endsWith('.git') &&
!str($this->repository_url)->contains('github.com') (!str($this->repository_url)->contains('github.com') ||
!str($this->repository_url)->contains('git.sr.ht'))
) { ) {
$this->repository_url = $this->repository_url . '.git'; $this->repository_url = $this->repository_url . '.git';
} }
@@ -204,6 +205,9 @@ class PublicGitRepository extends Component
]; ];
} }
if ($this->build_pack === 'dockerfile' || $this->build_pack === 'dockerimage') {
$application_init['health_check_enabled'] = false;
}
$application = Application::create($application_init); $application = Application::create($application_init);
$application->settings->is_static = $this->is_static; $application->settings->is_static = $this->is_static;

View File

@@ -2,6 +2,7 @@
namespace App\Livewire\Project\Service; namespace App\Livewire\Project\Service;
use App\Actions\Docker\GetContainersStatus;
use App\Jobs\ContainerStatusJob; use App\Jobs\ContainerStatusJob;
use App\Models\Service; use App\Models\Service;
use Livewire\Component; use Livewire\Component;
@@ -64,7 +65,8 @@ class Configuration extends Component
public function check_status() public function check_status()
{ {
try { try {
dispatch_sync(new ContainerStatusJob($this->service->server)); GetContainersStatus::run($this->service->server);
// dispatch_sync(new ContainerStatusJob($this->service->server));
$this->dispatch('refresh')->self(); $this->dispatch('refresh')->self();
$this->dispatch('updateStatus'); $this->dispatch('updateStatus');
} catch (\Exception $e) { } catch (\Exception $e) {

View File

@@ -3,6 +3,7 @@
namespace App\Livewire\Project\Shared; namespace App\Livewire\Project\Shared;
use App\Actions\Application\StopApplicationOneServer; use App\Actions\Application\StopApplicationOneServer;
use App\Actions\Docker\GetContainersStatus;
use App\Events\ApplicationStatusChanged; use App\Events\ApplicationStatusChanged;
use App\Jobs\ContainerStatusJob; use App\Jobs\ContainerStatusJob;
use App\Models\Server; use App\Models\Server;
@@ -90,7 +91,8 @@ class Destination extends Component
} }
public function refreshServers() public function refreshServers()
{ {
ContainerStatusJob::dispatchSync($this->resource->destination->server); GetContainersStatus::run($this->resource->destination->server);
// ContainerStatusJob::dispatchSync($this->resource->destination->server);
$this->loadData(); $this->loadData();
$this->dispatch('refresh'); $this->dispatch('refresh');
ApplicationStatusChanged::dispatch(data_get($this->resource, 'environment.project.team.id')); ApplicationStatusChanged::dispatch(data_get($this->resource, 'environment.project.team.id'));

View File

@@ -122,7 +122,7 @@ class ExecuteContainerCommand extends Component
if ($server->isForceDisabled()) { if ($server->isForceDisabled()) {
throw new \RuntimeException('Server is disabled.'); throw new \RuntimeException('Server is disabled.');
} }
$cmd = 'sh -c "if [ -f ~/.profile ]; then . ~/.profile; fi; ' . str_replace('"', '\"', $this->command) . '"'; $cmd = "sh -c 'if [ -f ~/.profile ]; then . ~/.profile; fi; " . str_replace("'", "'\''", $this->command) . "'";
if (!empty($this->workDir)) { if (!empty($this->workDir)) {
$exec = "docker exec -w {$this->workDir} {$container_name} {$cmd}"; $exec = "docker exec -w {$this->workDir} {$container_name} {$cmd}";
} else { } else {

View File

@@ -27,7 +27,7 @@ class Logs extends Component
public $query; public $query;
public $status; public $status;
public $serviceSubType; public $serviceSubType;
public $cpu;
public function loadContainers($server_id) public function loadContainers($server_id)
{ {
try { try {
@@ -49,6 +49,14 @@ class Logs extends Component
return handleError($e, $this); return handleError($e, $this);
} }
} }
public function loadMetrics()
{
return;
$server = data_get($this->resource, 'destination.server');
if ($server->isFunctional()) {
$this->cpu = $server->getMetrics();
}
}
public function mount() public function mount()
{ {
try { try {
@@ -95,6 +103,7 @@ class Logs extends Component
} }
} }
$this->containers = $this->containers->sort(); $this->containers = $this->containers->sort();
$this->loadMetrics();
} catch (\Exception $e) { } catch (\Exception $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View File

@@ -2,6 +2,7 @@
namespace App\Livewire\Server\Proxy; namespace App\Livewire\Server\Proxy;
use App\Actions\Docker\GetContainersStatus;
use App\Actions\Proxy\CheckProxy; use App\Actions\Proxy\CheckProxy;
use App\Jobs\ContainerStatusJob; use App\Jobs\ContainerStatusJob;
use App\Models\Server; use App\Models\Server;
@@ -49,7 +50,8 @@ class Status extends Component
public function getProxyStatus() public function getProxyStatus()
{ {
try { try {
dispatch_sync(new ContainerStatusJob($this->server)); GetContainersStatus::run($this->server);
// dispatch_sync(new ContainerStatusJob($this->server));
$this->dispatch('proxyStatusUpdated'); $this->dispatch('proxyStatusUpdated');
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);

View File

@@ -26,6 +26,7 @@ class Deployments extends Component
"server_id", "server_id",
"status" "status"
])->sortBy('id')->groupBy('server_name')->toArray(); ])->sortBy('id')->groupBy('server_name')->toArray();
$this->dispatch('deployments', $this->deployments_per_tag_per_server);
} catch (\Exception $e) { } catch (\Exception $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View File

@@ -20,6 +20,12 @@ class Index extends Component
public $webhook = null; public $webhook = null;
public $deployments_per_tag_per_server = []; public $deployments_per_tag_per_server = [];
protected $listeners = ['deployments' => 'update_deployments'];
public function update_deployments($deployments)
{
$this->deployments_per_tag_per_server = $deployments;
}
public function tag_updated() public function tag_updated()
{ {
if ($this->tag == "") { if ($this->tag == "") {
@@ -39,14 +45,13 @@ class Index extends Component
public function redeploy_all() public function redeploy_all()
{ {
try { try {
$message = collect([]); $this->applications->each(function ($resource){
$this->applications->each(function ($resource) use ($message) {
$deploy = new Deploy(); $deploy = new Deploy();
$message->push($deploy->deploy_resource($resource)); $deploy->deploy_resource($resource);
}); });
$this->services->each(function ($resource) use ($message) { $this->services->each(function ($resource) {
$deploy = new Deploy(); $deploy = new Deploy();
$message->push($deploy->deploy_resource($resource)); $deploy->deploy_resource($resource);
}); });
$this->dispatch('success', 'Mass deployment started.'); $this->dispatch('success', 'Mass deployment started.');
} catch (\Exception $e) { } catch (\Exception $e) {

View File

@@ -847,7 +847,7 @@ class Application extends BaseModel
if (!$composeFileContent) { if (!$composeFileContent) {
$this->docker_compose_location = $initialDockerComposeLocation; $this->docker_compose_location = $initialDockerComposeLocation;
$this->save(); $this->save();
throw new \RuntimeException("Could not load base compose file from $workdir$composeFile"); throw new \RuntimeException("Docker Compose file not found at: $workdir$composeFile");
} else { } else {
$this->docker_compose_raw = $composeFileContent; $this->docker_compose_raw = $composeFileContent;
$this->save(); $this->save();

View File

@@ -3,14 +3,16 @@
namespace App\Models; namespace App\Models;
use App\Actions\Server\InstallDocker; use App\Actions\Server\InstallDocker;
use App\Actions\Server\StartSentinel;
use App\Enums\ProxyTypes; use App\Enums\ProxyTypes;
use App\Jobs\PullSentinelImageJob;
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\Collection;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
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;
@@ -463,58 +465,98 @@ $schema://$host {
Storage::disk('ssh-keys')->delete($sshKeyFileLocation); Storage::disk('ssh-keys')->delete($sshKeyFileLocation);
Storage::disk('ssh-mux')->delete($this->muxFilename()); Storage::disk('ssh-mux')->delete($this->muxFilename());
} }
public function checkSentinel()
{
ray("Checking sentinel on server: {$this->name}");
if ($this->is_metrics_enabled) {
$sentinel_found = instant_remote_process(["docker inspect coolify-sentinel"], $this, false);
$sentinel_found = json_decode($sentinel_found, true);
$status = data_get($sentinel_found, '0.State.Status', 'exited');
if ($status !== 'running') {
ray('Sentinel is not running, starting it...');
PullSentinelImageJob::dispatch($this);
} else {
ray('Sentinel is running');
}
}
}
public function getMetrics()
{
if ($this->is_metrics_enabled) {
$from = now()->subMinutes(5)->toIso8601ZuluString();
$cpu = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl http://localhost:8888/api/cpu/history?from=$from'"], $this, false);
$cpu = str($cpu)->explode("\n")->skip(1)->all();
$parsedCollection = collect($cpu)->flatMap(function ($item) {
return collect(explode("\n", trim($item)))->map(function ($line) {
list($time, $value) = explode(',', trim($line));
return [(int) $time, (float) $value];
});
})->toArray();
return $parsedCollection;
}
}
public function isServerReady(int $tries = 3) public function isServerReady(int $tries = 3)
{ {
if ($this->skipServer()) { if ($this->skipServer()) {
return false; return false;
} }
$checkIteration = 1; $serverUptimeCheckNumber = $this->unreachable_count;
$isServerReady = false; if ($this->unreachable_count < $tries) {
while ($checkIteration < $tries) { $serverUptimeCheckNumber = $this->unreachable_count + 1;
['uptime' => $uptime] = $this->validateConnection(); }
if ($uptime) { if ($this->unreachable_count > $tries) {
if ($this->unreachable_notification_sent === true) { $serverUptimeCheckNumber = $tries;
$this->update(['unreachable_notification_sent' => false]); }
$serverUptimeCheckNumberMax = $tries;
// ray('server: ' . $this->name);
// ray('serverUptimeCheckNumber: ' . $serverUptimeCheckNumber);
// ray('serverUptimeCheckNumberMax: ' . $serverUptimeCheckNumberMax);
['uptime' => $uptime] = $this->validateConnection();
if ($uptime) {
if ($this->unreachable_notification_sent === true) {
$this->update(['unreachable_notification_sent' => false]);
}
return true;
} else {
if ($serverUptimeCheckNumber >= $serverUptimeCheckNumberMax) {
// Reached max number of retries
if ($this->unreachable_notification_sent === false) {
ray('Server unreachable, sending notification...');
$this->team?->notify(new Unreachable($this));
$this->update(['unreachable_notification_sent' => true]);
}
if ($this->settings->is_reachable === true) {
$this->settings()->update([
'is_reachable' => false,
]);
}
foreach ($this->applications() as $application) {
$application->update(['status' => 'exited']);
}
foreach ($this->databases() as $database) {
$database->update(['status' => 'exited']);
}
foreach ($this->services()->get() as $service) {
$apps = $service->applications()->get();
$dbs = $service->databases()->get();
foreach ($apps as $app) {
$app->update(['status' => 'exited']);
}
foreach ($dbs as $db) {
$db->update(['status' => 'exited']);
}
} }
$this->settings()->update([
'is_reachable' => true,
]);
$isServerReady = true;
break;
} else { } else {
ray('Server is not ready yet.'); $this->update([
$checkIteration++; 'unreachable_count' => $this->unreachable_count + 1,
Sleep::for(10)->seconds(); ]);
} }
return false;
} }
if ($isServerReady) {
return $isServerReady;
}
if ($this->unreachable_notification_sent === false) {
ray('Server unreachable, sending notification...');
$this->team?->notify(new Unreachable($this));
$this->update(['unreachable_notification_sent' => true]);
}
$this->settings()->update([
'is_reachable' => false,
]);
foreach ($this->applications() as $application) {
$application->update(['status' => 'exited']);
}
foreach ($this->databases() as $database) {
$database->update(['status' => 'exited']);
}
foreach ($this->services()->get() as $service) {
$apps = $service->applications()->get();
$dbs = $service->databases()->get();
foreach ($apps as $app) {
$app->update(['status' => 'exited']);
}
foreach ($dbs as $db) {
$db->update(['status' => 'exited']);
}
}
return false;
} }
public function getDiskUsage() public function getDiskUsage()
{ {
@@ -539,7 +581,36 @@ $schema://$host {
{ {
return instant_remote_process(["docker start $id"], $this); return instant_remote_process(["docker start $id"], $this);
} }
public function loadUnmanagedContainers() public function getContainers(): Collection
{
$sentinel_found = instant_remote_process(["docker inspect coolify-sentinel"], $this, false);
$sentinel_found = json_decode($sentinel_found, true);
$status = data_get($sentinel_found, '0.State.Status', 'exited');
if ($status === 'running') {
$containers = instant_remote_process(['docker exec coolify-sentinel sh -c "curl http://127.0.0.1:8888/api/containers"'], $this, false);
if (is_null($containers)) {
return collect([]);
}
$containers = data_get(json_decode($containers, true), 'containers', []);
return collect($containers);
} else {
if ($this->isSwarm()) {
$containers = instant_remote_process(["docker service inspect $(docker service ls -q) --format '{{json .}}'"], $this, false);
} else {
$containers = instant_remote_process(["docker container ls -q"], $this, false);
if (!$containers) {
return collect([]);
}
$containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this, false);
}
if (is_null($containers)) {
return collect([]);
}
return format_docker_command_output_to_json($containers);
}
}
public function loadUnmanagedContainers(): Collection
{ {
if ($this->isFunctional()) { if ($this->isFunctional()) {
$containers = instant_remote_process(["docker ps -a --format '{{json .}}'"], $this); $containers = instant_remote_process(["docker ps -a --format '{{json .}}'"], $this);
@@ -762,11 +833,6 @@ $schema://$host {
$server->settings()->update([ $server->settings()->update([
'is_reachable' => false, 'is_reachable' => false,
]); ]);
if (data_get($server, 'unreachable_notification_sent') === false) {
ray('Server unreachable, sending notification...');
$this->team?->notify(new Unreachable($this));
$this->update(['unreachable_notification_sent' => true]);
}
return ['uptime' => false, 'error' => $e->getMessage()]; return ['uptime' => false, 'error' => $e->getMessage()];
} }
} }

View File

@@ -171,7 +171,7 @@ class Service extends BaseModel
], ],
]); ]);
} }
$fields->put('Tolgee', $data); $fields->put('Tolgee', $data->toArray());
break; break;
case str($image)?->contains('logto'): case str($image)?->contains('logto'):
$data = collect([]); $data = collect([]);
@@ -195,7 +195,7 @@ class Service extends BaseModel
], ],
]); ]);
} }
$fields->put('Logto', $data); $fields->put('Logto', $data->toArray());
break; break;
case str($image)?->contains('unleash-server'): case str($image)?->contains('unleash-server'):
$data = collect([]); $data = collect([]);
@@ -218,7 +218,7 @@ class Service extends BaseModel
], ],
]); ]);
} }
$fields->put('Unleash', $data); $fields->put('Unleash', $data->toArray());
break; break;
case str($image)?->contains('grafana'): case str($image)?->contains('grafana'):
$data = collect([]); $data = collect([]);
@@ -241,7 +241,7 @@ class Service extends BaseModel
], ],
]); ]);
} }
$fields->put('Grafana', $data); $fields->put('Grafana', $data->toArray());
break; break;
case str($image)?->contains('directus'): case str($image)?->contains('directus'):
$data = collect([]); $data = collect([]);
@@ -267,7 +267,7 @@ class Service extends BaseModel
], ],
]); ]);
} }
$fields->put('Directus', $data); $fields->put('Directus', $data->toArray());
break; break;
case str($image)?->contains('kong'): case str($image)?->contains('kong'):
$data = collect([]); $data = collect([]);
@@ -370,7 +370,7 @@ class Service extends BaseModel
], ],
]); ]);
} }
$fields->put('Weblate', $data); $fields->put('Weblate', $data->toArray());
break; break;
case str($image)?->contains('meilisearch'): case str($image)?->contains('meilisearch'):
$data = collect([]); $data = collect([]);
@@ -384,7 +384,7 @@ class Service extends BaseModel
], ],
]); ]);
} }
$fields->put('Meilisearch', $data); $fields->put('Meilisearch', $data->toArray());
break; break;
case str($image)?->contains('ghost'): case str($image)?->contains('ghost'):
$data = collect([]); $data = collect([]);
@@ -444,7 +444,31 @@ class Service extends BaseModel
]); ]);
} }
$fields->put('Ghost', $data); $fields->put('Ghost', $data->toArray());
break;
default:
$data = collect([]);
$admin_user = $this->environment_variables()->where('key', 'SERVICE_USER_ADMIN')->first();
$admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_ADMIN')->first();
$data = $data->merge([
'User' => [
'key' => 'SERVICE_USER_ADMIN',
'value' => data_get($admin_user, 'value', 'admin'),
'readonly' => true,
'rules' => 'required',
],
]);
if ($admin_password) {
$data = $data->merge([
'Password' => [
'key' => 'SERVICE_PASSWORD_ADMIN',
'value' => data_get($admin_password, 'value'),
'rules' => 'required',
'isPassword' => true,
],
]);
}
$fields->put('Admin', $data->toArray());
break; break;
} }
} }

View File

@@ -2,6 +2,7 @@
namespace App\Notifications\Server; namespace App\Notifications\Server;
use App\Actions\Docker\GetContainersStatus;
use App\Jobs\ContainerStatusJob; use App\Jobs\ContainerStatusJob;
use App\Models\Server; use App\Models\Server;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
@@ -22,7 +23,8 @@ class Revived extends Notification implements ShouldQueue
if ($this->server->unreachable_notification_sent === false) { if ($this->server->unreachable_notification_sent === false) {
return; return;
} }
dispatch(new ContainerStatusJob($server)); GetContainersStatus::dispatch($server);
// dispatch(new ContainerStatusJob($server));
} }
public function via(object $notifiable): array public function via(object $notifiable): array

View File

@@ -147,6 +147,18 @@ function get_route_parameters(): array
return Route::current()->parameters(); return Route::current()->parameters();
} }
function get_latest_sentinel_version(): string
{
try {
$response = Http::get('https://cdn.coollabs.io/coolify/versions.json');
$versions = $response->json();
return data_get($versions, 'coolify.sentinel.version');
} catch (\Throwable $e) {
//throw $e;
ray($e->getMessage());
return '0.0.0';
}
}
function get_latest_version_of_coolify(): string function get_latest_version_of_coolify(): string
{ {
try { try {
@@ -637,7 +649,6 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$allServices = getServiceTemplates(); $allServices = getServiceTemplates();
$topLevelVolumes = collect(data_get($yaml, 'volumes', [])); $topLevelVolumes = collect(data_get($yaml, 'volumes', []));
$topLevelNetworks = collect(data_get($yaml, 'networks', [])); $topLevelNetworks = collect(data_get($yaml, 'networks', []));
$dockerComposeVersion = data_get($yaml, 'version') ?? '3.8';
$services = data_get($yaml, 'services'); $services = data_get($yaml, 'services');
$generatedServiceFQDNS = collect([]); $generatedServiceFQDNS = collect([]);
@@ -1192,7 +1203,6 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
return $service; return $service;
}); });
$finalServices = [ $finalServices = [
'version' => $dockerComposeVersion,
'services' => $services->toArray(), 'services' => $services->toArray(),
'volumes' => $topLevelVolumes->toArray(), 'volumes' => $topLevelVolumes->toArray(),
'networks' => $topLevelNetworks->toArray(), 'networks' => $topLevelNetworks->toArray(),
@@ -1230,7 +1240,6 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$topLevelVolumes = collect([]); $topLevelVolumes = collect([]);
} }
$topLevelNetworks = collect(data_get($yaml, 'networks', [])); $topLevelNetworks = collect(data_get($yaml, 'networks', []));
$dockerComposeVersion = data_get($yaml, 'version') ?? '3.8';
$services = data_get($yaml, 'services'); $services = data_get($yaml, 'services');
$generatedServiceFQDNS = collect([]); $generatedServiceFQDNS = collect([]);
@@ -1661,7 +1670,6 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
}); });
} }
$finalServices = [ $finalServices = [
'version' => $dockerComposeVersion,
'services' => $services->toArray(), 'services' => $services->toArray(),
'volumes' => $topLevelVolumes->toArray(), 'volumes' => $topLevelVolumes->toArray(),
'networks' => $topLevelNetworks->toArray(), 'networks' => $topLevelNetworks->toArray(),
@@ -1841,7 +1849,7 @@ function validate_dns_entry(string $fqdn, Server $server)
$dns_servers = data_get($settings, 'custom_dns_servers'); $dns_servers = data_get($settings, 'custom_dns_servers');
$dns_servers = str($dns_servers)->explode(','); $dns_servers = str($dns_servers)->explode(',');
if ($server->id === 0) { if ($server->id === 0) {
$ip = data_get($settings, 'public_ipv4') || data_get($settings, 'public_ipv6') || $server->ip; $ip = data_get($settings, 'public_ipv4', data_get($settings, 'public_ipv6', $server->ip));
} else { } else {
$ip = $server->ip; $ip = $server->ip;
} }
@@ -1920,7 +1928,6 @@ function check_domain_usage(ServiceApplication|Application|null $resource = null
$naked_domain = str($domain)->value(); $naked_domain = str($domain)->value();
if ($domains->contains($naked_domain)) { if ($domains->contains($naked_domain)) {
if (data_get($resource, 'uuid')) { if (data_get($resource, 'uuid')) {
ray($resource->uuid, $app->uuid);
if ($resource->uuid !== $app->uuid) { if ($resource->uuid !== $app->uuid) {
throw new \RuntimeException("Domain $naked_domain is already in use by another resource called: <br><br>{$app->name}."); throw new \RuntimeException("Domain $naked_domain is already in use by another resource called: <br><br>{$app->name}.");
} }

View File

@@ -14,4 +14,5 @@ return [
'helper_image' => env('HELPER_IMAGE', 'ghcr.io/coollabsio/coolify-helper:latest'), 'helper_image' => env('HELPER_IMAGE', 'ghcr.io/coollabsio/coolify-helper:latest'),
'is_horizon_enabled' => env('HORIZON_ENABLED', true), 'is_horizon_enabled' => env('HORIZON_ENABLED', true),
'is_scheduler_enabled' => env('SCHEDULER_ENABLED', true), 'is_scheduler_enabled' => env('SCHEDULER_ENABLED', true),
'is_sentinel_enabled' => env('SENTINEL_ENABLED', false),
]; ];

View File

@@ -7,7 +7,7 @@ return [
// 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.273', 'release' => '4.0.0-beta.277',
// 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.273'; return '4.0.0-beta.277';

View File

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

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('servers', function (Blueprint $table) {
$table->boolean('is_metrics_enabled')->default(true);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('servers', function (Blueprint $table) {
$table->dropColumn('is_metrics_enabled');
});
}
};

View File

@@ -1,5 +1,3 @@
version: "3.8"
services: services:
coolify: coolify:
build: build:

View File

@@ -1,4 +1,3 @@
version: '3.8'
services: services:
coolify: coolify:
image: "ghcr.io/coollabsio/coolify:${LATEST_IMAGE:-latest}" image: "ghcr.io/coollabsio/coolify:${LATEST_IMAGE:-latest}"

View File

@@ -1,4 +1,3 @@
version: '3.8'
services: services:
coolify-testing-host: coolify-testing-host:
init: true init: true

View File

@@ -1,4 +1,3 @@
version: '3.8'
services: services:
coolify: coolify:
container_name: coolify container_name: coolify
@@ -11,7 +10,6 @@ services:
depends_on: depends_on:
- postgres - postgres
- redis - redis
postgres: postgres:
image: postgres:15-alpine image: postgres:15-alpine
container_name: coolify-db container_name: coolify-db

30
lang/zh-cn.json Normal file
View File

@@ -0,0 +1,30 @@
{
"auth.login": "登录",
"auth.login.azure": "使用 Microsoft 登录",
"auth.login.bitbucket": "使用 Bitbucket 登录",
"auth.login.github": "使用 GitHub 登录",
"auth.login.gitlab": "使用 Gitlab 登录",
"auth.login.google": "使用 Google 登录",
"auth.already_registered": "已经注册?",
"auth.confirm_password": "确认密码",
"auth.forgot_password": "忘记密码",
"auth.forgot_password_send_email": "发送密码重置邮件",
"auth.register_now": "注册",
"auth.logout": "退出登录",
"auth.register": "注册",
"auth.registration_disabled": "注册已禁用,请联系管理员",
"auth.reset_password": "重置密码",
"auth.failed": "这些凭据与我们的记录不符",
"auth.failed.callback": "处理第三方登录的回调时出错",
"auth.failed.password": "密码错误",
"auth.failed.email": "该账户未注册",
"auth.throttle": "登录次数过多,请在 :seconds 秒后重试",
"input.name": "用户名",
"input.email": "邮箱",
"input.password": "密码",
"input.password.again": "确认密码",
"input.code": "验证码",
"input.recovery_code": "恢复码",
"button.save": "保存",
"repository.url": "<span class='text-helper'>示例</span><br>对于公共代码仓库,请使用 <span class='text-helper'>https://...</span>。<br>对于私有代码仓库,请使用 <span class='text-helper'>git@...</span>。<br><br>https://github.com/coollabsio/coolify-examples <span class='text-helper'>main</span> 分支将被选择<br>https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify <span class='text-helper'>nodejs-fastify</span> 分支将被选择。<br>https://gitea.com/sedlav/expressjs.git <span class='text-helper'>main</span> 分支将被选择。<br>https://gitlab.com/andrasbacsai/nodejs-example.git <span class='text-helper'>main</span> 分支将被选择"
}

View File

@@ -0,0 +1,11 @@
$handle = fopen("/tmp/export.csv", "w");
App\Models\Team::chunk(100, function ($teams) use ($handle) {
foreach ($teams as $team) {
if ($team->subscription->stripe_invoice_paid == true) {
foreach ($team->members as $member) {
fputcsv($handle, [$member->email, $member->name], ",");
}
}
}
});
fclose($handle);

2
public/svgs/listmonk.svg Normal file
View File

@@ -0,0 +1,2 @@
<svg xmlns="http://www.w3.org/2000/svg" width="163.03" height="30.38" viewBox="0 0 43.135 8.038" xmlns:v="https://vecta.io/nano"><circle cx="4.019" cy="4.019" r="3.149" fill="#fff" stroke="#0055d4" stroke-width="1.74"/><path d="M11.457 7.303q-.566 0-.879-.322-.313-.331-.313-.932V.712L11.5.572v5.442q0 .305.253.305.139 0 .244-.052l.253.879q-.357.157-.792.157zm2.619-4.754v4.615H12.84V2.549zM13.449.172q.331 0 .54.209.218.2.218.514 0 .313-.218.522-.209.2-.54.2-.331 0-.54-.2-.209-.209-.209-.522 0-.313.209-.514.209-.209.54-.209zm3.319 2.238q.975 0 1.672.557l-.47.705q-.583-.366-1.149-.366-.305 0-.47.113-.165.113-.165.305 0 .139.07.235.078.096.279.183.209.087.618.209.731.2 1.088.54.357.331.357.914 0 .462-.27.801-.261.34-.714.522-.453.174-1.01.174-.583 0-1.062-.174-.479-.183-.819-.496l.61-.679q.583.453 1.237.453.348 0 .549-.131.209-.139.209-.374 0-.183-.078-.287-.078-.104-.287-.192-.209-.096-.653-.218-.697-.192-1.036-.54-.331-.357-.331-.879 0-.392.226-.705.226-.313.636-.488.418-.183.967-.183zm5.342 4.536q-.253.174-.575.261-.313.096-.627.096-.714-.009-1.08-.409-.366-.401-.366-1.176V3.42h-.688v-.871h.688v-1.01l1.237-.148v1.158h1.062l-.122.871h-.94v2.273q0 .331.113.479.113.148.348.148.235 0 .522-.157zm5.493-4.536q.549 0 .879.374.34.374.34 1.019v3.361h-1.237V4.012q0-.679-.453-.679-.244 0-.427.157-.183.157-.374.488v3.187h-1.237V4.012q0-.679-.453-.679-.244 0-.427.165-.183.157-.366.479v3.187h-1.237V2.549h1.071l.096.575q.261-.348.583-.531.331-.183.758-.183.392 0 .679.2.287.192.418.549.287-.374.618-.557.34-.192.766-.192zm4.148 0q1.036 0 1.62.653.583.644.583 1.794 0 .731-.27 1.289-.261.549-.766.853-.496.305-1.176.305-1.036 0-1.628-.644-.583-.653-.583-1.803 0-.731.261-1.28.27-.557.766-.862.505-.305 1.193-.305zm0 .923q-.47 0-.705.374-.226.366-.226 1.149 0 .784.226 1.158.235.366.697.366.462 0 .688-.366.235-.374.235-1.158 0-.784-.226-1.149-.226-.374-.688-.374zm5.271-.923q.61 0 .949.374.34.366.34 1.019v3.361h-1.237V4.012q0-.374-.131-.522-.122-.157-.374-.157-.261 0-.479.165-.209.157-.409.479v3.187h-1.237V2.549h1.071l.096.583q.287-.357.627-.54.348-.183.784-.183zM40.2.572v6.592h-1.237V.712zm2.804 1.977l-1.472 2.029 1.602 2.586h-1.402l-1.489-2.525 1.48-2.09z"/></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -32,7 +32,7 @@ body {
@apply block w-full py-1.5 rounded border-0 text-sm ring-1 ring-inset; @apply block w-full py-1.5 rounded border-0 text-sm ring-1 ring-inset;
} }
.input[type='password'] { .input[type="password"] {
@apply pr-10; @apply pr-10;
} }
@@ -52,7 +52,6 @@ button[isHighlighted]:not(:disabled) {
@apply text-white bg-coollabs hover:bg-coollabs-100; @apply text-white bg-coollabs hover:bg-coollabs-100;
} }
h1 { h1 {
@apply text-2xl font-bold dark:text-white; @apply text-2xl font-bold dark:text-white;
} }
@@ -78,7 +77,7 @@ label {
} }
table { table {
@apply min-w-full divide-y dark:divide-coolgray-200 divide-neutral-300 ; @apply min-w-full divide-y dark:divide-coolgray-200 divide-neutral-300;
} }
thead { thead {
@@ -117,7 +116,7 @@ tr td:first-child {
@apply flex items-center gap-2 text-error; @apply flex items-center gap-2 text-error;
} }
.tag { .tag {
@apply px-2 py-1 cursor-pointer box-description dark:bg-coolgray-100 dark:hover:bg-coolgray-300 bg-neutral-100 hover:bg-neutral-200 @apply px-2 py-1 cursor-pointer box-description dark:bg-coolgray-100 dark:hover:bg-coolgray-300 bg-neutral-100 hover:bg-neutral-200;
} }
.add-tag { .add-tag {
@apply flex items-center px-2 text-xs cursor-pointer dark:text-neutral-500/20 text-neutral-500 group-hover:text-neutral-700 group-hover:dark:text-white dark:hover:bg-coolgray-300 hover:bg-neutral-200; @apply flex items-center px-2 text-xs cursor-pointer dark:text-neutral-500/20 text-neutral-500 group-hover:text-neutral-700 group-hover:dark:text-white dark:hover:bg-coolgray-300 hover:bg-neutral-200;
@@ -135,7 +134,6 @@ tr td:first-child {
.badge-absolute { .badge-absolute {
@apply absolute top-0 right-0 w-2 h-2 border-none rounded-t-none rounded-r-none; @apply absolute top-0 right-0 w-2 h-2 border-none rounded-t-none rounded-r-none;
} }
.badge-success { .badge-success {
@@ -159,7 +157,7 @@ tr td:first-child {
} }
.menu-item { .menu-item {
@apply flex items-center w-full gap-3 py-1 pl-2 dark:hover:bg-coolgray-100 dark:hover:text-white hover:bg-neutral-300; @apply flex items-center w-full gap-3 px-2 py-1 text-sm sm:pr-0 dark:hover:bg-coolgray-100 dark:hover:text-white hover:bg-neutral-300 min-w-fit sm:min-w-64;
} }
.menu-item-active { .menu-item-active {
@@ -174,7 +172,6 @@ tr td:first-child {
@apply w-6 h-6 dark:hover:text-white; @apply w-6 h-6 dark:hover:text-white;
} }
.scrollbar { .scrollbar {
@apply scrollbar-thumb-coollabs-100 dark:scrollbar-track-coolgray-200 scrollbar-track-neutral-200 scrollbar-w-2; @apply scrollbar-thumb-coollabs-100 dark:scrollbar-track-coolgray-200 scrollbar-track-neutral-200 scrollbar-w-2;
} }
@@ -188,7 +185,7 @@ tr td:first-child {
} }
.navbar-main { .navbar-main {
@apply flex items-center h-10 gap-6 pb-2 border-b-2 border-solid dark:border-coolgray-200; @apply flex flex-col gap-4 pb-2 border-b-2 border-solid h-fit md:flex-row justify-items-start sm:justify-between dark:border-coolgray-200 md:items-center;
} }
.loading { .loading {
@@ -203,20 +200,19 @@ tr td:first-child {
@apply relative flex lg:flex-row flex-col p-2 transition-colors cursor-pointer min-h-[4rem] dark:bg-coolgray-100 bg-white border text-black dark:text-white hover:text-black border-neutral-200 dark:border-black hover:bg-neutral-100 dark:hover:bg-coollabs-100 dark:hover:text-white hover:no-underline; @apply relative flex lg:flex-row flex-col p-2 transition-colors cursor-pointer min-h-[4rem] dark:bg-coolgray-100 bg-white border text-black dark:text-white hover:text-black border-neutral-200 dark:border-black hover:bg-neutral-100 dark:hover:bg-coollabs-100 dark:hover:text-white hover:no-underline;
} }
.box-boarding { .box-boarding {
@apply flex lg:flex-row flex-col p-2 transition-colors cursor-pointer min-h-[4rem] dark:bg-coolgray-100 dark:text-white bg-neutral-50 border border-neutral-200 dark:border-black hover:bg-neutral-100 dark:hover:bg-coollabs-100 dark:hover:text-white hover:text-black hover:no-underline text-black ; @apply flex lg:flex-row flex-col p-2 transition-colors cursor-pointer min-h-[4rem] dark:bg-coolgray-100 dark:text-white bg-neutral-50 border border-neutral-200 dark:border-black hover:bg-neutral-100 dark:hover:bg-coollabs-100 dark:hover:text-white hover:text-black hover:no-underline text-black;
} }
.box-without-bg { .box-without-bg {
@apply flex p-2 transition-colors dark:hover:text-white hover:no-underline min-h-[4rem] border border-neutral-200 dark:border-black; @apply flex p-2 transition-colors dark:hover:text-white hover:no-underline min-h-[4rem] border border-neutral-200 dark:border-black;
} }
.box-without-bg-without-border { .box-without-bg-without-border {
@apply flex p-2 transition-colors dark:hover:text-white hover:no-underline min-h-[4rem] ; @apply flex p-2 transition-colors dark:hover:text-white hover:no-underline min-h-[4rem];
} }
.on-box { .on-box {
@apply rounded hover:bg-neutral-300 dark:hover:bg-coolgray-500/20; @apply rounded hover:bg-neutral-300 dark:hover:bg-coolgray-500/20;
} }
.box-title { .box-title {
@apply font-bold text-black dark:text-white group-hover:dark:text-white; @apply font-bold text-black dark:text-white group-hover:dark:text-white;
} }

View File

@@ -4,8 +4,7 @@
<a class="flex items-center mb-6 text-5xl font-extrabold tracking-tight text-gray-900 dark:text-white"> <a class="flex items-center mb-6 text-5xl font-extrabold tracking-tight text-gray-900 dark:text-white">
Coolify Coolify
</a> </a>
<div <div class="w-full bg-white rounded-lg shadow md:mt-0 sm:max-w-md xl:p-0 dark:bg-base">
class="w-full bg-white rounded-lg shadow md:mt-0 sm:max-w-md xl:p-0 dark:bg-base">
<div class="p-6 space-y-4 md:space-y-6 sm:p-8"> <div class="p-6 space-y-4 md:space-y-6 sm:p-8">
<h1 class="text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl dark:text-white"> <h1 class="text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl dark:text-white">
Create an account Create an account

View File

@@ -84,7 +84,7 @@
<livewire:switch-team /> <livewire:switch-team />
</div> </div>
<ul role="list" class="flex flex-col flex-1 gap-y-7"> <ul role="list" class="flex flex-col flex-1 gap-y-7">
<li class="flex-1 "> <li class="flex-1 overflow-x-hidden">
<ul role="list" class="flex flex-col h-full space-y-1.5"> <ul role="list" class="flex flex-col h-full space-y-1.5">
@if (isSubscribed() || !isCloud()) @if (isSubscribed() || !isCloud())
<li> <li>

View File

@@ -1,18 +1,20 @@
<div class="pb-6"> <div class="pb-6">
<h1>Notifications</h1> <h1>Notifications</h1>
<div class="subtitle">Get notified about your infrastructure.</div> <div class="subtitle">Get notified about your infrastructure.</div>
<nav class="navbar-main"> <div class="navbar-main">
<a class="{{ request()->routeIs('notifications.email') ? 'dark:text-white' : '' }}" <nav class="flex items-center gap-6 min-h-10">
href="{{ route('notifications.email') }}"> <a class="{{ request()->routeIs('notifications.email') ? 'dark:text-white' : '' }}"
<button>Email</button> href="{{ route('notifications.email') }}">
</a> <button>Email</button>
<a class="{{ request()->routeIs('notifications.telegram') ? 'dark:text-white' : '' }}" </a>
href="{{ route('notifications.telegram') }}"> <a class="{{ request()->routeIs('notifications.telegram') ? 'dark:text-white' : '' }}"
<button>Telegram</button> href="{{ route('notifications.telegram') }}">
</a> <button>Telegram</button>
<a class="{{ request()->routeIs('notifications.discord') ? 'dark:text-white' : '' }}" </a>
href="{{ route('notifications.discord') }}"> <a class="{{ request()->routeIs('notifications.discord') ? 'dark:text-white' : '' }}"
<button>Discord</button> href="{{ route('notifications.discord') }}">
</a> <button>Discord</button>
</nav> </a>
</nav>
</div>
</div> </div>

View File

@@ -8,7 +8,7 @@
x-transition:leave-end="translate-y-full" x-init="setTimeout(() => { bannerVisible = true }, bannerVisibleAfter);" x-transition:leave-end="translate-y-full" x-init="setTimeout(() => { bannerVisible = true }, bannerVisibleAfter);"
class="fixed bottom-0 right-0 h-auto duration-300 ease-out px-5 pb-5 max-w-[46rem] z-[999]" x-cloak> class="fixed bottom-0 right-0 h-auto duration-300 ease-out px-5 pb-5 max-w-[46rem] z-[999]" x-cloak>
<div <div
class="flex flex-col items-center justify-between w-full h-full max-w-4xl p-6 mx-auto bg-white border shadow-lg lg:border-t dark:border-coolgray-300 dark:bg-coolgray-100/40 hover:dark:bg-coolgray-100/100 lg:p-8 lg:flex-row sm:rounded"> class="flex flex-col items-center justify-between w-full h-full max-w-4xl p-6 mx-auto bg-white border shadow-lg lg:border-t dark:border-coolgray-300 dark:bg-coolgray-100 hover:dark:bg-coolgray-100 lg:p-8 lg:flex-row sm:rounded">
<div <div
class="flex flex-col items-start h-full pb-0 text-xs lg:items-center lg:flex-row lg:pr-6 lg:space-x-5 dark:text-neutral-300 "> class="flex flex-col items-start h-full pb-0 text-xs lg:items-center lg:flex-row lg:pr-6 lg:space-x-5 dark:text-neutral-300 ">
@if (isset($icon)) @if (isset($icon))

View File

@@ -1,35 +1,34 @@
<nav class="flex pt-2 pb-10"> <nav class="flex pt-2 pb-10">
<ol class="flex items-center"> <ol class="flex items-center flex-wrap gap-y-1">
<li class="inline-flex items-center"> <li class="inline-flex items-center">
<a wire:navigate class="text-xs truncate lg:text-sm"
href="{{ route('project.show', ['project_uuid' => $this->parameters['project_uuid']]) }}">
{{ data_get($resource, 'environment.project.name', 'Undefined Name') }}</a>
</li>
<li>
<div class="flex items-center">
<svg aria-hidden="true" class="w-4 h-4 mx-1 font-bold dark:text-warning" fill="currentColor" viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
clip-rule="evenodd"></path>
</svg>
<a class="text-xs truncate lg:text-sm"
href="{{ route('project.resource.index', ['environment_name' => $this->parameters['environment_name'], 'project_uuid' => $this->parameters['project_uuid']]) }}">{{ $this->parameters['environment_name'] }}</a>
</div>
</li>
<li>
<div class="flex items-center"> <div class="flex items-center">
<a wire:navigate class="text-xs truncate lg:text-sm"
href="{{ route('project.show', ['project_uuid' => $this->parameters['project_uuid']]) }}">
{{ data_get($resource, 'environment.project.name', 'Undefined Name') }}</a>
<svg aria-hidden="true" class="w-4 h-4 mx-1 font-bold dark:text-warning" fill="currentColor" <svg aria-hidden="true" class="w-4 h-4 mx-1 font-bold dark:text-warning" fill="currentColor"
viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"> viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" <path fill-rule="evenodd"
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
clip-rule="evenodd"></path> clip-rule="evenodd"></path>
</svg> </svg>
<span class="text-xs truncate lg:text-sm">{{ data_get($resource, 'name') }}</span>
</div> </div>
</li> </li>
<li> <li>
<div class="flex items-center"> <div class="flex items-center">
<a class="text-xs truncate lg:text-sm"
href="{{ route('project.resource.index', ['environment_name' => $this->parameters['environment_name'], 'project_uuid' => $this->parameters['project_uuid']]) }}">{{ $this->parameters['environment_name'] }}</a>
<svg aria-hidden="true" class="w-4 h-4 mx-1 font-bold dark:text-warning" fill="currentColor"
viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
clip-rule="evenodd"></path>
</svg>
</div>
</li>
<li>
<div class="flex items-center">
<span class="text-xs truncate lg:text-sm">{{ data_get($resource, 'name') }}</span>
<svg aria-hidden="true" class="w-4 h-4 mx-1 font-bold dark:text-warning" fill="currentColor" <svg aria-hidden="true" class="w-4 h-4 mx-1 font-bold dark:text-warning" fill="currentColor"
viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"> viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" <path fill-rule="evenodd"

View File

@@ -1,12 +1,14 @@
<div class="pb-6"> <div class="pb-6">
<h1>Security</h1> <h1>Security</h1>
<div class="subtitle">Security related settings.</div> <div class="subtitle">Security related settings.</div>
<nav class="navbar-main"> <div class="navbar-main">
<a href="{{ route('security.private-key.index') }}"> <nav class="flex items-center gap-6 scrollbar min-h-10">
<button>Private Keys</button> <a href="{{ route('security.private-key.index') }}">
</a> <button>Private Keys</button>
<a href="{{ route('security.api-tokens') }}"> </a>
<button>API tokens</button> <a href="{{ route('security.api-tokens') }}">
</a> <button>API tokens</button>
</nav> </a>
</nav>
</div>
</div> </div>

View File

@@ -5,47 +5,49 @@
<livewire:server.proxy.status :server="$server" /> <livewire:server.proxy.status :server="$server" />
</div> </div>
<div class="subtitle">{{ data_get($server, 'name') }}.</div> <div class="subtitle">{{ data_get($server, 'name') }}.</div>
<nav class="navbar-main"> <div class="navbar-main">
<a class="{{ request()->routeIs('server.show') ? 'dark:text-white' : '' }}" <nav class="flex items-center gap-6 overflow-x-scroll sm:overflow-x-hidden scrollbar min-h-10 whitespace-nowrap">
href="{{ route('server.show', [ <a class="{{ request()->routeIs('server.show') ? 'dark:text-white' : '' }}"
'server_uuid' => data_get($parameters, 'server_uuid'), href="{{ route('server.show', [
]) }}">
<button>General</button>
</a>
<a class="{{ request()->routeIs('server.private-key') ? 'dark:text-white' : '' }}"
href="{{ route('server.private-key', [
'server_uuid' => data_get($parameters, 'server_uuid'),
]) }}">
<button>Private Key</button>
</a>
<a class="{{ request()->routeIs('server.resources') ? 'dark:text-white' : '' }}"
href="{{ route('server.resources', [
'server_uuid' => data_get($parameters, 'server_uuid'),
]) }}">
<button>Resources</button>
</a>
@if (!$server->isSwarmWorker() && !$server->settings->is_build_server)
<a class="{{ request()->routeIs('server.proxy') ? 'dark:text-white' : '' }}"
href="{{ route('server.proxy', [
'server_uuid' => data_get($parameters, 'server_uuid'), 'server_uuid' => data_get($parameters, 'server_uuid'),
]) }}"> ]) }}">
<button>Proxy</button> <button>General</button>
</a> </a>
<a class="{{ request()->routeIs('server.destinations') ? 'dark:text-white' : '' }}" <a class="{{ request()->routeIs('server.private-key') ? 'dark:text-white' : '' }}"
href="{{ route('server.destinations', [ href="{{ route('server.private-key', [
'server_uuid' => data_get($parameters, 'server_uuid'), 'server_uuid' => data_get($parameters, 'server_uuid'),
]) }}"> ]) }}">
<button>Destinations</button> <button>Private Key</button>
</a> </a>
<a class="{{ request()->routeIs('server.log-drains') ? 'dark:text-white' : '' }}" <a class="{{ request()->routeIs('server.resources') ? 'dark:text-white' : '' }}"
href="{{ route('server.log-drains', [ href="{{ route('server.resources', [
'server_uuid' => data_get($parameters, 'server_uuid'), 'server_uuid' => data_get($parameters, 'server_uuid'),
]) }}"> ]) }}">
<button>Log Drains</button> <button>Resources</button>
</a> </a>
@endif @if (!$server->isSwarmWorker() && !$server->settings->is_build_server)
<a class="{{ request()->routeIs('server.proxy') ? 'dark:text-white' : '' }}"
<div class="flex-1"></div> href="{{ route('server.proxy', [
<livewire:server.proxy.deploy :server="$server" /> 'server_uuid' => data_get($parameters, 'server_uuid'),
</nav> ]) }}">
<button>Proxy</button>
</a>
<a class="{{ request()->routeIs('server.destinations') ? 'dark:text-white' : '' }}"
href="{{ route('server.destinations', [
'server_uuid' => data_get($parameters, 'server_uuid'),
]) }}">
<button>Destinations</button>
</a>
<a class="{{ request()->routeIs('server.log-drains') ? 'dark:text-white' : '' }}"
href="{{ route('server.log-drains', [
'server_uuid' => data_get($parameters, 'server_uuid'),
]) }}">
<button>Log Drains</button>
</a>
@endif
</nav>
<div class="order-first sm:order-last">
<livewire:server.proxy.deploy :server="$server" />
</div>
</div>
</div> </div>

View File

@@ -1,17 +1,19 @@
<div class="pb-5"> <div class="pb-5">
<h1>Settings</h1> <h1>Settings</h1>
<div class="subtitle">Instance wide settings for Coolify.</div> <div class="subtitle">Instance wide settings for Coolify.</div>
<nav class="navbar-main"> <div class="navbar-main">
<a class="{{ request()->routeIs('settings.index') ? 'dark:text-white' : '' }}" <nav class="flex items-center gap-6 min-h-10 whitespace-nowrap">
href="{{ route('settings.index') }}"> <a class="{{ request()->routeIs('settings.index') ? 'dark:text-white' : '' }}"
<button>Configuration</button> href="{{ route('settings.index') }}">
</a> <button>Configuration</button>
@if (isCloud())
<a class="{{ request()->routeIs('settings.license') ? 'dark:text-white' : '' }}"
href="{{ route('settings.license') }}">
<button>Resale License</button>
</a> </a>
@endif @if (isCloud())
<div class="flex-1"></div> <a class="{{ request()->routeIs('settings.license') ? 'dark:text-white' : '' }}"
</nav> href="{{ route('settings.license') }}">
<button>Resale License</button>
</a>
@endif
<div class="flex-1"></div>
</nav>
</div>
</div> </div>

View File

@@ -6,14 +6,16 @@
</x-modal-input> </x-modal-input>
</div> </div>
<div class="subtitle">Team wide configurations.</div> <div class="subtitle">Team wide configurations.</div>
<nav class="navbar-main"> <div class="navbar-main">
<a class="{{ request()->routeIs('team.index') ? 'dark:text-white' : '' }}" href="{{ route('team.index') }}"> <nav class="flex items-center gap-6 min-h-10">
<button>General</button> <a class="{{ request()->routeIs('team.index') ? 'dark:text-white' : '' }}" href="{{ route('team.index') }}">
</a> <button>General</button>
<a class="{{ request()->routeIs('team.member.index') ? 'dark:text-white' : '' }}" </a>
href="{{ route('team.member.index') }}"> <a class="{{ request()->routeIs('team.member.index') ? 'dark:text-white' : '' }}"
<button>Members</button> href="{{ route('team.member.index') }}">
</a> <button>Members</button>
<div class="flex-1"></div> </a>
</nav> <div class="flex-1"></div>
</nav>
</div>
</div> </div>

View File

@@ -9,10 +9,10 @@
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 stroke-current shrink-0" fill="none" <svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 stroke-current shrink-0" fill="none"
viewBox="0 0 24 24"> viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /> d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg> </svg>
Your subscription has been activated! Welcome onboard! <br>It could take a few seconds before your Your subscription has been activated! Welcome onboard! <br>It could take a few seconds before your
subscription is activated.<br> Please be patient. subscription is activated.<br> Please be patient.
</div> </div>
@endif @endif
<h3 class="pb-4">Projects</h3> <h3 class="pb-4">Projects</h3>
@@ -23,23 +23,24 @@
@if (data_get($project, 'environments')->count() === 1) onclick="gotoProject('{{ data_get($project, 'uuid') }}', '{{ data_get($project, 'environments.0.name', 'production') }}')" @if (data_get($project, 'environments')->count() === 1) onclick="gotoProject('{{ data_get($project, 'uuid') }}', '{{ data_get($project, 'environments.0.name', 'production') }}')"
@else @else
onclick="window.location.href = '{{ route('project.show', ['project_uuid' => data_get($project, 'uuid')]) }}'" @endif> onclick="window.location.href = '{{ route('project.show', ['project_uuid' => data_get($project, 'uuid')]) }}'" @endif>
<div class="flex flex-col justify-center flex-1 mx-6"> <div class="flex flex-1 mx-6">
<div class="box-title">{{ $project->name }}</div> <div class="flex flex-col justify-center flex-1">
<div class="box-description"> <div class="box-title">{{ $project->name }}</div>
{{ $project->description }}</div> <div class="box-description">
</div> {{ $project->description }}</div>
<span </div>
class="flex items-center justify-center gap-2 pt-4 pb-2 mr-4 text-xs lg:py-0 lg:justify-normal"> <div class="flex items-center justify-center gap-2 text-xs font-bold ">
<a class="hover:underline" <a class="hover:underline"
href="{{ route('project.resource.create', ['project_uuid' => data_get($project, 'uuid'), 'environment_name' => data_get($project, 'environments.0.name', 'production')]) }}"> href="{{ route('project.resource.create', ['project_uuid' => data_get($project, 'uuid'), 'environment_name' => data_get($project, 'environments.0.name', 'production')]) }}">
<span class="p-2 font-bold">+ <span class="p-2 font-bold">+
Add Resource</span> Add Resource</span>
</a> </a>
<a class="font-bold hover:underline" <a class="hover:underline"
href="{{ route('project.edit', ['project_uuid' => data_get($project, 'uuid')]) }}"> href="{{ route('project.edit', ['project_uuid' => data_get($project, 'uuid')]) }}">
Settings Settings
</a> </a>
</span> </div>
</div>
</div> </div>
@endforeach @endforeach
</div> </div>

View File

@@ -1,10 +1,10 @@
<div> <div>
<div class="flex flex-col"> <div class="flex flex-col md:w-96">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<h2>Advanced</h2> <h2>Advanced</h2>
</div> </div>
<div>Advanced configuration for your application.</div> <div>Advanced configuration for your application.</div>
<div class="flex flex-col gap-1 pt-4 md:w-96"> <div class="flex flex-col gap-1 pt-4">
<h3>General</h3> <h3>General</h3>
@if ($application->git_based()) @if ($application->git_based())
<x-forms.checkbox helper="Automatically deploy new commits based on Git webhooks." instantSave <x-forms.checkbox helper="Automatically deploy new commits based on Git webhooks." instantSave
@@ -16,10 +16,6 @@
<x-forms.checkbox <x-forms.checkbox
helper="Your application will be available only on https if your domain starts with https://..." helper="Your application will be available only on https if your domain starts with https://..."
instantSave id="is_force_https_enabled" label="Force Https" /> instantSave id="is_force_https_enabled" label="Force Https" />
<x-forms.checkbox
helper="The deployed container will have the same name ({{ $application->uuid }}). <span class='font-bold dark:text-warning'>You will lose the rolling update feature!</span>"
instantSave id="application.settings.is_consistent_container_name_enabled"
label="Consistent Container Names" />
<x-forms.checkbox label="Enable gzip compression" <x-forms.checkbox label="Enable gzip compression"
helper="You can disable gzip compression if you want. Some services are compressing data by default. In this case, you do not need this." helper="You can disable gzip compression if you want. Some services are compressing data by default. In this case, you do not need this."
instantSave id="is_gzip_enabled" /> instantSave id="is_gzip_enabled" />
@@ -30,13 +26,22 @@
label="Raw Compose Deployment" label="Raw Compose Deployment"
helper="WARNING: Advanced use cases only. Your docker compose file will be deployed as-is. Nothing is modified by Coolify. You need to configure the proxy parts. More info in the <a class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/docker/compose#raw-docker-compose-deployment'>documentation.</a>" /> helper="WARNING: Advanced use cases only. Your docker compose file will be deployed as-is. Nothing is modified by Coolify. You need to configure the proxy parts. More info in the <a class='underline dark:text-white' href='https://coolify.io/docs/knowledge-base/docker/compose#raw-docker-compose-deployment'>documentation.</a>" />
@endif @endif
<h3>Container Names</h3>
<x-forms.checkbox
helper="The deployed container will have the same name ({{ $application->uuid }}). <span class='font-bold dark:text-warning'>You will lose the rolling update feature!</span>"
instantSave id="application.settings.is_consistent_container_name_enabled"
label="Consistent Container Names" />
<form class="flex items-end gap-2 pl-2" wire:submit.prevent='saveCustomName'>
<x-forms.input
helper="You can add a custom internal name for your container. This name will be used in the internal network. <br><br>The name will be converted to slug format when you save it. <span class='font-bold dark:text-warning'>You will lose the rolling update feature!</span>"
instantSave id="application.settings.custom_internal_name" label="Add Custom Internal Name" />
<x-forms.button type="submit">Save</x-forms.button>
</form>
@if ($application->build_pack === 'dockercompose') @if ($application->build_pack === 'dockercompose')
<h3>Network</h3> <h3>Network</h3>
<div class="w-96"> <x-forms.checkbox instantSave id="application.settings.connect_to_docker_network"
<x-forms.checkbox instantSave id="application.settings.connect_to_docker_network" label="Connect To Predefined Network"
label="Connect To Predefined Network" helper="By default, you do not reach the Coolify defined networks.<br>Starting a docker compose based resource will have an internal network. <br>If you connect to a Coolify defined network, you maybe need to use different internal DNS names to connect to a resource.<br><br>For more information, check <a class='underline dark:text-white' target='_blank' href='https://coolify.io/docs/knowledge-base/docker/compose#connect-to-predefined-networks'>this</a>." />
helper="By default, you do not reach the Coolify defined networks.<br>Starting a docker compose based resource will have an internal network. <br>If you connect to a Coolify defined network, you maybe need to use different internal DNS names to connect to a resource.<br><br>For more information, check <a class='underline dark:text-white' target='_blank' href='https://coolify.io/docs/knowledge-base/docker/compose#connect-to-predefined-networks'>this</a>." />
</div>
@endif @endif
@if (!$application->settings->is_raw_compose_deployment_enabled) @if (!$application->settings->is_raw_compose_deployment_enabled)
<h3>Logs</h3> <h3>Logs</h3>
@@ -60,16 +65,14 @@
@endif @endif
<form wire:submit="submit"> <form wire:submit="submit">
@if ($application->build_pack !== 'dockercompose') @if ($application->build_pack !== 'dockercompose')
<div class="w-96"> <x-forms.checkbox
<x-forms.checkbox helper="Enable GPU usage for this application. More info <a href='https://docs.docker.com/compose/gpu-support/' class='underline dark:text-white' target='_blank'>here</a>."
helper="Enable GPU usage for this application. More info <a href='https://docs.docker.com/compose/gpu-support/' class='underline dark:text-white' target='_blank'>here</a>." instantSave id="application.settings.is_gpu_enabled" label="Attach GPU" />
instantSave id="application.settings.is_gpu_enabled" label="Attach GPU" /> @if ($application->settings->is_gpu_enabled)
@if ($application->settings->is_gpu_enabled) <h5>GPU Settings</h5>
<h5>GPU Settings</h5>
<x-forms.button type="submit">Save</x-forms.button> <x-forms.button type="submit">Save</x-forms.button>
@endif @endif
</div>
@endif @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">

View File

@@ -2,8 +2,8 @@
<h1>Configuration</h1> <h1>Configuration</h1>
<livewire:project.shared.configuration-checker :resource="$application" /> <livewire:project.shared.configuration-checker :resource="$application" />
<livewire:project.application.heading :application="$application" /> <livewire:project.application.heading :application="$application" />
<div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : 'general' }" class="flex h-full pt-6"> <div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : 'general' }" class="flex flex-col h-full gap-8 pt-6 sm:flex-row">
<div class="flex flex-col gap-2 xl:w-48"> <div class="flex flex-col items-start gap-2 min-w-fit">
<a class="menu-item" :class="activeTab === 'general' && 'menu-item-active'" <a class="menu-item" :class="activeTab === 'general' && 'menu-item-active'"
@click.prevent="activeTab = 'general'; window.location.hash = 'general'" href="#">General</a> @click.prevent="activeTab = 'general'; window.location.hash = 'general'" href="#">General</a>
@if ($application->destination->server->isSwarm()) @if ($application->destination->server->isSwarm())
@@ -78,7 +78,7 @@
@click.prevent="activeTab = 'danger'; window.location.hash = 'danger'" href="#">Danger Zone @click.prevent="activeTab = 'danger'; window.location.hash = 'danger'" href="#">Danger Zone
</a> </a>
</div> </div>
<div class="w-full pl-8"> <div class="w-full">
<div x-cloak x-show="activeTab === 'general'" class="h-full"> <div x-cloak x-show="activeTab === 'general'" class="h-full">
<livewire:project.application.general :application="$application" /> <livewire:project.application.general :application="$application" />
</div> </div>

View File

@@ -116,7 +116,7 @@
@if ($application->build_pack !== 'dockerimage') @if ($application->build_pack !== 'dockerimage')
<h3 class="pt-8">Build</h3> <h3 class="pt-8">Build</h3>
@if ($application->build_pack !== 'dockercompose') @if ($application->build_pack !== 'dockercompose')
<div class="w-96"> <div class="max-w-96">
<x-forms.checkbox <x-forms.checkbox
helper="Use a build server to build your application. You can configure your build server in the Server settings. This is experimental. For more info, check the <a href='https://coolify.io/docs/knowledge-base/server/build-server' class='underline' target='_blank'>documentation</a>." helper="Use a build server to build your application. You can configure your build server in the Server settings. This is experimental. For more info, check the <a href='https://coolify.io/docs/knowledge-base/server/build-server' class='underline' target='_blank'>documentation</a>."
instantSave id="application.settings.is_build_server_enabled" instantSave id="application.settings.is_build_server_enabled"
@@ -242,7 +242,10 @@
@endif @endif
</div> </div>
<x-forms.textarea label="Container Labels" rows="15" id="customLabels"></x-forms.textarea> <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> <x-modal-confirmation buttonFullWidth action="resetDefaultLabels" buttonTitle="Reset to Coolify Generated Labels">
Are you sure you want to reset the labels to Coolify generated labels? <br>It could break the proxy configuration after you restart the container.
</x-modal-confirmation>
@endif @endif
<h3 class="pt-8">Pre/Post Deployment Commands</h3> <h3 class="pt-8">Pre/Post Deployment Commands</h3>

View File

@@ -1,30 +1,33 @@
<nav wire:poll.5000ms="check_status"> <nav wire:poll.5000ms="check_status">
<x-resources.breadcrumbs :resource="$application" :parameters="$parameters" /> <x-resources.breadcrumbs :resource="$application" :parameters="$parameters" />
<div class="navbar-main"> <div class="navbar-main">
<a href="{{ route('project.application.configuration', $parameters) }}"> <nav class="flex items-center flex-shrink-0 gap-6 scrollbar min-h-10 whitespace-nowrap">
Configuration <a href="{{ route('project.application.configuration', $parameters) }}">
</a> Configuration
<a href="{{ route('project.application.deployment.index', $parameters) }}">
<button>Deployments</button>
</a>
<a href="{{ route('project.application.logs', $parameters) }}">
<button>Logs</button>
</a>
@if (!$application->destination->server->isSwarm())
<a href="{{ route('project.application.command', $parameters) }}">
<button>Command</button>
</a> </a>
@endif <a href="{{ route('project.application.deployment.index', $parameters) }}">
<x-applications.links :application="$application" /> <button>Deployments</button>
<div class="flex-1"></div> </a>
<div class="flex items-center gap-2"> <a href="{{ route('project.application.logs', $parameters) }}">
<button>Logs</button>
</a>
@if (!$application->destination->server->isSwarm())
<a href="{{ route('project.application.command', $parameters) }}">
<button>Command</button>
</a>
@endif
<x-applications.links :application="$application" />
</nav>
<div class="flex flex-wrap items-center gap-2">
@if ($application->build_pack === 'dockercompose' && is_null($application->docker_compose_raw)) @if ($application->build_pack === 'dockercompose' && is_null($application->docker_compose_raw))
<div>Please load a Compose file.</div> <div>Please load a Compose file.</div>
@else @else
@if (!$application->destination->server->isSwarm()) @if (!$application->destination->server->isSwarm())
<div>
<x-applications.advanced :application="$application" /> <x-applications.advanced :application="$application" />
</div>
@endif @endif
<div class="flex gap-2"> <div class="flex flex-wrap gap-2">
@if (!str($application->status)->startsWith('exited')) @if (!str($application->status)->startsWith('exited'))
@if (!$application->destination->server->isSwarm()) @if (!$application->destination->server->isSwarm())
<x-forms.button title="With rolling update if possible" wire:click='deploy'> <x-forms.button title="With rolling update if possible" wire:click='deploy'>
@@ -100,6 +103,7 @@
</div> </div>
@endif @endif
</div> </div>
</div> </div>
@script @script
<script> <script>

View File

@@ -2,53 +2,53 @@
<h1>Configuration</h1> <h1>Configuration</h1>
<livewire:project.shared.configuration-checker :resource="$database" /> <livewire:project.shared.configuration-checker :resource="$database" />
<livewire:project.database.heading :database="$database" /> <livewire:project.database.heading :database="$database" />
<div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : 'general' }" class="flex h-full pt-6"> <div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : 'general' }" class="flex flex-col h-full gap-8 pt-6 sm:flex-row">
<div class="flex flex-col gap-4 min-w-fit"> <div class="flex flex-col items-start gap-2 min-w-fit">
<a :class="activeTab === 'general' && 'dark:text-white'" <a class="menu-item" :class="activeTab === 'general' && 'menu-item-active'"
@click.prevent="activeTab = 'general'; @click.prevent="activeTab = 'general';
window.location.hash = 'general'" window.location.hash = 'general'"
href="#">General</a> href="#">General</a>
<a :class="activeTab === 'environment-variables' && 'dark:text-white'" <a class="menu-item" :class="activeTab === 'environment-variables' && 'menu-item-active'"
@click.prevent="activeTab = 'environment-variables'; window.location.hash = 'environment-variables'" @click.prevent="activeTab = 'environment-variables'; window.location.hash = 'environment-variables'"
href="#">Environment href="#">Environment
Variables</a> Variables</a>
<a :class="activeTab === 'servers' && 'dark:text-white'" <a class="menu-item" :class="activeTab === 'servers' && 'menu-item-active'"
@click.prevent="activeTab = 'servers'; @click.prevent="activeTab = 'servers';
window.location.hash = 'servers'" window.location.hash = 'servers'"
href="#">Servers href="#">Servers
</a> </a>
<a :class="activeTab === 'storages' && 'dark:text-white'" <a class="menu-item" :class="activeTab === 'storages' && 'menu-item-active'"
@click.prevent="activeTab = 'storages'; @click.prevent="activeTab = 'storages';
window.location.hash = 'storages'" window.location.hash = 'storages'"
href="#">Storages href="#">Storages
</a> </a>
<a :class="activeTab === 'import' && 'dark:text-white'" <a class="menu-item" :class="activeTab === 'import' && 'menu-item-active'"
@click.prevent="activeTab = 'import'; @click.prevent="activeTab = 'import';
window.location.hash = 'import'" href="#">Import window.location.hash = 'import'" href="#">Import
Backup Backup
</a> </a>
<a :class="activeTab === 'webhooks' && 'dark:text-white'" <a class="menu-item" :class="activeTab === 'webhooks' && 'menu-item-active'"
@click.prevent="activeTab = 'webhooks'; window.location.hash = 'webhooks'" href="#">Webhooks @click.prevent="activeTab = 'webhooks'; window.location.hash = 'webhooks'" href="#">Webhooks
</a> </a>
<a :class="activeTab === 'resource-limits' && 'dark:text-white'" <a class="menu-item" :class="activeTab === 'resource-limits' && 'menu-item-active'"
@click.prevent="activeTab = 'resource-limits'; @click.prevent="activeTab = 'resource-limits';
window.location.hash = 'resource-limits'" window.location.hash = 'resource-limits'"
href="#">Resource Limits href="#">Resource Limits
</a> </a>
<a :class="activeTab === 'resource-operations' && 'dark:text-white'" <a class="menu-item" :class="activeTab === 'resource-operations' && 'menu-item-active'"
@click.prevent="activeTab = 'resource-operations'; window.location.hash = 'resource-operations'" @click.prevent="activeTab = 'resource-operations'; window.location.hash = 'resource-operations'"
href="#">Resource Operations href="#">Resource Operations
</a> </a>
<a :class="activeTab === 'tags' && 'dark:text-white'" <a class="menu-item" :class="activeTab === 'tags' && 'menu-item-active'"
@click.prevent="activeTab = 'tags'; window.location.hash = 'tags'" href="#">Tags @click.prevent="activeTab = 'tags'; window.location.hash = 'tags'" href="#">Tags
</a> </a>
<a :class="activeTab === 'danger' && 'dark:text-white'" <a class="menu-item" :class="activeTab === 'danger' && 'menu-item-active'"
@click.prevent="activeTab = 'danger'; @click.prevent="activeTab = 'danger';
window.location.hash = 'danger'" window.location.hash = 'danger'"
href="#">Danger Zone href="#">Danger Zone
</a> </a>
</div> </div>
<div class="w-full pl-8"> <div class="w-full">
<div x-cloak x-show="activeTab === 'general'" class="h-full"> <div x-cloak x-show="activeTab === 'general'" class="h-full">
@if ($database->type() === 'standalone-postgresql') @if ($database->type() === 'standalone-postgresql')
<livewire:project.database.postgresql.general :database="$database" /> <livewire:project.database.postgresql.general :database="$database" />

View File

@@ -7,67 +7,70 @@
</x-slot:content> </x-slot:content>
</x-slide-over> </x-slide-over>
<div class="navbar-main"> <div class="navbar-main">
<a class="{{ request()->routeIs('project.database.configuration') ? 'dark:text-white' : '' }}" <nav class="flex items-center flex-shrink-0 gap-6 overflow-x-scroll sm:overflow-x-hidden scrollbar min-h-10 whitespace-nowrap">
href="{{ route('project.database.configuration', $parameters) }}"> <a class="{{ request()->routeIs('project.database.configuration') ? 'dark:text-white' : '' }}"
<button>Configuration</button> href="{{ route('project.database.configuration', $parameters) }}">
</a> <button>Configuration</button>
<a class="{{ request()->routeIs('project.database.command') ? 'dark:text-white' : '' }}"
href="{{ route('project.database.command', $parameters) }}">
<button>Execute Command</button>
</a>
<a class="{{ request()->routeIs('project.database.logs') ? 'dark:text-white' : '' }}"
href="{{ route('project.database.logs', $parameters) }}">
<button>Logs</button>
</a>
@if (
$database->getMorphClass() === 'App\Models\StandalonePostgresql' ||
$database->getMorphClass() === 'App\Models\StandaloneMongodb' ||
$database->getMorphClass() === 'App\Models\StandaloneMysql' ||
$database->getMorphClass() === 'App\Models\StandaloneMariadb')
<a class="{{ request()->routeIs('project.database.backup.index') ? 'dark:text-white' : '' }}"
href="{{ route('project.database.backup.index', $parameters) }}">
<button>Backups</button>
</a> </a>
@endif <a class="{{ request()->routeIs('project.database.command') ? 'dark:text-white' : '' }}"
<div class="flex-1"></div> href="{{ route('project.database.command', $parameters) }}">
@if (!str($database->status)->startsWith('exited')) <button>Execute Command</button>
<x-modal-confirmation @click="$wire.dispatch('stopEvent')"> </a>
<x-slot:button-title> <a class="{{ request()->routeIs('project.database.logs') ? 'dark:text-white' : '' }}"
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24" href="{{ route('project.database.logs', $parameters) }}">
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" <button>Logs</button>
</a>
@if (
$database->getMorphClass() === 'App\Models\StandalonePostgresql' ||
$database->getMorphClass() === 'App\Models\StandaloneMongodb' ||
$database->getMorphClass() === 'App\Models\StandaloneMysql' ||
$database->getMorphClass() === 'App\Models\StandaloneMariadb')
<a class="{{ request()->routeIs('project.database.backup.index') ? 'dark:text-white' : '' }}"
href="{{ route('project.database.backup.index', $parameters) }}">
<button>Backups</button>
</a>
@endif
</nav>
<div class="flex flex-wrap items-center gap-2">
@if (!str($database->status)->startsWith('exited'))
<x-modal-confirmation @click="$wire.dispatch('stopEvent')">
<x-slot:button-title>
<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
</x-slot:button-title>
This database will be stopped. <br>Please think again.
</x-modal-confirmation>
@else
<button @click="$wire.dispatch('startEvent')" class="gap-2 button">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
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> <path stroke="none" d="M0 0h24v24H0z" fill="none" />
<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 d="M7 4v16l13 -8z" />
</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> </svg>
Stop Start
</x-slot:button-title> </button>
This database will be stopped. <br>Please think again. @endif
</x-modal-confirmation> @script
@else <script>
<button @click="$wire.dispatch('startEvent')" class="gap-2 button"> $wire.$on('startEvent', () => {
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24" window.dispatchEvent(new CustomEvent('startdatabase'));
stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" $wire.$call('start');
stroke-linejoin="round"> });
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> $wire.$on('stopEvent', () => {
<path d="M7 4v16l13 -8z" /> $wire.$dispatch('info', 'Stopping database.');
</svg> $wire.$call('stop');
Start });
</button> </script>
@endif @endscript
@script </div>
<script>
$wire.$on('startEvent', () => {
window.dispatchEvent(new CustomEvent('startdatabase'));
$wire.$call('start');
});
$wire.$on('stopEvent', () => {
$wire.$dispatch('info', 'Stopping database.');
$wire.$call('stop');
});
</script>
@endscript
</div> </div>
</nav> </nav>

View File

@@ -20,7 +20,7 @@
Save Save
</x-forms.button> </x-forms.button>
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2 flex-wrap sm:flex-nowrap">
<x-forms.input label="Name" id="database.name" /> <x-forms.input label="Name" id="database.name" />
<x-forms.input label="Description" id="database.description" /> <x-forms.input label="Description" id="database.description" />
<x-forms.input label="Image" id="database.image" required <x-forms.input label="Image" id="database.image" required
@@ -28,7 +28,7 @@
</div> </div>
@if ($database->started_at) @if ($database->started_at)
<div class="flex gap-2"> <div class="flex gap-2 flex-wrap sm:flex-nowrap">
<x-forms.input label="Initial Username" id="database.postgres_user" placeholder="If empty: postgres" <x-forms.input label="Initial Username" id="database.postgres_user" placeholder="If empty: postgres"
readonly helper="You can only change this in the database." /> readonly helper="You can only change this in the database." />
<x-forms.input label="Initial Password" id="database.postgres_password" type="password" required <x-forms.input label="Initial Password" id="database.postgres_password" type="password" required

View File

@@ -6,22 +6,30 @@
<livewire:project.delete-environment :disabled="!$environment->isEmpty()" :environment_id="$environment->id" /> <livewire:project.delete-environment :disabled="!$environment->isEmpty()" :environment_id="$environment->id" />
</div> </div>
<nav class="flex pt-2 pb-10"> <nav class="flex pt-2 pb-10">
<ol class="flex items-center"> <ol class="flex items-center flex-wrap gap-y-1">
<li class="inline-flex items-center"> <li class="inline-flex items-center">
<a class="text-xs truncate lg:text-sm"
href="{{ route('project.show', ['project_uuid' => data_get($parameters, 'project_uuid')]) }}">
{{ $project->name }}</a>
</li>
<li>
<div class="flex items-center"> <div class="flex items-center">
<a class="text-xs truncate lg:text-sm"
href="{{ route('project.show', ['project_uuid' => data_get($parameters, 'project_uuid')]) }}">
{{ $project->name }}</a>
<svg aria-hidden="true" class="w-4 h-4 mx-1 font-bold dark:text-warning" fill="currentColor" <svg aria-hidden="true" class="w-4 h-4 mx-1 font-bold dark:text-warning" fill="currentColor"
viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"> viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" <path fill-rule="evenodd"
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
clip-rule="evenodd"></path> clip-rule="evenodd"></path>
</svg> </svg>
</div>
</li>
<li>
<div class="flex items-center">
<a class="text-xs truncate lg:text-sm" <a class="text-xs truncate lg:text-sm"
href="{{ route('project.resource.index', ['environment_name' => data_get($parameters, 'environment_name'), 'project_uuid' => data_get($parameters, 'project_uuid')]) }}">{{ data_get($parameters, 'environment_name') }}</a> href="{{ route('project.resource.index', ['environment_name' => data_get($parameters, 'environment_name'), 'project_uuid' => data_get($parameters, 'project_uuid')]) }}">{{ data_get($parameters, 'environment_name') }}</a>
<svg aria-hidden="true" class="w-4 h-4 mx-1 font-bold dark:text-warning" fill="currentColor"
viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
clip-rule="evenodd"></path>
</svg>
</div> </div>
</li> </li>
<li> <li>

View File

@@ -8,15 +8,14 @@
<div class="subtitle">All your projects are here.</div> <div class="subtitle">All your projects are here.</div>
<div class="grid gap-2 lg:grid-cols-2"> <div class="grid gap-2 lg:grid-cols-2">
@forelse ($projects as $project) @forelse ($projects as $project)
<div class="box group" x-data <div class="box group" x-data x-on:click="goto('{{ $project->uuid }}')">
x-on:click="goto('{{ $project->uuid }}')">
<a class="flex flex-col justify-center flex-1 mx-6" <a class="flex flex-col justify-center flex-1 mx-6"
href="{{ route('project.show', ['project_uuid' => data_get($project, 'uuid')]) }}"> href="{{ route('project.show', ['project_uuid' => data_get($project, 'uuid')]) }}">
<div class="box-title">{{ $project->name }}</div> <div class="box-title">{{ $project->name }}</div>
<div class="box-description "> <div class="box-description ">
{{ $project->description }}</div> {{ $project->description }}</div>
</a> </a>
<div class="flex items-center text-xs"> <div class="flex items-center justify-center gap-2 pt-4 pb-2 mr-4 text-xs lg:py-0 lg:justify-normal">
<a class="mx-4 font-bold hover:underline" <a class="mx-4 font-bold hover:underline"
href="{{ route('project.edit', ['project_uuid' => data_get($project, 'uuid')]) }}"> href="{{ route('project.edit', ['project_uuid' => data_get($project, 'uuid')]) }}">
Settings Settings

View File

@@ -33,14 +33,16 @@
</div> </div>
</div> </div>
</div> </div>
<div class="flex flex-col items-center justify-center">
<x-loading wire:loading wire:target="loadRepositories({{ $ghapp->id }})" />
</div>
@endforeach @endforeach
</div> </div>
@endif @endif
@if ($current_step === 'repository') @if ($current_step === 'repository')
@if ($repositories->count() > 0) @if ($repositories->count() > 0)
<div class="flex items-end gap-2"> <div class="flex items-end gap-2">
<x-forms.select class="w-full" label="Repository" <x-forms.select class="w-full" label="Repository" wire:model="selected_repository_id">
wire:model="selected_repository_id">
@foreach ($repositories as $repo) @foreach ($repositories as $repo)
@if ($loop->first) @if ($loop->first)
<option selected value="{{ data_get($repo, 'id') }}"> <option selected value="{{ data_get($repo, 'id') }}">

View File

@@ -71,7 +71,8 @@
<div class="max-w-full px-4 truncate box-description" x-text="item.fqdn"></div> <div class="max-w-full px-4 truncate box-description" x-text="item.fqdn"></div>
</div> </div>
</a> </a>
<div class="flex flex-wrap gap-1 pt-1 group-hover:dark:text-white group-hover:text-black group min-h-6"> <div
class="flex flex-wrap gap-1 pt-1 group-hover:dark:text-white group-hover:text-black group min-h-6">
<template x-for="tag in item.tags"> <template x-for="tag in item.tags">
<div class="tag" @click.prevent="gotoTag(tag.name)" x-text="tag.name"></div> <div class="tag" @click.prevent="gotoTag(tag.name)" x-text="tag.name"></div>
</template> </template>

View File

@@ -1,45 +1,46 @@
<div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : 'service-stack' }" x-init="$wire.check_status" wire:poll.5000ms="check_status"> <div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : 'service-stack' }" x-init="$wire.check_status" wire:poll.5000ms="check_status">
<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 flex-col h-full gap-8 pt-6 sm:flex-row">
<div class="flex flex-col items-start gap-4 min-w-fit"> <div class="flex flex-col items-start gap-2 min-w-fit">
<a target="_blank" href="{{ $service->documentation() }}">Documentation <x-external-link /></a> <a class="menu-item sm:min-w-fit" target="_blank" href="{{ $service->documentation() }}">Documentation
<a :class="activeTab === 'service-stack' && 'dark:text-white'" <x-external-link /></a>
<a class="menu-item sm:min-w-fit" :class="activeTab === 'service-stack' && 'menu-item-active'"
@click.prevent="activeTab = 'service-stack'; @click.prevent="activeTab = 'service-stack';
window.location.hash = 'service-stack'" window.location.hash = 'service-stack'"
href="#">Service Stack</a> href="#">Service Stack</a>
<a :class="activeTab === 'environment-variables' && 'dark:text-white'" <a class="menu-item sm:min-w-fit" :class="activeTab === 'environment-variables' && 'menu-item-active'"
@click.prevent="activeTab = 'environment-variables'; window.location.hash = 'environment-variables'" @click.prevent="activeTab = 'environment-variables'; window.location.hash = 'environment-variables'"
href="#">Environment href="#">Environment
Variables</a> Variables</a>
<a :class="activeTab === 'storages' && 'dark:text-white'" <a class="menu-item sm:min-w-fit" :class="activeTab === 'storages' && 'menu-item-active'"
@click.prevent="activeTab = 'storages'; @click.prevent="activeTab = 'storages';
window.location.hash = 'storages'" window.location.hash = 'storages'"
href="#">Storages</a> href="#">Storages</a>
<a :class="activeTab === 'execute-command' && 'dark:text-white'" <a class="menu-item sm:min-w-fit" :class="activeTab === 'execute-command' && 'menu-item-active'"
@click.prevent="activeTab = 'execute-command'; @click.prevent="activeTab = 'execute-command';
window.location.hash = 'execute-command'" window.location.hash = 'execute-command'"
href="#">Execute Command</a> href="#">Execute Command</a>
<a :class="activeTab === 'logs' && 'dark:text-white'" <a class="menu-item sm:min-w-fit" :class="activeTab === 'logs' && 'menu-item-active'"
@click.prevent="activeTab = 'logs'; @click.prevent="activeTab = 'logs';
window.location.hash = 'logs'" window.location.hash = 'logs'"
href="#">Logs</a> href="#">Logs</a>
<a :class="activeTab === 'webhooks' && 'dark:text-white'" <a class="menu-item sm:min-w-fit" :class="activeTab === 'webhooks' && 'menu-item-active'"
@click.prevent="activeTab = 'webhooks'; window.location.hash = 'webhooks'" href="#">Webhooks @click.prevent="activeTab = 'webhooks'; window.location.hash = 'webhooks'" href="#">Webhooks
</a> </a>
<a :class="activeTab === 'resource-operations' && 'dark:text-white'" <a class="menu-item sm:min-w-fit" :class="activeTab === 'resource-operations' && 'menu-item-active'"
@click.prevent="activeTab = 'resource-operations'; window.location.hash = 'resource-operations'" @click.prevent="activeTab = 'resource-operations'; window.location.hash = 'resource-operations'"
href="#">Resource Operations href="#">Resource Operations
</a> </a>
<a :class="activeTab === 'tags' && 'dark:text-white'" <a class="menu-item sm:min-w-fit" :class="activeTab === 'tags' && 'menu-item-active'"
@click.prevent="activeTab = 'tags'; window.location.hash = 'tags'" href="#">Tags @click.prevent="activeTab = 'tags'; window.location.hash = 'tags'" href="#">Tags
</a> </a>
<a :class="activeTab === 'danger' && 'dark:text-white'" <a class="menu-item sm:min-w-fit" :class="activeTab === 'danger' && 'menu-item-active'"
@click.prevent="activeTab = 'danger'; @click.prevent="activeTab = 'danger';
window.location.hash = 'danger'" window.location.hash = 'danger'"
href="#">Danger Zone href="#">Danger Zone
</a> </a>
</div> </div>
<div class="w-full pl-8"> <div class="w-full">
<div x-cloak x-show="activeTab === 'service-stack'"> <div x-cloak x-show="activeTab === 'service-stack'">
<livewire:project.service.stack-form :service="$service" /> <livewire:project.service.stack-form :service="$service" />
<h3>Services</h3> <h3>Services</h3>

View File

@@ -1,30 +1,31 @@
<div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : 'general' }"> <div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : 'general' }">
<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 flex-col h-full gap-8 pt-6 sm:flex-row">
<div class="flex flex-col gap-4 min-w-fit"> <div class="flex flex-col items-start gap-2 min-w-fit">
<a class="{{ request()->routeIs('project.service.configuration') ? 'dark:text-white' : '' }}" <a class="menu-item"
class="{{ request()->routeIs('project.service.configuration') ? 'menu-item-active' : '' }}"
href="{{ route('project.service.configuration', [...$parameters, 'stack_service_uuid' => null]) }}"> href="{{ route('project.service.configuration', [...$parameters, 'stack_service_uuid' => null]) }}">
<button><- Back</button> <button><- Back</button>
</a> </a>
<a :class="activeTab === 'general' && 'dark:text-white'" <a class="menu-item" :class="activeTab === 'general' && 'menu-item-active'"
@click.prevent="activeTab = 'general'; window.location.hash = 'general'; if(window.location.search) window.location.search = ''" @click.prevent="activeTab = 'general'; window.location.hash = 'general'; if(window.location.search) window.location.search = ''"
href="#">General</a> href="#">General</a>
<a :class="activeTab === 'storages' && 'dark:text-white'" <a class="menu-item" :class="activeTab === 'storages' && 'menu-item-active'"
@click.prevent="activeTab = 'storages'; window.location.hash = 'storages'; if(window.location.search) window.location.search = ''" @click.prevent="activeTab = 'storages'; window.location.hash = 'storages'; if(window.location.search) window.location.search = ''"
href="#">Storages href="#">Storages
</a> </a>
<a :class="activeTab === 'scheduled-tasks' && 'dark:text-white'" <a class="menu-item" :class="activeTab === 'scheduled-tasks' && 'menu-item-active'"
@click.prevent="activeTab = 'scheduled-tasks'; window.location.hash = 'scheduled-tasks'" @click.prevent="activeTab = 'scheduled-tasks'; window.location.hash = 'scheduled-tasks'"
href="#">Scheduled Tasks href="#">Scheduled Tasks
</a> </a>
@if (str($serviceDatabase?->databaseType())->contains('mysql') || @if (str($serviceDatabase?->databaseType())->contains('mysql') ||
str($serviceDatabase?->databaseType())->contains('postgres') || str($serviceDatabase?->databaseType())->contains('postgres') ||
str($serviceDatabase?->databaseType())->contains('mariadb')) str($serviceDatabase?->databaseType())->contains('mariadb'))
<a :class="activeTab === 'backups' && 'dark:text-white'" <a :class="activeTab === 'backups' && 'menu-item-active'" class="menu-item"
@click.prevent="activeTab = 'backups'; window.location.hash = 'backups'" href="#">Backups</a> @click.prevent="activeTab = 'backups'; window.location.hash = 'backups'" href="#">Backups</a>
@endif @endif
</div> </div>
<div class="w-full pl-8"> <div class="w-full">
@isset($serviceApplication) @isset($serviceApplication)
<div x-cloak x-show="activeTab === 'general'" class="h-full"> <div x-cloak x-show="activeTab === 'general'" class="h-full">
<livewire:project.service.service-application-view :application="$serviceApplication" /> <livewire:project.service.service-application-view :application="$serviceApplication" />

View File

@@ -9,13 +9,14 @@
<h1>Configuration</h1> <h1>Configuration</h1>
<x-resources.breadcrumbs :resource="$service" :parameters="$parameters" /> <x-resources.breadcrumbs :resource="$service" :parameters="$parameters" />
<div class="navbar-main" x-data> <div class="navbar-main" x-data>
<a class="{{ request()->routeIs('project.service.configuration') ? 'dark:text-white' : '' }}" <nav class="flex items-center flex-shrink-0 gap-6 scrollbar min-h-10 whitespace-nowrap">
href="{{ route('project.service.configuration', $parameters) }}"> <a class="{{ request()->routeIs('project.service.configuration') ? 'dark:text-white' : '' }}"
<button>Configuration</button> href="{{ route('project.service.configuration', $parameters) }}">
</a> <button>Configuration</button>
<x-services.links :service="$service" /> </a>
<div class="flex-1"></div> <x-services.links :service="$service" />
<div class="flex gap-2"> </nav>
<div class="flex flex-wrap items-center order-first gap-2 sm:order-last">
@if (str($service->status())->contains('running')) @if (str($service->status())->contains('running'))
<button @click="$wire.dispatch('restartEvent')" class="gap-2 button"> <button @click="$wire.dispatch('restartEvent')" class="gap-2 button">
<svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">

View File

@@ -54,4 +54,78 @@
@endforelse @endforelse
</div> </div>
@endif @endif
{{-- <section x-data="apex_app" class="container p-5 mx-auto my-20 bg-white drop-shadow-xl rounded-xl">
<div class="w-full" x-ref="chart"></div>
</section>
<script src="https://cdn.jsdelivr.net/npm/apexcharts"></script>
<script>
document.addEventListener("alpine:init", () => {
Alpine.data("apex_app", () => ({
data: @js($cpu),
init() {
let chart = new ApexCharts(this.$refs.chart, this.options);
chart.render();
this.$watch("data", () => {
chart.updateOptions(this.options);
});
},
get options() {
return {
colors: [function({
value,
seriesIndex,
w
}) {
if (value < 55) {
return '#7E36AF'
} else {
return '#D9534F'
}
}, function({
value,
seriesIndex,
w
}) {
if (value < 111) {
return '#7E36AF'
} else {
return '#D9534F'
}
}],
xaxis: {
type: 'datetime'
},
dataLabels: {
enabled: false
},
series: [{
name: "Series name",
data: this.data
}],
tooltip: {
enabled: true
},
chart: {
stroke: {
curve: 'smooth',
},
height: 500,
width: "100%",
type: "line",
toolbar: {
show: true
},
animations: {
initialAnimation: {
enabled: false
}
}
},
};
}
}));
});
</script> --}}
</div> </div>

View File

@@ -6,7 +6,7 @@
<div class="pb-4"> <div class="pb-4">
<div class="flex flex-col flex-wrap gap-2"> <div class="flex flex-col flex-wrap gap-2">
@foreach ($servers->sortBy('id') as $server) @foreach ($servers->sortBy('id') as $server)
<h5>Server: <span class="font-bold text-white">{{ $server->name }}</span></h5> <h5>Server: <span class="font-bold text-dark dark:text-white">{{ $server->name }}</span></h5>
@foreach ($server->destinations() as $destination) @foreach ($server->destinations() as $destination)
<x-modal-confirmation action="cloneTo({{ data_get($destination, 'id') }})"> <x-modal-confirmation action="cloneTo({{ data_get($destination, 'id') }})">
<x:slot name="content"> <x:slot name="content">
@@ -33,7 +33,7 @@
</div> </div>
<div class="flex flex-col flex-wrap gap-2"> <div class="flex flex-col flex-wrap gap-2">
@forelse ($projects as $project) @forelse ($projects as $project)
<h5>Project: <span class="font-bold text-white">{{ $project->name }}</span></h5> <h5>Project: <span class="font-bold text-dark dark:text-white">{{ $project->name }}</span></h5>
@foreach ($project->environments as $environment) @foreach ($project->environments as $environment)
<x-modal-confirmation action="moveTo({{ data_get($environment, 'id') }})"> <x-modal-confirmation action="moveTo({{ data_get($environment, 'id') }})">

View File

@@ -21,7 +21,7 @@
<form wire:submit='submit' class="flex items-end gap-2 pt-4"> <form wire:submit='submit' class="flex items-end gap-2 pt-4">
<div class="w-64"> <div class="w-64">
<x-forms.input label="Create new or assign existing tags" <x-forms.input label="Create new or assign existing tags"
helper="You add more at once with space seperated list: web api something<br><br>If the tag does not exists, it will be created." helper="You add more at once with space separated list: web api something<br><br>If the tag does not exists, it will be created."
wire:model="new_tag" /> wire:model="new_tag" />
</div> </div>
<x-forms.button type="submit">Add</x-forms.button> <x-forms.button type="submit">Add</x-forms.button>

View File

@@ -11,18 +11,26 @@
@forelse ($project->environments as $environment) @forelse ($project->environments as $environment)
<div class="gap-2 border border-transparent cursor-pointer box group" x-data <div class="gap-2 border border-transparent cursor-pointer box group" x-data
x-on:click="goto('{{ $project->uuid }}','{{ $environment->name }}')"> x-on:click="goto('{{ $project->uuid }}','{{ $environment->name }}')">
<a class="flex flex-col justify-center flex-1 mx-6 hover:no-underline" <div class="flex flex-1 mx-6">
href="{{ route('project.resource.index', [$project->uuid, $environment->name]) }}"> <a class="flex flex-col justify-center flex-1"
<div class="font-bold dark:text-white"> {{ $environment->name }}</div> href="{{ route('project.resource.index', [$project->uuid, $environment->name]) }}">
<div class="description"> <div class="font-bold dark:text-white"> {{ $environment->name }}</div>
{{ $environment->description }}</div> <div class="description">
</a> {{ $environment->description }}</div>
<div class="flex items-center text-xs"> </a>
<div class="flex items-center justify-center gap-2 text-xs">
<a class="font-bold hover:underline"
href="{{ route('project.environment.edit', ['project_uuid' => data_get($project, 'uuid'), 'environment_name' => $environment->name]) }}">
Settings
</a>
</div>
</div>
{{-- <div class="flex items-center justify-center gap-2 pt-4 pb-2 mr-4 text-xs lg:py-0 lg:justify-normal">
<a class="mx-4 font-bold hover:underline" <a class="mx-4 font-bold hover:underline"
href="{{ route('project.environment.edit', ['project_uuid' => data_get($project, 'uuid'), 'environment_name' => $environment->name]) }}"> href="{{ route('project.environment.edit', ['project_uuid' => data_get($project, 'uuid'), 'environment_name' => $environment->name]) }}">
Settings Settings
</a> </a>
</div> </div> --}}
</div> </div>
@empty @empty
<p>No environments found.</p> <p>No environments found.</p>

View File

@@ -132,7 +132,7 @@
@if ($server->isFunctional()) @if ($server->isFunctional())
<h3 class="py-4">Settings</h3> <h3 class="py-4">Settings</h3>
<div class="flex gap-2"> <div class="flex gap-2 flex-wrap sm:flex-nowrap">
<x-forms.input id="cleanup_after_percentage" label="Disk cleanup threshold (%)" required <x-forms.input id="cleanup_after_percentage" label="Disk cleanup threshold (%)" required
helper="The disk cleanup task will run when the disk usage exceeds this threshold." /> helper="The disk cleanup task will run when the disk usage exceeds this threshold." />
<x-forms.input id="server.settings.concurrent_builds" label="Number of concurrent builds" required <x-forms.input id="server.settings.concurrent_builds" label="Number of concurrent builds" required

View File

@@ -3,11 +3,11 @@
<x-limit-reached name="servers" /> <x-limit-reached name="servers" />
@else @else
<form class="flex flex-col w-full gap-2" wire:submit='submit'> <form class="flex flex-col w-full gap-2" wire:submit='submit'>
<div class="flex w-full gap-2"> <div class="flex w-full gap-2 flex-wrap sm:flex-nowrap">
<x-forms.input autofocus id="name" label="Name" required /> <x-forms.input autofocus id="name" label="Name" required />
<x-forms.input id="description" label="Description" /> <x-forms.input id="description" label="Description" />
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2 flex-wrap sm:flex-nowrap">
<x-forms.input id="ip" label="IP Address/Domain" required <x-forms.input id="ip" label="IP Address/Domain" required
helper="An IP Address (127.0.0.1) or domain (example.com)." /> helper="An IP Address (127.0.0.1) or domain (example.com)." />
<x-forms.input type="number" id="port" label="Port" required /> <x-forms.input type="number" id="port" label="Port" required />

View File

@@ -1,13 +1,13 @@
<div> <div>
<x-server.navbar :server="$server" :parameters="$parameters" /> <x-server.navbar :server="$server" :parameters="$parameters" />
<div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : 'managed' }" class="flex h-full"> <div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : 'managed' }" class="flex flex-col h-full gap-8 md:flex-row">
<div class="flex flex-col gap-4"> <div class="flex flex-row gap-4 md:flex-col">
<a :class="activeTab === 'managed' && 'dark:text-white'" <a :class="activeTab === 'managed' && 'dark:text-white'"
@click.prevent="activeTab = 'managed'; window.location.hash = 'managed'" href="#">Managed</a> @click.prevent="activeTab = 'managed'; window.location.hash = 'managed'" href="#">Managed</a>
<a :class="activeTab === 'unmanaged' && 'dark:text-white'" <a :class="activeTab === 'unmanaged' && 'dark:text-white'"
@click.prevent="activeTab = 'unmanaged'; window.location.hash = 'unmanaged'" href="#">Unmanaged</a> @click.prevent="activeTab = 'unmanaged'; window.location.hash = 'unmanaged'" href="#">Unmanaged</a>
</div> </div>
<div class="w-full pl-8"> <div class="w-full">
<div x-cloak x-show="activeTab === 'managed'" class="h-full"> <div x-cloak x-show="activeTab === 'managed'" class="h-full">
<div class="flex flex-col"> <div class="flex flex-col">
<div class="flex gap-2"> <div class="flex gap-2">

View File

@@ -9,12 +9,14 @@
<div>General configuration for your Coolify instance.</div> <div>General configuration for your Coolify instance.</div>
<div class="flex flex-col gap-2 pt-4"> <div class="flex flex-col gap-2 pt-4">
<div class="flex items-end gap-2"> <div class="flex items-end gap-2 flex-wrap">
<x-forms.input id="settings.fqdn" label="Instance's Domain" placeholder="https://coolify.io" /> <x-forms.input id="settings.fqdn" label="Instance's Domain" placeholder="https://coolify.io" />
<x-forms.input id="settings.custom_dns_servers" label="DNS Servers" <x-forms.input id="settings.custom_dns_servers" label="DNS Servers"
helper="DNS servers for validation FQDNS againts. A comma separated list of DNS servers." helper="DNS servers for validation FQDNs againts. A comma separated list of DNS servers."
placeholder="1.1.1.1,8.8.8.8" /> placeholder="1.1.1.1,8.8.8.8" />
<x-forms.checkbox instantSave id="is_dns_validation_enabled" label="Validate DNS settings?" /> <div class="md:w-96">
<x-forms.checkbox instantSave id="is_dns_validation_enabled" label="Validate DNS settings?" />
</div>
</div> </div>
{{-- <div class="flex gap-2 "> {{-- <div class="flex gap-2 ">
@@ -24,12 +26,12 @@
</div> </div>
</form> </form>
<h2 class="pt-6">Advanced</h2> <h2 class="pt-6">Advanced</h2>
<div class="text-right w-80"> <div class="text-right md:w-96">
@if (!is_null(env('AUTOUPDATE', null))) @if (!is_null(env('AUTOUPDATE', null)))
<x-forms.checkbox instantSave helper="AUTOUPDATE is set in .env file, you need to modify it there." disabled <x-forms.checkbox instantSave helper="AUTOUPDATE is set in .env file, you need to modify it there." disabled
id="is_auto_update_enabled" label="Auto Update Coolify" /> id="is_auto_update_enabled" label="Auto Update Coolify" />
@else @else
<x-forms.checkbox instantSave id="is_auto_update_enabled" label="Auto Update Coolify" /> <x-forms.checkbox instantSave id="is_auto_update_enabled" label="Auto Update Coolify" />
@endif @endif
<x-forms.checkbox instantSave id="is_registration_enabled" label="Registration Allowed" /> <x-forms.checkbox instantSave id="is_registration_enabled" label="Registration Allowed" />
<x-forms.checkbox instantSave id="do_not_track" label="Do Not Track" /> <x-forms.checkbox instantSave id="do_not_track" label="Do Not Track" />
@@ -38,7 +40,7 @@
id="next_channel" label="Enable pre-release (early) updates" /> id="next_channel" label="Enable pre-release (early) updates" />
@else @else
<x-forms.checkbox disabled instantSave <x-forms.checkbox disabled instantSave
helper="Currently disabled. Do not recommended, only if you like to live on the edge." id="next_channel" helper="Currently disabled. Not recommended. Only if you like to live on the edge." id="next_channel"
label="Enable pre-release (early) updates" /> label="Enable pre-release (early) updates" />
@endif @endif
</div> </div>

View File

@@ -1,18 +1,19 @@
<div> <div>
<x-settings.navbar /> <x-settings.navbar />
<div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : 'general' }" class="flex h-full pt-1"> <div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : 'general' }" class="flex flex-col h-full gap-8 pt-1 sm:flex-row">
<div class="flex flex-col gap-4 min-w-fit"> <div class="flex gap-6 overflow-x-scroll sm:gap-2 sm:overflow-x-hidden scrollbar sm:flex-col whitespace-nowrap">
<a :class="activeTab === 'general' && 'dark:text-white'" <a class="menu-item" :class="activeTab === 'general' && 'menu-item-active'"
@click.prevent="activeTab = 'general'; window.location.hash = 'general'" href="#">General</a> @click.prevent="activeTab = 'general'; window.location.hash = 'general'" href="#">General</a>
<a :class="activeTab === 'backup' && 'dark:text-white'" <a class="menu-item" :class="activeTab === 'backup' && 'menu-item-active'"
@click.prevent="activeTab = 'backup'; window.location.hash = 'backup'" href="#">Instance Backup</a> @click.prevent="activeTab = 'backup'; window.location.hash = 'backup'" href="#">Instance Backup</a>
<a :class="activeTab === 'smtp' && 'dark:text-white'" <a class="menu-item" :class="activeTab === 'smtp' && 'menu-item-active'"
@click.prevent="activeTab = 'smtp'; window.location.hash = 'smtp'" href="#">Transactional @click.prevent="activeTab = 'smtp'; window.location.hash = 'smtp'" href="#">Transactional
Email</a> Email</a>
<a :class="activeTab === 'auth' && 'dark:text-white'" <a class="menu-item" :class="activeTab === 'auth' && 'menu-item-active'"
@click.prevent="activeTab = 'auth'; window.location.hash = 'auth'" href="#">Authentication (OAuth)</a> @click.prevent="activeTab = 'auth'; window.location.hash = 'auth'" href="#">Authentication
(OAuth)</a>
</div> </div>
<div class="w-full pl-8"> <div class="w-full">
<div x-cloak x-show="activeTab === 'general'" class="h-full"> <div x-cloak x-show="activeTab === 'general'" class="h-full">
<livewire:settings.configuration :settings="$settings" /> <livewire:settings.configuration :settings="$settings" />
</div> </div>

View File

@@ -1,25 +1,24 @@
<div wire:poll.1000ms="get_deployments" class="grid grid-cols-1"> <div wire:poll.2000ms="get_deployments" wire:init='get_deployments'>
@forelse ($deployments_per_tag_per_server as $server_name => $deployments) @forelse ($deployments_per_tag_per_server as $server_name => $deployments)
<h4 class="py-4">{{ $server_name }}</h4> <h4 class="py-4">{{ $server_name }}</h4>
<div class="grid grid-cols-1 gap-2 lg:grid-cols-3"> <div class="grid grid-cols-1 gap-2">
@foreach ($deployments as $deployment) @foreach ($deployments as $deployment)
<div @class([ <a href="{{ data_get($deployment, 'deployment_url') }}" @class([
'box-without-bg dark:bg-coolgray-100 bg-white gap-2 cursor-pointer group border-l-2 border-dotted', 'box-without-bg-without-border dark:bg-coolgray-100 bg-white gap-2 cursor-pointer group border-l-2',
'dark:border-coolgray-300' => data_get($deployment, 'status') === 'queued', 'dark:border-coolgray-300' => data_get($deployment, 'status') === 'queued',
'border-yellow-500' => data_get($deployment, 'status') === 'in_progress', 'dark:border-yellow-500' =>
data_get($deployment, 'status') === 'in_progress',
])> ])>
<a href="{{ data_get($deployment, 'deployment_url') }}"> <div class="flex flex-col mx-6">
<div class="flex flex-col mx-6"> <div class="box-title">
<div class="box-title"> {{ data_get($deployment, 'application_name') }}
{{ data_get($deployment, 'application_name') }}
</div>
<div class="box-description">
{{ str(data_get($deployment, 'status'))->headline() }}
</div>
</div> </div>
<div class="flex-1"></div> <div class="box-description">
</a> {{ str(data_get($deployment, 'status'))->headline() }}
</div> </div>
</div>
<div class="flex-1"></div>
</a>
@endforeach @endforeach
</div> </div>
@empty @empty

View File

@@ -2,7 +2,7 @@
<h1>Tags</h1> <h1>Tags</h1>
<div class="flex flex-col pb-6 "> <div class="flex flex-col pb-6 ">
<div class="subtitle">Tags help you to perform actions on multiple resources.</div> <div class="subtitle">Tags help you to perform actions on multiple resources.</div>
<div class="flex flex-wrap gap-2"> <div class="">
@if ($tags->count() === 0) @if ($tags->count() === 0)
<div>No tags yet defined yet. Go to a resource and add a tag there.</div> <div>No tags yet defined yet. Go to a resource and add a tag there.</div>
@else @else

View File

@@ -10,7 +10,7 @@
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> <path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M19.5 13.572l-7.5 7.428l-7.5 -7.428m0 0a5 5 0 1 1 7.5 -6.566a5 5 0 1 1 7.5 6.572" /> <path d="M19.5 13.572l-7.5 7.428l-7.5 -7.428m0 0a5 5 0 1 1 7.5 -6.566a5 5 0 1 1 7.5 6.572" />
</svg> </svg>
Inprogress In progress
@else @else
<svg xmlns="http://www.w3.org/2000/svg" <svg xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 text-pink-500 transition-colors hover:text-pink-300" viewBox="0 0 24 24" class="w-6 h-6 text-pink-500 transition-colors hover:text-pink-300" viewBox="0 0 24 24"

View File

@@ -12,6 +12,11 @@ DOCKER_VERSION="24.0"
CDN="https://cdn.coollabs.io/coolify" CDN="https://cdn.coollabs.io/coolify"
OS_TYPE=$(grep -w "ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"') OS_TYPE=$(grep -w "ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"')
# Check if the OS is manjaro, if so, change it to arch
if [ "$OS_TYPE" = "manjaro" ]; then
OS_TYPE="arch"
fi
if [ "$OS_TYPE" = "arch" ]; then if [ "$OS_TYPE" = "arch" ]; then
OS_VERSION="rolling" OS_VERSION="rolling"
else else
@@ -73,6 +78,9 @@ centos | fedora | rhel | ol | rocky | almalinux | amzn)
if [ "$OS_TYPE" = "amzn" ]; then if [ "$OS_TYPE" = "amzn" ]; then
dnf install -y wget git jq >/dev/null 2>&1 dnf install -y wget git jq >/dev/null 2>&1
else else
if ! command -v dnf >/dev/null 2>&1; then
yum install -y dnf >/dev/null 2>&1
fi
dnf install -y curl wget git jq >/dev/null 2>&1 dnf install -y curl wget git jq >/dev/null 2>&1
fi fi
;; ;;
@@ -252,7 +260,7 @@ fi
echo -e "-------------" echo -e "-------------"
mkdir -p /data/coolify/{source,ssh,applications,databases,backups,services,proxy,webhooks-during-maintenance} mkdir -p /data/coolify/{source,ssh,applications,databases,backups,services,proxy,webhooks-during-maintenance,metrics,logs}
mkdir -p /data/coolify/ssh/{keys,mux} mkdir -p /data/coolify/ssh/{keys,mux}
mkdir -p /data/coolify/proxy/dynamic mkdir -p /data/coolify/proxy/dynamic

View File

@@ -11,7 +11,6 @@ x-logging: &x-logging
options: options:
max-file: '5' max-file: '5'
max-size: '10m' max-size: '10m'
version: '3'
services: services:
appwrite: appwrite:

View File

@@ -4,8 +4,6 @@
# logo: svgs/authentik.png # logo: svgs/authentik.png
# port: 9000 # port: 9000
version: "3.4"
services: services:
postgresql: postgresql:
image: docker.io/library/postgres:12-alpine image: docker.io/library/postgres:12-alpine

View File

@@ -4,7 +4,6 @@
# logo: svgs/glitchtip.png # logo: svgs/glitchtip.png
# port: 8080 # port: 8080
version: "3.8"
services: services:
postgres: postgres:
image: postgres:16-alpine image: postgres:16-alpine

View File

@@ -0,0 +1,56 @@
# documentation: https://listmonk.app/
# slogan: Self-hosted newsletter and mailing list manager
# tags: newsletter, mailing list, self-hosted, open source
# logo: svgs/listmonk.svg
# port: 9000
services:
listmonk:
image: listmonk/listmonk:latest
environment:
- SERVICE_FQDN_LISTMONK_9000
- LISTMONK_app__address=0.0.0.0:9000
- LISTMONK_db__host=postgres
- LISTMONK_db__name=listmonk
- LISTMONK_db__user=$SERVICE_USER_POSTGRES
- LISTMONK_db__password=$SERVICE_PASSWORD_POSTGRES
- LISTMONK_db__port=5432
- LISTMONK_app__admin_username=admin
- LISTMONK_app__admin_password=$SERVICE_PASSWORD_ADMIN
- TZ=Etc/UTC
volumes:
- "listmonk-data:/listmonk/uploads"
depends_on:
postgres:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:9000"]
interval: 5s
timeout: 20s
retries: 10
listmonk-initial-database-setup:
image: listmonk/listmonk:latest
command: "./listmonk --install --yes --idempotent"
restart: "no"
depends_on:
postgres:
condition: service_healthy
environment:
- LISTMONK_db__host=postgres
- LISTMONK_db__name=listmonk
- LISTMONK_db__user=$SERVICE_USER_POSTGRES
- LISTMONK_db__password=$SERVICE_PASSWORD_POSTGRES
- LISTMONK_db__port=5432
postgres:
image: "postgres:latest"
environment:
- POSTGRES_DB=listmonk
- POSTGRES_PASSWORD=$SERVICE_PASSWORD_POSTGRES
- POSTGRES_USER=$SERVICE_USER_POSTGRES
volumes:
- "pg-data:/var/lib/postgresql/data"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 20s
retries: 10

View File

@@ -2,7 +2,6 @@
# slogan: Penpot is the first Open Source design and prototyping platform for product teams. # slogan: Penpot is the first Open Source design and prototyping platform for product teams.
# tags: penpot,design,prototyping,figma,open,source # tags: penpot,design,prototyping,figma,open,source
version: "3.5"
services: services:
frontend: frontend:
image: "penpotapp/frontend:latest" image: "penpotapp/frontend:latest"

View File

@@ -4,7 +4,6 @@
# tags: analytics, privacy, google, alternative # tags: analytics, privacy, google, alternative
# port: 8000 # port: 8000
version: "3.3"
services: services:
plausible: plausible:
image: plausible/analytics:v2.0 image: plausible/analytics:v2.0

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +1,10 @@
{ {
"coolify": { "coolify": {
"v4": { "v4": {
"version": "4.0.0-beta.273" "version": "4.0.0-beta.277"
},
"sentinel": {
"version": "0.0.4"
} }
} }
} }