From 8cc10ab10a9536351c51e3dd8e699a13303e8d6a Mon Sep 17 00:00:00 2001 From: Maurits de Ruiter Date: Mon, 23 Feb 2026 21:06:06 +0100 Subject: [PATCH 01/17] fix: enable preview deployment page for deploy key applications --- app/Livewire/Project/Application/Configuration.php | 4 +--- .../livewire/project/application/configuration.blade.php | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/app/Livewire/Project/Application/Configuration.php b/app/Livewire/Project/Application/Configuration.php index 5d7f3fd31..cc1bf15b9 100644 --- a/app/Livewire/Project/Application/Configuration.php +++ b/app/Livewire/Project/Application/Configuration.php @@ -51,9 +51,7 @@ public function mount() $this->environment = $environment; $this->application = $application; - if ($this->application->deploymentType() === 'deploy_key' && $this->currentRoute === 'project.application.preview-deployments') { - return redirect()->route('project.application.configuration', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]); - } + if ($this->application->build_pack === 'dockercompose' && $this->currentRoute === 'project.application.healthcheck') { return redirect()->route('project.application.configuration', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]); diff --git a/resources/views/livewire/project/application/configuration.blade.php b/resources/views/livewire/project/application/configuration.blade.php index 34c859a18..597bfa0a4 100644 --- a/resources/views/livewire/project/application/configuration.blade.php +++ b/resources/views/livewire/project/application/configuration.blade.php @@ -46,7 +46,7 @@ href="{{ route('project.application.scheduled-tasks.show', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'application_uuid' => $application->uuid]) }}">Scheduled Tasks Webhooks - @if ($application->deploymentType() !== 'deploy_key') + @if ($application->git_based()) Preview Deployments @endif From 30c1d9bbd04e6af82421c91cae0f8947163a135a Mon Sep 17 00:00:00 2001 From: "Brendan G. Lim" <1788+brendanlim@users.noreply.github.com> Date: Thu, 26 Feb 2026 21:07:09 -0800 Subject: [PATCH 02/17] feat: add configurable timeout for public database TCP proxy Adds a per-database 'Proxy Timeout' setting for publicly exposed databases. The nginx stream proxy_timeout can now be configured in the UI, defaulting to 3600s (1 hour) instead of nginx's 10min default. Set to 0 for no timeout. Fixes #7743 --- app/Actions/Database/StartDatabaseProxy.php | 4 ++ .../Project/Database/Clickhouse/General.php | 6 ++ .../Project/Database/Dragonfly/General.php | 6 ++ .../Project/Database/Keydb/General.php | 6 ++ .../Project/Database/Mariadb/General.php | 7 +++ .../Project/Database/Mongodb/General.php | 7 +++ .../Project/Database/Mysql/General.php | 7 +++ .../Project/Database/Postgresql/General.php | 7 +++ .../Project/Database/Redis/General.php | 7 +++ app/Livewire/Project/Service/Index.php | 5 ++ app/Models/ServiceDatabase.php | 4 ++ app/Models/StandaloneClickhouse.php | 1 + app/Models/StandaloneDragonfly.php | 1 + app/Models/StandaloneKeydb.php | 1 + app/Models/StandaloneMariadb.php | 1 + app/Models/StandaloneMongodb.php | 1 + app/Models/StandaloneMysql.php | 1 + app/Models/StandalonePostgresql.php | 1 + app/Models/StandaloneRedis.php | 1 + ...0_add_public_port_timeout_to_databases.php | 60 +++++++++++++++++++ .../database/clickhouse/general.blade.php | 2 + .../database/dragonfly/general.blade.php | 2 + .../project/database/keydb/general.blade.php | 2 + .../database/mariadb/general.blade.php | 2 + .../database/mongodb/general.blade.php | 2 + .../project/database/mysql/general.blade.php | 2 + .../database/postgresql/general.blade.php | 2 + .../project/database/redis/general.blade.php | 2 + 28 files changed, 150 insertions(+) create mode 100644 database/migrations/2026_02_27_000000_add_public_port_timeout_to_databases.php diff --git a/app/Actions/Database/StartDatabaseProxy.php b/app/Actions/Database/StartDatabaseProxy.php index 4331c6ae7..c7713a965 100644 --- a/app/Actions/Database/StartDatabaseProxy.php +++ b/app/Actions/Database/StartDatabaseProxy.php @@ -54,6 +54,8 @@ public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|St if (isDev()) { $configuration_dir = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/databases/'.$database->uuid.'/proxy'; } + $timeout = $database->public_port_timeout ?? 3600; + $timeoutConfig = $timeout === 0 ? 'proxy_timeout 0;' : "proxy_timeout {$timeout}s;"; $nginxconf = <<public_port; proxy_pass $containerName:$internalPort; + $timeoutConfig + proxy_connect_timeout 60s; } } EOF; diff --git a/app/Livewire/Project/Database/Clickhouse/General.php b/app/Livewire/Project/Database/Clickhouse/General.php index 7ad453fd5..ee2ae7bd4 100644 --- a/app/Livewire/Project/Database/Clickhouse/General.php +++ b/app/Livewire/Project/Database/Clickhouse/General.php @@ -36,6 +36,8 @@ class General extends Component public ?int $publicPort = null; + public ?int $publicPortTimeout = 3600; + public ?string $customDockerRunOptions = null; public ?string $dbUrl = null; @@ -80,6 +82,7 @@ protected function rules(): array 'portsMappings' => 'nullable|string', 'isPublic' => 'nullable|boolean', 'publicPort' => 'nullable|integer', + 'publicPortTimeout' => 'nullable|integer', 'customDockerRunOptions' => 'nullable|string', 'dbUrl' => 'nullable|string', 'dbUrlPublic' => 'nullable|string', @@ -99,6 +102,7 @@ protected function messages(): array 'image.required' => 'The Docker Image field is required.', 'image.string' => 'The Docker Image must be a string.', 'publicPort.integer' => 'The Public Port must be an integer.', + 'publicPortTimeout.integer' => 'The Public Port Timeout must be an integer.', ] ); } @@ -115,6 +119,7 @@ public function syncData(bool $toModel = false) $this->database->ports_mappings = $this->portsMappings; $this->database->is_public = $this->isPublic; $this->database->public_port = $this->publicPort; + $this->database->public_port_timeout = $this->publicPortTimeout; $this->database->custom_docker_run_options = $this->customDockerRunOptions; $this->database->is_log_drain_enabled = $this->isLogDrainEnabled; $this->database->save(); @@ -130,6 +135,7 @@ public function syncData(bool $toModel = false) $this->portsMappings = $this->database->ports_mappings; $this->isPublic = $this->database->is_public; $this->publicPort = $this->database->public_port; + $this->publicPortTimeout = $this->database->public_port_timeout; $this->customDockerRunOptions = $this->database->custom_docker_run_options; $this->isLogDrainEnabled = $this->database->is_log_drain_enabled; $this->dbUrl = $this->database->internal_db_url; diff --git a/app/Livewire/Project/Database/Dragonfly/General.php b/app/Livewire/Project/Database/Dragonfly/General.php index 4e325b9ee..6d1b5f74f 100644 --- a/app/Livewire/Project/Database/Dragonfly/General.php +++ b/app/Livewire/Project/Database/Dragonfly/General.php @@ -36,6 +36,8 @@ class General extends Component public ?int $publicPort = null; + public ?int $publicPortTimeout = 3600; + public ?string $customDockerRunOptions = null; public ?string $dbUrl = null; @@ -91,6 +93,7 @@ protected function rules(): array 'portsMappings' => 'nullable|string', 'isPublic' => 'nullable|boolean', 'publicPort' => 'nullable|integer', + 'publicPortTimeout' => 'nullable|integer', 'customDockerRunOptions' => 'nullable|string', 'dbUrl' => 'nullable|string', 'dbUrlPublic' => 'nullable|string', @@ -109,6 +112,7 @@ protected function messages(): array 'image.required' => 'The Docker Image field is required.', 'image.string' => 'The Docker Image must be a string.', 'publicPort.integer' => 'The Public Port must be an integer.', + 'publicPortTimeout.integer' => 'The Public Port Timeout must be an integer.', ] ); } @@ -124,6 +128,7 @@ public function syncData(bool $toModel = false) $this->database->ports_mappings = $this->portsMappings; $this->database->is_public = $this->isPublic; $this->database->public_port = $this->publicPort; + $this->database->public_port_timeout = $this->publicPortTimeout; $this->database->custom_docker_run_options = $this->customDockerRunOptions; $this->database->is_log_drain_enabled = $this->isLogDrainEnabled; $this->database->enable_ssl = $this->enable_ssl; @@ -139,6 +144,7 @@ public function syncData(bool $toModel = false) $this->portsMappings = $this->database->ports_mappings; $this->isPublic = $this->database->is_public; $this->publicPort = $this->database->public_port; + $this->publicPortTimeout = $this->database->public_port_timeout; $this->customDockerRunOptions = $this->database->custom_docker_run_options; $this->isLogDrainEnabled = $this->database->is_log_drain_enabled; $this->enable_ssl = $this->database->enable_ssl; diff --git a/app/Livewire/Project/Database/Keydb/General.php b/app/Livewire/Project/Database/Keydb/General.php index f02aa6674..19726e413 100644 --- a/app/Livewire/Project/Database/Keydb/General.php +++ b/app/Livewire/Project/Database/Keydb/General.php @@ -38,6 +38,8 @@ class General extends Component public ?int $publicPort = null; + public ?int $publicPortTimeout = 3600; + public ?string $customDockerRunOptions = null; public ?string $dbUrl = null; @@ -94,6 +96,7 @@ protected function rules(): array 'portsMappings' => 'nullable|string', 'isPublic' => 'nullable|boolean', 'publicPort' => 'nullable|integer', + 'publicPortTimeout' => 'nullable|integer', 'customDockerRunOptions' => 'nullable|string', 'dbUrl' => 'nullable|string', 'dbUrlPublic' => 'nullable|string', @@ -114,6 +117,7 @@ protected function messages(): array 'image.required' => 'The Docker Image field is required.', 'image.string' => 'The Docker Image must be a string.', 'publicPort.integer' => 'The Public Port must be an integer.', + 'publicPortTimeout.integer' => 'The Public Port Timeout must be an integer.', ] ); } @@ -130,6 +134,7 @@ public function syncData(bool $toModel = false) $this->database->ports_mappings = $this->portsMappings; $this->database->is_public = $this->isPublic; $this->database->public_port = $this->publicPort; + $this->database->public_port_timeout = $this->publicPortTimeout; $this->database->custom_docker_run_options = $this->customDockerRunOptions; $this->database->is_log_drain_enabled = $this->isLogDrainEnabled; $this->database->enable_ssl = $this->enable_ssl; @@ -146,6 +151,7 @@ public function syncData(bool $toModel = false) $this->portsMappings = $this->database->ports_mappings; $this->isPublic = $this->database->is_public; $this->publicPort = $this->database->public_port; + $this->publicPortTimeout = $this->database->public_port_timeout; $this->customDockerRunOptions = $this->database->custom_docker_run_options; $this->isLogDrainEnabled = $this->database->is_log_drain_enabled; $this->enable_ssl = $this->database->enable_ssl; diff --git a/app/Livewire/Project/Database/Mariadb/General.php b/app/Livewire/Project/Database/Mariadb/General.php index 74658e2a4..cb7b99a83 100644 --- a/app/Livewire/Project/Database/Mariadb/General.php +++ b/app/Livewire/Project/Database/Mariadb/General.php @@ -44,6 +44,8 @@ class General extends Component public ?int $publicPort = null; + public ?int $publicPortTimeout = 3600; + public bool $isLogDrainEnabled = false; public ?string $customDockerRunOptions = null; @@ -79,6 +81,7 @@ protected function rules(): array 'portsMappings' => 'nullable', 'isPublic' => 'nullable|boolean', 'publicPort' => 'nullable|integer', + 'publicPortTimeout' => 'nullable|integer', 'isLogDrainEnabled' => 'nullable|boolean', 'customDockerRunOptions' => 'nullable', 'enableSsl' => 'boolean', @@ -97,6 +100,7 @@ protected function messages(): array 'mariadbDatabase.required' => 'The MariaDB Database field is required.', 'image.required' => 'The Docker Image field is required.', 'publicPort.integer' => 'The Public Port must be an integer.', + 'publicPortTimeout.integer' => 'The Public Port Timeout must be an integer.', ] ); } @@ -113,6 +117,7 @@ protected function messages(): array 'portsMappings' => 'Port Mapping', 'isPublic' => 'Is Public', 'publicPort' => 'Public Port', + 'publicPortTimeout' => 'Public Port Timeout', 'customDockerRunOptions' => 'Custom Docker Options', 'enableSsl' => 'Enable SSL', ]; @@ -154,6 +159,7 @@ public function syncData(bool $toModel = false) $this->database->ports_mappings = $this->portsMappings; $this->database->is_public = $this->isPublic; $this->database->public_port = $this->publicPort; + $this->database->public_port_timeout = $this->publicPortTimeout; $this->database->is_log_drain_enabled = $this->isLogDrainEnabled; $this->database->custom_docker_run_options = $this->customDockerRunOptions; $this->database->enable_ssl = $this->enableSsl; @@ -173,6 +179,7 @@ public function syncData(bool $toModel = false) $this->portsMappings = $this->database->ports_mappings; $this->isPublic = $this->database->is_public; $this->publicPort = $this->database->public_port; + $this->publicPortTimeout = $this->database->public_port_timeout; $this->isLogDrainEnabled = $this->database->is_log_drain_enabled; $this->customDockerRunOptions = $this->database->custom_docker_run_options; $this->enableSsl = $this->database->enable_ssl; diff --git a/app/Livewire/Project/Database/Mongodb/General.php b/app/Livewire/Project/Database/Mongodb/General.php index 9f34b73d5..8c7eea1b3 100644 --- a/app/Livewire/Project/Database/Mongodb/General.php +++ b/app/Livewire/Project/Database/Mongodb/General.php @@ -42,6 +42,8 @@ class General extends Component public ?int $publicPort = null; + public ?int $publicPortTimeout = 3600; + public bool $isLogDrainEnabled = false; public ?string $customDockerRunOptions = null; @@ -78,6 +80,7 @@ protected function rules(): array 'portsMappings' => 'nullable', 'isPublic' => 'nullable|boolean', 'publicPort' => 'nullable|integer', + 'publicPortTimeout' => 'nullable|integer', 'isLogDrainEnabled' => 'nullable|boolean', 'customDockerRunOptions' => 'nullable', 'enableSsl' => 'boolean', @@ -96,6 +99,7 @@ protected function messages(): array 'mongoInitdbDatabase.required' => 'The MongoDB Database field is required.', 'image.required' => 'The Docker Image field is required.', 'publicPort.integer' => 'The Public Port must be an integer.', + 'publicPortTimeout.integer' => 'The Public Port Timeout must be an integer.', 'sslMode.in' => 'The SSL Mode must be one of: allow, prefer, require, verify-full.', ] ); @@ -112,6 +116,7 @@ protected function messages(): array 'portsMappings' => 'Port Mapping', 'isPublic' => 'Is Public', 'publicPort' => 'Public Port', + 'publicPortTimeout' => 'Public Port Timeout', 'customDockerRunOptions' => 'Custom Docker Run Options', 'enableSsl' => 'Enable SSL', 'sslMode' => 'SSL Mode', @@ -153,6 +158,7 @@ public function syncData(bool $toModel = false) $this->database->ports_mappings = $this->portsMappings; $this->database->is_public = $this->isPublic; $this->database->public_port = $this->publicPort; + $this->database->public_port_timeout = $this->publicPortTimeout; $this->database->is_log_drain_enabled = $this->isLogDrainEnabled; $this->database->custom_docker_run_options = $this->customDockerRunOptions; $this->database->enable_ssl = $this->enableSsl; @@ -172,6 +178,7 @@ public function syncData(bool $toModel = false) $this->portsMappings = $this->database->ports_mappings; $this->isPublic = $this->database->is_public; $this->publicPort = $this->database->public_port; + $this->publicPortTimeout = $this->database->public_port_timeout; $this->isLogDrainEnabled = $this->database->is_log_drain_enabled; $this->customDockerRunOptions = $this->database->custom_docker_run_options; $this->enableSsl = $this->database->enable_ssl; diff --git a/app/Livewire/Project/Database/Mysql/General.php b/app/Livewire/Project/Database/Mysql/General.php index 86b109251..371ab7f68 100644 --- a/app/Livewire/Project/Database/Mysql/General.php +++ b/app/Livewire/Project/Database/Mysql/General.php @@ -44,6 +44,8 @@ class General extends Component public ?int $publicPort = null; + public ?int $publicPortTimeout = 3600; + public bool $isLogDrainEnabled = false; public ?string $customDockerRunOptions = null; @@ -81,6 +83,7 @@ protected function rules(): array 'portsMappings' => 'nullable', 'isPublic' => 'nullable|boolean', 'publicPort' => 'nullable|integer', + 'publicPortTimeout' => 'nullable|integer', 'isLogDrainEnabled' => 'nullable|boolean', 'customDockerRunOptions' => 'nullable', 'enableSsl' => 'boolean', @@ -100,6 +103,7 @@ protected function messages(): array 'mysqlDatabase.required' => 'The MySQL Database field is required.', 'image.required' => 'The Docker Image field is required.', 'publicPort.integer' => 'The Public Port must be an integer.', + 'publicPortTimeout.integer' => 'The Public Port Timeout must be an integer.', 'sslMode.in' => 'The SSL Mode must be one of: PREFERRED, REQUIRED, VERIFY_CA, VERIFY_IDENTITY.', ] ); @@ -117,6 +121,7 @@ protected function messages(): array 'portsMappings' => 'Port Mapping', 'isPublic' => 'Is Public', 'publicPort' => 'Public Port', + 'publicPortTimeout' => 'Public Port Timeout', 'customDockerRunOptions' => 'Custom Docker Run Options', 'enableSsl' => 'Enable SSL', 'sslMode' => 'SSL Mode', @@ -159,6 +164,7 @@ public function syncData(bool $toModel = false) $this->database->ports_mappings = $this->portsMappings; $this->database->is_public = $this->isPublic; $this->database->public_port = $this->publicPort; + $this->database->public_port_timeout = $this->publicPortTimeout; $this->database->is_log_drain_enabled = $this->isLogDrainEnabled; $this->database->custom_docker_run_options = $this->customDockerRunOptions; $this->database->enable_ssl = $this->enableSsl; @@ -179,6 +185,7 @@ public function syncData(bool $toModel = false) $this->portsMappings = $this->database->ports_mappings; $this->isPublic = $this->database->is_public; $this->publicPort = $this->database->public_port; + $this->publicPortTimeout = $this->database->public_port_timeout; $this->isLogDrainEnabled = $this->database->is_log_drain_enabled; $this->customDockerRunOptions = $this->database->custom_docker_run_options; $this->enableSsl = $this->database->enable_ssl; diff --git a/app/Livewire/Project/Database/Postgresql/General.php b/app/Livewire/Project/Database/Postgresql/General.php index e24674315..fa8c69789 100644 --- a/app/Livewire/Project/Database/Postgresql/General.php +++ b/app/Livewire/Project/Database/Postgresql/General.php @@ -48,6 +48,8 @@ class General extends Component public ?int $publicPort = null; + public ?int $publicPortTimeout = 3600; + public bool $isLogDrainEnabled = false; public ?string $customDockerRunOptions = null; @@ -93,6 +95,7 @@ protected function rules(): array 'portsMappings' => 'nullable', 'isPublic' => 'nullable|boolean', 'publicPort' => 'nullable|integer', + 'publicPortTimeout' => 'nullable|integer', 'isLogDrainEnabled' => 'nullable|boolean', 'customDockerRunOptions' => 'nullable', 'enableSsl' => 'boolean', @@ -111,6 +114,7 @@ protected function messages(): array 'postgresDb.required' => 'The Postgres Database field is required.', 'image.required' => 'The Docker Image field is required.', 'publicPort.integer' => 'The Public Port must be an integer.', + 'publicPortTimeout.integer' => 'The Public Port Timeout must be an integer.', 'sslMode.in' => 'The SSL Mode must be one of: allow, prefer, require, verify-ca, verify-full.', ] ); @@ -130,6 +134,7 @@ protected function messages(): array 'portsMappings' => 'Port Mapping', 'isPublic' => 'Is Public', 'publicPort' => 'Public Port', + 'publicPortTimeout' => 'Public Port Timeout', 'customDockerRunOptions' => 'Custom Docker Run Options', 'enableSsl' => 'Enable SSL', 'sslMode' => 'SSL Mode', @@ -174,6 +179,7 @@ public function syncData(bool $toModel = false) $this->database->ports_mappings = $this->portsMappings; $this->database->is_public = $this->isPublic; $this->database->public_port = $this->publicPort; + $this->database->public_port_timeout = $this->publicPortTimeout; $this->database->is_log_drain_enabled = $this->isLogDrainEnabled; $this->database->custom_docker_run_options = $this->customDockerRunOptions; $this->database->enable_ssl = $this->enableSsl; @@ -196,6 +202,7 @@ public function syncData(bool $toModel = false) $this->portsMappings = $this->database->ports_mappings; $this->isPublic = $this->database->is_public; $this->publicPort = $this->database->public_port; + $this->publicPortTimeout = $this->database->public_port_timeout; $this->isLogDrainEnabled = $this->database->is_log_drain_enabled; $this->customDockerRunOptions = $this->database->custom_docker_run_options; $this->enableSsl = $this->database->enable_ssl; diff --git a/app/Livewire/Project/Database/Redis/General.php b/app/Livewire/Project/Database/Redis/General.php index 08bcdc343..be2242024 100644 --- a/app/Livewire/Project/Database/Redis/General.php +++ b/app/Livewire/Project/Database/Redis/General.php @@ -36,6 +36,8 @@ class General extends Component public ?int $publicPort = null; + public ?int $publicPortTimeout = 3600; + public bool $isLogDrainEnabled = false; public ?string $customDockerRunOptions = null; @@ -74,6 +76,7 @@ protected function rules(): array 'portsMappings' => 'nullable', 'isPublic' => 'nullable|boolean', 'publicPort' => 'nullable|integer', + 'publicPortTimeout' => 'nullable|integer', 'isLogDrainEnabled' => 'nullable|boolean', 'customDockerRunOptions' => 'nullable', 'redisUsername' => 'required', @@ -90,6 +93,7 @@ protected function messages(): array 'name.required' => 'The Name field is required.', 'image.required' => 'The Docker Image field is required.', 'publicPort.integer' => 'The Public Port must be an integer.', + 'publicPortTimeout.integer' => 'The Public Port Timeout must be an integer.', 'redisUsername.required' => 'The Redis Username field is required.', 'redisPassword.required' => 'The Redis Password field is required.', ] @@ -104,6 +108,7 @@ protected function messages(): array 'portsMappings' => 'Port Mapping', 'isPublic' => 'Is Public', 'publicPort' => 'Public Port', + 'publicPortTimeout' => 'Public Port Timeout', 'customDockerRunOptions' => 'Custom Docker Options', 'redisUsername' => 'Redis Username', 'redisPassword' => 'Redis Password', @@ -143,6 +148,7 @@ public function syncData(bool $toModel = false) $this->database->ports_mappings = $this->portsMappings; $this->database->is_public = $this->isPublic; $this->database->public_port = $this->publicPort; + $this->database->public_port_timeout = $this->publicPortTimeout; $this->database->is_log_drain_enabled = $this->isLogDrainEnabled; $this->database->custom_docker_run_options = $this->customDockerRunOptions; $this->database->enable_ssl = $this->enableSsl; @@ -158,6 +164,7 @@ public function syncData(bool $toModel = false) $this->portsMappings = $this->database->ports_mappings; $this->isPublic = $this->database->is_public; $this->publicPort = $this->database->public_port; + $this->publicPortTimeout = $this->database->public_port_timeout; $this->isLogDrainEnabled = $this->database->is_log_drain_enabled; $this->customDockerRunOptions = $this->database->custom_docker_run_options; $this->enableSsl = $this->database->enable_ssl; diff --git a/app/Livewire/Project/Service/Index.php b/app/Livewire/Project/Service/Index.php index 360282911..f12a11215 100644 --- a/app/Livewire/Project/Service/Index.php +++ b/app/Livewire/Project/Service/Index.php @@ -53,6 +53,8 @@ class Index extends Component public ?int $publicPort = null; + public ?int $publicPortTimeout = 3600; + public bool $isPublic = false; public bool $isLogDrainEnabled = false; @@ -90,6 +92,7 @@ class Index extends Component 'image' => 'required', 'excludeFromStatus' => 'required|boolean', 'publicPort' => 'nullable|integer', + 'publicPortTimeout' => 'nullable|integer', 'isPublic' => 'required|boolean', 'isLogDrainEnabled' => 'required|boolean', // Application-specific rules @@ -158,6 +161,7 @@ private function syncDatabaseData(bool $toModel = false): void $this->serviceDatabase->image = $this->image; $this->serviceDatabase->exclude_from_status = $this->excludeFromStatus; $this->serviceDatabase->public_port = $this->publicPort; + $this->serviceDatabase->public_port_timeout = $this->publicPortTimeout; $this->serviceDatabase->is_public = $this->isPublic; $this->serviceDatabase->is_log_drain_enabled = $this->isLogDrainEnabled; } else { @@ -166,6 +170,7 @@ private function syncDatabaseData(bool $toModel = false): void $this->image = $this->serviceDatabase->image; $this->excludeFromStatus = $this->serviceDatabase->exclude_from_status ?? false; $this->publicPort = $this->serviceDatabase->public_port; + $this->publicPortTimeout = $this->serviceDatabase->public_port_timeout; $this->isPublic = $this->serviceDatabase->is_public ?? false; $this->isLogDrainEnabled = $this->serviceDatabase->is_log_drain_enabled ?? false; } diff --git a/app/Models/ServiceDatabase.php b/app/Models/ServiceDatabase.php index 7b0abe59e..c6a0143a8 100644 --- a/app/Models/ServiceDatabase.php +++ b/app/Models/ServiceDatabase.php @@ -11,6 +11,10 @@ class ServiceDatabase extends BaseModel protected $guarded = []; + protected $casts = [ + 'public_port_timeout' => 'integer', + ]; + protected static function booted() { static::deleting(function ($service) { diff --git a/app/Models/StandaloneClickhouse.php b/app/Models/StandaloneClickhouse.php index 86323db8c..33f32dd59 100644 --- a/app/Models/StandaloneClickhouse.php +++ b/app/Models/StandaloneClickhouse.php @@ -19,6 +19,7 @@ class StandaloneClickhouse extends BaseModel protected $casts = [ 'clickhouse_password' => 'encrypted', + 'public_port_timeout' => 'integer', 'restart_count' => 'integer', 'last_restart_at' => 'datetime', 'last_restart_type' => 'string', diff --git a/app/Models/StandaloneDragonfly.php b/app/Models/StandaloneDragonfly.php index 4db7866b7..074c5b509 100644 --- a/app/Models/StandaloneDragonfly.php +++ b/app/Models/StandaloneDragonfly.php @@ -19,6 +19,7 @@ class StandaloneDragonfly extends BaseModel protected $casts = [ 'dragonfly_password' => 'encrypted', + 'public_port_timeout' => 'integer', 'restart_count' => 'integer', 'last_restart_at' => 'datetime', 'last_restart_type' => 'string', diff --git a/app/Models/StandaloneKeydb.php b/app/Models/StandaloneKeydb.php index f23499608..23b4c65e6 100644 --- a/app/Models/StandaloneKeydb.php +++ b/app/Models/StandaloneKeydb.php @@ -19,6 +19,7 @@ class StandaloneKeydb extends BaseModel protected $casts = [ 'keydb_password' => 'encrypted', + 'public_port_timeout' => 'integer', 'restart_count' => 'integer', 'last_restart_at' => 'datetime', 'last_restart_type' => 'string', diff --git a/app/Models/StandaloneMariadb.php b/app/Models/StandaloneMariadb.php index e7ba75b67..4d4b84776 100644 --- a/app/Models/StandaloneMariadb.php +++ b/app/Models/StandaloneMariadb.php @@ -20,6 +20,7 @@ class StandaloneMariadb extends BaseModel protected $casts = [ 'mariadb_password' => 'encrypted', + 'public_port_timeout' => 'integer', 'restart_count' => 'integer', 'last_restart_at' => 'datetime', 'last_restart_type' => 'string', diff --git a/app/Models/StandaloneMongodb.php b/app/Models/StandaloneMongodb.php index d6de5874c..b5401dd2c 100644 --- a/app/Models/StandaloneMongodb.php +++ b/app/Models/StandaloneMongodb.php @@ -18,6 +18,7 @@ class StandaloneMongodb extends BaseModel protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; protected $casts = [ + 'public_port_timeout' => 'integer', 'restart_count' => 'integer', 'last_restart_at' => 'datetime', 'last_restart_type' => 'string', diff --git a/app/Models/StandaloneMysql.php b/app/Models/StandaloneMysql.php index 98a5cab77..0b144575c 100644 --- a/app/Models/StandaloneMysql.php +++ b/app/Models/StandaloneMysql.php @@ -20,6 +20,7 @@ class StandaloneMysql extends BaseModel protected $casts = [ 'mysql_password' => 'encrypted', 'mysql_root_password' => 'encrypted', + 'public_port_timeout' => 'integer', 'restart_count' => 'integer', 'last_restart_at' => 'datetime', 'last_restart_type' => 'string', diff --git a/app/Models/StandalonePostgresql.php b/app/Models/StandalonePostgresql.php index 5d35f335b..92b2efd31 100644 --- a/app/Models/StandalonePostgresql.php +++ b/app/Models/StandalonePostgresql.php @@ -20,6 +20,7 @@ class StandalonePostgresql extends BaseModel protected $casts = [ 'init_scripts' => 'array', 'postgres_password' => 'encrypted', + 'public_port_timeout' => 'integer', 'restart_count' => 'integer', 'last_restart_at' => 'datetime', 'last_restart_type' => 'string', diff --git a/app/Models/StandaloneRedis.php b/app/Models/StandaloneRedis.php index e906bbb81..352d27cfd 100644 --- a/app/Models/StandaloneRedis.php +++ b/app/Models/StandaloneRedis.php @@ -18,6 +18,7 @@ class StandaloneRedis extends BaseModel protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status']; protected $casts = [ + 'public_port_timeout' => 'integer', 'restart_count' => 'integer', 'last_restart_at' => 'datetime', 'last_restart_type' => 'string', diff --git a/database/migrations/2026_02_27_000000_add_public_port_timeout_to_databases.php b/database/migrations/2026_02_27_000000_add_public_port_timeout_to_databases.php new file mode 100644 index 000000000..defebcce4 --- /dev/null +++ b/database/migrations/2026_02_27_000000_add_public_port_timeout_to_databases.php @@ -0,0 +1,60 @@ +integer('public_port_timeout')->nullable()->default(3600)->after('public_port'); + }); + } + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + $tables = [ + 'standalone_postgresqls', + 'standalone_mysqls', + 'standalone_mariadbs', + 'standalone_redis', + 'standalone_mongodbs', + 'standalone_clickhouses', + 'standalone_keydbs', + 'standalone_dragonflies', + 'service_databases', + ]; + + foreach ($tables as $table) { + if (Schema::hasTable($table) && Schema::hasColumn($table, 'public_port_timeout')) { + Schema::table($table, function (Blueprint $table) { + $table->dropColumn('public_port_timeout'); + }); + } + } + } +}; diff --git a/resources/views/livewire/project/database/clickhouse/general.blade.php b/resources/views/livewire/project/database/clickhouse/general.blade.php index 2010e0afc..d4b95d444 100644 --- a/resources/views/livewire/project/database/clickhouse/general.blade.php +++ b/resources/views/livewire/project/database/clickhouse/general.blade.php @@ -78,6 +78,8 @@ +

