From f515870f36a4275eaff388acd87c4488e8359590 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Thu, 18 Sep 2025 16:51:08 +0200
Subject: [PATCH 01/15] fix(docker): enhance container status aggregation to
include restarting and exited states
---
app/Actions/Docker/GetContainersStatus.php | 24 +++++-
app/Actions/Shared/ComplexStatusCheck.php | 94 +++++++++++++++++++---
openapi.json | 5 +-
openapi.yaml | 4 +-
4 files changed, 112 insertions(+), 15 deletions(-)
diff --git a/app/Actions/Docker/GetContainersStatus.php b/app/Actions/Docker/GetContainersStatus.php
index ad7c4a606..f5d5f82b6 100644
--- a/app/Actions/Docker/GetContainersStatus.php
+++ b/app/Actions/Docker/GetContainersStatus.php
@@ -96,7 +96,11 @@ public function handle(Server $server, ?Collection $containers = null, ?Collecti
}
$containerStatus = data_get($container, 'State.Status');
$containerHealth = data_get($container, 'State.Health.Status', 'unhealthy');
- $containerStatus = "$containerStatus ($containerHealth)";
+ if ($containerStatus === 'restarting') {
+ $containerStatus = "restarting ($containerHealth)";
+ } else {
+ $containerStatus = "$containerStatus ($containerHealth)";
+ }
$labels = Arr::undot(format_docker_labels_to_json($labels));
$applicationId = data_get($labels, 'coolify.applicationId');
if ($applicationId) {
@@ -386,19 +390,33 @@ private function aggregateApplicationStatus($application, Collection $containerS
return null;
}
- // Aggregate status: if any container is running, app is running
$hasRunning = false;
+ $hasRestarting = false;
$hasUnhealthy = false;
+ $hasExited = false;
foreach ($relevantStatuses as $status) {
- if (str($status)->contains('running')) {
+ if (str($status)->contains('restarting')) {
+ $hasRestarting = true;
+ } elseif (str($status)->contains('running')) {
$hasRunning = true;
if (str($status)->contains('unhealthy')) {
$hasUnhealthy = true;
}
+ } elseif (str($status)->contains('exited')) {
+ $hasExited = true;
+ $hasUnhealthy = true;
}
}
+ if ($hasRestarting) {
+ return 'degraded (unhealthy)';
+ }
+
+ if ($hasRunning && $hasExited) {
+ return 'degraded (unhealthy)';
+ }
+
if ($hasRunning) {
return $hasUnhealthy ? 'running (unhealthy)' : 'running (healthy)';
}
diff --git a/app/Actions/Shared/ComplexStatusCheck.php b/app/Actions/Shared/ComplexStatusCheck.php
index 5a7ba6637..e06136e3c 100644
--- a/app/Actions/Shared/ComplexStatusCheck.php
+++ b/app/Actions/Shared/ComplexStatusCheck.php
@@ -26,22 +26,22 @@ public function handle(Application $application)
continue;
}
}
- $container = instant_remote_process(["docker container inspect $(docker container ls -q --filter 'label=coolify.applicationId={$application->id}' --filter 'label=coolify.pullRequestId=0') --format '{{json .}}'"], $server, false);
- $container = format_docker_command_output_to_json($container);
- if ($container->count() === 1) {
- $container = $container->first();
- $containerStatus = data_get($container, 'State.Status');
- $containerHealth = data_get($container, 'State.Health.Status', 'unhealthy');
+ $containers = instant_remote_process(["docker container inspect $(docker container ls -q --filter 'label=coolify.applicationId={$application->id}' --filter 'label=coolify.pullRequestId=0') --format '{{json .}}'"], $server, false);
+ $containers = format_docker_command_output_to_json($containers);
+
+ if ($containers->count() > 0) {
+ $statusToSet = $this->aggregateContainerStatuses($application, $containers);
+
if ($is_main_server) {
$statusFromDb = $application->status;
- if ($statusFromDb !== $containerStatus) {
- $application->update(['status' => "$containerStatus:$containerHealth"]);
+ if ($statusFromDb !== $statusToSet) {
+ $application->update(['status' => $statusToSet]);
}
} else {
$additional_server = $application->additional_servers()->wherePivot('server_id', $server->id);
$statusFromDb = $additional_server->first()->pivot->status;
- if ($statusFromDb !== $containerStatus) {
- $additional_server->updateExistingPivot($server->id, ['status' => "$containerStatus:$containerHealth"]);
+ if ($statusFromDb !== $statusToSet) {
+ $additional_server->updateExistingPivot($server->id, ['status' => $statusToSet]);
}
}
} else {
@@ -57,4 +57,78 @@ public function handle(Application $application)
}
}
}
+
+ private function aggregateContainerStatuses($application, $containers)
+ {
+ $dockerComposeRaw = data_get($application, 'docker_compose_raw');
+ $excludedContainers = collect();
+
+ if ($dockerComposeRaw) {
+ try {
+ $dockerCompose = \Symfony\Component\Yaml\Yaml::parse($dockerComposeRaw);
+ $services = data_get($dockerCompose, 'services', []);
+
+ foreach ($services as $serviceName => $serviceConfig) {
+ $excludeFromHc = data_get($serviceConfig, 'exclude_from_hc', false);
+ $restartPolicy = data_get($serviceConfig, 'restart', 'always');
+
+ if ($excludeFromHc || $restartPolicy === 'no') {
+ $excludedContainers->push($serviceName);
+ }
+ }
+ } catch (\Exception $e) {
+ // If we can't parse, treat all containers as included
+ }
+ }
+
+ $hasRunning = false;
+ $hasRestarting = false;
+ $hasUnhealthy = false;
+ $hasExited = false;
+ $relevantContainerCount = 0;
+
+ foreach ($containers as $container) {
+ $labels = data_get($container, 'Config.Labels', []);
+ $serviceName = data_get($labels, 'com.docker.compose.service');
+
+ if ($serviceName && $excludedContainers->contains($serviceName)) {
+ continue;
+ }
+
+ $relevantContainerCount++;
+ $containerStatus = data_get($container, 'State.Status');
+ $containerHealth = data_get($container, 'State.Health.Status', 'unhealthy');
+
+ if ($containerStatus === 'restarting') {
+ $hasRestarting = true;
+ $hasUnhealthy = true;
+ } elseif ($containerStatus === 'running') {
+ $hasRunning = true;
+ if ($containerHealth === 'unhealthy') {
+ $hasUnhealthy = true;
+ }
+ } elseif ($containerStatus === 'exited') {
+ $hasExited = true;
+ $hasUnhealthy = true;
+ }
+ }
+
+ if ($relevantContainerCount === 0) {
+ return 'running:healthy';
+ }
+
+ if ($hasRestarting) {
+ return 'degraded:unhealthy';
+ }
+
+ if ($hasRunning && $hasExited) {
+ return 'degraded:unhealthy';
+ }
+
+ if ($hasRunning) {
+ return $hasUnhealthy ? 'running:unhealthy' : 'running:healthy';
+ }
+
+ return 'exited:unhealthy';
+ }
}
diff --git a/openapi.json b/openapi.json
index d5b3b14c4..2b0a81c6e 100644
--- a/openapi.json
+++ b/openapi.json
@@ -8360,7 +8360,10 @@
"is_preview": {
"type": "boolean"
},
- "is_buildtime_only": {
+ "is_runtime": {
+ "type": "boolean"
+ },
+ "is_buildtime": {
"type": "boolean"
},
"is_shared": {
diff --git a/openapi.yaml b/openapi.yaml
index 69848d99a..9529fcf87 100644
--- a/openapi.yaml
+++ b/openapi.yaml
@@ -5411,7 +5411,9 @@ components:
type: boolean
is_preview:
type: boolean
- is_buildtime_only:
+ is_runtime:
+ type: boolean
+ is_buildtime:
type: boolean
is_shared:
type: boolean
From f33df13c4eebe15c2c08500bf4a22860820a5d52 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Thu, 18 Sep 2025 18:14:54 +0200
Subject: [PATCH 02/15] feat(environment): replace is_buildtime_only with
is_runtime and is_buildtime flags for environment variables, updating related
logic and views
---
.../Api/ApplicationsController.php | 40 +++++++----
.../Shared/EnvironmentVariable/Add.php | 16 +++--
.../Shared/EnvironmentVariable/All.php | 3 +-
.../Shared/EnvironmentVariable/Show.php | 13 ++--
app/Models/Application.php | 6 +-
app/Models/EnvironmentVariable.php | 34 +++++++++-
...ildtime_to_environment_variables_table.php | 67 +++++++++++++++++++
.../shared/environment-variable/add.blade.php | 13 ++--
.../environment-variable/show.blade.php | 50 +++++++++-----
9 files changed, 192 insertions(+), 50 deletions(-)
create mode 100644 database/migrations/2025_09_18_080152_add_runtime_and_buildtime_to_environment_variables_table.php
diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php
index b9c854ea1..cd640df17 100644
--- a/app/Http/Controllers/Api/ApplicationsController.php
+++ b/app/Http/Controllers/Api/ApplicationsController.php
@@ -2532,8 +2532,11 @@ public function update_env_by_uuid(Request $request)
if ($env->is_shown_once != $request->is_shown_once) {
$env->is_shown_once = $request->is_shown_once;
}
- if ($request->has('is_buildtime_only') && $env->is_buildtime_only != $request->is_buildtime_only) {
- $env->is_buildtime_only = $request->is_buildtime_only;
+ if ($request->has('is_runtime') && $env->is_runtime != $request->is_runtime) {
+ $env->is_runtime = $request->is_runtime;
+ }
+ if ($request->has('is_buildtime') && $env->is_buildtime != $request->is_buildtime) {
+ $env->is_buildtime = $request->is_buildtime;
}
$env->save();
@@ -2559,8 +2562,11 @@ public function update_env_by_uuid(Request $request)
if ($env->is_shown_once != $request->is_shown_once) {
$env->is_shown_once = $request->is_shown_once;
}
- if ($request->has('is_buildtime_only') && $env->is_buildtime_only != $request->is_buildtime_only) {
- $env->is_buildtime_only = $request->is_buildtime_only;
+ if ($request->has('is_runtime') && $env->is_runtime != $request->is_runtime) {
+ $env->is_runtime = $request->is_runtime;
+ }
+ if ($request->has('is_buildtime') && $env->is_buildtime != $request->is_buildtime) {
+ $env->is_buildtime = $request->is_buildtime;
}
$env->save();
@@ -2723,8 +2729,11 @@ public function create_bulk_envs(Request $request)
if ($env->is_shown_once != $item->get('is_shown_once')) {
$env->is_shown_once = $item->get('is_shown_once');
}
- if ($item->has('is_buildtime_only') && $env->is_buildtime_only != $item->get('is_buildtime_only')) {
- $env->is_buildtime_only = $item->get('is_buildtime_only');
+ if ($item->has('is_runtime') && $env->is_runtime != $item->get('is_runtime')) {
+ $env->is_runtime = $item->get('is_runtime');
+ }
+ if ($item->has('is_buildtime') && $env->is_buildtime != $item->get('is_buildtime')) {
+ $env->is_buildtime = $item->get('is_buildtime');
}
$env->save();
} else {
@@ -2735,7 +2744,8 @@ public function create_bulk_envs(Request $request)
'is_literal' => $is_literal,
'is_multiline' => $is_multi_line,
'is_shown_once' => $is_shown_once,
- 'is_buildtime_only' => $item->get('is_buildtime_only', false),
+ 'is_runtime' => $item->get('is_runtime', true),
+ 'is_buildtime' => $item->get('is_buildtime', true),
'resourceable_type' => get_class($application),
'resourceable_id' => $application->id,
]);
@@ -2753,8 +2763,11 @@ public function create_bulk_envs(Request $request)
if ($env->is_shown_once != $item->get('is_shown_once')) {
$env->is_shown_once = $item->get('is_shown_once');
}
- if ($item->has('is_buildtime_only') && $env->is_buildtime_only != $item->get('is_buildtime_only')) {
- $env->is_buildtime_only = $item->get('is_buildtime_only');
+ if ($item->has('is_runtime') && $env->is_runtime != $item->get('is_runtime')) {
+ $env->is_runtime = $item->get('is_runtime');
+ }
+ if ($item->has('is_buildtime') && $env->is_buildtime != $item->get('is_buildtime')) {
+ $env->is_buildtime = $item->get('is_buildtime');
}
$env->save();
} else {
@@ -2765,7 +2778,8 @@ public function create_bulk_envs(Request $request)
'is_literal' => $is_literal,
'is_multiline' => $is_multi_line,
'is_shown_once' => $is_shown_once,
- 'is_buildtime_only' => $item->get('is_buildtime_only', false),
+ 'is_runtime' => $item->get('is_runtime', true),
+ 'is_buildtime' => $item->get('is_buildtime', true),
'resourceable_type' => get_class($application),
'resourceable_id' => $application->id,
]);
@@ -2904,7 +2918,8 @@ public function create_env(Request $request)
'is_literal' => $request->is_literal ?? false,
'is_multiline' => $request->is_multiline ?? false,
'is_shown_once' => $request->is_shown_once ?? false,
- 'is_buildtime_only' => $request->is_buildtime_only ?? false,
+ 'is_runtime' => $request->is_runtime ?? true,
+ 'is_buildtime' => $request->is_buildtime ?? true,
'resourceable_type' => get_class($application),
'resourceable_id' => $application->id,
]);
@@ -2927,7 +2942,8 @@ public function create_env(Request $request)
'is_literal' => $request->is_literal ?? false,
'is_multiline' => $request->is_multiline ?? false,
'is_shown_once' => $request->is_shown_once ?? false,
- 'is_buildtime_only' => $request->is_buildtime_only ?? false,
+ 'is_runtime' => $request->is_runtime ?? true,
+ 'is_buildtime' => $request->is_buildtime ?? true,
'resourceable_type' => get_class($application),
'resourceable_id' => $application->id,
]);
diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/Add.php b/app/Livewire/Project/Shared/EnvironmentVariable/Add.php
index 9d5a5a39f..23a2cd59d 100644
--- a/app/Livewire/Project/Shared/EnvironmentVariable/Add.php
+++ b/app/Livewire/Project/Shared/EnvironmentVariable/Add.php
@@ -23,7 +23,9 @@ class Add extends Component
public bool $is_literal = false;
- public bool $is_buildtime_only = false;
+ public bool $is_runtime = true;
+
+ public bool $is_buildtime = true;
protected $listeners = ['clearAddEnv' => 'clear'];
@@ -32,7 +34,8 @@ class Add extends Component
'value' => 'nullable',
'is_multiline' => 'required|boolean',
'is_literal' => 'required|boolean',
- 'is_buildtime_only' => 'required|boolean',
+ 'is_runtime' => 'required|boolean',
+ 'is_buildtime' => 'required|boolean',
];
protected $validationAttributes = [
@@ -40,7 +43,8 @@ class Add extends Component
'value' => 'value',
'is_multiline' => 'multiline',
'is_literal' => 'literal',
- 'is_buildtime_only' => 'buildtime only',
+ 'is_runtime' => 'runtime',
+ 'is_buildtime' => 'buildtime',
];
public function mount()
@@ -56,7 +60,8 @@ public function submit()
'value' => $this->value,
'is_multiline' => $this->is_multiline,
'is_literal' => $this->is_literal,
- 'is_buildtime_only' => $this->is_buildtime_only,
+ 'is_runtime' => $this->is_runtime,
+ 'is_buildtime' => $this->is_buildtime,
'is_preview' => $this->is_preview,
]);
$this->clear();
@@ -68,6 +73,7 @@ public function clear()
$this->value = '';
$this->is_multiline = false;
$this->is_literal = false;
- $this->is_buildtime_only = false;
+ $this->is_runtime = true;
+ $this->is_buildtime = true;
}
}
diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/All.php b/app/Livewire/Project/Shared/EnvironmentVariable/All.php
index a71400f4c..639c025c7 100644
--- a/app/Livewire/Project/Shared/EnvironmentVariable/All.php
+++ b/app/Livewire/Project/Shared/EnvironmentVariable/All.php
@@ -221,7 +221,8 @@ private function createEnvironmentVariable($data)
$environment->value = $data['value'];
$environment->is_multiline = $data['is_multiline'] ?? false;
$environment->is_literal = $data['is_literal'] ?? false;
- $environment->is_buildtime_only = $data['is_buildtime_only'] ?? false;
+ $environment->is_runtime = $data['is_runtime'] ?? true;
+ $environment->is_buildtime = $data['is_buildtime'] ?? true;
$environment->is_preview = $data['is_preview'] ?? false;
$environment->resourceable_id = $this->resource->id;
$environment->resourceable_type = $this->resource->getMorphClass();
diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php
index ab70b70f4..0d0467c13 100644
--- a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php
+++ b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php
@@ -38,7 +38,9 @@ class Show extends Component
public bool $is_shown_once = false;
- public bool $is_buildtime_only = false;
+ public bool $is_runtime = true;
+
+ public bool $is_buildtime = true;
public bool $is_required = false;
@@ -58,7 +60,8 @@ class Show extends Component
'is_multiline' => 'required|boolean',
'is_literal' => 'required|boolean',
'is_shown_once' => 'required|boolean',
- 'is_buildtime_only' => 'required|boolean',
+ 'is_runtime' => 'required|boolean',
+ 'is_buildtime' => 'required|boolean',
'real_value' => 'nullable',
'is_required' => 'required|boolean',
];
@@ -102,7 +105,8 @@ public function syncData(bool $toModel = false)
} else {
$this->validate();
$this->env->is_required = $this->is_required;
- $this->env->is_buildtime_only = $this->is_buildtime_only;
+ $this->env->is_runtime = $this->is_runtime;
+ $this->env->is_buildtime = $this->is_buildtime;
$this->env->is_shared = $this->is_shared;
}
$this->env->key = $this->key;
@@ -117,7 +121,8 @@ public function syncData(bool $toModel = false)
$this->is_multiline = $this->env->is_multiline;
$this->is_literal = $this->env->is_literal;
$this->is_shown_once = $this->env->is_shown_once;
- $this->is_buildtime_only = $this->env->is_buildtime_only ?? false;
+ $this->is_runtime = $this->env->is_runtime ?? true;
+ $this->is_buildtime = $this->env->is_buildtime ?? true;
$this->is_required = $this->env->is_required ?? false;
$this->is_really_required = $this->env->is_really_required ?? false;
$this->is_shared = $this->env->is_shared ?? false;
diff --git a/app/Models/Application.php b/app/Models/Application.php
index 1f48e0211..07df53687 100644
--- a/app/Models/Application.php
+++ b/app/Models/Application.php
@@ -932,11 +932,11 @@ public function isLogDrainEnabled()
public function isConfigurationChanged(bool $save = false)
{
- $newConfigHash = base64_encode($this->fqdn.$this->git_repository.$this->git_branch.$this->git_commit_sha.$this->build_pack.$this->static_image.$this->install_command.$this->build_command.$this->start_command.$this->ports_exposes.$this->ports_mappings.$this->base_directory.$this->publish_directory.$this->dockerfile.$this->dockerfile_location.$this->custom_labels.$this->custom_docker_run_options.$this->dockerfile_target_build.$this->redirect.$this->custom_nginx_configuration.$this->custom_labels);
+ $newConfigHash = base64_encode($this->fqdn.$this->git_repository.$this->git_branch.$this->git_commit_sha.$this->build_pack.$this->static_image.$this->install_command.$this->build_command.$this->start_command.$this->ports_exposes.$this->ports_mappings.$this->base_directory.$this->publish_directory.$this->dockerfile.$this->dockerfile_location.$this->custom_labels.$this->custom_docker_run_options.$this->dockerfile_target_build.$this->redirect.$this->custom_nginx_configuration.$this->custom_labels.$this->settings->use_build_secrets);
if ($this->pull_request_id === 0 || $this->pull_request_id === null) {
- $newConfigHash .= json_encode($this->environment_variables()->get(['value', 'is_multiline', 'is_literal'])->sort());
+ $newConfigHash .= json_encode($this->environment_variables()->get(['value', 'is_multiline', 'is_literal', 'is_buildtime', 'is_runtime'])->sort());
} else {
- $newConfigHash .= json_encode($this->environment_variables_preview->get(['value', 'is_multiline', 'is_literal'])->sort());
+ $newConfigHash .= json_encode($this->environment_variables_preview->get(['value', 'is_multiline', 'is_literal', 'is_buildtime', 'is_runtime'])->sort());
}
$newConfigHash = md5($newConfigHash);
$oldConfigHash = data_get($this, 'config_hash');
diff --git a/app/Models/EnvironmentVariable.php b/app/Models/EnvironmentVariable.php
index 85fcdcecb..80399a16b 100644
--- a/app/Models/EnvironmentVariable.php
+++ b/app/Models/EnvironmentVariable.php
@@ -17,7 +17,8 @@
'is_literal' => ['type' => 'boolean'],
'is_multiline' => ['type' => 'boolean'],
'is_preview' => ['type' => 'boolean'],
- 'is_buildtime_only' => ['type' => 'boolean'],
+ 'is_runtime' => ['type' => 'boolean'],
+ 'is_buildtime' => ['type' => 'boolean'],
'is_shared' => ['type' => 'boolean'],
'is_shown_once' => ['type' => 'boolean'],
'key' => ['type' => 'string'],
@@ -37,13 +38,14 @@ class EnvironmentVariable extends BaseModel
'value' => 'encrypted',
'is_multiline' => 'boolean',
'is_preview' => 'boolean',
- 'is_buildtime_only' => 'boolean',
+ 'is_runtime' => 'boolean',
+ 'is_buildtime' => 'boolean',
'version' => 'string',
'resourceable_type' => 'string',
'resourceable_id' => 'integer',
];
- protected $appends = ['real_value', 'is_shared', 'is_really_required'];
+ protected $appends = ['real_value', 'is_shared', 'is_really_required', 'is_nixpacks', 'is_coolify'];
protected static function booted()
{
@@ -137,6 +139,32 @@ protected function isReallyRequired(): Attribute
);
}
+ protected function isNixpacks(): Attribute
+ {
+ return Attribute::make(
+ get: function () {
+ if (str($this->key)->startsWith('NIXPACKS_')) {
+ return true;
+ }
+
+ return false;
+ }
+ );
+ }
+
+ protected function isCoolify(): Attribute
+ {
+ return Attribute::make(
+ get: function () {
+ if (str($this->key)->startsWith('SERVICE_')) {
+ return true;
+ }
+
+ return false;
+ }
+ );
+ }
+
protected function isShared(): Attribute
{
return Attribute::make(
diff --git a/database/migrations/2025_09_18_080152_add_runtime_and_buildtime_to_environment_variables_table.php b/database/migrations/2025_09_18_080152_add_runtime_and_buildtime_to_environment_variables_table.php
new file mode 100644
index 000000000..6fd4bfed6
--- /dev/null
+++ b/database/migrations/2025_09_18_080152_add_runtime_and_buildtime_to_environment_variables_table.php
@@ -0,0 +1,67 @@
+boolean('is_runtime')->default(true)->after('is_buildtime_only');
+ $table->boolean('is_buildtime')->default(true)->after('is_runtime');
+ });
+
+ // Migrate existing data from is_buildtime_only to new fields
+ DB::table('environment_variables')
+ ->where('is_buildtime_only', true)
+ ->update([
+ 'is_runtime' => false,
+ 'is_buildtime' => true,
+ ]);
+
+ DB::table('environment_variables')
+ ->where('is_buildtime_only', false)
+ ->update([
+ 'is_runtime' => true,
+ 'is_buildtime' => true,
+ ]);
+
+ // Remove the old is_buildtime_only column
+ Schema::table('environment_variables', function (Blueprint $table) {
+ $table->dropColumn('is_buildtime_only');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('environment_variables', function (Blueprint $table) {
+ // Re-add the is_buildtime_only column
+ $table->boolean('is_buildtime_only')->default(false)->after('is_preview');
+ });
+
+ // Restore data to is_buildtime_only based on new fields
+ DB::table('environment_variables')
+ ->where('is_runtime', false)
+ ->where('is_buildtime', true)
+ ->update(['is_buildtime_only' => true]);
+
+ DB::table('environment_variables')
+ ->where('is_runtime', true)
+ ->update(['is_buildtime_only' => false]);
+
+ // Remove new columns
+ Schema::table('environment_variables', function (Blueprint $table) {
+ $table->dropColumn(['is_runtime', 'is_buildtime']);
+ });
+ }
+};
diff --git a/resources/views/livewire/project/shared/environment-variable/add.blade.php b/resources/views/livewire/project/shared/environment-variable/add.blade.php
index 5af9e6318..cd156634e 100644
--- a/resources/views/livewire/project/shared/environment-variable/add.blade.php
+++ b/resources/views/livewire/project/shared/environment-variable/add.blade.php
@@ -3,15 +3,18 @@
-
- @if (!$shared)
-
+ @if (!$shared || $isNixpacks)
+
+
@endif
+
+
Save
diff --git a/resources/views/livewire/project/shared/environment-variable/show.blade.php b/resources/views/livewire/project/shared/environment-variable/show.blade.php
index 688ddf7ee..6b2540b62 100644
--- a/resources/views/livewire/project/shared/environment-variable/show.blade.php
+++ b/resources/views/livewire/project/shared/environment-variable/show.blade.php
@@ -58,9 +58,12 @@
@if (!$is_redis_credential)
@if ($type === 'service')
-
+
+
@else
-
-
- @if ($is_multiline === false)
-
+ @if (!$env->is_coolify)
+
+ @if (!$env->is_nixpacks)
+
+
+ @if ($is_multiline === false)
+
+ @endif
+ @endif
@endif
@endif
@endif
@@ -120,9 +130,12 @@
@if (!$is_redis_credential)
@if ($type === 'service')
-
+
+
@else
-
+
+
@if ($is_multiline === false)
Date: Thu, 18 Sep 2025 18:15:20 +0200
Subject: [PATCH 03/15] feat(deployment): handle buildtime and runtime
variables during deployment
---
app/Jobs/ApplicationDeploymentJob.php | 118 +++++++++++++++++++-------
1 file changed, 88 insertions(+), 30 deletions(-)
diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php
index 3d2fd5b04..ae89649af 100644
--- a/app/Jobs/ApplicationDeploymentJob.php
+++ b/app/Jobs/ApplicationDeploymentJob.php
@@ -169,6 +169,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private bool $dockerBuildkitSupported = false;
+ private bool $skip_build = false;
+
private Collection|string $build_secrets;
public function tags()
@@ -566,7 +568,7 @@ private function deploy_docker_compose_buildpack()
if ($this->application->settings->is_raw_compose_deployment_enabled) {
$this->application->oldRawParser();
$yaml = $composeFile = $this->application->docker_compose_raw;
- $this->save_environment_variables();
+ $this->generate_runtime_environment_variables();
// For raw compose, we cannot automatically add secrets configuration
// User must define it manually in their docker-compose file
@@ -575,7 +577,7 @@ private function deploy_docker_compose_buildpack()
}
} else {
$composeFile = $this->application->parse(pull_request_id: $this->pull_request_id, preview_id: data_get($this->preview, 'id'));
- $this->save_environment_variables();
+ $this->generate_runtime_environment_variables();
if (filled($this->env_filename)) {
$services = collect(data_get($composeFile, 'services', []));
$services = $services->map(function ($service, $name) {
@@ -759,6 +761,10 @@ private function deploy_nixpacks_buildpack()
$this->generate_compose_file();
$this->generate_build_env_variables();
$this->build_image();
+
+ // For Nixpacks, save runtime environment variables AFTER the build
+ // to prevent them from being accessible during the build process
+ $this->save_runtime_environment_variables();
$this->push_to_docker_registry();
$this->rolling_update();
}
@@ -952,18 +958,17 @@ private function should_skip_build()
{
if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty()) {
if ($this->is_this_additional_server) {
+ $this->skip_build = true;
$this->application_deployment_queue->addLogEntry("Image found ({$this->production_image_name}) with the same Git Commit SHA. Build step skipped.");
$this->generate_compose_file();
$this->push_to_docker_registry();
$this->rolling_update();
- if ($this->restart_only) {
- $this->post_deployment();
- }
return true;
}
if (! $this->application->isConfigurationChanged()) {
$this->application_deployment_queue->addLogEntry("No configuration changed & image found ({$this->production_image_name}) with the same Git Commit SHA. Build step skipped.");
+ $this->skip_build = true;
$this->generate_compose_file();
$this->push_to_docker_registry();
$this->rolling_update();
@@ -1004,7 +1009,7 @@ private function check_image_locally_or_remotely()
}
}
- private function save_environment_variables()
+ private function generate_runtime_environment_variables()
{
$envs = collect([]);
$sort = $this->application->settings->is_env_sorting_enabled;
@@ -1061,9 +1066,9 @@ private function save_environment_variables()
}
}
- // Filter out buildtime-only variables from runtime environment
+ // Filter runtime variables (only include variables that are available at runtime)
$runtime_environment_variables = $sorted_environment_variables->filter(function ($env) {
- return ! $env->is_buildtime_only;
+ return $env->is_runtime;
});
// Sort runtime environment variables: those referencing SERVICE_ variables come after others
@@ -1117,9 +1122,9 @@ private function save_environment_variables()
}
}
- // Filter out buildtime-only variables from runtime environment for preview
+ // Filter runtime variables for preview (only include variables that are available at runtime)
$runtime_environment_variables_preview = $sorted_environment_variables_preview->filter(function ($env) {
- return ! $env->is_buildtime_only;
+ return $env->is_runtime;
});
// Sort runtime environment variables: those referencing SERVICE_ variables come after others
@@ -1176,13 +1181,53 @@ private function save_environment_variables()
}
$this->env_filename = null;
} else {
- $envs_base64 = base64_encode($envs->implode("\n"));
+ // For Nixpacks builds, we save the .env file AFTER the build to prevent
+ // runtime-only variables from being accessible during the build process
+ if ($this->application->build_pack !== 'nixpacks' || $this->skip_build) {
+ $envs_base64 = base64_encode($envs->implode("\n"));
+ $this->execute_remote_command(
+ [
+ executeInDocker($this->deployment_uuid, "echo '$envs_base64' | base64 -d | tee $this->workdir/{$this->env_filename} > /dev/null"),
+ ],
+
+ );
+ if ($this->use_build_server) {
+ $this->server = $this->original_server;
+ $this->execute_remote_command(
+ [
+ "echo '$envs_base64' | base64 -d | tee $this->configuration_dir/{$this->env_filename} > /dev/null",
+ ]
+ );
+ $this->server = $this->build_server;
+ } else {
+ $this->execute_remote_command(
+ [
+ "echo '$envs_base64' | base64 -d | tee $this->configuration_dir/{$this->env_filename} > /dev/null",
+ ]
+ );
+ }
+ }
+ }
+ $this->environment_variables = $envs;
+ }
+
+ private function save_runtime_environment_variables()
+ {
+ // This method saves the .env file with runtime variables
+ // It should be called AFTER the build for Nixpacks to prevent runtime-only variables
+ // from being accessible during the build process
+
+ if ($this->environment_variables && $this->environment_variables->isNotEmpty() && $this->env_filename) {
+ $envs_base64 = base64_encode($this->environment_variables->implode("\n"));
+
+ // Write .env file to workdir (for container runtime)
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "echo '$envs_base64' | base64 -d | tee $this->workdir/{$this->env_filename} > /dev/null"),
],
-
);
+
+ // Write .env file to configuration directory
if ($this->use_build_server) {
$this->server = $this->original_server;
$this->execute_remote_command(
@@ -1199,7 +1244,6 @@ private function save_environment_variables()
);
}
}
- $this->environment_variables = $envs;
}
private function elixir_finetunes()
@@ -1418,6 +1462,10 @@ private function deploy_pull_request()
$this->add_build_env_variables_to_dockerfile();
}
$this->build_image();
+ // For Nixpacks, save runtime environment variables AFTER the build
+ if ($this->application->build_pack === 'nixpacks') {
+ $this->save_runtime_environment_variables();
+ }
$this->push_to_docker_registry();
$this->rolling_update();
}
@@ -1681,6 +1729,7 @@ private function generate_nixpacks_confs()
{
$nixpacks_command = $this->nixpacks_build_cmd();
$this->application_deployment_queue->addLogEntry("Generating nixpacks configuration with: $nixpacks_command");
+
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, $nixpacks_command), 'save' => 'nixpacks_plan', 'hidden' => true],
[executeInDocker($this->deployment_uuid, "nixpacks detect {$this->workdir}"), 'save' => 'nixpacks_type', 'hidden' => true],
@@ -1700,6 +1749,7 @@ private function generate_nixpacks_confs()
$parsed = Toml::Parse($this->nixpacks_plan);
// Do any modifications here
+ // We need to generate envs here because nixpacks need to know to generate a proper Dockerfile
$this->generate_env_variables();
$merged_envs = collect(data_get($parsed, 'variables', []))->merge($this->env_args);
$aptPkgs = data_get($parsed, 'phases.setup.aptPkgs', []);
@@ -1872,13 +1922,13 @@ private function generate_env_variables()
$this->env_args->put('SOURCE_COMMIT', $this->commit);
$coolify_envs = $this->generate_coolify_env_variables();
- // Include ALL environment variables (both build-time and runtime) for all build packs
- // This deprecates the need for is_build_time flag
+ // For build process, include only environment variables where is_buildtime = true
if ($this->pull_request_id === 0) {
- // Get all environment variables except NIXPACKS_ prefixed ones for non-nixpacks builds
- $envs = $this->application->build_pack === 'nixpacks'
- ? $this->application->runtime_environment_variables
- : $this->application->environment_variables()->where('key', 'not like', 'NIXPACKS_%')->get();
+ // Get environment variables that are marked as available during build
+ $envs = $this->application->environment_variables()
+ ->where('key', 'not like', 'NIXPACKS_%')
+ ->where('is_buildtime', true)
+ ->get();
foreach ($envs as $env) {
if (! is_null($env->real_value)) {
@@ -1900,10 +1950,11 @@ private function generate_env_variables()
}
}
} else {
- // Get all preview environment variables except NIXPACKS_ prefixed ones for non-nixpacks builds
- $envs = $this->application->build_pack === 'nixpacks'
- ? $this->application->runtime_environment_variables_preview
- : $this->application->environment_variables_preview()->where('key', 'not like', 'NIXPACKS_%')->get();
+ // Get preview environment variables that are marked as available during build
+ $envs = $this->application->environment_variables_preview()
+ ->where('key', 'not like', 'NIXPACKS_%')
+ ->where('is_buildtime', true)
+ ->get();
foreach ($envs as $env) {
if (! is_null($env->real_value)) {
@@ -1935,8 +1986,7 @@ private function generate_compose_file()
$persistent_storages = $this->generate_local_persistent_volumes();
$persistent_file_volumes = $this->application->fileStorages()->get();
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
- // $environment_variables = $this->generate_environment_variables($ports);
- $this->save_environment_variables();
+ $this->generate_runtime_environment_variables();
if (data_get($this->application, 'custom_labels')) {
$this->application->parseContainerLabels();
$labels = collect(preg_split("/\r\n|\n|\r/", base64_decode($this->application->custom_labels)));
@@ -2652,6 +2702,7 @@ private function generate_build_env_variables()
if ($this->application->build_pack === 'nixpacks') {
$variables = collect($this->nixpacks_plan_json->get('variables'));
} else {
+ // Generate environment variables for build process (filters by is_buildtime = true)
$this->generate_env_variables();
$variables = collect([])->merge($this->env_args);
}
@@ -2678,8 +2729,8 @@ private function generate_docker_env_flags_for_secrets()
}
$variables = $this->pull_request_id === 0
- ? $this->application->environment_variables()->where('key', 'not like', 'NIXPACKS_%')->get()
- : $this->application->environment_variables_preview()->where('key', 'not like', 'NIXPACKS_%')->get();
+ ? $this->application->environment_variables()->where('key', 'not like', 'NIXPACKS_%')->where('is_buildtime', true)->get()
+ : $this->application->environment_variables_preview()->where('key', 'not like', 'NIXPACKS_%')->where('is_buildtime', true)->get();
if ($variables->isEmpty()) {
return '';
@@ -2722,7 +2773,11 @@ private function add_build_env_variables_to_dockerfile()
$dockerfile = collect(str($this->saved_outputs->get('dockerfile'))->trim()->explode("\n"));
if ($this->pull_request_id === 0) {
- $envs = $this->application->environment_variables()->where('key', 'not like', 'NIXPACKS_%')->get();
+ // Only add environment variables that are available during build
+ $envs = $this->application->environment_variables()
+ ->where('key', 'not like', 'NIXPACKS_%')
+ ->where('is_buildtime', true)
+ ->get();
foreach ($envs as $env) {
if (data_get($env, 'is_multiline') === true) {
$dockerfile->splice(1, 0, ["ARG {$env->key}"]);
@@ -2731,8 +2786,11 @@ private function add_build_env_variables_to_dockerfile()
}
}
} else {
- // Get all preview environment variables except NIXPACKS_ prefixed ones
- $envs = $this->application->environment_variables_preview()->where('key', 'not like', 'NIXPACKS_%')->get();
+ // Only add preview environment variables that are available during build
+ $envs = $this->application->environment_variables_preview()
+ ->where('key', 'not like', 'NIXPACKS_%')
+ ->where('is_buildtime', true)
+ ->get();
foreach ($envs as $env) {
if (data_get($env, 'is_multiline') === true) {
$dockerfile->splice(1, 0, ["ARG {$env->key}"]);
From b0ff584ff4a08f990c0d1de8bbfa0127ec2c487a Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Thu, 18 Sep 2025 18:17:37 +0200
Subject: [PATCH 04/15] fix(environment): correct grammatical errors in helper
text for environment variable sorting checkbox
---
.../project/shared/environment-variable/all.blade.php | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/resources/views/livewire/project/shared/environment-variable/all.blade.php b/resources/views/livewire/project/shared/environment-variable/all.blade.php
index 61e496d12..cee6b291d 100644
--- a/resources/views/livewire/project/shared/environment-variable/all.blade.php
+++ b/resources/views/livewire/project/shared/environment-variable/all.blade.php
@@ -19,11 +19,11 @@
@can('manageEnvironment', $resource)
@else
@endcan
From 711c16f0e6e14db62f59a7fef025c24aad3f25bc Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Thu, 18 Sep 2025 18:25:36 +0200
Subject: [PATCH 05/15] refactor(environment): conditionally render Docker
Build Secrets checkbox based on build pack type
---
.../shared/environment-variable/all.blade.php | 24 ++++++++++---------
1 file changed, 13 insertions(+), 11 deletions(-)
diff --git a/resources/views/livewire/project/shared/environment-variable/all.blade.php b/resources/views/livewire/project/shared/environment-variable/all.blade.php
index cee6b291d..6854ffaa4 100644
--- a/resources/views/livewire/project/shared/environment-variable/all.blade.php
+++ b/resources/views/livewire/project/shared/environment-variable/all.blade.php
@@ -28,17 +28,19 @@
@endcan
@endif
-
- @can('manageEnvironment', $resource)
-
- @else
-
- @endcan
-
+ @if (data_get($resource, 'build_pack') !== 'dockercompose')
+
+ @can('manageEnvironment', $resource)
+
+ @else
+
+ @endcan
+
+ @endif
@endif
@if ($resource->type() === 'service' || $resource?->build_pack === 'dockercompose')
From 429c43f9e57c94aa703e9df62bf9513abd44e6c6 Mon Sep 17 00:00:00 2001
From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com>
Date: Thu, 18 Sep 2025 19:13:45 +0200
Subject: [PATCH 06/15] chore: change order of runtime and buildtime
---
.../project/shared/environment-variable/add.blade.php | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/resources/views/livewire/project/shared/environment-variable/add.blade.php b/resources/views/livewire/project/shared/environment-variable/add.blade.php
index cd156634e..104cb8003 100644
--- a/resources/views/livewire/project/shared/environment-variable/add.blade.php
+++ b/resources/views/livewire/project/shared/environment-variable/add.blade.php
@@ -4,11 +4,12 @@
@if (!$shared || $isNixpacks)
-
+
From c0ddf73b753e54cf69396ec0581d2393fa5ce34a Mon Sep 17 00:00:00 2001
From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com>
Date: Thu, 18 Sep 2025 19:14:34 +0200
Subject: [PATCH 07/15] fix(ui): change order and fix ui on small screens
---
.../environment-variable/show.blade.php | 171 +++++++++---------
1 file changed, 84 insertions(+), 87 deletions(-)
diff --git a/resources/views/livewire/project/shared/environment-variable/show.blade.php b/resources/views/livewire/project/shared/environment-variable/show.blade.php
index 6b2540b62..d141463db 100644
--- a/resources/views/livewire/project/shared/environment-variable/show.blade.php
+++ b/resources/views/livewire/project/shared/environment-variable/show.blade.php
@@ -55,117 +55,114 @@
@endcan
@can('update', $this->env)
-
- @if (!$is_redis_credential)
- @if ($type === 'service')
-
-
-
-
- @else
- @if ($is_shared)
+
+
+ @if (!$is_redis_credential)
+ @if ($type === 'service')
+
+
+
@else
- @if ($isSharedVariable)
-
+ @if ($is_shared)
+
@else
- @if (!$env->is_coolify)
-
- @if (!$env->is_nixpacks)
-
-
- @if ($is_multiline === false)
-
+ @if ($isSharedVariable)
+
+ @else
+ @if (!$env->is_coolify)
+ @if (!$env->is_nixpacks)
+
+
+ @if ($is_multiline === false)
+
+ @endif
@endif
+
@endif
@endif
@endif
@endif
@endif
- @endif
-
- @if ($isDisabled)
-
- Update
-
-
- Lock
-
-
- @else
-
- Update
-
-
- Lock
-
-
- @endif
+
+
+ @if ($isDisabled)
+ Update
+ Lock
+
+ @else
+ Update
+ Lock
+
+ @endif
+
@else
-
- @if (!$is_redis_credential)
- @if ($type === 'service')
-
-
+
+ @if (!$is_redis_credential)
+ @if ($type === 'service')
+
-
-
- @else
- @if ($is_shared)
+
+
+
@else
- @if ($isSharedVariable)
-
+ @if ($is_shared)
+
@else
-
-
-
- @if ($is_multiline === false)
-
+ @if ($isSharedVariable)
+
+ @else
+
+
+
+ @if ($is_multiline === false)
+
+ @endif
@endif
@endif
@endif
@endif
- @endif
-
+
@endcan
@endif
From b64de1b5cd104952a4041720c591ed9ee8157b91 Mon Sep 17 00:00:00 2001
From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com>
Date: Thu, 18 Sep 2025 19:56:46 +0200
Subject: [PATCH 08/15] fix: order for git deploy types
---
.../project/shared/environment-variable/show.blade.php | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/resources/views/livewire/project/shared/environment-variable/show.blade.php b/resources/views/livewire/project/shared/environment-variable/show.blade.php
index d141463db..6598b66ff 100644
--- a/resources/views/livewire/project/shared/environment-variable/show.blade.php
+++ b/resources/views/livewire/project/shared/environment-variable/show.blade.php
@@ -83,6 +83,11 @@
+ @endif
+
+ @if (!$env->is_nixpacks)
@if ($is_multiline === false)
@endif
@endif
-
@endif
@endif
@endif
@@ -132,7 +134,6 @@
-
Date: Fri, 19 Sep 2025 10:17:48 +0200
Subject: [PATCH 09/15] feat(search): implement global search functionality
with caching and modal interface
---
app/Livewire/GlobalSearch.php | 371 ++++++++++++++++++
app/Models/Application.php | 123 +++---
app/Models/Server.php | 3 +-
app/Models/Service.php | 3 +-
app/Models/StandaloneClickhouse.php | 8 +-
app/Models/StandaloneDragonfly.php | 8 +-
app/Models/StandaloneKeydb.php | 8 +-
app/Models/StandaloneMariadb.php | 8 +-
app/Models/StandaloneMongodb.php | 8 +-
app/Models/StandaloneMysql.php | 8 +-
app/Models/StandalonePostgresql.php | 8 +-
app/Models/StandaloneRedis.php | 8 +-
app/Traits/ClearsGlobalSearchCache.php | 53 +++
resources/views/components/navbar.blade.php | 27 +-
.../views/livewire/global-search.blade.php | 236 +++++++++++
15 files changed, 797 insertions(+), 83 deletions(-)
create mode 100644 app/Livewire/GlobalSearch.php
create mode 100644 app/Traits/ClearsGlobalSearchCache.php
create mode 100644 resources/views/livewire/global-search.blade.php
diff --git a/app/Livewire/GlobalSearch.php b/app/Livewire/GlobalSearch.php
new file mode 100644
index 000000000..3b3075fc9
--- /dev/null
+++ b/app/Livewire/GlobalSearch.php
@@ -0,0 +1,371 @@
+searchQuery = '';
+ $this->isModalOpen = false;
+ $this->searchResults = [];
+ $this->allSearchableItems = [];
+ }
+
+ public function openSearchModal()
+ {
+ $this->isModalOpen = true;
+ $this->loadSearchableItems();
+ $this->dispatch('search-modal-opened');
+ }
+
+ public function closeSearchModal()
+ {
+ $this->isModalOpen = false;
+ $this->searchQuery = '';
+ $this->searchResults = [];
+ }
+
+ public static function getCacheKey($teamId)
+ {
+ return 'global_search_items_'.$teamId;
+ }
+
+ public static function clearTeamCache($teamId)
+ {
+ Cache::forget(self::getCacheKey($teamId));
+ }
+
+ public function updatedSearchQuery()
+ {
+ $this->search();
+ }
+
+ private function loadSearchableItems()
+ {
+ // Try to get from Redis cache first
+ $cacheKey = self::getCacheKey(auth()->user()->currentTeam()->id);
+
+ $this->allSearchableItems = Cache::remember($cacheKey, 300, function () {
+ $items = collect();
+ $team = auth()->user()->currentTeam();
+
+ // Get all applications
+ $applications = Application::ownedByCurrentTeam()
+ ->with(['environment.project'])
+ ->get()
+ ->map(function ($app) {
+ // Collect all FQDNs from the application
+ $fqdns = collect([]);
+
+ // For regular applications
+ if ($app->fqdn) {
+ $fqdns = collect(explode(',', $app->fqdn))->map(fn ($fqdn) => trim($fqdn));
+ }
+
+ // For docker compose based applications
+ if ($app->build_pack === 'dockercompose' && $app->docker_compose_domains) {
+ try {
+ $composeDomains = json_decode($app->docker_compose_domains, true);
+ if (is_array($composeDomains)) {
+ foreach ($composeDomains as $serviceName => $domains) {
+ if (is_array($domains)) {
+ $fqdns = $fqdns->merge($domains);
+ }
+ }
+ }
+ } catch (\Exception $e) {
+ // Ignore JSON parsing errors
+ }
+ }
+
+ $fqdnsString = $fqdns->implode(' ');
+
+ return [
+ 'id' => $app->id,
+ 'name' => $app->name,
+ 'type' => 'application',
+ 'uuid' => $app->uuid,
+ 'description' => $app->description,
+ 'link' => $app->link(),
+ 'project' => $app->environment->project->name ?? null,
+ 'environment' => $app->environment->name ?? null,
+ 'fqdns' => $fqdns->take(2)->implode(', '), // Show first 2 FQDNs in UI
+ 'search_text' => strtolower($app->name.' '.$app->description.' '.$fqdnsString),
+ ];
+ });
+
+ // Get all services
+ $services = Service::ownedByCurrentTeam()
+ ->with(['environment.project', 'applications'])
+ ->get()
+ ->map(function ($service) {
+ // Collect all FQDNs from service applications
+ $fqdns = collect([]);
+ foreach ($service->applications as $app) {
+ if ($app->fqdn) {
+ $appFqdns = collect(explode(',', $app->fqdn))->map(fn ($fqdn) => trim($fqdn));
+ $fqdns = $fqdns->merge($appFqdns);
+ }
+ }
+ $fqdnsString = $fqdns->implode(' ');
+
+ return [
+ 'id' => $service->id,
+ 'name' => $service->name,
+ 'type' => 'service',
+ 'uuid' => $service->uuid,
+ 'description' => $service->description,
+ 'link' => $service->link(),
+ 'project' => $service->environment->project->name ?? null,
+ 'environment' => $service->environment->name ?? null,
+ 'fqdns' => $fqdns->take(2)->implode(', '), // Show first 2 FQDNs in UI
+ 'search_text' => strtolower($service->name.' '.$service->description.' '.$fqdnsString),
+ ];
+ });
+
+ // Get all standalone databases
+ $databases = collect();
+
+ // PostgreSQL
+ $databases = $databases->merge(
+ StandalonePostgresql::ownedByCurrentTeam()
+ ->with(['environment.project'])
+ ->get()
+ ->map(function ($db) {
+ return [
+ 'id' => $db->id,
+ 'name' => $db->name,
+ 'type' => 'database',
+ 'subtype' => 'postgresql',
+ 'uuid' => $db->uuid,
+ 'description' => $db->description,
+ 'link' => $db->link(),
+ 'project' => $db->environment->project->name ?? null,
+ 'environment' => $db->environment->name ?? null,
+ 'search_text' => strtolower($db->name.' postgresql '.$db->description),
+ ];
+ })
+ );
+
+ // MySQL
+ $databases = $databases->merge(
+ StandaloneMysql::ownedByCurrentTeam()
+ ->with(['environment.project'])
+ ->get()
+ ->map(function ($db) {
+ return [
+ 'id' => $db->id,
+ 'name' => $db->name,
+ 'type' => 'database',
+ 'subtype' => 'mysql',
+ 'uuid' => $db->uuid,
+ 'description' => $db->description,
+ 'link' => $db->link(),
+ 'project' => $db->environment->project->name ?? null,
+ 'environment' => $db->environment->name ?? null,
+ 'search_text' => strtolower($db->name.' mysql '.$db->description),
+ ];
+ })
+ );
+
+ // MariaDB
+ $databases = $databases->merge(
+ StandaloneMariadb::ownedByCurrentTeam()
+ ->with(['environment.project'])
+ ->get()
+ ->map(function ($db) {
+ return [
+ 'id' => $db->id,
+ 'name' => $db->name,
+ 'type' => 'database',
+ 'subtype' => 'mariadb',
+ 'uuid' => $db->uuid,
+ 'description' => $db->description,
+ 'link' => $db->link(),
+ 'project' => $db->environment->project->name ?? null,
+ 'environment' => $db->environment->name ?? null,
+ 'search_text' => strtolower($db->name.' mariadb '.$db->description),
+ ];
+ })
+ );
+
+ // MongoDB
+ $databases = $databases->merge(
+ StandaloneMongodb::ownedByCurrentTeam()
+ ->with(['environment.project'])
+ ->get()
+ ->map(function ($db) {
+ return [
+ 'id' => $db->id,
+ 'name' => $db->name,
+ 'type' => 'database',
+ 'subtype' => 'mongodb',
+ 'uuid' => $db->uuid,
+ 'description' => $db->description,
+ 'link' => $db->link(),
+ 'project' => $db->environment->project->name ?? null,
+ 'environment' => $db->environment->name ?? null,
+ 'search_text' => strtolower($db->name.' mongodb '.$db->description),
+ ];
+ })
+ );
+
+ // Redis
+ $databases = $databases->merge(
+ StandaloneRedis::ownedByCurrentTeam()
+ ->with(['environment.project'])
+ ->get()
+ ->map(function ($db) {
+ return [
+ 'id' => $db->id,
+ 'name' => $db->name,
+ 'type' => 'database',
+ 'subtype' => 'redis',
+ 'uuid' => $db->uuid,
+ 'description' => $db->description,
+ 'link' => $db->link(),
+ 'project' => $db->environment->project->name ?? null,
+ 'environment' => $db->environment->name ?? null,
+ 'search_text' => strtolower($db->name.' redis '.$db->description),
+ ];
+ })
+ );
+
+ // KeyDB
+ $databases = $databases->merge(
+ StandaloneKeydb::ownedByCurrentTeam()
+ ->with(['environment.project'])
+ ->get()
+ ->map(function ($db) {
+ return [
+ 'id' => $db->id,
+ 'name' => $db->name,
+ 'type' => 'database',
+ 'subtype' => 'keydb',
+ 'uuid' => $db->uuid,
+ 'description' => $db->description,
+ 'link' => $db->link(),
+ 'project' => $db->environment->project->name ?? null,
+ 'environment' => $db->environment->name ?? null,
+ 'search_text' => strtolower($db->name.' keydb '.$db->description),
+ ];
+ })
+ );
+
+ // Dragonfly
+ $databases = $databases->merge(
+ StandaloneDragonfly::ownedByCurrentTeam()
+ ->with(['environment.project'])
+ ->get()
+ ->map(function ($db) {
+ return [
+ 'id' => $db->id,
+ 'name' => $db->name,
+ 'type' => 'database',
+ 'subtype' => 'dragonfly',
+ 'uuid' => $db->uuid,
+ 'description' => $db->description,
+ 'link' => $db->link(),
+ 'project' => $db->environment->project->name ?? null,
+ 'environment' => $db->environment->name ?? null,
+ 'search_text' => strtolower($db->name.' dragonfly '.$db->description),
+ ];
+ })
+ );
+
+ // Clickhouse
+ $databases = $databases->merge(
+ StandaloneClickhouse::ownedByCurrentTeam()
+ ->with(['environment.project'])
+ ->get()
+ ->map(function ($db) {
+ return [
+ 'id' => $db->id,
+ 'name' => $db->name,
+ 'type' => 'database',
+ 'subtype' => 'clickhouse',
+ 'uuid' => $db->uuid,
+ 'description' => $db->description,
+ 'link' => $db->link(),
+ 'project' => $db->environment->project->name ?? null,
+ 'environment' => $db->environment->name ?? null,
+ 'search_text' => strtolower($db->name.' clickhouse '.$db->description),
+ ];
+ })
+ );
+
+ // Get all servers
+ $servers = Server::ownedByCurrentTeam()
+ ->get()
+ ->map(function ($server) {
+ return [
+ 'id' => $server->id,
+ 'name' => $server->name,
+ 'type' => 'server',
+ 'uuid' => $server->uuid,
+ 'description' => $server->description,
+ 'link' => $server->url(),
+ 'project' => null,
+ 'environment' => null,
+ 'search_text' => strtolower($server->name.' '.$server->ip.' '.$server->description),
+ ];
+ });
+
+ // Merge all collections
+ $items = $items->merge($applications)
+ ->merge($services)
+ ->merge($databases)
+ ->merge($servers);
+
+ return $items->toArray();
+ });
+ }
+
+ private function search()
+ {
+ if (strlen($this->searchQuery) < 2) {
+ $this->searchResults = [];
+
+ return;
+ }
+
+ $query = strtolower($this->searchQuery);
+
+ // Case-insensitive search in the items
+ $this->searchResults = collect($this->allSearchableItems)
+ ->filter(function ($item) use ($query) {
+ return str_contains($item['search_text'], $query);
+ })
+ ->take(20)
+ ->values()
+ ->toArray();
+ }
+
+ public function render()
+ {
+ return view('livewire.global-search');
+ }
+}
diff --git a/app/Models/Application.php b/app/Models/Application.php
index 07df53687..094e5c82b 100644
--- a/app/Models/Application.php
+++ b/app/Models/Application.php
@@ -4,6 +4,7 @@
use App\Enums\ApplicationDeploymentStatus;
use App\Services\ConfigurationGenerator;
+use App\Traits\ClearsGlobalSearchCache;
use App\Traits\HasConfiguration;
use App\Traits\HasSafeStringAttribute;
use Illuminate\Database\Eloquent\Casts\Attribute;
@@ -110,7 +111,7 @@
class Application extends BaseModel
{
- use HasConfiguration, HasFactory, HasSafeStringAttribute, SoftDeletes;
+ use ClearsGlobalSearchCache, HasConfiguration, HasFactory, HasSafeStringAttribute, SoftDeletes;
private static $parserVersion = '5';
@@ -123,66 +124,6 @@ class Application extends BaseModel
'http_basic_auth_password' => 'encrypted',
];
- public function customNetworkAliases(): Attribute
- {
- return Attribute::make(
- set: function ($value) {
- if (is_null($value) || $value === '') {
- return null;
- }
-
- // If it's already a JSON string, decode it
- if (is_string($value) && $this->isJson($value)) {
- $value = json_decode($value, true);
- }
-
- // If it's a string but not JSON, treat it as a comma-separated list
- if (is_string($value) && ! is_array($value)) {
- $value = explode(',', $value);
- }
-
- $value = collect($value)
- ->map(function ($alias) {
- if (is_string($alias)) {
- return str_replace(' ', '-', trim($alias));
- }
-
- return null;
- })
- ->filter()
- ->unique() // Remove duplicate values
- ->values()
- ->toArray();
-
- return empty($value) ? null : json_encode($value);
- },
- get: function ($value) {
- if (is_null($value)) {
- return null;
- }
-
- if (is_string($value) && $this->isJson($value)) {
- return json_decode($value, true);
- }
-
- return is_array($value) ? $value : [];
- }
- );
- }
-
- /**
- * Check if a string is a valid JSON
- */
- private function isJson($string)
- {
- if (! is_string($string)) {
- return false;
- }
- json_decode($string);
-
- return json_last_error() === JSON_ERROR_NONE;
- }
-
protected static function booted()
{
static::addGlobalScope('withRelations', function ($builder) {
@@ -250,6 +191,66 @@ protected static function booted()
});
}
+ public function customNetworkAliases(): Attribute
+ {
+ return Attribute::make(
+ set: function ($value) {
+ if (is_null($value) || $value === '') {
+ return null;
+ }
+
+ // If it's already a JSON string, decode it
+ if (is_string($value) && $this->isJson($value)) {
+ $value = json_decode($value, true);
+ }
+
+ // If it's a string but not JSON, treat it as a comma-separated list
+ if (is_string($value) && ! is_array($value)) {
+ $value = explode(',', $value);
+ }
+
+ $value = collect($value)
+ ->map(function ($alias) {
+ if (is_string($alias)) {
+ return str_replace(' ', '-', trim($alias));
+ }
+
+ return null;
+ })
+ ->filter()
+ ->unique() // Remove duplicate values
+ ->values()
+ ->toArray();
+
+ return empty($value) ? null : json_encode($value);
+ },
+ get: function ($value) {
+ if (is_null($value)) {
+ return null;
+ }
+
+ if (is_string($value) && $this->isJson($value)) {
+ return json_decode($value, true);
+ }
+
+ return is_array($value) ? $value : [];
+ }
+ );
+ }
+
+ /**
+ * Check if a string is a valid JSON
+ */
+ private function isJson($string)
+ {
+ if (! is_string($string)) {
+ return false;
+ }
+ json_decode($string);
+
+ return json_last_error() === JSON_ERROR_NONE;
+ }
+
public static function ownedByCurrentTeamAPI(int $teamId)
{
return Application::whereRelation('environment.project.team', 'id', $teamId)->orderBy('name');
diff --git a/app/Models/Server.php b/app/Models/Server.php
index cc5315c6f..829a4b5aa 100644
--- a/app/Models/Server.php
+++ b/app/Models/Server.php
@@ -13,6 +13,7 @@
use App\Notifications\Server\Reachable;
use App\Notifications\Server\Unreachable;
use App\Services\ConfigurationRepository;
+use App\Traits\ClearsGlobalSearchCache;
use App\Traits\HasSafeStringAttribute;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
@@ -55,7 +56,7 @@
class Server extends BaseModel
{
- use HasFactory, SchemalessAttributesTrait, SoftDeletes;
+ use ClearsGlobalSearchCache, HasFactory, SchemalessAttributesTrait, SoftDeletes;
public static $batch_counter = 0;
diff --git a/app/Models/Service.php b/app/Models/Service.php
index dd8d0ac7e..d42d471c6 100644
--- a/app/Models/Service.php
+++ b/app/Models/Service.php
@@ -3,6 +3,7 @@
namespace App\Models;
use App\Enums\ProcessStatus;
+use App\Traits\ClearsGlobalSearchCache;
use App\Traits\HasSafeStringAttribute;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -41,7 +42,7 @@
)]
class Service extends BaseModel
{
- use HasFactory, HasSafeStringAttribute, SoftDeletes;
+ use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
private static $parserVersion = '5';
diff --git a/app/Models/StandaloneClickhouse.php b/app/Models/StandaloneClickhouse.php
index 87c5c3422..146ee0a2d 100644
--- a/app/Models/StandaloneClickhouse.php
+++ b/app/Models/StandaloneClickhouse.php
@@ -2,6 +2,7 @@
namespace App\Models;
+use App\Traits\ClearsGlobalSearchCache;
use App\Traits\HasSafeStringAttribute;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -9,7 +10,7 @@
class StandaloneClickhouse extends BaseModel
{
- use HasFactory, HasSafeStringAttribute, SoftDeletes;
+ use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
protected $guarded = [];
@@ -43,6 +44,11 @@ protected static function booted()
});
}
+ public static function ownedByCurrentTeam()
+ {
+ return StandaloneClickhouse::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
+ }
+
protected function serverStatus(): Attribute
{
return Attribute::make(
diff --git a/app/Models/StandaloneDragonfly.php b/app/Models/StandaloneDragonfly.php
index 118c72726..90e7304f1 100644
--- a/app/Models/StandaloneDragonfly.php
+++ b/app/Models/StandaloneDragonfly.php
@@ -2,6 +2,7 @@
namespace App\Models;
+use App\Traits\ClearsGlobalSearchCache;
use App\Traits\HasSafeStringAttribute;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -9,7 +10,7 @@
class StandaloneDragonfly extends BaseModel
{
- use HasFactory, HasSafeStringAttribute, SoftDeletes;
+ use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
protected $guarded = [];
@@ -43,6 +44,11 @@ protected static function booted()
});
}
+ public static function ownedByCurrentTeam()
+ {
+ return StandaloneDragonfly::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
+ }
+
protected function serverStatus(): Attribute
{
return Attribute::make(
diff --git a/app/Models/StandaloneKeydb.php b/app/Models/StandaloneKeydb.php
index 9d674b6c2..ad0cabf7e 100644
--- a/app/Models/StandaloneKeydb.php
+++ b/app/Models/StandaloneKeydb.php
@@ -2,6 +2,7 @@
namespace App\Models;
+use App\Traits\ClearsGlobalSearchCache;
use App\Traits\HasSafeStringAttribute;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -9,7 +10,7 @@
class StandaloneKeydb extends BaseModel
{
- use HasFactory, HasSafeStringAttribute, SoftDeletes;
+ use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
protected $guarded = [];
@@ -43,6 +44,11 @@ protected static function booted()
});
}
+ public static function ownedByCurrentTeam()
+ {
+ return StandaloneKeydb::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
+ }
+
protected function serverStatus(): Attribute
{
return Attribute::make(
diff --git a/app/Models/StandaloneMariadb.php b/app/Models/StandaloneMariadb.php
index 616d536c1..3d9e38147 100644
--- a/app/Models/StandaloneMariadb.php
+++ b/app/Models/StandaloneMariadb.php
@@ -2,6 +2,7 @@
namespace App\Models;
+use App\Traits\ClearsGlobalSearchCache;
use App\Traits\HasSafeStringAttribute;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -10,7 +11,7 @@
class StandaloneMariadb extends BaseModel
{
- use HasFactory, HasSafeStringAttribute, SoftDeletes;
+ use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
protected $guarded = [];
@@ -44,6 +45,11 @@ protected static function booted()
});
}
+ public static function ownedByCurrentTeam()
+ {
+ return StandaloneMariadb::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
+ }
+
protected function serverStatus(): Attribute
{
return Attribute::make(
diff --git a/app/Models/StandaloneMongodb.php b/app/Models/StandaloneMongodb.php
index b26b6c967..7cccd332a 100644
--- a/app/Models/StandaloneMongodb.php
+++ b/app/Models/StandaloneMongodb.php
@@ -2,6 +2,7 @@
namespace App\Models;
+use App\Traits\ClearsGlobalSearchCache;
use App\Traits\HasSafeStringAttribute;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -9,7 +10,7 @@
class StandaloneMongodb extends BaseModel
{
- use HasFactory, HasSafeStringAttribute, SoftDeletes;
+ use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
protected $guarded = [];
@@ -46,6 +47,11 @@ protected static function booted()
});
}
+ public static function ownedByCurrentTeam()
+ {
+ return StandaloneMongodb::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
+ }
+
protected function serverStatus(): Attribute
{
return Attribute::make(
diff --git a/app/Models/StandaloneMysql.php b/app/Models/StandaloneMysql.php
index 7b6f1b94e..80269972f 100644
--- a/app/Models/StandaloneMysql.php
+++ b/app/Models/StandaloneMysql.php
@@ -2,6 +2,7 @@
namespace App\Models;
+use App\Traits\ClearsGlobalSearchCache;
use App\Traits\HasSafeStringAttribute;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -9,7 +10,7 @@
class StandaloneMysql extends BaseModel
{
- use HasFactory, HasSafeStringAttribute, SoftDeletes;
+ use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
protected $guarded = [];
@@ -44,6 +45,11 @@ protected static function booted()
});
}
+ public static function ownedByCurrentTeam()
+ {
+ return StandaloneMysql::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
+ }
+
protected function serverStatus(): Attribute
{
return Attribute::make(
diff --git a/app/Models/StandalonePostgresql.php b/app/Models/StandalonePostgresql.php
index f13e6ffab..acde7a20c 100644
--- a/app/Models/StandalonePostgresql.php
+++ b/app/Models/StandalonePostgresql.php
@@ -2,6 +2,7 @@
namespace App\Models;
+use App\Traits\ClearsGlobalSearchCache;
use App\Traits\HasSafeStringAttribute;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -9,7 +10,7 @@
class StandalonePostgresql extends BaseModel
{
- use HasFactory, HasSafeStringAttribute, SoftDeletes;
+ use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
protected $guarded = [];
@@ -44,6 +45,11 @@ protected static function booted()
});
}
+ public static function ownedByCurrentTeam()
+ {
+ return StandalonePostgresql::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
+ }
+
public function workdir()
{
return database_configuration_dir()."/{$this->uuid}";
diff --git a/app/Models/StandaloneRedis.php b/app/Models/StandaloneRedis.php
index 9f7c96a08..001ebe36a 100644
--- a/app/Models/StandaloneRedis.php
+++ b/app/Models/StandaloneRedis.php
@@ -2,6 +2,7 @@
namespace App\Models;
+use App\Traits\ClearsGlobalSearchCache;
use App\Traits\HasSafeStringAttribute;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -9,7 +10,7 @@
class StandaloneRedis extends BaseModel
{
- use HasFactory, HasSafeStringAttribute, SoftDeletes;
+ use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
protected $guarded = [];
@@ -45,6 +46,11 @@ protected static function booted()
});
}
+ public static function ownedByCurrentTeam()
+ {
+ return StandaloneRedis::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
+ }
+
protected function serverStatus(): Attribute
{
return Attribute::make(
diff --git a/app/Traits/ClearsGlobalSearchCache.php b/app/Traits/ClearsGlobalSearchCache.php
new file mode 100644
index 000000000..fe6cbaa38
--- /dev/null
+++ b/app/Traits/ClearsGlobalSearchCache.php
@@ -0,0 +1,53 @@
+getTeamIdForCache();
+ if (filled($teamId)) {
+ GlobalSearch::clearTeamCache($teamId);
+ }
+ });
+
+ static::created(function ($model) {
+ // Clear search cache when model is created
+ $teamId = $model->getTeamIdForCache();
+ if (filled($teamId)) {
+ GlobalSearch::clearTeamCache($teamId);
+ }
+ });
+
+ static::deleted(function ($model) {
+ // Clear search cache when model is deleted
+ $teamId = $model->getTeamIdForCache();
+ if (filled($teamId)) {
+ GlobalSearch::clearTeamCache($teamId);
+ }
+ });
+ }
+
+ private function getTeamIdForCache()
+ {
+ // For database models, team is accessed through environment.project.team
+ if (method_exists($this, 'team')) {
+ $team = $this->team();
+ if (filled($team)) {
+ return is_object($team) ? $team->id : null;
+ }
+ }
+
+ // For models with direct team_id property
+ if (property_exists($this, 'team_id') || isset($this->team_id)) {
+ return $this->team_id;
+ }
+
+ return null;
+ }
+}
diff --git a/resources/views/components/navbar.blade.php b/resources/views/components/navbar.blade.php
index f61ea681e..1c5987e82 100644
--- a/resources/views/components/navbar.blade.php
+++ b/resources/views/components/navbar.blade.php
@@ -59,20 +59,20 @@
if (this.zoom === '90') {
const style = document.createElement('style');
style.textContent = `
- html {
- font-size: 93.75%;
- }
-
- :root {
- --vh: 1vh;
- }
-
- @media (min-width: 1024px) {
html {
- font-size: 87.5%;
+ font-size: 93.75%;
}
- }
- `;
+
+ :root {
+ --vh: 1vh;
+ }
+
+ @media (min-width: 1024px) {
+ html {
+ font-size: 87.5%;
+ }
+ }
+ `;
document.head.appendChild(style);
}
}
@@ -82,6 +82,9 @@
Coolify
+
+
+
diff --git a/resources/views/livewire/global-search.blade.php b/resources/views/livewire/global-search.blade.php
new file mode 100644
index 000000000..0792dadfb
--- /dev/null
+++ b/resources/views/livewire/global-search.blade.php
@@ -0,0 +1,236 @@
+
+
+
From 575793709bfb256e922a1a58158dbc18fa46b974 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Fri, 19 Sep 2025 10:22:24 +0200
Subject: [PATCH 10/15] feat(search): enable query logging for global search
caching
---
app/Livewire/GlobalSearch.php | 1 +
1 file changed, 1 insertion(+)
diff --git a/app/Livewire/GlobalSearch.php b/app/Livewire/GlobalSearch.php
index 3b3075fc9..dacc0d4db 100644
--- a/app/Livewire/GlobalSearch.php
+++ b/app/Livewire/GlobalSearch.php
@@ -69,6 +69,7 @@ private function loadSearchableItems()
$cacheKey = self::getCacheKey(auth()->user()->currentTeam()->id);
$this->allSearchableItems = Cache::remember($cacheKey, 300, function () {
+ ray()->showQueries();
$items = collect();
$team = auth()->user()->currentTeam();
From f2236236039f966b856d9833f14dbf621d7e7a24 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Fri, 19 Sep 2025 10:22:31 +0200
Subject: [PATCH 11/15] refactor(search): optimize cache clearing logic to only
trigger on searchable field changes
---
app/Traits/ClearsGlobalSearchCache.php | 42 +++++++++++++++++++++-----
1 file changed, 35 insertions(+), 7 deletions(-)
diff --git a/app/Traits/ClearsGlobalSearchCache.php b/app/Traits/ClearsGlobalSearchCache.php
index fe6cbaa38..0bcc5d319 100644
--- a/app/Traits/ClearsGlobalSearchCache.php
+++ b/app/Traits/ClearsGlobalSearchCache.php
@@ -8,16 +8,18 @@ trait ClearsGlobalSearchCache
{
protected static function bootClearsGlobalSearchCache()
{
- static::saved(function ($model) {
- // Clear search cache when model is saved
- $teamId = $model->getTeamIdForCache();
- if (filled($teamId)) {
- GlobalSearch::clearTeamCache($teamId);
+ static::saving(function ($model) {
+ // Only clear cache if searchable fields are being changed
+ if ($model->hasSearchableChanges()) {
+ $teamId = $model->getTeamIdForCache();
+ if (filled($teamId)) {
+ GlobalSearch::clearTeamCache($teamId);
+ }
}
});
static::created(function ($model) {
- // Clear search cache when model is created
+ // Always clear cache when model is created
$teamId = $model->getTeamIdForCache();
if (filled($teamId)) {
GlobalSearch::clearTeamCache($teamId);
@@ -25,7 +27,7 @@ protected static function bootClearsGlobalSearchCache()
});
static::deleted(function ($model) {
- // Clear search cache when model is deleted
+ // Always clear cache when model is deleted
$teamId = $model->getTeamIdForCache();
if (filled($teamId)) {
GlobalSearch::clearTeamCache($teamId);
@@ -33,6 +35,32 @@ protected static function bootClearsGlobalSearchCache()
});
}
+ private function hasSearchableChanges(): bool
+ {
+ // Define searchable fields based on model type
+ $searchableFields = ['name', 'description'];
+
+ // Add model-specific searchable fields
+ if ($this instanceof \App\Models\Application) {
+ $searchableFields[] = 'fqdn';
+ $searchableFields[] = 'docker_compose_domains';
+ } elseif ($this instanceof \App\Models\Server) {
+ $searchableFields[] = 'ip';
+ } elseif ($this instanceof \App\Models\Service) {
+ // Services don't have direct fqdn, but name and description are covered
+ }
+ // Database models only have name and description as searchable
+
+ // Check if any searchable field is dirty
+ foreach ($searchableFields as $field) {
+ if ($this->isDirty($field)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
private function getTeamIdForCache()
{
// For database models, team is accessed through environment.project.team
From bfaefed1aea4864eb30e6c813a919279bae4e785 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Fri, 19 Sep 2025 13:45:37 +0200
Subject: [PATCH 12/15] refactor(environment): streamline rendering of Docker
Build Secrets checkbox and adjust layout for environment variable settings
---
.../shared/environment-variable/all.blade.php | 24 ++++++-------
.../environment-variable/show.blade.php | 34 +++++++++----------
2 files changed, 27 insertions(+), 31 deletions(-)
diff --git a/resources/views/livewire/project/shared/environment-variable/all.blade.php b/resources/views/livewire/project/shared/environment-variable/all.blade.php
index 6854ffaa4..cee6b291d 100644
--- a/resources/views/livewire/project/shared/environment-variable/all.blade.php
+++ b/resources/views/livewire/project/shared/environment-variable/all.blade.php
@@ -28,19 +28,17 @@
@endcan
@endif
- @if (data_get($resource, 'build_pack') !== 'dockercompose')
-
- @can('manageEnvironment', $resource)
-
- @else
-
- @endcan
-
- @endif
+
+ @can('manageEnvironment', $resource)
+
+ @else
+
+ @endcan
+
@endif
@if ($resource->type() === 'service' || $resource?->build_pack === 'dockercompose')
diff --git a/resources/views/livewire/project/shared/environment-variable/show.blade.php b/resources/views/livewire/project/shared/environment-variable/show.blade.php
index 6598b66ff..953bc59fa 100644
--- a/resources/views/livewire/project/shared/environment-variable/show.blade.php
+++ b/resources/views/livewire/project/shared/environment-variable/show.blade.php
@@ -78,22 +78,20 @@
@if ($isSharedVariable)
@else
- @if (!$env->is_coolify)
- @if (!$env->is_nixpacks)
-
- @endif
-
- @if (!$env->is_nixpacks)
-
- @if ($is_multiline === false)
-
- @endif
+ @if (!$env->is_nixpacks)
+
+ @endif
+
+ @if (!$env->is_nixpacks)
+
+ @if ($is_multiline === false)
+
@endif
@endif
@endif
@@ -129,8 +127,8 @@
@if (!$is_redis_credential)
@if ($type === 'service')
+ helper="Make this variable available during Docker build process. Useful for build secrets and dependencies."
+ label="Available at Buildtime" />
From 593c1b476743b0129d7a346c8232d835ecb18600 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Fri, 19 Sep 2025 13:46:00 +0200
Subject: [PATCH 13/15] fix(deployment): enhance Dockerfile modification for
build-time variables and secrets during deployment in case of docker compose
buildpack
---
app/Jobs/ApplicationDeploymentJob.php | 172 +++++++++++++++++++++++++-
1 file changed, 170 insertions(+), 2 deletions(-)
diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php
index ae89649af..c880057e5 100644
--- a/app/Jobs/ApplicationDeploymentJob.php
+++ b/app/Jobs/ApplicationDeploymentJob.php
@@ -606,6 +606,9 @@ private function deploy_docker_compose_buildpack()
executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d | tee {$this->workdir}{$this->docker_compose_location} > /dev/null"),
'hidden' => true,
]);
+
+ // Modify Dockerfiles for ARGs and build secrets
+ $this->modify_dockerfiles_for_compose($composeFile);
// Build new container to limit downtime.
$this->application_deployment_queue->addLogEntry('Pulling & building required images.');
@@ -632,6 +635,13 @@ private function deploy_docker_compose_buildpack()
} else {
$command .= " --project-name {$this->application->uuid} --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} build --pull";
}
+
+ if (! $this->application->settings->use_build_secrets && $this->build_args instanceof \Illuminate\Support\Collection && $this->build_args->isNotEmpty()) {
+ $build_args_string = $this->build_args->implode(' ');
+ $command .= " {$build_args_string}";
+ $this->application_deployment_queue->addLogEntry('Adding build arguments to Docker Compose build command.');
+ }
+
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, $command), 'hidden' => true],
);
@@ -2830,8 +2840,8 @@ private function modify_dockerfile_for_secrets($dockerfile_path)
// Get environment variables for secrets
$variables = $this->pull_request_id === 0
- ? $this->application->environment_variables()->where('key', 'not like', 'NIXPACKS_%')->get()
- : $this->application->environment_variables_preview()->where('key', 'not like', 'NIXPACKS_%')->get();
+ ? $this->application->environment_variables()->where('key', 'not like', 'NIXPACKS_%')->where('is_buildtime', true)->get()
+ : $this->application->environment_variables_preview()->where('key', 'not like', 'NIXPACKS_%')->where('is_buildtime', true)->get();
if ($variables->isEmpty()) {
return;
@@ -2868,6 +2878,164 @@ private function modify_dockerfile_for_secrets($dockerfile_path)
}
}
+ private function modify_dockerfiles_for_compose($composeFile)
+ {
+ if ($this->application->build_pack !== 'dockercompose') {
+ return;
+ }
+
+ $variables = $this->pull_request_id === 0
+ ? $this->application->environment_variables()
+ ->where('key', 'not like', 'NIXPACKS_%')
+ ->where('is_buildtime', true)
+ ->get()
+ : $this->application->environment_variables_preview()
+ ->where('key', 'not like', 'NIXPACKS_%')
+ ->where('is_buildtime', true)
+ ->get();
+
+ if ($variables->isEmpty()) {
+ $this->application_deployment_queue->addLogEntry('No build-time variables to add to Dockerfiles.');
+
+ return;
+ }
+
+ $services = data_get($composeFile, 'services', []);
+
+ foreach ($services as $serviceName => $service) {
+ if (! isset($service['build'])) {
+ continue;
+ }
+
+ $context = '.';
+ $dockerfile = 'Dockerfile';
+
+ if (is_string($service['build'])) {
+ $context = $service['build'];
+ } elseif (is_array($service['build'])) {
+ $context = data_get($service['build'], 'context', '.');
+ $dockerfile = data_get($service['build'], 'dockerfile', 'Dockerfile');
+ }
+
+ $dockerfilePath = rtrim($context, '/').'/'.ltrim($dockerfile, '/');
+ if (str_starts_with($dockerfilePath, './')) {
+ $dockerfilePath = substr($dockerfilePath, 2);
+ }
+ if (str_starts_with($dockerfilePath, '/')) {
+ $dockerfilePath = substr($dockerfilePath, 1);
+ }
+
+ $this->execute_remote_command([
+ executeInDocker($this->deployment_uuid, "test -f {$this->workdir}/{$dockerfilePath} && echo 'exists' || echo 'not found'"),
+ 'hidden' => true,
+ 'save' => 'dockerfile_check_'.$serviceName,
+ ]);
+
+ if (str($this->saved_outputs->get('dockerfile_check_'.$serviceName))->trim()->toString() !== 'exists') {
+ $this->application_deployment_queue->addLogEntry("Dockerfile not found for service {$serviceName} at {$dockerfilePath}, skipping ARG injection.");
+
+ continue;
+ }
+
+ $this->execute_remote_command([
+ executeInDocker($this->deployment_uuid, "cat {$this->workdir}/{$dockerfilePath}"),
+ 'hidden' => true,
+ 'save' => 'dockerfile_content_'.$serviceName,
+ ]);
+
+ $dockerfileContent = $this->saved_outputs->get('dockerfile_content_'.$serviceName);
+ if (! $dockerfileContent) {
+ continue;
+ }
+
+ $dockerfile_lines = collect(str($dockerfileContent)->trim()->explode("\n"));
+
+ $fromIndices = [];
+ $dockerfile_lines->each(function ($line, $index) use (&$fromIndices) {
+ if (str($line)->trim()->startsWith('FROM')) {
+ $fromIndices[] = $index;
+ }
+ });
+
+ if (empty($fromIndices)) {
+ $this->application_deployment_queue->addLogEntry("No FROM instruction found in Dockerfile for service {$serviceName}, skipping.");
+
+ continue;
+ }
+
+ $isMultiStage = count($fromIndices) > 1;
+
+ $argsToAdd = collect([]);
+ foreach ($variables as $env) {
+ $argsToAdd->push("ARG {$env->key}");
+ }
+
+ ray($argsToAdd);
+ if ($argsToAdd->isEmpty()) {
+ $this->application_deployment_queue->addLogEntry("Service {$serviceName}: No build-time variables to add.");
+
+ continue;
+ }
+
+ $totalAdded = 0;
+ $offset = 0;
+
+ foreach ($fromIndices as $stageIndex => $fromIndex) {
+ $adjustedIndex = $fromIndex + $offset;
+
+ $stageStart = $adjustedIndex + 1;
+ $stageEnd = isset($fromIndices[$stageIndex + 1])
+ ? $fromIndices[$stageIndex + 1] + $offset
+ : $dockerfile_lines->count();
+
+ $existingStageArgs = collect([]);
+ for ($i = $stageStart; $i < $stageEnd; $i++) {
+ $line = $dockerfile_lines->get($i);
+ if (! $line || ! str($line)->trim()->startsWith('ARG')) {
+ break;
+ }
+ $parts = explode(' ', trim($line), 2);
+ if (count($parts) >= 2) {
+ $argPart = $parts[1];
+ $keyValue = explode('=', $argPart, 2);
+ $existingStageArgs->push($keyValue[0]);
+ }
+ }
+
+ $stageArgsToAdd = $argsToAdd->filter(function ($arg) use ($existingStageArgs) {
+ $key = str($arg)->after('ARG ')->trim()->toString();
+
+ return ! $existingStageArgs->contains($key);
+ });
+
+ if ($stageArgsToAdd->isNotEmpty()) {
+ $dockerfile_lines->splice($adjustedIndex + 1, 0, $stageArgsToAdd->toArray());
+ $totalAdded += $stageArgsToAdd->count();
+ $offset += $stageArgsToAdd->count();
+ }
+ }
+
+ if ($totalAdded > 0) {
+ $dockerfile_base64 = base64_encode($dockerfile_lines->implode("\n"));
+ $this->execute_remote_command([
+ executeInDocker($this->deployment_uuid, "echo '{$dockerfile_base64}' | base64 -d | tee {$this->workdir}/{$dockerfilePath} > /dev/null"),
+ 'hidden' => true,
+ ]);
+
+ $stageInfo = $isMultiStage ? ' (multi-stage build, added to '.count($fromIndices).' stages)' : '';
+ $this->application_deployment_queue->addLogEntry("Added {$totalAdded} ARG declarations to Dockerfile for service {$serviceName}{$stageInfo}.");
+ } else {
+ $this->application_deployment_queue->addLogEntry("Service {$serviceName}: All required ARG declarations already exist.");
+ }
+
+ if ($this->application->settings->use_build_secrets && $this->dockerBuildkitSupported && ! empty($this->build_secrets)) {
+ $fullDockerfilePath = "{$this->workdir}/{$dockerfilePath}";
+ $this->modify_dockerfile_for_secrets($fullDockerfilePath);
+ $this->application_deployment_queue->addLogEntry("Modified Dockerfile for service {$serviceName} to use build secrets.");
+ }
+ }
+ }
+
private function add_build_secrets_to_compose($composeFile)
{
// Get environment variables for secrets
From 99fd4b424d186c6557c3f48aa43708935c827bef Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Fri, 19 Sep 2025 14:17:10 +0200
Subject: [PATCH 14/15] feat(environment): add dynamic checkbox options for
environment variable settings based on user permissions and variable types
---
.../environment-variable/show.blade.php | 89 +++++++++++++++++++
1 file changed, 89 insertions(+)
diff --git a/resources/views/livewire/project/shared/environment-variable/show.blade.php b/resources/views/livewire/project/shared/environment-variable/show.blade.php
index 953bc59fa..a04b477d5 100644
--- a/resources/views/livewire/project/shared/environment-variable/show.blade.php
+++ b/resources/views/livewire/project/shared/environment-variable/show.blade.php
@@ -21,6 +21,95 @@
step2ButtonText="Permanently Delete" />
@endcan
+ @can('update', $this->env)
+
+
+ @if (!$is_redis_credential)
+ @if ($type === 'service')
+
+
+
+
+ @else
+ @if ($is_shared)
+
+ @else
+ @if ($isSharedVariable)
+
+ @else
+ @if (!$env->is_nixpacks)
+
+ @endif
+
+ @if (!$env->is_nixpacks)
+
+ @if ($is_multiline === false)
+
+ @endif
+ @endif
+ @endif
+ @endif
+ @endif
+ @endif
+
+
+ @else
+
+
+ @if (!$is_redis_credential)
+ @if ($type === 'service')
+
+
+
+
+ @else
+ @if ($is_shared)
+
+ @else
+ @if ($isSharedVariable)
+
+ @else
+
+
+
+ @if ($is_multiline === false)
+
+ @endif
+ @endif
+ @endif
+ @endif
+ @endif
+
+
+ @endcan
@else
@can('update', $this->env)
@if ($isDisabled)
From 3f48dcb5750011c4ab6db724988e170c1b2bb314 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Fri, 19 Sep 2025 15:54:44 +0200
Subject: [PATCH 15/15] feat(redaction): implement sensitive information
redaction in logs and commands
---
app/Models/ApplicationDeploymentQueue.php | 43 +++++++++++++++++++-
app/Traits/ExecuteRemoteCommand.php | 48 +++++++++++++++++++++--
2 files changed, 86 insertions(+), 5 deletions(-)
diff --git a/app/Models/ApplicationDeploymentQueue.php b/app/Models/ApplicationDeploymentQueue.php
index 2a9bea67a..8df6877ab 100644
--- a/app/Models/ApplicationDeploymentQueue.php
+++ b/app/Models/ApplicationDeploymentQueue.php
@@ -85,6 +85,47 @@ public function commitMessage()
return str($this->commit_message)->value();
}
+ private function redactSensitiveInfo($text)
+ {
+ $text = remove_iip($text);
+
+ $app = $this->application;
+ if (! $app) {
+ return $text;
+ }
+
+ $lockedVars = collect([]);
+
+ if ($app->environment_variables) {
+ $lockedVars = $lockedVars->merge(
+ $app->environment_variables
+ ->where('is_shown_once', true)
+ ->pluck('real_value', 'key')
+ ->filter()
+ );
+ }
+
+ if ($this->pull_request_id !== 0 && $app->environment_variables_preview) {
+ $lockedVars = $lockedVars->merge(
+ $app->environment_variables_preview
+ ->where('is_shown_once', true)
+ ->pluck('real_value', 'key')
+ ->filter()
+ );
+ }
+
+ foreach ($lockedVars as $key => $value) {
+ $escapedValue = preg_quote($value, '/');
+ $text = preg_replace(
+ '/'.$escapedValue.'/',
+ REDACTED,
+ $text
+ );
+ }
+
+ return $text;
+ }
+
public function addLogEntry(string $message, string $type = 'stdout', bool $hidden = false)
{
if ($type === 'error') {
@@ -96,7 +137,7 @@ public function addLogEntry(string $message, string $type = 'stdout', bool $hidd
}
$newLogEntry = [
'command' => null,
- 'output' => remove_iip($message),
+ 'output' => $this->redactSensitiveInfo($message),
'type' => $type,
'timestamp' => Carbon::now('UTC'),
'hidden' => $hidden,
diff --git a/app/Traits/ExecuteRemoteCommand.php b/app/Traits/ExecuteRemoteCommand.php
index 0c3414efe..f9df19c16 100644
--- a/app/Traits/ExecuteRemoteCommand.php
+++ b/app/Traits/ExecuteRemoteCommand.php
@@ -17,6 +17,46 @@ trait ExecuteRemoteCommand
public static int $batch_counter = 0;
+ private function redact_sensitive_info($text)
+ {
+ $text = remove_iip($text);
+
+ if (! isset($this->application)) {
+ return $text;
+ }
+
+ $lockedVars = collect([]);
+
+ if (isset($this->application->environment_variables)) {
+ $lockedVars = $lockedVars->merge(
+ $this->application->environment_variables
+ ->where('is_shown_once', true)
+ ->pluck('real_value', 'key')
+ ->filter()
+ );
+ }
+
+ if (isset($this->pull_request_id) && $this->pull_request_id !== 0 && isset($this->application->environment_variables_preview)) {
+ $lockedVars = $lockedVars->merge(
+ $this->application->environment_variables_preview
+ ->where('is_shown_once', true)
+ ->pluck('real_value', 'key')
+ ->filter()
+ );
+ }
+
+ foreach ($lockedVars as $key => $value) {
+ $escapedValue = preg_quote($value, '/');
+ $text = preg_replace(
+ '/'.$escapedValue.'/',
+ REDACTED,
+ $text
+ );
+ }
+
+ return $text;
+ }
+
public function execute_remote_command(...$commands)
{
static::$batch_counter++;
@@ -74,7 +114,7 @@ public function execute_remote_command(...$commands)
// Track SSH retry event in Sentry
$this->trackSshRetryEvent($attempt, $maxRetries, $delay, $errorMessage, [
'server' => $this->server->name ?? $this->server->ip ?? 'unknown',
- 'command' => remove_iip($command),
+ 'command' => $this->redact_sensitive_info($command),
'trait' => 'ExecuteRemoteCommand',
]);
@@ -125,8 +165,8 @@ private function executeCommandWithProcess($command, $hidden, $customType, $appe
$sanitized_output = sanitize_utf8_text($output);
$new_log_entry = [
- 'command' => remove_iip($command),
- 'output' => remove_iip($sanitized_output),
+ 'command' => $this->redact_sensitive_info($command),
+ 'output' => $this->redact_sensitive_info($sanitized_output),
'type' => $customType ?? $type === 'err' ? 'stderr' : 'stdout',
'timestamp' => Carbon::now('UTC'),
'hidden' => $hidden,
@@ -194,7 +234,7 @@ private function addRetryLogEntry(int $attempt, int $maxRetries, int $delay, str
$retryMessage = "SSH connection failed. Retrying... (Attempt {$attempt}/{$maxRetries}, waiting {$delay}s)\nError: {$errorMessage}";
$new_log_entry = [
- 'output' => remove_iip($retryMessage),
+ 'output' => $this->redact_sensitive_info($retryMessage),
'type' => 'stdout',
'timestamp' => Carbon::now('UTC'),
'hidden' => false,