From 96d0d39fd8bf2ff1826a238daba7ecb68242f04e Mon Sep 17 00:00:00 2001 From: thevinodpatidar Date: Thu, 30 Oct 2025 16:35:22 +0530 Subject: [PATCH 01/64] 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/64] 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/64] 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 ffa4123a721d13c41d881965c1e78dd68e6fce87 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 6 Nov 2025 14:06:03 +0000 Subject: [PATCH 04/64] chore(deps-dev): bump tar from 7.5.1 to 7.5.2 Bumps [tar](https://github.com/isaacs/node-tar) from 7.5.1 to 7.5.2. - [Release notes](https://github.com/isaacs/node-tar/releases) - [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md) - [Commits](https://github.com/isaacs/node-tar/compare/v7.5.1...v7.5.2) --- updated-dependencies: - dependency-name: tar dependency-version: 7.5.2 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9e8fe7328..f8ef518d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -916,7 +916,8 @@ "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@tailwindcss/forms": { "version": "0.5.10", @@ -1431,8 +1432,7 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/asynckit": { "version": "0.4.0", @@ -1595,6 +1595,7 @@ "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1", @@ -1609,6 +1610,7 @@ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ms": "^2.1.3" }, @@ -1627,6 +1629,7 @@ "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10.0.0" } @@ -2388,7 +2391,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -2465,7 +2467,6 @@ "integrity": "sha512-wp3HqIIUc1GRyu1XrP6m2dgyE9MoCsXVsWNlohj0rjSkLf+a0jLvEyVubdg58oMk7bhjBWnFClgp8jfAa6Ak4Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "tweetnacl": "^1.0.3" } @@ -2550,6 +2551,7 @@ "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.2", @@ -2566,6 +2568,7 @@ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ms": "^2.1.3" }, @@ -2584,6 +2587,7 @@ "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1" @@ -2598,6 +2602,7 @@ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ms": "^2.1.3" }, @@ -2646,8 +2651,7 @@ "version": "4.1.10", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.10.tgz", "integrity": "sha512-P3nr6WkvKV/ONsTzj6Gb57sWPMX29EPNPopo7+FcpkQaNsrNpZ1pv8QmrYI2RqEKD7mlGqLnGovlcYnBK0IqUA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/tapable": { "version": "2.3.0", @@ -2664,11 +2668,11 @@ } }, "node_modules/tar": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.1.tgz", - "integrity": "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==", + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.2.tgz", + "integrity": "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", @@ -2716,7 +2720,6 @@ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -2816,7 +2819,6 @@ "integrity": "sha512-rjOV2ecxMd5SiAmof2xzh2WxntRcigkX/He4YFJ6WdRvVUrbt6DxC1Iujh10XLl8xCDRDtGKMeO3D+pRQ1PP9w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.16", "@vue/compiler-sfc": "3.5.16", @@ -2839,6 +2841,7 @@ "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10.0.0" }, @@ -2860,6 +2863,7 @@ "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", "dev": true, + "peer": true, "engines": { "node": ">=0.4.0" } From 560c98e280b68e4da1fe3acfcbef2b92a081fa17 Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Thu, 6 Nov 2025 15:11:13 +0100 Subject: [PATCH 05/64] ci(workflow): fix changelog generation --- .github/workflows/generate-changelog.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/generate-changelog.yml b/.github/workflows/generate-changelog.yml index f62b41736..935a88721 100644 --- a/.github/workflows/generate-changelog.yml +++ b/.github/workflows/generate-changelog.yml @@ -16,7 +16,6 @@ jobs: - name: Checkout uses: actions/checkout@v4 with: - persist-credentials: false fetch-depth: 0 - name: Generate changelog From 3801be2fd4427899ca1b47c95a94ae14e1e7cb46 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 7 Nov 2025 08:19:47 +0100 Subject: [PATCH 06/64] ci(workflows): refactor build-push jobs to use matrix strategy for multi-architecture support --- .github/workflows/coolify-helper-next.yml | 69 ++++++------------ .github/workflows/coolify-helper.yml | 69 ++++++------------ .../workflows/coolify-production-build.yml | 68 ++++++------------ .github/workflows/coolify-realtime-next.yml | 71 ++++++------------- .github/workflows/coolify-realtime.yml | 70 ++++++------------ .github/workflows/coolify-testing-host.yml | 65 ++++++----------- 6 files changed, 126 insertions(+), 286 deletions(-) diff --git a/.github/workflows/coolify-helper-next.yml b/.github/workflows/coolify-helper-next.yml index ba8a69d28..fec54d54a 100644 --- a/.github/workflows/coolify-helper-next.yml +++ b/.github/workflows/coolify-helper-next.yml @@ -17,8 +17,17 @@ env: IMAGE_NAME: "coollabsio/coolify-helper" jobs: - amd64: - runs-on: ubuntu-latest + build-push: + strategy: + matrix: + include: + - arch: amd64 + platform: linux/amd64 + runner: ubuntu-24.04 + - arch: aarch64 + platform: linux/aarch64 + runner: ubuntu-24.04-arm + runs-on: ${{ matrix.runner }} steps: - uses: actions/checkout@v5 with: @@ -43,60 +52,22 @@ jobs: run: | echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getHelperVersion.php)"|xargs >> $GITHUB_OUTPUT - - name: Build and Push Image + - name: Build and Push Image (${{ matrix.arch }}) uses: docker/build-push-action@v6 with: context: . file: docker/coolify-helper/Dockerfile - platforms: linux/amd64 + platforms: ${{ matrix.platform }} push: true tags: | - ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next - ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next - labels: | - coolify.managed=true - aarch64: - runs-on: [ self-hosted, arm64 ] - steps: - - uses: actions/checkout@v5 - with: - persist-credentials: false - - - name: Login to ${{ env.GITHUB_REGISTRY }} - uses: docker/login-action@v3 - with: - registry: ${{ env.GITHUB_REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Login to ${{ env.DOCKER_REGISTRY }} - uses: docker/login-action@v3 - with: - registry: ${{ env.DOCKER_REGISTRY }} - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_TOKEN }} - - - name: Get Version - id: version - run: | - echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getHelperVersion.php)"|xargs >> $GITHUB_OUTPUT - - - name: Build and Push Image - uses: docker/build-push-action@v6 - with: - context: . - file: docker/coolify-helper/Dockerfile - platforms: linux/aarch64 - push: true - tags: | - ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 - ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-${{ matrix.arch }} + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-${{ matrix.arch }} labels: | coolify.managed=true merge-manifest: - runs-on: ubuntu-latest - needs: [ amd64, aarch64 ] + runs-on: ubuntu-24.04 + needs: build-push steps: - uses: actions/checkout@v5 with: @@ -126,14 +97,16 @@ jobs: - name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }} run: | docker buildx imagetools create \ - --append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 \ + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-amd64 \ + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 \ --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next \ --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:next - name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }} run: | docker buildx imagetools create \ - --append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 \ + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-amd64 \ + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 \ --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next \ --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:next diff --git a/.github/workflows/coolify-helper.yml b/.github/workflows/coolify-helper.yml index 738a3480c..0c9996ec8 100644 --- a/.github/workflows/coolify-helper.yml +++ b/.github/workflows/coolify-helper.yml @@ -17,8 +17,17 @@ env: IMAGE_NAME: "coollabsio/coolify-helper" jobs: - amd64: - runs-on: ubuntu-latest + build-push: + strategy: + matrix: + include: + - arch: amd64 + platform: linux/amd64 + runner: ubuntu-24.04 + - arch: aarch64 + platform: linux/aarch64 + runner: ubuntu-24.04-arm + runs-on: ${{ matrix.runner }} steps: - uses: actions/checkout@v5 with: @@ -43,59 +52,21 @@ jobs: run: | echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getHelperVersion.php)"|xargs >> $GITHUB_OUTPUT - - name: Build and Push Image + - name: Build and Push Image (${{ matrix.arch }}) uses: docker/build-push-action@v6 with: context: . file: docker/coolify-helper/Dockerfile - platforms: linux/amd64 + platforms: ${{ matrix.platform }} push: true tags: | - ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} - ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} - labels: | - coolify.managed=true - aarch64: - runs-on: [ self-hosted, arm64 ] - steps: - - uses: actions/checkout@v5 - with: - persist-credentials: false - - - name: Login to ${{ env.GITHUB_REGISTRY }} - uses: docker/login-action@v3 - with: - registry: ${{ env.GITHUB_REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Login to ${{ env.DOCKER_REGISTRY }} - uses: docker/login-action@v3 - with: - registry: ${{ env.DOCKER_REGISTRY }} - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_TOKEN }} - - - name: Get Version - id: version - run: | - echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getHelperVersion.php)"|xargs >> $GITHUB_OUTPUT - - - name: Build and Push Image - uses: docker/build-push-action@v6 - with: - context: . - file: docker/coolify-helper/Dockerfile - platforms: linux/aarch64 - push: true - tags: | - ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 - ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-${{ matrix.arch }} + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-${{ matrix.arch }} labels: | coolify.managed=true merge-manifest: - runs-on: ubuntu-latest - needs: [ amd64, aarch64 ] + runs-on: ubuntu-24.04 + needs: build-push steps: - uses: actions/checkout@v5 with: @@ -125,14 +96,16 @@ jobs: - name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }} run: | docker buildx imagetools create \ - --append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \ + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-amd64 \ + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \ --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \ --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest - name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }} run: | docker buildx imagetools create \ - --append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \ + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-amd64 \ + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \ --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \ --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest diff --git a/.github/workflows/coolify-production-build.yml b/.github/workflows/coolify-production-build.yml index b6cfd34ae..21871b103 100644 --- a/.github/workflows/coolify-production-build.yml +++ b/.github/workflows/coolify-production-build.yml @@ -24,8 +24,17 @@ env: IMAGE_NAME: "coollabsio/coolify" jobs: - amd64: - runs-on: ubuntu-latest + build-push: + strategy: + matrix: + include: + - arch: amd64 + platform: linux/amd64 + runner: ubuntu-24.04 + - arch: aarch64 + platform: linux/aarch64 + runner: ubuntu-24.04-arm + runs-on: ${{ matrix.runner }} steps: - uses: actions/checkout@v5 with: @@ -50,57 +59,20 @@ jobs: run: | echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT - - name: Build and Push Image + - name: Build and Push Image (${{ matrix.arch }}) uses: docker/build-push-action@v6 with: context: . file: docker/production/Dockerfile - platforms: linux/amd64 + platforms: ${{ matrix.platform }} push: true tags: | - ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} - ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} - - aarch64: - runs-on: [self-hosted, arm64] - steps: - - uses: actions/checkout@v5 - with: - persist-credentials: false - - - name: Login to ${{ env.GITHUB_REGISTRY }} - uses: docker/login-action@v3 - with: - registry: ${{ env.GITHUB_REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Login to ${{ env.DOCKER_REGISTRY }} - uses: docker/login-action@v3 - with: - registry: ${{ env.DOCKER_REGISTRY }} - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_TOKEN }} - - - name: Get Version - id: version - run: | - echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT - - - name: Build and Push Image - uses: docker/build-push-action@v6 - with: - context: . - file: docker/production/Dockerfile - platforms: linux/aarch64 - push: true - tags: | - ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 - ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-${{ matrix.arch }} + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-${{ matrix.arch }} merge-manifest: - runs-on: ubuntu-latest - needs: [amd64, aarch64] + runs-on: ubuntu-24.04 + needs: build-push steps: - uses: actions/checkout@v5 with: @@ -130,14 +102,16 @@ jobs: - name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }} run: | docker buildx imagetools create \ - --append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \ + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-amd64 \ + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \ --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \ --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest - name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }} run: | docker buildx imagetools create \ - --append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \ + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-amd64 \ + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \ --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \ --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest diff --git a/.github/workflows/coolify-realtime-next.yml b/.github/workflows/coolify-realtime-next.yml index 7a6071bde..7ab4dcc42 100644 --- a/.github/workflows/coolify-realtime-next.yml +++ b/.github/workflows/coolify-realtime-next.yml @@ -21,8 +21,17 @@ env: IMAGE_NAME: "coollabsio/coolify-realtime" jobs: - amd64: - runs-on: ubuntu-latest + build-push: + strategy: + matrix: + include: + - arch: amd64 + platform: linux/amd64 + runner: ubuntu-24.04 + - arch: aarch64 + platform: linux/aarch64 + runner: ubuntu-24.04-arm + runs-on: ${{ matrix.runner }} steps: - uses: actions/checkout@v5 with: @@ -47,62 +56,22 @@ jobs: run: | echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getRealtimeVersion.php)"|xargs >> $GITHUB_OUTPUT - - name: Build and Push Image + - name: Build and Push Image (${{ matrix.arch }}) uses: docker/build-push-action@v6 with: context: . file: docker/coolify-realtime/Dockerfile - platforms: linux/amd64 + platforms: ${{ matrix.platform }} push: true tags: | - ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next - ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next - labels: | - coolify.managed=true - - aarch64: - runs-on: [ self-hosted, arm64 ] - steps: - - uses: actions/checkout@v5 - with: - persist-credentials: false - - - - name: Login to ${{ env.GITHUB_REGISTRY }} - uses: docker/login-action@v3 - with: - registry: ${{ env.GITHUB_REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Login to ${{ env.DOCKER_REGISTRY }} - uses: docker/login-action@v3 - with: - registry: ${{ env.DOCKER_REGISTRY }} - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_TOKEN }} - - - name: Get Version - id: version - run: | - echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getRealtimeVersion.php)"|xargs >> $GITHUB_OUTPUT - - - name: Build and Push Image - uses: docker/build-push-action@v6 - with: - context: . - file: docker/coolify-realtime/Dockerfile - platforms: linux/aarch64 - push: true - tags: | - ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 - ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-${{ matrix.arch }} + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-${{ matrix.arch }} labels: | coolify.managed=true merge-manifest: - runs-on: ubuntu-latest - needs: [ amd64, aarch64 ] + runs-on: ubuntu-24.04 + needs: build-push steps: - uses: actions/checkout@v5 with: @@ -132,14 +101,16 @@ jobs: - name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }} run: | docker buildx imagetools create \ - --append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 \ + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-amd64 \ + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 \ --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next \ --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:next - name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }} run: | docker buildx imagetools create \ - --append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 \ + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-amd64 \ + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 \ --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next \ --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:next diff --git a/.github/workflows/coolify-realtime.yml b/.github/workflows/coolify-realtime.yml index 1074af3ee..5efe445c5 100644 --- a/.github/workflows/coolify-realtime.yml +++ b/.github/workflows/coolify-realtime.yml @@ -21,8 +21,17 @@ env: IMAGE_NAME: "coollabsio/coolify-realtime" jobs: - amd64: - runs-on: ubuntu-latest + build-push: + strategy: + matrix: + include: + - arch: amd64 + platform: linux/amd64 + runner: ubuntu-24.04 + - arch: aarch64 + platform: linux/aarch64 + runner: ubuntu-24.04-arm + runs-on: ${{ matrix.runner }} steps: - uses: actions/checkout@v5 with: @@ -47,61 +56,22 @@ jobs: run: | echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getRealtimeVersion.php)"|xargs >> $GITHUB_OUTPUT - - name: Build and Push Image + - name: Build and Push Image (${{ matrix.arch }}) uses: docker/build-push-action@v6 with: context: . file: docker/coolify-realtime/Dockerfile - platforms: linux/amd64 + platforms: ${{ matrix.platform }} push: true tags: | - ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} - ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} - labels: | - coolify.managed=true - - aarch64: - runs-on: [ self-hosted, arm64 ] - steps: - - uses: actions/checkout@v5 - with: - persist-credentials: false - - - name: Login to ${{ env.GITHUB_REGISTRY }} - uses: docker/login-action@v3 - with: - registry: ${{ env.GITHUB_REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Login to ${{ env.DOCKER_REGISTRY }} - uses: docker/login-action@v3 - with: - registry: ${{ env.DOCKER_REGISTRY }} - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_TOKEN }} - - - name: Get Version - id: version - run: | - echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getRealtimeVersion.php)"|xargs >> $GITHUB_OUTPUT - - - name: Build and Push Image - uses: docker/build-push-action@v6 - with: - context: . - file: docker/coolify-realtime/Dockerfile - platforms: linux/aarch64 - push: true - tags: | - ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 - ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-${{ matrix.arch }} + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-${{ matrix.arch }} labels: | coolify.managed=true merge-manifest: - runs-on: ubuntu-latest - needs: [ amd64, aarch64 ] + runs-on: ubuntu-24.04 + needs: build-push steps: - uses: actions/checkout@v5 with: @@ -131,14 +101,16 @@ jobs: - name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }} run: | docker buildx imagetools create \ - --append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \ + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-amd64 \ + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \ --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \ --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest - name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }} run: | docker buildx imagetools create \ - --append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \ + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-amd64 \ + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \ --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \ --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest diff --git a/.github/workflows/coolify-testing-host.yml b/.github/workflows/coolify-testing-host.yml index c4aecd85e..24133887a 100644 --- a/.github/workflows/coolify-testing-host.yml +++ b/.github/workflows/coolify-testing-host.yml @@ -17,8 +17,17 @@ env: IMAGE_NAME: "coollabsio/coolify-testing-host" jobs: - amd64: - runs-on: ubuntu-latest + build-push: + strategy: + matrix: + include: + - arch: amd64 + platform: linux/amd64 + runner: ubuntu-24.04 + - arch: aarch64 + platform: linux/aarch64 + runner: ubuntu-24.04-arm + runs-on: ${{ matrix.runner }} steps: - uses: actions/checkout@v5 with: @@ -38,56 +47,22 @@ jobs: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_TOKEN }} - - name: Build and Push Image + - name: Build and Push Image (${{ matrix.arch }}) uses: docker/build-push-action@v6 with: context: . file: docker/testing-host/Dockerfile - platforms: linux/amd64 + platforms: ${{ matrix.platform }} push: true tags: | - ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest - ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest - labels: | - coolify.managed=true - - aarch64: - runs-on: [ self-hosted, arm64 ] - steps: - - uses: actions/checkout@v5 - with: - persist-credentials: false - - - name: Login to ${{ env.GITHUB_REGISTRY }} - uses: docker/login-action@v3 - with: - registry: ${{ env.GITHUB_REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Login to ${{ env.DOCKER_REGISTRY }} - uses: docker/login-action@v3 - with: - registry: ${{ env.DOCKER_REGISTRY }} - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_TOKEN }} - - - name: Build and Push Image - uses: docker/build-push-action@v6 - with: - context: . - file: docker/testing-host/Dockerfile - platforms: linux/aarch64 - push: true - tags: | - ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64 - ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64 + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-${{ matrix.arch }} + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-${{ matrix.arch }} labels: | coolify.managed=true merge-manifest: - runs-on: ubuntu-latest - needs: [ amd64, aarch64 ] + runs-on: ubuntu-24.04 + needs: build-push steps: - uses: actions/checkout@v5 with: @@ -112,13 +87,15 @@ jobs: - name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }} run: | docker buildx imagetools create \ - --append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64 \ + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-amd64 \ + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64 \ --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest - name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }} run: | docker buildx imagetools create \ - --append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64 \ + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-amd64 \ + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64 \ --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest - uses: sarisia/actions-status-discord@v1 From 73985350ec1dbef1a62c3379ab2bd95eafd6f1b5 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 7 Nov 2025 08:26:58 +0100 Subject: [PATCH 07/64] fix: update version numbers to 4.0.0-beta.443 and 4.0.0-beta.444 --- config/constants.php | 2 +- other/nightly/versions.json | 4 ++-- versions.json | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/config/constants.php b/config/constants.php index 02a1eaae6..770e00ffe 100644 --- a/config/constants.php +++ b/config/constants.php @@ -2,7 +2,7 @@ return [ 'coolify' => [ - 'version' => '4.0.0-beta.442', + 'version' => '4.0.0-beta.443', 'helper_version' => '1.0.12', 'realtime_version' => '1.0.10', 'self_hosted' => env('SELF_HOSTED', true), diff --git a/other/nightly/versions.json b/other/nightly/versions.json index a83b4c8ce..0d9519bf8 100644 --- a/other/nightly/versions.json +++ b/other/nightly/versions.json @@ -1,10 +1,10 @@ { "coolify": { "v4": { - "version": "4.0.0-beta.442" + "version": "4.0.0-beta.443" }, "nightly": { - "version": "4.0.0-beta.443" + "version": "4.0.0-beta.444" }, "helper": { "version": "1.0.11" diff --git a/versions.json b/versions.json index a83b4c8ce..0d9519bf8 100644 --- a/versions.json +++ b/versions.json @@ -1,10 +1,10 @@ { "coolify": { "v4": { - "version": "4.0.0-beta.442" + "version": "4.0.0-beta.443" }, "nightly": { - "version": "4.0.0-beta.443" + "version": "4.0.0-beta.444" }, "helper": { "version": "1.0.11" From 183c70e3c854aeec9dad957939631525f6634cee Mon Sep 17 00:00:00 2001 From: hareland Date: Fri, 7 Nov 2025 13:29:49 +0100 Subject: [PATCH 08/64] **Update rybbit.yaml schema: add category field and adjust tags formatting** --- templates/compose/rybbit.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/templates/compose/rybbit.yaml b/templates/compose/rybbit.yaml index 3c8f7564c..fe214bf16 100644 --- a/templates/compose/rybbit.yaml +++ b/templates/compose/rybbit.yaml @@ -1,6 +1,7 @@ # documentation: https://rybbit.io/docs # slogan: Open-source, privacy-first web analytics. -# tags: analytics,web,privacy,self-hosted,clickhouse,postgres +# category: analytics +# tags: analytics, web, privacy, self-hosted, clickhouse, postgres # logo: svgs/rybbit.svg # port: 3002 @@ -130,4 +131,4 @@ services: 0 - \ No newline at end of file + From b08ea4402add7b41d12ce4a84499bb11beb3a15f Mon Sep 17 00:00:00 2001 From: hareland Date: Fri, 7 Nov 2025 13:46:12 +0100 Subject: [PATCH 09/64] Embystat: change category from 'media' to 'analytics' --- templates/compose/embystat.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/compose/embystat.yaml b/templates/compose/embystat.yaml index 957f67dfb..165a9f21d 100644 --- a/templates/compose/embystat.yaml +++ b/templates/compose/embystat.yaml @@ -1,6 +1,6 @@ # documentation: https://github.com/mregni/EmbyStat # slogan: EmbyStat is a web analytics tool, designed to provide insight into website traffic and user behavior. -# category: media +# category: analytics # tags: media, server, movies, tv, music # port: 6555 From 07ce375ac501dd7d853869d3db07d89b431a5aef Mon Sep 17 00:00:00 2001 From: hareland Date: Fri, 7 Nov 2025 13:50:19 +0100 Subject: [PATCH 10/64] Embystat: change category from 'media' to 'analytics' --- templates/compose/embystat.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/compose/embystat.yaml b/templates/compose/embystat.yaml index 165a9f21d..84e25d4a8 100644 --- a/templates/compose/embystat.yaml +++ b/templates/compose/embystat.yaml @@ -1,7 +1,7 @@ # documentation: https://github.com/mregni/EmbyStat # slogan: EmbyStat is a web analytics tool, designed to provide insight into website traffic and user behavior. # category: analytics -# tags: media, server, movies, tv, music +# tags: analytics, insights, statistics, web, traffic # port: 6555 services: 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 11/64] 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 12/64] 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 13/64] 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 7fd1d799b4f230daa8301df0bf37f7c74e33dcd9 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 14/64] 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 775216e7a57f40e6efb620a0dc826564e081e510 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 7 Nov 2025 14:33:25 +0100 Subject: [PATCH 15/64] jean jean --- jean.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jean.json b/jean.json index c81ca07c4..4e5c788ed 100644 --- a/jean.json +++ b/jean.json @@ -1,5 +1,5 @@ { "scripts": { - "setup": "cp $JEAN_ROOT_PATH/.env . && mkdir -p .claude && cp $JEAN_ROOT_PATH/.claude/settings.local.json .claude/settings.local.json", + "setup": "cp $JEAN_ROOT_PATH/.env . && mkdir -p .claude && cp $JEAN_ROOT_PATH/.claude/settings.local.json .claude/settings.local.json" } } 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 16/64] 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 17/64] 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 18/64] 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 b22e79caec94195eed721cae0cc3e7cf68bf6e2a Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 10 Nov 2025 11:11:18 +0100 Subject: [PATCH 19/64] feat(jobs): improve scheduled tasks with retry logic and queue cleanup - Add retry configuration to CoolifyTask (3 tries, 600s timeout) - Add retry configuration to ScheduledTaskJob (3 tries, configurable timeout) - Add retry configuration to DatabaseBackupJob (2 tries) - Implement exponential backoff for all jobs (30s, 60s, 120s intervals) - Add failed() handlers with comprehensive error logging to scheduled-errors channel - Add execution tracking: started_at, retry_count, duration (decimal), error_details - Add configurable timeout field to scheduled tasks (60-3600s, default 300s) - Update UI to include timeout configuration in task creation/editing forms - Increase ScheduledJobManager lock expiration from 60s to 90s for high-load environments - Implement safe queue cleanup with restart vs runtime modes - Restart mode: aggressive cleanup (marks all processing jobs as failed) - Runtime mode: conservative cleanup (only marks jobs >12h as failed, skips deployments) - Add cleanup:redis --restart flag for system startup - Integrate cleanup into Dev.php init() for development environment - Increase scheduled-errors log retention from 7 to 14 days - Create comprehensive test suite (unit and feature tests) - Add TESTING_GUIDE.md with manual testing instructions Fixes issues with jobs failing after single attempt and "attempted too many times" errors --- TESTING_GUIDE.md | 235 ++++++++++++++++++ app/Console/Commands/CleanupRedis.php | 104 +++++++- app/Console/Commands/Dev.php | 10 + app/Console/Commands/Init.php | 2 +- app/Jobs/CoolifyTask.php | 49 ++++ app/Jobs/DatabaseBackupJob.php | 38 ++- app/Jobs/ScheduledJobManager.php | 2 +- app/Jobs/ScheduledTaskJob.php | 82 +++++- .../Project/Shared/ScheduledTask/Add.php | 6 + .../Project/Shared/ScheduledTask/Show.php | 5 + app/Models/ScheduledTask.php | 8 + app/Models/ScheduledTaskExecution.php | 10 + config/logging.php | 4 +- ...1_add_timeout_to_scheduled_tasks_table.php | 28 +++ ...ove_scheduled_task_executions_tracking.php | 31 +++ package-lock.json | 22 +- .../shared/scheduled-task/add.blade.php | 3 + .../shared/scheduled-task/show.blade.php | 2 + templates/service-templates-latest.json | 16 +- templates/service-templates.json | 16 +- tests/Feature/CoolifyTaskRetryTest.php | 70 ++++++ tests/Unit/ScheduledJobsRetryConfigTest.php | 56 +++++ 22 files changed, 762 insertions(+), 37 deletions(-) create mode 100644 TESTING_GUIDE.md create mode 100644 database/migrations/2025_11_09_000001_add_timeout_to_scheduled_tasks_table.php create mode 100644 database/migrations/2025_11_09_000002_improve_scheduled_task_executions_tracking.php create mode 100644 tests/Feature/CoolifyTaskRetryTest.php create mode 100644 tests/Unit/ScheduledJobsRetryConfigTest.php diff --git a/TESTING_GUIDE.md b/TESTING_GUIDE.md new file mode 100644 index 000000000..91a79cd62 --- /dev/null +++ b/TESTING_GUIDE.md @@ -0,0 +1,235 @@ +# Testing Guide: Scheduled Tasks Improvements + +## Overview +This guide covers testing all the improvements made to the scheduled tasks system, including retry logic, timeout handling, and error logging. + +## Jobs Modified + +1. **CoolifyTask** - Infrastructure job for SSH operations (3 retries, 600s timeout) +2. **ScheduledTaskJob** - Scheduled container commands (3 retries, configurable timeout) +3. **DatabaseBackupJob** - Database backups (2 retries, existing timeout) + +--- + +## Quick Test Commands + +### Run Unit Tests (No Database Required) +```bash +./vendor/bin/pest tests/Unit/ScheduledJobsRetryConfigTest.php +``` + +### Run Feature Tests (Requires Database - Run in Docker) +```bash +docker exec coolify php artisan test --filter=CoolifyTaskRetryTest +``` + +--- + +## Manual Testing + +### 1. Test ScheduledTaskJob ✅ (You tested this) + +**How to test:** +1. Create a scheduled task in the UI +2. Set a short frequency (every minute) +3. Monitor execution in the UI +4. Check logs: `storage/logs/scheduled-errors-2025-11-09.log` + +**What to verify:** +- Task executes successfully +- Duration is recorded (in seconds with 2 decimal places) +- Retry count is tracked +- Timeout configuration is respected + +--- + +### 2. Test DatabaseBackupJob ✅ (You tested this) + +**How to test:** +1. Create a scheduled database backup +2. Set frequency to manual or very short interval +3. Trigger backup manually or wait for schedule +4. Check logs for any errors + +**What to verify:** +- Backup completes successfully +- Retry logic works if there's a transient failure +- Error logging is consistent +- Backoff timing is correct (60s, 300s) + +--- + +### 3. Test CoolifyTask ⚠️ (IMPORTANT - Not tested yet) + +CoolifyTask is used throughout the application for ALL SSH operations. Here are multiple ways to test it: + +#### **Option A: Server Validation** (Easiest) +1. Go to **Servers** in Coolify UI +2. Select any server +3. Click **"Validate Server"** or **"Check Connection"** +4. This triggers CoolifyTask jobs +5. Check Horizon dashboard for job processing +6. Check logs: `storage/logs/scheduled-errors-2025-11-09.log` + +#### **Option B: Container Operations** +1. Go to any **Application** or **Service** +2. Try these actions (each triggers CoolifyTask): + - Restart container + - View logs + - Execute command in container +3. Monitor Horizon for job processing +4. Check logs for errors + +#### **Option C: Application Deployment** +1. Deploy or redeploy any application +2. This triggers MANY CoolifyTask jobs +3. Watch Horizon dashboard - you should see: + - Jobs being dispatched + - Jobs completing successfully + - If any fail, they should retry (check "Failed Jobs") +4. Check logs for retry attempts + +#### **Option D: Docker Cleanup** +1. Wait for or trigger Docker cleanup (runs on schedule) +2. This uses CoolifyTask for cleanup commands +3. Check logs: `storage/logs/scheduled-errors-2025-11-09.log` + +--- + +## Monitoring & Verification + +### Horizon Dashboard +1. Open Horizon: `/horizon` +2. Watch these sections: + - **Recent Jobs** - See jobs being processed + - **Failed Jobs** - Jobs that failed permanently after retries + - **Monitoring** - Job throughput and wait times + +### Log Monitoring +```bash +# Watch scheduled errors in real-time +tail -f storage/logs/scheduled-errors-2025-11-09.log + +# Check for specific job errors +grep "CoolifyTask" storage/logs/scheduled-errors-2025-11-09.log +grep "ScheduledTaskJob" storage/logs/scheduled-errors-2025-11-09.log +grep "DatabaseBackupJob" storage/logs/scheduled-errors-2025-11-09.log +``` + +### Database Verification +```sql +-- Check execution tracking +SELECT * FROM scheduled_task_executions +ORDER BY created_at DESC +LIMIT 10; + +-- Verify duration is decimal (not throwing errors) +SELECT id, duration, retry_count, started_at, finished_at +FROM scheduled_task_executions +WHERE duration IS NOT NULL; + +-- Check for tasks with retries +SELECT * FROM scheduled_task_executions +WHERE retry_count > 0; +``` + +--- + +## Expected Behavior + +### ✅ Success Indicators + +1. **Jobs Complete Successfully** + - Horizon shows completed jobs + - No errors in scheduled-errors log + - Execution records in database + +2. **Retry Logic Works** + - Failed jobs retry automatically + - Backoff timing is respected (30s, 60s, etc.) + - Jobs marked failed only after all retries exhausted + +3. **Timeout Enforcement** + - Long-running jobs terminate at timeout + - Timeout is configurable per task + - No hanging jobs + +4. **Error Logging** + - All errors logged to `storage/logs/scheduled-errors-2025-11-09.log` + - Consistent format with job name, attempt count, error details + - Trace included for debugging + +5. **Execution Tracking** + - Duration recorded correctly (decimal with 2 places) + - Retry count incremented on failures + - Started/finished timestamps accurate + +--- + +## Troubleshooting + +### Issue: Jobs fail immediately without retrying +**Check:** +- Verify `$tries` property is set on the job +- Check if exception is being caught and re-thrown correctly +- Look for `maxExceptions` being reached + +### Issue: "Invalid text representation" errors +**Fix Applied:** +- Duration field changed from integer to decimal(10,2) +- If you see this, run migrations again + +### Issue: Jobs not appearing in Horizon +**Check:** +- Horizon is running (`php artisan horizon`) +- Queue workers are active +- Job is dispatched to correct queue ('high' for these jobs) + +### Issue: Timeout not working +**Check:** +- Timeout is set on job (CoolifyTask: 600s, ScheduledTask: configurable) +- PHP `max_execution_time` allows job timeout +- Queue worker timeout is higher than job timeout + +--- + +## Test Checklist + +- [ ] Unit tests pass: `./vendor/bin/pest tests/Unit/ScheduledJobsRetryConfigTest.php` +- [ ] ScheduledTaskJob tested manually ✅ +- [ ] DatabaseBackupJob tested manually ✅ +- [ ] CoolifyTask tested manually (server validation, container ops, or deployment) +- [ ] Retry logic verified (force a failure, watch retry attempts) +- [ ] Timeout enforcement tested (create long-running task with short timeout) +- [ ] Error logs checked: `storage/logs/scheduled-errors-2025-11-09.log` +- [ ] Horizon dashboard shows jobs processing correctly +- [ ] Database execution records show duration as decimal +- [ ] UI shows timeout configuration field for scheduled tasks + +--- + +## Next Steps After Testing + +1. If all tests pass, run migrations on production/staging: + ```bash + php artisan migrate + ``` + +2. Monitor logs for the first 24 hours: + ```bash + tail -f storage/logs/scheduled-errors-2025-11-09.log + ``` + +3. Check Horizon for any failed jobs needing attention + +4. Verify existing scheduled tasks now have retry capability + +--- + +## Questions? + +If you encounter issues: +1. Check `storage/logs/scheduled-errors-2025-11-09.log` first +2. Check `storage/logs/laravel.log` for general errors +3. Look at Horizon "Failed Jobs" for detailed error info +4. Review database execution records for patterns diff --git a/app/Console/Commands/CleanupRedis.php b/app/Console/Commands/CleanupRedis.php index f6a2de75b..a5fdc33e0 100644 --- a/app/Console/Commands/CleanupRedis.php +++ b/app/Console/Commands/CleanupRedis.php @@ -7,7 +7,7 @@ class CleanupRedis extends Command { - protected $signature = 'cleanup:redis {--dry-run : Show what would be deleted without actually deleting} {--skip-overlapping : Skip overlapping queue cleanup} {--clear-locks : Clear stale WithoutOverlapping locks}'; + protected $signature = 'cleanup:redis {--dry-run : Show what would be deleted without actually deleting} {--skip-overlapping : Skip overlapping queue cleanup} {--clear-locks : Clear stale WithoutOverlapping locks} {--restart : Aggressive cleanup mode for system restart (marks all processing jobs as failed)}'; protected $description = 'Cleanup Redis (Horizon jobs, metrics, overlapping queues, cache locks, and related data)'; @@ -63,6 +63,14 @@ public function handle() $deletedCount += $locksCleaned; } + // Clean up stuck jobs (restart mode = aggressive, runtime mode = conservative) + $isRestart = $this->option('restart'); + if ($isRestart || $this->option('clear-locks')) { + $this->info($isRestart ? 'Cleaning up stuck jobs (RESTART MODE - aggressive)...' : 'Checking for stuck jobs (runtime mode - conservative)...'); + $jobsCleaned = $this->cleanupStuckJobs($redis, $prefix, $dryRun, $isRestart); + $deletedCount += $jobsCleaned; + } + if ($dryRun) { $this->info("DRY RUN: Would delete {$deletedCount} out of {$totalKeys} keys"); } else { @@ -332,4 +340,98 @@ private function cleanupCacheLocks(bool $dryRun): int return $cleanedCount; } + + /** + * Clean up stuck jobs based on mode (restart vs runtime). + * + * @param mixed $redis Redis connection + * @param string $prefix Horizon prefix + * @param bool $dryRun Dry run mode + * @param bool $isRestart Restart mode (aggressive) vs runtime mode (conservative) + * @return int Number of jobs cleaned + */ + private function cleanupStuckJobs($redis, string $prefix, bool $dryRun, bool $isRestart): int + { + $cleanedCount = 0; + $now = time(); + + // Get all keys with the horizon prefix + $keys = $redis->keys('*'); + + foreach ($keys as $key) { + $keyWithoutPrefix = str_replace($prefix, '', $key); + $type = $redis->command('type', [$keyWithoutPrefix]); + + // Only process hash-type keys (individual jobs) + if ($type !== 5) { + continue; + } + + $data = $redis->command('hgetall', [$keyWithoutPrefix]); + $status = data_get($data, 'status'); + $payload = data_get($data, 'payload'); + + // Only process jobs in "processing" or "reserved" state + if (! in_array($status, ['processing', 'reserved'])) { + continue; + } + + // Parse job payload to get job class and started time + $payloadData = json_decode($payload, true); + $jobClass = data_get($payloadData, 'displayName', 'Unknown'); + $pushedAt = (int) data_get($data, 'pushed_at', 0); + + // Calculate how long the job has been processing + $processingTime = $now - $pushedAt; + + $shouldFail = false; + $reason = ''; + + if ($isRestart) { + // RESTART MODE: Mark ALL processing/reserved jobs as failed + // Safe because all workers are dead on restart + $shouldFail = true; + $reason = 'System restart - all workers terminated'; + } else { + // RUNTIME MODE: Only mark truly stuck jobs as failed + // Be conservative to avoid killing legitimate long-running jobs + + // Skip ApplicationDeploymentJob entirely (has dynamic_timeout, can run 2+ hours) + if (str_contains($jobClass, 'ApplicationDeploymentJob')) { + continue; + } + + // Skip DatabaseBackupJob (large backups can take hours) + if (str_contains($jobClass, 'DatabaseBackupJob')) { + continue; + } + + // For other jobs, only fail if processing > 12 hours + if ($processingTime > 43200) { // 12 hours + $shouldFail = true; + $reason = 'Processing for more than 12 hours'; + } + } + + if ($shouldFail) { + if ($dryRun) { + $this->warn(" Would mark as FAILED: {$jobClass} (processing for ".round($processingTime / 60, 1)." min) - {$reason}"); + } else { + // Mark job as failed + $redis->command('hset', [$keyWithoutPrefix, 'status', 'failed']); + $redis->command('hset', [$keyWithoutPrefix, 'failed_at', $now]); + $redis->command('hset', [$keyWithoutPrefix, 'exception', "Job cleaned up by cleanup:redis - {$reason}"]); + + $this->info(" ✓ Marked as FAILED: {$jobClass} (processing for ".round($processingTime / 60, 1).' min) - '.$reason); + } + $cleanedCount++; + } + } + + if ($cleanedCount === 0) { + $this->info($isRestart ? ' No jobs to clean up' : ' No stuck jobs found (all jobs running normally)'); + } + + return $cleanedCount; + } } diff --git a/app/Console/Commands/Dev.php b/app/Console/Commands/Dev.php index 8f26d78ff..f04d67482 100644 --- a/app/Console/Commands/Dev.php +++ b/app/Console/Commands/Dev.php @@ -45,6 +45,16 @@ public function init() } else { echo "Instance already initialized.\n"; } + + // Clean up stuck jobs and stale locks on development startup + try { + echo "Cleaning up Redis (stuck jobs and stale locks)...\n"; + Artisan::call('cleanup:redis', ['--restart' => true, '--clear-locks' => true]); + echo "Redis cleanup completed.\n"; + } catch (\Throwable $e) { + echo "Error in cleanup:redis: {$e->getMessage()}\n"; + } + CheckHelperImageJob::dispatch(); } } diff --git a/app/Console/Commands/Init.php b/app/Console/Commands/Init.php index 4bc818f0a..aba99199e 100644 --- a/app/Console/Commands/Init.php +++ b/app/Console/Commands/Init.php @@ -73,7 +73,7 @@ public function handle() $this->cleanupUnusedNetworkFromCoolifyProxy(); try { - $this->call('cleanup:redis', ['--clear-locks' => true]); + $this->call('cleanup:redis', ['--restart' => true, '--clear-locks' => true]); } catch (\Throwable $e) { echo "Error in cleanup:redis command: {$e->getMessage()}\n"; } diff --git a/app/Jobs/CoolifyTask.php b/app/Jobs/CoolifyTask.php index 49a5ba8dd..d6dc6fa05 100755 --- a/app/Jobs/CoolifyTask.php +++ b/app/Jobs/CoolifyTask.php @@ -3,18 +3,35 @@ namespace App\Jobs; use App\Actions\CoolifyTask\RunRemoteProcess; +use App\Enums\ProcessStatus; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Facades\Log; use Spatie\Activitylog\Models\Activity; class CoolifyTask implements ShouldBeEncrypted, ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; + /** + * The number of times the job may be attempted. + */ + public $tries = 3; + + /** + * The maximum number of unhandled exceptions to allow before failing. + */ + public $maxExceptions = 1; + + /** + * The number of seconds the job can run before timing out. + */ + public $timeout = 600; + /** * Create a new job instance. */ @@ -42,4 +59,36 @@ public function handle(): void $remote_process(); } + + /** + * Calculate the number of seconds to wait before retrying the job. + */ + public function backoff(): array + { + return [30, 90, 180]; // 30s, 90s, 180s between retries + } + + /** + * Handle a job failure. + */ + public function failed(?\Throwable $exception): void + { + Log::channel('scheduled-errors')->error('CoolifyTask permanently failed', [ + 'job' => 'CoolifyTask', + 'activity_id' => $this->activity->id, + 'server_uuid' => $this->activity->getExtraProperty('server_uuid'), + 'command_preview' => substr($this->activity->getExtraProperty('command') ?? '', 0, 200), + 'error' => $exception?->getMessage(), + 'total_attempts' => $this->attempts(), + 'trace' => $exception?->getTraceAsString(), + ]); + + // Update activity status to reflect permanent failure + $this->activity->properties = $this->activity->properties->merge([ + 'status' => ProcessStatus::ERROR->value, + 'error' => $exception?->getMessage() ?? 'Job permanently failed', + 'failed_at' => now()->toIso8601String(), + ]); + $this->activity->save(); + } } diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php index 45586f0d0..ff16c78e0 100644 --- a/app/Jobs/DatabaseBackupJob.php +++ b/app/Jobs/DatabaseBackupJob.php @@ -23,6 +23,7 @@ use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Facades\Log; use Illuminate\Support\Str; use Throwable; use Visus\Cuid2\Cuid2; @@ -31,6 +32,16 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; + /** + * The number of times the job may be attempted. + */ + public $tries = 2; + + /** + * The maximum number of unhandled exceptions to allow before failing. + */ + public $maxExceptions = 1; + public ?Team $team = null; public Server $server; @@ -659,17 +670,42 @@ private function getFullImageName(): string return "{$helperImage}:{$latestVersion}"; } + /** + * Calculate the number of seconds to wait before retrying the job. + */ + public function backoff(): array + { + return [60, 300]; // 1min, 5min between retries + } + public function failed(?Throwable $exception): void { + Log::channel('scheduled-errors')->error('DatabaseBackup permanently failed', [ + 'job' => 'DatabaseBackupJob', + 'backup_id' => $this->backup->uuid, + 'database' => $this->database?->name ?? 'unknown', + 'database_type' => get_class($this->database ?? new \stdClass), + 'server' => $this->server?->name ?? 'unknown', + 'total_attempts' => $this->attempts(), + 'error' => $exception?->getMessage(), + 'trace' => $exception?->getTraceAsString(), + ]); + $log = ScheduledDatabaseBackupExecution::where('uuid', $this->backup_log_uuid)->first(); if ($log) { $log->update([ 'status' => 'failed', - 'message' => 'Job failed: '.($exception?->getMessage() ?? 'Unknown error'), + 'message' => 'Job permanently failed after '.$this->attempts().' attempts: '.($exception?->getMessage() ?? 'Unknown error'), 'size' => 0, 'filename' => null, + 'finished_at' => Carbon::now(), ]); } + + // Notify team about permanent failure + if ($this->team) { + $this->team->notify(new BackupFailed($this->backup, $this->database, $this->backup_output)); + } } } diff --git a/app/Jobs/ScheduledJobManager.php b/app/Jobs/ScheduledJobManager.php index 9937444b8..75ff883c2 100644 --- a/app/Jobs/ScheduledJobManager.php +++ b/app/Jobs/ScheduledJobManager.php @@ -52,7 +52,7 @@ public function middleware(): array { return [ (new WithoutOverlapping('scheduled-job-manager')) - ->expireAfter(60) // Lock expires after 1 minute to prevent stale locks + ->expireAfter(90) // Lock expires after 90s to handle high-load environments with many tasks ->dontRelease(), // Don't re-queue on lock conflict ]; } diff --git a/app/Jobs/ScheduledTaskJob.php b/app/Jobs/ScheduledTaskJob.php index 609595356..95c3b0eaf 100644 --- a/app/Jobs/ScheduledTaskJob.php +++ b/app/Jobs/ScheduledTaskJob.php @@ -18,11 +18,27 @@ use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Facades\Log; class ScheduledTaskJob implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; + /** + * The number of times the job may be attempted. + */ + public $tries = 3; + + /** + * The maximum number of unhandled exceptions to allow before failing. + */ + public $maxExceptions = 1; + + /** + * The number of seconds the job can run before timing out. + */ + public $timeout = 300; + public Team $team; public Server $server; @@ -55,6 +71,9 @@ public function __construct($task) } $this->team = Team::findOrFail($task->team_id); $this->server_timezone = $this->getServerTimezone(); + + // Set timeout from task configuration + $this->timeout = $this->task->timeout ?? 300; } private function getServerTimezone(): string @@ -70,9 +89,13 @@ private function getServerTimezone(): string public function handle(): void { + $startTime = Carbon::now(); + try { $this->task_log = ScheduledTaskExecution::create([ 'scheduled_task_id' => $this->task->id, + 'started_at' => $startTime, + 'retry_count' => $this->attempts() - 1, ]); $this->server = $this->resource->destination->server; @@ -129,15 +152,70 @@ public function handle(): void 'message' => $this->task_output ?? $e->getMessage(), ]); } - $this->team?->notify(new TaskFailed($this->task, $e->getMessage())); + + // Log the error to the scheduled-errors channel + Log::channel('scheduled-errors')->error('ScheduledTask execution failed', [ + 'job' => 'ScheduledTaskJob', + 'task_id' => $this->task->uuid, + 'task_name' => $this->task->name, + 'server' => $this->server->name ?? 'unknown', + 'attempt' => $this->attempts(), + 'error' => $e->getMessage(), + ]); + + // Only notify and throw on final failure + if ($this->attempts() >= $this->tries) { + $this->team?->notify(new TaskFailed($this->task, $e->getMessage())); + } + + // Re-throw to trigger Laravel's retry mechanism with backoff throw $e; } finally { ScheduledTaskDone::dispatch($this->team->id); if ($this->task_log) { + $finishedAt = Carbon::now(); + $duration = round($startTime->floatDiffInSeconds($finishedAt), 2); + $this->task_log->update([ - 'finished_at' => Carbon::now()->toImmutable(), + 'finished_at' => $finishedAt->toImmutable(), + 'duration' => $duration, ]); } } } + + /** + * Calculate the number of seconds to wait before retrying the job. + */ + public function backoff(): array + { + return [30, 60, 120]; // 30s, 60s, 120s between retries + } + + /** + * Handle a job failure. + */ + public function failed(?\Throwable $exception): void + { + Log::channel('scheduled-errors')->error('ScheduledTask permanently failed', [ + 'job' => 'ScheduledTaskJob', + 'task_id' => $this->task->uuid, + 'task_name' => $this->task->name, + 'server' => $this->server->name ?? 'unknown', + 'total_attempts' => $this->attempts(), + 'error' => $exception?->getMessage(), + 'trace' => $exception?->getTraceAsString(), + ]); + + if ($this->task_log) { + $this->task_log->update([ + 'status' => 'failed', + 'message' => 'Job permanently failed after '.$this->attempts().' attempts: '.($exception?->getMessage() ?? 'Unknown error'), + 'finished_at' => Carbon::now()->toImmutable(), + ]); + } + + // Notify team about permanent failure + $this->team?->notify(new TaskFailed($this->task, $exception?->getMessage() ?? 'Unknown error')); + } } diff --git a/app/Livewire/Project/Shared/ScheduledTask/Add.php b/app/Livewire/Project/Shared/ScheduledTask/Add.php index e4b666532..d7210c15d 100644 --- a/app/Livewire/Project/Shared/ScheduledTask/Add.php +++ b/app/Livewire/Project/Shared/ScheduledTask/Add.php @@ -34,11 +34,14 @@ class Add extends Component public ?string $container = ''; + public int $timeout = 300; + protected $rules = [ 'name' => 'required|string', 'command' => 'required|string', 'frequency' => 'required|string', 'container' => 'nullable|string', + 'timeout' => 'required|integer|min:60|max:3600', ]; protected $validationAttributes = [ @@ -46,6 +49,7 @@ class Add extends Component 'command' => 'command', 'frequency' => 'frequency', 'container' => 'container', + 'timeout' => 'timeout', ]; public function mount() @@ -103,6 +107,7 @@ public function saveScheduledTask() $task->command = $this->command; $task->frequency = $this->frequency; $task->container = $this->container; + $task->timeout = $this->timeout; $task->team_id = currentTeam()->id; switch ($this->type) { @@ -130,5 +135,6 @@ public function clear() $this->command = ''; $this->frequency = ''; $this->container = ''; + $this->timeout = 300; } } diff --git a/app/Livewire/Project/Shared/ScheduledTask/Show.php b/app/Livewire/Project/Shared/ScheduledTask/Show.php index c8d07ae36..920a0efe5 100644 --- a/app/Livewire/Project/Shared/ScheduledTask/Show.php +++ b/app/Livewire/Project/Shared/ScheduledTask/Show.php @@ -40,6 +40,9 @@ class Show extends Component #[Validate(['string', 'nullable'])] public ?string $container = null; + #[Validate(['integer', 'required', 'min:60', 'max:3600'])] + public int $timeout = 300; + #[Locked] public ?string $application_uuid; @@ -99,6 +102,7 @@ public function syncData(bool $toModel = false) $this->task->command = str($this->command)->trim()->value(); $this->task->frequency = str($this->frequency)->trim()->value(); $this->task->container = str($this->container)->trim()->value(); + $this->task->timeout = $this->timeout; $this->task->save(); } else { $this->isEnabled = $this->task->enabled; @@ -106,6 +110,7 @@ public function syncData(bool $toModel = false) $this->command = $this->task->command; $this->frequency = $this->task->frequency; $this->container = $this->task->container; + $this->timeout = $this->task->timeout ?? 300; } } diff --git a/app/Models/ScheduledTask.php b/app/Models/ScheduledTask.php index 06903ffb6..bada0b7a5 100644 --- a/app/Models/ScheduledTask.php +++ b/app/Models/ScheduledTask.php @@ -12,6 +12,14 @@ class ScheduledTask extends BaseModel protected $guarded = []; + protected function casts(): array + { + return [ + 'enabled' => 'boolean', + 'timeout' => 'integer', + ]; + } + public function service() { return $this->belongsTo(Service::class); diff --git a/app/Models/ScheduledTaskExecution.php b/app/Models/ScheduledTaskExecution.php index de13fefb0..02fd6917a 100644 --- a/app/Models/ScheduledTaskExecution.php +++ b/app/Models/ScheduledTaskExecution.php @@ -8,6 +8,16 @@ class ScheduledTaskExecution extends BaseModel { protected $guarded = []; + protected function casts(): array + { + return [ + 'started_at' => 'datetime', + 'finished_at' => 'datetime', + 'retry_count' => 'integer', + 'duration' => 'decimal:2', + ]; + } + public function scheduledTask(): BelongsTo { return $this->belongsTo(ScheduledTask::class); diff --git a/config/logging.php b/config/logging.php index 488327414..1a75978f3 100644 --- a/config/logging.php +++ b/config/logging.php @@ -129,8 +129,8 @@ 'scheduled-errors' => [ 'driver' => 'daily', 'path' => storage_path('logs/scheduled-errors.log'), - 'level' => 'debug', - 'days' => 7, + 'level' => 'warning', + 'days' => 14, ], ], diff --git a/database/migrations/2025_11_09_000001_add_timeout_to_scheduled_tasks_table.php b/database/migrations/2025_11_09_000001_add_timeout_to_scheduled_tasks_table.php new file mode 100644 index 000000000..067861e16 --- /dev/null +++ b/database/migrations/2025_11_09_000001_add_timeout_to_scheduled_tasks_table.php @@ -0,0 +1,28 @@ +integer('timeout')->default(300)->after('frequency'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('scheduled_tasks', function (Blueprint $table) { + $table->dropColumn('timeout'); + }); + } +}; diff --git a/database/migrations/2025_11_09_000002_improve_scheduled_task_executions_tracking.php b/database/migrations/2025_11_09_000002_improve_scheduled_task_executions_tracking.php new file mode 100644 index 000000000..14fdd5998 --- /dev/null +++ b/database/migrations/2025_11_09_000002_improve_scheduled_task_executions_tracking.php @@ -0,0 +1,31 @@ +timestamp('started_at')->nullable()->after('scheduled_task_id'); + $table->integer('retry_count')->default(0)->after('status'); + $table->decimal('duration', 10, 2)->nullable()->after('retry_count')->comment('Duration in seconds'); + $table->text('error_details')->nullable()->after('message'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('scheduled_task_executions', function (Blueprint $table) { + $table->dropColumn(['started_at', 'retry_count', 'duration', 'error_details']); + }); + } +}; diff --git a/package-lock.json b/package-lock.json index f8ef518d2..b076800e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -916,8 +916,7 @@ "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@tailwindcss/forms": { "version": "0.5.10", @@ -1432,7 +1431,8 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/asynckit": { "version": "0.4.0", @@ -1595,7 +1595,6 @@ "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1", @@ -1610,7 +1609,6 @@ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ms": "^2.1.3" }, @@ -1629,7 +1627,6 @@ "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10.0.0" } @@ -2391,6 +2388,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -2467,6 +2465,7 @@ "integrity": "sha512-wp3HqIIUc1GRyu1XrP6m2dgyE9MoCsXVsWNlohj0rjSkLf+a0jLvEyVubdg58oMk7bhjBWnFClgp8jfAa6Ak4Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "tweetnacl": "^1.0.3" } @@ -2551,7 +2550,6 @@ "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.2", @@ -2568,7 +2566,6 @@ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ms": "^2.1.3" }, @@ -2587,7 +2584,6 @@ "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1" @@ -2602,7 +2598,6 @@ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ms": "^2.1.3" }, @@ -2651,7 +2646,8 @@ "version": "4.1.10", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.10.tgz", "integrity": "sha512-P3nr6WkvKV/ONsTzj6Gb57sWPMX29EPNPopo7+FcpkQaNsrNpZ1pv8QmrYI2RqEKD7mlGqLnGovlcYnBK0IqUA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/tapable": { "version": "2.3.0", @@ -2720,6 +2716,7 @@ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -2819,6 +2816,7 @@ "integrity": "sha512-rjOV2ecxMd5SiAmof2xzh2WxntRcigkX/He4YFJ6WdRvVUrbt6DxC1Iujh10XLl8xCDRDtGKMeO3D+pRQ1PP9w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.16", "@vue/compiler-sfc": "3.5.16", @@ -2841,7 +2839,6 @@ "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10.0.0" }, @@ -2863,7 +2860,6 @@ "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", "dev": true, - "peer": true, "engines": { "node": ">=0.4.0" } diff --git a/resources/views/livewire/project/shared/scheduled-task/add.blade.php b/resources/views/livewire/project/shared/scheduled-task/add.blade.php index 0c4b8a4d6..6fa04c28b 100644 --- a/resources/views/livewire/project/shared/scheduled-task/add.blade.php +++ b/resources/views/livewire/project/shared/scheduled-task/add.blade.php @@ -4,6 +4,9 @@ + @if ($type === 'application') @if ($containerNames->count() > 1) diff --git a/resources/views/livewire/project/shared/scheduled-task/show.blade.php b/resources/views/livewire/project/shared/scheduled-task/show.blade.php index 1ede7775a..fa2ce0ad9 100644 --- a/resources/views/livewire/project/shared/scheduled-task/show.blade.php +++ b/resources/views/livewire/project/shared/scheduled-task/show.blade.php @@ -35,6 +35,8 @@ + @if ($type === 'application') first(); + + if (! $server) { + $this->markTestSkipped('No servers available for testing'); + } + + Queue::fake(); + + // Create an activity for the task + $activity = activity() + ->withProperties([ + 'server_uuid' => $server->uuid, + 'command' => 'echo "test"', + 'type' => 'inline', + ]) + ->event('inline') + ->log('[]'); + + // Dispatch the job + CoolifyTask::dispatch( + activity: $activity, + ignore_errors: false, + call_event_on_finish: null, + call_event_data: null + ); + + // Assert job was dispatched + Queue::assertPushed(CoolifyTask::class); +}); + +it('has correct retry configuration on CoolifyTask', function () { + $server = Server::where('ip', '!=', '1.2.3.4')->first(); + + if (! $server) { + $this->markTestSkipped('No servers available for testing'); + } + + $activity = activity() + ->withProperties([ + 'server_uuid' => $server->uuid, + 'command' => 'echo "test"', + 'type' => 'inline', + ]) + ->event('inline') + ->log('[]'); + + $job = new CoolifyTask( + activity: $activity, + ignore_errors: false, + call_event_on_finish: null, + call_event_data: null + ); + + // Assert retry configuration + expect($job->tries)->toBe(3); + expect($job->maxExceptions)->toBe(1); + expect($job->timeout)->toBe(600); + expect($job->backoff())->toBe([30, 90, 180]); +}); diff --git a/tests/Unit/ScheduledJobsRetryConfigTest.php b/tests/Unit/ScheduledJobsRetryConfigTest.php new file mode 100644 index 000000000..bf959a0f5 --- /dev/null +++ b/tests/Unit/ScheduledJobsRetryConfigTest.php @@ -0,0 +1,56 @@ +hasProperty('tries'))->toBeTrue() + ->and($reflection->hasProperty('maxExceptions'))->toBeTrue() + ->and($reflection->hasProperty('timeout'))->toBeTrue() + ->and($reflection->hasMethod('backoff'))->toBeTrue(); + + // Get default values from class definition + $defaultProperties = $reflection->getDefaultProperties(); + + expect($defaultProperties['tries'])->toBe(3) + ->and($defaultProperties['maxExceptions'])->toBe(1) + ->and($defaultProperties['timeout'])->toBe(600); +}); + +it('ScheduledTaskJob has correct retry properties defined', function () { + $reflection = new ReflectionClass(ScheduledTaskJob::class); + + // Check public properties exist + expect($reflection->hasProperty('tries'))->toBeTrue() + ->and($reflection->hasProperty('maxExceptions'))->toBeTrue() + ->and($reflection->hasProperty('timeout'))->toBeTrue() + ->and($reflection->hasMethod('backoff'))->toBeTrue() + ->and($reflection->hasMethod('failed'))->toBeTrue(); + + // Get default values from class definition + $defaultProperties = $reflection->getDefaultProperties(); + + expect($defaultProperties['tries'])->toBe(3) + ->and($defaultProperties['maxExceptions'])->toBe(1) + ->and($defaultProperties['timeout'])->toBe(300); +}); + +it('DatabaseBackupJob has correct retry properties defined', function () { + $reflection = new ReflectionClass(DatabaseBackupJob::class); + + // Check public properties exist + expect($reflection->hasProperty('tries'))->toBeTrue() + ->and($reflection->hasProperty('maxExceptions'))->toBeTrue() + ->and($reflection->hasMethod('backoff'))->toBeTrue() + ->and($reflection->hasMethod('failed'))->toBeTrue(); + + // Get default values from class definition + $defaultProperties = $reflection->getDefaultProperties(); + + expect($defaultProperties['tries'])->toBe(2) + ->and($defaultProperties['maxExceptions'])->toBe(1); +}); 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 20/64] 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 1580c0d3add99af2f8b2d3cde5ee00e2ac8df1dc Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 10 Nov 2025 11:41:50 +0100 Subject: [PATCH 21/64] Update app/Jobs/ScheduledTaskJob.php Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- app/Jobs/ScheduledTaskJob.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/Jobs/ScheduledTaskJob.php b/app/Jobs/ScheduledTaskJob.php index 95c3b0eaf..41a6ec8e2 100644 --- a/app/Jobs/ScheduledTaskJob.php +++ b/app/Jobs/ScheduledTaskJob.php @@ -164,9 +164,6 @@ public function handle(): void ]); // Only notify and throw on final failure - if ($this->attempts() >= $this->tries) { - $this->team?->notify(new TaskFailed($this->task, $e->getMessage())); - } // Re-throw to trigger Laravel's retry mechanism with backoff throw $e; From 372ea268103b77cb47a648e63df392d1122672f1 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 10 Nov 2025 11:42:18 +0100 Subject: [PATCH 22/64] chore: remove outdated testing guide for scheduled tasks --- TESTING_GUIDE.md | 235 ----------------------------------------------- 1 file changed, 235 deletions(-) delete mode 100644 TESTING_GUIDE.md diff --git a/TESTING_GUIDE.md b/TESTING_GUIDE.md deleted file mode 100644 index 91a79cd62..000000000 --- a/TESTING_GUIDE.md +++ /dev/null @@ -1,235 +0,0 @@ -# Testing Guide: Scheduled Tasks Improvements - -## Overview -This guide covers testing all the improvements made to the scheduled tasks system, including retry logic, timeout handling, and error logging. - -## Jobs Modified - -1. **CoolifyTask** - Infrastructure job for SSH operations (3 retries, 600s timeout) -2. **ScheduledTaskJob** - Scheduled container commands (3 retries, configurable timeout) -3. **DatabaseBackupJob** - Database backups (2 retries, existing timeout) - ---- - -## Quick Test Commands - -### Run Unit Tests (No Database Required) -```bash -./vendor/bin/pest tests/Unit/ScheduledJobsRetryConfigTest.php -``` - -### Run Feature Tests (Requires Database - Run in Docker) -```bash -docker exec coolify php artisan test --filter=CoolifyTaskRetryTest -``` - ---- - -## Manual Testing - -### 1. Test ScheduledTaskJob ✅ (You tested this) - -**How to test:** -1. Create a scheduled task in the UI -2. Set a short frequency (every minute) -3. Monitor execution in the UI -4. Check logs: `storage/logs/scheduled-errors-2025-11-09.log` - -**What to verify:** -- Task executes successfully -- Duration is recorded (in seconds with 2 decimal places) -- Retry count is tracked -- Timeout configuration is respected - ---- - -### 2. Test DatabaseBackupJob ✅ (You tested this) - -**How to test:** -1. Create a scheduled database backup -2. Set frequency to manual or very short interval -3. Trigger backup manually or wait for schedule -4. Check logs for any errors - -**What to verify:** -- Backup completes successfully -- Retry logic works if there's a transient failure -- Error logging is consistent -- Backoff timing is correct (60s, 300s) - ---- - -### 3. Test CoolifyTask ⚠️ (IMPORTANT - Not tested yet) - -CoolifyTask is used throughout the application for ALL SSH operations. Here are multiple ways to test it: - -#### **Option A: Server Validation** (Easiest) -1. Go to **Servers** in Coolify UI -2. Select any server -3. Click **"Validate Server"** or **"Check Connection"** -4. This triggers CoolifyTask jobs -5. Check Horizon dashboard for job processing -6. Check logs: `storage/logs/scheduled-errors-2025-11-09.log` - -#### **Option B: Container Operations** -1. Go to any **Application** or **Service** -2. Try these actions (each triggers CoolifyTask): - - Restart container - - View logs - - Execute command in container -3. Monitor Horizon for job processing -4. Check logs for errors - -#### **Option C: Application Deployment** -1. Deploy or redeploy any application -2. This triggers MANY CoolifyTask jobs -3. Watch Horizon dashboard - you should see: - - Jobs being dispatched - - Jobs completing successfully - - If any fail, they should retry (check "Failed Jobs") -4. Check logs for retry attempts - -#### **Option D: Docker Cleanup** -1. Wait for or trigger Docker cleanup (runs on schedule) -2. This uses CoolifyTask for cleanup commands -3. Check logs: `storage/logs/scheduled-errors-2025-11-09.log` - ---- - -## Monitoring & Verification - -### Horizon Dashboard -1. Open Horizon: `/horizon` -2. Watch these sections: - - **Recent Jobs** - See jobs being processed - - **Failed Jobs** - Jobs that failed permanently after retries - - **Monitoring** - Job throughput and wait times - -### Log Monitoring -```bash -# Watch scheduled errors in real-time -tail -f storage/logs/scheduled-errors-2025-11-09.log - -# Check for specific job errors -grep "CoolifyTask" storage/logs/scheduled-errors-2025-11-09.log -grep "ScheduledTaskJob" storage/logs/scheduled-errors-2025-11-09.log -grep "DatabaseBackupJob" storage/logs/scheduled-errors-2025-11-09.log -``` - -### Database Verification -```sql --- Check execution tracking -SELECT * FROM scheduled_task_executions -ORDER BY created_at DESC -LIMIT 10; - --- Verify duration is decimal (not throwing errors) -SELECT id, duration, retry_count, started_at, finished_at -FROM scheduled_task_executions -WHERE duration IS NOT NULL; - --- Check for tasks with retries -SELECT * FROM scheduled_task_executions -WHERE retry_count > 0; -``` - ---- - -## Expected Behavior - -### ✅ Success Indicators - -1. **Jobs Complete Successfully** - - Horizon shows completed jobs - - No errors in scheduled-errors log - - Execution records in database - -2. **Retry Logic Works** - - Failed jobs retry automatically - - Backoff timing is respected (30s, 60s, etc.) - - Jobs marked failed only after all retries exhausted - -3. **Timeout Enforcement** - - Long-running jobs terminate at timeout - - Timeout is configurable per task - - No hanging jobs - -4. **Error Logging** - - All errors logged to `storage/logs/scheduled-errors-2025-11-09.log` - - Consistent format with job name, attempt count, error details - - Trace included for debugging - -5. **Execution Tracking** - - Duration recorded correctly (decimal with 2 places) - - Retry count incremented on failures - - Started/finished timestamps accurate - ---- - -## Troubleshooting - -### Issue: Jobs fail immediately without retrying -**Check:** -- Verify `$tries` property is set on the job -- Check if exception is being caught and re-thrown correctly -- Look for `maxExceptions` being reached - -### Issue: "Invalid text representation" errors -**Fix Applied:** -- Duration field changed from integer to decimal(10,2) -- If you see this, run migrations again - -### Issue: Jobs not appearing in Horizon -**Check:** -- Horizon is running (`php artisan horizon`) -- Queue workers are active -- Job is dispatched to correct queue ('high' for these jobs) - -### Issue: Timeout not working -**Check:** -- Timeout is set on job (CoolifyTask: 600s, ScheduledTask: configurable) -- PHP `max_execution_time` allows job timeout -- Queue worker timeout is higher than job timeout - ---- - -## Test Checklist - -- [ ] Unit tests pass: `./vendor/bin/pest tests/Unit/ScheduledJobsRetryConfigTest.php` -- [ ] ScheduledTaskJob tested manually ✅ -- [ ] DatabaseBackupJob tested manually ✅ -- [ ] CoolifyTask tested manually (server validation, container ops, or deployment) -- [ ] Retry logic verified (force a failure, watch retry attempts) -- [ ] Timeout enforcement tested (create long-running task with short timeout) -- [ ] Error logs checked: `storage/logs/scheduled-errors-2025-11-09.log` -- [ ] Horizon dashboard shows jobs processing correctly -- [ ] Database execution records show duration as decimal -- [ ] UI shows timeout configuration field for scheduled tasks - ---- - -## Next Steps After Testing - -1. If all tests pass, run migrations on production/staging: - ```bash - php artisan migrate - ``` - -2. Monitor logs for the first 24 hours: - ```bash - tail -f storage/logs/scheduled-errors-2025-11-09.log - ``` - -3. Check Horizon for any failed jobs needing attention - -4. Verify existing scheduled tasks now have retry capability - ---- - -## Questions? - -If you encounter issues: -1. Check `storage/logs/scheduled-errors-2025-11-09.log` first -2. Check `storage/logs/laravel.log` for general errors -3. Look at Horizon "Failed Jobs" for detailed error info -4. Review database execution records for patterns 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 23/64] 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)