Advanced

diff --git a/resources/views/livewire/project/database/dragonfly/general.blade.php b/resources/views/livewire/project/database/dragonfly/general.blade.php index 2b2e5d355..f33cf1546 100644 --- a/resources/views/livewire/project/database/dragonfly/general.blade.php +++ b/resources/views/livewire/project/database/dragonfly/general.blade.php @@ -115,6 +115,8 @@ +

Advanced

diff --git a/resources/views/livewire/project/database/keydb/general.blade.php b/resources/views/livewire/project/database/keydb/general.blade.php index 00c30edff..6e69fd76d 100644 --- a/resources/views/livewire/project/database/keydb/general.blade.php +++ b/resources/views/livewire/project/database/keydb/general.blade.php @@ -115,6 +115,8 @@ + + diff --git a/resources/views/livewire/project/database/mongodb/general.blade.php b/resources/views/livewire/project/database/mongodb/general.blade.php index a474153f1..4202578dd 100644 --- a/resources/views/livewire/project/database/mongodb/general.blade.php +++ b/resources/views/livewire/project/database/mongodb/general.blade.php @@ -153,6 +153,8 @@ + diff --git a/resources/views/livewire/project/database/mysql/general.blade.php b/resources/views/livewire/project/database/mysql/general.blade.php index 8187878e4..cc23cae20 100644 --- a/resources/views/livewire/project/database/mysql/general.blade.php +++ b/resources/views/livewire/project/database/mysql/general.blade.php @@ -155,6 +155,8 @@ +

Advanced

diff --git a/resources/views/livewire/project/database/postgresql/general.blade.php b/resources/views/livewire/project/database/postgresql/general.blade.php index 7300b913a..e5cfe4785 100644 --- a/resources/views/livewire/project/database/postgresql/general.blade.php +++ b/resources/views/livewire/project/database/postgresql/general.blade.php @@ -165,6 +165,8 @@ +
diff --git a/resources/views/livewire/project/database/redis/general.blade.php b/resources/views/livewire/project/database/redis/general.blade.php index f37674186..e0c3e430a 100644 --- a/resources/views/livewire/project/database/redis/general.blade.php +++ b/resources/views/livewire/project/database/redis/general.blade.php @@ -134,6 +134,8 @@
+ "$host_configuration_dir/nginx.conf", 'target' => '/etc/nginx/nginx.conf', ], ], From 040658c14241c6b962c64354255b7dda9bec5785 Mon Sep 17 00:00:00 2001 From: "Brendan G. Lim" <1788+brendanlim@users.noreply.github.com> Date: Fri, 27 Feb 2026 14:24:04 -0800 Subject: [PATCH 04/17] fix: address review feedback on proxy timeout - Fix disable logic: timeout editable when proxy is stopped - Remove hardcoded proxy_connect_timeout (60s is nginx default) - Remove misleading '0 for no timeout' helper text - Add min:1 validation for timeout value --- app/Actions/Database/StartDatabaseProxy.php | 1 - app/Livewire/Project/Database/Clickhouse/General.php | 3 ++- app/Livewire/Project/Database/Dragonfly/General.php | 3 ++- app/Livewire/Project/Database/Keydb/General.php | 3 ++- app/Livewire/Project/Database/Mariadb/General.php | 3 ++- app/Livewire/Project/Database/Mongodb/General.php | 3 ++- app/Livewire/Project/Database/Mysql/General.php | 3 ++- app/Livewire/Project/Database/Postgresql/General.php | 3 ++- app/Livewire/Project/Database/Redis/General.php | 3 ++- .../livewire/project/database/clickhouse/general.blade.php | 4 ++-- .../livewire/project/database/dragonfly/general.blade.php | 4 ++-- .../views/livewire/project/database/keydb/general.blade.php | 4 ++-- .../views/livewire/project/database/mariadb/general.blade.php | 4 ++-- .../views/livewire/project/database/mongodb/general.blade.php | 4 ++-- .../views/livewire/project/database/mysql/general.blade.php | 4 ++-- .../livewire/project/database/postgresql/general.blade.php | 4 ++-- .../views/livewire/project/database/redis/general.blade.php | 4 ++-- 17 files changed, 32 insertions(+), 25 deletions(-) diff --git a/app/Actions/Database/StartDatabaseProxy.php b/app/Actions/Database/StartDatabaseProxy.php index c7713a965..0d20fa4a4 100644 --- a/app/Actions/Database/StartDatabaseProxy.php +++ b/app/Actions/Database/StartDatabaseProxy.php @@ -70,7 +70,6 @@ public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|St listen $database->public_port; proxy_pass $containerName:$internalPort; $timeoutConfig - proxy_connect_timeout 60s; } } EOF; diff --git a/app/Livewire/Project/Database/Clickhouse/General.php b/app/Livewire/Project/Database/Clickhouse/General.php index ee2ae7bd4..9de75c1c5 100644 --- a/app/Livewire/Project/Database/Clickhouse/General.php +++ b/app/Livewire/Project/Database/Clickhouse/General.php @@ -82,7 +82,7 @@ protected function rules(): array 'portsMappings' => 'nullable|string', 'isPublic' => 'nullable|boolean', 'publicPort' => 'nullable|integer', - 'publicPortTimeout' => 'nullable|integer', + 'publicPortTimeout' => 'nullable|integer|min:1', 'customDockerRunOptions' => 'nullable|string', 'dbUrl' => 'nullable|string', 'dbUrlPublic' => 'nullable|string', @@ -103,6 +103,7 @@ protected function messages(): array 'image.string' => 'The Docker Image must be a string.', 'publicPort.integer' => 'The Public Port must be an integer.', 'publicPortTimeout.integer' => 'The Public Port Timeout must be an integer.', + 'publicPortTimeout.min' => 'The Public Port Timeout must be at least 1.', ] ); } diff --git a/app/Livewire/Project/Database/Dragonfly/General.php b/app/Livewire/Project/Database/Dragonfly/General.php index 6d1b5f74f..d35e57a9d 100644 --- a/app/Livewire/Project/Database/Dragonfly/General.php +++ b/app/Livewire/Project/Database/Dragonfly/General.php @@ -93,7 +93,7 @@ protected function rules(): array 'portsMappings' => 'nullable|string', 'isPublic' => 'nullable|boolean', 'publicPort' => 'nullable|integer', - 'publicPortTimeout' => 'nullable|integer', + 'publicPortTimeout' => 'nullable|integer|min:1', 'customDockerRunOptions' => 'nullable|string', 'dbUrl' => 'nullable|string', 'dbUrlPublic' => 'nullable|string', @@ -113,6 +113,7 @@ protected function messages(): array 'image.string' => 'The Docker Image must be a string.', 'publicPort.integer' => 'The Public Port must be an integer.', 'publicPortTimeout.integer' => 'The Public Port Timeout must be an integer.', + 'publicPortTimeout.min' => 'The Public Port Timeout must be at least 1.', ] ); } diff --git a/app/Livewire/Project/Database/Keydb/General.php b/app/Livewire/Project/Database/Keydb/General.php index 19726e413..adb4ccb5f 100644 --- a/app/Livewire/Project/Database/Keydb/General.php +++ b/app/Livewire/Project/Database/Keydb/General.php @@ -96,7 +96,7 @@ protected function rules(): array 'portsMappings' => 'nullable|string', 'isPublic' => 'nullable|boolean', 'publicPort' => 'nullable|integer', - 'publicPortTimeout' => 'nullable|integer', + 'publicPortTimeout' => 'nullable|integer|min:1', 'customDockerRunOptions' => 'nullable|string', 'dbUrl' => 'nullable|string', 'dbUrlPublic' => 'nullable|string', @@ -118,6 +118,7 @@ protected function messages(): array 'image.string' => 'The Docker Image must be a string.', 'publicPort.integer' => 'The Public Port must be an integer.', 'publicPortTimeout.integer' => 'The Public Port Timeout must be an integer.', + 'publicPortTimeout.min' => 'The Public Port Timeout must be at least 1.', ] ); } diff --git a/app/Livewire/Project/Database/Mariadb/General.php b/app/Livewire/Project/Database/Mariadb/General.php index cb7b99a83..14240c82d 100644 --- a/app/Livewire/Project/Database/Mariadb/General.php +++ b/app/Livewire/Project/Database/Mariadb/General.php @@ -81,7 +81,7 @@ protected function rules(): array 'portsMappings' => 'nullable', 'isPublic' => 'nullable|boolean', 'publicPort' => 'nullable|integer', - 'publicPortTimeout' => 'nullable|integer', + 'publicPortTimeout' => 'nullable|integer|min:1', 'isLogDrainEnabled' => 'nullable|boolean', 'customDockerRunOptions' => 'nullable', 'enableSsl' => 'boolean', @@ -101,6 +101,7 @@ protected function messages(): array 'image.required' => 'The Docker Image field is required.', 'publicPort.integer' => 'The Public Port must be an integer.', 'publicPortTimeout.integer' => 'The Public Port Timeout must be an integer.', + 'publicPortTimeout.min' => 'The Public Port Timeout must be at least 1.', ] ); } diff --git a/app/Livewire/Project/Database/Mongodb/General.php b/app/Livewire/Project/Database/Mongodb/General.php index 8c7eea1b3..11419ec71 100644 --- a/app/Livewire/Project/Database/Mongodb/General.php +++ b/app/Livewire/Project/Database/Mongodb/General.php @@ -80,7 +80,7 @@ protected function rules(): array 'portsMappings' => 'nullable', 'isPublic' => 'nullable|boolean', 'publicPort' => 'nullable|integer', - 'publicPortTimeout' => 'nullable|integer', + 'publicPortTimeout' => 'nullable|integer|min:1', 'isLogDrainEnabled' => 'nullable|boolean', 'customDockerRunOptions' => 'nullable', 'enableSsl' => 'boolean', @@ -100,6 +100,7 @@ protected function messages(): array 'image.required' => 'The Docker Image field is required.', 'publicPort.integer' => 'The Public Port must be an integer.', 'publicPortTimeout.integer' => 'The Public Port Timeout must be an integer.', + 'publicPortTimeout.min' => 'The Public Port Timeout must be at least 1.', 'sslMode.in' => 'The SSL Mode must be one of: allow, prefer, require, verify-full.', ] ); diff --git a/app/Livewire/Project/Database/Mysql/General.php b/app/Livewire/Project/Database/Mysql/General.php index 371ab7f68..4f0f5eb19 100644 --- a/app/Livewire/Project/Database/Mysql/General.php +++ b/app/Livewire/Project/Database/Mysql/General.php @@ -83,7 +83,7 @@ protected function rules(): array 'portsMappings' => 'nullable', 'isPublic' => 'nullable|boolean', 'publicPort' => 'nullable|integer', - 'publicPortTimeout' => 'nullable|integer', + 'publicPortTimeout' => 'nullable|integer|min:1', 'isLogDrainEnabled' => 'nullable|boolean', 'customDockerRunOptions' => 'nullable', 'enableSsl' => 'boolean', @@ -104,6 +104,7 @@ protected function messages(): array 'image.required' => 'The Docker Image field is required.', 'publicPort.integer' => 'The Public Port must be an integer.', 'publicPortTimeout.integer' => 'The Public Port Timeout must be an integer.', + 'publicPortTimeout.min' => 'The Public Port Timeout must be at least 1.', 'sslMode.in' => 'The SSL Mode must be one of: PREFERRED, REQUIRED, VERIFY_CA, VERIFY_IDENTITY.', ] ); diff --git a/app/Livewire/Project/Database/Postgresql/General.php b/app/Livewire/Project/Database/Postgresql/General.php index fa8c69789..4e044672b 100644 --- a/app/Livewire/Project/Database/Postgresql/General.php +++ b/app/Livewire/Project/Database/Postgresql/General.php @@ -95,7 +95,7 @@ protected function rules(): array 'portsMappings' => 'nullable', 'isPublic' => 'nullable|boolean', 'publicPort' => 'nullable|integer', - 'publicPortTimeout' => 'nullable|integer', + 'publicPortTimeout' => 'nullable|integer|min:1', 'isLogDrainEnabled' => 'nullable|boolean', 'customDockerRunOptions' => 'nullable', 'enableSsl' => 'boolean', @@ -115,6 +115,7 @@ protected function messages(): array 'image.required' => 'The Docker Image field is required.', 'publicPort.integer' => 'The Public Port must be an integer.', 'publicPortTimeout.integer' => 'The Public Port Timeout must be an integer.', + 'publicPortTimeout.min' => 'The Public Port Timeout must be at least 1.', 'sslMode.in' => 'The SSL Mode must be one of: allow, prefer, require, verify-ca, verify-full.', ] ); diff --git a/app/Livewire/Project/Database/Redis/General.php b/app/Livewire/Project/Database/Redis/General.php index be2242024..ebe2f3ba0 100644 --- a/app/Livewire/Project/Database/Redis/General.php +++ b/app/Livewire/Project/Database/Redis/General.php @@ -76,7 +76,7 @@ protected function rules(): array 'portsMappings' => 'nullable', 'isPublic' => 'nullable|boolean', 'publicPort' => 'nullable|integer', - 'publicPortTimeout' => 'nullable|integer', + 'publicPortTimeout' => 'nullable|integer|min:1', 'isLogDrainEnabled' => 'nullable|boolean', 'customDockerRunOptions' => 'nullable', 'redisUsername' => 'required', @@ -94,6 +94,7 @@ protected function messages(): array 'image.required' => 'The Docker Image field is required.', 'publicPort.integer' => 'The Public Port must be an integer.', 'publicPortTimeout.integer' => 'The Public Port Timeout must be an integer.', + 'publicPortTimeout.min' => 'The Public Port Timeout must be at least 1.', 'redisUsername.required' => 'The Redis Username field is required.', 'redisPassword.required' => 'The Redis Password field is required.', ] diff --git a/resources/views/livewire/project/database/clickhouse/general.blade.php b/resources/views/livewire/project/database/clickhouse/general.blade.php index d4b95d444..ceaaac508 100644 --- a/resources/views/livewire/project/database/clickhouse/general.blade.php +++ b/resources/views/livewire/project/database/clickhouse/general.blade.php @@ -78,8 +78,8 @@ - +

Advanced

diff --git a/resources/views/livewire/project/database/dragonfly/general.blade.php b/resources/views/livewire/project/database/dragonfly/general.blade.php index f33cf1546..e81d51c07 100644 --- a/resources/views/livewire/project/database/dragonfly/general.blade.php +++ b/resources/views/livewire/project/database/dragonfly/general.blade.php @@ -115,8 +115,8 @@ - +

Advanced

diff --git a/resources/views/livewire/project/database/keydb/general.blade.php b/resources/views/livewire/project/database/keydb/general.blade.php index 6e69fd76d..522b96c0a 100644 --- a/resources/views/livewire/project/database/keydb/general.blade.php +++ b/resources/views/livewire/project/database/keydb/general.blade.php @@ -115,8 +115,8 @@ - + - + diff --git a/resources/views/livewire/project/database/mongodb/general.blade.php b/resources/views/livewire/project/database/mongodb/general.blade.php index 4202578dd..fa34b9795 100644 --- a/resources/views/livewire/project/database/mongodb/general.blade.php +++ b/resources/views/livewire/project/database/mongodb/general.blade.php @@ -153,8 +153,8 @@ - + diff --git a/resources/views/livewire/project/database/mysql/general.blade.php b/resources/views/livewire/project/database/mysql/general.blade.php index cc23cae20..b1a75c455 100644 --- a/resources/views/livewire/project/database/mysql/general.blade.php +++ b/resources/views/livewire/project/database/mysql/general.blade.php @@ -155,8 +155,8 @@ - +

Advanced

