diff --git a/.gitpod.yml b/.gitpod.yml
index 228f1b94c..6fd6797b5 100644
--- a/.gitpod.yml
+++ b/.gitpod.yml
@@ -3,7 +3,7 @@ tasks:
# 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)
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#USERID=#USERID=33333#g" .env
sed -i "s#GROUPID=#GROUPID=33333#g" .env
@@ -20,7 +20,7 @@ tasks:
echo "Waiting for Sail environment to boot up."
gp sync-await spin-is-ready
./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
command: |
diff --git a/README.md b/README.md
index 01c1ccef9..97580b875 100644
--- a/README.md
+++ b/README.md
@@ -34,7 +34,12 @@ # Donations
## Github Sponsors ($40+)
+
+
+
+
+
@@ -62,6 +67,7 @@ ## Organizations
+
## Individuals
diff --git a/app/Actions/Database/StartClickhouse.php b/app/Actions/Database/StartClickhouse.php
index d043da410..5f567802f 100644
--- a/app/Actions/Database/StartClickhouse.php
+++ b/app/Actions/Database/StartClickhouse.php
@@ -33,7 +33,6 @@ public function handle(StandaloneClickhouse $database)
$environment_variables = $this->generate_environment_variables();
$docker_compose = [
- 'version' => '3.8',
'services' => [
$container_name => [
'image' => $this->database->image,
diff --git a/app/Actions/Database/StartDatabaseProxy.php b/app/Actions/Database/StartDatabaseProxy.php
index a1e47710c..547884b7a 100644
--- a/app/Actions/Database/StartDatabaseProxy.php
+++ b/app/Actions/Database/StartDatabaseProxy.php
@@ -107,7 +107,6 @@ public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|St
COPY nginx.conf /etc/nginx/nginx.conf
EOF;
$docker_compose = [
- 'version' => '3.8',
'services' => [
$proxyContainerName => [
'build' => [
diff --git a/app/Actions/Database/StartDragonfly.php b/app/Actions/Database/StartDragonfly.php
index bb71d8c48..92daf195d 100644
--- a/app/Actions/Database/StartDragonfly.php
+++ b/app/Actions/Database/StartDragonfly.php
@@ -36,7 +36,6 @@ public function handle(StandaloneDragonfly $database)
$environment_variables = $this->generate_environment_variables();
$docker_compose = [
- 'version' => '3.8',
'services' => [
$container_name => [
'image' => $this->database->image,
diff --git a/app/Actions/Database/StartKeydb.php b/app/Actions/Database/StartKeydb.php
index 489c74053..8c833efd5 100644
--- a/app/Actions/Database/StartKeydb.php
+++ b/app/Actions/Database/StartKeydb.php
@@ -37,7 +37,6 @@ public function handle(StandaloneKeydb $database)
$this->add_custom_keydb();
$docker_compose = [
- 'version' => '3.8',
'services' => [
$container_name => [
'image' => $this->database->image,
@@ -96,7 +95,7 @@ public function handle(StandaloneKeydb $database)
if (count($volume_names) > 0) {
$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'][] = [
'type' => 'bind',
'source' => $this->configuration_dir . '/keydb.conf',
@@ -162,7 +161,7 @@ private function generate_environment_variables()
}
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;
}
$filename = 'keydb.conf';
diff --git a/app/Actions/Database/StartMariadb.php b/app/Actions/Database/StartMariadb.php
index e02b28b2e..c79df0dc5 100644
--- a/app/Actions/Database/StartMariadb.php
+++ b/app/Actions/Database/StartMariadb.php
@@ -32,7 +32,6 @@ public function handle(StandaloneMariadb $database)
$environment_variables = $this->generate_environment_variables();
$this->add_custom_mysql();
$docker_compose = [
- 'version' => '3.8',
'services' => [
$container_name => [
'image' => $this->database->image,
@@ -90,7 +89,7 @@ public function handle(StandaloneMariadb $database)
if (count($volume_names) > 0) {
$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'][] = [
'type' => 'bind',
'source' => $this->configuration_dir . '/custom-config.cnf',
@@ -165,7 +164,7 @@ private function generate_environment_variables()
}
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;
}
$filename = 'custom-config.cnf';
diff --git a/app/Actions/Database/StartMongodb.php b/app/Actions/Database/StartMongodb.php
index 7bb6cbcd0..46b426ad8 100644
--- a/app/Actions/Database/StartMongodb.php
+++ b/app/Actions/Database/StartMongodb.php
@@ -35,7 +35,6 @@ public function handle(StandaloneMongodb $database)
$this->add_custom_mongo_conf();
$docker_compose = [
- 'version' => '3.8',
'services' => [
$container_name => [
'image' => $this->database->image,
@@ -51,8 +50,9 @@ public function handle(StandaloneMongodb $database)
],
'healthcheck' => [
'test' => [
- 'CMD-SHELL',
- 'mongosh --eval "printjson(db.runCommand(\"ping\"))"'
+ "CMD",
+ "echo",
+ "ok"
],
'interval' => '5s',
'timeout' => '5s',
@@ -97,7 +97,7 @@ public function handle(StandaloneMongodb $database)
if (count($volume_names) > 0) {
$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'][] = [
'type' => 'bind',
'source' => $this->configuration_dir . '/mongod.conf',
@@ -178,7 +178,7 @@ private function generate_environment_variables()
}
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;
}
$filename = 'mongod.conf';
diff --git a/app/Actions/Database/StartMysql.php b/app/Actions/Database/StartMysql.php
index b3f695d72..6fdc8cdad 100644
--- a/app/Actions/Database/StartMysql.php
+++ b/app/Actions/Database/StartMysql.php
@@ -32,7 +32,6 @@ public function handle(StandaloneMysql $database)
$environment_variables = $this->generate_environment_variables();
$this->add_custom_mysql();
$docker_compose = [
- 'version' => '3.8',
'services' => [
$container_name => [
'image' => $this->database->image,
@@ -90,7 +89,7 @@ public function handle(StandaloneMysql $database)
if (count($volume_names) > 0) {
$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'][] = [
'type' => 'bind',
'source' => $this->configuration_dir . '/custom-config.cnf',
@@ -165,7 +164,7 @@ private function generate_environment_variables()
}
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;
}
$filename = 'custom-config.cnf';
diff --git a/app/Actions/Database/StartPostgresql.php b/app/Actions/Database/StartPostgresql.php
index f19a8b036..8db874ea6 100644
--- a/app/Actions/Database/StartPostgresql.php
+++ b/app/Actions/Database/StartPostgresql.php
@@ -35,7 +35,6 @@ public function handle(StandalonePostgresql $database)
$this->add_custom_conf();
$docker_compose = [
- 'version' => '3.8',
'services' => [
$container_name => [
'image' => $this->database->image,
@@ -78,7 +77,6 @@ public function handle(StandalonePostgresql $database)
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
- ray('Log Drain Enabled');
$docker_compose['services'][$container_name]['logging'] = [
'driver' => 'fluentd',
'options' => [
@@ -107,7 +105,7 @@ public function handle(StandalonePostgresql $database)
];
}
}
- 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'][] = [
'type' => 'bind',
'source' => $this->configuration_dir . '/custom-postgres.conf',
@@ -165,8 +163,6 @@ private function generate_local_persistent_volumes_only_volume_names()
private function generate_environment_variables()
{
$environment_variables = collect();
- ray('Generate Environment Variables')->green();
- ray($this->database->runtime_environment_variables)->green();
foreach ($this->database->runtime_environment_variables as $env) {
$environment_variables->push("$env->key=$env->real_value");
}
@@ -203,11 +199,16 @@ private function generate_init_scripts()
}
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;
}
$filename = 'custom-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);
$this->commands[] = "echo '{$content_base64}' | base64 -d | tee $this->configuration_dir/{$filename} > /dev/null";
}
diff --git a/app/Actions/Database/StartRedis.php b/app/Actions/Database/StartRedis.php
index 01e9a9bef..5b6ab2999 100644
--- a/app/Actions/Database/StartRedis.php
+++ b/app/Actions/Database/StartRedis.php
@@ -37,7 +37,6 @@ public function handle(StandaloneRedis $database)
$this->add_custom_redis();
$docker_compose = [
- 'version' => '3.8',
'services' => [
$container_name => [
'image' => $this->database->image,
@@ -100,7 +99,7 @@ public function handle(StandaloneRedis $database)
if (count($volume_names) > 0) {
$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'][] = [
'type' => 'bind',
'source' => $this->configuration_dir . '/redis.conf',
@@ -166,7 +165,7 @@ private function generate_environment_variables()
}
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;
}
$filename = 'redis.conf';
diff --git a/app/Actions/Docker/GetContainersStatus.php b/app/Actions/Docker/GetContainersStatus.php
new file mode 100644
index 000000000..f0e1de8f6
--- /dev/null
+++ b/app/Actions/Docker/GetContainersStatus.php
@@ -0,0 +1,657 @@
+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 = data_get($service_db, 'service.uuid');
+ if ($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);
+ }
+ }
+}
diff --git a/app/Actions/Server/StartSentinel.php b/app/Actions/Server/StartSentinel.php
new file mode 100644
index 000000000..6f3c81d77
--- /dev/null
+++ b/app/Actions/Server/StartSentinel.php
@@ -0,0 +1,22 @@
+server, false);
$this->latestVersion = get_latest_version_of_coolify();
$this->currentVersion = config('version');
- if ($settings->next_channel) {
- ray('next channel enabled');
- $this->latestVersion = 'next';
- }
+ // if ($settings->next_channel) {
+ // ray('next channel enabled');
+ // $this->latestVersion = 'next';
+ // }
if ($force) {
$this->update();
} else {
diff --git a/app/Console/Commands/AdminRemoveUser.php b/app/Console/Commands/AdminRemoveUser.php
new file mode 100644
index 000000000..76af0a97f
--- /dev/null
+++ b/app/Console/Commands/AdminRemoveUser.php
@@ -0,0 +1,54 @@
+argument('email');
+ $confirm = $this->confirm('Are you sure you want to remove user with email: ' . $email . '?');
+ if (!$confirm) {
+ $this->info('User removal cancelled.');
+ return;
+ }
+ $this->info("Removing user with email: $email");
+ $user = User::whereEmail($email)->firstOrFail();
+ $teams = $user->teams;
+ foreach ($teams as $team) {
+ if ($team->members->count() > 1) {
+ $this->error('User is a member of a team with more than one member. Please remove user from team first.');
+ return;
+ }
+ $team->delete();
+ }
+ $user->delete();
+ } catch (\Exception $e) {
+ $this->error('Failed to remove user.');
+ $this->error($e->getMessage());
+ return;
+ }
+ }
+}
diff --git a/app/Console/Commands/Cloud.php b/app/Console/Commands/Cloud.php
deleted file mode 100644
index 1386b296c..000000000
--- a/app/Console/Commands/Cloud.php
+++ /dev/null
@@ -1,33 +0,0 @@
-whereNotNull('team.subscription')->where('team.subscription.stripe_trial_already_ended',true)->each(function($server){
- $this->info($server->name);
- });
- }
-}
diff --git a/app/Console/Commands/RootResetPassword.php b/app/Console/Commands/RootResetPassword.php
index df385002e..af2b1a45c 100644
--- a/app/Console/Commands/RootResetPassword.php
+++ b/app/Console/Commands/RootResetPassword.php
@@ -29,7 +29,6 @@ class RootResetPassword extends Command
*/
public function handle()
{
- //
$this->info('You are about to reset the root password.');
$password = password('Give me a new password for root user: ');
$passwordAgain = password('Again');
diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php
index 31760f2fd..23289f90e 100644
--- a/app/Console/Kernel.php
+++ b/app/Console/Kernel.php
@@ -9,6 +9,7 @@
use App\Jobs\InstanceAutoUpdateJob;
use App\Jobs\ContainerStatusJob;
use App\Jobs\PullHelperImageJob;
+use App\Jobs\PullSentinelImageJob;
use App\Jobs\ServerStatusJob;
use App\Models\InstanceSettings;
use App\Models\ScheduledDatabaseBackup;
@@ -20,8 +21,10 @@
class Kernel extends ConsoleKernel
{
+ private $all_servers;
protected function schedule(Schedule $schedule): void
{
+ $this->all_servers = Server::all();
if (isDev()) {
// Instance Jobs
$schedule->command('horizon:snapshot')->everyMinute();
@@ -55,35 +58,38 @@ protected function schedule(Schedule $schedule): void
}
private function pull_helper_image($schedule)
{
- $servers = Server::all()->where('settings.is_usable', true)->where('settings.is_reachable', true)->where('ip', '!=', '1.2.3.4');
+ $servers = $this->all_servers->where('settings.is_usable', true)->where('settings.is_reachable', true)->where('ip', '!=', '1.2.3.4');
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)
{
if (isCloud()) {
- $servers = Server::all()->whereNotNull('team.subscription')->where('team.subscription.stripe_trial_already_ended', false)->where('ip', '!=', '1.2.3.4');
+ $servers = $this->all_servers->whereNotNull('team.subscription')->where('team.subscription.stripe_trial_already_ended', false)->where('ip', '!=', '1.2.3.4');
$own = Team::find(0)->servers;
$servers = $servers->merge($own);
$containerServers = $servers->where('settings.is_swarm_worker', false)->where('settings.is_build_server', false);
} else {
- $servers = Server::all()->where('ip', '!=', '1.2.3.4');
+ $servers = $this->all_servers->where('ip', '!=', '1.2.3.4');
$containerServers = $servers->where('settings.is_swarm_worker', false)->where('settings.is_build_server', false);
}
foreach ($containerServers as $server) {
- $schedule->job(new ContainerStatusJob($server))->everyTwoMinutes()->onOneServer();
+ $schedule->job(new ContainerStatusJob($server))->everyMinute()->onOneServer();
if ($server->isLogDrainEnabled()) {
- $schedule->job(new CheckLogDrainContainerJob($server))->everyTwoMinutes()->onOneServer();
+ $schedule->job(new CheckLogDrainContainerJob($server))->everyMinute()->onOneServer();
}
}
foreach ($servers as $server) {
- $schedule->job(new ServerStatusJob($server))->everyTwoMinutes()->onOneServer();
+ $schedule->job(new ServerStatusJob($server))->everyMinute()->onOneServer();
}
}
private function instance_auto_update($schedule)
{
- if (isDev()) {
+ if (isDev() || isCloud()) {
return;
}
$settings = InstanceSettings::get();
@@ -134,7 +140,16 @@ private function check_scheduled_tasks($schedule)
$scheduled_task->delete();
continue;
}
-
+ if ($application) {
+ if (str($application->status)->contains('running') === false) {
+ continue;
+ }
+ }
+ if ($service) {
+ if (str($service->status())->contains('running') === false) {
+ continue;
+ }
+ }
if (isset(VALID_CRON_STRINGS[$scheduled_task->frequency])) {
$scheduled_task->frequency = VALID_CRON_STRINGS[$scheduled_task->frequency];
}
diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php
index 1241751f0..daba1cecb 100644
--- a/app/Http/Controllers/Controller.php
+++ b/app/Http/Controllers/Controller.php
@@ -37,7 +37,7 @@ public function verify() {
public function email_verify(EmailVerificationRequest $request) {
$request->fulfill();
$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);
}
public function forgot_password(Request $request) {
diff --git a/app/Http/Controllers/Webhook/Bitbucket.php b/app/Http/Controllers/Webhook/Bitbucket.php
index 485720c23..7b569c278 100644
--- a/app/Http/Controllers/Webhook/Bitbucket.php
+++ b/app/Http/Controllers/Webhook/Bitbucket.php
@@ -47,7 +47,7 @@ public function manual(Request $request)
if ($x_bitbucket_event === 'repo:push') {
$branch = data_get($payload, 'push.changes.0.new.name');
$full_name = data_get($payload, 'repository.full_name');
-
+ $commit = data_get($payload, 'push.changes.0.new.target.hash');
if (!$branch) {
return response([
'status' => 'failed',
@@ -104,6 +104,7 @@ public function manual(Request $request)
queue_application_deployment(
application: $application,
deployment_uuid: $deployment_uuid,
+ commit: $commit,
force_rebuild: false,
is_webhook: true
);
diff --git a/app/Http/Controllers/Webhook/Github.php b/app/Http/Controllers/Webhook/Github.php
index 214843aab..baa23deec 100644
--- a/app/Http/Controllers/Webhook/Github.php
+++ b/app/Http/Controllers/Webhook/Github.php
@@ -129,6 +129,7 @@ public function manual(Request $request)
application: $application,
deployment_uuid: $deployment_uuid,
force_rebuild: false,
+ commit: data_get($payload, 'after', 'HEAD'),
is_webhook: true,
);
$return_payloads->push([
@@ -177,6 +178,7 @@ public function manual(Request $request)
pull_request_id: $pull_request_id,
deployment_uuid: $deployment_uuid,
force_rebuild: false,
+ commit: data_get($payload, 'head.sha', 'HEAD'),
is_webhook: true,
git_type: 'github'
);
@@ -338,6 +340,7 @@ public function normal(Request $request)
queue_application_deployment(
application: $application,
deployment_uuid: $deployment_uuid,
+ commit: data_get($payload, 'after', 'HEAD'),
force_rebuild: false,
is_webhook: true,
);
@@ -387,6 +390,7 @@ public function normal(Request $request)
pull_request_id: $pull_request_id,
deployment_uuid: $deployment_uuid,
force_rebuild: false,
+ commit: data_get($payload, 'head.sha', 'HEAD'),
is_webhook: true,
git_type: 'github'
);
diff --git a/app/Http/Controllers/Webhook/Gitlab.php b/app/Http/Controllers/Webhook/Gitlab.php
index 65ce9910b..dfa9394eb 100644
--- a/app/Http/Controllers/Webhook/Gitlab.php
+++ b/app/Http/Controllers/Webhook/Gitlab.php
@@ -38,6 +38,15 @@ public function manual(Request $request)
$headers = $request->headers->all();
$x_gitlab_token = data_get($headers, 'x-gitlab-token.0');
$x_gitlab_event = data_get($payload, 'object_kind');
+ $allowed_events = ['push', 'merge_request'];
+ if (!in_array($x_gitlab_event, $allowed_events)) {
+ $return_payloads->push([
+ 'status' => 'failed',
+ 'message' => 'Event not allowed. Only push and merge_request events are allowed.',
+ ]);
+ return response($return_payloads);
+ }
+
if ($x_gitlab_event === 'push') {
$branch = data_get($payload, 'ref');
$full_name = data_get($payload, 'project.path_with_namespace');
@@ -124,6 +133,7 @@ public function manual(Request $request)
queue_application_deployment(
application: $application,
deployment_uuid: $deployment_uuid,
+ commit: data_get($payload, 'after', 'HEAD'),
force_rebuild: false,
is_webhook: true,
);
@@ -173,6 +183,7 @@ public function manual(Request $request)
application: $application,
pull_request_id: $pull_request_id,
deployment_uuid: $deployment_uuid,
+ commit: data_get($payload, 'object_attributes.last_commit.id', 'HEAD'),
force_rebuild: false,
is_webhook: true,
git_type: 'gitlab'
diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php
index 1717c5d08..47319ac10 100644
--- a/app/Jobs/ApplicationDeploymentJob.php
+++ b/app/Jobs/ApplicationDeploymentJob.php
@@ -2,6 +2,7 @@
namespace App\Jobs;
+use App\Actions\Docker\GetContainersStatus;
use App\Enums\ApplicationDeploymentStatus;
use App\Enums\ProcessStatus;
use App\Events\ApplicationStatusChanged;
@@ -95,7 +96,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private ?string $buildTarget = null;
private Collection $saved_outputs;
private ?string $full_healthcheck_url = null;
- private bool $custom_healthcheck_found = false;
private string $serverUser = 'root';
private string $serverUserHomeDir = '/root';
@@ -107,6 +107,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private ?string $fullRepoUrl = null;
private ?string $branch = null;
+ private ?string $coolify_variables = null;
+
public $tries = 1;
public function __construct(int $application_deployment_queue_id)
{
@@ -303,7 +305,8 @@ private function post_deployment()
{
if ($this->server->isProxyShouldRun()) {
- dispatch(new ContainerStatusJob($this->server));
+ GetContainersStatus::dispatch($this->server);
+ // dispatch(new ContainerStatusJob($this->server));
}
$this->next(ApplicationDeploymentStatus::FINISHED->value);
if ($this->pull_request_id !== 0) {
@@ -405,7 +408,7 @@ private function deploy_docker_compose_buildpack()
);
} else {
$this->execute_remote_command(
- [executeInDocker($this->deployment_uuid, "SOURCE_COMMIT={$this->commit} docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} build"), "hidden" => true],
+ [executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} build"), "hidden" => true],
);
}
@@ -435,9 +438,9 @@ private function deploy_docker_compose_buildpack()
} else {
$this->write_deployment_configurations();
$server_workdir = $this->application->workdir();
- ray("SOURCE_COMMIT={$this->commit} docker compose --project-directory {$server_workdir} -f {$server_workdir}{$this->docker_compose_location} up -d");
+ ray("{$this->coolify_variables} docker compose --project-directory {$server_workdir} -f {$server_workdir}{$this->docker_compose_location} up -d");
$this->execute_remote_command(
- ["SOURCE_COMMIT={$this->commit} docker compose --project-directory {$server_workdir} -f {$server_workdir}{$this->docker_compose_location} up -d", "hidden" => true],
+ ["{$this->coolify_variables} docker compose --project-directory {$server_workdir} -f {$server_workdir}{$this->docker_compose_location} up -d", "hidden" => true],
);
}
} else {
@@ -448,7 +451,7 @@ private function deploy_docker_compose_buildpack()
$this->write_deployment_configurations();
} else {
$this->execute_remote_command(
- [executeInDocker($this->deployment_uuid, "SOURCE_COMMIT={$this->commit} docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up -d"), "hidden" => true],
+ [executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up -d"), "hidden" => true],
);
$this->write_deployment_configurations();
}
@@ -710,10 +713,40 @@ private function check_image_locally_or_remotely()
private function save_environment_variables()
{
$envs = collect([]);
+ $local_branch = $this->branch;
+ if ($this->pull_request_id !== 0) {
+ $local_branch = "pull/{$this->pull_request_id}/head";
+ }
+ $sort = $this->application->settings->is_env_sorting_enabled;
+ if ($sort) {
+ $sorted_environment_variables = $this->application->environment_variables->sortBy('key');
+ $sorted_environment_variables_preview = $this->application->environment_variables_preview->sortBy('key');
+ } else {
+ $sorted_environment_variables = $this->application->environment_variables->sortBy('id');
+ $sorted_environment_variables_preview = $this->application->environment_variables_preview->sortBy('id');
+ }
$ports = $this->application->main_port();
if ($this->pull_request_id !== 0) {
$this->env_filename = ".env-pr-$this->pull_request_id";
- foreach ($this->application->environment_variables_preview as $env) {
+ // Add SOURCE_COMMIT if not exists
+ if ($this->application->environment_variables_preview->where('key', 'SOURCE_COMMIT')->isEmpty()) {
+ if (!is_null($this->commit)) {
+ $envs->push("SOURCE_COMMIT={$this->commit}");
+ } else {
+ $envs->push("SOURCE_COMMIT=unknown");
+ }
+ }
+ if ($this->application->environment_variables_preview->where('key', 'COOLIFY_FQDN')->isEmpty()) {
+ $envs->push("COOLIFY_FQDN={$this->preview->fqdn}");
+ }
+ if ($this->application->environment_variables_preview->where('key', 'COOLIFY_URL')->isEmpty()) {
+ $url = str($this->preview->fqdn)->replace('http://', '')->replace('https://', '');
+ $envs->push("COOLIFY_URL={$url}");
+ }
+ if ($this->application->environment_variables_preview->where('key', 'COOLIFY_BRANCH')->isEmpty()) {
+ $envs->push("COOLIFY_BRANCH={$local_branch}");
+ }
+ foreach ($sorted_environment_variables_preview as $env) {
$real_value = $env->real_value;
if ($env->version === '4.0.0-beta.239') {
$real_value = $env->real_value;
@@ -734,20 +767,27 @@ private function save_environment_variables()
if ($this->application->environment_variables_preview->where('key', 'HOST')->isEmpty()) {
$envs->push("HOST=0.0.0.0");
}
+ } else {
+ $this->env_filename = ".env";
// Add SOURCE_COMMIT if not exists
- if ($this->application->environment_variables_preview->where('key', 'SOURCE_COMMIT')->isEmpty()) {
+ if ($this->application->environment_variables->where('key', 'SOURCE_COMMIT')->isEmpty()) {
if (!is_null($this->commit)) {
$envs->push("SOURCE_COMMIT={$this->commit}");
} else {
$envs->push("SOURCE_COMMIT=unknown");
}
}
- $envs = $envs->sort(function ($a, $b) {
- return strpos($a, '$') === false ? -1 : 1;
- });
- } else {
- $this->env_filename = ".env";
- foreach ($this->application->environment_variables as $env) {
+ if ($this->application->environment_variables->where('key', 'COOLIFY_FQDN')->isEmpty()) {
+ $envs->push("COOLIFY_FQDN={$this->application->fqdn}");
+ }
+ if ($this->application->environment_variables->where('key', 'COOLIFY_URL')->isEmpty()) {
+ $url = str($this->application->fqdn)->replace('http://', '')->replace('https://', '');
+ $envs->push("COOLIFY_URL={$url}");
+ }
+ if ($this->application->environment_variables_preview->where('key', 'COOLIFY_BRANCH')->isEmpty()) {
+ $envs->push("COOLIFY_BRANCH={$local_branch}");
+ }
+ foreach ($sorted_environment_variables as $env) {
$real_value = $env->real_value;
if ($env->version === '4.0.0-beta.239') {
$real_value = $env->real_value;
@@ -768,17 +808,6 @@ private function save_environment_variables()
if ($this->application->environment_variables->where('key', 'HOST')->isEmpty()) {
$envs->push("HOST=0.0.0.0");
}
- // Add SOURCE_COMMIT if not exists
- if ($this->application->environment_variables->where('key', 'SOURCE_COMMIT')->isEmpty()) {
- if (!is_null($this->commit)) {
- $envs->push("SOURCE_COMMIT={$this->commit}");
- } else {
- $envs->push("SOURCE_COMMIT=unknown");
- }
- }
- $envs = $envs->sort(function ($a, $b) {
- return strpos($a, '$') === false ? -1 : 1;
- });
}
if ($envs->isEmpty()) {
@@ -870,7 +899,7 @@ private function rolling_update()
$this->write_deployment_configurations();
$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("----------------------------------------");
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.");
@@ -878,6 +907,9 @@ private function rolling_update()
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.");
}
+ 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) {
$this->application->settings->is_consistent_container_name_enabled = true;
$this->application_deployment_queue->addLogEntry("Pull request deployment, rolling update is not supported.");
@@ -903,10 +935,13 @@ private function health_check()
if ($this->server->isSwarm()) {
// Implement healthcheck for swarm
} else {
- if ($this->application->isHealthcheckDisabled() && $this->custom_healthcheck_found === false) {
+ if ($this->application->isHealthcheckDisabled() && $this->application->custom_healthcheck_found === false) {
$this->newVersionIsHealthy = true;
return;
}
+ if ($this->application->custom_healthcheck_found) {
+ $this->application_deployment_queue->addLogEntry("Custom healthcheck found, skipping default healthcheck.");
+ }
// ray('New container name: ', $this->container_name);
if ($this->container_name) {
$counter = 1;
@@ -914,6 +949,12 @@ private function health_check()
if ($this->full_healthcheck_url) {
$this->application_deployment_queue->addLogEntry("Healthcheck URL (inside the container): {$this->full_healthcheck_url}");
}
+ $this->application_deployment_queue->addLogEntry("Waiting for the start period ({$this->application->health_check_start_period} seconds) before starting healthcheck.");
+ $sleeptime = 0;
+ while ($sleeptime < $this->application->health_check_start_period) {
+ Sleep::for(1)->seconds();
+ $sleeptime++;
+ }
while ($counter <= $this->application->health_check_retries) {
$this->execute_remote_command(
[
@@ -922,9 +963,23 @@ private function health_check()
"save" => "health_check",
"append" => false
],
-
+ [
+ "docker inspect --format='{{json .State.Health.Log}}' {$this->container_name}",
+ "hidden" => true,
+ "save" => "health_check_logs",
+ "append" => false
+ ],
);
$this->application_deployment_queue->addLogEntry("Attempt {$counter} of {$this->application->health_check_retries} | Healthcheck status: {$this->saved_outputs->get('health_check')}");
+ $health_check_logs = data_get(collect(json_decode($this->saved_outputs->get('health_check_logs')))->last(), 'Output', '(no logs)');
+ if (empty($health_check_logs)) {
+ $health_check_logs = '(no logs)';
+ }
+ $health_check_return_code = data_get(collect(json_decode($this->saved_outputs->get('health_check_logs')))->last(), 'ExitCode', '(no return code)');
+ if ($health_check_logs !== '(no logs)' || $health_check_return_code !== '(no return code)') {
+ $this->application_deployment_queue->addLogEntry("Healthcheck logs: {$health_check_logs} | Return code: {$health_check_return_code}");
+ }
+
if (Str::of($this->saved_outputs->get('health_check'))->replace('"', '')->value() === 'healthy') {
$this->newVersionIsHealthy = true;
$this->application->update(['status' => 'running']);
@@ -936,7 +991,11 @@ private function health_check()
break;
}
$counter++;
- Sleep::for($this->application->health_check_interval)->seconds();
+ $sleeptime = 0;
+ while ($sleeptime < $this->application->health_check_interval) {
+ Sleep::for(1)->seconds();
+ $sleeptime++;
+ }
}
}
}
@@ -954,6 +1013,7 @@ private function deploy_pull_request()
$this->generate_image_names();
$this->application_deployment_queue->addLogEntry("Starting pull request (#{$this->pull_request_id}) deployment of {$this->customRepository}:{$this->application->git_branch}.");
$this->prepare_builder_image();
+ $this->check_git_if_build_needed();
$this->clone_repository();
$this->set_base_dir();
$this->cleanup_git();
@@ -1005,7 +1065,9 @@ private function prepare_builder_image()
"command" => "docker rm -f {$this->deployment_uuid}",
"ignore_errors" => true,
"hidden" => true
- ],
+ ]
+ );
+ $this->execute_remote_command(
[
$runCommand,
"hidden" => true,
@@ -1061,9 +1123,30 @@ private function set_base_dir()
{
$this->application_deployment_queue->addLogEntry("Setting base directory to {$this->workdir}.");
}
+ private function set_coolify_variables()
+ {
+ $this->coolify_variables = "SOURCE_COMMIT={$this->commit} ";
+ if ($this->pull_request_id === 0) {
+ $fqdn = $this->application->fqdn;
+ } else {
+ $fqdn = $this->preview->fqdn;
+ }
+ if (isset($fqdn)) {
+ $this->coolify_variables .= "COOLIFY_FQDN={$fqdn} ";
+ $url = str($fqdn)->replace('http://', '')->replace('https://', '');
+ $this->coolify_variables .= "COOLIFY_URL={$url} ";
+ }
+ if (isset($this->application->git_branch)) {
+ $this->coolify_variables .= "COOLIFY_BRANCH={$this->application->git_branch} ";
+ }
+ }
private function check_git_if_build_needed()
{
$this->generate_git_import_commands();
+ $local_branch = $this->branch;
+ if ($this->pull_request_id !== 0) {
+ $local_branch = "pull/{$this->pull_request_id}/head";
+ }
$private_key = data_get($this->application, 'private_key.private_key');
if ($private_key) {
$private_key = base64_encode($private_key);
@@ -1078,7 +1161,7 @@ private function check_git_if_build_needed()
executeInDocker($this->deployment_uuid, "chmod 600 /root/.ssh/id_rsa")
],
[
- executeInDocker($this->deployment_uuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git ls-remote {$this->fullRepoUrl} {$this->branch}"),
+ executeInDocker($this->deployment_uuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git ls-remote {$this->fullRepoUrl} {$local_branch}"),
"hidden" => true,
"save" => "git_commit_sha"
],
@@ -1086,15 +1169,19 @@ private function check_git_if_build_needed()
} else {
$this->execute_remote_command(
[
- executeInDocker($this->deployment_uuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git ls-remote {$this->fullRepoUrl} {$this->branch}"),
+ executeInDocker($this->deployment_uuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git ls-remote {$this->fullRepoUrl} {$local_branch}"),
"hidden" => true,
"save" => "git_commit_sha"
],
);
}
+ ray("GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git ls-remote {$this->fullRepoUrl} {$local_branch}");
if ($this->saved_outputs->get('git_commit_sha') && !$this->rollback) {
$this->commit = $this->saved_outputs->get('git_commit_sha')->before("\t");
+ $this->application_deployment_queue->commit = $this->commit;
+ $this->application_deployment_queue->save();
}
+ $this->set_coolify_variables();
}
private function clone_repository()
{
@@ -1104,12 +1191,29 @@ private function clone_repository()
if ($this->pull_request_id !== 0) {
$this->application_deployment_queue->addLogEntry("Checking out tag pull/{$this->pull_request_id}/head.");
}
+ ray($importCommands);
$this->execute_remote_command(
[
$importCommands, "hidden" => true
]
);
$this->create_workdir();
+ $this->execute_remote_command(
+ [
+ executeInDocker($this->deployment_uuid, "cd {$this->workdir} && git log -1 {$this->commit} --pretty=%B"),
+ "hidden" => true,
+ "save" => "commit_message"
+ ]
+ );
+ ray($this->saved_outputs->get('commit_message'));
+ raY($this->commit);
+ if ($this->saved_outputs->get('commit_message')) {
+ $commit_message = str($this->saved_outputs->get('commit_message'))->limit(47);
+ $this->application_deployment_queue->commit_message = $commit_message->value();
+ ApplicationDeploymentQueue::whereCommit($this->commit)->whereApplicationId($this->application->id)->update(
+ ['commit_message' => $commit_message->value()]
+ );
+ }
}
private function generate_git_import_commands()
@@ -1197,6 +1301,7 @@ private function generate_nixpacks_env_variables()
private function generate_env_variables()
{
$this->env_args = collect([]);
+ $this->env_args->put('SOURCE_COMMIT', $this->commit);
if ($this->pull_request_id === 0) {
foreach ($this->application->build_environment_variables as $env) {
if (!is_null($env->real_value)) {
@@ -1210,7 +1315,6 @@ private function generate_env_variables()
}
}
}
- $this->env_args->put('SOURCE_COMMIT', $this->commit);
}
private function generate_compose_file()
@@ -1258,23 +1362,22 @@ private function generate_compose_file()
if ($this->pull_request_id !== 0) {
$labels = collect(generateLabelsApplication($this->application, $this->preview));
}
- $labels = $labels->map(function ($value, $key) {
- return escapeDollarSign($value);
- });
+ if ($this->application->settings->is_container_label_escape_enabled) {
+ $labels = $labels->map(function ($value, $key) {
+ return escapeDollarSign($value);
+ });
+ }
$labels = $labels->merge(defaultLabels($this->application->id, $this->application->uuid, $this->pull_request_id))->toArray();
+
// Check for custom HEALTHCHECK
- $this->custom_healthcheck_found = false;
if ($this->application->build_pack === 'dockerfile' || $this->application->dockerfile) {
$this->execute_remote_command([
executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"), "hidden" => true, "save" => 'dockerfile_from_repo', "ignore_errors" => true
]);
$dockerfile = collect(Str::of($this->saved_outputs->get('dockerfile_from_repo'))->trim()->explode("\n"));
- if (str($dockerfile)->contains('HEALTHCHECK')) {
- $this->custom_healthcheck_found = true;
- }
+ $this->application->parseHealthcheckFromDockerfile($dockerfile);
}
$docker_compose = [
- 'version' => '3.8',
'services' => [
$this->container_name => [
'image' => $this->production_image_name,
@@ -1282,7 +1385,11 @@ private function generate_compose_file()
'restart' => RESTART_MODE,
'expose' => $ports,
'networks' => [
- $this->destination->network,
+ $this->destination->network => [
+ 'aliases' => [
+ $this->container_name
+ ]
+ ]
],
'mem_limit' => $this->application->limits_memory,
'memswap_limit' => $this->application->limits_memory_swap,
@@ -1300,6 +1407,9 @@ private function generate_compose_file()
]
]
];
+ 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 (data_get($docker_compose, "services.{$this->container_name}.env_file")) {
// $docker_compose['services'][$this->container_name]['env_file'][] = '.env';
@@ -1317,18 +1427,17 @@ private function generate_compose_file()
if (!is_null($this->env_filename)) {
$docker_compose['services'][$this->container_name]['env_file'] = [$this->env_filename];
}
- if (!$this->custom_healthcheck_found) {
- $docker_compose['services'][$this->container_name]['healthcheck'] = [
- 'test' => [
- 'CMD-SHELL',
- $this->generate_healthcheck_commands()
- ],
- 'interval' => $this->application->health_check_interval . 's',
- 'timeout' => $this->application->health_check_timeout . 's',
- 'retries' => $this->application->health_check_retries,
- 'start_period' => $this->application->health_check_start_period . 's'
- ];
- }
+ $docker_compose['services'][$this->container_name]['healthcheck'] = [
+ 'test' => [
+ 'CMD-SHELL',
+ $this->generate_healthcheck_commands()
+ ],
+ 'interval' => $this->application->health_check_interval . 's',
+ 'timeout' => $this->application->health_check_timeout . 's',
+ 'retries' => $this->application->health_check_retries,
+ 'start_period' => $this->application->health_check_start_period . 's'
+ ];
+
if (!is_null($this->application->limits_cpuset)) {
data_set($docker_compose, 'services.' . $this->container_name . '.cpuset', $this->application->limits_cpuset);
}
@@ -1510,95 +1619,8 @@ private function generate_local_persistent_volumes_only_volume_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()
{
- 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) {
$health_check_port = $this->application->ports_exposes_array[0];
} else {
@@ -1610,12 +1632,12 @@ private function generate_healthcheck_commands()
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}";
$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 {
$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 = [
- "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);
@@ -1804,12 +1826,17 @@ private function stop_running_container(bool $force = false)
["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(
["docker rm -f $this->container_name >/dev/null 2>&1", "hidden" => true, "ignore_errors" => true],
);
}
} 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->update([
'status' => ApplicationDeploymentStatus::FAILED->value,
@@ -1827,11 +1854,11 @@ private function build_by_compose_file()
$this->application_deployment_queue->addLogEntry("Pulling latest images from the registry.");
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} pull"), "hidden" => true],
- [executeInDocker($this->deployment_uuid, "SOURCE_COMMIT={$this->commit} docker compose --project-directory {$this->workdir} build"), "hidden" => true],
+ [executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-directory {$this->workdir} build"), "hidden" => true],
);
} else {
$this->execute_remote_command(
- [executeInDocker($this->deployment_uuid, "SOURCE_COMMIT={$this->commit} docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} build"), "hidden" => true],
+ [executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} build"), "hidden" => true],
);
}
$this->application_deployment_queue->addLogEntry("New images built.");
@@ -1843,16 +1870,16 @@ private function start_by_compose_file()
$this->application_deployment_queue->addLogEntry("Pulling latest images from the registry.");
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} pull"), "hidden" => true],
- [executeInDocker($this->deployment_uuid, "SOURCE_COMMIT={$this->commit} docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true],
+ [executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true],
);
} else {
if ($this->use_build_server) {
$this->execute_remote_command(
- ["SOURCE_COMMIT={$this->commit} docker compose --project-directory {$this->configuration_dir} -f {$this->configuration_dir}{$this->docker_compose_location} up --build -d", "hidden" => true],
+ ["{$this->coolify_variables} docker compose --project-directory {$this->configuration_dir} -f {$this->configuration_dir}{$this->docker_compose_location} up --build -d", "hidden" => true],
);
} else {
$this->execute_remote_command(
- [executeInDocker($this->deployment_uuid, "SOURCE_COMMIT={$this->commit} docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up --build -d"), "hidden" => true],
+ [executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up --build -d"), "hidden" => true],
);
}
}
diff --git a/app/Jobs/ContainerStatusJob.php b/app/Jobs/ContainerStatusJob.php
index a9cec009b..11e7013ee 100644
--- a/app/Jobs/ContainerStatusJob.php
+++ b/app/Jobs/ContainerStatusJob.php
@@ -2,15 +2,8 @@
namespace App\Jobs;
-use App\Actions\Database\StartDatabaseProxy;
-use App\Actions\Proxy\CheckProxy;
-use App\Actions\Proxy\StartProxy;
-use App\Actions\Shared\ComplexStatusCheck;
-use App\Models\ApplicationPreview;
+use App\Actions\Docker\GetContainersStatus;
use App\Models\Server;
-use App\Models\ServiceDatabase;
-use App\Notifications\Container\ContainerRestarted;
-use App\Notifications\Container\ContainerStopped;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
@@ -18,7 +11,6 @@
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
-use Illuminate\Support\Arr;
class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
{
@@ -44,335 +36,6 @@ public function uniqueId(): int
public function handle()
{
- if (!$this->server->isFunctional()) {
- return 'Server is not ready.';
- };
- $applications = $this->server->applications();
- $skip_these_applications = collect([]);
- foreach ($applications as $application) {
- if ($application->additional_servers->count() > 0) {
- $skip_these_applications->push($application);
- 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);
- });
- try {
- 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 = $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 = $applications->pluck('id')->diff($foundApplications);
- foreach ($notRunningApplications as $applicationId) {
- $application = $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);
- }
- } catch (\Throwable $e) {
- send_internal_notification("ContainerStatusJob failed on ({$this->server->id}) with: " . $e->getMessage());
- ray($e->getMessage());
- return handleError($e);
- }
+ GetContainersStatus::run($this->server);
}
}
diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php
index befeffed0..ed9694536 100644
--- a/app/Jobs/DatabaseBackupJob.php
+++ b/app/Jobs/DatabaseBackupJob.php
@@ -289,7 +289,7 @@ public function handle(): void
if ($this->backup->save_s3) {
$this->upload_to_s3();
}
- $this->team?->notify(new BackupSuccess($this->backup, $this->database));
+ $this->team?->notify(new BackupSuccess($this->backup, $this->database, $database));
$this->backup_log->update([
'status' => 'success',
'message' => $this->backup_output,
@@ -305,8 +305,7 @@ public function handle(): void
]);
}
send_internal_notification('DatabaseBackupJob failed with: ' . $e->getMessage());
- $this->team?->notify(new BackupFailed($this->backup, $this->database, $this->backup_output));
- throw $e;
+ $this->team?->notify(new BackupFailed($this->backup, $this->database, $this->backup_output, $database));
}
}
} catch (\Throwable $e) {
@@ -319,10 +318,15 @@ public function handle(): void
private function backup_standalone_mongodb(string $databaseWithCollections): void
{
try {
+ ray($this->database->toArray());
$url = $this->database->get_db_url(useInternal: true);
if ($databaseWithCollections === 'all') {
$commands[] = "mkdir -p " . $this->backup_dir;
- $commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --gzip --archive > $this->backup_location";
+ if (str($this->database->image)->startsWith('mongo:4.0')) {
+ $commands[] = "docker exec $this->container_name mongodump --uri=$url --gzip --archive > $this->backup_location";
+ } else {
+ $commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --gzip --archive > $this->backup_location";
+ }
} else {
if (str($databaseWithCollections)->contains(':')) {
$databaseName = str($databaseWithCollections)->before(':');
@@ -333,9 +337,17 @@ private function backup_standalone_mongodb(string $databaseWithCollections): voi
}
$commands[] = "mkdir -p " . $this->backup_dir;
if ($collectionsToExclude->count() === 0) {
- $commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --db $databaseName --gzip --archive > $this->backup_location";
+ if (str($this->database->image)->startsWith('mongo:4.0')) {
+ $commands[] = "docker exec $this->container_name mongodump --uri=$url --gzip --archive > $this->backup_location";
+ } else {
+ $commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --db $databaseName --gzip --archive > $this->backup_location";
+ }
} else {
- $commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --db $databaseName --gzip --excludeCollection " . $collectionsToExclude->implode(' --excludeCollection ') . " --archive > $this->backup_location";
+ if (str($this->database->image)->startsWith('mongo:4.0')) {
+ $commands[] = "docker exec $this->container_name mongodump --uri=$url --gzip --excludeCollection " . $collectionsToExclude->implode(' --excludeCollection ') . " --archive > $this->backup_location";
+ } else {
+ $commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --db $databaseName --gzip --excludeCollection " . $collectionsToExclude->implode(' --excludeCollection ') . " --archive > $this->backup_location";
+ }
}
}
$this->backup_output = instant_remote_process($commands, $this->server);
diff --git a/app/Jobs/InstanceAutoUpdateJob.php b/app/Jobs/InstanceAutoUpdateJob.php
index fa5c29421..dc35aa2b1 100644
--- a/app/Jobs/InstanceAutoUpdateJob.php
+++ b/app/Jobs/InstanceAutoUpdateJob.php
@@ -15,7 +15,8 @@ class InstanceAutoUpdateJob implements ShouldQueue, ShouldBeUnique, ShouldBeEncr
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
- public $timeout = 120;
+ public $timeout = 600;
+ public $tries = 1;
public function __construct(private bool $force = false)
{
diff --git a/app/Jobs/PullSentinelImageJob.php b/app/Jobs/PullSentinelImageJob.php
new file mode 100644
index 000000000..1c51928f6
--- /dev/null
+++ b/app/Jobs/PullSentinelImageJob.php
@@ -0,0 +1,56 @@
+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;
+ }
+ }
+}
diff --git a/app/Jobs/ScheduledTaskJob.php b/app/Jobs/ScheduledTaskJob.php
index 4a38a005b..a28f85901 100644
--- a/app/Jobs/ScheduledTaskJob.php
+++ b/app/Jobs/ScheduledTaskJob.php
@@ -8,14 +8,13 @@
use App\Models\Application;
use App\Models\Service;
use App\Models\Team;
+use App\Notifications\ScheduledTask\TaskFailed;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
-use Illuminate\Support\Collection;
-use Throwable;
class ScheduledTaskJob implements ShouldQueue
{
@@ -77,8 +76,12 @@ public function handle(): void
$this->containers[] = data_get($application, 'name') . '-' . data_get($this->resource, 'uuid');
}
});
+ $this->resource->databases()->get()->each(function ($database) {
+ if (str(data_get($database, 'status'))->contains('running')) {
+ $this->containers[] = data_get($database, 'name') . '-' . data_get($this->resource, 'uuid');
+ }
+ });
}
-
if (count($this->containers) == 0) {
throw new \Exception('ScheduledTaskJob failed: No containers running.');
}
@@ -89,7 +92,7 @@ public function handle(): void
foreach ($this->containers as $containerName) {
if (count($this->containers) == 1 || str_starts_with($containerName, $this->task->container . '-' . $this->resource->uuid)) {
- $cmd = 'sh -c "' . str_replace('"', '\"', $this->task->command) . '"';
+ $cmd = "sh -c '" . str_replace("'", "'\''", $this->task->command) . "'";
$exec = "docker exec {$containerName} {$cmd}";
$this->task_output = instant_remote_process([$exec], $this->server, true);
$this->task_log->update([
@@ -110,6 +113,7 @@ public function handle(): void
'message' => $this->task_output ?? $e->getMessage(),
]);
}
+ $this->team?->notify(new TaskFailed($this->task, $e->getMessage()));
// send_internal_notification('ScheduledTaskJob failed with: ' . $e->getMessage());
throw $e;
}
diff --git a/app/Jobs/SendMessageToDiscordJob.php b/app/Jobs/SendMessageToDiscordJob.php
index b9255baaa..ddd6bd271 100644
--- a/app/Jobs/SendMessageToDiscordJob.php
+++ b/app/Jobs/SendMessageToDiscordJob.php
@@ -20,11 +20,12 @@ class SendMessageToDiscordJob implements ShouldQueue, ShouldBeEncrypted
* @var int
*/
public $tries = 5;
+ public $backoff = 10;
/**
* The maximum number of unhandled exceptions to allow before failing.
*/
- public int $maxExceptions = 3;
+ public int $maxExceptions = 5;
public function __construct(
public string $text,
@@ -40,7 +41,6 @@ public function handle(): void
$payload = [
'content' => $this->text,
];
- ray($payload);
Http::post($this->webhookUrl, $payload);
}
}
diff --git a/app/Jobs/SendMessageToTelegramJob.php b/app/Jobs/SendMessageToTelegramJob.php
index 4785da669..4191b02fe 100644
--- a/app/Jobs/SendMessageToTelegramJob.php
+++ b/app/Jobs/SendMessageToTelegramJob.php
@@ -57,7 +57,7 @@ public function handle(): void
}
}
$payload = [
- 'parse_mode' => 'markdown',
+ // 'parse_mode' => 'markdown',
'reply_markup' => json_encode([
'inline_keyboard' => [
[...$inlineButtons],
diff --git a/app/Jobs/ServerLimitCheckJob.php b/app/Jobs/ServerLimitCheckJob.php
index 052260895..9d0e5db94 100644
--- a/app/Jobs/ServerLimitCheckJob.php
+++ b/app/Jobs/ServerLimitCheckJob.php
@@ -40,7 +40,7 @@ public function handle()
try {
$servers = $this->team->servers;
$servers_count = $servers->count();
- $limit = $this->team->limits['serverLimit'];
+ $limit = data_get($this->team->limits, 'serverLimit', 2);
$number_of_servers_to_disable = $servers_count - $limit;
ray('ServerLimitCheckJob', $this->team->uuid, $servers_count, $limit, $number_of_servers_to_disable);
if ($number_of_servers_to_disable > 0) {
diff --git a/app/Jobs/ServerStatusJob.php b/app/Jobs/ServerStatusJob.php
index 31683d097..449ab85a0 100644
--- a/app/Jobs/ServerStatusJob.php
+++ b/app/Jobs/ServerStatusJob.php
@@ -17,7 +17,7 @@ class ServerStatusJob implements ShouldQueue, ShouldBeEncrypted
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int|string|null $disk_usage = null;
- public $tries = 4;
+ public $tries = 3;
public function backoff(): int
{
return isDev() ? 1 : 3;
@@ -43,6 +43,10 @@ public function handle()
try {
if ($this->server->isFunctional()) {
$this->cleanup(notify: false);
+ $this->removeCoolifyYaml();
+ if (config('coolify.is_sentinel_enabled')) {
+ $this->server->checkSentinel();
+ }
}
} catch (\Throwable $e) {
send_internal_notification('ServerStatusJob failed with: ' . $e->getMessage());
@@ -50,6 +54,16 @@ public function handle()
return handleError($e);
}
}
+ private function removeCoolifyYaml()
+ {
+ // This will remote the coolify.yaml file from the server as it is not needed on cloud servers
+ if (isCloud() && $this->server->id !== 0) {
+ $file = $this->server->proxyPath() . "/dynamic/coolify.yaml";
+ return instant_remote_process([
+ "rm -f $file",
+ ], $this->server, false);
+ }
+ }
public function cleanup(bool $notify = false): void
{
$this->disk_usage = $this->server->getDiskUsage();
diff --git a/app/Livewire/Boarding/Index.php b/app/Livewire/Boarding/Index.php
index 2681b69e0..8f4e87090 100644
--- a/app/Livewire/Boarding/Index.php
+++ b/app/Livewire/Boarding/Index.php
@@ -52,6 +52,9 @@ class Index extends Component
public function mount()
{
+ if (auth()->user()?->isMember() && auth()->user()->currentTeam()->show_boarding === true) {
+ return redirect()->route('dashboard');
+ }
$this->privateKeyName = generate_random_name();
$this->remoteServerName = generate_random_name();
if (isDev()) {
diff --git a/app/Livewire/Notifications/Discord.php b/app/Livewire/Notifications/Discord.php
index 8aad8ccf0..88705437b 100644
--- a/app/Livewire/Notifications/Discord.php
+++ b/app/Livewire/Notifications/Discord.php
@@ -16,6 +16,7 @@ class Discord extends Component
'team.discord_notifications_deployments' => 'nullable|boolean',
'team.discord_notifications_status_changes' => 'nullable|boolean',
'team.discord_notifications_database_backups' => 'nullable|boolean',
+ 'team.discord_notifications_scheduled_tasks' => 'nullable|boolean',
];
protected $validationAttributes = [
'team.discord_webhook_url' => 'Discord Webhook',
diff --git a/app/Livewire/Notifications/Email.php b/app/Livewire/Notifications/Email.php
index 343cbda3e..6ef9b2255 100644
--- a/app/Livewire/Notifications/Email.php
+++ b/app/Livewire/Notifications/Email.php
@@ -28,6 +28,7 @@ class Email extends Component
'team.smtp_notifications_deployments' => 'nullable|boolean',
'team.smtp_notifications_status_changes' => 'nullable|boolean',
'team.smtp_notifications_database_backups' => 'nullable|boolean',
+ 'team.smtp_notifications_scheduled_tasks' => 'nullable|boolean',
'team.use_instance_email_settings' => 'boolean',
'team.resend_enabled' => 'nullable|boolean',
'team.resend_api_key' => 'nullable',
diff --git a/app/Livewire/Notifications/Telegram.php b/app/Livewire/Notifications/Telegram.php
index 35b868527..685c9e8eb 100644
--- a/app/Livewire/Notifications/Telegram.php
+++ b/app/Livewire/Notifications/Telegram.php
@@ -18,10 +18,12 @@ class Telegram extends Component
'team.telegram_notifications_deployments' => 'nullable|boolean',
'team.telegram_notifications_status_changes' => 'nullable|boolean',
'team.telegram_notifications_database_backups' => 'nullable|boolean',
+ 'team.telegram_notifications_scheduled_tasks' => 'nullable|boolean',
'team.telegram_notifications_test_message_thread_id' => 'nullable|string',
'team.telegram_notifications_deployments_message_thread_id' => 'nullable|string',
'team.telegram_notifications_status_changes_message_thread_id' => 'nullable|string',
'team.telegram_notifications_database_backups_message_thread_id' => 'nullable|string',
+ 'team.telegram_notifications_scheduled_tasks_thread_id' => 'nullable|string',
];
protected $validationAttributes = [
'team.telegram_token' => 'Token',
diff --git a/app/Livewire/Project/Application/Advanced.php b/app/Livewire/Project/Application/Advanced.php
index d35867e8f..45cb57ee3 100644
--- a/app/Livewire/Project/Application/Advanced.php
+++ b/app/Livewire/Project/Application/Advanced.php
@@ -21,6 +21,7 @@ class Advanced extends Component
'application.settings.is_gpu_enabled' => 'boolean|required',
'application.settings.is_build_server_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_stripprefix_enabled' => 'boolean|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.connect_to_docker_network' => 'boolean|required',
];
- public function mount() {
+ public function mount()
+ {
$this->is_force_https_enabled = $this->application->isForceHttpsEnabled();
$this->is_gzip_enabled = $this->application->isGzipEnabled();
$this->is_stripprefix_enabled = $this->application->isStripprefixEnabled();
@@ -65,7 +67,8 @@ public function instantSave()
$this->dispatch('success', 'Settings saved.');
$this->dispatch('configurationChanged');
}
- public function submit() {
+ public function submit()
+ {
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->application->settings->gpu_count = null;
@@ -76,6 +79,16 @@ public function submit() {
$this->application->settings->save();
$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()
{
return view('livewire.project.application.advanced');
diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php
index 6926e52cb..718312d2d 100644
--- a/app/Livewire/Project/Application/General.php
+++ b/app/Livewire/Project/Application/General.php
@@ -22,6 +22,7 @@ class General extends Component
public ?string $git_commit_sha = null;
public string $build_pack;
public ?string $ports_exposes = null;
+ public bool $is_container_label_escape_enabled = true;
public $customLabels;
public bool $labelsChanged = false;
@@ -30,7 +31,7 @@ class General extends Component
public ?string $initialDockerComposeLocation = null;
public ?string $initialDockerComposePrLocation = null;
- public $parsedServices = [];
+ public null|Collection $parsedServices;
public $parsedServiceDomains = [];
protected $listeners = [
@@ -74,6 +75,7 @@ class General extends Component
'application.post_deployment_command_container' => 'nullable',
'application.settings.is_static' => 'boolean|required',
'application.settings.is_build_server_enabled' => 'boolean|required',
+ 'application.settings.is_container_label_escape_enabled' => 'boolean|required',
'application.watch_paths' => 'nullable',
];
protected $validationAttributes = [
@@ -109,12 +111,17 @@ class General extends Component
'application.docker_compose_custom_build_command' => 'Docker compose custom build command',
'application.settings.is_static' => 'Is static',
'application.settings.is_build_server_enabled' => 'Is build server enabled',
+ 'application.settings.is_container_label_escape_enabled' => 'Is container label escape enabled',
'application.watch_paths' => 'Watch paths',
];
public function mount()
{
try {
$this->parsedServices = $this->application->parseCompose();
+ if (is_null($this->parsedServices) || empty($this->parsedServices)) {
+ $this->dispatch('error', "Failed to parse your docker-compose file. Please check the syntax and try again.");
+ return;
+ }
} catch (\Throwable $e) {
$this->dispatch('error', $e->getMessage());
}
@@ -124,6 +131,7 @@ public function mount()
}
$this->parsedServiceDomains = $this->application->docker_compose_domains ? json_decode($this->application->docker_compose_domains, true) : [];
$this->ports_exposes = $this->application->ports_exposes;
+ $this->is_container_label_escape_enabled = $this->application->settings->is_container_label_escape_enabled;
$this->customLabels = $this->application->parseContainerLabels();
if (!$this->customLabels && $this->application->destination->server->proxyType() !== 'NONE') {
$this->customLabels = str(implode("|", generateLabelsApplication($this->application)))->replace("|", "\n");
@@ -145,7 +153,7 @@ public function instantSave()
$this->application->settings->save();
$this->dispatch('success', 'Settings saved.');
$this->application->refresh();
- if ($this->ports_exposes !== $this->application->ports_exposes) {
+ if ($this->ports_exposes !== $this->application->ports_exposes || $this->is_container_label_escape_enabled !== $this->application->settings->is_container_label_escape_enabled) {
$this->resetDefaultLabels(false);
}
}
@@ -156,6 +164,10 @@ public function loadComposeFile($isInit = false)
return;
}
['parsedServices' => $this->parsedServices, 'initialDockerComposeLocation' => $this->initialDockerComposeLocation, 'initialDockerComposePrLocation' => $this->initialDockerComposePrLocation] = $this->application->loadComposeFile($isInit);
+ if (is_null($this->parsedServices)) {
+ $this->dispatch('error', "Failed to parse your docker-compose file. Please check the syntax and try again.");
+ return;
+ }
$compose = $this->application->parseCompose();
$services = data_get($compose, 'services');
if ($services) {
@@ -186,6 +198,7 @@ public function loadComposeFile($isInit = false)
$this->dispatch('success', 'Docker compose file loaded.');
$this->dispatch('compose_loaded');
$this->dispatch('refresh_storages');
+ $this->dispatch('refreshEnvs');
} catch (\Throwable $e) {
$this->application->docker_compose_location = $this->initialDockerComposeLocation;
$this->application->docker_compose_pr_location = $this->initialDockerComposePrLocation;
@@ -203,6 +216,9 @@ public function generateDomain(string $serviceName)
$this->application->docker_compose_domains = json_encode($this->parsedServiceDomains);
$this->application->save();
$this->dispatch('success', 'Domain generated.');
+ if ($this->application->build_pack === 'dockercompose') {
+ $this->loadComposeFile();
+ }
return $domain;
}
public function updatedApplicationBaseDirectory()
@@ -254,12 +270,14 @@ public function getWildcardDomain()
}
public function resetDefaultLabels()
{
- ray('resetDefaultLabels');
$this->customLabels = str(implode("|", generateLabelsApplication($this->application)))->replace("|", "\n");
$this->ports_exposes = $this->application->ports_exposes;
-
+ $this->is_container_label_escape_enabled = $this->application->settings->is_container_label_escape_enabled;
$this->application->custom_labels = base64_encode($this->customLabels);
$this->application->save();
+ if ($this->application->build_pack === 'dockercompose') {
+ $this->loadComposeFile();
+ }
}
public function checkFqdns($showToaster = true)
@@ -298,10 +316,13 @@ public function submit($showToaster = true)
}
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();
- if ($this->ports_exposes !== $this->application->ports_exposes) {
+ if ($this->ports_exposes !== $this->application->ports_exposes || $this->is_container_label_escape_enabled !== $this->application->settings->is_container_label_escape_enabled) {
$this->resetDefaultLabels();
}
if (data_get($this->application, 'build_pack') === 'dockerimage') {
diff --git a/app/Livewire/Project/Application/Heading.php b/app/Livewire/Project/Application/Heading.php
index 0717a51f0..619be693d 100644
--- a/app/Livewire/Project/Application/Heading.php
+++ b/app/Livewire/Project/Application/Heading.php
@@ -3,8 +3,8 @@
namespace App\Livewire\Project\Application;
use App\Actions\Application\StopApplication;
+use App\Actions\Docker\GetContainersStatus;
use App\Events\ApplicationStatusChanged;
-use App\Jobs\ComplexContainerStatusJob;
use App\Jobs\ContainerStatusJob;
use App\Jobs\ServerStatusJob;
use App\Models\Application;
@@ -14,6 +14,8 @@
class Heading extends Component
{
public Application $application;
+ public ?string $lastDeploymentInfo = null;
+ public ?string $lastDeploymentLink = null;
public array $parameters;
protected string $deploymentUuid;
@@ -28,18 +30,23 @@ public function getListeners()
public function mount()
{
$this->parameters = get_route_parameters();
+ $lastDeployment = $this->application->get_last_successful_deployment();
+ $this->lastDeploymentInfo = data_get_str($lastDeployment, 'commit')->limit(7) . ' ' . data_get($lastDeployment, 'commit_message');
+ $this->lastDeploymentLink = $this->application->gitCommitLink(data_get($lastDeployment, 'commit'));
}
public function check_status($showNotification = false)
{
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 {
dispatch(new ServerStatusJob($this->application->destination->server));
}
if ($showNotification) $this->dispatch('success', "Success", "Application status updated.");
- $this->dispatch('configurationChanged');
+ // Removed because it caused flickering
+ // $this->dispatch('configurationChanged');
}
public function force_deploy_without_cache()
diff --git a/app/Livewire/Project/Database/Backup/Execution.php b/app/Livewire/Project/Database/Backup/Execution.php
index 1f790d643..000b6fb2b 100644
--- a/app/Livewire/Project/Database/Backup/Execution.php
+++ b/app/Livewire/Project/Database/Backup/Execution.php
@@ -35,11 +35,6 @@ public function mount()
$this->executions = $executions;
$this->s3s = currentTeam()->s3s;
}
- public function cleanupFailed()
- {
- $this->backup->executions()->where('status', 'failed')->delete();
- $this->dispatch('refreshBackupExecutions');
- }
public function render()
{
return view('livewire.project.database.backup.execution');
diff --git a/app/Livewire/Project/Database/BackupEdit.php b/app/Livewire/Project/Database/BackupEdit.php
index f5f476257..d7f7f5503 100644
--- a/app/Livewire/Project/Database/BackupEdit.php
+++ b/app/Livewire/Project/Database/BackupEdit.php
@@ -35,7 +35,7 @@ class BackupEdit extends Component
public function mount()
{
$this->parameters = get_route_parameters();
- if (is_null($this->backup->s3_storage_id)) {
+ if (is_null(data_get($this->backup, 's3_storage_id'))) {
$this->backup->s3_storage_id = 'default';
}
}
diff --git a/app/Livewire/Project/Database/BackupExecutions.php b/app/Livewire/Project/Database/BackupExecutions.php
index b127a685c..101bb4593 100644
--- a/app/Livewire/Project/Database/BackupExecutions.php
+++ b/app/Livewire/Project/Database/BackupExecutions.php
@@ -2,9 +2,7 @@
namespace App\Livewire\Project\Database;
-use Illuminate\Support\Facades\Storage;
use Livewire\Component;
-use Symfony\Component\HttpFoundation\StreamedResponse;
class BackupExecutions extends Component
{
@@ -16,11 +14,15 @@ public function getListeners()
$userId = auth()->user()->id;
return [
"echo-private:team.{$userId},BackupCreated" => 'refreshBackupExecutions',
- "refreshBackupExecutions",
"deleteBackup"
];
}
+ public function cleanupFailed()
+ {
+ $this->backup?->executions()->where('status', 'failed')->delete();
+ $this->refreshBackupExecutions();
+ }
public function deleteBackup($exeuctionId)
{
$execution = $this->backup->executions()->where('id', $exeuctionId)->first();
diff --git a/app/Livewire/Project/Database/Heading.php b/app/Livewire/Project/Database/Heading.php
index 960ff2689..d6a0fe087 100644
--- a/app/Livewire/Project/Database/Heading.php
+++ b/app/Livewire/Project/Database/Heading.php
@@ -11,6 +11,7 @@
use App\Actions\Database\StartPostgresql;
use App\Actions\Database\StartRedis;
use App\Actions\Database\StopDatabase;
+use App\Actions\Docker\GetContainersStatus;
use App\Jobs\ContainerStatusJob;
use Livewire\Component;
@@ -44,7 +45,8 @@ public function activityFinished()
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();
if ($showNotification) $this->dispatch('success', 'Database status updated.');
}
diff --git a/app/Livewire/Project/Database/Import.php b/app/Livewire/Project/Database/Import.php
index 74e41056a..d435289fa 100644
--- a/app/Livewire/Project/Database/Import.php
+++ b/app/Livewire/Project/Database/Import.php
@@ -27,6 +27,7 @@ class Import extends Component
public string $postgresqlRestoreCommand = 'pg_restore -U $POSTGRES_USER -d $POSTGRES_DB';
public string $mysqlRestoreCommand = 'mysql -u $MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE';
public string $mariadbRestoreCommand = 'mariadb -u $MARIADB_USER -p$MARIADB_PASSWORD $MARIADB_DATABASE';
+ public string $mongodbRestoreCommand = 'mongorestore --authenticationDatabase=admin --username $MONGO_INITDB_ROOT_USERNAME --password $MONGO_INITDB_ROOT_PASSWORD --uri mongodb://localhost:27017 --gzip --archive=';
public function getListeners()
{
@@ -62,8 +63,7 @@ public function getContainers()
$this->resource->getMorphClass() == 'App\Models\StandaloneRedis' ||
$this->resource->getMorphClass() == 'App\Models\StandaloneKeydb' ||
$this->resource->getMorphClass() == 'App\Models\StandaloneDragonfly' ||
- $this->resource->getMorphClass() == 'App\Models\StandaloneClickhouse' ||
- $this->resource->getMorphClass() == 'App\Models\StandaloneMongodb'
+ $this->resource->getMorphClass() == 'App\Models\StandaloneClickhouse'
) {
$this->unsupported = true;
}
@@ -101,6 +101,10 @@ public function runImport()
$this->importCommands[] = "docker exec {$this->container} sh -c '{$this->postgresqlRestoreCommand} {$tmpPath}'";
$this->importCommands[] = "rm {$tmpPath}";
break;
+ case 'App\Models\StandaloneMongodb':
+ $this->importCommands[] = "docker exec {$this->container} sh -c '{$this->mongodbRestoreCommand}{$tmpPath}'";
+ $this->importCommands[] = "rm {$tmpPath}";
+ break;
}
$this->importCommands[] = "docker exec {$this->container} sh -c 'rm {$tmpPath}'";
diff --git a/app/Livewire/Project/New/GithubPrivateRepository.php b/app/Livewire/Project/New/GithubPrivateRepository.php
index 322fd4a4e..58e3fe586 100644
--- a/app/Livewire/Project/New/GithubPrivateRepository.php
+++ b/app/Livewire/Project/New/GithubPrivateRepository.php
@@ -150,7 +150,7 @@ public function submit()
'repository_project_id' => $this->selected_repository_id,
'git_repository' => "{$this->selected_repository_owner}/{$this->selected_repository_repo}",
'git_branch' => $this->selected_branch_name,
- 'build_pack' => 'nixpacks',
+ 'build_pack' => $this->build_pack,
'ports_exposes' => $this->port,
'publish_directory' => $this->publish_directory,
'environment_id' => $environment->id,
@@ -162,6 +162,9 @@ public function submit()
$application->settings->is_static = $this->is_static;
$application->settings->save();
+ if ($this->build_pack === 'dockerfile' || $this->build_pack === 'dockerimage') {
+ $application->health_check_enabled = false;
+ }
$fqdn = generateFqdn($destination->server, $application->uuid);
$application->fqdn = $fqdn;
diff --git a/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php b/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php
index ad52b9070..691b246fd 100644
--- a/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php
+++ b/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php
@@ -19,7 +19,7 @@ class GithubPrivateRepositoryDeployKey extends Component
public $current_step = 'private_keys';
public $parameters;
public $query;
- public $private_keys =[];
+ public $private_keys = [];
public int $private_key_id;
public int $port = 3000;
@@ -125,7 +125,7 @@ public function submit()
'name' => generate_random_name(),
'git_repository' => $this->git_repository,
'git_branch' => $this->branch,
- 'build_pack' => 'nixpacks',
+ 'build_pack' => $this->build_pack,
'ports_exposes' => $this->port,
'publish_directory' => $this->publish_directory,
'environment_id' => $environment->id,
@@ -138,7 +138,7 @@ public function submit()
'name' => generate_random_name(),
'git_repository' => $this->git_repository,
'git_branch' => $this->branch,
- 'build_pack' => 'nixpacks',
+ 'build_pack' => $this->build_pack,
'ports_exposes' => $this->port,
'publish_directory' => $this->publish_directory,
'environment_id' => $environment->id,
@@ -149,7 +149,9 @@ public function submit()
'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->settings->is_static = $this->is_static;
$application->settings->save();
diff --git a/app/Livewire/Project/New/PublicGitRepository.php b/app/Livewire/Project/New/PublicGitRepository.php
index 8bbb5b052..f4f3008d4 100644
--- a/app/Livewire/Project/New/PublicGitRepository.php
+++ b/app/Livewire/Project/New/PublicGitRepository.php
@@ -94,6 +94,18 @@ public function load_branch()
$repository = str($this->repository_url)->after(':')->before('.git');
$this->repository_url = 'https://' . str($github_instance) . '/' . $repository;
}
+ if (
+ (str($this->repository_url)->startsWith('https://') ||
+ str($this->repository_url)->startsWith('http://')) &&
+ !str($this->repository_url)->endsWith('.git') &&
+ (!str($this->repository_url)->contains('github.com') ||
+ !str($this->repository_url)->contains('git.sr.ht'))
+ ) {
+ $this->repository_url = $this->repository_url . '.git';
+ }
+ if (str($this->repository_url)->contains('github.com')) {
+ $this->repository_url = str($this->repository_url)->before('.git')->value();
+ }
} catch (\Throwable $e) {
return handleError($e, $this);
}
@@ -170,7 +182,6 @@ public function submit()
'name' => generate_random_name(),
'git_repository' => $this->git_repository,
'git_branch' => $this->git_branch,
- 'build_pack' => 'nixpacks',
'ports_exposes' => $this->port,
'publish_directory' => $this->publish_directory,
'environment_id' => $environment->id,
@@ -183,7 +194,6 @@ public function submit()
'name' => generate_application_name($this->git_repository, $this->git_branch),
'git_repository' => $this->git_repository,
'git_branch' => $this->git_branch,
- 'build_pack' => 'nixpacks',
'ports_exposes' => $this->port,
'publish_directory' => $this->publish_directory,
'environment_id' => $environment->id,
@@ -195,7 +205,9 @@ public function submit()
];
}
-
+ if ($this->build_pack === 'dockerfile' || $this->build_pack === 'dockerimage') {
+ $application_init['health_check_enabled'] = false;
+ }
$application = Application::create($application_init);
$application->settings->is_static = $this->is_static;
diff --git a/app/Livewire/Project/New/SimpleDockerfile.php b/app/Livewire/Project/New/SimpleDockerfile.php
index 55b48041a..172403a1a 100644
--- a/app/Livewire/Project/New/SimpleDockerfile.php
+++ b/app/Livewire/Project/New/SimpleDockerfile.php
@@ -70,6 +70,8 @@ public function submit()
'fqdn' => $fqdn
]);
+ $application->parseHealthcheckFromDockerfile(dockerfile: collect(str($this->dockerfile)->trim()->explode("\n")), isInit: true);
+
return redirect()->route('project.application.configuration', [
'application_uuid' => $application->uuid,
'environment_name' => $environment->name,
diff --git a/app/Livewire/Project/Resource/Create.php b/app/Livewire/Project/Resource/Create.php
index 3705d6f93..8ea77950e 100644
--- a/app/Livewire/Project/Resource/Create.php
+++ b/app/Livewire/Project/Resource/Create.php
@@ -12,7 +12,6 @@ class Create extends Component
public $type;
public function mount()
{
- $services = getServiceTemplates();
$type = str(request()->query('type'));
$destination_uuid = request()->query('destination');
$server_id = request()->query('server_id');
@@ -25,83 +24,87 @@ public function mount()
if (!$environment) {
return redirect()->route('dashboard');
}
- if (in_array($type, DATABASE_TYPES)) {
- if ($type->value() === "postgresql") {
- $database = create_standalone_postgresql($environment->id, $destination_uuid);
- } else if ($type->value() === 'redis') {
- $database = create_standalone_redis($environment->id, $destination_uuid);
- } else if ($type->value() === 'mongodb') {
- $database = create_standalone_mongodb($environment->id, $destination_uuid);
- } else if ($type->value() === 'mysql') {
- $database = create_standalone_mysql($environment->id, $destination_uuid);
- } else if ($type->value() === 'mariadb') {
- $database = create_standalone_mariadb($environment->id, $destination_uuid);
- } else if ($type->value() === 'keydb') {
- $database = create_standalone_keydb($environment->id, $destination_uuid);
- } else if ($type->value() === 'dragonfly') {
- $database = create_standalone_dragonfly($environment->id, $destination_uuid);
- } else if ($type->value() === 'clickhouse') {
- $database = create_standalone_clickhouse($environment->id, $destination_uuid);
- }
- return redirect()->route('project.database.configuration', [
- 'project_uuid' => $project->uuid,
- 'environment_name' => $environment->name,
- 'database_uuid' => $database->uuid,
- ]);
- }
- if ($type->startsWith('one-click-service-') && !is_null((int)$server_id)) {
- $oneClickServiceName = $type->after('one-click-service-')->value();
- $oneClickService = data_get($services, "$oneClickServiceName.compose");
- $oneClickDotEnvs = data_get($services, "$oneClickServiceName.envs", null);
- if ($oneClickDotEnvs) {
- $oneClickDotEnvs = str(base64_decode($oneClickDotEnvs))->split('/\r\n|\r|\n/')->filter(function ($value) {
- return !empty($value);
- });
- }
- if ($oneClickService) {
- $destination = StandaloneDocker::whereUuid($destination_uuid)->first();
- $service_payload = [
- 'name' => "$oneClickServiceName-" . str()->random(10),
- 'docker_compose_raw' => base64_decode($oneClickService),
- 'environment_id' => $environment->id,
- 'service_type' => $oneClickServiceName,
- 'server_id' => (int) $server_id,
- 'destination_id' => $destination->id,
- 'destination_type' => $destination->getMorphClass(),
- ];
- if ($oneClickServiceName === 'cloudflared') {
- data_set($service_payload, 'connect_to_docker_network', true);
+ if (isset($type) && isset($destination_uuid) && isset($server_id)) {
+ $services = getServiceTemplates();
+
+ if (in_array($type, DATABASE_TYPES)) {
+ if ($type->value() === "postgresql") {
+ $database = create_standalone_postgresql($environment->id, $destination_uuid);
+ } else if ($type->value() === 'redis') {
+ $database = create_standalone_redis($environment->id, $destination_uuid);
+ } else if ($type->value() === 'mongodb') {
+ $database = create_standalone_mongodb($environment->id, $destination_uuid);
+ } else if ($type->value() === 'mysql') {
+ $database = create_standalone_mysql($environment->id, $destination_uuid);
+ } else if ($type->value() === 'mariadb') {
+ $database = create_standalone_mariadb($environment->id, $destination_uuid);
+ } else if ($type->value() === 'keydb') {
+ $database = create_standalone_keydb($environment->id, $destination_uuid);
+ } else if ($type->value() === 'dragonfly') {
+ $database = create_standalone_dragonfly($environment->id, $destination_uuid);
+ } else if ($type->value() === 'clickhouse') {
+ $database = create_standalone_clickhouse($environment->id, $destination_uuid);
}
- $service = Service::create($service_payload);
- $service->name = "$oneClickServiceName-" . $service->uuid;
- $service->save();
- if ($oneClickDotEnvs?->count() > 0) {
- $oneClickDotEnvs->each(function ($value) use ($service) {
- $key = str()->before($value, '=');
- $value = str(str()->after($value, '='));
- $generatedValue = $value;
- if ($value->contains('SERVICE_')) {
- $command = $value->after('SERVICE_')->beforeLast('_');
- $generatedValue = generateEnvValue($command->value(), $service);
- }
- EnvironmentVariable::create([
- 'key' => $key,
- 'value' => $generatedValue,
- 'service_id' => $service->id,
- 'is_build_time' => false,
- 'is_preview' => false,
- ]);
- });
- }
- $service->parse(isNew: true);
- return redirect()->route('project.service.configuration', [
- 'service_uuid' => $service->uuid,
- 'environment_name' => $environment->name,
+ return redirect()->route('project.database.configuration', [
'project_uuid' => $project->uuid,
+ 'environment_name' => $environment->name,
+ 'database_uuid' => $database->uuid,
]);
}
+ if ($type->startsWith('one-click-service-') && !is_null((int)$server_id)) {
+ $oneClickServiceName = $type->after('one-click-service-')->value();
+ $oneClickService = data_get($services, "$oneClickServiceName.compose");
+ $oneClickDotEnvs = data_get($services, "$oneClickServiceName.envs", null);
+ if ($oneClickDotEnvs) {
+ $oneClickDotEnvs = str(base64_decode($oneClickDotEnvs))->split('/\r\n|\r|\n/')->filter(function ($value) {
+ return !empty($value);
+ });
+ }
+ if ($oneClickService) {
+ $destination = StandaloneDocker::whereUuid($destination_uuid)->first();
+ $service_payload = [
+ 'name' => "$oneClickServiceName-" . str()->random(10),
+ 'docker_compose_raw' => base64_decode($oneClickService),
+ 'environment_id' => $environment->id,
+ 'service_type' => $oneClickServiceName,
+ 'server_id' => (int) $server_id,
+ 'destination_id' => $destination->id,
+ 'destination_type' => $destination->getMorphClass(),
+ ];
+ if ($oneClickServiceName === 'cloudflared') {
+ data_set($service_payload, 'connect_to_docker_network', true);
+ }
+ $service = Service::create($service_payload);
+ $service->name = "$oneClickServiceName-" . $service->uuid;
+ $service->save();
+ if ($oneClickDotEnvs?->count() > 0) {
+ $oneClickDotEnvs->each(function ($value) use ($service) {
+ $key = str()->before($value, '=');
+ $value = str(str()->after($value, '='));
+ $generatedValue = $value;
+ if ($value->contains('SERVICE_')) {
+ $command = $value->after('SERVICE_')->beforeLast('_');
+ $generatedValue = generateEnvValue($command->value(), $service);
+ }
+ EnvironmentVariable::create([
+ 'key' => $key,
+ 'value' => $generatedValue,
+ 'service_id' => $service->id,
+ 'is_build_time' => false,
+ 'is_preview' => false,
+ ]);
+ });
+ }
+ $service->parse(isNew: true);
+ return redirect()->route('project.service.configuration', [
+ 'service_uuid' => $service->uuid,
+ 'environment_name' => $environment->name,
+ 'project_uuid' => $project->uuid,
+ ]);
+ }
+ }
+ $this->type = $type->value();
}
- $this->type = $type->value();
}
public function render()
{
diff --git a/app/Livewire/Project/Service/Configuration.php b/app/Livewire/Project/Service/Configuration.php
index 2cbda4e02..86c9a8a31 100644
--- a/app/Livewire/Project/Service/Configuration.php
+++ b/app/Livewire/Project/Service/Configuration.php
@@ -2,7 +2,7 @@
namespace App\Livewire\Project\Service;
-use App\Jobs\ContainerStatusJob;
+use App\Actions\Docker\GetContainersStatus;
use App\Models\Service;
use Livewire\Component;
@@ -64,7 +64,8 @@ public function restartDatabase($id)
public function check_status()
{
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('updateStatus');
} catch (\Exception $e) {
diff --git a/app/Livewire/Project/Service/EditCompose.php b/app/Livewire/Project/Service/EditCompose.php
index 0f9c449f9..d6e867956 100644
--- a/app/Livewire/Project/Service/EditCompose.php
+++ b/app/Livewire/Project/Service/EditCompose.php
@@ -12,6 +12,7 @@ class EditCompose extends Component
protected $rules = [
'service.docker_compose_raw' => 'required',
'service.docker_compose' => 'required',
+ 'service.is_container_label_escape_enabled' => 'required',
];
public function mount()
{
@@ -23,6 +24,14 @@ public function saveEditedCompose()
$this->dispatch('info', "Saving new docker compose...");
$this->dispatch('saveCompose', $this->service->docker_compose_raw);
}
+ public function instantSave()
+ {
+ $this->validate([
+ 'service.is_container_label_escape_enabled' => 'required',
+ ]);
+ $this->service->save(['is_container_label_escape_enabled' => $this->service->is_container_label_escape_enabled]);
+ $this->dispatch('success', "Service updated successfully");
+ }
public function render()
{
return view('livewire.project.service.edit-compose');
diff --git a/app/Livewire/Project/Shared/Destination.php b/app/Livewire/Project/Shared/Destination.php
index fa19e8c42..2ccae47fd 100644
--- a/app/Livewire/Project/Shared/Destination.php
+++ b/app/Livewire/Project/Shared/Destination.php
@@ -3,6 +3,7 @@
namespace App\Livewire\Project\Shared;
use App\Actions\Application\StopApplicationOneServer;
+use App\Actions\Docker\GetContainersStatus;
use App\Events\ApplicationStatusChanged;
use App\Jobs\ContainerStatusJob;
use App\Models\Server;
@@ -90,7 +91,8 @@ public function promote(int $network_id, int $server_id)
}
public function refreshServers()
{
- ContainerStatusJob::dispatchSync($this->resource->destination->server);
+ GetContainersStatus::run($this->resource->destination->server);
+ // ContainerStatusJob::dispatchSync($this->resource->destination->server);
$this->loadData();
$this->dispatch('refresh');
ApplicationStatusChanged::dispatch(data_get($this->resource, 'environment.project.team.id'));
diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/All.php b/app/Livewire/Project/Shared/EnvironmentVariable/All.php
index 6a6d94142..561d20d19 100644
--- a/app/Livewire/Project/Shared/EnvironmentVariable/All.php
+++ b/app/Livewire/Project/Shared/EnvironmentVariable/All.php
@@ -5,11 +5,11 @@
use App\Models\EnvironmentVariable;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
-use Illuminate\Support\Str;
class All extends Component
{
public $resource;
+ public string $resourceClass;
public bool $showPreview = false;
public ?string $modalId = null;
public ?string $variables = null;
@@ -19,17 +19,44 @@ class All extends Component
'refreshEnvs',
'saveKey' => 'submit',
];
+ protected $rules = [
+ 'resource.settings.is_env_sorting_enabled' => 'required|boolean',
+ ];
+
public function mount()
{
- $resourceClass = get_class($this->resource);
+ $this->resourceClass = get_class($this->resource);
$resourceWithPreviews = ['App\Models\Application'];
$simpleDockerfile = !is_null(data_get($this->resource, 'dockerfile'));
- if (Str::of($resourceClass)->contains($resourceWithPreviews) && !$simpleDockerfile) {
+ if (str($this->resourceClass)->contains($resourceWithPreviews) && !$simpleDockerfile) {
$this->showPreview = true;
}
$this->modalId = new Cuid2(7);
+ $this->sortMe();
$this->getDevView();
}
+
+ public function sortMe()
+ {
+ if ($this->resourceClass === 'App\Models\Application' && data_get($this->resource, 'build_pack') !== 'dockercompose') {
+ if ($this->resource->settings->is_env_sorting_enabled) {
+ $this->resource->environment_variables = $this->resource->environment_variables->sortBy('key');
+ $this->resource->environment_variables_preview = $this->resource->environment_variables_preview->sortBy('key');
+ } else {
+ $this->resource->environment_variables = $this->resource->environment_variables->sortBy('id');
+ $this->resource->environment_variables_preview = $this->resource->environment_variables_preview->sortBy('id');
+ }
+ }
+ $this->getDevView();
+ }
+ public function instantSave()
+ {
+ if ($this->resourceClass === 'App\Models\Application' && data_get($this->resource, 'build_pack') !== 'dockercompose') {
+ $this->resource->settings->save();
+ $this->dispatch('success', 'Environment variable settings updated.');
+ $this->sortMe();
+ }
+ }
public function getDevView()
{
$this->variables = $this->resource->environment_variables->map(function ($item) {
@@ -40,7 +67,7 @@ public function getDevView()
return "$item->key=(multiline, edit in normal view)";
}
return "$item->key=$item->value";
- })->sort()->join('
+ })->join('
');
if ($this->showPreview) {
$this->variablesPreview = $this->resource->environment_variables_preview->map(function ($item) {
@@ -51,13 +78,18 @@ public function getDevView()
return "$item->key=(multiline, edit in normal view)";
}
return "$item->key=$item->value";
- })->sort()->join('
+ })->join('
');
}
}
public function switch()
{
- $this->view = $this->view === 'normal' ? 'dev' : 'normal';
+ if ($this->view === 'normal') {
+ $this->view = 'dev';
+ } else {
+ $this->view = 'normal';
+ }
+ $this->sortMe();
}
public function saveVariables($isPreview)
{
@@ -66,6 +98,7 @@ public function saveVariables($isPreview)
$this->resource->environment_variables_preview()->whereNotIn('key', array_keys($variables))->delete();
} else {
$variables = parseEnvFormatToArray($this->variables);
+ ray($variables, $this->variables);
$this->resource->environment_variables()->whereNotIn('key', array_keys($variables))->delete();
}
foreach ($variables as $key => $variable) {
diff --git a/app/Livewire/Project/Shared/ExecuteContainerCommand.php b/app/Livewire/Project/Shared/ExecuteContainerCommand.php
index 52d628dc1..4fc8bb8c6 100644
--- a/app/Livewire/Project/Shared/ExecuteContainerCommand.php
+++ b/app/Livewire/Project/Shared/ExecuteContainerCommand.php
@@ -122,7 +122,7 @@ public function runCommand()
if ($server->isForceDisabled()) {
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)) {
$exec = "docker exec -w {$this->workDir} {$container_name} {$cmd}";
} else {
diff --git a/app/Livewire/Project/Shared/GetLogs.php b/app/Livewire/Project/Shared/GetLogs.php
index 996131f37..e14cd6113 100644
--- a/app/Livewire/Project/Shared/GetLogs.php
+++ b/app/Livewire/Project/Shared/GetLogs.php
@@ -91,15 +91,35 @@ public function getLogs($refresh = false)
if ($this->container) {
if ($this->showTimeStamps) {
if ($this->server->isSwarm()) {
- $sshCommand = generateSshCommand($this->server, "docker service logs -n {$this->numberOfLines} -t {$this->container}");
+ $command = "docker service logs -n {$this->numberOfLines} -t {$this->container}";
+ if ($this->server->isNonRoot()) {
+ $command = parseCommandsByLineForSudo(collect($command), $this->server);
+ $command = $command[0];
+ }
+ $sshCommand = generateSshCommand($this->server, $command);
} else {
- $sshCommand = generateSshCommand($this->server, "docker logs -n {$this->numberOfLines} -t {$this->container}");
+ $command = "docker logs -n {$this->numberOfLines} -t {$this->container}";
+ if ($this->server->isNonRoot()) {
+ $command = parseCommandsByLineForSudo(collect($command), $this->server);
+ $command = $command[0];
+ }
+ $sshCommand = generateSshCommand($this->server, $command);
}
} else {
if ($this->server->isSwarm()) {
- $sshCommand = generateSshCommand($this->server, "docker service logs -n {$this->numberOfLines} {$this->container}");
+ $command = "docker service logs -n {$this->numberOfLines} {$this->container}";
+ if ($this->server->isNonRoot()) {
+ $command = parseCommandsByLineForSudo(collect($command), $this->server);
+ $command = $command[0];
+ }
+ $sshCommand = generateSshCommand($this->server, $command);
} else {
- $sshCommand = generateSshCommand($this->server, "docker logs -n {$this->numberOfLines} {$this->container}");
+ $command = "docker logs -n {$this->numberOfLines} {$this->container}";
+ if ($this->server->isNonRoot()) {
+ $command = parseCommandsByLineForSudo(collect($command), $this->server);
+ $command = $command[0];
+ }
+ $sshCommand = generateSshCommand($this->server, $command);
}
}
if ($refresh) {
diff --git a/app/Livewire/Project/Shared/HealthChecks.php b/app/Livewire/Project/Shared/HealthChecks.php
index 3bf507cab..56f5a2759 100644
--- a/app/Livewire/Project/Shared/HealthChecks.php
+++ b/app/Livewire/Project/Shared/HealthChecks.php
@@ -17,18 +17,17 @@ class HealthChecks extends Component
'resource.health_check_return_code' => 'integer',
'resource.health_check_scheme' => 'string',
'resource.health_check_response_text' => 'nullable|string',
- 'resource.health_check_interval' => 'integer',
- 'resource.health_check_timeout' => 'integer',
- 'resource.health_check_retries' => 'integer',
+ 'resource.health_check_interval' => 'integer|min:1',
+ 'resource.health_check_timeout' => 'integer|min:1',
+ 'resource.health_check_retries' => 'integer|min:1',
'resource.health_check_start_period' => 'integer',
+ 'resource.custom_healthcheck_found' => 'boolean',
];
public function instantSave()
{
$this->resource->save();
$this->dispatch('success', 'Health check updated.');
-
-
}
public function submit()
{
diff --git a/app/Livewire/Project/Shared/Logs.php b/app/Livewire/Project/Shared/Logs.php
index 68e4e193e..f1d70bf28 100644
--- a/app/Livewire/Project/Shared/Logs.php
+++ b/app/Livewire/Project/Shared/Logs.php
@@ -27,7 +27,7 @@ class Logs extends Component
public $query;
public $status;
public $serviceSubType;
-
+ public $cpu;
public function loadContainers($server_id)
{
try {
@@ -49,6 +49,14 @@ public function loadContainers($server_id)
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()
{
try {
@@ -95,6 +103,7 @@ public function mount()
}
}
$this->containers = $this->containers->sort();
+ $this->loadMetrics();
} catch (\Exception $e) {
return handleError($e, $this);
}
diff --git a/app/Livewire/Project/Shared/ScheduledTask/Add.php b/app/Livewire/Project/Shared/ScheduledTask/Add.php
index 3a7a3fa23..c415ff3e4 100644
--- a/app/Livewire/Project/Shared/ScheduledTask/Add.php
+++ b/app/Livewire/Project/Shared/ScheduledTask/Add.php
@@ -2,11 +2,14 @@
namespace App\Livewire\Project\Shared\ScheduledTask;
+use Illuminate\Support\Collection;
use Livewire\Component;
class Add extends Component
{
public $parameters;
+ public string $type;
+ public Collection $containerNames;
public string $name;
public string $command;
public string $frequency;
@@ -29,6 +32,9 @@ class Add extends Component
public function mount()
{
$this->parameters = get_route_parameters();
+ if ($this->containerNames->count() > 0) {
+ $this->container = $this->containerNames->first();
+ }
}
public function submit()
@@ -40,6 +46,11 @@ public function submit()
$this->dispatch('error', 'Invalid Cron / Human expression.');
return;
}
+ if (empty($this->container) || $this->container == 'null') {
+ if ($this->type == 'service') {
+ $this->container = $this->subServiceName;
+ }
+ }
$this->dispatch('saveScheduledTask', [
'name' => $this->name,
'command' => $this->command,
diff --git a/app/Livewire/Project/Shared/ScheduledTask/All.php b/app/Livewire/Project/Shared/ScheduledTask/All.php
index 975d695fa..e5ea66d13 100644
--- a/app/Livewire/Project/Shared/ScheduledTask/All.php
+++ b/app/Livewire/Project/Shared/ScheduledTask/All.php
@@ -3,14 +3,13 @@
namespace App\Livewire\Project\Shared\ScheduledTask;
use App\Models\ScheduledTask;
+use Illuminate\Support\Collection;
use Livewire\Component;
-use Visus\Cuid2\Cuid2;
-use Illuminate\Support\Str;
class All extends Component
{
public $resource;
- public string|null $modalId = null;
+ public Collection $containerNames;
public ?string $variables = null;
public array $parameters;
protected $listeners = ['refreshTasks', 'saveScheduledTask' => 'submit'];
@@ -18,7 +17,18 @@ class All extends Component
public function mount()
{
$this->parameters = get_route_parameters();
- $this->modalId = new Cuid2(7);
+ if ($this->resource->type() == 'service') {
+ $this->containerNames = $this->resource->applications()->pluck('name');
+ $this->containerNames = $this->containerNames->merge($this->resource->databases()->pluck('name'));
+ } elseif ($this->resource->type() == 'application') {
+ if ($this->resource->build_pack === 'dockercompose') {
+ $parsed = $this->resource->parseCompose();
+ $containers = collect(data_get($parsed,'services'))->keys();
+ $this->containerNames = $containers;
+ } else {
+ $this->containerNames = collect([]);
+ }
+ }
}
public function refreshTasks()
{
diff --git a/app/Livewire/Project/Shared/ScheduledTask/Show.php b/app/Livewire/Project/Shared/ScheduledTask/Show.php
index 87b752509..7490c7055 100644
--- a/app/Livewire/Project/Shared/ScheduledTask/Show.php
+++ b/app/Livewire/Project/Shared/ScheduledTask/Show.php
@@ -17,6 +17,7 @@ class Show extends Component
public string $type;
protected $rules = [
+ 'task.enabled' => 'required|boolean',
'task.name' => 'required|string',
'task.command' => 'required|string',
'task.frequency' => 'required|string',
@@ -45,9 +46,18 @@ public function mount()
$this->task = ModelsScheduledTask::where('uuid', request()->route('task_uuid'))->first();
}
+ public function instantSave()
+ {
+ $this->validateOnly('task.enabled');
+ $this->task->save(['enabled' => $this->task->enabled]);
+ $this->dispatch('success', 'Scheduled task updated.');
+ $this->dispatch('refreshTasks');
+ }
public function submit()
{
$this->validate();
+ $this->task->name = str($this->task->name)->trim()->value();
+ $this->task->container = str($this->task->container)->trim()->value();
$this->task->save();
$this->dispatch('success', 'Scheduled task updated.');
$this->dispatch('refreshTasks');
@@ -60,11 +70,9 @@ public function delete()
if ($this->type == 'application') {
return redirect()->route('project.application.configuration', $this->parameters);
- }
- else {
+ } else {
return redirect()->route('project.service.configuration', $this->parameters);
}
-
} catch (\Exception $e) {
return handleError($e);
}
diff --git a/app/Livewire/Server/Form.php b/app/Livewire/Server/Form.php
index 14a2809c7..c1dcd34ce 100644
--- a/app/Livewire/Server/Form.php
+++ b/app/Livewire/Server/Form.php
@@ -82,6 +82,7 @@ public function checkLocalhostConnection()
$this->server->settings->is_reachable = true;
$this->server->settings->is_usable = true;
$this->server->settings->save();
+ $this->dispatch('proxyStatusUpdated');
} else {
$this->dispatch('error', 'Server is not reachable.', 'Please validate your configuration and connection.
Check this documentation for further help.
Error: ' . $error);
return;
diff --git a/app/Livewire/Server/Proxy/Status.php b/app/Livewire/Server/Proxy/Status.php
index bd0ffe431..fbc16fde4 100644
--- a/app/Livewire/Server/Proxy/Status.php
+++ b/app/Livewire/Server/Proxy/Status.php
@@ -2,6 +2,7 @@
namespace App\Livewire\Server\Proxy;
+use App\Actions\Docker\GetContainersStatus;
use App\Actions\Proxy\CheckProxy;
use App\Jobs\ContainerStatusJob;
use App\Models\Server;
@@ -49,7 +50,8 @@ public function checkProxy(bool $notification = false)
public function getProxyStatus()
{
try {
- dispatch_sync(new ContainerStatusJob($this->server));
+ GetContainersStatus::run($this->server);
+ // dispatch_sync(new ContainerStatusJob($this->server));
$this->dispatch('proxyStatusUpdated');
} catch (\Throwable $e) {
return handleError($e, $this);
diff --git a/app/Livewire/Settings/Configuration.php b/app/Livewire/Settings/Configuration.php
index 5281a1e01..54dbe1bdb 100644
--- a/app/Livewire/Settings/Configuration.php
+++ b/app/Livewire/Settings/Configuration.php
@@ -13,7 +13,7 @@ class Configuration extends Component
public bool $is_auto_update_enabled;
public bool $is_registration_enabled;
public bool $is_dns_validation_enabled;
- public bool $next_channel;
+ // public bool $next_channel;
protected string $dynamic_config_path = '/data/coolify/proxy/dynamic';
protected Server $server;
@@ -37,7 +37,7 @@ public function mount()
$this->do_not_track = $this->settings->do_not_track;
$this->is_auto_update_enabled = $this->settings->is_auto_update_enabled;
$this->is_registration_enabled = $this->settings->is_registration_enabled;
- $this->next_channel = $this->settings->next_channel;
+ // $this->next_channel = $this->settings->next_channel;
$this->is_dns_validation_enabled = $this->settings->is_dns_validation_enabled;
}
@@ -47,12 +47,12 @@ public function instantSave()
$this->settings->is_auto_update_enabled = $this->is_auto_update_enabled;
$this->settings->is_registration_enabled = $this->is_registration_enabled;
$this->settings->is_dns_validation_enabled = $this->is_dns_validation_enabled;
- if ($this->next_channel) {
- $this->settings->next_channel = false;
- $this->next_channel = false;
- } else {
- $this->settings->next_channel = $this->next_channel;
- }
+ // if ($this->next_channel) {
+ // $this->settings->next_channel = false;
+ // $this->next_channel = false;
+ // } else {
+ // $this->settings->next_channel = $this->next_channel;
+ // }
$this->settings->save();
$this->dispatch('success', 'Settings updated!');
}
diff --git a/app/Livewire/Storage/Create.php b/app/Livewire/Storage/Create.php
index d1af807d5..1b2510f5d 100644
--- a/app/Livewire/Storage/Create.php
+++ b/app/Livewire/Storage/Create.php
@@ -16,13 +16,13 @@ class Create extends Component
public string $endpoint;
public S3Storage $storage;
protected $rules = [
- 'name' => 'nullable|min:3|max:255',
+ 'name' => 'required|min:3|max:255',
'description' => 'nullable|min:3|max:255',
'region' => 'required|max:255',
'key' => 'required|max:255',
'secret' => 'required|max:255',
'bucket' => 'required|max:255',
- 'endpoint' => 'nullable|url|max:255',
+ 'endpoint' => 'required|url|max:255',
];
protected $validationAttributes = [
'name' => 'Name',
diff --git a/app/Livewire/Tags/Deployments.php b/app/Livewire/Tags/Deployments.php
index 5c43edfb1..07034ed5d 100644
--- a/app/Livewire/Tags/Deployments.php
+++ b/app/Livewire/Tags/Deployments.php
@@ -26,6 +26,7 @@ public function get_deployments()
"server_id",
"status"
])->sortBy('id')->groupBy('server_name')->toArray();
+ $this->dispatch('deployments', $this->deployments_per_tag_per_server);
} catch (\Exception $e) {
return handleError($e, $this);
}
diff --git a/app/Livewire/Tags/Index.php b/app/Livewire/Tags/Index.php
index d04bb53f9..c2b2a5928 100644
--- a/app/Livewire/Tags/Index.php
+++ b/app/Livewire/Tags/Index.php
@@ -20,6 +20,12 @@ class Index extends Component
public $webhook = null;
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()
{
if ($this->tag == "") {
@@ -39,14 +45,13 @@ public function tag_updated()
public function redeploy_all()
{
try {
- $message = collect([]);
- $this->applications->each(function ($resource) use ($message) {
+ $this->applications->each(function ($resource){
$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();
- $message->push($deploy->deploy_resource($resource));
+ $deploy->deploy_resource($resource);
});
$this->dispatch('success', 'Mass deployment started.');
} catch (\Exception $e) {
diff --git a/app/Livewire/Team/AdminView.php b/app/Livewire/Team/AdminView.php
new file mode 100644
index 000000000..12546ff1b
--- /dev/null
+++ b/app/Livewire/Team/AdminView.php
@@ -0,0 +1,117 @@
+route('dashboard');
+ }
+ $this->getUsers();
+ }
+ public function submitSearch()
+ {
+ if ($this->search !== "") {
+ $this->users = User::where(function ($query) {
+ $query->where('name', 'like', "%{$this->search}%")
+ ->orWhere('email', 'like', "%{$this->search}%");
+ })->get()->filter(function ($user) {
+ return $user->id !== auth()->id();
+ });
+ } else {
+ $this->getUsers();
+ }
+ }
+ public function getUsers()
+ {
+ $this->users = User::where('id', '!=', auth()->id())->get();
+ // $this->users = User::all();
+ }
+ private function finalizeDeletion(User $user, Team $team)
+ {
+ $servers = $team->servers;
+ foreach ($servers as $server) {
+ $resources = $server->definedResources();
+ foreach ($resources as $resource) {
+ ray("Deleting resource: " . $resource->name);
+ $resource->forceDelete();
+ }
+ ray("Deleting server: " . $server->name);
+ $server->forceDelete();
+ }
+
+ $projects = $team->projects;
+ foreach ($projects as $project) {
+ ray("Deleting project: " . $project->name);
+ $project->forceDelete();
+ }
+ $team->members()->detach($user->id);
+ ray('Deleting team: ' . $team->name);
+ $team->delete();
+ }
+ public function delete($id)
+ {
+ $user = User::find($id);
+ $teams = $user->teams;
+ foreach ($teams as $team) {
+ ray($team->name);
+ $user_alone_in_team = $team->members->count() === 1;
+ if ($team->id === 0) {
+ if ($user_alone_in_team) {
+ ray('user is alone in the root team, do nothing');
+ return $this->dispatch('error', 'User is alone in the root team, cannot delete');
+ }
+ }
+ if ($user_alone_in_team) {
+ ray('user is alone in the team');
+ $this->finalizeDeletion($user, $team);
+ continue;
+ }
+ ray('user is not alone in the team');
+ if ($user->isOwner()) {
+ $found_other_owner_or_admin = $team->members->filter(function ($member) {
+ return $member->pivot->role === 'owner' || $member->pivot->role === 'admin';
+ })->where('id', '!=', $user->id)->first();
+
+ if ($found_other_owner_or_admin) {
+ ray('found other owner or admin');
+ $team->members()->detach($user->id);
+ continue;
+ } else {
+ $found_other_member_who_is_not_owner = $team->members->filter(function ($member) {
+ return $member->pivot->role === 'member';
+ })->first();
+ if ($found_other_member_who_is_not_owner) {
+ ray('found other member who is not owner');
+ $found_other_member_who_is_not_owner->pivot->role = 'owner';
+ $found_other_member_who_is_not_owner->pivot->save();
+ $team->members()->detach($user->id);
+ } else {
+ // This should never happen as if the user is the only member in the team, the team should be deleted already.
+ ray('found no other member who is not owner');
+ $this->finalizeDeletion($user, $team);
+ }
+ continue;
+ }
+ } else {
+ ray('user is not owner');
+ $team->members()->detach($user->id);
+ }
+ }
+ ray("Deleting user: " . $user->name);
+ $user->delete();
+ $this->getUsers();
+ }
+ public function render()
+ {
+ return view('livewire.team.admin-view');
+ }
+}
diff --git a/app/Livewire/Upgrade.php b/app/Livewire/Upgrade.php
index 96ee76325..5ef966f43 100644
--- a/app/Livewire/Upgrade.php
+++ b/app/Livewire/Upgrade.php
@@ -3,7 +3,7 @@
namespace App\Livewire;
use App\Actions\Server\UpdateCoolify;
-use App\Models\InstanceSettings;
+
use Livewire\Component;
use DanHarrin\LivewireRateLimiting\WithRateLimiting;
@@ -11,6 +11,7 @@ class Upgrade extends Component
{
use WithRateLimiting;
public bool $showProgress = false;
+ public bool $updateInProgress = false;
public bool $isUpgradeAvailable = false;
public string $latestVersion = '';
@@ -22,23 +23,17 @@ public function checkUpdate()
if (isDev()) {
$this->isUpgradeAvailable = true;
}
- $settings = InstanceSettings::get();
- if ($settings->next_channel) {
- $this->isUpgradeAvailable = true;
- $this->latestVersion = 'next';
- }
}
public function upgrade()
{
try {
- if ($this->showProgress) {
+ if ($this->updateInProgress) {
return;
}
- $this->rateLimit(1, 30);
- $this->showProgress = true;
+ $this->rateLimit(1, 60);
+ $this->updateInProgress = true;
UpdateCoolify::run(force: true, async: true);
- $this->dispatch('success', "Updating Coolify to {$this->latestVersion} version...");
} catch (\Throwable $e) {
return handleError($e, $this);
}
diff --git a/app/Models/Application.php b/app/Models/Application.php
index f28d389f4..0f3425dd6 100644
--- a/app/Models/Application.php
+++ b/app/Models/Application.php
@@ -113,6 +113,18 @@ public function link()
}
return null;
}
+ public function failedTaskLink($task_uuid)
+ {
+ if (data_get($this, 'environment.project.uuid')) {
+ return route('project.application.scheduled-tasks', [
+ 'project_uuid' => data_get($this, 'environment.project.uuid'),
+ 'environment_name' => data_get($this, 'environment.name'),
+ 'application_uuid' => data_get($this, 'uuid'),
+ 'task_uuid' => $task_uuid
+ ]);
+ }
+ return null;
+ }
public function settings()
{
return $this->hasOne(ApplicationSetting::class);
@@ -146,9 +158,13 @@ public function gitBranchLocation(): Attribute
if (!is_null($this->source?->html_url) && !is_null($this->git_repository) && !is_null($this->git_branch)) {
return "{$this->source->html_url}/{$this->git_repository}/tree/{$this->git_branch}";
}
+ // Convert the SSH URL to HTTPS URL
+ if (strpos($this->git_repository, 'git@') === 0) {
+ $git_repository = str_replace(['git@', ':', '.git'], ['', '/', ''], $this->git_repository);
+ return "https://{$git_repository}/tree/{$this->git_branch}";
+ }
return $this->git_repository;
}
-
);
}
@@ -159,6 +175,11 @@ public function gitWebhook(): Attribute
if (!is_null($this->source?->html_url) && !is_null($this->git_repository) && !is_null($this->git_branch)) {
return "{$this->source->html_url}/{$this->git_repository}/settings/hooks";
}
+ // Convert the SSH URL to HTTPS URL
+ if (strpos($this->git_repository, 'git@') === 0) {
+ $git_repository = str_replace(['git@', ':', '.git'], ['', '/', ''], $this->git_repository);
+ return "https://{$git_repository}/settings/hooks";
+ }
return $this->git_repository;
}
);
@@ -171,10 +192,29 @@ public function gitCommits(): Attribute
if (!is_null($this->source?->html_url) && !is_null($this->git_repository) && !is_null($this->git_branch)) {
return "{$this->source->html_url}/{$this->git_repository}/commits/{$this->git_branch}";
}
+ // Convert the SSH URL to HTTPS URL
+ if (strpos($this->git_repository, 'git@') === 0) {
+ $git_repository = str_replace(['git@', ':', '.git'], ['', '/', ''], $this->git_repository);
+ return "https://{$git_repository}/commits/{$this->git_branch}";
+ }
return $this->git_repository;
}
);
}
+ public function gitCommitLink($link): string
+ {
+ if (!is_null($this->source?->html_url) && !is_null($this->git_repository) && !is_null($this->git_branch)) {
+ if (str($this->source->html_url)->contains('bitbucket')) {
+ return "{$this->source->html_url}/{$this->git_repository}/commits/{$link}";
+ }
+ return "{$this->source->html_url}/{$this->git_repository}/commit/{$link}";
+ }
+ if (strpos($this->git_repository, 'git@') === 0) {
+ $git_repository = str_replace(['git@', ':', '.git'], ['', '/', ''], $this->git_repository);
+ return "https://{$git_repository}/commit/{$link}";
+ }
+ return $this->git_repository;
+ }
public function dockerfileLocation(): Attribute
{
return Attribute::make(
@@ -429,6 +469,10 @@ public function isDeploymentInprogress()
}
return false;
}
+ public function get_last_successful_deployment()
+ {
+ return ApplicationDeploymentQueue::where('application_id', $this->id)->where('status', 'finished')->where('pull_request_id', 0)->orderBy('created_at', 'desc')->first();
+ }
public function get_last_days_deployments()
{
return ApplicationDeploymentQueue::where('application_id', $this->id)->where('created_at', '>=', now()->subDays(7))->orderBy('created_at', 'desc')->get();
@@ -847,7 +891,7 @@ function loadComposeFile($isInit = false)
if (!$composeFileContent) {
$this->docker_compose_location = $initialDockerComposeLocation;
$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
Check if you used the right extension (.yaml or .yml) in the compose file name.");
} else {
$this->docker_compose_raw = $composeFileContent;
$this->save();
@@ -963,4 +1007,52 @@ public function getFilesFromServer(bool $isInit = false)
{
getFilesystemVolumesFromServer($this, $isInit);
}
+
+ public function parseHealthcheckFromDockerfile($dockerfile, bool $isInit = false)
+ {
+ if (str($dockerfile)->contains('HEALTHCHECK') && ($this->isHealthcheckDisabled() || $isInit)) {
+ $healthcheckCommand = null;
+ $lines = $dockerfile->toArray();
+ foreach ($lines as $line) {
+ $trimmedLine = trim($line);
+ if (str_starts_with($trimmedLine, 'HEALTHCHECK')) {
+ $healthcheckCommand .= trim($trimmedLine, '\\ ');
+ continue;
+ }
+ if (isset($healthcheckCommand) && str_contains($trimmedLine, '\\')) {
+ $healthcheckCommand .= ' ' . trim($trimmedLine, '\\ ');
+ }
+ if (isset($healthcheckCommand) && !str_contains($trimmedLine, '\\') && !empty($healthcheckCommand)) {
+ $healthcheckCommand .= ' ' . $trimmedLine;
+ break;
+ }
+ }
+ if (str($healthcheckCommand)->isNotEmpty()) {
+ $interval = str($healthcheckCommand)->match('/--interval=(\d+)/');
+ $timeout = str($healthcheckCommand)->match('/--timeout=(\d+)/');
+ $start_period = str($healthcheckCommand)->match('/--start-period=(\d+)/');
+ $start_interval = str($healthcheckCommand)->match('/--start-interval=(\d+)/');
+ $retries = str($healthcheckCommand)->match('/--retries=(\d+)/');
+ if ($interval->isNotEmpty()) {
+ $this->health_check_interval = $interval->toInteger();
+ }
+ if ($timeout->isNotEmpty()) {
+ $this->health_check_timeout = $timeout->toInteger();
+ }
+ if ($start_period->isNotEmpty()) {
+ $this->health_check_start_period = $start_period->toInteger();
+ }
+ // if ($start_interval) {
+ // $this->health_check_start_interval = $start_interval->value();
+ // }
+ if ($retries->isNotEmpty()) {
+ $this->health_check_retries = $retries->toInteger();
+ }
+ if ($interval || $timeout || $start_period || $start_interval || $retries) {
+ $this->custom_healthcheck_found = true;
+ $this->save();
+ }
+ }
+ }
+ }
}
diff --git a/app/Models/ApplicationDeploymentQueue.php b/app/Models/ApplicationDeploymentQueue.php
index 7f3f36d0a..c55f89e21 100644
--- a/app/Models/ApplicationDeploymentQueue.php
+++ b/app/Models/ApplicationDeploymentQueue.php
@@ -9,7 +9,8 @@ class ApplicationDeploymentQueue extends Model
{
protected $guarded = [];
- public function setStatus(string $status) {
+ public function setStatus(string $status)
+ {
$this->update([
'status' => $status,
]);
@@ -21,7 +22,13 @@ public function getOutput($name)
}
return collect(json_decode($this->logs))->where('name', $name)->first()?->output ?? null;
}
-
+ public function commitMessage()
+ {
+ if (empty($this->commit_message) || is_null($this->commit_message)) {
+ return null;
+ }
+ return str($this->commit_message)->trim()->limit(50)->value();
+ }
public function addLogEntry(string $message, string $type = 'stdout', bool $hidden = false)
{
if ($type === 'error') {
diff --git a/app/Models/Environment.php b/app/Models/Environment.php
index 7ed9e38e5..a1f3e4190 100644
--- a/app/Models/Environment.php
+++ b/app/Models/Environment.php
@@ -8,6 +8,18 @@
class Environment extends Model
{
protected $guarded = [];
+
+ protected static function booted()
+ {
+ static::deleting(function ($environment) {
+ $shared_variables = $environment->environment_variables();
+ foreach ($shared_variables as $shared_variable) {
+ ray('Deleting environment shared variable: ' . $shared_variable->name);
+ $shared_variable->delete();
+ }
+
+ });
+ }
public function isEmpty()
{
return $this->applications()->count() == 0 &&
diff --git a/app/Models/Project.php b/app/Models/Project.php
index 2621d3da1..c2be8cc32 100644
--- a/app/Models/Project.php
+++ b/app/Models/Project.php
@@ -25,6 +25,11 @@ protected static function booted()
static::deleting(function ($project) {
$project->environments()->delete();
$project->settings()->delete();
+ $shared_variables = $project->environment_variables();
+ foreach ($shared_variables as $shared_variable) {
+ ray('Deleting project shared variable: ' . $shared_variable->name);
+ $shared_variable->delete();
+ }
});
}
public function environment_variables()
@@ -55,6 +60,7 @@ public function applications()
return $this->hasManyThrough(Application::class, Environment::class);
}
+
public function postgresqls()
{
return $this->hasManyThrough(StandalonePostgresql::class, Environment::class);
@@ -91,4 +97,7 @@ public function resource_count()
{
return $this->applications()->count() + $this->postgresqls()->count() + $this->redis()->count() + $this->mongodbs()->count() + $this->mysqls()->count() + $this->mariadbs()->count() + $this->keydbs()->count() + $this->dragonflies()->count() + $this->services()->count() + $this->clickhouses()->count();
}
+ public function databases() {
+ return $this->postgresqls()->get()->merge($this->redis()->get())->merge($this->mongodbs()->get())->merge($this->mysqls()->get())->merge($this->mariadbs()->get())->merge($this->keydbs()->get())->merge($this->dragonflies()->get())->merge($this->clickhouses()->get());
+ }
}
diff --git a/app/Models/Server.php b/app/Models/Server.php
index bda044320..2f4c29080 100644
--- a/app/Models/Server.php
+++ b/app/Models/Server.php
@@ -3,11 +3,14 @@
namespace App\Models;
use App\Actions\Server\InstallDocker;
+use App\Actions\Server\StartSentinel;
use App\Enums\ProxyTypes;
+use App\Jobs\PullSentinelImageJob;
use App\Notifications\Server\Revived;
use App\Notifications\Server\Unreachable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
+use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Spatie\SchemalessAttributes\Casts\SchemalessAttributes;
@@ -239,7 +242,7 @@ public function setupDynamicProxyConfiguration()
$dynamic_config_path = $this->proxyPath() . "/dynamic";
if ($this->proxyType() === 'TRAEFIK_V2') {
$file = "$dynamic_config_path/coolify.yaml";
- if (empty($settings->fqdn)) {
+ if (empty($settings->fqdn) || (isCloud() && $this->id !== 0)) {
instant_remote_process([
"rm -f $file",
], $this);
@@ -358,7 +361,7 @@ public function setupDynamicProxyConfiguration()
}
} else if ($this->proxyType() === 'CADDY') {
$file = "$dynamic_config_path/coolify.caddy";
- if (empty($settings->fqdn)) {
+ if (empty($settings->fqdn) || (isCloud() && $this->id !== 0)) {
instant_remote_process([
"rm -f $file",
], $this);
@@ -462,6 +465,36 @@ public function forceDisableServer()
Storage::disk('ssh-keys')->delete($sshKeyFileLocation);
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)
{
if ($this->skipServer()) {
@@ -548,7 +581,36 @@ public function startUnmanaged($id)
{
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()) {
$containers = instant_remote_process(["docker ps -a --format '{{json .}}'"], $this);
diff --git a/app/Models/Service.php b/app/Models/Service.php
index 41e61cbb8..ab40a761c 100644
--- a/app/Models/Service.php
+++ b/app/Models/Service.php
@@ -171,7 +171,7 @@ public function extraFields()
],
]);
}
- $fields->put('Tolgee', $data);
+ $fields->put('Tolgee', $data->toArray());
break;
case str($image)?->contains('logto'):
$data = collect([]);
@@ -195,7 +195,7 @@ public function extraFields()
],
]);
}
- $fields->put('Logto', $data);
+ $fields->put('Logto', $data->toArray());
break;
case str($image)?->contains('unleash-server'):
$data = collect([]);
@@ -218,7 +218,7 @@ public function extraFields()
],
]);
}
- $fields->put('Unleash', $data);
+ $fields->put('Unleash', $data->toArray());
break;
case str($image)?->contains('grafana'):
$data = collect([]);
@@ -241,7 +241,7 @@ public function extraFields()
],
]);
}
- $fields->put('Grafana', $data);
+ $fields->put('Grafana', $data->toArray());
break;
case str($image)?->contains('directus'):
$data = collect([]);
@@ -267,7 +267,7 @@ public function extraFields()
],
]);
}
- $fields->put('Directus', $data);
+ $fields->put('Directus', $data->toArray());
break;
case str($image)?->contains('kong'):
$data = collect([]);
@@ -370,7 +370,7 @@ public function extraFields()
],
]);
}
- $fields->put('Weblate', $data);
+ $fields->put('Weblate', $data->toArray());
break;
case str($image)?->contains('meilisearch'):
$data = collect([]);
@@ -384,7 +384,7 @@ public function extraFields()
],
]);
}
- $fields->put('Meilisearch', $data);
+ $fields->put('Meilisearch', $data->toArray());
break;
case str($image)?->contains('ghost'):
$data = collect([]);
@@ -444,7 +444,33 @@ public function extraFields()
]);
}
- $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();
+ if ($admin_user) {
+ $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;
case str($image)?->contains('vaultwarden'):
$data = collect([]);
@@ -723,6 +749,18 @@ public function link()
}
return null;
}
+ public function failedTaskLink($task_uuid)
+ {
+ if (data_get($this, 'environment.project.uuid')) {
+ return route('project.service.scheduled-tasks', [
+ 'project_uuid' => data_get($this, 'environment.project.uuid'),
+ 'environment_name' => data_get($this, 'environment.name'),
+ 'application_uuid' => data_get($this, 'uuid'),
+ 'task_uuid' => $task_uuid
+ ]);
+ }
+ return null;
+ }
public function documentation()
{
$services = getServiceTemplates();
@@ -749,6 +787,17 @@ public function server()
{
return $this->belongsTo(Server::class);
}
+ public function byUuid(string $uuid) {
+ $app = $this->applications()->whereUuid($uuid)->first();
+ if ($app) {
+ return $app;
+ }
+ $db = $this->databases()->whereUuid($uuid)->first();
+ if ($db) {
+ return $db;
+ }
+ return null;
+ }
public function byName(string $name)
{
$app = $this->applications()->whereName($name)->first();
diff --git a/app/Models/StandaloneClickhouse.php b/app/Models/StandaloneClickhouse.php
index 3746a32f5..2197d51df 100644
--- a/app/Models/StandaloneClickhouse.php
+++ b/app/Models/StandaloneClickhouse.php
@@ -207,7 +207,4 @@ public function scheduledBackups()
{
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
- public function database_name() {
- return $this->clickhouse_db;
- }
}
diff --git a/app/Models/StandaloneDragonfly.php b/app/Models/StandaloneDragonfly.php
index adc1ea6cc..7b18666b8 100644
--- a/app/Models/StandaloneDragonfly.php
+++ b/app/Models/StandaloneDragonfly.php
@@ -207,7 +207,4 @@ public function scheduledBackups()
{
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
- public function database_name() {
- return '0';
- }
}
diff --git a/app/Models/StandaloneKeydb.php b/app/Models/StandaloneKeydb.php
index ff91322a0..c2c1b98da 100644
--- a/app/Models/StandaloneKeydb.php
+++ b/app/Models/StandaloneKeydb.php
@@ -208,7 +208,4 @@ public function scheduledBackups()
{
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
- public function database_name() {
- return '0';
- }
}
diff --git a/app/Models/StandaloneMariadb.php b/app/Models/StandaloneMariadb.php
index 37d39f882..5e18bbfde 100644
--- a/app/Models/StandaloneMariadb.php
+++ b/app/Models/StandaloneMariadb.php
@@ -208,7 +208,4 @@ public function scheduledBackups()
{
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
- public function database_name() {
- return $this->mariadb_database;
- }
}
diff --git a/app/Models/StandaloneMongodb.php b/app/Models/StandaloneMongodb.php
index 5538efe1a..8e4d327a3 100644
--- a/app/Models/StandaloneMongodb.php
+++ b/app/Models/StandaloneMongodb.php
@@ -223,7 +223,4 @@ public function scheduledBackups()
{
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
- public function database_name() {
- return $this->mongo_db;
- }
}
diff --git a/app/Models/StandaloneMysql.php b/app/Models/StandaloneMysql.php
index 53e9b6f22..eede451d7 100644
--- a/app/Models/StandaloneMysql.php
+++ b/app/Models/StandaloneMysql.php
@@ -209,7 +209,4 @@ public function scheduledBackups()
{
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
- public function database_name() {
- return $this->mysql_database;
- }
}
diff --git a/app/Models/StandalonePostgresql.php b/app/Models/StandalonePostgresql.php
index 6435c49de..cf449a815 100644
--- a/app/Models/StandalonePostgresql.php
+++ b/app/Models/StandalonePostgresql.php
@@ -208,7 +208,4 @@ public function scheduledBackups()
{
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
- public function database_name() {
- return $this->postgres_db;
- }
}
diff --git a/app/Models/StandaloneRedis.php b/app/Models/StandaloneRedis.php
index de18c8c07..da4701df9 100644
--- a/app/Models/StandaloneRedis.php
+++ b/app/Models/StandaloneRedis.php
@@ -204,7 +204,4 @@ public function scheduledBackups()
{
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
- public function database_name() {
- return '0';
- }
}
diff --git a/app/Models/Team.php b/app/Models/Team.php
index 29e434a5d..81206019f 100644
--- a/app/Models/Team.php
+++ b/app/Models/Team.php
@@ -26,6 +26,34 @@ protected static function booted()
throw new \Exception('You are not allowed to update this team.');
}
});
+
+ static::deleting(function ($team) {
+ $keys = $team->privateKeys;
+ foreach ($keys as $key) {
+ ray('Deleting key: ' . $key->name);
+ $key->delete();
+ }
+ $sources = $team->sources();
+ foreach ($sources as $source) {
+ ray('Deleting source: ' . $source->name);
+ $source->delete();
+ }
+ $tags = Tag::whereTeamId($team->id)->get();
+ foreach ($tags as $tag) {
+ ray('Deleting tag: ' . $tag->name);
+ $tag->delete();
+ }
+ $shared_variables = $team->environment_variables();
+ foreach ($shared_variables as $shared_variable) {
+ ray('Deleting team shared variable: ' . $shared_variable->name);
+ $shared_variable->delete();
+ }
+ $s3s = $team->s3s;
+ foreach ($s3s as $s3) {
+ ray('Deleting s3: ' . $s3->name);
+ $s3->delete();
+ }
+ });
}
public function routeNotificationForDiscord()
diff --git a/app/Models/User.php b/app/Models/User.php
index 0fa8ead2f..0e66fdaea 100644
--- a/app/Models/User.php
+++ b/app/Models/User.php
@@ -183,6 +183,7 @@ public function role()
if (data_get($this, 'pivot')) {
return $this->pivot->role;
}
- return auth()->user()->teams->where('id', currentTeam()->id)->first()->pivot->role;
+ $user = auth()->user()->teams->where('id', currentTeam()->id)->first();
+ return data_get($user, 'pivot.role');
}
}
diff --git a/app/Notifications/Application/DeploymentFailed.php b/app/Notifications/Application/DeploymentFailed.php
index 1705deda1..05fe544d0 100644
--- a/app/Notifications/Application/DeploymentFailed.php
+++ b/app/Notifications/Application/DeploymentFailed.php
@@ -69,10 +69,10 @@ public function toMail(): MailMessage
public function toDiscord(): string
{
if ($this->preview) {
- $message = 'Coolify: Pull request #' . $this->preview->pull_request_id . ' of **' . $this->application_name . '** (' . $this->preview->fqdn . ') deployment failed: ';
+ $message = 'Coolify: Pull request #' . $this->preview->pull_request_id . ' of ' . $this->application_name . ' (' . $this->preview->fqdn . ') deployment failed: ';
$message .= '[View Deployment Logs](' . $this->deployment_url . ')';
} else {
- $message = 'Coolify: Deployment failed of **' . $this->application_name . '** (' . $this->fqdn . '): ';
+ $message = 'Coolify: Deployment failed of ' . $this->application_name . ' (' . $this->fqdn . '): ';
$message .= '[View Deployment Logs](' . $this->deployment_url . ')';
}
return $message;
@@ -80,9 +80,9 @@ public function toDiscord(): string
public function toTelegram(): array
{
if ($this->preview) {
- $message = 'Coolify: Pull request #' . $this->preview->pull_request_id . ' of **' . $this->application_name . '** (' . $this->preview->fqdn . ') deployment failed: ';
+ $message = 'Coolify: Pull request #' . $this->preview->pull_request_id . ' of ' . $this->application_name . ' (' . $this->preview->fqdn . ') deployment failed: ';
} else {
- $message = 'Coolify: Deployment failed of **' . $this->application_name . '** (' . $this->fqdn . '): ';
+ $message = 'Coolify: Deployment failed of ' . $this->application_name . ' (' . $this->fqdn . '): ';
}
$buttons[] = [
"text" => "Deployment logs",
diff --git a/app/Notifications/Application/DeploymentSuccess.php b/app/Notifications/Application/DeploymentSuccess.php
index 322df5cec..e138ac91e 100644
--- a/app/Notifications/Application/DeploymentSuccess.php
+++ b/app/Notifications/Application/DeploymentSuccess.php
@@ -45,13 +45,11 @@ public function via(object $notifiable): array
{
$channels = setNotificationChannels($notifiable, 'deployments');
if (isCloud()) {
- $channels = array_filter($channels, function ($channel) {
- return $channel !== 'App\Notifications\Channels\EmailChannel';
- });
+ // TODO: Make batch notifications work with email
+ $channels = array_diff($channels, ['App\Notifications\Channels\EmailChannel']);
}
return $channels;
}
-
public function toMail(): MailMessage
{
$mail = new MailMessage();
diff --git a/app/Notifications/Channels/TelegramChannel.php b/app/Notifications/Channels/TelegramChannel.php
index 1401bb324..6101ef208 100644
--- a/app/Notifications/Channels/TelegramChannel.php
+++ b/app/Notifications/Channels/TelegramChannel.php
@@ -14,22 +14,27 @@ public function send($notifiable, $notification): void
$buttons = data_get($data, 'buttons', []);
$telegramToken = data_get($telegramData, 'token');
$chatId = data_get($telegramData, 'chat_id');
- $topicId = null;
+ $topicId = null;
$topicsInstance = get_class($notification);
switch ($topicsInstance) {
- case 'App\Notifications\StatusChange':
- $topicId = data_get($notifiable, 'telegram_notifications_status_changes_message_thread_id');
- break;
case 'App\Notifications\Test':
$topicId = data_get($notifiable, 'telegram_notifications_test_message_thread_id');
break;
- case 'App\Notifications\Deployment':
+ case 'App\Notifications\Application\StatusChanged':
+ $topicId = data_get($notifiable, 'telegram_notifications_status_changes_message_thread_id');
+ break;
+ case 'App\Notifications\Application\DeploymentSuccess':
+ case 'App\Notifications\Application\DeploymentFailed':
$topicId = data_get($notifiable, 'telegram_notifications_deployments_message_thread_id');
break;
- case 'App\Notifications\DatabaseBackup':
+ case 'App\Notifications\Database\BackupSuccess':
+ case 'App\Notifications\Database\BackupFailed':
$topicId = data_get($notifiable, 'telegram_notifications_database_backups_message_thread_id');
break;
+ case 'App\Notifications\ScheduledTask\TaskFailed':
+ $topicId = data_get($notifiable, 'telegram_notifications_scheduled_tasks_thread_id');
+ break;
}
if (!$telegramToken || !$chatId || !$message) {
return;
diff --git a/app/Notifications/Container/ContainerRestarted.php b/app/Notifications/Container/ContainerRestarted.php
index 21dc799f8..d9c524da4 100644
--- a/app/Notifications/Container/ContainerRestarted.php
+++ b/app/Notifications/Container/ContainerRestarted.php
@@ -31,7 +31,7 @@ public function toMail(): MailMessage
$mail->view('emails.container-restarted', [
'containerName' => $this->name,
'serverName' => $this->server->name,
- 'url' => $this->url ,
+ 'url' => $this->url,
]);
return $mail;
}
diff --git a/app/Notifications/Database/BackupFailed.php b/app/Notifications/Database/BackupFailed.php
index 3aa63ffd9..7cad486b3 100644
--- a/app/Notifications/Database/BackupFailed.php
+++ b/app/Notifications/Database/BackupFailed.php
@@ -15,21 +15,20 @@ class BackupFailed extends Notification implements ShouldQueue
{
use Queueable;
- public $tries = 1;
+ public $backoff = 10;
+ public $tries = 2;
public string $name;
- public string $database_name;
public string $frequency;
- public function __construct(ScheduledDatabaseBackup $backup, public $database, public $output)
+ public function __construct(ScheduledDatabaseBackup $backup, public $database, public $output, public $database_name)
{
$this->name = $database->name;
- $this->database_name = $database->database_name();
$this->frequency = $backup->frequency;
}
public function via(object $notifiable): array
{
- return [DiscordChannel::class, TelegramChannel::class, MailChannel::class];
+ return setNotificationChannels($notifiable, 'database_backups');
}
public function toMail(): MailMessage
@@ -47,11 +46,11 @@ public function toMail(): MailMessage
public function toDiscord(): string
{
- return "Coolify: Database backup for {$this->name} (db:{$this->database_name}) with frequency of {$this->frequency} was FAILED.\n\nReason: {$this->output}";
+ return "Coolify: Database backup for {$this->name} (db:{$this->database_name}) with frequency of {$this->frequency} was FAILED.\n\nReason:\n{$this->output}";
}
public function toTelegram(): array
{
- $message = "Coolify: Database backup for {$this->name} (db:{$this->database_name}) with frequency of {$this->frequency} was FAILED.\n\nReason: {$this->output}";
+ $message = "Coolify: Database backup for {$this->name} (db:{$this->database_name}) with frequency of {$this->frequency} was FAILED.\n\nReason:\n{$this->output}";
return [
"message" => $message,
];
diff --git a/app/Notifications/Database/BackupSuccess.php b/app/Notifications/Database/BackupSuccess.php
index 9ca3234e1..c43a12276 100644
--- a/app/Notifications/Database/BackupSuccess.php
+++ b/app/Notifications/Database/BackupSuccess.php
@@ -12,15 +12,14 @@ class BackupSuccess extends Notification implements ShouldQueue
{
use Queueable;
- public $tries = 1;
+ public $backoff = 10;
+ public $tries = 3;
public string $name;
- public string $database_name;
public string $frequency;
- public function __construct(ScheduledDatabaseBackup $backup, public $database)
+ public function __construct(ScheduledDatabaseBackup $backup, public $database, public $database_name)
{
$this->name = $database->name;
- $this->database_name = $database->database_name();
$this->frequency = $backup->frequency;
}
@@ -48,6 +47,7 @@ public function toDiscord(): string
public function toTelegram(): array
{
$message = "Coolify: Database backup for {$this->name} (db:{$this->database_name}) with frequency of {$this->frequency} was successful.";
+ ray($message);
return [
"message" => $message,
];
diff --git a/app/Notifications/ScheduledTask/TaskFailed.php b/app/Notifications/ScheduledTask/TaskFailed.php
new file mode 100644
index 000000000..f61b1f573
--- /dev/null
+++ b/app/Notifications/ScheduledTask/TaskFailed.php
@@ -0,0 +1,64 @@
+application) {
+ $this->url = $task->application->failedTaskLink($task->uuid);
+ } else if ($task->service) {
+ $this->url = $task->service->failedTaskLink($task->uuid);
+ }
+ }
+
+ public function via(object $notifiable): array
+ {
+
+ return setNotificationChannels($notifiable, 'scheduled_tasks');
+ }
+
+ public function toMail(): MailMessage
+ {
+ $mail = new MailMessage();
+ $mail->subject("Coolify: [ACTION REQUIRED] Scheduled task ({$this->task->name}) failed.");
+ $mail->view('emails.scheduled-task-failed', [
+ 'task' => $this->task,
+ 'url' => $this->url,
+ 'output' => $this->output,
+ ]);
+ return $mail;
+ }
+
+ public function toDiscord(): string
+ {
+ return "Coolify: Scheduled task ({$this->task->name}, [link]({$this->url})) failed with output: {$this->output}";
+ }
+ public function toTelegram(): array
+ {
+ $message = "Coolify: Scheduled task ({$this->task->name}) failed with output: {$this->output}";
+ if ($this->url) {
+ $buttons[] = [
+ "text" => "Open task in Coolify",
+ "url" => (string) $this->url
+ ];
+ }
+ return [
+ "message" => $message,
+ ];
+ }
+}
diff --git a/app/Notifications/Server/Revived.php b/app/Notifications/Server/Revived.php
index c670ded9a..36775976b 100644
--- a/app/Notifications/Server/Revived.php
+++ b/app/Notifications/Server/Revived.php
@@ -2,6 +2,7 @@
namespace App\Notifications\Server;
+use App\Actions\Docker\GetContainersStatus;
use App\Jobs\ContainerStatusJob;
use App\Models\Server;
use Illuminate\Bus\Queueable;
@@ -22,7 +23,8 @@ public function __construct(public Server $server)
if ($this->server->unreachable_notification_sent === false) {
return;
}
- dispatch(new ContainerStatusJob($server));
+ GetContainersStatus::dispatch($server);
+ // dispatch(new ContainerStatusJob($server));
}
public function via(object $notifiable): array
diff --git a/bootstrap/helpers/applications.php b/bootstrap/helpers/applications.php
index c0aaf4abf..a1995c645 100644
--- a/bootstrap/helpers/applications.php
+++ b/bootstrap/helpers/applications.php
@@ -6,7 +6,6 @@
use App\Models\ApplicationDeploymentQueue;
use App\Models\Server;
use App\Models\StandaloneDocker;
-use Illuminate\Support\Collection;
use Spatie\Url\Url;
function queue_application_deployment(Application $application, string $deployment_uuid, int | null $pull_request_id = 0, string $commit = 'HEAD', bool $force_rebuild = false, bool $is_webhook = false, bool $restart_only = false, ?string $git_type = null, bool $no_questions_asked = false, Server $server = null, StandaloneDocker $destination = null, bool $only_this_server = false, bool $rollback = false)
diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php
index a70c85a72..a087c92c5 100644
--- a/bootstrap/helpers/docker.php
+++ b/bootstrap/helpers/docker.php
@@ -8,6 +8,7 @@
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Spatie\Url\Url;
+use Visus\Cuid2\Cuid2;
function getCurrentApplicationContainerStatus(Server $server, int $id, ?int $pullRequestId = null, ?bool $includePullrequests = false): Collection
{
@@ -272,7 +273,7 @@ function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains,
}
return $labels->sort();
}
-function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?bool $is_stripprefix_enabled = true, ?string $service_name = null)
+function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled = false, $onlyPort = null, ?Collection $serviceLabels = null, ?bool $is_gzip_enabled = true, ?bool $is_stripprefix_enabled = true, ?string $service_name = null, bool $generate_unique_uuid = false)
{
$labels = collect([]);
$labels->push('traefik.enable=true');
@@ -313,7 +314,9 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
}
foreach ($domains as $loop => $domain) {
try {
- // $uuid = new Cuid2(7);
+ if ($generate_unique_uuid) {
+ $uuid = new Cuid2(7);
+ }
$url = Url::fromString($domain);
$host = $url->getHost();
$path = $url->getPath();
diff --git a/bootstrap/helpers/services.php b/bootstrap/helpers/services.php
index 8e3c0337e..26a69222a 100644
--- a/bootstrap/helpers/services.php
+++ b/bootstrap/helpers/services.php
@@ -18,7 +18,7 @@ function collectRegex(string $name)
}
function replaceVariables($variable)
{
- return $variable->replaceFirst('$', '')->replaceFirst('{', '')->replaceLast('}', '');
+ return $variable->before('}')->replaceFirst('$', '')->replaceFirst('{', '');
}
function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase|Application $oneService, bool $isInit = false)
@@ -27,7 +27,7 @@ function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase|Appli
if ($oneService->getMorphClass() === 'App\Models\Application') {
$workdir = $oneService->workdir();
$server = $oneService->destination->server;
- } else{
+ } else {
$workdir = $oneService->service->workdir();
$server = $oneService->service->server;
}
diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php
index b2c34900e..6453108eb 100644
--- a/bootstrap/helpers/shared.php
+++ b/bootstrap/helpers/shared.php
@@ -95,6 +95,9 @@ function currentTeam()
function showBoarding(): bool
{
+ if (auth()->user()?->isMember()) {
+ return false;
+ }
return currentTeam()->show_boarding ?? false;
}
function refreshSession(?Team $team = null): void
@@ -147,6 +150,18 @@ function get_route_parameters(): array
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
{
try {
@@ -637,7 +652,6 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$allServices = getServiceTemplates();
$topLevelVolumes = collect(data_get($yaml, 'volumes', []));
$topLevelNetworks = collect(data_get($yaml, 'networks', []));
- $dockerComposeVersion = data_get($yaml, 'version') ?? '3.8';
$services = data_get($yaml, 'services');
$generatedServiceFQDNS = collect([]);
@@ -988,20 +1002,18 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
if ($fqdns_exploded->count() > 1) {
continue;
}
- if ($resource->server->proxyType() === 'CADDY') {
- $env = EnvironmentVariable::where([
- 'key' => $key,
- 'service_id' => $resource->id,
- ])->first();
- if ($env) {
+ $env = EnvironmentVariable::where([
+ 'key' => $key,
+ 'service_id' => $resource->id,
+ ])->first();
+ if ($env) {
- $env_url = Url::fromString($savedService->fqdn);
- $env_port = $env_url->getPort();
- if ($env_port !== $predefinedPort) {
- $env_url = $env_url->withPort($predefinedPort);
- $savedService->fqdn = $env_url->__toString();
- $savedService->save();
- }
+ $env_url = Url::fromString($savedService->fqdn);
+ $env_port = $env_url->getPort();
+ if ($env_port !== $predefinedPort) {
+ $env_url = $env_url->withPort($predefinedPort);
+ $savedService->fqdn = $env_url->__toString();
+ $savedService->save();
}
}
}
@@ -1165,6 +1177,13 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
]
]);
}
+ if ($serviceLabels->count() > 0) {
+ if ($resource->is_container_label_escape_enabled) {
+ $serviceLabels = $serviceLabels->map(function ($value, $key) {
+ return escapeDollarSign($value);
+ });
+ }
+ }
data_set($service, 'labels', $serviceLabels->toArray());
data_forget($service, 'is_database');
if (!data_get($service, 'restart')) {
@@ -1194,7 +1213,6 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
return $service;
});
$finalServices = [
- 'version' => $dockerComposeVersion,
'services' => $services->toArray(),
'volumes' => $topLevelVolumes->toArray(),
'networks' => $topLevelNetworks->toArray(),
@@ -1217,13 +1235,13 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
try {
$yaml = Yaml::parse($resource->docker_compose_pr_raw);
} catch (\Exception $e) {
- throw new \Exception($e->getMessage());
+ return;
}
} else {
try {
$yaml = Yaml::parse($resource->docker_compose_raw);
} catch (\Exception $e) {
- throw new \Exception($e->getMessage());
+ return;
}
}
$server = $resource->destination->server;
@@ -1232,7 +1250,6 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$topLevelVolumes = collect([]);
}
$topLevelNetworks = collect(data_get($yaml, 'networks', []));
- $dockerComposeVersion = data_get($yaml, 'version') ?? '3.8';
$services = data_get($yaml, 'services');
$generatedServiceFQDNS = collect([]);
@@ -1252,6 +1269,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$servicePorts = collect(data_get($service, 'ports', []));
$serviceNetworks = collect(data_get($service, 'networks', []));
$serviceVariables = collect(data_get($service, 'environment', []));
+ $serviceDependencies = collect(data_get($service, 'depends_on', []));
$serviceLabels = collect(data_get($service, 'labels', []));
$serviceBuildVariables = collect(data_get($service, 'build.args', []));
$serviceVariables = $serviceVariables->merge($serviceBuildVariables);
@@ -1268,11 +1286,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$serviceLabels->push("$removedLabelName=$removedLabel");
}
}
- if ($serviceLabels->count() > 0) {
- $serviceLabels = $serviceLabels->map(function ($value, $key) {
- return escapeDollarSign($value);
- });
- }
+
$baseName = generateApplicationContainerName($resource, $pull_request_id);
$containerName = "$serviceName-$baseName";
if (count($serviceVolumes) > 0) {
@@ -1363,6 +1377,13 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
data_set($service, 'volumes', $serviceVolumes->toArray());
}
+ if ($pull_request_id !== 0 && count($serviceDependencies) > 0) {
+ $serviceDependencies = $serviceDependencies->map(function ($dependency) use ($pull_request_id) {
+ return $dependency . "-pr-$pull_request_id";
+ });
+ data_set($service, 'depends_on', $serviceDependencies->toArray());
+ }
+
// Decide if the service is a database
$isDatabase = isDatabaseImage(data_get_str($service, 'image'));
data_set($service, 'is_database', $isDatabase);
@@ -1620,7 +1641,8 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
$serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik(
uuid: $resource->uuid,
domains: $fqdns,
- serviceLabels: $serviceLabels
+ serviceLabels: $serviceLabels,
+ generate_unique_uuid: $resource->build_pack === 'dockercompose'
));
$serviceLabels = $serviceLabels->merge(fqdnLabelsForCaddy(
network: $resource->destination->network,
@@ -1644,6 +1666,13 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
]
]);
}
+ if ($serviceLabels->count() > 0) {
+ if ($resource->settings->is_container_label_escape_enabled) {
+ $serviceLabels = $serviceLabels->map(function ($value, $key) {
+ return escapeDollarSign($value);
+ });
+ }
+ }
data_set($service, 'labels', $serviceLabels->toArray());
data_forget($service, 'is_database');
if (!data_get($service, 'restart')) {
@@ -1662,7 +1691,6 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
});
}
$finalServices = [
- 'version' => $dockerComposeVersion,
'services' => $services->toArray(),
'volumes' => $topLevelVolumes->toArray(),
'networks' => $topLevelNetworks->toArray(),
@@ -1842,7 +1870,7 @@ function validate_dns_entry(string $fqdn, Server $server)
$dns_servers = data_get($settings, 'custom_dns_servers');
$dns_servers = str($dns_servers)->explode(',');
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 {
$ip = $server->ip;
}
@@ -1921,7 +1949,6 @@ function check_domain_usage(ServiceApplication|Application|null $resource = null
$naked_domain = str($domain)->value();
if ($domains->contains($naked_domain)) {
if (data_get($resource, 'uuid')) {
- ray($resource->uuid, $app->uuid);
if ($resource->uuid !== $app->uuid) {
throw new \RuntimeException("Domain $naked_domain is already in use by another resource called:
{$app->name}.");
}
diff --git a/config/constants.php b/config/constants.php
index 091c60996..53f43ae5a 100644
--- a/config/constants.php
+++ b/config/constants.php
@@ -32,6 +32,7 @@
'basic' => env('LIMIT_SERVER_BASIC', 2),
'pro' => env('LIMIT_SERVER_PRO', 10),
'ultimate' => env('LIMIT_SERVER_ULTIMATE', 25),
+ 'dynamic' => env('LIMIT_SERVER_DYNAMIC', 2),
],
'email' => [
'zero' => true,
@@ -39,6 +40,7 @@
'basic' => true,
'pro' => true,
'ultimate' => true,
+ 'dynamic' => true,
],
],
];
diff --git a/config/coolify.php b/config/coolify.php
index a6d6d8581..c7cfe6101 100644
--- a/config/coolify.php
+++ b/config/coolify.php
@@ -14,4 +14,5 @@
'helper_image' => env('HELPER_IMAGE', 'ghcr.io/coollabsio/coolify-helper:latest'),
'is_horizon_enabled' => env('HORIZON_ENABLED', true),
'is_scheduler_enabled' => env('SCHEDULER_ENABLED', true),
+ 'is_sentinel_enabled' => env('SENTINEL_ENABLED', false),
];
diff --git a/config/sentry.php b/config/sentry.php
index 2b48e1f14..693c33c3d 100644
--- a/config/sentry.php
+++ b/config/sentry.php
@@ -7,7 +7,7 @@
// The release version of your application
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
- 'release' => '4.0.0-beta.269',
+ 'release' => '4.0.0-beta.285',
// When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'),
diff --git a/config/version.php b/config/version.php
index bf35359e8..e09c4fd6a 100644
--- a/config/version.php
+++ b/config/version.php
@@ -1,3 +1,3 @@
boolean('custom_healthcheck_found')->default(false);
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('applications', function (Blueprint $table) {
+ $table->dropColumn('custom_healthcheck_found');
+ });
+ }
+};
diff --git a/database/migrations/2024_05_06_093236_add_custom_name_to_application_settings.php b/database/migrations/2024_05_06_093236_add_custom_name_to_application_settings.php
new file mode 100644
index 000000000..e2d68d240
--- /dev/null
+++ b/database/migrations/2024_05_06_093236_add_custom_name_to_application_settings.php
@@ -0,0 +1,28 @@
+string('custom_internal_name')->nullable();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('application_settings', function (Blueprint $table) {
+ $table->dropColumn('custom_internal_name');
+ });
+ }
+};
diff --git a/database/migrations/2024_05_07_124019_add_server_metrics.php b/database/migrations/2024_05_07_124019_add_server_metrics.php
new file mode 100644
index 000000000..40c74850b
--- /dev/null
+++ b/database/migrations/2024_05_07_124019_add_server_metrics.php
@@ -0,0 +1,28 @@
+boolean('is_metrics_enabled')->default(true);
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('servers', function (Blueprint $table) {
+ $table->dropColumn('is_metrics_enabled');
+ });
+ }
+};
diff --git a/database/migrations/2024_05_10_085215_make_stripe_comment_longer.php b/database/migrations/2024_05_10_085215_make_stripe_comment_longer.php
new file mode 100644
index 000000000..a51896f42
--- /dev/null
+++ b/database/migrations/2024_05_10_085215_make_stripe_comment_longer.php
@@ -0,0 +1,28 @@
+longText('stripe_comment')->change();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('subscriptions', function (Blueprint $table) {
+ $table->string('stripe_comment')->change();
+ });
+ }
+};
diff --git a/database/migrations/2024_05_15_091757_add_commit_message_to_app_deployment_queue.php b/database/migrations/2024_05_15_091757_add_commit_message_to_app_deployment_queue.php
new file mode 100644
index 000000000..78608f503
--- /dev/null
+++ b/database/migrations/2024_05_15_091757_add_commit_message_to_app_deployment_queue.php
@@ -0,0 +1,28 @@
+string('commit_message', 50)->nullable();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('application_deployment_queues', function (Blueprint $table) {
+ $table->dropColumn('commit_message');
+ });
+ }
+};
diff --git a/database/migrations/2024_05_15_151236_add_container_escape_toggle.php b/database/migrations/2024_05_15_151236_add_container_escape_toggle.php
new file mode 100644
index 000000000..aa1384518
--- /dev/null
+++ b/database/migrations/2024_05_15_151236_add_container_escape_toggle.php
@@ -0,0 +1,34 @@
+boolean('is_container_label_escape_enabled')->default(true);
+ });
+ Schema::table('services', function (Blueprint $table) {
+ $table->boolean('is_container_label_escape_enabled')->default(true);
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('application_settings', function (Blueprint $table) {
+ $table->dropColumn('is_container_label_escape_enabled');
+ });
+ Schema::table('services', function (Blueprint $table) {
+ $table->dropColumn('is_container_label_escape_enabled');
+ });
+ }
+};
diff --git a/database/migrations/2024_05_17_082012_add_env_sorting_toggle.php b/database/migrations/2024_05_17_082012_add_env_sorting_toggle.php
new file mode 100644
index 000000000..d4e120e2b
--- /dev/null
+++ b/database/migrations/2024_05_17_082012_add_env_sorting_toggle.php
@@ -0,0 +1,28 @@
+boolean('is_env_sorting_enabled')->default(true);
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('application_settings', function (Blueprint $table) {
+ $table->dropColumn('is_env_sorting_enabled');
+ });
+ }
+};
diff --git a/database/migrations/2024_05_21_125739_add_scheduled_tasks_notification_to_teams.php b/database/migrations/2024_05_21_125739_add_scheduled_tasks_notification_to_teams.php
new file mode 100644
index 000000000..0fcbb0655
--- /dev/null
+++ b/database/migrations/2024_05_21_125739_add_scheduled_tasks_notification_to_teams.php
@@ -0,0 +1,34 @@
+boolean('telegram_notifications_scheduled_tasks')->default(true);
+ $table->boolean('smtp_notifications_scheduled_tasks')->default(false)->after('smtp_notifications_status_changes');
+ $table->boolean('discord_notifications_scheduled_tasks')->default(true)->after('discord_notifications_status_changes');
+ $table->text('telegram_notifications_scheduled_tasks_thread_id')->nullable();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('teams', function (Blueprint $table) {
+ $table->dropColumn('telegram_notifications_scheduled_tasks');
+ $table->dropColumn('smtp_notifications_scheduled_tasks');
+ $table->dropColumn('discord_notifications_scheduled_tasks');
+ $table->dropColumn('telegram_notifications_scheduled_tasks_thread_id');
+ });
+ }
+};
diff --git a/database/seeders/TestTeamSeeder.php b/database/seeders/TestTeamSeeder.php
new file mode 100644
index 000000000..1d660c713
--- /dev/null
+++ b/database/seeders/TestTeamSeeder.php
@@ -0,0 +1,42 @@
+create([
+ 'name' => '1 personal, 1 other team, owner, no other members',
+ 'email' => '1@example.com',
+ ]);
+ $team = Team::create([
+ 'name' => "1@example.com",
+ 'personal_team' => false,
+ 'show_boarding' => true
+ ]);
+ $user->teams()->attach($team, ['role' => 'owner']);
+
+ // User has 2 teams, 1 personal, 1 other where it is the owner and 1 other member is in the team
+ $user = User::factory()->create([
+ 'name' => 'owner: 1 personal, 1 other team, owner, 1 other member',
+ 'email' => '2@example.com',
+ ]);
+ $team = Team::create([
+ 'name' => "2@example.com",
+ 'personal_team' => false,
+ 'show_boarding' => true
+ ]);
+ $user->teams()->attach($team, ['role' => 'owner']);
+ $user = User::factory()->create([
+ 'name' => 'member: 1 personal, 1 other team, owner, 1 other member',
+ 'email' => '3@example.com',
+ ]);
+ $team->members()->attach($user, ['role' => 'member']);
+ }
+}
diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml
index 6d76a9abd..91e90b989 100644
--- a/docker-compose.dev.yml
+++ b/docker-compose.dev.yml
@@ -1,5 +1,3 @@
-version: "3.8"
-
services:
coolify:
build:
diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml
index f68b2c41c..f3dda9748 100644
--- a/docker-compose.prod.yml
+++ b/docker-compose.prod.yml
@@ -1,4 +1,3 @@
-version: '3.8'
services:
coolify:
image: "ghcr.io/coollabsio/coolify:${LATEST_IMAGE:-latest}"
diff --git a/docker-compose.windows.yml b/docker-compose.windows.yml
index e35ece624..af5ecc0f7 100644
--- a/docker-compose.windows.yml
+++ b/docker-compose.windows.yml
@@ -1,4 +1,3 @@
-version: '3.8'
services:
coolify-testing-host:
init: true
diff --git a/docker-compose.yml b/docker-compose.yml
index 6adfaf98a..8eed44f8c 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,4 +1,3 @@
-version: '3.8'
services:
coolify:
container_name: coolify
@@ -11,7 +10,6 @@ services:
depends_on:
- postgres
- redis
-
postgres:
image: postgres:15-alpine
container_name: coolify-db
diff --git a/docker/coolify-helper/Dockerfile b/docker/coolify-helper/Dockerfile
index 5798c92bd..768d2ca89 100644
--- a/docker/coolify-helper/Dockerfile
+++ b/docker/coolify-helper/Dockerfile
@@ -2,15 +2,15 @@ FROM alpine:3.17
ARG TARGETPLATFORM
# https://download.docker.com/linux/static/stable/
-ARG DOCKER_VERSION=24.0.9
+ARG DOCKER_VERSION=26.1.2
# https://github.com/docker/compose/releases
-ARG DOCKER_COMPOSE_VERSION=2.25.0
+ARG DOCKER_COMPOSE_VERSION=2.27.0
# https://github.com/docker/buildx/releases
-ARG DOCKER_BUILDX_VERSION=0.13.1
+ARG DOCKER_BUILDX_VERSION=0.14.0
# https://github.com/buildpacks/pack/releases
ARG PACK_VERSION=0.33.2
# https://github.com/railwayapp/nixpacks/releases
-ARG NIXPACKS_VERSION=1.21.2
+ARG NIXPACKS_VERSION=1.21.3
USER root
WORKDIR /artifacts
diff --git a/docker/dev-ssu/Dockerfile b/docker/dev-ssu/Dockerfile
index 0c7ce2b2a..f0e353d28 100644
--- a/docker/dev-ssu/Dockerfile
+++ b/docker/dev-ssu/Dockerfile
@@ -2,7 +2,7 @@ FROM serversideup/php:8.2-fpm-nginx-v2.2.1
ARG TARGETPLATFORM
# https://github.com/cloudflare/cloudflared/releases
-ARG CLOUDFLARED_VERSION=2024.2.1
+ARG CLOUDFLARED_VERSION=2024.4.1
ARG POSTGRES_VERSION=15
RUN apt-get update
diff --git a/docker/prod-ssu/Dockerfile b/docker/prod-ssu/Dockerfile
index dcc1c334d..2192f4f0e 100644
--- a/docker/prod-ssu/Dockerfile
+++ b/docker/prod-ssu/Dockerfile
@@ -15,7 +15,7 @@ FROM serversideup/php:8.2-fpm-nginx-v2.2.1
ARG TARGETPLATFORM
# https://github.com/cloudflare/cloudflared/releases
-ARG CLOUDFLARED_VERSION=2024.2.1
+ARG CLOUDFLARED_VERSION=2024.4.1
ARG POSTGRES_VERSION=15
WORKDIR /var/www/html
diff --git a/docker/testing-host/Dockerfile b/docker/testing-host/Dockerfile
index 6d0d0d5c5..deb09eeba 100644
--- a/docker/testing-host/Dockerfile
+++ b/docker/testing-host/Dockerfile
@@ -2,11 +2,11 @@ FROM debian:12-slim
ARG TARGETPLATFORM
# https://download.docker.com/linux/static/stable/
-ARG DOCKER_VERSION=24.0.5
+ARG DOCKER_VERSION=26.1.2
# https://github.com/docker/compose/releases
-ARG DOCKER_COMPOSE_VERSION=2.21.0
+ARG DOCKER_COMPOSE_VERSION=2.27.0
# https://github.com/docker/buildx/releases
-ARG DOCKER_BUILDX_VERSION=0.11.2
+ARG DOCKER_BUILDX_VERSION=0.14.0
USER root
WORKDIR /root
diff --git a/lang/zh-cn.json b/lang/zh-cn.json
new file mode 100644
index 000000000..70c457fa8
--- /dev/null
+++ b/lang/zh-cn.json
@@ -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": "示例
对于公共代码仓库,请使用 https://...。
对于私有代码仓库,请使用 git@...。
https://github.com/coollabsio/coolify-examples main 分支将被选择
https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify nodejs-fastify 分支将被选择。
https://gitea.com/sedlav/expressjs.git main 分支将被选择。
https://gitlab.com/andrasbacsai/nodejs-example.git main 分支将被选择"
+}
diff --git a/other/scripts/get-subs.php b/other/scripts/get-subs.php
new file mode 100644
index 000000000..3a23fc073
--- /dev/null
+++ b/other/scripts/get-subs.php
@@ -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);
diff --git a/public/svgs/listmonk.svg b/public/svgs/listmonk.svg
new file mode 100644
index 000000000..a4e5efd5f
--- /dev/null
+++ b/public/svgs/listmonk.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/public/svgs/twenty.svg b/public/svgs/twenty.svg
new file mode 100644
index 000000000..eef3a382a
--- /dev/null
+++ b/public/svgs/twenty.svg
@@ -0,0 +1 @@
+
diff --git a/public/svgs/vikunja.svg b/public/svgs/vikunja.svg
new file mode 100644
index 000000000..53176d66e
--- /dev/null
+++ b/public/svgs/vikunja.svg
@@ -0,0 +1,12 @@
+
diff --git a/resources/css/app.css b/resources/css/app.css
index 1f218490d..cae83b0de 100644
--- a/resources/css/app.css
+++ b/resources/css/app.css
@@ -32,7 +32,7 @@ .select {
@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;
}
@@ -41,7 +41,7 @@ option {
}
.button {
- @apply flex items-center justify-center gap-2 px-2 py-1 text-sm text-black normal-case border rounded cursor-pointer bg-neutral-200/50 border-neutral-300 hover:bg-neutral-300 dark:bg-coolgray-200 dark:text-white dark:hover:text-white dark:hover:bg-coolgray-500 dark:border-black hover:text-black disabled:cursor-not-allowed min-w-fit focus:outline-1 dark:disabled:text-neutral-600 disabled:border-none disabled:hover:bg-transparent disabled:bg-transparent disabled:text-neutral-300;
+ @apply flex items-center justify-center gap-2 px-2 py-1 text-sm text-black normal-case border rounded cursor-pointer bg-neutral-200/50 border-neutral-300 hover:bg-neutral-300 dark:bg-coolgray-200 dark:text-white dark:hover:text-white dark:hover:bg-coolgray-500 dark:border-coolgray-300 hover:text-black disabled:cursor-not-allowed min-w-fit focus:outline-1 dark:disabled:text-neutral-600 disabled:border-none disabled:hover:bg-transparent disabled:bg-transparent disabled:text-neutral-300;
}
button[isError]:not(:disabled) {
@@ -52,7 +52,6 @@ button[isHighlighted]:not(:disabled) {
@apply text-white bg-coollabs hover:bg-coollabs-100;
}
-
h1 {
@apply text-2xl font-bold dark:text-white;
}
@@ -78,7 +77,7 @@ label {
}
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 {
@@ -117,7 +116,7 @@ .alert-error {
@apply flex items-center gap-2 text-error;
}
.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 {
@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 @@ .badge {
.badge-absolute {
@apply absolute top-0 right-0 w-2 h-2 border-none rounded-t-none rounded-r-none;
-
}
.badge-success {
@@ -159,7 +157,7 @@ .menu {
}
.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 {
@@ -174,7 +172,6 @@ .icon {
@apply w-6 h-6 dark:hover:text-white;
}
-
.scrollbar {
@apply scrollbar-thumb-coollabs-100 dark:scrollbar-track-coolgray-200 scrollbar-track-neutral-200 scrollbar-w-2;
}
@@ -188,7 +185,7 @@ .custom-modal {
}
.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 {
@@ -203,20 +200,19 @@ .box {
@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 {
- @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 {
- @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 {
- @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 {
@apply rounded hover:bg-neutral-300 dark:hover:bg-coolgray-500/20;
}
-
.box-title {
@apply font-bold text-black dark:text-white group-hover:dark:text-white;
}
diff --git a/resources/views/auth/register.blade.php b/resources/views/auth/register.blade.php
index f57d8c6fc..e04d3633d 100644
--- a/resources/views/auth/register.blade.php
+++ b/resources/views/auth/register.blade.php
@@ -4,8 +4,7 @@
Coolify
-
{{ $description }}
++{{ $output }} ++ +Click [here]({{ $url }}) to view the task. +
Username should be