From 96d0d39fd8bf2ff1826a238daba7ecb68242f04e Mon Sep 17 00:00:00 2001 From: thevinodpatidar Date: Thu, 30 Oct 2025 16:35:22 +0530 Subject: [PATCH 01/25] Add Postgresus one-click service template --- public/svgs/postgresus.svg | 1 + templates/compose/postgresus.yaml | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 public/svgs/postgresus.svg create mode 100644 templates/compose/postgresus.yaml diff --git a/public/svgs/postgresus.svg b/public/svgs/postgresus.svg new file mode 100644 index 000000000..a45e81167 --- /dev/null +++ b/public/svgs/postgresus.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/templates/compose/postgresus.yaml b/templates/compose/postgresus.yaml new file mode 100644 index 000000000..8c71ae163 --- /dev/null +++ b/templates/compose/postgresus.yaml @@ -0,0 +1,20 @@ +# documentation: https://postgresus.com +# slogan: Postgresus is a free, open source and self-hosted tool to backup PostgreSQL. +# category: devtools +# tags: postgres,backup,self-hosted,open-source +# logo: svgs/postgresus.svg +# port: 4005 + +services: + postgresus: + image: rostislavdugin/postgresus:latest + environment: + - SERVICE_URL_POSTGRESUS_4005 + volumes: + - postgresus-data:/postgresus-data + healthcheck: + test: + ["CMD", "wget", "-qO-", "http://localhost:4005/api/v1/system/health"] + interval: 5s + timeout: 10s + retries: 5 From b131a89d030c671f12bf17dcc6dd13e3b68ac1ce Mon Sep 17 00:00:00 2001 From: thevinodpatidar Date: Thu, 30 Oct 2025 16:58:14 +0530 Subject: [PATCH 02/25] Add Postgresus service template files --- templates/service-templates-latest.json | 15 +++++++++++++++ templates/service-templates.json | 15 +++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/templates/service-templates-latest.json b/templates/service-templates-latest.json index dfabce600..5bee88282 100644 --- a/templates/service-templates-latest.json +++ b/templates/service-templates-latest.json @@ -3375,6 +3375,21 @@ "minversion": "0.0.0", "port": "9000" }, + "postgresus": { + "documentation": "https://postgresus.com?utm_source=coolify.io", + "slogan": "Postgresus is a free, open source and self-hosted tool to backup PostgreSQL.", + "compose": "c2VydmljZXM6CiAgcG9zdGdyZXN1czoKICAgIGltYWdlOiAncm9zdGlzbGF2ZHVnaW4vcG9zdGdyZXN1czpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9QT1NUR1JFU1VTXzQwMDUKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3Bvc3RncmVzdXMtZGF0YTovcG9zdGdyZXN1cy1kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHdnZXQKICAgICAgICAtICctcU8tJwogICAgICAgIC0gJ2h0dHA6Ly9sb2NhbGhvc3Q6NDAwNS9hcGkvdjEvc3lzdGVtL2hlYWx0aCcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiA1Cg==", + "tags": [ + "postgres", + "backup", + "self-hosted", + "open-source" + ], + "category": "devtools", + "logo": "svgs/postgresus.svg", + "minversion": "0.0.0", + "port": "4005" + }, "postiz": { "documentation": "https://docs.postiz.com?utm_source=coolify.io", "slogan": "Open source social media scheduling tool.", diff --git a/templates/service-templates.json b/templates/service-templates.json index 3d49b1620..0d35416b8 100644 --- a/templates/service-templates.json +++ b/templates/service-templates.json @@ -3375,6 +3375,21 @@ "minversion": "0.0.0", "port": "9000" }, + "postgresus": { + "documentation": "https://postgresus.com?utm_source=coolify.io", + "slogan": "Postgresus is a free, open source and self-hosted tool to backup PostgreSQL.", + "compose": "c2VydmljZXM6CiAgcG9zdGdyZXN1czoKICAgIGltYWdlOiAncm9zdGlzbGF2ZHVnaW4vcG9zdGdyZXN1czpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fUE9TVEdSRVNVU180MDA1CiAgICB2b2x1bWVzOgogICAgICAtICdwb3N0Z3Jlc3VzLWRhdGE6L3Bvc3RncmVzdXMtZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSB3Z2V0CiAgICAgICAgLSAnLXFPLScKICAgICAgICAtICdodHRwOi8vbG9jYWxob3N0OjQwMDUvYXBpL3YxL3N5c3RlbS9oZWFsdGgnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogNQo=", + "tags": [ + "postgres", + "backup", + "self-hosted", + "open-source" + ], + "category": "devtools", + "logo": "svgs/postgresus.svg", + "minversion": "0.0.0", + "port": "4005" + }, "postiz": { "documentation": "https://docs.postiz.com?utm_source=coolify.io", "slogan": "Open source social media scheduling tool.", From 3be0dc07b8f3b1d5900053de2e23a578588d4202 Mon Sep 17 00:00:00 2001 From: thevinodpatidar Date: Fri, 31 Oct 2025 11:00:41 +0530 Subject: [PATCH 03/25] Change version and documentation url --- templates/compose/postgresus.yaml | 6 +++--- templates/service-templates-latest.json | 8 +++----- templates/service-templates.json | 8 +++----- 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/templates/compose/postgresus.yaml b/templates/compose/postgresus.yaml index 8c71ae163..a3a8a55e9 100644 --- a/templates/compose/postgresus.yaml +++ b/templates/compose/postgresus.yaml @@ -1,13 +1,13 @@ -# documentation: https://postgresus.com +# documentation: https://postgresus.com/#guide # slogan: Postgresus is a free, open source and self-hosted tool to backup PostgreSQL. # category: devtools -# tags: postgres,backup,self-hosted,open-source +# tags: postgres,backup # logo: svgs/postgresus.svg # port: 4005 services: postgresus: - image: rostislavdugin/postgresus:latest + image: rostislavdugin/postgresus:7fb59bb5d02fbaf856b0bcfc7a0786575818b96f # Released on 30 Sep, 2025 environment: - SERVICE_URL_POSTGRESUS_4005 volumes: diff --git a/templates/service-templates-latest.json b/templates/service-templates-latest.json index 5bee88282..3633a306f 100644 --- a/templates/service-templates-latest.json +++ b/templates/service-templates-latest.json @@ -3376,14 +3376,12 @@ "port": "9000" }, "postgresus": { - "documentation": "https://postgresus.com?utm_source=coolify.io", + "documentation": "https://postgresus.com/#guide?utm_source=coolify.io", "slogan": "Postgresus is a free, open source and self-hosted tool to backup PostgreSQL.", - "compose": "c2VydmljZXM6CiAgcG9zdGdyZXN1czoKICAgIGltYWdlOiAncm9zdGlzbGF2ZHVnaW4vcG9zdGdyZXN1czpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9QT1NUR1JFU1VTXzQwMDUKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3Bvc3RncmVzdXMtZGF0YTovcG9zdGdyZXN1cy1kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHdnZXQKICAgICAgICAtICctcU8tJwogICAgICAgIC0gJ2h0dHA6Ly9sb2NhbGhvc3Q6NDAwNS9hcGkvdjEvc3lzdGVtL2hlYWx0aCcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiA1Cg==", + "compose": "c2VydmljZXM6CiAgcG9zdGdyZXN1czoKICAgIGltYWdlOiAncm9zdGlzbGF2ZHVnaW4vcG9zdGdyZXN1czo3ZmI1OWJiNWQwMmZiYWY4NTZiMGJjZmM3YTA3ODY1NzU4MThiOTZmJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9VUkxfUE9TVEdSRVNVU180MDA1CiAgICB2b2x1bWVzOgogICAgICAtICdwb3N0Z3Jlc3VzLWRhdGE6L3Bvc3RncmVzdXMtZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSB3Z2V0CiAgICAgICAgLSAnLXFPLScKICAgICAgICAtICdodHRwOi8vbG9jYWxob3N0OjQwMDUvYXBpL3YxL3N5c3RlbS9oZWFsdGgnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogNQo=", "tags": [ "postgres", - "backup", - "self-hosted", - "open-source" + "backup" ], "category": "devtools", "logo": "svgs/postgresus.svg", diff --git a/templates/service-templates.json b/templates/service-templates.json index 0d35416b8..0d356c5ed 100644 --- a/templates/service-templates.json +++ b/templates/service-templates.json @@ -3376,14 +3376,12 @@ "port": "9000" }, "postgresus": { - "documentation": "https://postgresus.com?utm_source=coolify.io", + "documentation": "https://postgresus.com/#guide?utm_source=coolify.io", "slogan": "Postgresus is a free, open source and self-hosted tool to backup PostgreSQL.", - "compose": "c2VydmljZXM6CiAgcG9zdGdyZXN1czoKICAgIGltYWdlOiAncm9zdGlzbGF2ZHVnaW4vcG9zdGdyZXN1czpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fUE9TVEdSRVNVU180MDA1CiAgICB2b2x1bWVzOgogICAgICAtICdwb3N0Z3Jlc3VzLWRhdGE6L3Bvc3RncmVzdXMtZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSB3Z2V0CiAgICAgICAgLSAnLXFPLScKICAgICAgICAtICdodHRwOi8vbG9jYWxob3N0OjQwMDUvYXBpL3YxL3N5c3RlbS9oZWFsdGgnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogNQo=", + "compose": "c2VydmljZXM6CiAgcG9zdGdyZXN1czoKICAgIGltYWdlOiAncm9zdGlzbGF2ZHVnaW4vcG9zdGdyZXN1czo3ZmI1OWJiNWQwMmZiYWY4NTZiMGJjZmM3YTA3ODY1NzU4MThiOTZmJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX1BPU1RHUkVTVVNfNDAwNQogICAgdm9sdW1lczoKICAgICAgLSAncG9zdGdyZXN1cy1kYXRhOi9wb3N0Z3Jlc3VzLWRhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gd2dldAogICAgICAgIC0gJy1xTy0nCiAgICAgICAgLSAnaHR0cDovL2xvY2FsaG9zdDo0MDA1L2FwaS92MS9zeXN0ZW0vaGVhbHRoJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDUK", "tags": [ "postgres", - "backup", - "self-hosted", - "open-source" + "backup" ], "category": "devtools", "logo": "svgs/postgresus.svg", From 468d5fe7d77dfe1f1f34770a81e45062c272c92d Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 7 Nov 2025 14:03:19 +0100 Subject: [PATCH 04/25] refactor: improve docker compose validation and transaction handling in StackForm --- app/Livewire/Project/Service/StackForm.php | 25 ++++++++++------ bootstrap/helpers/parsers.php | 30 +++++++++++++------- tests/Unit/VolumeArrayFormatSecurityTest.php | 30 ++++++++++++++++++++ tests/Unit/VolumeSecurityTest.php | 21 ++++++++++++++ 4 files changed, 88 insertions(+), 18 deletions(-) diff --git a/app/Livewire/Project/Service/StackForm.php b/app/Livewire/Project/Service/StackForm.php index 85cd21a7f..8a7b6e090 100644 --- a/app/Livewire/Project/Service/StackForm.php +++ b/app/Livewire/Project/Service/StackForm.php @@ -5,6 +5,7 @@ use App\Models\Service; use App\Support\ValidationPatterns; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\DB; use Livewire\Component; class StackForm extends Component @@ -22,7 +23,7 @@ class StackForm extends Component public string $dockerComposeRaw; - public string $dockerCompose; + public ?string $dockerCompose = null; public ?bool $connectToDockerNetwork = null; @@ -30,7 +31,7 @@ protected function rules(): array { $baseRules = [ 'dockerComposeRaw' => 'required', - 'dockerCompose' => 'required', + 'dockerCompose' => 'nullable', 'name' => ValidationPatterns::nameRules(), 'description' => ValidationPatterns::descriptionRules(), 'connectToDockerNetwork' => 'nullable', @@ -140,18 +141,26 @@ public function submit($notify = true) $this->validate(); $this->syncData(true); - // Validate for command injection BEFORE saving to database + // Validate for command injection BEFORE any database operations validateDockerComposeForInjection($this->service->docker_compose_raw); - $this->service->save(); - $this->service->saveExtraFields($this->fields); - $this->service->parse(); - $this->service->refresh(); - $this->service->saveComposeConfigs(); + // Use transaction to ensure atomicity - if parse fails, save is rolled back + DB::transaction(function () { + $this->service->save(); + $this->service->saveExtraFields($this->fields); + $this->service->parse(); + $this->service->refresh(); + $this->service->saveComposeConfigs(); + }); + $this->dispatch('refreshEnvs'); $this->dispatch('refreshServices'); $notify && $this->dispatch('success', 'Service saved.'); } catch (\Throwable $e) { + // On error, refresh from database to restore clean state + $this->service->refresh(); + $this->syncData(false); + return handleError($e, $this); } finally { if (is_null($this->service->config_hash)) { diff --git a/bootstrap/helpers/parsers.php b/bootstrap/helpers/parsers.php index 1deec45d7..a210aa1cc 100644 --- a/bootstrap/helpers/parsers.php +++ b/bootstrap/helpers/parsers.php @@ -59,11 +59,13 @@ function validateDockerComposeForInjection(string $composeYaml): void if (isset($volume['source'])) { $source = $volume['source']; if (is_string($source)) { - // Allow simple env vars and env vars with defaults (validated in parseDockerVolumeString) + // Allow env vars and env vars with defaults (validated in parseDockerVolumeString) + // Also allow env vars followed by safe path concatenation (e.g., ${VAR}/path) $isSimpleEnvVar = preg_match('/^\$\{[a-zA-Z_][a-zA-Z0-9_]*\}$/', $source); $isEnvVarWithDefault = preg_match('/^\$\{[^}]+:-[^}]*\}$/', $source); + $isEnvVarWithPath = preg_match('/^\$\{[a-zA-Z_][a-zA-Z0-9_]*\}[\/\w\.\-]*$/', $source); - if (! $isSimpleEnvVar && ! $isEnvVarWithDefault) { + if (! $isSimpleEnvVar && ! $isEnvVarWithDefault && ! $isEnvVarWithPath) { try { validateShellSafePath($source, 'volume source'); } catch (\Exception $e) { @@ -310,15 +312,17 @@ function parseDockerVolumeString(string $volumeString): array // Validate source path for command injection attempts // We validate the final source value after environment variable processing if ($source !== null) { - // Allow simple environment variables like ${VAR_NAME} or ${VAR} - // but validate everything else for shell metacharacters + // Allow environment variables like ${VAR_NAME} or ${VAR} + // Also allow env vars followed by safe path concatenation (e.g., ${VAR}/path) $sourceStr = is_string($source) ? $source : $source; // Skip validation for simple environment variable references - // Pattern: ${WORD_CHARS} with no special characters inside + // Pattern 1: ${WORD_CHARS} with no special characters inside + // Pattern 2: ${WORD_CHARS}/path/to/file (env var with path concatenation) $isSimpleEnvVar = preg_match('/^\$\{[a-zA-Z_][a-zA-Z0-9_]*\}$/', $sourceStr); + $isEnvVarWithPath = preg_match('/^\$\{[a-zA-Z_][a-zA-Z0-9_]*\}[\/\w\.\-]*$/', $sourceStr); - if (! $isSimpleEnvVar) { + if (! $isSimpleEnvVar && ! $isEnvVarWithPath) { try { validateShellSafePath($sourceStr, 'volume source'); } catch (\Exception $e) { @@ -711,9 +715,12 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int // Validate source and target for command injection (array/long syntax) if ($source !== null && ! empty($source->value())) { $sourceValue = $source->value(); - // Allow simple environment variable references + // Allow environment variable references and env vars with path concatenation $isSimpleEnvVar = preg_match('/^\$\{[a-zA-Z_][a-zA-Z0-9_]*\}$/', $sourceValue); - if (! $isSimpleEnvVar) { + $isEnvVarWithDefault = preg_match('/^\$\{[^}]+:-[^}]*\}$/', $sourceValue); + $isEnvVarWithPath = preg_match('/^\$\{[a-zA-Z_][a-zA-Z0-9_]*\}[\/\w\.\-]*$/', $sourceValue); + + if (! $isSimpleEnvVar && ! $isEnvVarWithDefault && ! $isEnvVarWithPath) { try { validateShellSafePath($sourceValue, 'volume source'); } catch (\Exception $e) { @@ -1812,9 +1819,12 @@ function serviceParser(Service $resource): Collection // Validate source and target for command injection (array/long syntax) if ($source !== null && ! empty($source->value())) { $sourceValue = $source->value(); - // Allow simple environment variable references + // Allow environment variable references and env vars with path concatenation $isSimpleEnvVar = preg_match('/^\$\{[a-zA-Z_][a-zA-Z0-9_]*\}$/', $sourceValue); - if (! $isSimpleEnvVar) { + $isEnvVarWithDefault = preg_match('/^\$\{[^}]+:-[^}]*\}$/', $sourceValue); + $isEnvVarWithPath = preg_match('/^\$\{[a-zA-Z_][a-zA-Z0-9_]*\}[\/\w\.\-]*$/', $sourceValue); + + if (! $isSimpleEnvVar && ! $isEnvVarWithDefault && ! $isEnvVarWithPath) { try { validateShellSafePath($sourceValue, 'volume source'); } catch (\Exception $e) { diff --git a/tests/Unit/VolumeArrayFormatSecurityTest.php b/tests/Unit/VolumeArrayFormatSecurityTest.php index 97a6819b2..08174fff3 100644 --- a/tests/Unit/VolumeArrayFormatSecurityTest.php +++ b/tests/Unit/VolumeArrayFormatSecurityTest.php @@ -194,6 +194,36 @@ ->not->toThrow(Exception::class); }); +test('array-format with environment variable and path concatenation', function () { + // This is the reported issue #7127 - ${VAR}/path should be allowed + $dockerComposeYaml = <<<'YAML' +services: + web: + image: nginx + volumes: + - type: bind + source: '${VOLUMES_PATH}/mysql' + target: /var/lib/mysql + - type: bind + source: '${DATA_PATH}/config' + target: /etc/config + - type: bind + source: '${VOLUME_PATH}/app_data' + target: /app/data +YAML; + + $parsed = Yaml::parse($dockerComposeYaml); + + // Verify all three volumes have the correct source format + expect($parsed['services']['web']['volumes'][0]['source'])->toBe('${VOLUMES_PATH}/mysql'); + expect($parsed['services']['web']['volumes'][1]['source'])->toBe('${DATA_PATH}/config'); + expect($parsed['services']['web']['volumes'][2]['source'])->toBe('${VOLUME_PATH}/app_data'); + + // The validation should allow this - the reported bug was that it was blocked + expect(fn () => validateDockerComposeForInjection($dockerComposeYaml)) + ->not->toThrow(Exception::class); +}); + test('array-format with malicious environment variable default', function () { $dockerComposeYaml = <<<'YAML' services: diff --git a/tests/Unit/VolumeSecurityTest.php b/tests/Unit/VolumeSecurityTest.php index d7f20fc0e..f4cd6c268 100644 --- a/tests/Unit/VolumeSecurityTest.php +++ b/tests/Unit/VolumeSecurityTest.php @@ -94,6 +94,27 @@ } }); +test('parseDockerVolumeString accepts environment variables with path concatenation', function () { + $volumes = [ + '${VOLUMES_PATH}/mysql:/var/lib/mysql', + '${DATA_PATH}/config:/etc/config', + '${VOLUME_PATH}/app_data:/app', + '${MY_VAR_123}/deep/nested/path:/data', + '${VAR}/path:/app', + '${VAR}_suffix:/app', + '${VAR}-suffix:/app', + '${VAR}.ext:/app', + '${VOLUMES_PATH}/mysql:/var/lib/mysql:ro', + '${DATA_PATH}/config:/etc/config:rw', + ]; + + foreach ($volumes as $volume) { + $result = parseDockerVolumeString($volume); + expect($result)->toBeArray(); + expect($result['source'])->not->toBeNull(); + } +}); + test('parseDockerVolumeString rejects environment variables with command injection in default', function () { $maliciousVolumes = [ '${VAR:-`whoami`}:/app', From 049affe216c2afcf995ec3e2ed3a0fc548b156d3 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 7 Nov 2025 14:04:09 +0100 Subject: [PATCH 05/25] refactor: rename onWorktreeCreate script to setup in jean.json --- jean.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/jean.json b/jean.json index c625e08c0..c81ca07c4 100644 --- a/jean.json +++ b/jean.json @@ -1,6 +1,5 @@ { "scripts": { - "onWorktreeCreate": "cp $JEAN_ROOT_PATH/.env . && mkdir -p .claude && cp $JEAN_ROOT_PATH/.claude/settings.local.json .claude/settings.local.json", - "run": "docker rm -f coolify coolify-realtime coolify-minio coolify-testing-host coolify-redis coolify-db coolify-mail coolify-vite && spin up" + "setup": "cp $JEAN_ROOT_PATH/.env . && mkdir -p .claude && cp $JEAN_ROOT_PATH/.claude/settings.local.json .claude/settings.local.json", } } From e86575d6f7ca8471d70766ec4c55fd1058e3db26 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 7 Nov 2025 14:14:43 +0100 Subject: [PATCH 06/25] fix: guard against null or empty docker compose in saveComposeConfigs method --- app/Models/Service.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/Models/Service.php b/app/Models/Service.php index 12d3d6a11..ef755d105 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -1287,6 +1287,11 @@ public function workdir() public function saveComposeConfigs() { + // Guard against null or empty docker_compose + if (! $this->docker_compose) { + return; + } + $workdir = $this->workdir(); instant_remote_process([ From 712d60c75b5db2cad57906c9a71fb3c6538fa29c Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 7 Nov 2025 15:19:57 +0100 Subject: [PATCH 07/25] feat: ensure .env file exists for docker compose and auto-inject in payloads --- app/Actions/Service/StartService.php | 4 ++++ app/Jobs/ApplicationDeploymentJob.php | 6 ++++++ bootstrap/helpers/parsers.php | 6 ++++++ 3 files changed, 16 insertions(+) diff --git a/app/Actions/Service/StartService.php b/app/Actions/Service/StartService.php index dfef6a566..50011c74f 100644 --- a/app/Actions/Service/StartService.php +++ b/app/Actions/Service/StartService.php @@ -22,6 +22,10 @@ public function handle(Service $service, bool $pullLatestImages = false, bool $s $service->isConfigurationChanged(save: true); $commands[] = 'cd '.$service->workdir(); $commands[] = "echo 'Saved configuration files to {$service->workdir()}.'"; + // Ensure .env file exists before docker compose tries to load it + // This is defensive programming - saveComposeConfigs() already creates it, + // but we guarantee it here in case of any edge cases or manual deployments + $commands[] = 'touch .env'; if ($pullLatestImages) { $commands[] = "echo 'Pulling images.'"; $commands[] = 'docker compose pull'; diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index ea8cdff95..0fd007e9a 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -3029,6 +3029,12 @@ private function stop_running_container(bool $force = false) private function start_by_compose_file() { + // Ensure .env file exists before docker compose tries to load it (defensive programming) + $this->execute_remote_command( + ["touch {$this->workdir}/.env", 'hidden' => true], + ["touch {$this->configuration_dir}/.env", 'hidden' => true], + ); + if ($this->application->build_pack === 'dockerimage') { $this->application_deployment_queue->addLogEntry('Pulling latest images from the registry.'); $this->execute_remote_command( diff --git a/bootstrap/helpers/parsers.php b/bootstrap/helpers/parsers.php index a210aa1cc..9b17e6810 100644 --- a/bootstrap/helpers/parsers.php +++ b/bootstrap/helpers/parsers.php @@ -1300,6 +1300,9 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int if ($depends_on->count() > 0) { $payload['depends_on'] = $depends_on; } + // Auto-inject .env file so Coolify environment variables are available inside containers + // This makes Applications behave consistently with manual .env file usage + $payload['env_file'] = ['.env']; if ($isPullRequest) { $serviceName = addPreviewDeploymentSuffix($serviceName, $pullRequestId); } @@ -2279,6 +2282,9 @@ function serviceParser(Service $resource): Collection if ($depends_on->count() > 0) { $payload['depends_on'] = $depends_on; } + // Auto-inject .env file so Coolify environment variables are available inside containers + // This makes Services behave consistently with Applications + $payload['env_file'] = ['.env']; $parsedServices->put($serviceName, $payload); } From 67605d50fc9735cdccda1f71b34979974a7ff065 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sun, 9 Nov 2025 14:41:35 +0100 Subject: [PATCH 08/25] fix(deployment): prevent base deployments from being killed when PRs close (#7113) - Fix container filtering to properly distinguish base deployments (pullRequestId=0) from PR deployments - Add deployment cancellation when PR closes via webhook to prevent race conditions - Prevent CleanupHelperContainersJob from killing active deployment containers - Enhance error messages with exit codes and actual errors instead of vague "Oops" messages - Protect status transitions in finally blocks to ensure proper job failure handling --- app/Http/Controllers/Webhook/Github.php | 89 +++++++++++++++++++++++++ app/Jobs/ApplicationDeploymentJob.php | 50 +++++++++----- app/Jobs/CleanupHelperContainersJob.php | 49 +++++++++++++- app/Jobs/DeleteResourceJob.php | 48 ++++++++++++- app/Traits/ExecuteRemoteCommand.php | 15 ++++- bootstrap/helpers/docker.php | 26 +++++++- templates/service-templates-latest.json | 16 ++--- templates/service-templates.json | 16 ++--- 8 files changed, 269 insertions(+), 40 deletions(-) diff --git a/app/Http/Controllers/Webhook/Github.php b/app/Http/Controllers/Webhook/Github.php index 5ba9c08e7..2aee15a8d 100644 --- a/app/Http/Controllers/Webhook/Github.php +++ b/app/Http/Controllers/Webhook/Github.php @@ -246,6 +246,50 @@ public function manual(Request $request) if ($action === 'closed') { $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); if ($found) { + // Cancel any active deployments for this PR immediately + $activeDeployment = \App\Models\ApplicationDeploymentQueue::where('application_id', $application->id) + ->where('pull_request_id', $pull_request_id) + ->whereIn('status', [ + \App\Enums\ApplicationDeploymentStatus::QUEUED->value, + \App\Enums\ApplicationDeploymentStatus::IN_PROGRESS->value, + ]) + ->first(); + + if ($activeDeployment) { + try { + // Mark deployment as cancelled + $activeDeployment->update([ + 'status' => \App\Enums\ApplicationDeploymentStatus::CANCELLED_BY_USER->value, + ]); + + // Add cancellation log entry + $activeDeployment->addLogEntry('Deployment cancelled: Pull request closed.', 'stderr'); + + // Check if helper container exists and kill it + $deployment_uuid = $activeDeployment->deployment_uuid; + $server = $application->destination->server; + $checkCommand = "docker ps -a --filter name={$deployment_uuid} --format '{{.Names}}'"; + $containerExists = instant_remote_process([$checkCommand], $server); + + if ($containerExists && str($containerExists)->trim()->isNotEmpty()) { + instant_remote_process(["docker rm -f {$deployment_uuid}"], $server); + $activeDeployment->addLogEntry('Deployment container stopped.'); + } + + // Kill running process if process ID exists + if ($activeDeployment->current_process_id) { + try { + $processKillCommand = "kill -9 {$activeDeployment->current_process_id}"; + instant_remote_process([$processKillCommand], $server); + } catch (\Throwable $e) { + // Process might already be gone + } + } + } catch (\Throwable $e) { + // Silently handle errors during deployment cancellation + } + } + DeleteResourceJob::dispatch($found); $return_payloads->push([ 'application' => $application->name, @@ -481,6 +525,51 @@ public function normal(Request $request) if ($action === 'closed' || $action === 'close') { $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); if ($found) { + // Cancel any active deployments for this PR immediately + $activeDeployment = \App\Models\ApplicationDeploymentQueue::where('application_id', $application->id) + ->where('pull_request_id', $pull_request_id) + ->whereIn('status', [ + \App\Enums\ApplicationDeploymentStatus::QUEUED->value, + \App\Enums\ApplicationDeploymentStatus::IN_PROGRESS->value, + ]) + ->first(); + + if ($activeDeployment) { + try { + // Mark deployment as cancelled + $activeDeployment->update([ + 'status' => \App\Enums\ApplicationDeploymentStatus::CANCELLED_BY_USER->value, + ]); + + // Add cancellation log entry + $activeDeployment->addLogEntry('Deployment cancelled: Pull request closed.', 'stderr'); + + // Check if helper container exists and kill it + $deployment_uuid = $activeDeployment->deployment_uuid; + $server = $application->destination->server; + $checkCommand = "docker ps -a --filter name={$deployment_uuid} --format '{{.Names}}'"; + $containerExists = instant_remote_process([$checkCommand], $server); + + if ($containerExists && str($containerExists)->trim()->isNotEmpty()) { + instant_remote_process(["docker rm -f {$deployment_uuid}"], $server); + $activeDeployment->addLogEntry('Deployment container stopped.'); + } + + // Kill running process if process ID exists + if ($activeDeployment->current_process_id) { + try { + $processKillCommand = "kill -9 {$activeDeployment->current_process_id}"; + instant_remote_process([$processKillCommand], $server); + } catch (\Throwable $e) { + // Process might already be gone + } + } + } catch (\Throwable $e) { + // Silently handle errors during deployment cancellation + } + } + + // Clean up any deployed containers $containers = getCurrentApplicationContainerStatus($application->destination->server, $application->id, $pull_request_id); if ($containers->isNotEmpty()) { $containers->each(function ($container) use ($application) { diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index ea8cdff95..67c6b1497 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -341,20 +341,42 @@ public function handle(): void $this->fail($e); throw $e; } finally { - $this->application_deployment_queue->update([ - 'finished_at' => Carbon::now()->toImmutable(), - ]); - - if ($this->use_build_server) { - $this->server = $this->build_server; - } else { - $this->write_deployment_configurations(); + // Wrap cleanup operations in try-catch to prevent exceptions from interfering + // with Laravel's job failure handling and status updates + try { + $this->application_deployment_queue->update([ + 'finished_at' => Carbon::now()->toImmutable(), + ]); + } catch (Exception $e) { + // Log but don't fail - finished_at is not critical + \Log::warning('Failed to update finished_at for deployment '.$this->deployment_uuid.': '.$e->getMessage()); } - $this->application_deployment_queue->addLogEntry("Gracefully shutting down build container: {$this->deployment_uuid}"); - $this->graceful_shutdown_container($this->deployment_uuid); + try { + if ($this->use_build_server) { + $this->server = $this->build_server; + } else { + $this->write_deployment_configurations(); + } + } catch (Exception $e) { + // Log but don't fail - configuration writing errors shouldn't prevent status updates + $this->application_deployment_queue->addLogEntry('Warning: Failed to write deployment configurations: '.$e->getMessage(), 'stderr'); + } - ServiceStatusChanged::dispatch(data_get($this->application, 'environment.project.team.id')); + try { + $this->application_deployment_queue->addLogEntry("Gracefully shutting down build container: {$this->deployment_uuid}"); + $this->graceful_shutdown_container($this->deployment_uuid); + } catch (Exception $e) { + // Log but don't fail - container cleanup errors are expected when container is already gone + \Log::warning('Failed to shutdown container '.$this->deployment_uuid.': '.$e->getMessage()); + } + + try { + ServiceStatusChanged::dispatch(data_get($this->application, 'environment.project.team.id')); + } catch (Exception $e) { + // Log but don't fail - event dispatch errors shouldn't prevent status updates + \Log::warning('Failed to dispatch ServiceStatusChanged for deployment '.$this->deployment_uuid.': '.$e->getMessage()); + } } } @@ -3798,10 +3820,8 @@ private function failDeployment(): void public function failed(Throwable $exception): void { $this->failDeployment(); - $this->application_deployment_queue->addLogEntry('Oops something is not okay, are you okay? 😢', 'stderr'); - if (str($exception->getMessage())->isNotEmpty()) { - $this->application_deployment_queue->addLogEntry($exception->getMessage(), 'stderr'); - } + $errorMessage = $exception->getMessage() ?: 'Unknown error occurred'; + $this->application_deployment_queue->addLogEntry("Deployment failed: {$errorMessage}", 'stderr'); if ($this->application->build_pack !== 'dockercompose') { $code = $exception->getCode(); diff --git a/app/Jobs/CleanupHelperContainersJob.php b/app/Jobs/CleanupHelperContainersJob.php index c82a27ce9..f6f5e8b5b 100644 --- a/app/Jobs/CleanupHelperContainersJob.php +++ b/app/Jobs/CleanupHelperContainersJob.php @@ -2,6 +2,8 @@ namespace App\Jobs; +use App\Enums\ApplicationDeploymentStatus; +use App\Models\ApplicationDeploymentQueue; use App\Models\Server; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldBeEncrypted; @@ -20,10 +22,51 @@ public function __construct(public Server $server) {} public function handle(): void { try { + // Get all active deployments on this server + $activeDeployments = ApplicationDeploymentQueue::where('server_id', $this->server->id) + ->whereIn('status', [ + ApplicationDeploymentStatus::IN_PROGRESS->value, + ApplicationDeploymentStatus::QUEUED->value, + ]) + ->pluck('deployment_uuid') + ->toArray(); + + \Log::info('CleanupHelperContainersJob - Active deployments', [ + 'server' => $this->server->name, + 'active_deployment_uuids' => $activeDeployments, + ]); + $containers = instant_remote_process_with_timeout(['docker container ps --format \'{{json .}}\' | jq -s \'map(select(.Image | contains("'.config('constants.coolify.registry_url').'/coollabsio/coolify-helper")))\''], $this->server, false); - $containerIds = collect(json_decode($containers))->pluck('ID'); - if ($containerIds->count() > 0) { - foreach ($containerIds as $containerId) { + $helperContainers = collect(json_decode($containers)); + + if ($helperContainers->count() > 0) { + foreach ($helperContainers as $container) { + $containerId = data_get($container, 'ID'); + $containerName = data_get($container, 'Names'); + + // Check if this container belongs to an active deployment + $isActiveDeployment = false; + foreach ($activeDeployments as $deploymentUuid) { + if (str_contains($containerName, $deploymentUuid)) { + $isActiveDeployment = true; + break; + } + } + + if ($isActiveDeployment) { + \Log::info('CleanupHelperContainersJob - Skipping active deployment container', [ + 'container' => $containerName, + 'id' => $containerId, + ]); + + continue; + } + + \Log::info('CleanupHelperContainersJob - Removing orphaned helper container', [ + 'container' => $containerName, + 'id' => $containerId, + ]); + instant_remote_process_with_timeout(['docker container rm -f '.$containerId], $this->server, false); } } diff --git a/app/Jobs/DeleteResourceJob.php b/app/Jobs/DeleteResourceJob.php index b9fbebcc9..ad707d357 100644 --- a/app/Jobs/DeleteResourceJob.php +++ b/app/Jobs/DeleteResourceJob.php @@ -124,6 +124,51 @@ private function deleteApplicationPreview() $this->resource->delete(); } + // Cancel any active deployments for this PR (same logic as API cancel_deployment) + $activeDeployment = \App\Models\ApplicationDeploymentQueue::where('application_id', $application->id) + ->where('pull_request_id', $pull_request_id) + ->whereIn('status', [ + \App\Enums\ApplicationDeploymentStatus::QUEUED->value, + \App\Enums\ApplicationDeploymentStatus::IN_PROGRESS->value, + ]) + ->first(); + + if ($activeDeployment) { + try { + // Mark deployment as cancelled + $activeDeployment->update([ + 'status' => \App\Enums\ApplicationDeploymentStatus::CANCELLED_BY_USER->value, + ]); + + // Add cancellation log entry + $activeDeployment->addLogEntry('Deployment cancelled: Pull request closed.', 'stderr'); + + // Check if helper container exists and kill it + $deployment_uuid = $activeDeployment->deployment_uuid; + $checkCommand = "docker ps -a --filter name={$deployment_uuid} --format '{{.Names}}'"; + $containerExists = instant_remote_process([$checkCommand], $server); + + if ($containerExists && str($containerExists)->trim()->isNotEmpty()) { + instant_remote_process(["docker rm -f {$deployment_uuid}"], $server); + $activeDeployment->addLogEntry('Deployment container stopped.'); + } else { + $activeDeployment->addLogEntry('Helper container not yet started. Deployment will be cancelled when job checks status.'); + } + + // Kill running process if process ID exists + if ($activeDeployment->current_process_id) { + try { + $processKillCommand = "kill -9 {$activeDeployment->current_process_id}"; + instant_remote_process([$processKillCommand], $server); + } catch (\Throwable $e) { + // Process might already be gone + } + } + } catch (\Throwable $e) { + // Silently handle errors during deployment cancellation + } + } + try { if ($server->isSwarm()) { instant_remote_process(["docker stack rm {$application->uuid}-{$pull_request_id}"], $server); @@ -133,7 +178,7 @@ private function deleteApplicationPreview() } } catch (\Throwable $e) { // Log the error but don't fail the job - ray('Error stopping preview containers: '.$e->getMessage()); + \Log::warning('Error stopping preview containers for application '.$application->uuid.', PR #'.$pull_request_id.': '.$e->getMessage()); } // Finally, force delete to trigger resource cleanup @@ -156,7 +201,6 @@ private function stopPreviewContainers(array $containers, $server, int $timeout "docker stop --time=$timeout $containerList", "docker rm -f $containerList", ]; - instant_remote_process( command: $commands, server: $server, diff --git a/app/Traits/ExecuteRemoteCommand.php b/app/Traits/ExecuteRemoteCommand.php index 4aa5aae8b..58ae5f249 100644 --- a/app/Traits/ExecuteRemoteCommand.php +++ b/app/Traits/ExecuteRemoteCommand.php @@ -219,9 +219,22 @@ private function executeCommandWithProcess($command, $hidden, $customType, $appe $process_result = $process->wait(); if ($process_result->exitCode() !== 0) { if (! $ignore_errors) { + // Check if deployment was cancelled while command was running + if (isset($this->application_deployment_queue)) { + $this->application_deployment_queue->refresh(); + if ($this->application_deployment_queue->status === \App\Enums\ApplicationDeploymentStatus::CANCELLED_BY_USER->value) { + throw new \RuntimeException('Deployment cancelled by user', 69420); + } + } + // Don't immediately set to FAILED - let the retry logic handle it // This prevents premature status changes during retryable SSH errors - throw new \RuntimeException($process_result->errorOutput()); + $error = $process_result->errorOutput(); + if (empty($error)) { + $error = $process_result->output() ?: 'Command failed with no error output'; + } + $redactedCommand = $this->redact_sensitive_info($command); + throw new \RuntimeException("Command execution failed (exit code {$process_result->exitCode()}): {$redactedCommand}\nError: {$error}"); } } } diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php index 5bccb50f1..9da622ed3 100644 --- a/bootstrap/helpers/docker.php +++ b/bootstrap/helpers/docker.php @@ -17,13 +17,31 @@ function getCurrentApplicationContainerStatus(Server $server, int $id, ?int $pul if (! $server->isSwarm()) { $containers = instant_remote_process(["docker ps -a --filter='label=coolify.applicationId={$id}' --format '{{json .}}' "], $server); $containers = format_docker_command_output_to_json($containers); + $containers = $containers->map(function ($container) use ($pullRequestId, $includePullrequests) { $labels = data_get($container, 'Labels'); - if (! str($labels)->contains('coolify.pullRequestId=')) { - data_set($container, 'Labels', $labels.",coolify.pullRequestId={$pullRequestId}"); + $containerName = data_get($container, 'Names'); + $hasPrLabel = str($labels)->contains('coolify.pullRequestId='); + $prLabelValue = null; + if ($hasPrLabel) { + preg_match('/coolify\.pullRequestId=(\d+)/', $labels, $matches); + $prLabelValue = $matches[1] ?? null; + } + + // Treat pullRequestId=0 or missing label as base deployment (convention: 0 = no PR) + $isBaseDeploy = ! $hasPrLabel || (int) $prLabelValue === 0; + + // If we're looking for a specific PR and this is a base deployment, exclude it + if ($pullRequestId !== null && $pullRequestId !== 0 && $isBaseDeploy) { + return null; + } + + // If this is a base deployment, include it when not filtering for PRs + if ($isBaseDeploy) { return $container; } + if ($includePullrequests) { return $container; } @@ -34,7 +52,9 @@ function getCurrentApplicationContainerStatus(Server $server, int $id, ?int $pul return null; }); - return $containers->filter(); + $filtered = $containers->filter(); + + return $filtered; } return $containers; diff --git a/templates/service-templates-latest.json b/templates/service-templates-latest.json index 4d365b483..8ae37e5e5 100644 --- a/templates/service-templates-latest.json +++ b/templates/service-templates-latest.json @@ -976,13 +976,13 @@ "slogan": "EmbyStat is a web analytics tool, designed to provide insight into website traffic and user behavior.", "compose": "c2VydmljZXM6CiAgZW1ieXN0YXQ6CiAgICBpbWFnZTogJ2xzY3IuaW8vbGludXhzZXJ2ZXIvZW1ieXN0YXQ6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9VUkxfRU1CWVNUQVRfNjU1NQogICAgICAtIFBVSUQ9MTAwMAogICAgICAtIFBHSUQ9MTAwMAogICAgICAtIFRaPUV1cm9wZS9NYWRyaWQKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2VtYnlzdGF0LWNvbmZpZzovY29uZmlnJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjY1NTUnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK", "tags": [ - "media", - "server", - "movies", - "tv", - "music" + "analytics", + "insights", + "statistics", + "web", + "traffic" ], - "category": "media", + "category": "analytics", "logo": "svgs/default.webp", "minversion": "0.0.0", "port": "6555" @@ -3664,7 +3664,7 @@ "rybbit": { "documentation": "https://rybbit.io/docs?utm_source=coolify.io", "slogan": "Open-source, privacy-first web analytics.", - "compose": "c2VydmljZXM6CiAgcnliYml0OgogICAgaW1hZ2U6ICdnaGNyLmlvL3J5YmJpdC1pby9yeWJiaXQtY2xpZW50OnYxLjYuMScKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX1JZQkJJVF8zMDAyCiAgICAgIC0gTk9ERV9FTlY9cHJvZHVjdGlvbgogICAgICAtICdORVhUX1BVQkxJQ19CQUNLRU5EX1VSTD0ke1NFUlZJQ0VfVVJMX1JZQkJJVH0nCiAgICAgIC0gJ05FWFRfUFVCTElDX0RJU0FCTEVfU0lHTlVQPSR7RElTQUJMRV9TSUdOVVA6LWZhbHNlfScKICAgIGRlcGVuZHNfb246CiAgICAgIC0gcnliYml0X2JhY2tlbmQKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAnbmMgLXogMTI3LjAuMC4xIDMwMDInCiAgICAgIGludGVydmFsOiAzMHMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDMKICAgICAgc3RhcnRfcGVyaW9kOiAxMHMKICByeWJiaXRfYmFja2VuZDoKICAgIGltYWdlOiAnZ2hjci5pby9yeWJiaXQtaW8vcnliYml0LWJhY2tlbmQ6djEuNi4xJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gTk9ERV9FTlY9cHJvZHVjdGlvbgogICAgICAtIFRSVVNUX1BST1hZPXRydWUKICAgICAgLSAnQkFTRV9VUkw9JHtTRVJWSUNFX1VSTF9SWUJCSVR9JwogICAgICAtICdDTElDS0hPVVNFX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9DTElDS0hPVVNFfScKICAgICAgLSAnUE9TVEdSRVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfScKICAgICAgLSAnQkVUVEVSX0FVVEhfU0VDUkVUPSR7U0VSVklDRV9CQVNFNjRfNjRfQkFDS0VORH0nCiAgICAgIC0gJ0RJU0FCTEVfU0lHTlVQPSR7RElTQUJMRV9TSUdOVVA6LWZhbHNlfScKICAgICAgLSAnRElTQUJMRV9URUxFTUVUUlk9JHtESVNBQkxFX1RFTEVNRVRSWTotdHJ1ZX0nCiAgICAgIC0gJ0NMSUNLSE9VU0VfSE9TVD1odHRwOi8vcnliYml0X2NsaWNraG91c2U6ODEyMycKICAgICAgLSAnQ0xJQ0tIT1VTRV9VU0VSPSR7Q0xJQ0tIT1VTRV9VU0VSOi1kZWZhdWx0fScKICAgICAgLSAnQ0xJQ0tIT1VTRV9EQj0ke0NMSUNLSE9VU0VfREI6LWFuYWx5dGljc30nCiAgICAgIC0gUE9TVEdSRVNfSE9TVD1yeWJiaXRfcG9zdGdyZXMKICAgICAgLSBQT1NUR1JFU19QT1JUPTU0MzIKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotYW5hbHl0aWNzfScKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1BPU1RHUkVTX1VTRVI6LWZyb2d9JwogICAgZGVwZW5kc19vbjoKICAgICAgcnliYml0X2NsaWNraG91c2U6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcnliYml0X3Bvc3RncmVzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gd2dldAogICAgICAgIC0gJy0tbm8tdmVyYm9zZScKICAgICAgICAtICctLXRyaWVzPTEnCiAgICAgICAgLSAnLS1zcGlkZXInCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTozMDAxL2FwaS9oZWFsdGgnCiAgICAgIGludGVydmFsOiAzMHMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDMKICAgICAgc3RhcnRfcGVyaW9kOiAxMHMKICByeWJiaXRfcG9zdGdyZXM6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE3LjQnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotYW5hbHl0aWNzfScKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1BPU1RHUkVTX1VTRVI6LWZyb2d9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgdm9sdW1lczoKICAgICAgLSAncG9zdGdyZXNfZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogMzBzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAzCiAgcnliYml0X2NsaWNraG91c2U6CiAgICBpbWFnZTogJ2NsaWNraG91c2UvY2xpY2tob3VzZS1zZXJ2ZXI6MjUuNC4yJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0NMSUNLSE9VU0VfREI9JHtDTElDS0hPVVNFX0RCOi1hbmFseXRpY3N9JwogICAgICAtICdDTElDS0hPVVNFX1VTRVI9JHtDTElDS0hPVVNFX1VTRVI6LWRlZmF1bHR9JwogICAgICAtICdDTElDS0hPVVNFX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9DTElDS0hPVVNFfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSB3Z2V0CiAgICAgICAgLSAnLS1uby12ZXJib3NlJwogICAgICAgIC0gJy0tdHJpZXM9MScKICAgICAgICAtICctLXNwaWRlcicKICAgICAgICAtICdodHRwOi8vbG9jYWxob3N0OjgxMjMvcGluZycKICAgICAgaW50ZXJ2YWw6IDMwcwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMwogICAgICBzdGFydF9wZXJpb2Q6IDEwcwogICAgdm9sdW1lczoKICAgICAgLSAnY2xpY2tob3VzZV9kYXRhOi92YXIvbGliL2NsaWNraG91c2UnCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2NsaWNraG91c2VfY29uZmlnL2VuYWJsZV9qc29uLnhtbAogICAgICAgIHRhcmdldDogL2V0Yy9jbGlja2hvdXNlLXNlcnZlci9jb25maWcuZC9lbmFibGVfanNvbi54bWwKICAgICAgICBjb250ZW50OiAiPGNsaWNraG91c2U+XG4gICAgPHNldHRpbmdzPlxuICAgICAgICA8ZW5hYmxlX2pzb25fdHlwZT4xPC9lbmFibGVfanNvbl90eXBlPlxuICAgIDwvc2V0dGluZ3M+XG48L2NsaWNraG91c2U+XG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2NsaWNraG91c2VfY29uZmlnL2xvZ2dpbmdfcnVsZXMueG1sCiAgICAgICAgdGFyZ2V0OiAvZXRjL2NsaWNraG91c2Utc2VydmVyL2NvbmZpZy5kL2xvZ2dpbmdfcnVsZXMueG1sCiAgICAgICAgY29udGVudDogIjxjbGlja2hvdXNlPlxuICAgIDxsb2dnZXI+XG4gICAgICAgIDxsZXZlbD53YXJuaW5nPC9sZXZlbD5cbiAgICAgICAgPGNvbnNvbGU+dHJ1ZTwvY29uc29sZT5cbiAgICA8L2xvZ2dlcj5cbiAgICA8cXVlcnlfdGhyZWFkX2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG4gICAgPHF1ZXJ5X2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG4gICAgPHRleHRfbG9nIHJlbW92ZT1cInJlbW92ZVwiLz5cbiAgICA8dHJhY2VfbG9nIHJlbW92ZT1cInJlbW92ZVwiLz5cbiAgICA8bWV0cmljX2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG4gICAgPGFzeW5jaHJvbm91c19tZXRyaWNfbG9nIHJlbW92ZT1cInJlbW92ZVwiLz5cbiAgICA8c2Vzc2lvbl9sb2cgcmVtb3ZlPVwicmVtb3ZlXCIvPlxuICAgIDxwYXJ0X2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG4gICAgPGxhdGVuY3lfbG9nIHJlbW92ZT1cInJlbW92ZVwiLz5cbiAgICA8cHJvY2Vzc29yc19wcm9maWxlX2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG48L2NsaWNraG91c2U+IgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9jbGlja2hvdXNlX2NvbmZpZy9uZXR3b3JrLnhtbAogICAgICAgIHRhcmdldDogL2V0Yy9jbGlja2hvdXNlLXNlcnZlci9jb25maWcuZC9uZXR3b3JrLnhtbAogICAgICAgIGNvbnRlbnQ6ICI8Y2xpY2tob3VzZT5cbiAgICA8bGlzdGVuX2hvc3Q+MC4wLjAuMDwvbGlzdGVuX2hvc3Q+XG48L2NsaWNraG91c2U+XG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2NsaWNraG91c2VfY29uZmlnL3VzZXJfbG9nZ2luZy54bWwKICAgICAgICB0YXJnZXQ6IC9ldGMvY2xpY2tob3VzZS1zZXJ2ZXIvY29uZmlnLmQvdXNlcl9sb2dnaW5nLnhtbAogICAgICAgIGNvbnRlbnQ6ICI8Y2xpY2tob3VzZT5cbiAgICA8cHJvZmlsZXM+XG4gICAgICAgIDxkZWZhdWx0PlxuICAgICAgICAgICAgPGxvZ19xdWVyaWVzPjA8L2xvZ19xdWVyaWVzPlxuICAgICAgICAgICAgPGxvZ19xdWVyeV90aHJlYWRzPjA8L2xvZ19xdWVyeV90aHJlYWRzPlxuICAgICAgICAgICAgPGxvZ19wcm9jZXNzb3JzX3Byb2ZpbGVzPjA8L2xvZ19wcm9jZXNzb3JzX3Byb2ZpbGVzPlxuICAgICAgICA8L2RlZmF1bHQ+XG4gICAgPC9wcm9maWxlcz5cbjwvY2xpY2tob3VzZT4iCg==", + "compose": "c2VydmljZXM6CiAgcnliYml0OgogICAgaW1hZ2U6ICdnaGNyLmlvL3J5YmJpdC1pby9yeWJiaXQtY2xpZW50OnYxLjYuMScKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX1JZQkJJVF8zMDAyCiAgICAgIC0gTk9ERV9FTlY9cHJvZHVjdGlvbgogICAgICAtICdORVhUX1BVQkxJQ19CQUNLRU5EX1VSTD0ke1NFUlZJQ0VfVVJMX1JZQkJJVH0nCiAgICAgIC0gJ05FWFRfUFVCTElDX0RJU0FCTEVfU0lHTlVQPSR7RElTQUJMRV9TSUdOVVA6LWZhbHNlfScKICAgIGRlcGVuZHNfb246CiAgICAgIC0gcnliYml0X2JhY2tlbmQKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAnbmMgLXogMTI3LjAuMC4xIDMwMDInCiAgICAgIGludGVydmFsOiAzMHMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDMKICAgICAgc3RhcnRfcGVyaW9kOiAxMHMKICByeWJiaXRfYmFja2VuZDoKICAgIGltYWdlOiAnZ2hjci5pby9yeWJiaXQtaW8vcnliYml0LWJhY2tlbmQ6djEuNi4xJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gTk9ERV9FTlY9cHJvZHVjdGlvbgogICAgICAtIFRSVVNUX1BST1hZPXRydWUKICAgICAgLSAnQkFTRV9VUkw9JHtTRVJWSUNFX1VSTF9SWUJCSVR9JwogICAgICAtICdDTElDS0hPVVNFX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9DTElDS0hPVVNFfScKICAgICAgLSAnUE9TVEdSRVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfScKICAgICAgLSAnQkVUVEVSX0FVVEhfU0VDUkVUPSR7U0VSVklDRV9CQVNFNjRfNjRfQkFDS0VORH0nCiAgICAgIC0gJ0RJU0FCTEVfU0lHTlVQPSR7RElTQUJMRV9TSUdOVVA6LWZhbHNlfScKICAgICAgLSAnRElTQUJMRV9URUxFTUVUUlk9JHtESVNBQkxFX1RFTEVNRVRSWTotdHJ1ZX0nCiAgICAgIC0gJ0NMSUNLSE9VU0VfSE9TVD1odHRwOi8vcnliYml0X2NsaWNraG91c2U6ODEyMycKICAgICAgLSAnQ0xJQ0tIT1VTRV9VU0VSPSR7Q0xJQ0tIT1VTRV9VU0VSOi1kZWZhdWx0fScKICAgICAgLSAnQ0xJQ0tIT1VTRV9EQj0ke0NMSUNLSE9VU0VfREI6LWFuYWx5dGljc30nCiAgICAgIC0gUE9TVEdSRVNfSE9TVD1yeWJiaXRfcG9zdGdyZXMKICAgICAgLSBQT1NUR1JFU19QT1JUPTU0MzIKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotYW5hbHl0aWNzfScKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1BPU1RHUkVTX1VTRVI6LWZyb2d9JwogICAgZGVwZW5kc19vbjoKICAgICAgcnliYml0X2NsaWNraG91c2U6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcnliYml0X3Bvc3RncmVzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gd2dldAogICAgICAgIC0gJy0tbm8tdmVyYm9zZScKICAgICAgICAtICctLXRyaWVzPTEnCiAgICAgICAgLSAnLS1zcGlkZXInCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTozMDAxL2FwaS9oZWFsdGgnCiAgICAgIGludGVydmFsOiAzMHMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDMKICAgICAgc3RhcnRfcGVyaW9kOiAxMHMKICByeWJiaXRfcG9zdGdyZXM6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE3LjQnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotYW5hbHl0aWNzfScKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1BPU1RHUkVTX1VTRVI6LWZyb2d9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgdm9sdW1lczoKICAgICAgLSAncG9zdGdyZXNfZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogMzBzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAzCiAgcnliYml0X2NsaWNraG91c2U6CiAgICBpbWFnZTogJ2NsaWNraG91c2UvY2xpY2tob3VzZS1zZXJ2ZXI6MjUuNC4yJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0NMSUNLSE9VU0VfREI9JHtDTElDS0hPVVNFX0RCOi1hbmFseXRpY3N9JwogICAgICAtICdDTElDS0hPVVNFX1VTRVI9JHtDTElDS0hPVVNFX1VTRVI6LWRlZmF1bHR9JwogICAgICAtICdDTElDS0hPVVNFX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9DTElDS0hPVVNFfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSB3Z2V0CiAgICAgICAgLSAnLS1uby12ZXJib3NlJwogICAgICAgIC0gJy0tdHJpZXM9MScKICAgICAgICAtICctLXNwaWRlcicKICAgICAgICAtICdodHRwOi8vbG9jYWxob3N0OjgxMjMvcGluZycKICAgICAgaW50ZXJ2YWw6IDMwcwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMwogICAgICBzdGFydF9wZXJpb2Q6IDEwcwogICAgdm9sdW1lczoKICAgICAgLSAnY2xpY2tob3VzZV9kYXRhOi92YXIvbGliL2NsaWNraG91c2UnCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2NsaWNraG91c2VfY29uZmlnL2VuYWJsZV9qc29uLnhtbAogICAgICAgIHRhcmdldDogL2V0Yy9jbGlja2hvdXNlLXNlcnZlci9jb25maWcuZC9lbmFibGVfanNvbi54bWwKICAgICAgICBjb250ZW50OiAiPGNsaWNraG91c2U+XG4gICAgPHNldHRpbmdzPlxuICAgICAgICA8ZW5hYmxlX2pzb25fdHlwZT4xPC9lbmFibGVfanNvbl90eXBlPlxuICAgIDwvc2V0dGluZ3M+XG48L2NsaWNraG91c2U+XG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2NsaWNraG91c2VfY29uZmlnL2xvZ2dpbmdfcnVsZXMueG1sCiAgICAgICAgdGFyZ2V0OiAvZXRjL2NsaWNraG91c2Utc2VydmVyL2NvbmZpZy5kL2xvZ2dpbmdfcnVsZXMueG1sCiAgICAgICAgY29udGVudDogIjxjbGlja2hvdXNlPlxuICAgIDxsb2dnZXI+XG4gICAgICAgIDxsZXZlbD53YXJuaW5nPC9sZXZlbD5cbiAgICAgICAgPGNvbnNvbGU+dHJ1ZTwvY29uc29sZT5cbiAgICA8L2xvZ2dlcj5cbiAgICA8cXVlcnlfdGhyZWFkX2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG4gICAgPHF1ZXJ5X2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG4gICAgPHRleHRfbG9nIHJlbW92ZT1cInJlbW92ZVwiLz5cbiAgICA8dHJhY2VfbG9nIHJlbW92ZT1cInJlbW92ZVwiLz5cbiAgICA8bWV0cmljX2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG4gICAgPGFzeW5jaHJvbm91c19tZXRyaWNfbG9nIHJlbW92ZT1cInJlbW92ZVwiLz5cbiAgICA8c2Vzc2lvbl9sb2cgcmVtb3ZlPVwicmVtb3ZlXCIvPlxuICAgIDxwYXJ0X2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG4gICAgPGxhdGVuY3lfbG9nIHJlbW92ZT1cInJlbW92ZVwiLz5cbiAgICA8cHJvY2Vzc29yc19wcm9maWxlX2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG48L2NsaWNraG91c2U+IgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9jbGlja2hvdXNlX2NvbmZpZy9uZXR3b3JrLnhtbAogICAgICAgIHRhcmdldDogL2V0Yy9jbGlja2hvdXNlLXNlcnZlci9jb25maWcuZC9uZXR3b3JrLnhtbAogICAgICAgIGNvbnRlbnQ6ICI8Y2xpY2tob3VzZT5cbiAgICA8bGlzdGVuX2hvc3Q+MC4wLjAuMDwvbGlzdGVuX2hvc3Q+XG48L2NsaWNraG91c2U+XG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2NsaWNraG91c2VfY29uZmlnL3VzZXJfbG9nZ2luZy54bWwKICAgICAgICB0YXJnZXQ6IC9ldGMvY2xpY2tob3VzZS1zZXJ2ZXIvY29uZmlnLmQvdXNlcl9sb2dnaW5nLnhtbAogICAgICAgIGNvbnRlbnQ6ICI8Y2xpY2tob3VzZT5cbiAgICA8cHJvZmlsZXM+XG4gICAgICAgIDxkZWZhdWx0PlxuICAgICAgICAgICAgPGxvZ19xdWVyaWVzPjA8L2xvZ19xdWVyaWVzPlxuICAgICAgICAgICAgPGxvZ19xdWVyeV90aHJlYWRzPjA8L2xvZ19xdWVyeV90aHJlYWRzPlxuICAgICAgICAgICAgPGxvZ19wcm9jZXNzb3JzX3Byb2ZpbGVzPjA8L2xvZ19wcm9jZXNzb3JzX3Byb2ZpbGVzPlxuICAgICAgICA8L2RlZmF1bHQ+XG4gICAgPC9wcm9maWxlcz5cbjwvY2xpY2tob3VzZT5cbiIK", "tags": [ "analytics", "web", @@ -3673,7 +3673,7 @@ "clickhouse", "postgres" ], - "category": null, + "category": "analytics", "logo": "svgs/rybbit.svg", "minversion": "0.0.0", "port": "3002" diff --git a/templates/service-templates.json b/templates/service-templates.json index d711b9d95..774b9fba1 100644 --- a/templates/service-templates.json +++ b/templates/service-templates.json @@ -976,13 +976,13 @@ "slogan": "EmbyStat is a web analytics tool, designed to provide insight into website traffic and user behavior.", "compose": "c2VydmljZXM6CiAgZW1ieXN0YXQ6CiAgICBpbWFnZTogJ2xzY3IuaW8vbGludXhzZXJ2ZXIvZW1ieXN0YXQ6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0VNQllTVEFUXzY1NTUKICAgICAgLSBQVUlEPTEwMDAKICAgICAgLSBQR0lEPTEwMDAKICAgICAgLSBUWj1FdXJvcGUvTWFkcmlkCiAgICB2b2x1bWVzOgogICAgICAtICdlbWJ5c3RhdC1jb25maWc6L2NvbmZpZycKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo2NTU1JwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1Cg==", "tags": [ - "media", - "server", - "movies", - "tv", - "music" + "analytics", + "insights", + "statistics", + "web", + "traffic" ], - "category": "media", + "category": "analytics", "logo": "svgs/default.webp", "minversion": "0.0.0", "port": "6555" @@ -3664,7 +3664,7 @@ "rybbit": { "documentation": "https://rybbit.io/docs?utm_source=coolify.io", "slogan": "Open-source, privacy-first web analytics.", - "compose": "c2VydmljZXM6CiAgcnliYml0OgogICAgaW1hZ2U6ICdnaGNyLmlvL3J5YmJpdC1pby9yeWJiaXQtY2xpZW50OnYxLjYuMScKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9SWUJCSVRfMzAwMgogICAgICAtIE5PREVfRU5WPXByb2R1Y3Rpb24KICAgICAgLSAnTkVYVF9QVUJMSUNfQkFDS0VORF9VUkw9JHtTRVJWSUNFX0ZRRE5fUllCQklUfScKICAgICAgLSAnTkVYVF9QVUJMSUNfRElTQUJMRV9TSUdOVVA9JHtESVNBQkxFX1NJR05VUDotZmFsc2V9JwogICAgZGVwZW5kc19vbjoKICAgICAgLSByeWJiaXRfYmFja2VuZAogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICduYyAteiAxMjcuMC4wLjEgMzAwMicKICAgICAgaW50ZXJ2YWw6IDMwcwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMwogICAgICBzdGFydF9wZXJpb2Q6IDEwcwogIHJ5YmJpdF9iYWNrZW5kOgogICAgaW1hZ2U6ICdnaGNyLmlvL3J5YmJpdC1pby9yeWJiaXQtYmFja2VuZDp2MS42LjEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBOT0RFX0VOVj1wcm9kdWN0aW9uCiAgICAgIC0gVFJVU1RfUFJPWFk9dHJ1ZQogICAgICAtICdCQVNFX1VSTD0ke1NFUlZJQ0VfRlFETl9SWUJCSVR9JwogICAgICAtICdDTElDS0hPVVNFX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9DTElDS0hPVVNFfScKICAgICAgLSAnUE9TVEdSRVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfScKICAgICAgLSAnQkVUVEVSX0FVVEhfU0VDUkVUPSR7U0VSVklDRV9CQVNFNjRfNjRfQkFDS0VORH0nCiAgICAgIC0gJ0RJU0FCTEVfU0lHTlVQPSR7RElTQUJMRV9TSUdOVVA6LWZhbHNlfScKICAgICAgLSAnRElTQUJMRV9URUxFTUVUUlk9JHtESVNBQkxFX1RFTEVNRVRSWTotdHJ1ZX0nCiAgICAgIC0gJ0NMSUNLSE9VU0VfSE9TVD1odHRwOi8vcnliYml0X2NsaWNraG91c2U6ODEyMycKICAgICAgLSAnQ0xJQ0tIT1VTRV9VU0VSPSR7Q0xJQ0tIT1VTRV9VU0VSOi1kZWZhdWx0fScKICAgICAgLSAnQ0xJQ0tIT1VTRV9EQj0ke0NMSUNLSE9VU0VfREI6LWFuYWx5dGljc30nCiAgICAgIC0gUE9TVEdSRVNfSE9TVD1yeWJiaXRfcG9zdGdyZXMKICAgICAgLSBQT1NUR1JFU19QT1JUPTU0MzIKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotYW5hbHl0aWNzfScKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1BPU1RHUkVTX1VTRVI6LWZyb2d9JwogICAgZGVwZW5kc19vbjoKICAgICAgcnliYml0X2NsaWNraG91c2U6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcnliYml0X3Bvc3RncmVzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gd2dldAogICAgICAgIC0gJy0tbm8tdmVyYm9zZScKICAgICAgICAtICctLXRyaWVzPTEnCiAgICAgICAgLSAnLS1zcGlkZXInCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTozMDAxL2FwaS9oZWFsdGgnCiAgICAgIGludGVydmFsOiAzMHMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDMKICAgICAgc3RhcnRfcGVyaW9kOiAxMHMKICByeWJiaXRfcG9zdGdyZXM6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE3LjQnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotYW5hbHl0aWNzfScKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1BPU1RHUkVTX1VTRVI6LWZyb2d9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgdm9sdW1lczoKICAgICAgLSAncG9zdGdyZXNfZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogMzBzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAzCiAgcnliYml0X2NsaWNraG91c2U6CiAgICBpbWFnZTogJ2NsaWNraG91c2UvY2xpY2tob3VzZS1zZXJ2ZXI6MjUuNC4yJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0NMSUNLSE9VU0VfREI9JHtDTElDS0hPVVNFX0RCOi1hbmFseXRpY3N9JwogICAgICAtICdDTElDS0hPVVNFX1VTRVI9JHtDTElDS0hPVVNFX1VTRVI6LWRlZmF1bHR9JwogICAgICAtICdDTElDS0hPVVNFX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9DTElDS0hPVVNFfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSB3Z2V0CiAgICAgICAgLSAnLS1uby12ZXJib3NlJwogICAgICAgIC0gJy0tdHJpZXM9MScKICAgICAgICAtICctLXNwaWRlcicKICAgICAgICAtICdodHRwOi8vbG9jYWxob3N0OjgxMjMvcGluZycKICAgICAgaW50ZXJ2YWw6IDMwcwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMwogICAgICBzdGFydF9wZXJpb2Q6IDEwcwogICAgdm9sdW1lczoKICAgICAgLSAnY2xpY2tob3VzZV9kYXRhOi92YXIvbGliL2NsaWNraG91c2UnCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2NsaWNraG91c2VfY29uZmlnL2VuYWJsZV9qc29uLnhtbAogICAgICAgIHRhcmdldDogL2V0Yy9jbGlja2hvdXNlLXNlcnZlci9jb25maWcuZC9lbmFibGVfanNvbi54bWwKICAgICAgICBjb250ZW50OiAiPGNsaWNraG91c2U+XG4gICAgPHNldHRpbmdzPlxuICAgICAgICA8ZW5hYmxlX2pzb25fdHlwZT4xPC9lbmFibGVfanNvbl90eXBlPlxuICAgIDwvc2V0dGluZ3M+XG48L2NsaWNraG91c2U+XG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2NsaWNraG91c2VfY29uZmlnL2xvZ2dpbmdfcnVsZXMueG1sCiAgICAgICAgdGFyZ2V0OiAvZXRjL2NsaWNraG91c2Utc2VydmVyL2NvbmZpZy5kL2xvZ2dpbmdfcnVsZXMueG1sCiAgICAgICAgY29udGVudDogIjxjbGlja2hvdXNlPlxuICAgIDxsb2dnZXI+XG4gICAgICAgIDxsZXZlbD53YXJuaW5nPC9sZXZlbD5cbiAgICAgICAgPGNvbnNvbGU+dHJ1ZTwvY29uc29sZT5cbiAgICA8L2xvZ2dlcj5cbiAgICA8cXVlcnlfdGhyZWFkX2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG4gICAgPHF1ZXJ5X2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG4gICAgPHRleHRfbG9nIHJlbW92ZT1cInJlbW92ZVwiLz5cbiAgICA8dHJhY2VfbG9nIHJlbW92ZT1cInJlbW92ZVwiLz5cbiAgICA8bWV0cmljX2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG4gICAgPGFzeW5jaHJvbm91c19tZXRyaWNfbG9nIHJlbW92ZT1cInJlbW92ZVwiLz5cbiAgICA8c2Vzc2lvbl9sb2cgcmVtb3ZlPVwicmVtb3ZlXCIvPlxuICAgIDxwYXJ0X2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG4gICAgPGxhdGVuY3lfbG9nIHJlbW92ZT1cInJlbW92ZVwiLz5cbiAgICA8cHJvY2Vzc29yc19wcm9maWxlX2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG48L2NsaWNraG91c2U+IgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9jbGlja2hvdXNlX2NvbmZpZy9uZXR3b3JrLnhtbAogICAgICAgIHRhcmdldDogL2V0Yy9jbGlja2hvdXNlLXNlcnZlci9jb25maWcuZC9uZXR3b3JrLnhtbAogICAgICAgIGNvbnRlbnQ6ICI8Y2xpY2tob3VzZT5cbiAgICA8bGlzdGVuX2hvc3Q+MC4wLjAuMDwvbGlzdGVuX2hvc3Q+XG48L2NsaWNraG91c2U+XG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2NsaWNraG91c2VfY29uZmlnL3VzZXJfbG9nZ2luZy54bWwKICAgICAgICB0YXJnZXQ6IC9ldGMvY2xpY2tob3VzZS1zZXJ2ZXIvY29uZmlnLmQvdXNlcl9sb2dnaW5nLnhtbAogICAgICAgIGNvbnRlbnQ6ICI8Y2xpY2tob3VzZT5cbiAgICA8cHJvZmlsZXM+XG4gICAgICAgIDxkZWZhdWx0PlxuICAgICAgICAgICAgPGxvZ19xdWVyaWVzPjA8L2xvZ19xdWVyaWVzPlxuICAgICAgICAgICAgPGxvZ19xdWVyeV90aHJlYWRzPjA8L2xvZ19xdWVyeV90aHJlYWRzPlxuICAgICAgICAgICAgPGxvZ19wcm9jZXNzb3JzX3Byb2ZpbGVzPjA8L2xvZ19wcm9jZXNzb3JzX3Byb2ZpbGVzPlxuICAgICAgICA8L2RlZmF1bHQ+XG4gICAgPC9wcm9maWxlcz5cbjwvY2xpY2tob3VzZT4iCg==", + "compose": "c2VydmljZXM6CiAgcnliYml0OgogICAgaW1hZ2U6ICdnaGNyLmlvL3J5YmJpdC1pby9yeWJiaXQtY2xpZW50OnYxLjYuMScKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9SWUJCSVRfMzAwMgogICAgICAtIE5PREVfRU5WPXByb2R1Y3Rpb24KICAgICAgLSAnTkVYVF9QVUJMSUNfQkFDS0VORF9VUkw9JHtTRVJWSUNFX0ZRRE5fUllCQklUfScKICAgICAgLSAnTkVYVF9QVUJMSUNfRElTQUJMRV9TSUdOVVA9JHtESVNBQkxFX1NJR05VUDotZmFsc2V9JwogICAgZGVwZW5kc19vbjoKICAgICAgLSByeWJiaXRfYmFja2VuZAogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICduYyAteiAxMjcuMC4wLjEgMzAwMicKICAgICAgaW50ZXJ2YWw6IDMwcwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMwogICAgICBzdGFydF9wZXJpb2Q6IDEwcwogIHJ5YmJpdF9iYWNrZW5kOgogICAgaW1hZ2U6ICdnaGNyLmlvL3J5YmJpdC1pby9yeWJiaXQtYmFja2VuZDp2MS42LjEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBOT0RFX0VOVj1wcm9kdWN0aW9uCiAgICAgIC0gVFJVU1RfUFJPWFk9dHJ1ZQogICAgICAtICdCQVNFX1VSTD0ke1NFUlZJQ0VfRlFETl9SWUJCSVR9JwogICAgICAtICdDTElDS0hPVVNFX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9DTElDS0hPVVNFfScKICAgICAgLSAnUE9TVEdSRVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfScKICAgICAgLSAnQkVUVEVSX0FVVEhfU0VDUkVUPSR7U0VSVklDRV9CQVNFNjRfNjRfQkFDS0VORH0nCiAgICAgIC0gJ0RJU0FCTEVfU0lHTlVQPSR7RElTQUJMRV9TSUdOVVA6LWZhbHNlfScKICAgICAgLSAnRElTQUJMRV9URUxFTUVUUlk9JHtESVNBQkxFX1RFTEVNRVRSWTotdHJ1ZX0nCiAgICAgIC0gJ0NMSUNLSE9VU0VfSE9TVD1odHRwOi8vcnliYml0X2NsaWNraG91c2U6ODEyMycKICAgICAgLSAnQ0xJQ0tIT1VTRV9VU0VSPSR7Q0xJQ0tIT1VTRV9VU0VSOi1kZWZhdWx0fScKICAgICAgLSAnQ0xJQ0tIT1VTRV9EQj0ke0NMSUNLSE9VU0VfREI6LWFuYWx5dGljc30nCiAgICAgIC0gUE9TVEdSRVNfSE9TVD1yeWJiaXRfcG9zdGdyZXMKICAgICAgLSBQT1NUR1JFU19QT1JUPTU0MzIKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotYW5hbHl0aWNzfScKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1BPU1RHUkVTX1VTRVI6LWZyb2d9JwogICAgZGVwZW5kc19vbjoKICAgICAgcnliYml0X2NsaWNraG91c2U6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcnliYml0X3Bvc3RncmVzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gd2dldAogICAgICAgIC0gJy0tbm8tdmVyYm9zZScKICAgICAgICAtICctLXRyaWVzPTEnCiAgICAgICAgLSAnLS1zcGlkZXInCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTozMDAxL2FwaS9oZWFsdGgnCiAgICAgIGludGVydmFsOiAzMHMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDMKICAgICAgc3RhcnRfcGVyaW9kOiAxMHMKICByeWJiaXRfcG9zdGdyZXM6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE3LjQnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotYW5hbHl0aWNzfScKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1BPU1RHUkVTX1VTRVI6LWZyb2d9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgdm9sdW1lczoKICAgICAgLSAncG9zdGdyZXNfZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogMzBzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAzCiAgcnliYml0X2NsaWNraG91c2U6CiAgICBpbWFnZTogJ2NsaWNraG91c2UvY2xpY2tob3VzZS1zZXJ2ZXI6MjUuNC4yJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0NMSUNLSE9VU0VfREI9JHtDTElDS0hPVVNFX0RCOi1hbmFseXRpY3N9JwogICAgICAtICdDTElDS0hPVVNFX1VTRVI9JHtDTElDS0hPVVNFX1VTRVI6LWRlZmF1bHR9JwogICAgICAtICdDTElDS0hPVVNFX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9DTElDS0hPVVNFfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSB3Z2V0CiAgICAgICAgLSAnLS1uby12ZXJib3NlJwogICAgICAgIC0gJy0tdHJpZXM9MScKICAgICAgICAtICctLXNwaWRlcicKICAgICAgICAtICdodHRwOi8vbG9jYWxob3N0OjgxMjMvcGluZycKICAgICAgaW50ZXJ2YWw6IDMwcwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMwogICAgICBzdGFydF9wZXJpb2Q6IDEwcwogICAgdm9sdW1lczoKICAgICAgLSAnY2xpY2tob3VzZV9kYXRhOi92YXIvbGliL2NsaWNraG91c2UnCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2NsaWNraG91c2VfY29uZmlnL2VuYWJsZV9qc29uLnhtbAogICAgICAgIHRhcmdldDogL2V0Yy9jbGlja2hvdXNlLXNlcnZlci9jb25maWcuZC9lbmFibGVfanNvbi54bWwKICAgICAgICBjb250ZW50OiAiPGNsaWNraG91c2U+XG4gICAgPHNldHRpbmdzPlxuICAgICAgICA8ZW5hYmxlX2pzb25fdHlwZT4xPC9lbmFibGVfanNvbl90eXBlPlxuICAgIDwvc2V0dGluZ3M+XG48L2NsaWNraG91c2U+XG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2NsaWNraG91c2VfY29uZmlnL2xvZ2dpbmdfcnVsZXMueG1sCiAgICAgICAgdGFyZ2V0OiAvZXRjL2NsaWNraG91c2Utc2VydmVyL2NvbmZpZy5kL2xvZ2dpbmdfcnVsZXMueG1sCiAgICAgICAgY29udGVudDogIjxjbGlja2hvdXNlPlxuICAgIDxsb2dnZXI+XG4gICAgICAgIDxsZXZlbD53YXJuaW5nPC9sZXZlbD5cbiAgICAgICAgPGNvbnNvbGU+dHJ1ZTwvY29uc29sZT5cbiAgICA8L2xvZ2dlcj5cbiAgICA8cXVlcnlfdGhyZWFkX2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG4gICAgPHF1ZXJ5X2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG4gICAgPHRleHRfbG9nIHJlbW92ZT1cInJlbW92ZVwiLz5cbiAgICA8dHJhY2VfbG9nIHJlbW92ZT1cInJlbW92ZVwiLz5cbiAgICA8bWV0cmljX2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG4gICAgPGFzeW5jaHJvbm91c19tZXRyaWNfbG9nIHJlbW92ZT1cInJlbW92ZVwiLz5cbiAgICA8c2Vzc2lvbl9sb2cgcmVtb3ZlPVwicmVtb3ZlXCIvPlxuICAgIDxwYXJ0X2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG4gICAgPGxhdGVuY3lfbG9nIHJlbW92ZT1cInJlbW92ZVwiLz5cbiAgICA8cHJvY2Vzc29yc19wcm9maWxlX2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG48L2NsaWNraG91c2U+IgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9jbGlja2hvdXNlX2NvbmZpZy9uZXR3b3JrLnhtbAogICAgICAgIHRhcmdldDogL2V0Yy9jbGlja2hvdXNlLXNlcnZlci9jb25maWcuZC9uZXR3b3JrLnhtbAogICAgICAgIGNvbnRlbnQ6ICI8Y2xpY2tob3VzZT5cbiAgICA8bGlzdGVuX2hvc3Q+MC4wLjAuMDwvbGlzdGVuX2hvc3Q+XG48L2NsaWNraG91c2U+XG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2NsaWNraG91c2VfY29uZmlnL3VzZXJfbG9nZ2luZy54bWwKICAgICAgICB0YXJnZXQ6IC9ldGMvY2xpY2tob3VzZS1zZXJ2ZXIvY29uZmlnLmQvdXNlcl9sb2dnaW5nLnhtbAogICAgICAgIGNvbnRlbnQ6ICI8Y2xpY2tob3VzZT5cbiAgICA8cHJvZmlsZXM+XG4gICAgICAgIDxkZWZhdWx0PlxuICAgICAgICAgICAgPGxvZ19xdWVyaWVzPjA8L2xvZ19xdWVyaWVzPlxuICAgICAgICAgICAgPGxvZ19xdWVyeV90aHJlYWRzPjA8L2xvZ19xdWVyeV90aHJlYWRzPlxuICAgICAgICAgICAgPGxvZ19wcm9jZXNzb3JzX3Byb2ZpbGVzPjA8L2xvZ19wcm9jZXNzb3JzX3Byb2ZpbGVzPlxuICAgICAgICA8L2RlZmF1bHQ+XG4gICAgPC9wcm9maWxlcz5cbjwvY2xpY2tob3VzZT5cbiIK", "tags": [ "analytics", "web", @@ -3673,7 +3673,7 @@ "clickhouse", "postgres" ], - "category": null, + "category": "analytics", "logo": "svgs/rybbit.svg", "minversion": "0.0.0", "port": "3002" From 919fc184b77ff170a0ca915905ac17d423439351 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sun, 9 Nov 2025 20:35:03 +0100 Subject: [PATCH 09/25] fix(docker): improve pull request ID check in container status function --- bootstrap/helpers/docker.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php index 9da622ed3..c62c2ad8e 100644 --- a/bootstrap/helpers/docker.php +++ b/bootstrap/helpers/docker.php @@ -45,7 +45,7 @@ function getCurrentApplicationContainerStatus(Server $server, int $id, ?int $pul if ($includePullrequests) { return $container; } - if (str($labels)->contains("coolify.pullRequestId=$pullRequestId")) { + if ($pullRequestId !== null && $pullRequestId !== 0 && str($labels)->contains("coolify.pullRequestId={$pullRequestId}")) { return $container; } From 40e242b874388a415be7d0df36f0490be2e025a2 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 10 Nov 2025 11:32:49 +0100 Subject: [PATCH 10/25] perf(nginx): increase client body buffer size to 256k for Sentinel payloads Increases client_body_buffer_size from default (8k-16k) to 256k to handle Sentinel monitoring data in memory instead of buffering to disk. This eliminates the "client request body is buffered to a temporary file" warning and improves performance for servers with many containers (50-100+) sending health check data. Affects both production and development nginx configurations. --- .../development/etc/nginx/site-opts.d/http.conf | 3 +++ .../production/etc/nginx/site-opts.d/http.conf | 3 +++ templates/service-templates-latest.json | 16 ++++++++-------- templates/service-templates.json | 16 ++++++++-------- 4 files changed, 22 insertions(+), 16 deletions(-) diff --git a/docker/development/etc/nginx/site-opts.d/http.conf b/docker/development/etc/nginx/site-opts.d/http.conf index a5bbd78a3..d7855ae80 100644 --- a/docker/development/etc/nginx/site-opts.d/http.conf +++ b/docker/development/etc/nginx/site-opts.d/http.conf @@ -13,6 +13,9 @@ charset utf-8; # Set max upload to 2048M client_max_body_size 2048M; +# Set client body buffer to handle Sentinel payloads in memory +client_body_buffer_size 256k; + # Healthchecks: Set /healthcheck to be the healthcheck URL location /healthcheck { access_log off; diff --git a/docker/production/etc/nginx/site-opts.d/http.conf b/docker/production/etc/nginx/site-opts.d/http.conf index a5bbd78a3..d7855ae80 100644 --- a/docker/production/etc/nginx/site-opts.d/http.conf +++ b/docker/production/etc/nginx/site-opts.d/http.conf @@ -13,6 +13,9 @@ charset utf-8; # Set max upload to 2048M client_max_body_size 2048M; +# Set client body buffer to handle Sentinel payloads in memory +client_body_buffer_size 256k; + # Healthchecks: Set /healthcheck to be the healthcheck URL location /healthcheck { access_log off; diff --git a/templates/service-templates-latest.json b/templates/service-templates-latest.json index 4d365b483..8ae37e5e5 100644 --- a/templates/service-templates-latest.json +++ b/templates/service-templates-latest.json @@ -976,13 +976,13 @@ "slogan": "EmbyStat is a web analytics tool, designed to provide insight into website traffic and user behavior.", "compose": "c2VydmljZXM6CiAgZW1ieXN0YXQ6CiAgICBpbWFnZTogJ2xzY3IuaW8vbGludXhzZXJ2ZXIvZW1ieXN0YXQ6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9VUkxfRU1CWVNUQVRfNjU1NQogICAgICAtIFBVSUQ9MTAwMAogICAgICAtIFBHSUQ9MTAwMAogICAgICAtIFRaPUV1cm9wZS9NYWRyaWQKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2VtYnlzdGF0LWNvbmZpZzovY29uZmlnJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjY1NTUnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK", "tags": [ - "media", - "server", - "movies", - "tv", - "music" + "analytics", + "insights", + "statistics", + "web", + "traffic" ], - "category": "media", + "category": "analytics", "logo": "svgs/default.webp", "minversion": "0.0.0", "port": "6555" @@ -3664,7 +3664,7 @@ "rybbit": { "documentation": "https://rybbit.io/docs?utm_source=coolify.io", "slogan": "Open-source, privacy-first web analytics.", - "compose": "c2VydmljZXM6CiAgcnliYml0OgogICAgaW1hZ2U6ICdnaGNyLmlvL3J5YmJpdC1pby9yeWJiaXQtY2xpZW50OnYxLjYuMScKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX1JZQkJJVF8zMDAyCiAgICAgIC0gTk9ERV9FTlY9cHJvZHVjdGlvbgogICAgICAtICdORVhUX1BVQkxJQ19CQUNLRU5EX1VSTD0ke1NFUlZJQ0VfVVJMX1JZQkJJVH0nCiAgICAgIC0gJ05FWFRfUFVCTElDX0RJU0FCTEVfU0lHTlVQPSR7RElTQUJMRV9TSUdOVVA6LWZhbHNlfScKICAgIGRlcGVuZHNfb246CiAgICAgIC0gcnliYml0X2JhY2tlbmQKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAnbmMgLXogMTI3LjAuMC4xIDMwMDInCiAgICAgIGludGVydmFsOiAzMHMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDMKICAgICAgc3RhcnRfcGVyaW9kOiAxMHMKICByeWJiaXRfYmFja2VuZDoKICAgIGltYWdlOiAnZ2hjci5pby9yeWJiaXQtaW8vcnliYml0LWJhY2tlbmQ6djEuNi4xJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gTk9ERV9FTlY9cHJvZHVjdGlvbgogICAgICAtIFRSVVNUX1BST1hZPXRydWUKICAgICAgLSAnQkFTRV9VUkw9JHtTRVJWSUNFX1VSTF9SWUJCSVR9JwogICAgICAtICdDTElDS0hPVVNFX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9DTElDS0hPVVNFfScKICAgICAgLSAnUE9TVEdSRVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfScKICAgICAgLSAnQkVUVEVSX0FVVEhfU0VDUkVUPSR7U0VSVklDRV9CQVNFNjRfNjRfQkFDS0VORH0nCiAgICAgIC0gJ0RJU0FCTEVfU0lHTlVQPSR7RElTQUJMRV9TSUdOVVA6LWZhbHNlfScKICAgICAgLSAnRElTQUJMRV9URUxFTUVUUlk9JHtESVNBQkxFX1RFTEVNRVRSWTotdHJ1ZX0nCiAgICAgIC0gJ0NMSUNLSE9VU0VfSE9TVD1odHRwOi8vcnliYml0X2NsaWNraG91c2U6ODEyMycKICAgICAgLSAnQ0xJQ0tIT1VTRV9VU0VSPSR7Q0xJQ0tIT1VTRV9VU0VSOi1kZWZhdWx0fScKICAgICAgLSAnQ0xJQ0tIT1VTRV9EQj0ke0NMSUNLSE9VU0VfREI6LWFuYWx5dGljc30nCiAgICAgIC0gUE9TVEdSRVNfSE9TVD1yeWJiaXRfcG9zdGdyZXMKICAgICAgLSBQT1NUR1JFU19QT1JUPTU0MzIKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotYW5hbHl0aWNzfScKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1BPU1RHUkVTX1VTRVI6LWZyb2d9JwogICAgZGVwZW5kc19vbjoKICAgICAgcnliYml0X2NsaWNraG91c2U6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcnliYml0X3Bvc3RncmVzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gd2dldAogICAgICAgIC0gJy0tbm8tdmVyYm9zZScKICAgICAgICAtICctLXRyaWVzPTEnCiAgICAgICAgLSAnLS1zcGlkZXInCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTozMDAxL2FwaS9oZWFsdGgnCiAgICAgIGludGVydmFsOiAzMHMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDMKICAgICAgc3RhcnRfcGVyaW9kOiAxMHMKICByeWJiaXRfcG9zdGdyZXM6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE3LjQnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotYW5hbHl0aWNzfScKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1BPU1RHUkVTX1VTRVI6LWZyb2d9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgdm9sdW1lczoKICAgICAgLSAncG9zdGdyZXNfZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogMzBzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAzCiAgcnliYml0X2NsaWNraG91c2U6CiAgICBpbWFnZTogJ2NsaWNraG91c2UvY2xpY2tob3VzZS1zZXJ2ZXI6MjUuNC4yJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0NMSUNLSE9VU0VfREI9JHtDTElDS0hPVVNFX0RCOi1hbmFseXRpY3N9JwogICAgICAtICdDTElDS0hPVVNFX1VTRVI9JHtDTElDS0hPVVNFX1VTRVI6LWRlZmF1bHR9JwogICAgICAtICdDTElDS0hPVVNFX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9DTElDS0hPVVNFfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSB3Z2V0CiAgICAgICAgLSAnLS1uby12ZXJib3NlJwogICAgICAgIC0gJy0tdHJpZXM9MScKICAgICAgICAtICctLXNwaWRlcicKICAgICAgICAtICdodHRwOi8vbG9jYWxob3N0OjgxMjMvcGluZycKICAgICAgaW50ZXJ2YWw6IDMwcwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMwogICAgICBzdGFydF9wZXJpb2Q6IDEwcwogICAgdm9sdW1lczoKICAgICAgLSAnY2xpY2tob3VzZV9kYXRhOi92YXIvbGliL2NsaWNraG91c2UnCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2NsaWNraG91c2VfY29uZmlnL2VuYWJsZV9qc29uLnhtbAogICAgICAgIHRhcmdldDogL2V0Yy9jbGlja2hvdXNlLXNlcnZlci9jb25maWcuZC9lbmFibGVfanNvbi54bWwKICAgICAgICBjb250ZW50OiAiPGNsaWNraG91c2U+XG4gICAgPHNldHRpbmdzPlxuICAgICAgICA8ZW5hYmxlX2pzb25fdHlwZT4xPC9lbmFibGVfanNvbl90eXBlPlxuICAgIDwvc2V0dGluZ3M+XG48L2NsaWNraG91c2U+XG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2NsaWNraG91c2VfY29uZmlnL2xvZ2dpbmdfcnVsZXMueG1sCiAgICAgICAgdGFyZ2V0OiAvZXRjL2NsaWNraG91c2Utc2VydmVyL2NvbmZpZy5kL2xvZ2dpbmdfcnVsZXMueG1sCiAgICAgICAgY29udGVudDogIjxjbGlja2hvdXNlPlxuICAgIDxsb2dnZXI+XG4gICAgICAgIDxsZXZlbD53YXJuaW5nPC9sZXZlbD5cbiAgICAgICAgPGNvbnNvbGU+dHJ1ZTwvY29uc29sZT5cbiAgICA8L2xvZ2dlcj5cbiAgICA8cXVlcnlfdGhyZWFkX2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG4gICAgPHF1ZXJ5X2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG4gICAgPHRleHRfbG9nIHJlbW92ZT1cInJlbW92ZVwiLz5cbiAgICA8dHJhY2VfbG9nIHJlbW92ZT1cInJlbW92ZVwiLz5cbiAgICA8bWV0cmljX2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG4gICAgPGFzeW5jaHJvbm91c19tZXRyaWNfbG9nIHJlbW92ZT1cInJlbW92ZVwiLz5cbiAgICA8c2Vzc2lvbl9sb2cgcmVtb3ZlPVwicmVtb3ZlXCIvPlxuICAgIDxwYXJ0X2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG4gICAgPGxhdGVuY3lfbG9nIHJlbW92ZT1cInJlbW92ZVwiLz5cbiAgICA8cHJvY2Vzc29yc19wcm9maWxlX2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG48L2NsaWNraG91c2U+IgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9jbGlja2hvdXNlX2NvbmZpZy9uZXR3b3JrLnhtbAogICAgICAgIHRhcmdldDogL2V0Yy9jbGlja2hvdXNlLXNlcnZlci9jb25maWcuZC9uZXR3b3JrLnhtbAogICAgICAgIGNvbnRlbnQ6ICI8Y2xpY2tob3VzZT5cbiAgICA8bGlzdGVuX2hvc3Q+MC4wLjAuMDwvbGlzdGVuX2hvc3Q+XG48L2NsaWNraG91c2U+XG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2NsaWNraG91c2VfY29uZmlnL3VzZXJfbG9nZ2luZy54bWwKICAgICAgICB0YXJnZXQ6IC9ldGMvY2xpY2tob3VzZS1zZXJ2ZXIvY29uZmlnLmQvdXNlcl9sb2dnaW5nLnhtbAogICAgICAgIGNvbnRlbnQ6ICI8Y2xpY2tob3VzZT5cbiAgICA8cHJvZmlsZXM+XG4gICAgICAgIDxkZWZhdWx0PlxuICAgICAgICAgICAgPGxvZ19xdWVyaWVzPjA8L2xvZ19xdWVyaWVzPlxuICAgICAgICAgICAgPGxvZ19xdWVyeV90aHJlYWRzPjA8L2xvZ19xdWVyeV90aHJlYWRzPlxuICAgICAgICAgICAgPGxvZ19wcm9jZXNzb3JzX3Byb2ZpbGVzPjA8L2xvZ19wcm9jZXNzb3JzX3Byb2ZpbGVzPlxuICAgICAgICA8L2RlZmF1bHQ+XG4gICAgPC9wcm9maWxlcz5cbjwvY2xpY2tob3VzZT4iCg==", + "compose": "c2VydmljZXM6CiAgcnliYml0OgogICAgaW1hZ2U6ICdnaGNyLmlvL3J5YmJpdC1pby9yeWJiaXQtY2xpZW50OnYxLjYuMScKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfVVJMX1JZQkJJVF8zMDAyCiAgICAgIC0gTk9ERV9FTlY9cHJvZHVjdGlvbgogICAgICAtICdORVhUX1BVQkxJQ19CQUNLRU5EX1VSTD0ke1NFUlZJQ0VfVVJMX1JZQkJJVH0nCiAgICAgIC0gJ05FWFRfUFVCTElDX0RJU0FCTEVfU0lHTlVQPSR7RElTQUJMRV9TSUdOVVA6LWZhbHNlfScKICAgIGRlcGVuZHNfb246CiAgICAgIC0gcnliYml0X2JhY2tlbmQKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAnbmMgLXogMTI3LjAuMC4xIDMwMDInCiAgICAgIGludGVydmFsOiAzMHMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDMKICAgICAgc3RhcnRfcGVyaW9kOiAxMHMKICByeWJiaXRfYmFja2VuZDoKICAgIGltYWdlOiAnZ2hjci5pby9yeWJiaXQtaW8vcnliYml0LWJhY2tlbmQ6djEuNi4xJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gTk9ERV9FTlY9cHJvZHVjdGlvbgogICAgICAtIFRSVVNUX1BST1hZPXRydWUKICAgICAgLSAnQkFTRV9VUkw9JHtTRVJWSUNFX1VSTF9SWUJCSVR9JwogICAgICAtICdDTElDS0hPVVNFX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9DTElDS0hPVVNFfScKICAgICAgLSAnUE9TVEdSRVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfScKICAgICAgLSAnQkVUVEVSX0FVVEhfU0VDUkVUPSR7U0VSVklDRV9CQVNFNjRfNjRfQkFDS0VORH0nCiAgICAgIC0gJ0RJU0FCTEVfU0lHTlVQPSR7RElTQUJMRV9TSUdOVVA6LWZhbHNlfScKICAgICAgLSAnRElTQUJMRV9URUxFTUVUUlk9JHtESVNBQkxFX1RFTEVNRVRSWTotdHJ1ZX0nCiAgICAgIC0gJ0NMSUNLSE9VU0VfSE9TVD1odHRwOi8vcnliYml0X2NsaWNraG91c2U6ODEyMycKICAgICAgLSAnQ0xJQ0tIT1VTRV9VU0VSPSR7Q0xJQ0tIT1VTRV9VU0VSOi1kZWZhdWx0fScKICAgICAgLSAnQ0xJQ0tIT1VTRV9EQj0ke0NMSUNLSE9VU0VfREI6LWFuYWx5dGljc30nCiAgICAgIC0gUE9TVEdSRVNfSE9TVD1yeWJiaXRfcG9zdGdyZXMKICAgICAgLSBQT1NUR1JFU19QT1JUPTU0MzIKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotYW5hbHl0aWNzfScKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1BPU1RHUkVTX1VTRVI6LWZyb2d9JwogICAgZGVwZW5kc19vbjoKICAgICAgcnliYml0X2NsaWNraG91c2U6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcnliYml0X3Bvc3RncmVzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gd2dldAogICAgICAgIC0gJy0tbm8tdmVyYm9zZScKICAgICAgICAtICctLXRyaWVzPTEnCiAgICAgICAgLSAnLS1zcGlkZXInCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTozMDAxL2FwaS9oZWFsdGgnCiAgICAgIGludGVydmFsOiAzMHMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDMKICAgICAgc3RhcnRfcGVyaW9kOiAxMHMKICByeWJiaXRfcG9zdGdyZXM6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE3LjQnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotYW5hbHl0aWNzfScKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1BPU1RHUkVTX1VTRVI6LWZyb2d9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgdm9sdW1lczoKICAgICAgLSAncG9zdGdyZXNfZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogMzBzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAzCiAgcnliYml0X2NsaWNraG91c2U6CiAgICBpbWFnZTogJ2NsaWNraG91c2UvY2xpY2tob3VzZS1zZXJ2ZXI6MjUuNC4yJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0NMSUNLSE9VU0VfREI9JHtDTElDS0hPVVNFX0RCOi1hbmFseXRpY3N9JwogICAgICAtICdDTElDS0hPVVNFX1VTRVI9JHtDTElDS0hPVVNFX1VTRVI6LWRlZmF1bHR9JwogICAgICAtICdDTElDS0hPVVNFX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9DTElDS0hPVVNFfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSB3Z2V0CiAgICAgICAgLSAnLS1uby12ZXJib3NlJwogICAgICAgIC0gJy0tdHJpZXM9MScKICAgICAgICAtICctLXNwaWRlcicKICAgICAgICAtICdodHRwOi8vbG9jYWxob3N0OjgxMjMvcGluZycKICAgICAgaW50ZXJ2YWw6IDMwcwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMwogICAgICBzdGFydF9wZXJpb2Q6IDEwcwogICAgdm9sdW1lczoKICAgICAgLSAnY2xpY2tob3VzZV9kYXRhOi92YXIvbGliL2NsaWNraG91c2UnCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2NsaWNraG91c2VfY29uZmlnL2VuYWJsZV9qc29uLnhtbAogICAgICAgIHRhcmdldDogL2V0Yy9jbGlja2hvdXNlLXNlcnZlci9jb25maWcuZC9lbmFibGVfanNvbi54bWwKICAgICAgICBjb250ZW50OiAiPGNsaWNraG91c2U+XG4gICAgPHNldHRpbmdzPlxuICAgICAgICA8ZW5hYmxlX2pzb25fdHlwZT4xPC9lbmFibGVfanNvbl90eXBlPlxuICAgIDwvc2V0dGluZ3M+XG48L2NsaWNraG91c2U+XG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2NsaWNraG91c2VfY29uZmlnL2xvZ2dpbmdfcnVsZXMueG1sCiAgICAgICAgdGFyZ2V0OiAvZXRjL2NsaWNraG91c2Utc2VydmVyL2NvbmZpZy5kL2xvZ2dpbmdfcnVsZXMueG1sCiAgICAgICAgY29udGVudDogIjxjbGlja2hvdXNlPlxuICAgIDxsb2dnZXI+XG4gICAgICAgIDxsZXZlbD53YXJuaW5nPC9sZXZlbD5cbiAgICAgICAgPGNvbnNvbGU+dHJ1ZTwvY29uc29sZT5cbiAgICA8L2xvZ2dlcj5cbiAgICA8cXVlcnlfdGhyZWFkX2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG4gICAgPHF1ZXJ5X2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG4gICAgPHRleHRfbG9nIHJlbW92ZT1cInJlbW92ZVwiLz5cbiAgICA8dHJhY2VfbG9nIHJlbW92ZT1cInJlbW92ZVwiLz5cbiAgICA8bWV0cmljX2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG4gICAgPGFzeW5jaHJvbm91c19tZXRyaWNfbG9nIHJlbW92ZT1cInJlbW92ZVwiLz5cbiAgICA8c2Vzc2lvbl9sb2cgcmVtb3ZlPVwicmVtb3ZlXCIvPlxuICAgIDxwYXJ0X2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG4gICAgPGxhdGVuY3lfbG9nIHJlbW92ZT1cInJlbW92ZVwiLz5cbiAgICA8cHJvY2Vzc29yc19wcm9maWxlX2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG48L2NsaWNraG91c2U+IgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9jbGlja2hvdXNlX2NvbmZpZy9uZXR3b3JrLnhtbAogICAgICAgIHRhcmdldDogL2V0Yy9jbGlja2hvdXNlLXNlcnZlci9jb25maWcuZC9uZXR3b3JrLnhtbAogICAgICAgIGNvbnRlbnQ6ICI8Y2xpY2tob3VzZT5cbiAgICA8bGlzdGVuX2hvc3Q+MC4wLjAuMDwvbGlzdGVuX2hvc3Q+XG48L2NsaWNraG91c2U+XG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2NsaWNraG91c2VfY29uZmlnL3VzZXJfbG9nZ2luZy54bWwKICAgICAgICB0YXJnZXQ6IC9ldGMvY2xpY2tob3VzZS1zZXJ2ZXIvY29uZmlnLmQvdXNlcl9sb2dnaW5nLnhtbAogICAgICAgIGNvbnRlbnQ6ICI8Y2xpY2tob3VzZT5cbiAgICA8cHJvZmlsZXM+XG4gICAgICAgIDxkZWZhdWx0PlxuICAgICAgICAgICAgPGxvZ19xdWVyaWVzPjA8L2xvZ19xdWVyaWVzPlxuICAgICAgICAgICAgPGxvZ19xdWVyeV90aHJlYWRzPjA8L2xvZ19xdWVyeV90aHJlYWRzPlxuICAgICAgICAgICAgPGxvZ19wcm9jZXNzb3JzX3Byb2ZpbGVzPjA8L2xvZ19wcm9jZXNzb3JzX3Byb2ZpbGVzPlxuICAgICAgICA8L2RlZmF1bHQ+XG4gICAgPC9wcm9maWxlcz5cbjwvY2xpY2tob3VzZT5cbiIK", "tags": [ "analytics", "web", @@ -3673,7 +3673,7 @@ "clickhouse", "postgres" ], - "category": null, + "category": "analytics", "logo": "svgs/rybbit.svg", "minversion": "0.0.0", "port": "3002" diff --git a/templates/service-templates.json b/templates/service-templates.json index d711b9d95..774b9fba1 100644 --- a/templates/service-templates.json +++ b/templates/service-templates.json @@ -976,13 +976,13 @@ "slogan": "EmbyStat is a web analytics tool, designed to provide insight into website traffic and user behavior.", "compose": "c2VydmljZXM6CiAgZW1ieXN0YXQ6CiAgICBpbWFnZTogJ2xzY3IuaW8vbGludXhzZXJ2ZXIvZW1ieXN0YXQ6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0VNQllTVEFUXzY1NTUKICAgICAgLSBQVUlEPTEwMDAKICAgICAgLSBQR0lEPTEwMDAKICAgICAgLSBUWj1FdXJvcGUvTWFkcmlkCiAgICB2b2x1bWVzOgogICAgICAtICdlbWJ5c3RhdC1jb25maWc6L2NvbmZpZycKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo2NTU1JwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1Cg==", "tags": [ - "media", - "server", - "movies", - "tv", - "music" + "analytics", + "insights", + "statistics", + "web", + "traffic" ], - "category": "media", + "category": "analytics", "logo": "svgs/default.webp", "minversion": "0.0.0", "port": "6555" @@ -3664,7 +3664,7 @@ "rybbit": { "documentation": "https://rybbit.io/docs?utm_source=coolify.io", "slogan": "Open-source, privacy-first web analytics.", - "compose": "c2VydmljZXM6CiAgcnliYml0OgogICAgaW1hZ2U6ICdnaGNyLmlvL3J5YmJpdC1pby9yeWJiaXQtY2xpZW50OnYxLjYuMScKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9SWUJCSVRfMzAwMgogICAgICAtIE5PREVfRU5WPXByb2R1Y3Rpb24KICAgICAgLSAnTkVYVF9QVUJMSUNfQkFDS0VORF9VUkw9JHtTRVJWSUNFX0ZRRE5fUllCQklUfScKICAgICAgLSAnTkVYVF9QVUJMSUNfRElTQUJMRV9TSUdOVVA9JHtESVNBQkxFX1NJR05VUDotZmFsc2V9JwogICAgZGVwZW5kc19vbjoKICAgICAgLSByeWJiaXRfYmFja2VuZAogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICduYyAteiAxMjcuMC4wLjEgMzAwMicKICAgICAgaW50ZXJ2YWw6IDMwcwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMwogICAgICBzdGFydF9wZXJpb2Q6IDEwcwogIHJ5YmJpdF9iYWNrZW5kOgogICAgaW1hZ2U6ICdnaGNyLmlvL3J5YmJpdC1pby9yeWJiaXQtYmFja2VuZDp2MS42LjEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBOT0RFX0VOVj1wcm9kdWN0aW9uCiAgICAgIC0gVFJVU1RfUFJPWFk9dHJ1ZQogICAgICAtICdCQVNFX1VSTD0ke1NFUlZJQ0VfRlFETl9SWUJCSVR9JwogICAgICAtICdDTElDS0hPVVNFX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9DTElDS0hPVVNFfScKICAgICAgLSAnUE9TVEdSRVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfScKICAgICAgLSAnQkVUVEVSX0FVVEhfU0VDUkVUPSR7U0VSVklDRV9CQVNFNjRfNjRfQkFDS0VORH0nCiAgICAgIC0gJ0RJU0FCTEVfU0lHTlVQPSR7RElTQUJMRV9TSUdOVVA6LWZhbHNlfScKICAgICAgLSAnRElTQUJMRV9URUxFTUVUUlk9JHtESVNBQkxFX1RFTEVNRVRSWTotdHJ1ZX0nCiAgICAgIC0gJ0NMSUNLSE9VU0VfSE9TVD1odHRwOi8vcnliYml0X2NsaWNraG91c2U6ODEyMycKICAgICAgLSAnQ0xJQ0tIT1VTRV9VU0VSPSR7Q0xJQ0tIT1VTRV9VU0VSOi1kZWZhdWx0fScKICAgICAgLSAnQ0xJQ0tIT1VTRV9EQj0ke0NMSUNLSE9VU0VfREI6LWFuYWx5dGljc30nCiAgICAgIC0gUE9TVEdSRVNfSE9TVD1yeWJiaXRfcG9zdGdyZXMKICAgICAgLSBQT1NUR1JFU19QT1JUPTU0MzIKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotYW5hbHl0aWNzfScKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1BPU1RHUkVTX1VTRVI6LWZyb2d9JwogICAgZGVwZW5kc19vbjoKICAgICAgcnliYml0X2NsaWNraG91c2U6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcnliYml0X3Bvc3RncmVzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gd2dldAogICAgICAgIC0gJy0tbm8tdmVyYm9zZScKICAgICAgICAtICctLXRyaWVzPTEnCiAgICAgICAgLSAnLS1zcGlkZXInCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTozMDAxL2FwaS9oZWFsdGgnCiAgICAgIGludGVydmFsOiAzMHMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDMKICAgICAgc3RhcnRfcGVyaW9kOiAxMHMKICByeWJiaXRfcG9zdGdyZXM6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE3LjQnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotYW5hbHl0aWNzfScKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1BPU1RHUkVTX1VTRVI6LWZyb2d9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgdm9sdW1lczoKICAgICAgLSAncG9zdGdyZXNfZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogMzBzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAzCiAgcnliYml0X2NsaWNraG91c2U6CiAgICBpbWFnZTogJ2NsaWNraG91c2UvY2xpY2tob3VzZS1zZXJ2ZXI6MjUuNC4yJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0NMSUNLSE9VU0VfREI9JHtDTElDS0hPVVNFX0RCOi1hbmFseXRpY3N9JwogICAgICAtICdDTElDS0hPVVNFX1VTRVI9JHtDTElDS0hPVVNFX1VTRVI6LWRlZmF1bHR9JwogICAgICAtICdDTElDS0hPVVNFX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9DTElDS0hPVVNFfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSB3Z2V0CiAgICAgICAgLSAnLS1uby12ZXJib3NlJwogICAgICAgIC0gJy0tdHJpZXM9MScKICAgICAgICAtICctLXNwaWRlcicKICAgICAgICAtICdodHRwOi8vbG9jYWxob3N0OjgxMjMvcGluZycKICAgICAgaW50ZXJ2YWw6IDMwcwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMwogICAgICBzdGFydF9wZXJpb2Q6IDEwcwogICAgdm9sdW1lczoKICAgICAgLSAnY2xpY2tob3VzZV9kYXRhOi92YXIvbGliL2NsaWNraG91c2UnCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2NsaWNraG91c2VfY29uZmlnL2VuYWJsZV9qc29uLnhtbAogICAgICAgIHRhcmdldDogL2V0Yy9jbGlja2hvdXNlLXNlcnZlci9jb25maWcuZC9lbmFibGVfanNvbi54bWwKICAgICAgICBjb250ZW50OiAiPGNsaWNraG91c2U+XG4gICAgPHNldHRpbmdzPlxuICAgICAgICA8ZW5hYmxlX2pzb25fdHlwZT4xPC9lbmFibGVfanNvbl90eXBlPlxuICAgIDwvc2V0dGluZ3M+XG48L2NsaWNraG91c2U+XG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2NsaWNraG91c2VfY29uZmlnL2xvZ2dpbmdfcnVsZXMueG1sCiAgICAgICAgdGFyZ2V0OiAvZXRjL2NsaWNraG91c2Utc2VydmVyL2NvbmZpZy5kL2xvZ2dpbmdfcnVsZXMueG1sCiAgICAgICAgY29udGVudDogIjxjbGlja2hvdXNlPlxuICAgIDxsb2dnZXI+XG4gICAgICAgIDxsZXZlbD53YXJuaW5nPC9sZXZlbD5cbiAgICAgICAgPGNvbnNvbGU+dHJ1ZTwvY29uc29sZT5cbiAgICA8L2xvZ2dlcj5cbiAgICA8cXVlcnlfdGhyZWFkX2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG4gICAgPHF1ZXJ5X2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG4gICAgPHRleHRfbG9nIHJlbW92ZT1cInJlbW92ZVwiLz5cbiAgICA8dHJhY2VfbG9nIHJlbW92ZT1cInJlbW92ZVwiLz5cbiAgICA8bWV0cmljX2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG4gICAgPGFzeW5jaHJvbm91c19tZXRyaWNfbG9nIHJlbW92ZT1cInJlbW92ZVwiLz5cbiAgICA8c2Vzc2lvbl9sb2cgcmVtb3ZlPVwicmVtb3ZlXCIvPlxuICAgIDxwYXJ0X2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG4gICAgPGxhdGVuY3lfbG9nIHJlbW92ZT1cInJlbW92ZVwiLz5cbiAgICA8cHJvY2Vzc29yc19wcm9maWxlX2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG48L2NsaWNraG91c2U+IgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9jbGlja2hvdXNlX2NvbmZpZy9uZXR3b3JrLnhtbAogICAgICAgIHRhcmdldDogL2V0Yy9jbGlja2hvdXNlLXNlcnZlci9jb25maWcuZC9uZXR3b3JrLnhtbAogICAgICAgIGNvbnRlbnQ6ICI8Y2xpY2tob3VzZT5cbiAgICA8bGlzdGVuX2hvc3Q+MC4wLjAuMDwvbGlzdGVuX2hvc3Q+XG48L2NsaWNraG91c2U+XG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2NsaWNraG91c2VfY29uZmlnL3VzZXJfbG9nZ2luZy54bWwKICAgICAgICB0YXJnZXQ6IC9ldGMvY2xpY2tob3VzZS1zZXJ2ZXIvY29uZmlnLmQvdXNlcl9sb2dnaW5nLnhtbAogICAgICAgIGNvbnRlbnQ6ICI8Y2xpY2tob3VzZT5cbiAgICA8cHJvZmlsZXM+XG4gICAgICAgIDxkZWZhdWx0PlxuICAgICAgICAgICAgPGxvZ19xdWVyaWVzPjA8L2xvZ19xdWVyaWVzPlxuICAgICAgICAgICAgPGxvZ19xdWVyeV90aHJlYWRzPjA8L2xvZ19xdWVyeV90aHJlYWRzPlxuICAgICAgICAgICAgPGxvZ19wcm9jZXNzb3JzX3Byb2ZpbGVzPjA8L2xvZ19wcm9jZXNzb3JzX3Byb2ZpbGVzPlxuICAgICAgICA8L2RlZmF1bHQ+XG4gICAgPC9wcm9maWxlcz5cbjwvY2xpY2tob3VzZT4iCg==", + "compose": "c2VydmljZXM6CiAgcnliYml0OgogICAgaW1hZ2U6ICdnaGNyLmlvL3J5YmJpdC1pby9yeWJiaXQtY2xpZW50OnYxLjYuMScKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9SWUJCSVRfMzAwMgogICAgICAtIE5PREVfRU5WPXByb2R1Y3Rpb24KICAgICAgLSAnTkVYVF9QVUJMSUNfQkFDS0VORF9VUkw9JHtTRVJWSUNFX0ZRRE5fUllCQklUfScKICAgICAgLSAnTkVYVF9QVUJMSUNfRElTQUJMRV9TSUdOVVA9JHtESVNBQkxFX1NJR05VUDotZmFsc2V9JwogICAgZGVwZW5kc19vbjoKICAgICAgLSByeWJiaXRfYmFja2VuZAogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICduYyAteiAxMjcuMC4wLjEgMzAwMicKICAgICAgaW50ZXJ2YWw6IDMwcwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMwogICAgICBzdGFydF9wZXJpb2Q6IDEwcwogIHJ5YmJpdF9iYWNrZW5kOgogICAgaW1hZ2U6ICdnaGNyLmlvL3J5YmJpdC1pby9yeWJiaXQtYmFja2VuZDp2MS42LjEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBOT0RFX0VOVj1wcm9kdWN0aW9uCiAgICAgIC0gVFJVU1RfUFJPWFk9dHJ1ZQogICAgICAtICdCQVNFX1VSTD0ke1NFUlZJQ0VfRlFETl9SWUJCSVR9JwogICAgICAtICdDTElDS0hPVVNFX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9DTElDS0hPVVNFfScKICAgICAgLSAnUE9TVEdSRVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfScKICAgICAgLSAnQkVUVEVSX0FVVEhfU0VDUkVUPSR7U0VSVklDRV9CQVNFNjRfNjRfQkFDS0VORH0nCiAgICAgIC0gJ0RJU0FCTEVfU0lHTlVQPSR7RElTQUJMRV9TSUdOVVA6LWZhbHNlfScKICAgICAgLSAnRElTQUJMRV9URUxFTUVUUlk9JHtESVNBQkxFX1RFTEVNRVRSWTotdHJ1ZX0nCiAgICAgIC0gJ0NMSUNLSE9VU0VfSE9TVD1odHRwOi8vcnliYml0X2NsaWNraG91c2U6ODEyMycKICAgICAgLSAnQ0xJQ0tIT1VTRV9VU0VSPSR7Q0xJQ0tIT1VTRV9VU0VSOi1kZWZhdWx0fScKICAgICAgLSAnQ0xJQ0tIT1VTRV9EQj0ke0NMSUNLSE9VU0VfREI6LWFuYWx5dGljc30nCiAgICAgIC0gUE9TVEdSRVNfSE9TVD1yeWJiaXRfcG9zdGdyZXMKICAgICAgLSBQT1NUR1JFU19QT1JUPTU0MzIKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotYW5hbHl0aWNzfScKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1BPU1RHUkVTX1VTRVI6LWZyb2d9JwogICAgZGVwZW5kc19vbjoKICAgICAgcnliYml0X2NsaWNraG91c2U6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcnliYml0X3Bvc3RncmVzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gd2dldAogICAgICAgIC0gJy0tbm8tdmVyYm9zZScKICAgICAgICAtICctLXRyaWVzPTEnCiAgICAgICAgLSAnLS1zcGlkZXInCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTozMDAxL2FwaS9oZWFsdGgnCiAgICAgIGludGVydmFsOiAzMHMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDMKICAgICAgc3RhcnRfcGVyaW9kOiAxMHMKICByeWJiaXRfcG9zdGdyZXM6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE3LjQnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotYW5hbHl0aWNzfScKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1BPU1RHUkVTX1VTRVI6LWZyb2d9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgdm9sdW1lczoKICAgICAgLSAncG9zdGdyZXNfZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogMzBzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAzCiAgcnliYml0X2NsaWNraG91c2U6CiAgICBpbWFnZTogJ2NsaWNraG91c2UvY2xpY2tob3VzZS1zZXJ2ZXI6MjUuNC4yJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0NMSUNLSE9VU0VfREI9JHtDTElDS0hPVVNFX0RCOi1hbmFseXRpY3N9JwogICAgICAtICdDTElDS0hPVVNFX1VTRVI9JHtDTElDS0hPVVNFX1VTRVI6LWRlZmF1bHR9JwogICAgICAtICdDTElDS0hPVVNFX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9DTElDS0hPVVNFfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSB3Z2V0CiAgICAgICAgLSAnLS1uby12ZXJib3NlJwogICAgICAgIC0gJy0tdHJpZXM9MScKICAgICAgICAtICctLXNwaWRlcicKICAgICAgICAtICdodHRwOi8vbG9jYWxob3N0OjgxMjMvcGluZycKICAgICAgaW50ZXJ2YWw6IDMwcwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMwogICAgICBzdGFydF9wZXJpb2Q6IDEwcwogICAgdm9sdW1lczoKICAgICAgLSAnY2xpY2tob3VzZV9kYXRhOi92YXIvbGliL2NsaWNraG91c2UnCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2NsaWNraG91c2VfY29uZmlnL2VuYWJsZV9qc29uLnhtbAogICAgICAgIHRhcmdldDogL2V0Yy9jbGlja2hvdXNlLXNlcnZlci9jb25maWcuZC9lbmFibGVfanNvbi54bWwKICAgICAgICBjb250ZW50OiAiPGNsaWNraG91c2U+XG4gICAgPHNldHRpbmdzPlxuICAgICAgICA8ZW5hYmxlX2pzb25fdHlwZT4xPC9lbmFibGVfanNvbl90eXBlPlxuICAgIDwvc2V0dGluZ3M+XG48L2NsaWNraG91c2U+XG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2NsaWNraG91c2VfY29uZmlnL2xvZ2dpbmdfcnVsZXMueG1sCiAgICAgICAgdGFyZ2V0OiAvZXRjL2NsaWNraG91c2Utc2VydmVyL2NvbmZpZy5kL2xvZ2dpbmdfcnVsZXMueG1sCiAgICAgICAgY29udGVudDogIjxjbGlja2hvdXNlPlxuICAgIDxsb2dnZXI+XG4gICAgICAgIDxsZXZlbD53YXJuaW5nPC9sZXZlbD5cbiAgICAgICAgPGNvbnNvbGU+dHJ1ZTwvY29uc29sZT5cbiAgICA8L2xvZ2dlcj5cbiAgICA8cXVlcnlfdGhyZWFkX2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG4gICAgPHF1ZXJ5X2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG4gICAgPHRleHRfbG9nIHJlbW92ZT1cInJlbW92ZVwiLz5cbiAgICA8dHJhY2VfbG9nIHJlbW92ZT1cInJlbW92ZVwiLz5cbiAgICA8bWV0cmljX2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG4gICAgPGFzeW5jaHJvbm91c19tZXRyaWNfbG9nIHJlbW92ZT1cInJlbW92ZVwiLz5cbiAgICA8c2Vzc2lvbl9sb2cgcmVtb3ZlPVwicmVtb3ZlXCIvPlxuICAgIDxwYXJ0X2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG4gICAgPGxhdGVuY3lfbG9nIHJlbW92ZT1cInJlbW92ZVwiLz5cbiAgICA8cHJvY2Vzc29yc19wcm9maWxlX2xvZyByZW1vdmU9XCJyZW1vdmVcIi8+XG48L2NsaWNraG91c2U+IgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9jbGlja2hvdXNlX2NvbmZpZy9uZXR3b3JrLnhtbAogICAgICAgIHRhcmdldDogL2V0Yy9jbGlja2hvdXNlLXNlcnZlci9jb25maWcuZC9uZXR3b3JrLnhtbAogICAgICAgIGNvbnRlbnQ6ICI8Y2xpY2tob3VzZT5cbiAgICA8bGlzdGVuX2hvc3Q+MC4wLjAuMDwvbGlzdGVuX2hvc3Q+XG48L2NsaWNraG91c2U+XG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2NsaWNraG91c2VfY29uZmlnL3VzZXJfbG9nZ2luZy54bWwKICAgICAgICB0YXJnZXQ6IC9ldGMvY2xpY2tob3VzZS1zZXJ2ZXIvY29uZmlnLmQvdXNlcl9sb2dnaW5nLnhtbAogICAgICAgIGNvbnRlbnQ6ICI8Y2xpY2tob3VzZT5cbiAgICA8cHJvZmlsZXM+XG4gICAgICAgIDxkZWZhdWx0PlxuICAgICAgICAgICAgPGxvZ19xdWVyaWVzPjA8L2xvZ19xdWVyaWVzPlxuICAgICAgICAgICAgPGxvZ19xdWVyeV90aHJlYWRzPjA8L2xvZ19xdWVyeV90aHJlYWRzPlxuICAgICAgICAgICAgPGxvZ19wcm9jZXNzb3JzX3Byb2ZpbGVzPjA8L2xvZ19wcm9jZXNzb3JzX3Byb2ZpbGVzPlxuICAgICAgICA8L2RlZmF1bHQ+XG4gICAgPC9wcm9maWxlcz5cbjwvY2xpY2tob3VzZT5cbiIK", "tags": [ "analytics", "web", @@ -3673,7 +3673,7 @@ "clickhouse", "postgres" ], - "category": null, + "category": "analytics", "logo": "svgs/rybbit.svg", "minversion": "0.0.0", "port": "3002" From 68a9f2ca77eec9ff8221103dff9ed4ea805c5106 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 10 Nov 2025 13:04:31 +0100 Subject: [PATCH 11/25] feat: add container restart tracking and crash loop detection Track container restart counts from Docker and detect crash loops to provide better visibility into application health issues. - Add restart_count, last_restart_at, and last_restart_type columns to applications table - Detect restart count increases from Docker inspect data and send notifications - Show restart count badge in UI with warning icon on Logs navigation - Distinguish between crash restarts and manual restarts - Implement 30-second grace period to prevent false "exited" status during crash loops - Reset restart count on manual stop, restart, and redeploy actions - Add unit tests for restart count tracking logic This helps users quickly identify when containers are in crash loops and need attention, even when the container status flickers between states during Docker's restart backoff period. --- app/Actions/Docker/GetContainersStatus.php | 74 ++++++++++++++++- app/Livewire/Project/Application/Heading.php | 16 ++++ app/Models/Application.php | 2 + ...restart_tracking_to_applications_table.php | 30 +++++++ .../views/components/status/index.blade.php | 7 ++ .../project/application/heading.blade.php | 9 +- templates/service-templates-latest.json | 16 ++-- templates/service-templates.json | 16 ++-- tests/Unit/RestartCountTrackingTest.php | 82 +++++++++++++++++++ 9 files changed, 231 insertions(+), 21 deletions(-) create mode 100644 database/migrations/2025_11_10_112500_add_restart_tracking_to_applications_table.php create mode 100644 tests/Unit/RestartCountTrackingTest.php diff --git a/app/Actions/Docker/GetContainersStatus.php b/app/Actions/Docker/GetContainersStatus.php index f5d5f82b6..a985871dc 100644 --- a/app/Actions/Docker/GetContainersStatus.php +++ b/app/Actions/Docker/GetContainersStatus.php @@ -28,6 +28,8 @@ class GetContainersStatus protected ?Collection $applicationContainerStatuses; + protected ?Collection $applicationContainerRestartCounts; + public function handle(Server $server, ?Collection $containers = null, ?Collection $containerReplicates = null) { $this->containers = $containers; @@ -136,6 +138,18 @@ public function handle(Server $server, ?Collection $containers = null, ?Collecti if ($containerName) { $this->applicationContainerStatuses->get($applicationId)->put($containerName, $containerStatus); } + + // Track restart counts for applications + $restartCount = data_get($container, 'RestartCount', 0); + if (! isset($this->applicationContainerRestartCounts)) { + $this->applicationContainerRestartCounts = collect(); + } + if (! $this->applicationContainerRestartCounts->has($applicationId)) { + $this->applicationContainerRestartCounts->put($applicationId, collect()); + } + if ($containerName) { + $this->applicationContainerRestartCounts->get($applicationId)->put($containerName, $restartCount); + } } else { // Notify user that this container should not be there. } @@ -291,7 +305,24 @@ public function handle(Server $server, ?Collection $containers = null, ?Collecti continue; } - $application->update(['status' => 'exited']); + // If container was recently restarting (crash loop), keep it as degraded for a grace period + // This prevents false "exited" status during the brief moment between container removal and recreation + $recentlyRestarted = $application->restart_count > 0 && + $application->last_restart_at && + $application->last_restart_at->greaterThan(now()->subSeconds(30)); + + if ($recentlyRestarted) { + // Keep it as degraded if it was recently in a crash loop + $application->update(['status' => 'degraded (unhealthy)']); + } else { + // Reset restart count when application exits completely + $application->update([ + 'status' => 'exited', + 'restart_count' => 0, + 'last_restart_at' => null, + 'last_restart_type' => null, + ]); + } } $notRunningApplicationPreviews = $previews->pluck('id')->diff($foundApplicationPreviews); foreach ($notRunningApplicationPreviews as $previewId) { @@ -340,7 +371,37 @@ public function handle(Server $server, ?Collection $containers = null, ?Collecti continue; } - $aggregatedStatus = $this->aggregateApplicationStatus($application, $containerStatuses); + // Track restart counts first + $maxRestartCount = 0; + if (isset($this->applicationContainerRestartCounts) && $this->applicationContainerRestartCounts->has($applicationId)) { + $containerRestartCounts = $this->applicationContainerRestartCounts->get($applicationId); + $maxRestartCount = $containerRestartCounts->max() ?? 0; + $previousRestartCount = $application->restart_count ?? 0; + + if ($maxRestartCount > $previousRestartCount) { + // Restart count increased - this is a crash restart + $application->update([ + 'restart_count' => $maxRestartCount, + 'last_restart_at' => now(), + 'last_restart_type' => 'crash', + ]); + + // Send notification + $containerName = $application->name; + $projectUuid = data_get($application, 'environment.project.uuid'); + $environmentName = data_get($application, 'environment.name'); + $applicationUuid = data_get($application, 'uuid'); + + if ($projectUuid && $applicationUuid && $environmentName) { + $url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/application/'.$applicationUuid; + } else { + $url = null; + } + } + } + + // Aggregate status after tracking restart counts + $aggregatedStatus = $this->aggregateApplicationStatus($application, $containerStatuses, $maxRestartCount); if ($aggregatedStatus) { $statusFromDb = $application->status; if ($statusFromDb !== $aggregatedStatus) { @@ -355,7 +416,7 @@ public function handle(Server $server, ?Collection $containers = null, ?Collecti ServiceChecked::dispatch($this->server->team->id); } - private function aggregateApplicationStatus($application, Collection $containerStatuses): ?string + private function aggregateApplicationStatus($application, Collection $containerStatuses, int $maxRestartCount = 0): ?string { // Parse docker compose to check for excluded containers $dockerComposeRaw = data_get($application, 'docker_compose_raw'); @@ -413,6 +474,11 @@ private function aggregateApplicationStatus($application, Collection $containerS return 'degraded (unhealthy)'; } + // If container is exited but has restart count > 0, it's in a crash loop + if ($hasExited && $maxRestartCount > 0) { + return 'degraded (unhealthy)'; + } + if ($hasRunning && $hasExited) { return 'degraded (unhealthy)'; } @@ -421,7 +487,7 @@ private function aggregateApplicationStatus($application, Collection $containerS return $hasUnhealthy ? 'running (unhealthy)' : 'running (healthy)'; } - // All containers are exited + // All containers are exited with no restart count - truly stopped return 'exited (unhealthy)'; } } diff --git a/app/Livewire/Project/Application/Heading.php b/app/Livewire/Project/Application/Heading.php index 5231438e5..2c20926a3 100644 --- a/app/Livewire/Project/Application/Heading.php +++ b/app/Livewire/Project/Application/Heading.php @@ -94,6 +94,14 @@ public function deploy(bool $force_rebuild = false) return; } + + // Reset restart count on deployment + $this->application->update([ + 'restart_count' => 0, + 'last_restart_at' => null, + 'last_restart_type' => null, + ]); + $this->setDeploymentUuid(); $result = queue_application_deployment( application: $this->application, @@ -137,6 +145,14 @@ public function restart() return; } + + // Reset restart count on manual restart + $this->application->update([ + 'restart_count' => 0, + 'last_restart_at' => now(), + 'last_restart_type' => 'manual', + ]); + $this->setDeploymentUuid(); $result = queue_application_deployment( application: $this->application, diff --git a/app/Models/Application.php b/app/Models/Application.php index 615e35f68..be340375f 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -121,6 +121,8 @@ class Application extends BaseModel protected $casts = [ 'http_basic_auth_password' => 'encrypted', + 'restart_count' => 'integer', + 'last_restart_at' => 'datetime', ]; protected static function booted() diff --git a/database/migrations/2025_11_10_112500_add_restart_tracking_to_applications_table.php b/database/migrations/2025_11_10_112500_add_restart_tracking_to_applications_table.php new file mode 100644 index 000000000..329ac7af9 --- /dev/null +++ b/database/migrations/2025_11_10_112500_add_restart_tracking_to_applications_table.php @@ -0,0 +1,30 @@ +integer('restart_count')->default(0)->after('status'); + $table->timestamp('last_restart_at')->nullable()->after('restart_count'); + $table->string('last_restart_type', 10)->nullable()->after('last_restart_at'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('applications', function (Blueprint $table) { + $table->dropColumn(['restart_count', 'last_restart_at', 'last_restart_type']); + }); + } +}; diff --git a/resources/views/components/status/index.blade.php b/resources/views/components/status/index.blade.php index d592cff79..57e5409c6 100644 --- a/resources/views/components/status/index.blade.php +++ b/resources/views/components/status/index.blade.php @@ -12,6 +12,13 @@ @else @endif +@if (isset($resource->restart_count) && $resource->restart_count > 0 && !str($resource->status)->startsWith('exited')) +
+ + ({{ $resource->restart_count }}x restarts) + +
+@endif @if (!str($resource->status)->contains('exited') && $showRefreshButton)