diff --git a/resources/views/livewire/project/database/postgresql/general.blade.php b/resources/views/livewire/project/database/postgresql/general.blade.php index e5cfe4785..74b1a03a8 100644 --- a/resources/views/livewire/project/database/postgresql/general.blade.php +++ b/resources/views/livewire/project/database/postgresql/general.blade.php @@ -165,8 +165,8 @@ - +
diff --git a/resources/views/livewire/project/database/redis/general.blade.php b/resources/views/livewire/project/database/redis/general.blade.php index e0c3e430a..11ffddd81 100644 --- a/resources/views/livewire/project/database/redis/general.blade.php +++ b/resources/views/livewire/project/database/redis/general.blade.php @@ -134,8 +134,8 @@
- + server) { $this->dispatch('error', 'Server not found. Please refresh the page.'); - return; + return true; } try { @@ -613,7 +625,7 @@ public function restoreFromS3() if (! $this->validateBucketName($bucket)) { $this->dispatch('error', 'Invalid S3 bucket name. Bucket name must contain only alphanumerics, dots, dashes, and underscores.'); - return; + return true; } // Clean the S3 path @@ -623,7 +635,7 @@ public function restoreFromS3() if (! $this->validateS3Path($cleanPath)) { $this->dispatch('error', 'Invalid S3 path. Path must contain only safe characters (alphanumerics, dots, dashes, underscores, slashes).'); - return; + return true; } // Get helper image @@ -711,9 +723,12 @@ public function restoreFromS3() $this->dispatch('info', 'Restoring database from S3. Progress will be shown in the activity monitor...'); } catch (\Throwable $e) { $this->importRunning = false; + handleError($e, $this); - return handleError($e, $this); + return true; } + + return true; } public function buildRestoreCommand(string $tmpPath): string From 01aa53455616154c402b0f17ecb72ddde338a585 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:20:33 +0100 Subject: [PATCH 06/17] fix(application-source): support localhost key with id=0 Previously, the view checked $privateKeyId with ! operator, which incorrectly treats 0 (localhost key) as falsy. Changed to explicit is_null() checks to distinguish between null (no key) and 0 (localhost). Added test coverage for both cases. --- .../project/application/source.blade.php | 4 +- templates/service-templates-latest.json | 6 +- templates/service-templates.json | 6 +- .../ApplicationSourceLocalhostKeyTest.php | 59 +++++++++++++++++++ 4 files changed, 67 insertions(+), 8 deletions(-) create mode 100644 tests/Feature/ApplicationSourceLocalhostKeyTest.php diff --git a/resources/views/livewire/project/application/source.blade.php b/resources/views/livewire/project/application/source.blade.php index 9d0d53f2e..3178d52b9 100644 --- a/resources/views/livewire/project/application/source.blade.php +++ b/resources/views/livewire/project/application/source.blade.php @@ -28,7 +28,7 @@
Code source of your application.
- @if (!$privateKeyId) + @if (is_null($privateKeyId))
Currently connected source: {{ data_get($application, 'source.name', 'No source connected') }}
@@ -44,7 +44,7 @@ class="font-bold text-warning">{{ data_get($application, 'source.name', 'No sour
- @if ($privateKeyId) + @if (!is_null($privateKeyId))

Deploy Key

Currently attached Private Key: {{ $privateKeyName }} diff --git a/templates/service-templates-latest.json b/templates/service-templates-latest.json index 6c7af5dc5..2ea3ce8c5 100644 --- a/templates/service-templates-latest.json +++ b/templates/service-templates-latest.json @@ -2884,7 +2884,7 @@ "n8n-with-postgres-and-worker": { "documentation": "https://n8n.io?utm_source=coolify.io", "slogan": "n8n is an extendable workflow automation tool with queue mode and workers.", - "compose": "c2VydmljZXM6CiAgbjhuOgogICAgaW1hZ2U6ICduOG5pby9uOG46Mi4xLjUnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9OOE5fNTY3OAogICAgICAtICdOOE5fRURJVE9SX0JBU0VfVVJMPSR7U0VSVklDRV9VUkxfTjhOfScKICAgICAgLSAnV0VCSE9PS19VUkw9JHtTRVJWSUNFX1VSTF9OOE59JwogICAgICAtICdOOE5fSE9TVD0ke1NFUlZJQ0VfVVJMX044Tn0nCiAgICAgIC0gJ044Tl9QUk9UT0NPTD0ke044Tl9QUk9UT0NPTDotaHR0cHN9JwogICAgICAtICdHRU5FUklDX1RJTUVaT05FPSR7R0VORVJJQ19USU1FWk9ORTotVVRDfScKICAgICAgLSAnVFo9JHtUWjotVVRDfScKICAgICAgLSBEQl9UWVBFPXBvc3RncmVzZGIKICAgICAgLSAnREJfUE9TVEdSRVNEQl9EQVRBQkFTRT0ke1BPU1RHUkVTX0RCOi1uOG59JwogICAgICAtIERCX1BPU1RHUkVTREJfSE9TVD1wb3N0Z3Jlc3FsCiAgICAgIC0gREJfUE9TVEdSRVNEQl9QT1JUPTU0MzIKICAgICAgLSBEQl9QT1NUR1JFU0RCX1VTRVI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgICAtIERCX1BPU1RHUkVTREJfU0NIRU1BPXB1YmxpYwogICAgICAtIERCX1BPU1RHUkVTREJfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSBFWEVDVVRJT05TX01PREU9cXVldWUKICAgICAgLSBRVUVVRV9CVUxMX1JFRElTX0hPU1Q9cmVkaXMKICAgICAgLSBRVUVVRV9IRUFMVEhfQ0hFQ0tfQUNUSVZFPXRydWUKICAgICAgLSAnTjhOX0VOQ1JZUFRJT05fS0VZPSR7U0VSVklDRV9QQVNTV09SRF9FTkNSWVBUSU9OfScKICAgICAgLSBOOE5fUlVOTkVSU19FTkFCTEVEPXRydWUKICAgICAgLSBOOE5fUlVOTkVSU19NT0RFPWV4dGVybmFsCiAgICAgIC0gJ044Tl9SVU5ORVJTX0JST0tFUl9MSVNURU5fQUREUkVTUz0ke044Tl9SVU5ORVJTX0JST0tFUl9MSVNURU5fQUREUkVTUzotMC4wLjAuMH0nCiAgICAgIC0gJ044Tl9SVU5ORVJTX0JST0tFUl9QT1JUPSR7TjhOX1JVTk5FUlNfQlJPS0VSX1BPUlQ6LTU2Nzl9JwogICAgICAtIE44Tl9SVU5ORVJTX0FVVEhfVE9LRU49JFNFUlZJQ0VfUEFTU1dPUkRfTjhOCiAgICAgIC0gJ044Tl9OQVRJVkVfUFlUSE9OX1JVTk5FUj0ke044Tl9OQVRJVkVfUFlUSE9OX1JVTk5FUjotdHJ1ZX0nCiAgICAgIC0gJ044Tl9SVU5ORVJTX01BWF9DT05DVVJSRU5DWT0ke044Tl9SVU5ORVJTX01BWF9DT05DVVJSRU5DWTotNX0nCiAgICAgIC0gT0ZGTE9BRF9NQU5VQUxfRVhFQ1VUSU9OU19UT19XT1JLRVJTPXRydWUKICAgICAgLSAnTjhOX0JMT0NLX0VOVl9BQ0NFU1NfSU5fTk9ERT0ke044Tl9CTE9DS19FTlZfQUNDRVNTX0lOX05PREU6LXRydWV9JwogICAgICAtICdOOE5fR0lUX05PREVfRElTQUJMRV9CQVJFX1JFUE9TPSR7TjhOX0dJVF9OT0RFX0RJU0FCTEVfQkFSRV9SRVBPUzotdHJ1ZX0nCiAgICAgIC0gJ044Tl9FTkZPUkNFX1NFVFRJTkdTX0ZJTEVfUEVSTUlTU0lPTlM9JHtOOE5fRU5GT1JDRV9TRVRUSU5HU19GSUxFX1BFUk1JU1NJT05TOi10cnVlfScKICAgICAgLSAnTjhOX1BST1hZX0hPUFM9JHtOOE5fUFJPWFlfSE9QUzotMX0nCiAgICAgIC0gJ044Tl9TS0lQX0FVVEhfT05fT0FVVEhfQ0FMTEJBQ0s9JHtOOE5fU0tJUF9BVVRIX09OX09BVVRIX0NBTExCQUNLOi1mYWxzZX0nCiAgICB2b2x1bWVzOgogICAgICAtICduOG4tZGF0YTovaG9tZS9ub2RlLy5uOG4nCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3Jlc3FsOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHJlZGlzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3dnZXQgLXFPLSBodHRwOi8vMTI3LjAuMC4xOjU2NzgvJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgbjhuLXdvcmtlcjoKICAgIGltYWdlOiAnbjhuaW8vbjhuOjIuMS41JwogICAgY29tbWFuZDogd29ya2VyCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnR0VORVJJQ19USU1FWk9ORT0ke0dFTkVSSUNfVElNRVpPTkU6LVVUQ30nCiAgICAgIC0gJ1RaPSR7VFo6LVVUQ30nCiAgICAgIC0gREJfVFlQRT1wb3N0Z3Jlc2RiCiAgICAgIC0gJ0RCX1BPU1RHUkVTREJfREFUQUJBU0U9JHtQT1NUR1JFU19EQjotbjhufScKICAgICAgLSBEQl9QT1NUR1JFU0RCX0hPU1Q9cG9zdGdyZXNxbAogICAgICAtIERCX1BPU1RHUkVTREJfUE9SVD01NDMyCiAgICAgIC0gREJfUE9TVEdSRVNEQl9VU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBEQl9QT1NUR1JFU0RCX1NDSEVNQT1wdWJsaWMKICAgICAgLSBEQl9QT1NUR1JFU0RCX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gRVhFQ1VUSU9OU19NT0RFPXF1ZXVlCiAgICAgIC0gUVVFVUVfQlVMTF9SRURJU19IT1NUPXJlZGlzCiAgICAgIC0gUVVFVUVfSEVBTFRIX0NIRUNLX0FDVElWRT10cnVlCiAgICAgIC0gJ044Tl9FTkNSWVBUSU9OX0tFWT0ke1NFUlZJQ0VfUEFTU1dPUkRfRU5DUllQVElPTn0nCiAgICAgIC0gTjhOX1JVTk5FUlNfRU5BQkxFRD10cnVlCiAgICAgIC0gTjhOX1JVTk5FUlNfTU9ERT1leHRlcm5hbAogICAgICAtICdOOE5fUlVOTkVSU19CUk9LRVJfTElTVEVOX0FERFJFU1M9JHtOOE5fUlVOTkVSU19CUk9LRVJfTElTVEVOX0FERFJFU1M6LTAuMC4wLjB9JwogICAgICAtICdOOE5fUlVOTkVSU19CUk9LRVJfUE9SVD0ke044Tl9SVU5ORVJTX0JST0tFUl9QT1JUOi01Njc5fScKICAgICAgLSBOOE5fUlVOTkVSU19BVVRIX1RPS0VOPSRTRVJWSUNFX1BBU1NXT1JEX044TgogICAgICAtICdOOE5fTkFUSVZFX1BZVEhPTl9SVU5ORVI9JHtOOE5fTkFUSVZFX1BZVEhPTl9SVU5ORVI6LXRydWV9JwogICAgICAtICdOOE5fUlVOTkVSU19NQVhfQ09OQ1VSUkVOQ1k9JHtOOE5fUlVOTkVSU19NQVhfQ09OQ1VSUkVOQ1k6LTV9JwogICAgICAtICdOOE5fQkxPQ0tfRU5WX0FDQ0VTU19JTl9OT0RFPSR7TjhOX0JMT0NLX0VOVl9BQ0NFU1NfSU5fTk9ERTotdHJ1ZX0nCiAgICAgIC0gJ044Tl9HSVRfTk9ERV9ESVNBQkxFX0JBUkVfUkVQT1M9JHtOOE5fR0lUX05PREVfRElTQUJMRV9CQVJFX1JFUE9TOi10cnVlfScKICAgICAgLSAnTjhOX0VORk9SQ0VfU0VUVElOR1NfRklMRV9QRVJNSVNTSU9OUz0ke044Tl9FTkZPUkNFX1NFVFRJTkdTX0ZJTEVfUEVSTUlTU0lPTlM6LXRydWV9JwogICAgICAtICdOOE5fUFJPWFlfSE9QUz0ke044Tl9QUk9YWV9IT1BTOi0xfScKICAgICAgLSAnTjhOX1NLSVBfQVVUSF9PTl9PQVVUSF9DQUxMQkFDSz0ke044Tl9TS0lQX0FVVEhfT05fT0FVVEhfQ0FMTEJBQ0s6LWZhbHNlfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ244bi1kYXRhOi9ob21lL25vZGUvLm44bicKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAnd2dldCAtcU8tIGh0dHA6Ly8xMjcuMC4wLjE6NTY3OC9oZWFsdGh6JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgICBkZXBlbmRzX29uOgogICAgICBuOG46CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcG9zdGdyZXNxbDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICByZWRpczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogIHBvc3RncmVzcWw6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3Bvc3RncmVzcWwtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1NUR1JFU19VU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi1uOG59JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHJlZGlzOgogICAgaW1hZ2U6ICdyZWRpczo2LWFscGluZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3JlZGlzLWRhdGE6L2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gcmVkaXMtY2xpCiAgICAgICAgLSBwaW5nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiA1cwogICAgICByZXRyaWVzOiAxMAogIHRhc2stcnVubmVyczoKICAgIGltYWdlOiAnbjhuaW8vcnVubmVyczoyLjEuNScKICAgIGVudmlyb25tZW50OgogICAgICAtICdOOE5fUlVOTkVSU19UQVNLX0JST0tFUl9VUkk9JHtOOE5fUlVOTkVSU19UQVNLX0JST0tFUl9VUkk6LWh0dHA6Ly9uOG4td29ya2VyOjU2Nzl9JwogICAgICAtIE44Tl9SVU5ORVJTX0FVVEhfVE9LRU49JFNFUlZJQ0VfUEFTU1dPUkRfTjhOCiAgICAgIC0gJ044Tl9SVU5ORVJTX0FVVE9fU0hVVERPV05fVElNRU9VVD0ke044Tl9SVU5ORVJTX0FVVE9fU0hVVERPV05fVElNRU9VVDotMTV9JwogICAgICAtICdOOE5fUlVOTkVSU19NQVhfQ09OQ1VSUkVOQ1k9JHtOOE5fUlVOTkVSU19NQVhfQ09OQ1VSUkVOQ1k6LTV9JwogICAgZGVwZW5kc19vbjoKICAgICAgLSBuOG4KICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAnd2dldCAtcU8tIGh0dHA6Ly8xMjcuMC4wLjE6NTY4MC8nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK", + "compose": "c2VydmljZXM6CiAgbjhuOgogICAgaW1hZ2U6ICduOG5pby9uOG46Mi4xMC4yJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9VUkxfTjhOXzU2NzgKICAgICAgLSAnTjhOX0VESVRPUl9CQVNFX1VSTD0ke1NFUlZJQ0VfVVJMX044Tn0nCiAgICAgIC0gJ1dFQkhPT0tfVVJMPSR7U0VSVklDRV9VUkxfTjhOfScKICAgICAgLSAnTjhOX0hPU1Q9JHtTRVJWSUNFX1VSTF9OOE59JwogICAgICAtICdOOE5fUFJPVE9DT0w9JHtOOE5fUFJPVE9DT0w6LWh0dHBzfScKICAgICAgLSAnR0VORVJJQ19USU1FWk9ORT0ke0dFTkVSSUNfVElNRVpPTkU6LVVUQ30nCiAgICAgIC0gJ1RaPSR7VFo6LVVUQ30nCiAgICAgIC0gREJfVFlQRT1wb3N0Z3Jlc2RiCiAgICAgIC0gJ0RCX1BPU1RHUkVTREJfREFUQUJBU0U9JHtQT1NUR1JFU19EQjotbjhufScKICAgICAgLSBEQl9QT1NUR1JFU0RCX0hPU1Q9cG9zdGdyZXNxbAogICAgICAtIERCX1BPU1RHUkVTREJfUE9SVD01NDMyCiAgICAgIC0gREJfUE9TVEdSRVNEQl9VU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBEQl9QT1NUR1JFU0RCX1NDSEVNQT1wdWJsaWMKICAgICAgLSBEQl9QT1NUR1JFU0RCX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gRVhFQ1VUSU9OU19NT0RFPXF1ZXVlCiAgICAgIC0gUVVFVUVfQlVMTF9SRURJU19IT1NUPXJlZGlzCiAgICAgIC0gUVVFVUVfSEVBTFRIX0NIRUNLX0FDVElWRT10cnVlCiAgICAgIC0gJ044Tl9FTkNSWVBUSU9OX0tFWT0ke1NFUlZJQ0VfUEFTU1dPUkRfRU5DUllQVElPTn0nCiAgICAgIC0gTjhOX1JVTk5FUlNfRU5BQkxFRD10cnVlCiAgICAgIC0gTjhOX1JVTk5FUlNfTU9ERT1leHRlcm5hbAogICAgICAtICdOOE5fUlVOTkVSU19CUk9LRVJfTElTVEVOX0FERFJFU1M9JHtOOE5fUlVOTkVSU19CUk9LRVJfTElTVEVOX0FERFJFU1M6LTAuMC4wLjB9JwogICAgICAtICdOOE5fUlVOTkVSU19CUk9LRVJfUE9SVD0ke044Tl9SVU5ORVJTX0JST0tFUl9QT1JUOi01Njc5fScKICAgICAgLSBOOE5fUlVOTkVSU19BVVRIX1RPS0VOPSRTRVJWSUNFX1BBU1NXT1JEX044TgogICAgICAtICdOOE5fTkFUSVZFX1BZVEhPTl9SVU5ORVI9JHtOOE5fTkFUSVZFX1BZVEhPTl9SVU5ORVI6LXRydWV9JwogICAgICAtICdOOE5fUlVOTkVSU19NQVhfQ09OQ1VSUkVOQ1k9JHtOOE5fUlVOTkVSU19NQVhfQ09OQ1VSUkVOQ1k6LTV9JwogICAgICAtIE9GRkxPQURfTUFOVUFMX0VYRUNVVElPTlNfVE9fV09SS0VSUz10cnVlCiAgICAgIC0gJ044Tl9CTE9DS19FTlZfQUNDRVNTX0lOX05PREU9JHtOOE5fQkxPQ0tfRU5WX0FDQ0VTU19JTl9OT0RFOi10cnVlfScKICAgICAgLSAnTjhOX0dJVF9OT0RFX0RJU0FCTEVfQkFSRV9SRVBPUz0ke044Tl9HSVRfTk9ERV9ESVNBQkxFX0JBUkVfUkVQT1M6LXRydWV9JwogICAgICAtICdOOE5fRU5GT1JDRV9TRVRUSU5HU19GSUxFX1BFUk1JU1NJT05TPSR7TjhOX0VORk9SQ0VfU0VUVElOR1NfRklMRV9QRVJNSVNTSU9OUzotdHJ1ZX0nCiAgICAgIC0gJ044Tl9QUk9YWV9IT1BTPSR7TjhOX1BST1hZX0hPUFM6LTF9JwogICAgICAtICdOOE5fU0tJUF9BVVRIX09OX09BVVRIX0NBTExCQUNLPSR7TjhOX1NLSVBfQVVUSF9PTl9PQVVUSF9DQUxMQkFDSzotZmFsc2V9JwogICAgdm9sdW1lczoKICAgICAgLSAnbjhuLWRhdGE6L2hvbWUvbm9kZS8ubjhuJwogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXNxbDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICByZWRpczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICd3Z2V0IC1xTy0gaHR0cDovLzEyNy4wLjAuMTo1Njc4LycKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIG44bi13b3JrZXI6CiAgICBpbWFnZTogJ244bmlvL244bjoyLjEwLjInCiAgICBjb21tYW5kOiB3b3JrZXIKICAgIGVudmlyb25tZW50OgogICAgICAtICdHRU5FUklDX1RJTUVaT05FPSR7R0VORVJJQ19USU1FWk9ORTotVVRDfScKICAgICAgLSAnVFo9JHtUWjotVVRDfScKICAgICAgLSBEQl9UWVBFPXBvc3RncmVzZGIKICAgICAgLSAnREJfUE9TVEdSRVNEQl9EQVRBQkFTRT0ke1BPU1RHUkVTX0RCOi1uOG59JwogICAgICAtIERCX1BPU1RHUkVTREJfSE9TVD1wb3N0Z3Jlc3FsCiAgICAgIC0gREJfUE9TVEdSRVNEQl9QT1JUPTU0MzIKICAgICAgLSBEQl9QT1NUR1JFU0RCX1VTRVI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgICAtIERCX1BPU1RHUkVTREJfU0NIRU1BPXB1YmxpYwogICAgICAtIERCX1BPU1RHUkVTREJfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSBFWEVDVVRJT05TX01PREU9cXVldWUKICAgICAgLSBRVUVVRV9CVUxMX1JFRElTX0hPU1Q9cmVkaXMKICAgICAgLSBRVUVVRV9IRUFMVEhfQ0hFQ0tfQUNUSVZFPXRydWUKICAgICAgLSAnTjhOX0VOQ1JZUFRJT05fS0VZPSR7U0VSVklDRV9QQVNTV09SRF9FTkNSWVBUSU9OfScKICAgICAgLSBOOE5fUlVOTkVSU19FTkFCTEVEPXRydWUKICAgICAgLSBOOE5fUlVOTkVSU19NT0RFPWV4dGVybmFsCiAgICAgIC0gJ044Tl9SVU5ORVJTX0JST0tFUl9MSVNURU5fQUREUkVTUz0ke044Tl9SVU5ORVJTX0JST0tFUl9MSVNURU5fQUREUkVTUzotMC4wLjAuMH0nCiAgICAgIC0gJ044Tl9SVU5ORVJTX0JST0tFUl9QT1JUPSR7TjhOX1JVTk5FUlNfQlJPS0VSX1BPUlQ6LTU2Nzl9JwogICAgICAtIE44Tl9SVU5ORVJTX0FVVEhfVE9LRU49JFNFUlZJQ0VfUEFTU1dPUkRfTjhOCiAgICAgIC0gJ044Tl9OQVRJVkVfUFlUSE9OX1JVTk5FUj0ke044Tl9OQVRJVkVfUFlUSE9OX1JVTk5FUjotdHJ1ZX0nCiAgICAgIC0gJ044Tl9SVU5ORVJTX01BWF9DT05DVVJSRU5DWT0ke044Tl9SVU5ORVJTX01BWF9DT05DVVJSRU5DWTotNX0nCiAgICAgIC0gJ044Tl9CTE9DS19FTlZfQUNDRVNTX0lOX05PREU9JHtOOE5fQkxPQ0tfRU5WX0FDQ0VTU19JTl9OT0RFOi10cnVlfScKICAgICAgLSAnTjhOX0dJVF9OT0RFX0RJU0FCTEVfQkFSRV9SRVBPUz0ke044Tl9HSVRfTk9ERV9ESVNBQkxFX0JBUkVfUkVQT1M6LXRydWV9JwogICAgICAtICdOOE5fRU5GT1JDRV9TRVRUSU5HU19GSUxFX1BFUk1JU1NJT05TPSR7TjhOX0VORk9SQ0VfU0VUVElOR1NfRklMRV9QRVJNSVNTSU9OUzotdHJ1ZX0nCiAgICAgIC0gJ044Tl9QUk9YWV9IT1BTPSR7TjhOX1BST1hZX0hPUFM6LTF9JwogICAgICAtICdOOE5fU0tJUF9BVVRIX09OX09BVVRIX0NBTExCQUNLPSR7TjhOX1NLSVBfQVVUSF9PTl9PQVVUSF9DQUxMQkFDSzotZmFsc2V9JwogICAgdm9sdW1lczoKICAgICAgLSAnbjhuLWRhdGE6L2hvbWUvbm9kZS8ubjhuJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICd3Z2V0IC1xTy0gaHR0cDovLzEyNy4wLjAuMTo1Njc4L2hlYWx0aHonCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICAgIGRlcGVuZHNfb246CiAgICAgIG44bjoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICBwb3N0Z3Jlc3FsOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHJlZGlzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgcG9zdGdyZXNxbDoKICAgIGltYWdlOiAncG9zdGdyZXM6MTYtYWxwaW5lJwogICAgdm9sdW1lczoKICAgICAgLSAncG9zdGdyZXNxbC1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGVudmlyb25tZW50OgogICAgICAtIFBPU1RHUkVTX1VTRVI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgICAtIFBPU1RHUkVTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNfREI6LW44bn0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgcmVkaXM6CiAgICBpbWFnZTogJ3JlZGlzOjYtYWxwaW5lJwogICAgdm9sdW1lczoKICAgICAgLSAncmVkaXMtZGF0YTovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSByZWRpcy1jbGkKICAgICAgICAtIHBpbmcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIHJldHJpZXM6IDEwCiAgdGFzay1ydW5uZXJzOgogICAgaW1hZ2U6ICduOG5pby9ydW5uZXJzOjIuMTAuMicKICAgIGVudmlyb25tZW50OgogICAgICAtICdOOE5fUlVOTkVSU19UQVNLX0JST0tFUl9VUkk9JHtOOE5fUlVOTkVSU19UQVNLX0JST0tFUl9VUkk6LWh0dHA6Ly9uOG4td29ya2VyOjU2Nzl9JwogICAgICAtIE44Tl9SVU5ORVJTX0FVVEhfVE9LRU49JFNFUlZJQ0VfUEFTU1dPUkRfTjhOCiAgICAgIC0gJ044Tl9SVU5ORVJTX0FVVE9fU0hVVERPV05fVElNRU9VVD0ke044Tl9SVU5ORVJTX0FVVE9fU0hVVERPV05fVElNRU9VVDotMTV9JwogICAgICAtICdOOE5fUlVOTkVSU19NQVhfQ09OQ1VSUkVOQ1k9JHtOOE5fUlVOTkVSU19NQVhfQ09OQ1VSUkVOQ1k6LTV9JwogICAgZGVwZW5kc19vbjoKICAgICAgLSBuOG4KICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAnd2dldCAtcU8tIGh0dHA6Ly8xMjcuMC4wLjE6NTY4MC8nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK", "tags": [ "n8n", "workflow", @@ -2905,7 +2905,7 @@ "n8n-with-postgresql": { "documentation": "https://n8n.io?utm_source=coolify.io", "slogan": "n8n is an extendable workflow automation tool.", - "compose": "c2VydmljZXM6CiAgbjhuOgogICAgaW1hZ2U6ICduOG5pby9uOG46Mi4xLjUnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9OOE5fNTY3OAogICAgICAtICdOOE5fRURJVE9SX0JBU0VfVVJMPSR7U0VSVklDRV9VUkxfTjhOfScKICAgICAgLSAnV0VCSE9PS19VUkw9JHtTRVJWSUNFX1VSTF9OOE59JwogICAgICAtICdOOE5fSE9TVD0ke1NFUlZJQ0VfVVJMX044Tn0nCiAgICAgIC0gJ044Tl9QUk9UT0NPTD0ke044Tl9QUk9UT0NPTDotaHR0cHN9JwogICAgICAtICdHRU5FUklDX1RJTUVaT05FPSR7R0VORVJJQ19USU1FWk9ORTotVVRDfScKICAgICAgLSAnVFo9JHtUWjotVVRDfScKICAgICAgLSBEQl9UWVBFPXBvc3RncmVzZGIKICAgICAgLSAnREJfUE9TVEdSRVNEQl9EQVRBQkFTRT0ke1BPU1RHUkVTX0RCOi1uOG59JwogICAgICAtIERCX1BPU1RHUkVTREJfSE9TVD1wb3N0Z3Jlc3FsCiAgICAgIC0gREJfUE9TVEdSRVNEQl9QT1JUPTU0MzIKICAgICAgLSBEQl9QT1NUR1JFU0RCX1VTRVI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgICAtIERCX1BPU1RHUkVTREJfU0NIRU1BPXB1YmxpYwogICAgICAtIERCX1BPU1RHUkVTREJfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSBOOE5fUlVOTkVSU19FTkFCTEVEPXRydWUKICAgICAgLSBOOE5fUlVOTkVSU19NT0RFPWV4dGVybmFsCiAgICAgIC0gJ044Tl9SVU5ORVJTX0JST0tFUl9MSVNURU5fQUREUkVTUz0ke044Tl9SVU5ORVJTX0JST0tFUl9MSVNURU5fQUREUkVTUzotMC4wLjAuMH0nCiAgICAgIC0gJ044Tl9SVU5ORVJTX0JST0tFUl9QT1JUPSR7TjhOX1JVTk5FUlNfQlJPS0VSX1BPUlQ6LTU2Nzl9JwogICAgICAtIE44Tl9SVU5ORVJTX0FVVEhfVE9LRU49JFNFUlZJQ0VfUEFTU1dPUkRfTjhOCiAgICAgIC0gJ044Tl9OQVRJVkVfUFlUSE9OX1JVTk5FUj0ke044Tl9OQVRJVkVfUFlUSE9OX1JVTk5FUjotdHJ1ZX0nCiAgICAgIC0gJ044Tl9SVU5ORVJTX01BWF9DT05DVVJSRU5DWT0ke044Tl9SVU5ORVJTX01BWF9DT05DVVJSRU5DWTotNX0nCiAgICAgIC0gJ044Tl9CTE9DS19FTlZfQUNDRVNTX0lOX05PREU9JHtOOE5fQkxPQ0tfRU5WX0FDQ0VTU19JTl9OT0RFOi10cnVlfScKICAgICAgLSAnTjhOX0dJVF9OT0RFX0RJU0FCTEVfQkFSRV9SRVBPUz0ke044Tl9HSVRfTk9ERV9ESVNBQkxFX0JBUkVfUkVQT1M6LXRydWV9JwogICAgICAtICdOOE5fRU5GT1JDRV9TRVRUSU5HU19GSUxFX1BFUk1JU1NJT05TPSR7TjhOX0VORk9SQ0VfU0VUVElOR1NfRklMRV9QRVJNSVNTSU9OUzotdHJ1ZX0nCiAgICAgIC0gJ044Tl9QUk9YWV9IT1BTPSR7TjhOX1BST1hZX0hPUFM6LTF9JwogICAgICAtICdOOE5fU0tJUF9BVVRIX09OX09BVVRIX0NBTExCQUNLPSR7TjhOX1NLSVBfQVVUSF9PTl9PQVVUSF9DQUxMQkFDSzotZmFsc2V9JwogICAgdm9sdW1lczoKICAgICAgLSAnbjhuLWRhdGE6L2hvbWUvbm9kZS8ubjhuJwogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXNxbDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICd3Z2V0IC1xTy0gaHR0cDovLzEyNy4wLjAuMTo1Njc4LycKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHRhc2stcnVubmVyczoKICAgIGltYWdlOiAnbjhuaW8vcnVubmVyczoyLjEuNScKICAgIGVudmlyb25tZW50OgogICAgICAtICdOOE5fUlVOTkVSU19UQVNLX0JST0tFUl9VUkk9JHtOOE5fUlVOTkVSU19UQVNLX0JST0tFUl9VUkk6LWh0dHA6Ly9uOG46NTY3OX0nCiAgICAgIC0gTjhOX1JVTk5FUlNfQVVUSF9UT0tFTj0kU0VSVklDRV9QQVNTV09SRF9OOE4KICAgICAgLSAnTjhOX1JVTk5FUlNfQVVUT19TSFVURE9XTl9USU1FT1VUPSR7TjhOX1JVTk5FUlNfQVVUT19TSFVURE9XTl9USU1FT1VUOi0xNX0nCiAgICAgIC0gJ044Tl9SVU5ORVJTX01BWF9DT05DVVJSRU5DWT0ke044Tl9SVU5ORVJTX01BWF9DT05DVVJSRU5DWTotNX0nCiAgICBkZXBlbmRzX29uOgogICAgICAtIG44bgogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICd3Z2V0IC1xTy0gaHR0cDovLzEyNy4wLjAuMTo1NjgwLycKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHBvc3RncmVzcWw6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3Bvc3RncmVzcWwtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1NUR1JFU19VU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi1uOG59JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=", + "compose": "c2VydmljZXM6CiAgbjhuOgogICAgaW1hZ2U6ICduOG5pby9uOG46Mi4xMC4yJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9VUkxfTjhOXzU2NzgKICAgICAgLSAnTjhOX0VESVRPUl9CQVNFX1VSTD0ke1NFUlZJQ0VfVVJMX044Tn0nCiAgICAgIC0gJ1dFQkhPT0tfVVJMPSR7U0VSVklDRV9VUkxfTjhOfScKICAgICAgLSAnTjhOX0hPU1Q9JHtTRVJWSUNFX1VSTF9OOE59JwogICAgICAtICdOOE5fUFJPVE9DT0w9JHtOOE5fUFJPVE9DT0w6LWh0dHBzfScKICAgICAgLSAnR0VORVJJQ19USU1FWk9ORT0ke0dFTkVSSUNfVElNRVpPTkU6LVVUQ30nCiAgICAgIC0gJ1RaPSR7VFo6LVVUQ30nCiAgICAgIC0gREJfVFlQRT1wb3N0Z3Jlc2RiCiAgICAgIC0gJ0RCX1BPU1RHUkVTREJfREFUQUJBU0U9JHtQT1NUR1JFU19EQjotbjhufScKICAgICAgLSBEQl9QT1NUR1JFU0RCX0hPU1Q9cG9zdGdyZXNxbAogICAgICAtIERCX1BPU1RHUkVTREJfUE9SVD01NDMyCiAgICAgIC0gREJfUE9TVEdSRVNEQl9VU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBEQl9QT1NUR1JFU0RCX1NDSEVNQT1wdWJsaWMKICAgICAgLSBEQl9QT1NUR1JFU0RCX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gTjhOX1JVTk5FUlNfRU5BQkxFRD10cnVlCiAgICAgIC0gTjhOX1JVTk5FUlNfTU9ERT1leHRlcm5hbAogICAgICAtICdOOE5fUlVOTkVSU19CUk9LRVJfTElTVEVOX0FERFJFU1M9JHtOOE5fUlVOTkVSU19CUk9LRVJfTElTVEVOX0FERFJFU1M6LTAuMC4wLjB9JwogICAgICAtICdOOE5fUlVOTkVSU19CUk9LRVJfUE9SVD0ke044Tl9SVU5ORVJTX0JST0tFUl9QT1JUOi01Njc5fScKICAgICAgLSBOOE5fUlVOTkVSU19BVVRIX1RPS0VOPSRTRVJWSUNFX1BBU1NXT1JEX044TgogICAgICAtICdOOE5fTkFUSVZFX1BZVEhPTl9SVU5ORVI9JHtOOE5fTkFUSVZFX1BZVEhPTl9SVU5ORVI6LXRydWV9JwogICAgICAtICdOOE5fUlVOTkVSU19NQVhfQ09OQ1VSUkVOQ1k9JHtOOE5fUlVOTkVSU19NQVhfQ09OQ1VSUkVOQ1k6LTV9JwogICAgICAtICdOOE5fQkxPQ0tfRU5WX0FDQ0VTU19JTl9OT0RFPSR7TjhOX0JMT0NLX0VOVl9BQ0NFU1NfSU5fTk9ERTotdHJ1ZX0nCiAgICAgIC0gJ044Tl9HSVRfTk9ERV9ESVNBQkxFX0JBUkVfUkVQT1M9JHtOOE5fR0lUX05PREVfRElTQUJMRV9CQVJFX1JFUE9TOi10cnVlfScKICAgICAgLSAnTjhOX0VORk9SQ0VfU0VUVElOR1NfRklMRV9QRVJNSVNTSU9OUz0ke044Tl9FTkZPUkNFX1NFVFRJTkdTX0ZJTEVfUEVSTUlTU0lPTlM6LXRydWV9JwogICAgICAtICdOOE5fUFJPWFlfSE9QUz0ke044Tl9QUk9YWV9IT1BTOi0xfScKICAgICAgLSAnTjhOX1NLSVBfQVVUSF9PTl9PQVVUSF9DQUxMQkFDSz0ke044Tl9TS0lQX0FVVEhfT05fT0FVVEhfQ0FMTEJBQ0s6LWZhbHNlfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ244bi1kYXRhOi9ob21lL25vZGUvLm44bicKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzcWw6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAnd2dldCAtcU8tIGh0dHA6Ly8xMjcuMC4wLjE6NTY3OC8nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICB0YXNrLXJ1bm5lcnM6CiAgICBpbWFnZTogJ244bmlvL3J1bm5lcnM6Mi4xMC4yJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ044Tl9SVU5ORVJTX1RBU0tfQlJPS0VSX1VSST0ke044Tl9SVU5ORVJTX1RBU0tfQlJPS0VSX1VSSTotaHR0cDovL244bjo1Njc5fScKICAgICAgLSBOOE5fUlVOTkVSU19BVVRIX1RPS0VOPSRTRVJWSUNFX1BBU1NXT1JEX044TgogICAgICAtICdOOE5fUlVOTkVSU19BVVRPX1NIVVRET1dOX1RJTUVPVVQ9JHtOOE5fUlVOTkVSU19BVVRPX1NIVVRET1dOX1RJTUVPVVQ6LTE1fScKICAgICAgLSAnTjhOX1JVTk5FUlNfTUFYX0NPTkNVUlJFTkNZPSR7TjhOX1JVTk5FUlNfTUFYX0NPTkNVUlJFTkNZOi01fScKICAgIGRlcGVuZHNfb246CiAgICAgIC0gbjhuCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3dnZXQgLXFPLSBodHRwOi8vMTI3LjAuMC4xOjU2ODAvJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgcG9zdGdyZXNxbDoKICAgIGltYWdlOiAncG9zdGdyZXM6MTYtYWxwaW5lJwogICAgdm9sdW1lczoKICAgICAgLSAncG9zdGdyZXNxbC1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGVudmlyb25tZW50OgogICAgICAtIFBPU1RHUkVTX1VTRVI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgICAtIFBPU1RHUkVTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNfREI6LW44bn0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==", "tags": [ "n8n", "workflow", @@ -2923,7 +2923,7 @@ "n8n": { "documentation": "https://n8n.io?utm_source=coolify.io", "slogan": "n8n is an extendable workflow automation tool.", - "compose": "c2VydmljZXM6CiAgbjhuOgogICAgaW1hZ2U6ICduOG5pby9uOG46Mi4xLjUnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9OOE5fNTY3OAogICAgICAtICdOOE5fRURJVE9SX0JBU0VfVVJMPSR7U0VSVklDRV9VUkxfTjhOfScKICAgICAgLSAnV0VCSE9PS19VUkw9JHtTRVJWSUNFX1VSTF9OOE59JwogICAgICAtICdOOE5fSE9TVD0ke1NFUlZJQ0VfRlFETl9OOE59JwogICAgICAtICdOOE5fUFJPVE9DT0w9JHtOOE5fUFJPVE9DT0w6LWh0dHBzfScKICAgICAgLSAnR0VORVJJQ19USU1FWk9ORT0ke0dFTkVSSUNfVElNRVpPTkU6LVVUQ30nCiAgICAgIC0gJ1RaPSR7VFo6LVVUQ30nCiAgICAgIC0gJ0RCX1NRTElURV9QT09MX1NJWkU9JHtEQl9TUUxJVEVfUE9PTF9TSVpFOi0yfScKICAgICAgLSBOOE5fUlVOTkVSU19FTkFCTEVEPXRydWUKICAgICAgLSBOOE5fUlVOTkVSU19NT0RFPWV4dGVybmFsCiAgICAgIC0gJ044Tl9SVU5ORVJTX0JST0tFUl9MSVNURU5fQUREUkVTUz0ke044Tl9SVU5ORVJTX0JST0tFUl9MSVNURU5fQUREUkVTUzotMC4wLjAuMH0nCiAgICAgIC0gJ044Tl9SVU5ORVJTX0JST0tFUl9QT1JUPSR7TjhOX1JVTk5FUlNfQlJPS0VSX1BPUlQ6LTU2Nzl9JwogICAgICAtICdOOE5fUlVOTkVSU19BVVRIX1RPS0VOPSR7U0VSVklDRV9QQVNTV09SRF9OOE59JwogICAgICAtICdOOE5fTkFUSVZFX1BZVEhPTl9SVU5ORVI9JHtOOE5fTkFUSVZFX1BZVEhPTl9SVU5ORVI6LXRydWV9JwogICAgICAtICdOOE5fUlVOTkVSU19NQVhfQ09OQ1VSUkVOQ1k9JHtOOE5fUlVOTkVSU19NQVhfQ09OQ1VSUkVOQ1k6LTV9JwogICAgICAtICdOOE5fQkxPQ0tfRU5WX0FDQ0VTU19JTl9OT0RFPSR7TjhOX0JMT0NLX0VOVl9BQ0NFU1NfSU5fTk9ERTotdHJ1ZX0nCiAgICAgIC0gJ044Tl9HSVRfTk9ERV9ESVNBQkxFX0JBUkVfUkVQT1M9JHtOOE5fR0lUX05PREVfRElTQUJMRV9CQVJFX1JFUE9TOi10cnVlfScKICAgICAgLSAnTjhOX0VORk9SQ0VfU0VUVElOR1NfRklMRV9QRVJNSVNTSU9OUz0ke044Tl9FTkZPUkNFX1NFVFRJTkdTX0ZJTEVfUEVSTUlTU0lPTlM6LXRydWV9JwogICAgICAtICdOOE5fUFJPWFlfSE9QUz0ke044Tl9QUk9YWV9IT1BTOi0xfScKICAgICAgLSAnTjhOX1NLSVBfQVVUSF9PTl9PQVVUSF9DQUxMQkFDSz0ke044Tl9TS0lQX0FVVEhfT05fT0FVVEhfQ0FMTEJBQ0s6LWZhbHNlfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ244bi1kYXRhOi9ob21lL25vZGUvLm44bicKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAnd2dldCAtcU8tIGh0dHA6Ly8xMjcuMC4wLjE6NTY3OC8nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICB0YXNrLXJ1bm5lcnM6CiAgICBpbWFnZTogJ244bmlvL3J1bm5lcnM6Mi4xLjUnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnTjhOX1JVTk5FUlNfVEFTS19CUk9LRVJfVVJJPSR7TjhOX1JVTk5FUlNfVEFTS19CUk9LRVJfVVJJOi1odHRwOi8vbjhuOjU2Nzl9JwogICAgICAtICdOOE5fUlVOTkVSU19BVVRIX1RPS0VOPSR7U0VSVklDRV9QQVNTV09SRF9OOE59JwogICAgICAtICdOOE5fUlVOTkVSU19BVVRPX1NIVVRET1dOX1RJTUVPVVQ9JHtOOE5fUlVOTkVSU19BVVRPX1NIVVRET1dOX1RJTUVPVVQ6LTE1fScKICAgICAgLSAnTjhOX1JVTk5FUlNfTUFYX0NPTkNVUlJFTkNZPSR7TjhOX1JVTk5FUlNfTUFYX0NPTkNVUlJFTkNZOi01fScKICAgIGRlcGVuZHNfb246CiAgICAgIC0gbjhuCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3dnZXQgLXFPLSBodHRwOi8vMTI3LjAuMC4xOjU2ODAvJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==", + "compose": "c2VydmljZXM6CiAgbjhuOgogICAgaW1hZ2U6ICduOG5pby9uOG46Mi4xMC4yJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9VUkxfTjhOXzU2NzgKICAgICAgLSAnTjhOX0VESVRPUl9CQVNFX1VSTD0ke1NFUlZJQ0VfVVJMX044Tn0nCiAgICAgIC0gJ1dFQkhPT0tfVVJMPSR7U0VSVklDRV9VUkxfTjhOfScKICAgICAgLSAnTjhOX0hPU1Q9JHtTRVJWSUNFX0ZRRE5fTjhOfScKICAgICAgLSAnTjhOX1BST1RPQ09MPSR7TjhOX1BST1RPQ09MOi1odHRwc30nCiAgICAgIC0gJ0dFTkVSSUNfVElNRVpPTkU9JHtHRU5FUklDX1RJTUVaT05FOi1VVEN9JwogICAgICAtICdUWj0ke1RaOi1VVEN9JwogICAgICAtICdEQl9TUUxJVEVfUE9PTF9TSVpFPSR7REJfU1FMSVRFX1BPT0xfU0laRTotMn0nCiAgICAgIC0gTjhOX1JVTk5FUlNfRU5BQkxFRD10cnVlCiAgICAgIC0gTjhOX1JVTk5FUlNfTU9ERT1leHRlcm5hbAogICAgICAtICdOOE5fUlVOTkVSU19CUk9LRVJfTElTVEVOX0FERFJFU1M9JHtOOE5fUlVOTkVSU19CUk9LRVJfTElTVEVOX0FERFJFU1M6LTAuMC4wLjB9JwogICAgICAtICdOOE5fUlVOTkVSU19CUk9LRVJfUE9SVD0ke044Tl9SVU5ORVJTX0JST0tFUl9QT1JUOi01Njc5fScKICAgICAgLSAnTjhOX1JVTk5FUlNfQVVUSF9UT0tFTj0ke1NFUlZJQ0VfUEFTU1dPUkRfTjhOfScKICAgICAgLSAnTjhOX05BVElWRV9QWVRIT05fUlVOTkVSPSR7TjhOX05BVElWRV9QWVRIT05fUlVOTkVSOi10cnVlfScKICAgICAgLSAnTjhOX1JVTk5FUlNfTUFYX0NPTkNVUlJFTkNZPSR7TjhOX1JVTk5FUlNfTUFYX0NPTkNVUlJFTkNZOi01fScKICAgICAgLSAnTjhOX0JMT0NLX0VOVl9BQ0NFU1NfSU5fTk9ERT0ke044Tl9CTE9DS19FTlZfQUNDRVNTX0lOX05PREU6LXRydWV9JwogICAgICAtICdOOE5fR0lUX05PREVfRElTQUJMRV9CQVJFX1JFUE9TPSR7TjhOX0dJVF9OT0RFX0RJU0FCTEVfQkFSRV9SRVBPUzotdHJ1ZX0nCiAgICAgIC0gJ044Tl9FTkZPUkNFX1NFVFRJTkdTX0ZJTEVfUEVSTUlTU0lPTlM9JHtOOE5fRU5GT1JDRV9TRVRUSU5HU19GSUxFX1BFUk1JU1NJT05TOi10cnVlfScKICAgICAgLSAnTjhOX1BST1hZX0hPUFM9JHtOOE5fUFJPWFlfSE9QUzotMX0nCiAgICAgIC0gJ044Tl9TS0lQX0FVVEhfT05fT0FVVEhfQ0FMTEJBQ0s9JHtOOE5fU0tJUF9BVVRIX09OX09BVVRIX0NBTExCQUNLOi1mYWxzZX0nCiAgICB2b2x1bWVzOgogICAgICAtICduOG4tZGF0YTovaG9tZS9ub2RlLy5uOG4nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3dnZXQgLXFPLSBodHRwOi8vMTI3LjAuMC4xOjU2NzgvJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgdGFzay1ydW5uZXJzOgogICAgaW1hZ2U6ICduOG5pby9ydW5uZXJzOjIuMTAuMicKICAgIGVudmlyb25tZW50OgogICAgICAtICdOOE5fUlVOTkVSU19UQVNLX0JST0tFUl9VUkk9JHtOOE5fUlVOTkVSU19UQVNLX0JST0tFUl9VUkk6LWh0dHA6Ly9uOG46NTY3OX0nCiAgICAgIC0gJ044Tl9SVU5ORVJTX0FVVEhfVE9LRU49JHtTRVJWSUNFX1BBU1NXT1JEX044Tn0nCiAgICAgIC0gJ044Tl9SVU5ORVJTX0FVVE9fU0hVVERPV05fVElNRU9VVD0ke044Tl9SVU5ORVJTX0FVVE9fU0hVVERPV05fVElNRU9VVDotMTV9JwogICAgICAtICdOOE5fUlVOTkVSU19NQVhfQ09OQ1VSUkVOQ1k9JHtOOE5fUlVOTkVSU19NQVhfQ09OQ1VSUkVOQ1k6LTV9JwogICAgZGVwZW5kc19vbjoKICAgICAgLSBuOG4KICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAnd2dldCAtcU8tIGh0dHA6Ly8xMjcuMC4wLjE6NTY4MC8nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK", "tags": [ "n8n", "workflow", diff --git a/templates/service-templates.json b/templates/service-templates.json index 58f990de6..5307b2259 100644 --- a/templates/service-templates.json +++ b/templates/service-templates.json @@ -2884,7 +2884,7 @@ "n8n-with-postgres-and-worker": { "documentation": "https://n8n.io?utm_source=coolify.io", "slogan": "n8n is an extendable workflow automation tool with queue mode and workers.", - "compose": "c2VydmljZXM6CiAgbjhuOgogICAgaW1hZ2U6ICduOG5pby9uOG46Mi4xLjUnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fTjhOXzU2NzgKICAgICAgLSAnTjhOX0VESVRPUl9CQVNFX1VSTD0ke1NFUlZJQ0VfRlFETl9OOE59JwogICAgICAtICdXRUJIT09LX1VSTD0ke1NFUlZJQ0VfRlFETl9OOE59JwogICAgICAtICdOOE5fSE9TVD0ke1NFUlZJQ0VfRlFETl9OOE59JwogICAgICAtICdOOE5fUFJPVE9DT0w9JHtOOE5fUFJPVE9DT0w6LWh0dHBzfScKICAgICAgLSAnR0VORVJJQ19USU1FWk9ORT0ke0dFTkVSSUNfVElNRVpPTkU6LVVUQ30nCiAgICAgIC0gJ1RaPSR7VFo6LVVUQ30nCiAgICAgIC0gREJfVFlQRT1wb3N0Z3Jlc2RiCiAgICAgIC0gJ0RCX1BPU1RHUkVTREJfREFUQUJBU0U9JHtQT1NUR1JFU19EQjotbjhufScKICAgICAgLSBEQl9QT1NUR1JFU0RCX0hPU1Q9cG9zdGdyZXNxbAogICAgICAtIERCX1BPU1RHUkVTREJfUE9SVD01NDMyCiAgICAgIC0gREJfUE9TVEdSRVNEQl9VU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBEQl9QT1NUR1JFU0RCX1NDSEVNQT1wdWJsaWMKICAgICAgLSBEQl9QT1NUR1JFU0RCX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gRVhFQ1VUSU9OU19NT0RFPXF1ZXVlCiAgICAgIC0gUVVFVUVfQlVMTF9SRURJU19IT1NUPXJlZGlzCiAgICAgIC0gUVVFVUVfSEVBTFRIX0NIRUNLX0FDVElWRT10cnVlCiAgICAgIC0gJ044Tl9FTkNSWVBUSU9OX0tFWT0ke1NFUlZJQ0VfUEFTU1dPUkRfRU5DUllQVElPTn0nCiAgICAgIC0gTjhOX1JVTk5FUlNfRU5BQkxFRD10cnVlCiAgICAgIC0gTjhOX1JVTk5FUlNfTU9ERT1leHRlcm5hbAogICAgICAtICdOOE5fUlVOTkVSU19CUk9LRVJfTElTVEVOX0FERFJFU1M9JHtOOE5fUlVOTkVSU19CUk9LRVJfTElTVEVOX0FERFJFU1M6LTAuMC4wLjB9JwogICAgICAtICdOOE5fUlVOTkVSU19CUk9LRVJfUE9SVD0ke044Tl9SVU5ORVJTX0JST0tFUl9QT1JUOi01Njc5fScKICAgICAgLSBOOE5fUlVOTkVSU19BVVRIX1RPS0VOPSRTRVJWSUNFX1BBU1NXT1JEX044TgogICAgICAtICdOOE5fTkFUSVZFX1BZVEhPTl9SVU5ORVI9JHtOOE5fTkFUSVZFX1BZVEhPTl9SVU5ORVI6LXRydWV9JwogICAgICAtICdOOE5fUlVOTkVSU19NQVhfQ09OQ1VSUkVOQ1k9JHtOOE5fUlVOTkVSU19NQVhfQ09OQ1VSUkVOQ1k6LTV9JwogICAgICAtIE9GRkxPQURfTUFOVUFMX0VYRUNVVElPTlNfVE9fV09SS0VSUz10cnVlCiAgICAgIC0gJ044Tl9CTE9DS19FTlZfQUNDRVNTX0lOX05PREU9JHtOOE5fQkxPQ0tfRU5WX0FDQ0VTU19JTl9OT0RFOi10cnVlfScKICAgICAgLSAnTjhOX0dJVF9OT0RFX0RJU0FCTEVfQkFSRV9SRVBPUz0ke044Tl9HSVRfTk9ERV9ESVNBQkxFX0JBUkVfUkVQT1M6LXRydWV9JwogICAgICAtICdOOE5fRU5GT1JDRV9TRVRUSU5HU19GSUxFX1BFUk1JU1NJT05TPSR7TjhOX0VORk9SQ0VfU0VUVElOR1NfRklMRV9QRVJNSVNTSU9OUzotdHJ1ZX0nCiAgICAgIC0gJ044Tl9QUk9YWV9IT1BTPSR7TjhOX1BST1hZX0hPUFM6LTF9JwogICAgICAtICdOOE5fU0tJUF9BVVRIX09OX09BVVRIX0NBTExCQUNLPSR7TjhOX1NLSVBfQVVUSF9PTl9PQVVUSF9DQUxMQkFDSzotZmFsc2V9JwogICAgdm9sdW1lczoKICAgICAgLSAnbjhuLWRhdGE6L2hvbWUvbm9kZS8ubjhuJwogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXNxbDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICByZWRpczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICd3Z2V0IC1xTy0gaHR0cDovLzEyNy4wLjAuMTo1Njc4LycKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIG44bi13b3JrZXI6CiAgICBpbWFnZTogJ244bmlvL244bjoyLjEuNScKICAgIGNvbW1hbmQ6IHdvcmtlcgogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0dFTkVSSUNfVElNRVpPTkU9JHtHRU5FUklDX1RJTUVaT05FOi1VVEN9JwogICAgICAtICdUWj0ke1RaOi1VVEN9JwogICAgICAtIERCX1RZUEU9cG9zdGdyZXNkYgogICAgICAtICdEQl9QT1NUR1JFU0RCX0RBVEFCQVNFPSR7UE9TVEdSRVNfREI6LW44bn0nCiAgICAgIC0gREJfUE9TVEdSRVNEQl9IT1NUPXBvc3RncmVzcWwKICAgICAgLSBEQl9QT1NUR1JFU0RCX1BPUlQ9NTQzMgogICAgICAtIERCX1BPU1RHUkVTREJfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gREJfUE9TVEdSRVNEQl9TQ0hFTUE9cHVibGljCiAgICAgIC0gREJfUE9TVEdSRVNEQl9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtIEVYRUNVVElPTlNfTU9ERT1xdWV1ZQogICAgICAtIFFVRVVFX0JVTExfUkVESVNfSE9TVD1yZWRpcwogICAgICAtIFFVRVVFX0hFQUxUSF9DSEVDS19BQ1RJVkU9dHJ1ZQogICAgICAtICdOOE5fRU5DUllQVElPTl9LRVk9JHtTRVJWSUNFX1BBU1NXT1JEX0VOQ1JZUFRJT059JwogICAgICAtIE44Tl9SVU5ORVJTX0VOQUJMRUQ9dHJ1ZQogICAgICAtIE44Tl9SVU5ORVJTX01PREU9ZXh0ZXJuYWwKICAgICAgLSAnTjhOX1JVTk5FUlNfQlJPS0VSX0xJU1RFTl9BRERSRVNTPSR7TjhOX1JVTk5FUlNfQlJPS0VSX0xJU1RFTl9BRERSRVNTOi0wLjAuMC4wfScKICAgICAgLSAnTjhOX1JVTk5FUlNfQlJPS0VSX1BPUlQ9JHtOOE5fUlVOTkVSU19CUk9LRVJfUE9SVDotNTY3OX0nCiAgICAgIC0gTjhOX1JVTk5FUlNfQVVUSF9UT0tFTj0kU0VSVklDRV9QQVNTV09SRF9OOE4KICAgICAgLSAnTjhOX05BVElWRV9QWVRIT05fUlVOTkVSPSR7TjhOX05BVElWRV9QWVRIT05fUlVOTkVSOi10cnVlfScKICAgICAgLSAnTjhOX1JVTk5FUlNfTUFYX0NPTkNVUlJFTkNZPSR7TjhOX1JVTk5FUlNfTUFYX0NPTkNVUlJFTkNZOi01fScKICAgICAgLSAnTjhOX0JMT0NLX0VOVl9BQ0NFU1NfSU5fTk9ERT0ke044Tl9CTE9DS19FTlZfQUNDRVNTX0lOX05PREU6LXRydWV9JwogICAgICAtICdOOE5fR0lUX05PREVfRElTQUJMRV9CQVJFX1JFUE9TPSR7TjhOX0dJVF9OT0RFX0RJU0FCTEVfQkFSRV9SRVBPUzotdHJ1ZX0nCiAgICAgIC0gJ044Tl9FTkZPUkNFX1NFVFRJTkdTX0ZJTEVfUEVSTUlTU0lPTlM9JHtOOE5fRU5GT1JDRV9TRVRUSU5HU19GSUxFX1BFUk1JU1NJT05TOi10cnVlfScKICAgICAgLSAnTjhOX1BST1hZX0hPUFM9JHtOOE5fUFJPWFlfSE9QUzotMX0nCiAgICAgIC0gJ044Tl9TS0lQX0FVVEhfT05fT0FVVEhfQ0FMTEJBQ0s9JHtOOE5fU0tJUF9BVVRIX09OX09BVVRIX0NBTExCQUNLOi1mYWxzZX0nCiAgICB2b2x1bWVzOgogICAgICAtICduOG4tZGF0YTovaG9tZS9ub2RlLy5uOG4nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3dnZXQgLXFPLSBodHRwOi8vMTI3LjAuMC4xOjU2NzgvaGVhbHRoeicKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogICAgZGVwZW5kc19vbjoKICAgICAgbjhuOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHBvc3RncmVzcWw6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcmVkaXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICBwb3N0Z3Jlc3FsOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNi1hbHBpbmUnCiAgICB2b2x1bWVzOgogICAgICAtICdwb3N0Z3Jlc3FsLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUE9TVEdSRVNfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotbjhufScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICByZWRpczoKICAgIGltYWdlOiAncmVkaXM6Ni1hbHBpbmUnCiAgICB2b2x1bWVzOgogICAgICAtICdyZWRpcy1kYXRhOi9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHJlZGlzLWNsaQogICAgICAgIC0gcGluZwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogMTAKICB0YXNrLXJ1bm5lcnM6CiAgICBpbWFnZTogJ244bmlvL3J1bm5lcnM6Mi4xLjUnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnTjhOX1JVTk5FUlNfVEFTS19CUk9LRVJfVVJJPSR7TjhOX1JVTk5FUlNfVEFTS19CUk9LRVJfVVJJOi1odHRwOi8vbjhuLXdvcmtlcjo1Njc5fScKICAgICAgLSBOOE5fUlVOTkVSU19BVVRIX1RPS0VOPSRTRVJWSUNFX1BBU1NXT1JEX044TgogICAgICAtICdOOE5fUlVOTkVSU19BVVRPX1NIVVRET1dOX1RJTUVPVVQ9JHtOOE5fUlVOTkVSU19BVVRPX1NIVVRET1dOX1RJTUVPVVQ6LTE1fScKICAgICAgLSAnTjhOX1JVTk5FUlNfTUFYX0NPTkNVUlJFTkNZPSR7TjhOX1JVTk5FUlNfTUFYX0NPTkNVUlJFTkNZOi01fScKICAgIGRlcGVuZHNfb246CiAgICAgIC0gbjhuCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3dnZXQgLXFPLSBodHRwOi8vMTI3LjAuMC4xOjU2ODAvJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==", + "compose": "c2VydmljZXM6CiAgbjhuOgogICAgaW1hZ2U6ICduOG5pby9uOG46Mi4xMC4yJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX044Tl81Njc4CiAgICAgIC0gJ044Tl9FRElUT1JfQkFTRV9VUkw9JHtTRVJWSUNFX0ZRRE5fTjhOfScKICAgICAgLSAnV0VCSE9PS19VUkw9JHtTRVJWSUNFX0ZRRE5fTjhOfScKICAgICAgLSAnTjhOX0hPU1Q9JHtTRVJWSUNFX0ZRRE5fTjhOfScKICAgICAgLSAnTjhOX1BST1RPQ09MPSR7TjhOX1BST1RPQ09MOi1odHRwc30nCiAgICAgIC0gJ0dFTkVSSUNfVElNRVpPTkU9JHtHRU5FUklDX1RJTUVaT05FOi1VVEN9JwogICAgICAtICdUWj0ke1RaOi1VVEN9JwogICAgICAtIERCX1RZUEU9cG9zdGdyZXNkYgogICAgICAtICdEQl9QT1NUR1JFU0RCX0RBVEFCQVNFPSR7UE9TVEdSRVNfREI6LW44bn0nCiAgICAgIC0gREJfUE9TVEdSRVNEQl9IT1NUPXBvc3RncmVzcWwKICAgICAgLSBEQl9QT1NUR1JFU0RCX1BPUlQ9NTQzMgogICAgICAtIERCX1BPU1RHUkVTREJfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gREJfUE9TVEdSRVNEQl9TQ0hFTUE9cHVibGljCiAgICAgIC0gREJfUE9TVEdSRVNEQl9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtIEVYRUNVVElPTlNfTU9ERT1xdWV1ZQogICAgICAtIFFVRVVFX0JVTExfUkVESVNfSE9TVD1yZWRpcwogICAgICAtIFFVRVVFX0hFQUxUSF9DSEVDS19BQ1RJVkU9dHJ1ZQogICAgICAtICdOOE5fRU5DUllQVElPTl9LRVk9JHtTRVJWSUNFX1BBU1NXT1JEX0VOQ1JZUFRJT059JwogICAgICAtIE44Tl9SVU5ORVJTX0VOQUJMRUQ9dHJ1ZQogICAgICAtIE44Tl9SVU5ORVJTX01PREU9ZXh0ZXJuYWwKICAgICAgLSAnTjhOX1JVTk5FUlNfQlJPS0VSX0xJU1RFTl9BRERSRVNTPSR7TjhOX1JVTk5FUlNfQlJPS0VSX0xJU1RFTl9BRERSRVNTOi0wLjAuMC4wfScKICAgICAgLSAnTjhOX1JVTk5FUlNfQlJPS0VSX1BPUlQ9JHtOOE5fUlVOTkVSU19CUk9LRVJfUE9SVDotNTY3OX0nCiAgICAgIC0gTjhOX1JVTk5FUlNfQVVUSF9UT0tFTj0kU0VSVklDRV9QQVNTV09SRF9OOE4KICAgICAgLSAnTjhOX05BVElWRV9QWVRIT05fUlVOTkVSPSR7TjhOX05BVElWRV9QWVRIT05fUlVOTkVSOi10cnVlfScKICAgICAgLSAnTjhOX1JVTk5FUlNfTUFYX0NPTkNVUlJFTkNZPSR7TjhOX1JVTk5FUlNfTUFYX0NPTkNVUlJFTkNZOi01fScKICAgICAgLSBPRkZMT0FEX01BTlVBTF9FWEVDVVRJT05TX1RPX1dPUktFUlM9dHJ1ZQogICAgICAtICdOOE5fQkxPQ0tfRU5WX0FDQ0VTU19JTl9OT0RFPSR7TjhOX0JMT0NLX0VOVl9BQ0NFU1NfSU5fTk9ERTotdHJ1ZX0nCiAgICAgIC0gJ044Tl9HSVRfTk9ERV9ESVNBQkxFX0JBUkVfUkVQT1M9JHtOOE5fR0lUX05PREVfRElTQUJMRV9CQVJFX1JFUE9TOi10cnVlfScKICAgICAgLSAnTjhOX0VORk9SQ0VfU0VUVElOR1NfRklMRV9QRVJNSVNTSU9OUz0ke044Tl9FTkZPUkNFX1NFVFRJTkdTX0ZJTEVfUEVSTUlTU0lPTlM6LXRydWV9JwogICAgICAtICdOOE5fUFJPWFlfSE9QUz0ke044Tl9QUk9YWV9IT1BTOi0xfScKICAgICAgLSAnTjhOX1NLSVBfQVVUSF9PTl9PQVVUSF9DQUxMQkFDSz0ke044Tl9TS0lQX0FVVEhfT05fT0FVVEhfQ0FMTEJBQ0s6LWZhbHNlfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ244bi1kYXRhOi9ob21lL25vZGUvLm44bicKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzcWw6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcmVkaXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAnd2dldCAtcU8tIGh0dHA6Ly8xMjcuMC4wLjE6NTY3OC8nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICBuOG4td29ya2VyOgogICAgaW1hZ2U6ICduOG5pby9uOG46Mi4xMC4yJwogICAgY29tbWFuZDogd29ya2VyCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnR0VORVJJQ19USU1FWk9ORT0ke0dFTkVSSUNfVElNRVpPTkU6LVVUQ30nCiAgICAgIC0gJ1RaPSR7VFo6LVVUQ30nCiAgICAgIC0gREJfVFlQRT1wb3N0Z3Jlc2RiCiAgICAgIC0gJ0RCX1BPU1RHUkVTREJfREFUQUJBU0U9JHtQT1NUR1JFU19EQjotbjhufScKICAgICAgLSBEQl9QT1NUR1JFU0RCX0hPU1Q9cG9zdGdyZXNxbAogICAgICAtIERCX1BPU1RHUkVTREJfUE9SVD01NDMyCiAgICAgIC0gREJfUE9TVEdSRVNEQl9VU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBEQl9QT1NUR1JFU0RCX1NDSEVNQT1wdWJsaWMKICAgICAgLSBEQl9QT1NUR1JFU0RCX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gRVhFQ1VUSU9OU19NT0RFPXF1ZXVlCiAgICAgIC0gUVVFVUVfQlVMTF9SRURJU19IT1NUPXJlZGlzCiAgICAgIC0gUVVFVUVfSEVBTFRIX0NIRUNLX0FDVElWRT10cnVlCiAgICAgIC0gJ044Tl9FTkNSWVBUSU9OX0tFWT0ke1NFUlZJQ0VfUEFTU1dPUkRfRU5DUllQVElPTn0nCiAgICAgIC0gTjhOX1JVTk5FUlNfRU5BQkxFRD10cnVlCiAgICAgIC0gTjhOX1JVTk5FUlNfTU9ERT1leHRlcm5hbAogICAgICAtICdOOE5fUlVOTkVSU19CUk9LRVJfTElTVEVOX0FERFJFU1M9JHtOOE5fUlVOTkVSU19CUk9LRVJfTElTVEVOX0FERFJFU1M6LTAuMC4wLjB9JwogICAgICAtICdOOE5fUlVOTkVSU19CUk9LRVJfUE9SVD0ke044Tl9SVU5ORVJTX0JST0tFUl9QT1JUOi01Njc5fScKICAgICAgLSBOOE5fUlVOTkVSU19BVVRIX1RPS0VOPSRTRVJWSUNFX1BBU1NXT1JEX044TgogICAgICAtICdOOE5fTkFUSVZFX1BZVEhPTl9SVU5ORVI9JHtOOE5fTkFUSVZFX1BZVEhPTl9SVU5ORVI6LXRydWV9JwogICAgICAtICdOOE5fUlVOTkVSU19NQVhfQ09OQ1VSUkVOQ1k9JHtOOE5fUlVOTkVSU19NQVhfQ09OQ1VSUkVOQ1k6LTV9JwogICAgICAtICdOOE5fQkxPQ0tfRU5WX0FDQ0VTU19JTl9OT0RFPSR7TjhOX0JMT0NLX0VOVl9BQ0NFU1NfSU5fTk9ERTotdHJ1ZX0nCiAgICAgIC0gJ044Tl9HSVRfTk9ERV9ESVNBQkxFX0JBUkVfUkVQT1M9JHtOOE5fR0lUX05PREVfRElTQUJMRV9CQVJFX1JFUE9TOi10cnVlfScKICAgICAgLSAnTjhOX0VORk9SQ0VfU0VUVElOR1NfRklMRV9QRVJNSVNTSU9OUz0ke044Tl9FTkZPUkNFX1NFVFRJTkdTX0ZJTEVfUEVSTUlTU0lPTlM6LXRydWV9JwogICAgICAtICdOOE5fUFJPWFlfSE9QUz0ke044Tl9QUk9YWV9IT1BTOi0xfScKICAgICAgLSAnTjhOX1NLSVBfQVVUSF9PTl9PQVVUSF9DQUxMQkFDSz0ke044Tl9TS0lQX0FVVEhfT05fT0FVVEhfQ0FMTEJBQ0s6LWZhbHNlfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ244bi1kYXRhOi9ob21lL25vZGUvLm44bicKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAnd2dldCAtcU8tIGh0dHA6Ly8xMjcuMC4wLjE6NTY3OC9oZWFsdGh6JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgICBkZXBlbmRzX29uOgogICAgICBuOG46CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcG9zdGdyZXNxbDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICByZWRpczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogIHBvc3RncmVzcWw6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3Bvc3RncmVzcWwtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1NUR1JFU19VU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi1uOG59JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHJlZGlzOgogICAgaW1hZ2U6ICdyZWRpczo2LWFscGluZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3JlZGlzLWRhdGE6L2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gcmVkaXMtY2xpCiAgICAgICAgLSBwaW5nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiA1cwogICAgICByZXRyaWVzOiAxMAogIHRhc2stcnVubmVyczoKICAgIGltYWdlOiAnbjhuaW8vcnVubmVyczoyLjEwLjInCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnTjhOX1JVTk5FUlNfVEFTS19CUk9LRVJfVVJJPSR7TjhOX1JVTk5FUlNfVEFTS19CUk9LRVJfVVJJOi1odHRwOi8vbjhuLXdvcmtlcjo1Njc5fScKICAgICAgLSBOOE5fUlVOTkVSU19BVVRIX1RPS0VOPSRTRVJWSUNFX1BBU1NXT1JEX044TgogICAgICAtICdOOE5fUlVOTkVSU19BVVRPX1NIVVRET1dOX1RJTUVPVVQ9JHtOOE5fUlVOTkVSU19BVVRPX1NIVVRET1dOX1RJTUVPVVQ6LTE1fScKICAgICAgLSAnTjhOX1JVTk5FUlNfTUFYX0NPTkNVUlJFTkNZPSR7TjhOX1JVTk5FUlNfTUFYX0NPTkNVUlJFTkNZOi01fScKICAgIGRlcGVuZHNfb246CiAgICAgIC0gbjhuCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3dnZXQgLXFPLSBodHRwOi8vMTI3LjAuMC4xOjU2ODAvJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==", "tags": [ "n8n", "workflow", @@ -2905,7 +2905,7 @@ "n8n-with-postgresql": { "documentation": "https://n8n.io?utm_source=coolify.io", "slogan": "n8n is an extendable workflow automation tool.", - "compose": "c2VydmljZXM6CiAgbjhuOgogICAgaW1hZ2U6ICduOG5pby9uOG46Mi4xLjUnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fTjhOXzU2NzgKICAgICAgLSAnTjhOX0VESVRPUl9CQVNFX1VSTD0ke1NFUlZJQ0VfRlFETl9OOE59JwogICAgICAtICdXRUJIT09LX1VSTD0ke1NFUlZJQ0VfRlFETl9OOE59JwogICAgICAtICdOOE5fSE9TVD0ke1NFUlZJQ0VfRlFETl9OOE59JwogICAgICAtICdOOE5fUFJPVE9DT0w9JHtOOE5fUFJPVE9DT0w6LWh0dHBzfScKICAgICAgLSAnR0VORVJJQ19USU1FWk9ORT0ke0dFTkVSSUNfVElNRVpPTkU6LVVUQ30nCiAgICAgIC0gJ1RaPSR7VFo6LVVUQ30nCiAgICAgIC0gREJfVFlQRT1wb3N0Z3Jlc2RiCiAgICAgIC0gJ0RCX1BPU1RHUkVTREJfREFUQUJBU0U9JHtQT1NUR1JFU19EQjotbjhufScKICAgICAgLSBEQl9QT1NUR1JFU0RCX0hPU1Q9cG9zdGdyZXNxbAogICAgICAtIERCX1BPU1RHUkVTREJfUE9SVD01NDMyCiAgICAgIC0gREJfUE9TVEdSRVNEQl9VU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBEQl9QT1NUR1JFU0RCX1NDSEVNQT1wdWJsaWMKICAgICAgLSBEQl9QT1NUR1JFU0RCX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gTjhOX1JVTk5FUlNfRU5BQkxFRD10cnVlCiAgICAgIC0gTjhOX1JVTk5FUlNfTU9ERT1leHRlcm5hbAogICAgICAtICdOOE5fUlVOTkVSU19CUk9LRVJfTElTVEVOX0FERFJFU1M9JHtOOE5fUlVOTkVSU19CUk9LRVJfTElTVEVOX0FERFJFU1M6LTAuMC4wLjB9JwogICAgICAtICdOOE5fUlVOTkVSU19CUk9LRVJfUE9SVD0ke044Tl9SVU5ORVJTX0JST0tFUl9QT1JUOi01Njc5fScKICAgICAgLSBOOE5fUlVOTkVSU19BVVRIX1RPS0VOPSRTRVJWSUNFX1BBU1NXT1JEX044TgogICAgICAtICdOOE5fTkFUSVZFX1BZVEhPTl9SVU5ORVI9JHtOOE5fTkFUSVZFX1BZVEhPTl9SVU5ORVI6LXRydWV9JwogICAgICAtICdOOE5fUlVOTkVSU19NQVhfQ09OQ1VSUkVOQ1k9JHtOOE5fUlVOTkVSU19NQVhfQ09OQ1VSUkVOQ1k6LTV9JwogICAgICAtICdOOE5fQkxPQ0tfRU5WX0FDQ0VTU19JTl9OT0RFPSR7TjhOX0JMT0NLX0VOVl9BQ0NFU1NfSU5fTk9ERTotdHJ1ZX0nCiAgICAgIC0gJ044Tl9HSVRfTk9ERV9ESVNBQkxFX0JBUkVfUkVQT1M9JHtOOE5fR0lUX05PREVfRElTQUJMRV9CQVJFX1JFUE9TOi10cnVlfScKICAgICAgLSAnTjhOX0VORk9SQ0VfU0VUVElOR1NfRklMRV9QRVJNSVNTSU9OUz0ke044Tl9FTkZPUkNFX1NFVFRJTkdTX0ZJTEVfUEVSTUlTU0lPTlM6LXRydWV9JwogICAgICAtICdOOE5fUFJPWFlfSE9QUz0ke044Tl9QUk9YWV9IT1BTOi0xfScKICAgICAgLSAnTjhOX1NLSVBfQVVUSF9PTl9PQVVUSF9DQUxMQkFDSz0ke044Tl9TS0lQX0FVVEhfT05fT0FVVEhfQ0FMTEJBQ0s6LWZhbHNlfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ244bi1kYXRhOi9ob21lL25vZGUvLm44bicKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzcWw6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAnd2dldCAtcU8tIGh0dHA6Ly8xMjcuMC4wLjE6NTY3OC8nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICB0YXNrLXJ1bm5lcnM6CiAgICBpbWFnZTogJ244bmlvL3J1bm5lcnM6Mi4xLjUnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnTjhOX1JVTk5FUlNfVEFTS19CUk9LRVJfVVJJPSR7TjhOX1JVTk5FUlNfVEFTS19CUk9LRVJfVVJJOi1odHRwOi8vbjhuOjU2Nzl9JwogICAgICAtIE44Tl9SVU5ORVJTX0FVVEhfVE9LRU49JFNFUlZJQ0VfUEFTU1dPUkRfTjhOCiAgICAgIC0gJ044Tl9SVU5ORVJTX0FVVE9fU0hVVERPV05fVElNRU9VVD0ke044Tl9SVU5ORVJTX0FVVE9fU0hVVERPV05fVElNRU9VVDotMTV9JwogICAgICAtICdOOE5fUlVOTkVSU19NQVhfQ09OQ1VSUkVOQ1k9JHtOOE5fUlVOTkVSU19NQVhfQ09OQ1VSUkVOQ1k6LTV9JwogICAgZGVwZW5kc19vbjoKICAgICAgLSBuOG4KICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAnd2dldCAtcU8tIGh0dHA6Ly8xMjcuMC4wLjE6NTY4MC8nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICBwb3N0Z3Jlc3FsOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNi1hbHBpbmUnCiAgICB2b2x1bWVzOgogICAgICAtICdwb3N0Z3Jlc3FsLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUE9TVEdSRVNfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotbjhufScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK", + "compose": "c2VydmljZXM6CiAgbjhuOgogICAgaW1hZ2U6ICduOG5pby9uOG46Mi4xMC4yJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX044Tl81Njc4CiAgICAgIC0gJ044Tl9FRElUT1JfQkFTRV9VUkw9JHtTRVJWSUNFX0ZRRE5fTjhOfScKICAgICAgLSAnV0VCSE9PS19VUkw9JHtTRVJWSUNFX0ZRRE5fTjhOfScKICAgICAgLSAnTjhOX0hPU1Q9JHtTRVJWSUNFX0ZRRE5fTjhOfScKICAgICAgLSAnTjhOX1BST1RPQ09MPSR7TjhOX1BST1RPQ09MOi1odHRwc30nCiAgICAgIC0gJ0dFTkVSSUNfVElNRVpPTkU9JHtHRU5FUklDX1RJTUVaT05FOi1VVEN9JwogICAgICAtICdUWj0ke1RaOi1VVEN9JwogICAgICAtIERCX1RZUEU9cG9zdGdyZXNkYgogICAgICAtICdEQl9QT1NUR1JFU0RCX0RBVEFCQVNFPSR7UE9TVEdSRVNfREI6LW44bn0nCiAgICAgIC0gREJfUE9TVEdSRVNEQl9IT1NUPXBvc3RncmVzcWwKICAgICAgLSBEQl9QT1NUR1JFU0RCX1BPUlQ9NTQzMgogICAgICAtIERCX1BPU1RHUkVTREJfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gREJfUE9TVEdSRVNEQl9TQ0hFTUE9cHVibGljCiAgICAgIC0gREJfUE9TVEdSRVNEQl9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtIE44Tl9SVU5ORVJTX0VOQUJMRUQ9dHJ1ZQogICAgICAtIE44Tl9SVU5ORVJTX01PREU9ZXh0ZXJuYWwKICAgICAgLSAnTjhOX1JVTk5FUlNfQlJPS0VSX0xJU1RFTl9BRERSRVNTPSR7TjhOX1JVTk5FUlNfQlJPS0VSX0xJU1RFTl9BRERSRVNTOi0wLjAuMC4wfScKICAgICAgLSAnTjhOX1JVTk5FUlNfQlJPS0VSX1BPUlQ9JHtOOE5fUlVOTkVSU19CUk9LRVJfUE9SVDotNTY3OX0nCiAgICAgIC0gTjhOX1JVTk5FUlNfQVVUSF9UT0tFTj0kU0VSVklDRV9QQVNTV09SRF9OOE4KICAgICAgLSAnTjhOX05BVElWRV9QWVRIT05fUlVOTkVSPSR7TjhOX05BVElWRV9QWVRIT05fUlVOTkVSOi10cnVlfScKICAgICAgLSAnTjhOX1JVTk5FUlNfTUFYX0NPTkNVUlJFTkNZPSR7TjhOX1JVTk5FUlNfTUFYX0NPTkNVUlJFTkNZOi01fScKICAgICAgLSAnTjhOX0JMT0NLX0VOVl9BQ0NFU1NfSU5fTk9ERT0ke044Tl9CTE9DS19FTlZfQUNDRVNTX0lOX05PREU6LXRydWV9JwogICAgICAtICdOOE5fR0lUX05PREVfRElTQUJMRV9CQVJFX1JFUE9TPSR7TjhOX0dJVF9OT0RFX0RJU0FCTEVfQkFSRV9SRVBPUzotdHJ1ZX0nCiAgICAgIC0gJ044Tl9FTkZPUkNFX1NFVFRJTkdTX0ZJTEVfUEVSTUlTU0lPTlM9JHtOOE5fRU5GT1JDRV9TRVRUSU5HU19GSUxFX1BFUk1JU1NJT05TOi10cnVlfScKICAgICAgLSAnTjhOX1BST1hZX0hPUFM9JHtOOE5fUFJPWFlfSE9QUzotMX0nCiAgICAgIC0gJ044Tl9TS0lQX0FVVEhfT05fT0FVVEhfQ0FMTEJBQ0s9JHtOOE5fU0tJUF9BVVRIX09OX09BVVRIX0NBTExCQUNLOi1mYWxzZX0nCiAgICB2b2x1bWVzOgogICAgICAtICduOG4tZGF0YTovaG9tZS9ub2RlLy5uOG4nCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3Jlc3FsOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3dnZXQgLXFPLSBodHRwOi8vMTI3LjAuMC4xOjU2NzgvJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgdGFzay1ydW5uZXJzOgogICAgaW1hZ2U6ICduOG5pby9ydW5uZXJzOjIuMTAuMicKICAgIGVudmlyb25tZW50OgogICAgICAtICdOOE5fUlVOTkVSU19UQVNLX0JST0tFUl9VUkk9JHtOOE5fUlVOTkVSU19UQVNLX0JST0tFUl9VUkk6LWh0dHA6Ly9uOG46NTY3OX0nCiAgICAgIC0gTjhOX1JVTk5FUlNfQVVUSF9UT0tFTj0kU0VSVklDRV9QQVNTV09SRF9OOE4KICAgICAgLSAnTjhOX1JVTk5FUlNfQVVUT19TSFVURE9XTl9USU1FT1VUPSR7TjhOX1JVTk5FUlNfQVVUT19TSFVURE9XTl9USU1FT1VUOi0xNX0nCiAgICAgIC0gJ044Tl9SVU5ORVJTX01BWF9DT05DVVJSRU5DWT0ke044Tl9SVU5ORVJTX01BWF9DT05DVVJSRU5DWTotNX0nCiAgICBkZXBlbmRzX29uOgogICAgICAtIG44bgogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICd3Z2V0IC1xTy0gaHR0cDovLzEyNy4wLjAuMTo1NjgwLycKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHBvc3RncmVzcWw6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3Bvc3RncmVzcWwtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1NUR1JFU19VU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi1uOG59JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=", "tags": [ "n8n", "workflow", @@ -2923,7 +2923,7 @@ "n8n": { "documentation": "https://n8n.io?utm_source=coolify.io", "slogan": "n8n is an extendable workflow automation tool.", - "compose": "c2VydmljZXM6CiAgbjhuOgogICAgaW1hZ2U6ICduOG5pby9uOG46Mi4xLjUnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fTjhOXzU2NzgKICAgICAgLSAnTjhOX0VESVRPUl9CQVNFX1VSTD0ke1NFUlZJQ0VfRlFETl9OOE59JwogICAgICAtICdXRUJIT09LX1VSTD0ke1NFUlZJQ0VfRlFETl9OOE59JwogICAgICAtICdOOE5fSE9TVD0ke1NFUlZJQ0VfRlFETl9OOE59JwogICAgICAtICdOOE5fUFJPVE9DT0w9JHtOOE5fUFJPVE9DT0w6LWh0dHBzfScKICAgICAgLSAnR0VORVJJQ19USU1FWk9ORT0ke0dFTkVSSUNfVElNRVpPTkU6LVVUQ30nCiAgICAgIC0gJ1RaPSR7VFo6LVVUQ30nCiAgICAgIC0gJ0RCX1NRTElURV9QT09MX1NJWkU9JHtEQl9TUUxJVEVfUE9PTF9TSVpFOi0yfScKICAgICAgLSBOOE5fUlVOTkVSU19FTkFCTEVEPXRydWUKICAgICAgLSBOOE5fUlVOTkVSU19NT0RFPWV4dGVybmFsCiAgICAgIC0gJ044Tl9SVU5ORVJTX0JST0tFUl9MSVNURU5fQUREUkVTUz0ke044Tl9SVU5ORVJTX0JST0tFUl9MSVNURU5fQUREUkVTUzotMC4wLjAuMH0nCiAgICAgIC0gJ044Tl9SVU5ORVJTX0JST0tFUl9QT1JUPSR7TjhOX1JVTk5FUlNfQlJPS0VSX1BPUlQ6LTU2Nzl9JwogICAgICAtICdOOE5fUlVOTkVSU19BVVRIX1RPS0VOPSR7U0VSVklDRV9QQVNTV09SRF9OOE59JwogICAgICAtICdOOE5fTkFUSVZFX1BZVEhPTl9SVU5ORVI9JHtOOE5fTkFUSVZFX1BZVEhPTl9SVU5ORVI6LXRydWV9JwogICAgICAtICdOOE5fUlVOTkVSU19NQVhfQ09OQ1VSUkVOQ1k9JHtOOE5fUlVOTkVSU19NQVhfQ09OQ1VSUkVOQ1k6LTV9JwogICAgICAtICdOOE5fQkxPQ0tfRU5WX0FDQ0VTU19JTl9OT0RFPSR7TjhOX0JMT0NLX0VOVl9BQ0NFU1NfSU5fTk9ERTotdHJ1ZX0nCiAgICAgIC0gJ044Tl9HSVRfTk9ERV9ESVNBQkxFX0JBUkVfUkVQT1M9JHtOOE5fR0lUX05PREVfRElTQUJMRV9CQVJFX1JFUE9TOi10cnVlfScKICAgICAgLSAnTjhOX0VORk9SQ0VfU0VUVElOR1NfRklMRV9QRVJNSVNTSU9OUz0ke044Tl9FTkZPUkNFX1NFVFRJTkdTX0ZJTEVfUEVSTUlTU0lPTlM6LXRydWV9JwogICAgICAtICdOOE5fUFJPWFlfSE9QUz0ke044Tl9QUk9YWV9IT1BTOi0xfScKICAgICAgLSAnTjhOX1NLSVBfQVVUSF9PTl9PQVVUSF9DQUxMQkFDSz0ke044Tl9TS0lQX0FVVEhfT05fT0FVVEhfQ0FMTEJBQ0s6LWZhbHNlfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ244bi1kYXRhOi9ob21lL25vZGUvLm44bicKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAnd2dldCAtcU8tIGh0dHA6Ly8xMjcuMC4wLjE6NTY3OC8nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICB0YXNrLXJ1bm5lcnM6CiAgICBpbWFnZTogJ244bmlvL3J1bm5lcnM6Mi4xLjUnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnTjhOX1JVTk5FUlNfVEFTS19CUk9LRVJfVVJJPSR7TjhOX1JVTk5FUlNfVEFTS19CUk9LRVJfVVJJOi1odHRwOi8vbjhuOjU2Nzl9JwogICAgICAtICdOOE5fUlVOTkVSU19BVVRIX1RPS0VOPSR7U0VSVklDRV9QQVNTV09SRF9OOE59JwogICAgICAtICdOOE5fUlVOTkVSU19BVVRPX1NIVVRET1dOX1RJTUVPVVQ9JHtOOE5fUlVOTkVSU19BVVRPX1NIVVRET1dOX1RJTUVPVVQ6LTE1fScKICAgICAgLSAnTjhOX1JVTk5FUlNfTUFYX0NPTkNVUlJFTkNZPSR7TjhOX1JVTk5FUlNfTUFYX0NPTkNVUlJFTkNZOi01fScKICAgIGRlcGVuZHNfb246CiAgICAgIC0gbjhuCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3dnZXQgLXFPLSBodHRwOi8vMTI3LjAuMC4xOjU2ODAvJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==", + "compose": "c2VydmljZXM6CiAgbjhuOgogICAgaW1hZ2U6ICduOG5pby9uOG46Mi4xMC4yJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX044Tl81Njc4CiAgICAgIC0gJ044Tl9FRElUT1JfQkFTRV9VUkw9JHtTRVJWSUNFX0ZRRE5fTjhOfScKICAgICAgLSAnV0VCSE9PS19VUkw9JHtTRVJWSUNFX0ZRRE5fTjhOfScKICAgICAgLSAnTjhOX0hPU1Q9JHtTRVJWSUNFX0ZRRE5fTjhOfScKICAgICAgLSAnTjhOX1BST1RPQ09MPSR7TjhOX1BST1RPQ09MOi1odHRwc30nCiAgICAgIC0gJ0dFTkVSSUNfVElNRVpPTkU9JHtHRU5FUklDX1RJTUVaT05FOi1VVEN9JwogICAgICAtICdUWj0ke1RaOi1VVEN9JwogICAgICAtICdEQl9TUUxJVEVfUE9PTF9TSVpFPSR7REJfU1FMSVRFX1BPT0xfU0laRTotMn0nCiAgICAgIC0gTjhOX1JVTk5FUlNfRU5BQkxFRD10cnVlCiAgICAgIC0gTjhOX1JVTk5FUlNfTU9ERT1leHRlcm5hbAogICAgICAtICdOOE5fUlVOTkVSU19CUk9LRVJfTElTVEVOX0FERFJFU1M9JHtOOE5fUlVOTkVSU19CUk9LRVJfTElTVEVOX0FERFJFU1M6LTAuMC4wLjB9JwogICAgICAtICdOOE5fUlVOTkVSU19CUk9LRVJfUE9SVD0ke044Tl9SVU5ORVJTX0JST0tFUl9QT1JUOi01Njc5fScKICAgICAgLSAnTjhOX1JVTk5FUlNfQVVUSF9UT0tFTj0ke1NFUlZJQ0VfUEFTU1dPUkRfTjhOfScKICAgICAgLSAnTjhOX05BVElWRV9QWVRIT05fUlVOTkVSPSR7TjhOX05BVElWRV9QWVRIT05fUlVOTkVSOi10cnVlfScKICAgICAgLSAnTjhOX1JVTk5FUlNfTUFYX0NPTkNVUlJFTkNZPSR7TjhOX1JVTk5FUlNfTUFYX0NPTkNVUlJFTkNZOi01fScKICAgICAgLSAnTjhOX0JMT0NLX0VOVl9BQ0NFU1NfSU5fTk9ERT0ke044Tl9CTE9DS19FTlZfQUNDRVNTX0lOX05PREU6LXRydWV9JwogICAgICAtICdOOE5fR0lUX05PREVfRElTQUJMRV9CQVJFX1JFUE9TPSR7TjhOX0dJVF9OT0RFX0RJU0FCTEVfQkFSRV9SRVBPUzotdHJ1ZX0nCiAgICAgIC0gJ044Tl9FTkZPUkNFX1NFVFRJTkdTX0ZJTEVfUEVSTUlTU0lPTlM9JHtOOE5fRU5GT1JDRV9TRVRUSU5HU19GSUxFX1BFUk1JU1NJT05TOi10cnVlfScKICAgICAgLSAnTjhOX1BST1hZX0hPUFM9JHtOOE5fUFJPWFlfSE9QUzotMX0nCiAgICAgIC0gJ044Tl9TS0lQX0FVVEhfT05fT0FVVEhfQ0FMTEJBQ0s9JHtOOE5fU0tJUF9BVVRIX09OX09BVVRIX0NBTExCQUNLOi1mYWxzZX0nCiAgICB2b2x1bWVzOgogICAgICAtICduOG4tZGF0YTovaG9tZS9ub2RlLy5uOG4nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3dnZXQgLXFPLSBodHRwOi8vMTI3LjAuMC4xOjU2NzgvJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgdGFzay1ydW5uZXJzOgogICAgaW1hZ2U6ICduOG5pby9ydW5uZXJzOjIuMTAuMicKICAgIGVudmlyb25tZW50OgogICAgICAtICdOOE5fUlVOTkVSU19UQVNLX0JST0tFUl9VUkk9JHtOOE5fUlVOTkVSU19UQVNLX0JST0tFUl9VUkk6LWh0dHA6Ly9uOG46NTY3OX0nCiAgICAgIC0gJ044Tl9SVU5ORVJTX0FVVEhfVE9LRU49JHtTRVJWSUNFX1BBU1NXT1JEX044Tn0nCiAgICAgIC0gJ044Tl9SVU5ORVJTX0FVVE9fU0hVVERPV05fVElNRU9VVD0ke044Tl9SVU5ORVJTX0FVVE9fU0hVVERPV05fVElNRU9VVDotMTV9JwogICAgICAtICdOOE5fUlVOTkVSU19NQVhfQ09OQ1VSUkVOQ1k9JHtOOE5fUlVOTkVSU19NQVhfQ09OQ1VSUkVOQ1k6LTV9JwogICAgZGVwZW5kc19vbjoKICAgICAgLSBuOG4KICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAnd2dldCAtcU8tIGh0dHA6Ly8xMjcuMC4wLjE6NTY4MC8nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK", "tags": [ "n8n", "workflow", diff --git a/tests/Feature/ApplicationSourceLocalhostKeyTest.php b/tests/Feature/ApplicationSourceLocalhostKeyTest.php new file mode 100644 index 000000000..9b9b7b184 --- /dev/null +++ b/tests/Feature/ApplicationSourceLocalhostKeyTest.php @@ -0,0 +1,59 @@ +team = Team::factory()->create(); + $this->user = User::factory()->create(); + $this->team->members()->attach($this->user->id, ['role' => 'owner']); + + $this->actingAs($this->user); + session(['currentTeam' => $this->team]); + + $this->project = Project::factory()->create(['team_id' => $this->team->id]); + $this->environment = Environment::factory()->create(['project_id' => $this->project->id]); +}); + +describe('Application Source with localhost key (id=0)', function () { + test('renders deploy key section when private_key_id is 0', function () { + $privateKey = PrivateKey::create([ + 'id' => 0, + 'name' => 'localhost', + 'private_key' => 'test-key-content', + 'team_id' => $this->team->id, + ]); + + $application = Application::factory()->create([ + 'environment_id' => $this->environment->id, + 'private_key_id' => 0, + ]); + + Livewire::test(Source::class, ['application' => $application]) + ->assertSuccessful() + ->assertSet('privateKeyId', 0) + ->assertSee('Deploy Key'); + }); + + test('shows no source connected section when private_key_id is null', function () { + $application = Application::factory()->create([ + 'environment_id' => $this->environment->id, + 'private_key_id' => null, + ]); + + Livewire::test(Source::class, ['application' => $application]) + ->assertSuccessful() + ->assertSet('privateKeyId', null) + ->assertDontSee('Deploy Key') + ->assertSee('No source connected'); + }); +}); From 5b701ebb07cb90b2e2cf7ac98e47ee82bab18279 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 9 Mar 2026 17:23:34 +0100 Subject: [PATCH 07/17] refactor(application-source): use Laravel helpers for null checks Replace is_null() and !is_null() with blank() and filled() helper functions for better readability and Laravel idiomatic style. --- resources/views/livewire/project/application/source.blade.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/views/livewire/project/application/source.blade.php b/resources/views/livewire/project/application/source.blade.php index 3178d52b9..1e624738c 100644 --- a/resources/views/livewire/project/application/source.blade.php +++ b/resources/views/livewire/project/application/source.blade.php @@ -28,7 +28,7 @@
Code source of your application.
- @if (is_null($privateKeyId)) + @if (blank($privateKeyId))
Currently connected source: {{ data_get($application, 'source.name', 'No source connected') }}
@@ -44,7 +44,7 @@ class="font-bold text-warning">{{ data_get($application, 'source.name', 'No sour
- @if (!is_null($privateKeyId)) + @if (filled($privateKeyId))

Deploy Key

Currently attached Private Key: {{ $privateKeyName }} From e3daba0b1d960aa0a5e5305bb833002ed7c5d863 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 10 Mar 2026 09:43:29 +0100 Subject: [PATCH 08/17] chore: prepare for PR --- app/Jobs/ApplicationDeploymentJob.php | 12 ++- ...posePreserveRepositoryStartCommandTest.php | 95 +++++++++++++++++++ 2 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 tests/Unit/DockerComposePreserveRepositoryStartCommandTest.php diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index dfcf9ee09..c9f0f1eef 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -805,9 +805,15 @@ private function deploy_docker_compose_buildpack() ); $this->write_deployment_configurations(); - $this->execute_remote_command( - [executeInDocker($this->deployment_uuid, "cd {$this->basedir} && {$start_command}"), 'hidden' => true], - ); + if ($this->preserveRepository) { + $this->execute_remote_command( + ['command' => "cd {$server_workdir} && {$start_command}", 'hidden' => true], + ); + } else { + $this->execute_remote_command( + [executeInDocker($this->deployment_uuid, "cd {$this->basedir} && {$start_command}"), 'hidden' => true], + ); + } } else { $command = "{$this->coolify_variables} docker compose"; if ($this->preserveRepository) { diff --git a/tests/Unit/DockerComposePreserveRepositoryStartCommandTest.php b/tests/Unit/DockerComposePreserveRepositoryStartCommandTest.php new file mode 100644 index 000000000..2d33b60f9 --- /dev/null +++ b/tests/Unit/DockerComposePreserveRepositoryStartCommandTest.php @@ -0,0 +1,95 @@ +not->toContain('docker exec'); + expect($command)->toStartWith("cd {$serverWorkdir}"); + expect($command)->toContain($startCommand); +}); + +it('generates executeInDocker command when preserveRepository is false', function () { + $deploymentUuid = 'test-deployment-uuid'; + $serverWorkdir = '/data/coolify/applications/app-uuid'; + $basedir = '/artifacts/test-deployment-uuid'; + $workdir = '/artifacts/test-deployment-uuid/backend'; + $preserveRepository = false; + + $startCommand = 'docker compose -f /artifacts/test-deployment-uuid/backend/compose.yml --env-file /artifacts/test-deployment-uuid/backend/.env --profile all up -d'; + + // Simulate the logic from ApplicationDeploymentJob::deploy_docker_compose_buildpack() + if ($preserveRepository) { + $command = "cd {$serverWorkdir} && {$startCommand}"; + } else { + $command = executeInDocker($deploymentUuid, "cd {$basedir} && {$startCommand}"); + } + + // When preserveRepository is false, the command SHOULD be wrapped in executeInDocker + expect($command)->toContain('docker exec'); + expect($command)->toContain($deploymentUuid); + expect($command)->toContain("cd {$basedir}"); +}); + +it('uses host paths for env-file when preserveRepository is true', function () { + $serverWorkdir = '/data/coolify/applications/app-uuid'; + $composeLocation = '/compose.yml'; + $preserveRepository = true; + + $workdirPath = $preserveRepository ? $serverWorkdir : '/artifacts/deployment-uuid/backend'; + $startCommand = injectDockerComposeFlags( + 'docker compose --profile all up -d', + "{$workdirPath}{$composeLocation}", + "{$workdirPath}/.env" + ); + + // Verify the injected paths point to the host filesystem + expect($startCommand)->toContain("--env-file {$serverWorkdir}/.env"); + expect($startCommand)->toContain("-f {$serverWorkdir}{$composeLocation}"); +}); + +it('uses container paths for env-file when preserveRepository is false', function () { + $workdir = '/artifacts/deployment-uuid/backend'; + $composeLocation = '/compose.yml'; + $preserveRepository = false; + $serverWorkdir = '/data/coolify/applications/app-uuid'; + + $workdirPath = $preserveRepository ? $serverWorkdir : $workdir; + $startCommand = injectDockerComposeFlags( + 'docker compose --profile all up -d', + "{$workdirPath}{$composeLocation}", + "{$workdirPath}/.env" + ); + + // Verify the injected paths point to the container filesystem + expect($startCommand)->toContain("--env-file {$workdir}/.env"); + expect($startCommand)->toContain("-f {$workdir}{$composeLocation}"); + expect($startCommand)->not->toContain('/data/coolify/applications/'); +}); From 184fbb98f3ec08d092af6dd7bd58a1e8af57427a Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 10 Mar 2026 09:59:19 +0100 Subject: [PATCH 09/17] fix(proxy): add validation and normalization for database proxy timeout - Extract proxy timeout configuration logic into dedicated method - Add min:1 validation rule for publicPortTimeout - Normalize invalid timeout values (null, 0, negative) to default 3600s - Add tests for timeout configuration normalization and validation --- app/Actions/Database/StartDatabaseProxy.php | 12 ++++++++++-- app/Livewire/Project/Service/Index.php | 2 +- tests/Feature/StartDatabaseProxyTest.php | 12 ++++++++++++ tests/Unit/ServiceIndexValidationTest.php | 11 +++++++++++ 4 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 tests/Unit/ServiceIndexValidationTest.php diff --git a/app/Actions/Database/StartDatabaseProxy.php b/app/Actions/Database/StartDatabaseProxy.php index 0d20fa4a4..1de28a245 100644 --- a/app/Actions/Database/StartDatabaseProxy.php +++ b/app/Actions/Database/StartDatabaseProxy.php @@ -54,8 +54,7 @@ public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|St if (isDev()) { $configuration_dir = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/databases/'.$database->uuid.'/proxy'; } - $timeout = $database->public_port_timeout ?? 3600; - $timeoutConfig = $timeout === 0 ? 'proxy_timeout 0;' : "proxy_timeout {$timeout}s;"; + $timeoutConfig = $this->buildProxyTimeoutConfig($database->public_port_timeout); $nginxconf = << 'required', 'excludeFromStatus' => 'required|boolean', 'publicPort' => 'nullable|integer', - 'publicPortTimeout' => 'nullable|integer', + 'publicPortTimeout' => 'nullable|integer|min:1', 'isPublic' => 'required|boolean', 'isLogDrainEnabled' => 'required|boolean', // Application-specific rules diff --git a/tests/Feature/StartDatabaseProxyTest.php b/tests/Feature/StartDatabaseProxyTest.php index c62569866..b14cb414a 100644 --- a/tests/Feature/StartDatabaseProxyTest.php +++ b/tests/Feature/StartDatabaseProxyTest.php @@ -43,3 +43,15 @@ ->and($method->invoke($action, 'network timeout'))->toBeFalse() ->and($method->invoke($action, 'connection refused'))->toBeFalse(); }); + +test('buildProxyTimeoutConfig normalizes invalid values to default', function (?int $input, string $expected) { + $action = new StartDatabaseProxy; + $method = new ReflectionMethod($action, 'buildProxyTimeoutConfig'); + + expect($method->invoke($action, $input))->toBe($expected); +})->with([ + [null, 'proxy_timeout 3600s;'], + [0, 'proxy_timeout 3600s;'], + [-10, 'proxy_timeout 3600s;'], + [120, 'proxy_timeout 120s;'], +]); diff --git a/tests/Unit/ServiceIndexValidationTest.php b/tests/Unit/ServiceIndexValidationTest.php new file mode 100644 index 000000000..7b746cde6 --- /dev/null +++ b/tests/Unit/ServiceIndexValidationTest.php @@ -0,0 +1,11 @@ + $this->rules)->call($component); + + expect($rules['publicPortTimeout']) + ->toContain('min:1'); +}); From 470cc15e626ba48343c0ab6b79abd19742e5e0ef Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 10 Mar 2026 14:05:05 +0100 Subject: [PATCH 10/17] feat(jobs): implement encrypted queue jobs - Add ShouldBeEncrypted interface to all queue jobs to encrypt sensitive job payloads - Configure explicit retry policies for messaging jobs (5 attempts, 10-second backoff) --- app/Jobs/CheckTraefikVersionForServerJob.php | 3 ++- app/Jobs/CheckTraefikVersionJob.php | 3 ++- app/Jobs/RegenerateSslCertJob.php | 3 ++- app/Jobs/SendMessageToSlackJob.php | 13 ++++++++++++- app/Jobs/SendMessageToTelegramJob.php | 5 +++++ app/Jobs/ServerManagerJob.php | 3 ++- app/Jobs/StripeProcessJob.php | 3 ++- app/Jobs/SyncStripeSubscriptionsJob.php | 3 ++- app/Jobs/ValidateAndInstallServerJob.php | 3 ++- app/Jobs/VerifyStripeSubscriptionStatusJob.php | 3 ++- 10 files changed, 33 insertions(+), 9 deletions(-) diff --git a/app/Jobs/CheckTraefikVersionForServerJob.php b/app/Jobs/CheckTraefikVersionForServerJob.php index 92ec4cbd4..91869eb12 100644 --- a/app/Jobs/CheckTraefikVersionForServerJob.php +++ b/app/Jobs/CheckTraefikVersionForServerJob.php @@ -6,12 +6,13 @@ use App\Models\Server; use App\Notifications\Server\TraefikVersionOutdated; use Illuminate\Bus\Queueable; +use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -class CheckTraefikVersionForServerJob implements ShouldQueue +class CheckTraefikVersionForServerJob implements ShouldBeEncrypted, ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; diff --git a/app/Jobs/CheckTraefikVersionJob.php b/app/Jobs/CheckTraefikVersionJob.php index a513f280e..ac94aa23f 100644 --- a/app/Jobs/CheckTraefikVersionJob.php +++ b/app/Jobs/CheckTraefikVersionJob.php @@ -5,12 +5,13 @@ use App\Enums\ProxyTypes; use App\Models\Server; use Illuminate\Bus\Queueable; +use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -class CheckTraefikVersionJob implements ShouldQueue +class CheckTraefikVersionJob implements ShouldBeEncrypted, ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; diff --git a/app/Jobs/RegenerateSslCertJob.php b/app/Jobs/RegenerateSslCertJob.php index c0284e1ee..6f49cf30b 100644 --- a/app/Jobs/RegenerateSslCertJob.php +++ b/app/Jobs/RegenerateSslCertJob.php @@ -7,13 +7,14 @@ use App\Models\Team; use App\Notifications\SslExpirationNotification; use Illuminate\Bus\Queueable; +use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\Log; -class RegenerateSslCertJob implements ShouldQueue +class RegenerateSslCertJob implements ShouldBeEncrypted, ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; diff --git a/app/Jobs/SendMessageToSlackJob.php b/app/Jobs/SendMessageToSlackJob.php index fcd87a9dd..f869fd602 100644 --- a/app/Jobs/SendMessageToSlackJob.php +++ b/app/Jobs/SendMessageToSlackJob.php @@ -4,16 +4,27 @@ use App\Notifications\Dto\SlackMessage; use Illuminate\Bus\Queueable; +use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\Http; -class SendMessageToSlackJob implements ShouldQueue +class SendMessageToSlackJob implements ShouldBeEncrypted, ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; + /** + * The number of times the job may be attempted. + */ + public $tries = 5; + + /** + * The number of seconds to wait before retrying the job. + */ + public $backoff = 10; + public function __construct( private SlackMessage $message, private string $webhookUrl diff --git a/app/Jobs/SendMessageToTelegramJob.php b/app/Jobs/SendMessageToTelegramJob.php index 6b0a64ae3..6b04d2191 100644 --- a/app/Jobs/SendMessageToTelegramJob.php +++ b/app/Jobs/SendMessageToTelegramJob.php @@ -22,6 +22,11 @@ class SendMessageToTelegramJob implements ShouldBeEncrypted, ShouldQueue */ public $tries = 5; + /** + * The number of seconds to wait before retrying the job. + */ + public $backoff = 10; + /** * The maximum number of unhandled exceptions to allow before failing. */ diff --git a/app/Jobs/ServerManagerJob.php b/app/Jobs/ServerManagerJob.php index c8219a2ea..730ce547d 100644 --- a/app/Jobs/ServerManagerJob.php +++ b/app/Jobs/ServerManagerJob.php @@ -7,6 +7,7 @@ use App\Models\Team; use Cron\CronExpression; use Illuminate\Bus\Queueable; +use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; @@ -15,7 +16,7 @@ use Illuminate\Support\Collection; use Illuminate\Support\Facades\Log; -class ServerManagerJob implements ShouldQueue +class ServerManagerJob implements ShouldBeEncrypted, ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; diff --git a/app/Jobs/StripeProcessJob.php b/app/Jobs/StripeProcessJob.php index aebceaa6d..e61ac81e4 100644 --- a/app/Jobs/StripeProcessJob.php +++ b/app/Jobs/StripeProcessJob.php @@ -4,11 +4,12 @@ use App\Models\Subscription; use App\Models\Team; +use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Queue\Queueable; use Illuminate\Support\Str; -class StripeProcessJob implements ShouldQueue +class StripeProcessJob implements ShouldBeEncrypted, ShouldQueue { use Queueable; diff --git a/app/Jobs/SyncStripeSubscriptionsJob.php b/app/Jobs/SyncStripeSubscriptionsJob.php index 4301a80d1..0e221756d 100644 --- a/app/Jobs/SyncStripeSubscriptionsJob.php +++ b/app/Jobs/SyncStripeSubscriptionsJob.php @@ -4,12 +4,13 @@ use App\Models\Subscription; use Illuminate\Bus\Queueable; +use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -class SyncStripeSubscriptionsJob implements ShouldQueue +class SyncStripeSubscriptionsJob implements ShouldBeEncrypted, ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; diff --git a/app/Jobs/ValidateAndInstallServerJob.php b/app/Jobs/ValidateAndInstallServerJob.php index b5e1929de..9f02f9b78 100644 --- a/app/Jobs/ValidateAndInstallServerJob.php +++ b/app/Jobs/ValidateAndInstallServerJob.php @@ -8,13 +8,14 @@ use App\Events\ServerValidated; use App\Models\Server; use Illuminate\Bus\Queueable; +use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\Log; -class ValidateAndInstallServerJob implements ShouldQueue +class ValidateAndInstallServerJob implements ShouldBeEncrypted, ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; diff --git a/app/Jobs/VerifyStripeSubscriptionStatusJob.php b/app/Jobs/VerifyStripeSubscriptionStatusJob.php index 58b6944a2..cf7c3c0ea 100644 --- a/app/Jobs/VerifyStripeSubscriptionStatusJob.php +++ b/app/Jobs/VerifyStripeSubscriptionStatusJob.php @@ -4,12 +4,13 @@ use App\Models\Subscription; use Illuminate\Bus\Queueable; +use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -class VerifyStripeSubscriptionStatusJob implements ShouldQueue +class VerifyStripeSubscriptionStatusJob implements ShouldBeEncrypted, ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; From 872e300cf9ca606cd68e89aa1a56b3e5892fcedc Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 10 Mar 2026 17:14:08 +0100 Subject: [PATCH 11/17] fix(subscription): use optional chaining for preview object access Add optional chaining operator (?.) to all preview property accesses in the subscription actions view to prevent potential null reference errors when the preview object is undefined. --- resources/views/livewire/subscription/actions.blade.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/resources/views/livewire/subscription/actions.blade.php b/resources/views/livewire/subscription/actions.blade.php index 4b276aaf6..c2bc7f221 100644 --- a/resources/views/livewire/subscription/actions.blade.php +++ b/resources/views/livewire/subscription/actions.blade.php @@ -139,7 +139,7 @@ class="w-20 px-2 py-1 text-xl font-bold text-center rounded border dark:bg-coolg
Due now
Prorated charge - +

Charged immediately to your payment method.

@@ -147,8 +147,8 @@ class="w-20 px-2 py-1 text-xl font-bold text-center rounded border dark:bg-coolg
Next billing cycle
- - + +
@@ -156,7 +156,7 @@ class="w-20 px-2 py-1 text-xl font-bold text-center rounded border dark:bg-coolg
Total / month - +
From a36228297632177c85a45384ee6f9fc5988bc7de Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 10 Mar 2026 17:37:13 +0100 Subject: [PATCH 12/17] chore: prepare for PR --- bootstrap/helpers/parsers.php | 24 ++++++++++---- bootstrap/helpers/services.php | 5 +++ .../NestedEnvironmentVariableParsingTest.php | 33 +++++++++++++++++++ 3 files changed, 56 insertions(+), 6 deletions(-) diff --git a/bootstrap/helpers/parsers.php b/bootstrap/helpers/parsers.php index fa40857ac..6b66de8a2 100644 --- a/bootstrap/helpers/parsers.php +++ b/bootstrap/helpers/parsers.php @@ -442,9 +442,9 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int $value = str($value); $regex = '/\$(\{?([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)\}?)/'; preg_match_all($regex, $value, $valueMatches); - if (count($valueMatches[1]) > 0) { - foreach ($valueMatches[1] as $match) { - $match = replaceVariables($match); + if (count($valueMatches[2]) > 0) { + foreach ($valueMatches[2] as $match) { + $match = str($match); if ($match->startsWith('SERVICE_')) { if ($magicEnvironments->has($match->value())) { continue; @@ -1509,6 +1509,18 @@ function serviceParser(Service $resource): Collection return collect([]); } $services = data_get($yaml, 'services', collect([])); + + // Clean up corrupted environment variables from previous parser bugs + // (keys starting with $ or ending with } should not exist as env var names) + $resource->environment_variables() + ->where('resourceable_type', get_class($resource)) + ->where('resourceable_id', $resource->id) + ->where(function ($q) { + $q->where('key', 'LIKE', '$%') + ->orWhere('key', 'LIKE', '%}'); + }) + ->delete(); + $topLevel = collect([ 'volumes' => collect(data_get($yaml, 'volumes', [])), 'networks' => collect(data_get($yaml, 'networks', [])), @@ -1686,9 +1698,9 @@ function serviceParser(Service $resource): Collection $value = str($value); $regex = '/\$(\{?([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)\}?)/'; preg_match_all($regex, $value, $valueMatches); - if (count($valueMatches[1]) > 0) { - foreach ($valueMatches[1] as $match) { - $match = replaceVariables($match); + if (count($valueMatches[2]) > 0) { + foreach ($valueMatches[2] as $match) { + $match = str($match); if ($match->startsWith('SERVICE_')) { if ($magicEnvironments->has($match->value())) { continue; diff --git a/bootstrap/helpers/services.php b/bootstrap/helpers/services.php index bd741b76e..20b184a01 100644 --- a/bootstrap/helpers/services.php +++ b/bootstrap/helpers/services.php @@ -128,6 +128,11 @@ function replaceVariables(string $variable): Stringable return $str->replaceFirst('{', '')->before('}'); } + // Handle bare $VAR format (no braces) + if ($str->startsWith('$')) { + return $str->replaceFirst('$', ''); + } + return $str; } diff --git a/tests/Unit/NestedEnvironmentVariableParsingTest.php b/tests/Unit/NestedEnvironmentVariableParsingTest.php index 65e8738cc..b98f49dd7 100644 --- a/tests/Unit/NestedEnvironmentVariableParsingTest.php +++ b/tests/Unit/NestedEnvironmentVariableParsingTest.php @@ -206,6 +206,39 @@ expect($split['default'])->toBe('${SERVICE_URL_CONFIG}/v2/config.json'); }); +test('replaceVariables strips leading dollar sign from bare $VAR format', function () { + // Bug #8851: When a compose value is $SERVICE_USER_POSTGRES (bare $VAR, no braces), + // replaceVariables must strip the $ so the parsed name is SERVICE_USER_POSTGRES. + // Without this, the fallback code path creates a DB entry with key=$SERVICE_USER_POSTGRES. + expect(replaceVariables('$SERVICE_USER_POSTGRES')->value())->toBe('SERVICE_USER_POSTGRES') + ->and(replaceVariables('$SERVICE_PASSWORD_POSTGRES')->value())->toBe('SERVICE_PASSWORD_POSTGRES') + ->and(replaceVariables('$SERVICE_FQDN_APPWRITE')->value())->toBe('SERVICE_FQDN_APPWRITE'); +}); + +test('bare dollar variable in bash-style fallback does not capture trailing brace', function () { + // Bug #8851: ${_APP_DOMAIN:-$SERVICE_FQDN_APPWRITE} causes the regex to + // capture "SERVICE_FQDN_APPWRITE}" (with trailing }) because \}? in the regex + // greedily matches the closing brace of the outer ${...} construct. + // The fix uses capture group 2 (clean variable name) instead of group 1. + $value = '${_APP_DOMAIN:-$SERVICE_FQDN_APPWRITE}'; + + $regex = '/\$(\{?([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)\}?)/'; + preg_match_all($regex, $value, $valueMatches); + + // Group 2 should contain clean variable names without any braces + expect($valueMatches[2])->toContain('_APP_DOMAIN') + ->and($valueMatches[2])->toContain('SERVICE_FQDN_APPWRITE'); + + // Verify no match in group 2 has trailing } + foreach ($valueMatches[2] as $match) { + expect($match)->not->toEndWith('}', "Variable name '{$match}' should not end with }"); + } + + // Group 1 (previously used) would have the bug — SERVICE_FQDN_APPWRITE} + // This demonstrates why group 2 must be used instead + expect($valueMatches[1])->toContain('SERVICE_FQDN_APPWRITE}'); +}); + test('operator precedence with nesting', function () { // The first :- at depth 0 should be used, not the one inside nested braces $input = '${A:-${B:-default}}'; From 0679e91c85f283a527f1db46fb00c6a18f415659 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 10 Mar 2026 18:06:01 +0100 Subject: [PATCH 13/17] fix(parser): use firstOrCreate instead of updateOrCreate for environment variables Prevent unnecessary updates to existing environment variable records. The previous implementation would update matching records, but the intent is to retrieve or create the record without modifying existing ones. --- bootstrap/helpers/parsers.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bootstrap/helpers/parsers.php b/bootstrap/helpers/parsers.php index 6b66de8a2..99ce9185a 100644 --- a/bootstrap/helpers/parsers.php +++ b/bootstrap/helpers/parsers.php @@ -1940,7 +1940,7 @@ function serviceParser(Service $resource): Collection } else { $value = generateEnvValue($command, $resource); - $resource->environment_variables()->updateOrCreate([ + $resource->environment_variables()->firstOrCreate([ 'key' => $key->value(), 'resourceable_type' => get_class($resource), 'resourceable_id' => $resource->id, From 5c5f67f48b893c9ee0b7142f94b734da585e97e0 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 10 Mar 2026 20:37:22 +0100 Subject: [PATCH 14/17] chore: prepare for PR --- docker-compose-maxio.dev.yml | 1 + docker-compose.dev.yml | 1 + docker/coolify-realtime/Dockerfile | 1 + docker/coolify-realtime/terminal-server.js | 277 +++++++++++------- docker/coolify-realtime/terminal-utils.js | 127 ++++++++ .../coolify-realtime/terminal-utils.test.js | 47 +++ resources/js/terminal.js | 87 ++++-- .../execute-container-command.blade.php | 3 +- routes/web.php | 18 +- .../Feature/RealtimeTerminalPackagingTest.php | 34 +++ tests/Feature/TerminalAuthIpsRouteTest.php | 51 ++++ 11 files changed, 513 insertions(+), 134 deletions(-) create mode 100644 docker/coolify-realtime/terminal-utils.js create mode 100644 docker/coolify-realtime/terminal-utils.test.js create mode 100644 tests/Feature/RealtimeTerminalPackagingTest.php create mode 100644 tests/Feature/TerminalAuthIpsRouteTest.php diff --git a/docker-compose-maxio.dev.yml b/docker-compose-maxio.dev.yml index 2c8c94466..bbb483d7a 100644 --- a/docker-compose-maxio.dev.yml +++ b/docker-compose-maxio.dev.yml @@ -73,6 +73,7 @@ services: volumes: - ./storage:/var/www/html/storage - ./docker/coolify-realtime/terminal-server.js:/terminal/terminal-server.js + - ./docker/coolify-realtime/terminal-utils.js:/terminal/terminal-utils.js environment: SOKETI_DEBUG: "false" SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID:-coolify}" diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 808b50ff8..3af443c83 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -73,6 +73,7 @@ services: volumes: - ./storage:/var/www/html/storage - ./docker/coolify-realtime/terminal-server.js:/terminal/terminal-server.js + - ./docker/coolify-realtime/terminal-utils.js:/terminal/terminal-utils.js environment: SOKETI_DEBUG: "false" SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID:-coolify}" diff --git a/docker/coolify-realtime/Dockerfile b/docker/coolify-realtime/Dockerfile index 18c2f9301..99157268b 100644 --- a/docker/coolify-realtime/Dockerfile +++ b/docker/coolify-realtime/Dockerfile @@ -16,6 +16,7 @@ RUN npm i RUN npm rebuild node-pty --update-binary COPY docker/coolify-realtime/soketi-entrypoint.sh /soketi-entrypoint.sh COPY docker/coolify-realtime/terminal-server.js /terminal/terminal-server.js +COPY docker/coolify-realtime/terminal-utils.js /terminal/terminal-utils.js # Install Cloudflared based on architecture RUN if [ "${TARGETPLATFORM}" = "linux/amd64" ]; then \ diff --git a/docker/coolify-realtime/terminal-server.js b/docker/coolify-realtime/terminal-server.js index 2607d2aec..3ae77857f 100755 --- a/docker/coolify-realtime/terminal-server.js +++ b/docker/coolify-realtime/terminal-server.js @@ -4,8 +4,33 @@ import pty from 'node-pty'; import axios from 'axios'; import cookie from 'cookie'; import 'dotenv/config'; +import { + extractHereDocContent, + extractSshArgs, + extractTargetHost, + extractTimeout, + isAuthorizedTargetHost, +} from './terminal-utils.js'; const userSessions = new Map(); +const terminalDebugEnabled = ['local', 'development'].includes( + String(process.env.APP_ENV || process.env.NODE_ENV || '').toLowerCase() +); + +function logTerminal(level, message, context = {}) { + if (!terminalDebugEnabled) { + return; + } + + const formattedMessage = `[TerminalServer] ${message}`; + + if (Object.keys(context).length > 0) { + console[level](formattedMessage, context); + return; + } + + console[level](formattedMessage); +} const server = http.createServer((req, res) => { if (req.url === '/ready') { @@ -31,9 +56,19 @@ const getSessionCookie = (req) => { const verifyClient = async (info, callback) => { const { xsrfToken, laravelSession, sessionCookieName } = getSessionCookie(info.req); + const requestContext = { + remoteAddress: info.req.socket?.remoteAddress, + origin: info.origin, + sessionCookieName, + hasXsrfToken: Boolean(xsrfToken), + hasLaravelSession: Boolean(laravelSession), + }; + + logTerminal('log', 'Verifying websocket client.', requestContext); // Verify presence of required tokens if (!laravelSession || !xsrfToken) { + logTerminal('warn', 'Rejecting websocket client because required auth tokens are missing.', requestContext); return callback(false, 401, 'Unauthorized: Missing required tokens'); } @@ -47,13 +82,22 @@ const verifyClient = async (info, callback) => { }); if (response.status === 200) { - // Authentication successful + logTerminal('log', 'Websocket client authentication succeeded.', requestContext); callback(true); } else { + logTerminal('warn', 'Websocket client authentication returned a non-success status.', { + ...requestContext, + status: response.status, + }); callback(false, 401, 'Unauthorized: Invalid credentials'); } } catch (error) { - console.error('Authentication error:', error.message); + logTerminal('error', 'Websocket client authentication failed.', { + ...requestContext, + error: error.message, + responseStatus: error.response?.status, + responseData: error.response?.data, + }); callback(false, 500, 'Internal Server Error'); } }; @@ -65,28 +109,62 @@ wss.on('connection', async (ws, req) => { const userId = generateUserId(); const userSession = { ws, userId, ptyProcess: null, isActive: false, authorizedIPs: [] }; const { xsrfToken, laravelSession, sessionCookieName } = getSessionCookie(req); + const connectionContext = { + userId, + remoteAddress: req.socket?.remoteAddress, + sessionCookieName, + hasXsrfToken: Boolean(xsrfToken), + hasLaravelSession: Boolean(laravelSession), + }; // Verify presence of required tokens if (!laravelSession || !xsrfToken) { + logTerminal('warn', 'Closing websocket connection because required auth tokens are missing.', connectionContext); ws.close(401, 'Unauthorized: Missing required tokens'); return; } - const response = await axios.post(`http://coolify:8080/terminal/auth/ips`, null, { - headers: { - 'Cookie': `${sessionCookieName}=${laravelSession}`, - 'X-XSRF-TOKEN': xsrfToken - }, - }); - userSession.authorizedIPs = response.data.ipAddresses || []; + + try { + const response = await axios.post(`http://coolify:8080/terminal/auth/ips`, null, { + headers: { + 'Cookie': `${sessionCookieName}=${laravelSession}`, + 'X-XSRF-TOKEN': xsrfToken + }, + }); + userSession.authorizedIPs = response.data.ipAddresses || []; + logTerminal('log', 'Fetched authorized terminal hosts for websocket session.', { + ...connectionContext, + authorizedIPs: userSession.authorizedIPs, + }); + } catch (error) { + logTerminal('error', 'Failed to fetch authorized terminal hosts.', { + ...connectionContext, + error: error.message, + responseStatus: error.response?.status, + responseData: error.response?.data, + }); + ws.close(1011, 'Failed to fetch terminal authorization data'); + return; + } + userSessions.set(userId, userSession); + logTerminal('log', 'Terminal websocket connection established.', { + ...connectionContext, + authorizedHostCount: userSession.authorizedIPs.length, + }); ws.on('message', (message) => { handleMessage(userSession, message); - }); ws.on('error', (err) => handleError(err, userId)); - ws.on('close', () => handleClose(userId)); - + ws.on('close', (code, reason) => { + logTerminal('log', 'Terminal websocket connection closed.', { + userId, + code, + reason: reason?.toString(), + }); + handleClose(userId); + }); }); const messageHandlers = { @@ -98,6 +176,7 @@ const messageHandlers = { }, pause: (session) => session.ptyProcess.pause(), resume: (session) => session.ptyProcess.resume(), + ping: (session) => session.ws.send('pong'), checkActive: (session, data) => { if (data === 'force' && session.isActive) { killPtyProcess(session.userId); @@ -110,12 +189,34 @@ const messageHandlers = { function handleMessage(userSession, message) { const parsed = parseMessage(message); - if (!parsed) return; + if (!parsed) { + logTerminal('warn', 'Ignoring websocket message because JSON parsing failed.', { + userId: userSession.userId, + rawMessage: String(message).slice(0, 500), + }); + return; + } + + logTerminal('log', 'Received websocket message.', { + userId: userSession.userId, + keys: Object.keys(parsed), + isActive: userSession.isActive, + }); Object.entries(parsed).forEach(([key, value]) => { const handler = messageHandlers[key]; - if (handler && (userSession.isActive || key === 'checkActive' || key === 'command')) { + if (handler && (userSession.isActive || key === 'checkActive' || key === 'command' || key === 'ping')) { handler(userSession, value); + } else if (!handler) { + logTerminal('warn', 'Ignoring websocket message with unknown handler key.', { + userId: userSession.userId, + key, + }); + } else { + logTerminal('warn', 'Ignoring websocket message because no PTY session is active yet.', { + userId: userSession.userId, + key, + }); } }); } @@ -124,7 +225,9 @@ function parseMessage(message) { try { return JSON.parse(message); } catch (e) { - console.error('Failed to parse message:', e); + logTerminal('error', 'Failed to parse websocket message.', { + error: e?.message ?? e, + }); return null; } } @@ -134,6 +237,9 @@ async function handleCommand(ws, command, userId) { if (userSession && userSession.isActive) { const result = await killPtyProcess(userId); if (!result) { + logTerminal('warn', 'Rejecting new terminal command because the previous PTY could not be terminated.', { + userId, + }); // if terminal is still active, even after we tried to kill it, dont continue and show error ws.send('unprocessable'); return; @@ -147,13 +253,30 @@ async function handleCommand(ws, command, userId) { // Extract target host from SSH command const targetHost = extractTargetHost(sshArgs); + logTerminal('log', 'Parsed terminal command metadata.', { + userId, + targetHost, + timeout, + sshArgs, + authorizedIPs: userSession?.authorizedIPs ?? [], + }); + if (!targetHost) { + logTerminal('warn', 'Rejecting terminal command because no target host could be extracted.', { + userId, + sshArgs, + }); ws.send('Invalid SSH command: No target host found'); return; } // Validate target host against authorized IPs - if (!userSession.authorizedIPs.includes(targetHost)) { + if (!isAuthorizedTargetHost(targetHost, userSession.authorizedIPs)) { + logTerminal('warn', 'Rejecting terminal command because target host is not authorized.', { + userId, + targetHost, + authorizedIPs: userSession.authorizedIPs, + }); ws.send(`Unauthorized: Target host ${targetHost} not in authorized list`); return; } @@ -169,6 +292,11 @@ async function handleCommand(ws, command, userId) { // NOTE: - Initiates a process within the Terminal container // Establishes an SSH connection to root@coolify with RequestTTY enabled // Executes the 'docker exec' command to connect to a specific container + logTerminal('log', 'Spawning PTY process for terminal session.', { + userId, + targetHost, + timeout, + }); const ptyProcess = pty.spawn('ssh', sshArgs.concat([hereDocContent]), options); userSession.ptyProcess = ptyProcess; @@ -182,7 +310,11 @@ async function handleCommand(ws, command, userId) { // when parent closes ptyProcess.onExit(({ exitCode, signal }) => { - console.error(`Process exited with code ${exitCode} and signal ${signal}`); + logTerminal(exitCode === 0 ? 'log' : 'error', 'PTY process exited.', { + userId, + exitCode, + signal, + }); ws.send('pty-exited'); userSession.isActive = false; }); @@ -194,28 +326,18 @@ async function handleCommand(ws, command, userId) { } } -function extractTargetHost(sshArgs) { - // Find the argument that matches the pattern user@host - const userAtHost = sshArgs.find(arg => { - // Skip paths that contain 'storage/app/ssh/keys/' - if (arg.includes('storage/app/ssh/keys/')) { - return false; - } - return /^[^@]+@[^@]+$/.test(arg); - }); - if (!userAtHost) return null; - - // Extract host from user@host - const host = userAtHost.split('@')[1]; - return host; -} - async function handleError(err, userId) { - console.error('WebSocket error:', err); + logTerminal('error', 'WebSocket error.', { + userId, + error: err?.message ?? err, + }); await killPtyProcess(userId); } async function handleClose(userId) { + logTerminal('log', 'Cleaning up terminal websocket session.', { + userId, + }); await killPtyProcess(userId); userSessions.delete(userId); } @@ -231,6 +353,11 @@ async function killPtyProcess(userId) { const attemptKill = () => { killAttempts++; + logTerminal('log', 'Attempting to terminate PTY process.', { + userId, + killAttempts, + maxAttempts, + }); // session.ptyProcess.kill() wont work here because of https://github.com/moby/moby/issues/9098 // patch with https://github.com/moby/moby/issues/9098#issuecomment-189743947 @@ -238,6 +365,10 @@ async function killPtyProcess(userId) { setTimeout(() => { if (!session.isActive || !session.ptyProcess) { + logTerminal('log', 'PTY process terminated successfully.', { + userId, + killAttempts, + }); resolve(true); return; } @@ -245,6 +376,10 @@ async function killPtyProcess(userId) { if (killAttempts < maxAttempts) { attemptKill(); } else { + logTerminal('warn', 'PTY process still active after maximum termination attempts.', { + userId, + killAttempts, + }); resolve(false); } }, 500); @@ -258,76 +393,8 @@ function generateUserId() { return Math.random().toString(36).substring(2, 11); } -function extractTimeout(commandString) { - const timeoutMatch = commandString.match(/timeout (\d+)/); - return timeoutMatch ? parseInt(timeoutMatch[1], 10) : null; -} - -function extractSshArgs(commandString) { - const sshCommandMatch = commandString.match(/ssh (.+?) 'bash -se'/); - if (!sshCommandMatch) return []; - - const argsString = sshCommandMatch[1]; - let sshArgs = []; - - // Parse shell arguments respecting quotes - let current = ''; - let inQuotes = false; - let quoteChar = ''; - let i = 0; - - while (i < argsString.length) { - const char = argsString[i]; - const nextChar = argsString[i + 1]; - - if (!inQuotes && (char === '"' || char === "'")) { - // Starting a quoted section - inQuotes = true; - quoteChar = char; - current += char; - } else if (inQuotes && char === quoteChar) { - // Ending a quoted section - inQuotes = false; - current += char; - quoteChar = ''; - } else if (!inQuotes && char === ' ') { - // Space outside quotes - end of argument - if (current.trim()) { - sshArgs.push(current.trim()); - current = ''; - } - } else { - // Regular character - current += char; - } - i++; - } - - // Add final argument if exists - if (current.trim()) { - sshArgs.push(current.trim()); - } - - // Replace RequestTTY=no with RequestTTY=yes - sshArgs = sshArgs.map(arg => arg === 'RequestTTY=no' ? 'RequestTTY=yes' : arg); - - // Add RequestTTY=yes if not present - if (!sshArgs.includes('RequestTTY=yes') && !sshArgs.some(arg => arg.includes('RequestTTY='))) { - sshArgs.push('-o', 'RequestTTY=yes'); - } - - return sshArgs; -} - -function extractHereDocContent(commandString) { - const delimiterMatch = commandString.match(/<< (\S+)/); - const delimiter = delimiterMatch ? delimiterMatch[1] : null; - const escapedDelimiter = delimiter.slice(1).trim().replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&'); - const hereDocRegex = new RegExp(`<< \\\\${escapedDelimiter}([\\s\\S\\.]*?)${escapedDelimiter}`); - const hereDocMatch = commandString.match(hereDocRegex); - return hereDocMatch ? hereDocMatch[1] : ''; -} - server.listen(6002, () => { - console.log('Coolify realtime terminal server listening on port 6002. Let the hacking begin!'); + logTerminal('log', 'Terminal debug logging is enabled.', { + terminalDebugEnabled, + }); }); diff --git a/docker/coolify-realtime/terminal-utils.js b/docker/coolify-realtime/terminal-utils.js new file mode 100644 index 000000000..7456b282c --- /dev/null +++ b/docker/coolify-realtime/terminal-utils.js @@ -0,0 +1,127 @@ +export function extractTimeout(commandString) { + const timeoutMatch = commandString.match(/timeout (\d+)/); + return timeoutMatch ? parseInt(timeoutMatch[1], 10) : null; +} + +function normalizeShellArgument(argument) { + if (!argument) { + return argument; + } + + return argument + .replace(/'([^']*)'/g, '$1') + .replace(/"([^"]*)"/g, '$1'); +} + +export function extractSshArgs(commandString) { + const sshCommandMatch = commandString.match(/ssh (.+?) 'bash -se'/); + if (!sshCommandMatch) return []; + + const argsString = sshCommandMatch[1]; + let sshArgs = []; + + let current = ''; + let inQuotes = false; + let quoteChar = ''; + let i = 0; + + while (i < argsString.length) { + const char = argsString[i]; + + if (!inQuotes && (char === '"' || char === "'")) { + inQuotes = true; + quoteChar = char; + current += char; + } else if (inQuotes && char === quoteChar) { + inQuotes = false; + current += char; + quoteChar = ''; + } else if (!inQuotes && char === ' ') { + if (current.trim()) { + sshArgs.push(current.trim()); + current = ''; + } + } else { + current += char; + } + i++; + } + + if (current.trim()) { + sshArgs.push(current.trim()); + } + + sshArgs = sshArgs.map((arg) => normalizeShellArgument(arg)); + sshArgs = sshArgs.map(arg => arg === 'RequestTTY=no' ? 'RequestTTY=yes' : arg); + + if (!sshArgs.includes('RequestTTY=yes') && !sshArgs.some(arg => arg.includes('RequestTTY='))) { + sshArgs.push('-o', 'RequestTTY=yes'); + } + + return sshArgs; +} + +export function extractHereDocContent(commandString) { + const delimiterMatch = commandString.match(/<< (\S+)/); + const delimiter = delimiterMatch ? delimiterMatch[1] : null; + const escapedDelimiter = delimiter?.slice(1).trim().replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&'); + + if (!escapedDelimiter) { + return ''; + } + + const hereDocRegex = new RegExp(`<< \\\\${escapedDelimiter}([\\s\\S\\.]*?)${escapedDelimiter}`); + const hereDocMatch = commandString.match(hereDocRegex); + return hereDocMatch ? hereDocMatch[1] : ''; +} + +export function normalizeHostForAuthorization(host) { + if (!host) { + return null; + } + + let normalizedHost = host.trim(); + + while ( + normalizedHost.length >= 2 && + ((normalizedHost.startsWith("'") && normalizedHost.endsWith("'")) || + (normalizedHost.startsWith('"') && normalizedHost.endsWith('"'))) + ) { + normalizedHost = normalizedHost.slice(1, -1).trim(); + } + + if (normalizedHost.startsWith('[') && normalizedHost.endsWith(']')) { + normalizedHost = normalizedHost.slice(1, -1); + } + + return normalizedHost.toLowerCase(); +} + +export function extractTargetHost(sshArgs) { + const userAtHost = sshArgs.find(arg => { + if (arg.includes('storage/app/ssh/keys/')) { + return false; + } + + return /^[^@]+@[^@]+$/.test(arg); + }); + + if (!userAtHost) { + return null; + } + + const atIndex = userAtHost.indexOf('@'); + return normalizeHostForAuthorization(userAtHost.slice(atIndex + 1)); +} + +export function isAuthorizedTargetHost(targetHost, authorizedHosts = []) { + const normalizedTargetHost = normalizeHostForAuthorization(targetHost); + + if (!normalizedTargetHost) { + return false; + } + + return authorizedHosts + .map(host => normalizeHostForAuthorization(host)) + .includes(normalizedTargetHost); +} diff --git a/docker/coolify-realtime/terminal-utils.test.js b/docker/coolify-realtime/terminal-utils.test.js new file mode 100644 index 000000000..3da444155 --- /dev/null +++ b/docker/coolify-realtime/terminal-utils.test.js @@ -0,0 +1,47 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { + extractSshArgs, + extractTargetHost, + isAuthorizedTargetHost, + normalizeHostForAuthorization, +} from './terminal-utils.js'; + +test('extractTargetHost normalizes quoted IPv4 hosts from generated ssh commands', () => { + const sshArgs = extractSshArgs( + "timeout 3600 ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -o ServerAliveInterval=20 -o ConnectTimeout=10 'root'@'10.0.0.5' 'bash -se' << \\\\$abc\necho hi\nabc" + ); + + assert.equal(extractTargetHost(sshArgs), '10.0.0.5'); +}); + +test('extractSshArgs strips shell quotes from port and user host arguments before spawning ssh', () => { + const sshArgs = extractSshArgs( + "timeout 3600 ssh -p '22' -o StrictHostKeyChecking=no 'root'@'10.0.0.5' 'bash -se' << \\\\$abc\necho hi\nabc" + ); + + assert.deepEqual(sshArgs.slice(0, 5), ['-p', '22', '-o', 'StrictHostKeyChecking=no', 'root@10.0.0.5']); +}); + +test('extractSshArgs preserves proxy command as a single normalized ssh option value', () => { + const sshArgs = extractSshArgs( + "timeout 3600 ssh -o ProxyCommand='cloudflared access ssh --hostname %h' -o StrictHostKeyChecking=no 'root'@'example.com' 'bash -se' << \\\\$abc\necho hi\nabc" + ); + + assert.equal(sshArgs[1], 'ProxyCommand=cloudflared access ssh --hostname %h'); + assert.equal(sshArgs[4], 'root@example.com'); +}); + +test('isAuthorizedTargetHost matches normalized hosts against plain allowlist values', () => { + assert.equal(isAuthorizedTargetHost("'10.0.0.5'", ['10.0.0.5']), true); + assert.equal(isAuthorizedTargetHost('"host.docker.internal"', ['host.docker.internal']), true); +}); + +test('normalizeHostForAuthorization unwraps bracketed IPv6 hosts', () => { + assert.equal(normalizeHostForAuthorization("'[2001:db8::10]'"), '2001:db8::10'); + assert.equal(isAuthorizedTargetHost("'[2001:db8::10]'", ['2001:db8::10']), true); +}); + +test('isAuthorizedTargetHost rejects hosts that are not in the allowlist', () => { + assert.equal(isAuthorizedTargetHost("'10.0.0.9'", ['10.0.0.5']), false); +}); diff --git a/resources/js/terminal.js b/resources/js/terminal.js index 6707bec98..3c52edfa0 100644 --- a/resources/js/terminal.js +++ b/resources/js/terminal.js @@ -2,6 +2,16 @@ import { Terminal } from '@xterm/xterm'; import '@xterm/xterm/css/xterm.css'; import { FitAddon } from '@xterm/addon-fit'; +const terminalDebugEnabled = import.meta.env.DEV; + +function logTerminal(level, message, ...context) { + if (!terminalDebugEnabled) { + return; + } + + console[level](message, ...context); +} + export function initializeTerminalComponent() { function terminalData() { return { @@ -30,6 +40,8 @@ export function initializeTerminalComponent() { pingTimeoutId: null, heartbeatMissed: 0, maxHeartbeatMisses: 3, + // Command buffering for race condition prevention + pendingCommand: null, // Resize handling resizeObserver: null, resizeTimeout: null, @@ -120,6 +132,7 @@ export function initializeTerminalComponent() { this.checkIfProcessIsRunningAndKillIt(); this.clearAllTimers(); this.connectionState = 'disconnected'; + this.pendingCommand = null; if (this.socket) { this.socket.close(1000, 'Client cleanup'); } @@ -154,6 +167,7 @@ export function initializeTerminalComponent() { this.pendingWrites = 0; this.paused = false; this.commandBuffer = ''; + this.pendingCommand = null; // Notify parent component that terminal disconnected this.$wire.dispatch('terminalDisconnected'); @@ -188,7 +202,7 @@ export function initializeTerminalComponent() { initializeWebSocket() { if (this.socket && this.socket.readyState !== WebSocket.CLOSED) { - console.log('[Terminal] WebSocket already connecting/connected, skipping'); + logTerminal('log', '[Terminal] WebSocket already connecting/connected, skipping'); return; // Already connecting or connected } @@ -197,7 +211,7 @@ export function initializeTerminalComponent() { // Ensure terminal config is available if (!window.terminalConfig) { - console.warn('[Terminal] Terminal config not available, using defaults'); + logTerminal('warn', '[Terminal] Terminal config not available, using defaults'); window.terminalConfig = {}; } @@ -223,7 +237,7 @@ export function initializeTerminalComponent() { } const url = `${connectionString.protocol}://${connectionString.host}${connectionString.port}${connectionString.path}` - console.log(`[Terminal] Attempting connection to: ${url}`); + logTerminal('log', `[Terminal] Attempting connection to: ${url}`); try { this.socket = new WebSocket(url); @@ -232,7 +246,7 @@ export function initializeTerminalComponent() { const timeoutMs = this.reconnectAttempts === 0 ? 15000 : this.connectionTimeout; this.connectionTimeoutId = setTimeout(() => { if (this.connectionState === 'connecting') { - console.error(`[Terminal] Connection timeout after ${timeoutMs}ms`); + logTerminal('error', `[Terminal] Connection timeout after ${timeoutMs}ms`); this.socket.close(); this.handleConnectionError('Connection timeout'); } @@ -244,13 +258,13 @@ export function initializeTerminalComponent() { this.socket.onclose = this.handleSocketClose.bind(this); } catch (error) { - console.error('[Terminal] Failed to create WebSocket:', error); + logTerminal('error', '[Terminal] Failed to create WebSocket:', error); this.handleConnectionError(`Failed to create WebSocket connection: ${error.message}`); } }, handleSocketOpen() { - console.log('[Terminal] WebSocket connection established. Cool cool cool cool cool cool.'); + logTerminal('log', '[Terminal] WebSocket connection established.'); this.connectionState = 'connected'; this.reconnectAttempts = 0; this.heartbeatMissed = 0; @@ -262,6 +276,12 @@ export function initializeTerminalComponent() { this.connectionTimeoutId = null; } + // Flush any buffered command from before WebSocket was ready + if (this.pendingCommand) { + this.sendMessage(this.pendingCommand); + this.pendingCommand = null; + } + // Start ping timeout monitoring this.resetPingTimeout(); @@ -270,16 +290,16 @@ export function initializeTerminalComponent() { }, handleSocketError(error) { - console.error('[Terminal] WebSocket error:', error); - console.error('[Terminal] WebSocket state:', this.socket ? this.socket.readyState : 'No socket'); - console.error('[Terminal] Connection attempt:', this.reconnectAttempts + 1); + logTerminal('error', '[Terminal] WebSocket error:', error); + logTerminal('error', '[Terminal] WebSocket state:', this.socket ? this.socket.readyState : 'No socket'); + logTerminal('error', '[Terminal] Connection attempt:', this.reconnectAttempts + 1); this.handleConnectionError('WebSocket error occurred'); }, handleSocketClose(event) { - console.warn(`[Terminal] WebSocket connection closed. Code: ${event.code}, Reason: ${event.reason || 'No reason provided'}`); - console.log('[Terminal] Was clean close:', event.code === 1000); - console.log('[Terminal] Connection attempt:', this.reconnectAttempts + 1); + logTerminal('warn', `[Terminal] WebSocket connection closed. Code: ${event.code}, Reason: ${event.reason || 'No reason provided'}`); + logTerminal('log', '[Terminal] Was clean close:', event.code === 1000); + logTerminal('log', '[Terminal] Connection attempt:', this.reconnectAttempts + 1); this.connectionState = 'disconnected'; this.clearAllTimers(); @@ -297,7 +317,7 @@ export function initializeTerminalComponent() { }, handleConnectionError(reason) { - console.error(`[Terminal] Connection error: ${reason} (attempt ${this.reconnectAttempts + 1})`); + logTerminal('error', `[Terminal] Connection error: ${reason} (attempt ${this.reconnectAttempts + 1})`); this.connectionState = 'disconnected'; // Only dispatch error to UI after a few failed attempts to avoid immediate error on page load @@ -310,7 +330,7 @@ export function initializeTerminalComponent() { scheduleReconnect() { if (this.reconnectAttempts >= this.maxReconnectAttempts) { - console.error('[Terminal] Max reconnection attempts reached'); + logTerminal('error', '[Terminal] Max reconnection attempts reached'); this.message = '(connection failed - max retries exceeded)'; return; } @@ -323,7 +343,7 @@ export function initializeTerminalComponent() { this.maxReconnectDelay ); - console.warn(`[Terminal] Scheduling reconnect attempt ${this.reconnectAttempts + 1} in ${delay}ms`); + logTerminal('warn', `[Terminal] Scheduling reconnect attempt ${this.reconnectAttempts + 1} in ${delay}ms`); this.reconnectInterval = setTimeout(() => { this.reconnectAttempts++; @@ -335,17 +355,21 @@ export function initializeTerminalComponent() { if (this.socket && this.socket.readyState === WebSocket.OPEN) { this.socket.send(JSON.stringify(message)); } else { - console.warn('[Terminal] WebSocket not ready, message not sent:', message); + logTerminal('warn', '[Terminal] WebSocket not ready, message not sent:', message); } }, sendCommandWhenReady(message) { if (this.isWebSocketReady()) { this.sendMessage(message); + } else { + this.pendingCommand = message; } }, handleSocketMessage(event) { + logTerminal('log', '[Terminal] Received WebSocket message:', event.data); + // Handle pong responses if (event.data === 'pong') { this.heartbeatMissed = 0; @@ -354,6 +378,10 @@ export function initializeTerminalComponent() { return; } + if (!this.term?._initialized && event.data !== 'pty-ready') { + logTerminal('warn', '[Terminal] Received message before PTY initialization:', event.data); + } + if (event.data === 'pty-ready') { if (!this.term._initialized) { this.term.open(document.getElementById('terminal')); @@ -398,17 +426,24 @@ export function initializeTerminalComponent() { // Notify parent component that terminal disconnected this.$wire.dispatch('terminalDisconnected'); + } else if ( + typeof event.data === 'string' && + (event.data.startsWith('Unauthorized:') || event.data.startsWith('Invalid SSH command:')) + ) { + logTerminal('error', '[Terminal] Backend rejected terminal startup:', event.data); + this.$wire.dispatch('error', event.data); + this.terminalActive = false; } else { try { this.pendingWrites++; this.term.write(event.data, (err) => { if (err) { - console.error('[Terminal] Write error:', err); + logTerminal('error', '[Terminal] Write error:', err); } this.flowControlCallback(); }); } catch (error) { - console.error('[Terminal] Write operation failed:', error); + logTerminal('error', '[Terminal] Write operation failed:', error); this.pendingWrites = Math.max(0, this.pendingWrites - 1); } } @@ -483,10 +518,10 @@ export function initializeTerminalComponent() { clearTimeout(this.pingTimeoutId); this.pingTimeoutId = null; } - console.log('[Terminal] Tab hidden, pausing heartbeat monitoring'); + logTerminal('log', '[Terminal] Tab hidden, pausing heartbeat monitoring'); } else if (wasVisible === false) { // Tab is now visible again - console.log('[Terminal] Tab visible, resuming connection management'); + logTerminal('log', '[Terminal] Tab visible, resuming connection management'); if (this.wasConnectedBeforeHidden && this.socket && this.socket.readyState === WebSocket.OPEN) { // Send immediate ping to verify connection is still alive @@ -508,10 +543,10 @@ export function initializeTerminalComponent() { this.pingTimeoutId = setTimeout(() => { this.heartbeatMissed++; - console.warn(`[Terminal] Ping timeout - missed ${this.heartbeatMissed}/${this.maxHeartbeatMisses}`); + logTerminal('warn', `[Terminal] Ping timeout - missed ${this.heartbeatMissed}/${this.maxHeartbeatMisses}`); if (this.heartbeatMissed >= this.maxHeartbeatMisses) { - console.error('[Terminal] Too many missed heartbeats, closing connection'); + logTerminal('error', '[Terminal] Too many missed heartbeats, closing connection'); this.socket.close(1001, 'Heartbeat timeout'); } }, this.pingTimeout); @@ -553,7 +588,7 @@ export function initializeTerminalComponent() { // Check if dimensions are valid if (height <= 0 || width <= 0) { - console.warn('[Terminal] Invalid wrapper dimensions, retrying...', { height, width }); + logTerminal('warn', '[Terminal] Invalid wrapper dimensions, retrying...', { height, width }); setTimeout(() => this.resizeTerminal(), 100); return; } @@ -562,7 +597,7 @@ export function initializeTerminalComponent() { if (!charSize.height || !charSize.width) { // Fallback values if char size not available yet - console.warn('[Terminal] Character size not available, retrying...'); + logTerminal('warn', '[Terminal] Character size not available, retrying...'); setTimeout(() => this.resizeTerminal(), 100); return; } @@ -583,10 +618,10 @@ export function initializeTerminalComponent() { }); } } else { - console.warn('[Terminal] Invalid calculated dimensions:', { rows, cols, height, width, charSize }); + logTerminal('warn', '[Terminal] Invalid calculated dimensions:', { rows, cols, height, width, charSize }); } } catch (error) { - console.error('[Terminal] Resize error:', error); + logTerminal('error', '[Terminal] Resize error:', error); } }, diff --git a/resources/views/livewire/project/shared/execute-container-command.blade.php b/resources/views/livewire/project/shared/execute-container-command.blade.php index f980d6f3c..e7d3546fd 100644 --- a/resources/views/livewire/project/shared/execute-container-command.blade.php +++ b/resources/views/livewire/project/shared/execute-container-command.blade.php @@ -21,7 +21,8 @@
No containers are running or terminal access is disabled on this server.
@else
diff --git a/routes/web.php b/routes/web.php index b6c6c95ce..26863aa17 100644 --- a/routes/web.php +++ b/routes/web.php @@ -168,9 +168,23 @@ Route::post('/terminal/auth/ips', function () { if (auth()->check()) { $team = auth()->user()->currentTeam(); - $ipAddresses = $team->servers->where('settings.is_terminal_enabled', true)->pluck('ip')->toArray(); + $ipAddresses = $team->servers + ->where('settings.is_terminal_enabled', true) + ->pluck('ip') + ->filter() + ->values(); - return response()->json(['ipAddresses' => $ipAddresses], 200); + if (isDev()) { + $ipAddresses = $ipAddresses->merge([ + 'coolify-testing-host', + 'host.docker.internal', + 'localhost', + '127.0.0.1', + base_ip(), + ])->filter()->unique()->values(); + } + + return response()->json(['ipAddresses' => $ipAddresses->all()], 200); } return response()->json(['ipAddresses' => []], 401); diff --git a/tests/Feature/RealtimeTerminalPackagingTest.php b/tests/Feature/RealtimeTerminalPackagingTest.php new file mode 100644 index 000000000..e8fa5ff76 --- /dev/null +++ b/tests/Feature/RealtimeTerminalPackagingTest.php @@ -0,0 +1,34 @@ +toContain('COPY docker/coolify-realtime/terminal-utils.js /terminal/terminal-utils.js'); +}); + +it('mounts the realtime terminal utilities in local development compose files', function (string $composeFile) { + $composeContents = file_get_contents(base_path($composeFile)); + + expect($composeContents)->toContain('./docker/coolify-realtime/terminal-utils.js:/terminal/terminal-utils.js'); +})->with([ + 'default dev compose' => 'docker-compose.dev.yml', + 'maxio dev compose' => 'docker-compose-maxio.dev.yml', +]); + +it('keeps terminal browser logging restricted to Vite development mode', function () { + $terminalClient = file_get_contents(base_path('resources/js/terminal.js')); + + expect($terminalClient) + ->toContain('const terminalDebugEnabled = import.meta.env.DEV;') + ->toContain("logTerminal('log', '[Terminal] WebSocket connection established.');") + ->not->toContain("console.log('[Terminal] WebSocket connection established. Cool cool cool cool cool cool.');"); +}); + +it('keeps realtime terminal server logging restricted to development environments', function () { + $terminalServer = file_get_contents(base_path('docker/coolify-realtime/terminal-server.js')); + + expect($terminalServer) + ->toContain("const terminalDebugEnabled = ['local', 'development'].includes(") + ->toContain('if (!terminalDebugEnabled) {') + ->not->toContain("console.log('Coolify realtime terminal server listening on port 6002. Let the hacking begin!');"); +}); diff --git a/tests/Feature/TerminalAuthIpsRouteTest.php b/tests/Feature/TerminalAuthIpsRouteTest.php new file mode 100644 index 000000000..d4e51ad6c --- /dev/null +++ b/tests/Feature/TerminalAuthIpsRouteTest.php @@ -0,0 +1,51 @@ +set('app.env', 'local'); + + $this->user = User::factory()->create(); + $this->team = Team::factory()->create(); + $this->user->teams()->attach($this->team, ['role' => 'owner']); + $this->actingAs($this->user); + session(['currentTeam' => $this->team]); + + $this->privateKey = PrivateKey::create([ + 'name' => 'Test Key', + 'private_key' => '-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACBbhpqHhqv6aI67Mj9abM3DVbmcfYhZAhC7ca4d9UCevAAAAJi/QySHv0Mk +hwAAAAtzc2gtZWQyNTUxOQAAACBbhpqHhqv6aI67Mj9abM3DVbmcfYhZAhC7ca4d9UCevA +AAAECBQw4jg1WRT2IGHMncCiZhURCts2s24HoDS0thHnnRKVuGmoeGq/pojrsyP1pszcNV +uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA== +-----END OPENSSH PRIVATE KEY-----', + 'team_id' => $this->team->id, + ]); +}); + +it('includes development terminal host aliases for authenticated users', function () { + Server::factory()->create([ + 'name' => 'Localhost', + 'ip' => 'coolify-testing-host', + 'team_id' => $this->team->id, + 'private_key_id' => $this->privateKey->id, + ]); + + $response = $this->postJson('/terminal/auth/ips'); + + $response->assertSuccessful(); + $response->assertJsonPath('ipAddresses.0', 'coolify-testing-host'); + + expect($response->json('ipAddresses')) + ->toContain('coolify-testing-host') + ->toContain('localhost') + ->toContain('127.0.0.1') + ->toContain('host.docker.internal'); +}); From 1d3dfe4dc86cc0701341515d3f1cb7fbcdb12f9b Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 10 Mar 2026 20:40:49 +0100 Subject: [PATCH 15/17] chore(version): bump coolify, realtime, and sentinel versions --- config/constants.php | 4 ++-- versions.json | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/config/constants.php b/config/constants.php index be41c4618..85322a928 100644 --- a/config/constants.php +++ b/config/constants.php @@ -2,9 +2,9 @@ return [ 'coolify' => [ - 'version' => '4.0.0-beta.464', + 'version' => '4.0.0-beta.465', 'helper_version' => '1.0.12', - 'realtime_version' => '1.0.10', + 'realtime_version' => '1.0.11', 'self_hosted' => env('SELF_HOSTED', true), 'autoupdate' => env('AUTOUPDATE'), 'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'), diff --git a/versions.json b/versions.json index 7409fbc42..77c228847 100644 --- a/versions.json +++ b/versions.json @@ -1,19 +1,19 @@ { "coolify": { "v4": { - "version": "4.0.0-beta.464" + "version": "4.0.0-beta.465" }, "nightly": { - "version": "4.0.0-beta.465" + "version": "4.0.0-beta.466" }, "helper": { "version": "1.0.12" }, "realtime": { - "version": "1.0.10" + "version": "1.0.11" }, "sentinel": { - "version": "0.0.18" + "version": "0.0.19" } }, "traefik": { @@ -26,4 +26,4 @@ "v3.0": "3.0.4", "v2.11": "2.11.32" } -} \ No newline at end of file +} From b71d1561f3ca6df89dbb20ce991f7092d1900fe9 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 10 Mar 2026 21:07:14 +0100 Subject: [PATCH 16/17] chore(realtime): upgrade npm dependencies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update dependencies in coolify-realtime package: - @xterm/addon-fit 0.10.0 → 0.11.0 - @xterm/xterm 5.5.0 → 6.0.0 - axios 1.12.0 → 1.13.6 - cookie 1.0.2 → 1.1.1 - dotenv 16.5.0 → 17.3.1 - node-pty 1.0.0 → 1.1.0 (now uses node-addon-api instead of nan) - ws 8.18.1 → 8.19.0 --- docker/coolify-realtime/package-lock.json | 96 ++++++++++++----------- docker/coolify-realtime/package.json | 14 ++-- 2 files changed, 57 insertions(+), 53 deletions(-) diff --git a/docker/coolify-realtime/package-lock.json b/docker/coolify-realtime/package-lock.json index c445c972c..1c49ff930 100644 --- a/docker/coolify-realtime/package-lock.json +++ b/docker/coolify-realtime/package-lock.json @@ -5,29 +5,29 @@ "packages": { "": { "dependencies": { - "@xterm/addon-fit": "0.10.0", - "@xterm/xterm": "5.5.0", - "axios": "1.12.0", - "cookie": "1.0.2", - "dotenv": "16.5.0", - "node-pty": "1.0.0", - "ws": "8.18.1" + "@xterm/addon-fit": "0.11.0", + "@xterm/xterm": "6.0.0", + "axios": "1.13.6", + "cookie": "1.1.1", + "dotenv": "17.3.1", + "node-pty": "1.1.0", + "ws": "8.19.0" } }, "node_modules/@xterm/addon-fit": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz", - "integrity": "sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==", - "license": "MIT", - "peerDependencies": { - "@xterm/xterm": "^5.0.0" - } + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.11.0.tgz", + "integrity": "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==", + "license": "MIT" }, "node_modules/@xterm/xterm": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", - "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", - "license": "MIT" + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.0.0.tgz", + "integrity": "sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg==", + "license": "MIT", + "workspaces": [ + "addons/*" + ] }, "node_modules/asynckit": { "version": "0.4.0", @@ -36,13 +36,13 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.0.tgz", - "integrity": "sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg==", + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, @@ -72,12 +72,16 @@ } }, "node_modules/cookie": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", - "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", "license": "MIT", "engines": { "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/delayed-stream": { @@ -90,9 +94,9 @@ } }, "node_modules/dotenv": { - "version": "16.5.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", - "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", + "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -161,9 +165,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", "funding": [ { "type": "individual", @@ -181,9 +185,9 @@ } }, "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -323,20 +327,20 @@ "node": ">= 0.6" } }, - "node_modules/nan": { - "version": "2.23.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.23.0.tgz", - "integrity": "sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==", + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", "license": "MIT" }, "node_modules/node-pty": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.0.0.tgz", - "integrity": "sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.1.0.tgz", + "integrity": "sha512-20JqtutY6JPXTUnL0ij1uad7Qe1baT46lyolh2sSENDd4sTzKZ4nmAFkeAARDKwmlLjPx6XKRlwRUxwjOy+lUg==", "hasInstallScript": true, "license": "MIT", "dependencies": { - "nan": "^2.17.0" + "node-addon-api": "^7.1.0" } }, "node_modules/proxy-from-env": { @@ -346,9 +350,9 @@ "license": "MIT" }, "node_modules/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", "license": "MIT", "engines": { "node": ">=10.0.0" diff --git a/docker/coolify-realtime/package.json b/docker/coolify-realtime/package.json index aec3dbe3d..ebb7122c8 100644 --- a/docker/coolify-realtime/package.json +++ b/docker/coolify-realtime/package.json @@ -2,12 +2,12 @@ "private": true, "type": "module", "dependencies": { - "@xterm/addon-fit": "0.10.0", - "@xterm/xterm": "5.5.0", - "cookie": "1.0.2", - "axios": "1.12.0", - "dotenv": "16.5.0", - "node-pty": "1.0.0", - "ws": "8.18.1" + "@xterm/addon-fit": "0.11.0", + "@xterm/xterm": "6.0.0", + "cookie": "1.1.1", + "axios": "1.13.6", + "dotenv": "17.3.1", + "node-pty": "1.1.0", + "ws": "8.19.0" } } \ No newline at end of file From 473371e7edf5548b9b5aedb37ac4a438b22a2827 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 10 Mar 2026 21:14:30 +0100 Subject: [PATCH 17/17] chore(realtime): upgrade coolify-realtime to 1.0.11 --- docker-compose.prod.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index d42047245..0bd4ae2dd 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -60,7 +60,7 @@ services: retries: 10 timeout: 2s soketi: - image: '${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-realtime:1.0.10' + image: '${REGISTRY_URL:-ghcr.io}/coollabsio/coolify-realtime:1.0.11' ports: - "${SOKETI_PORT:-6001}:6001" - "6002:6002"