diff --git a/.github/workflows/pocketbase-release.yml b/.github/workflows/pocketbase-release.yml new file mode 100644 index 000000000..27b639a42 --- /dev/null +++ b/.github/workflows/pocketbase-release.yml @@ -0,0 +1,93 @@ +name: pocketbase-release + +on: + push: + paths: + - "others/pocketbase" + - ".github/workflows/pocketbase-release.yml" + branches: + - next + +jobs: + arm64: + runs-on: [self-hosted, arm64] + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + - name: Login to DockerHub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Build and push + uses: docker/build-push-action@v2 + with: + context: others/pocketbase/ + platforms: linux/arm64 + push: true + tags: coollabsio/pocketbase:0.8.0-arm64 + amd64: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - name: Login to DockerHub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Build and push + uses: docker/build-push-action@v3 + with: + context: others/pocketbase/ + platforms: linux/amd64 + push: true + tags: coollabsio/pocketbase:0.8.0-amd64 + aarch64: + runs-on: [self-hosted, arm64] + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + - name: Login to DockerHub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Build and push + uses: docker/build-push-action@v2 + with: + context: others/pocketbase/ + platforms: linux/aarch64 + push: true + tags: coollabsio/pocketbase:0.8.0-aarch64 + merge-manifest: + runs-on: ubuntu-latest + needs: [amd64, arm64, aarch64] + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - name: Login to DockerHub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Create & publish manifest + run: | + docker manifest create coollabsio/pocketbase:0.8.0 --amend coollabsio/pocketbase:0.8.0-amd64 --amend coollabsio/pocketbase:0.8.0-arm64 --amend coollabsio/pocketbase:0.8.0-aarch64 + docker manifest push coollabsio/pocketbase:0.8.0 diff --git a/.github/workflows/staging-release.yml b/.github/workflows/staging-release.yml index fa6b09473..c4a9988c4 100644 --- a/.github/workflows/staging-release.yml +++ b/.github/workflows/staging-release.yml @@ -3,9 +3,11 @@ name: staging-release on: push: paths: - - '**' + - "**" - "!others/fluentbit" + - "!others/pocketbase" - "!.github/workflows/fluent-bit-release.yml" + - "!.github/workflows/pocketbase-release.yml" branches: - next diff --git a/.gitignore b/.gitignore index 66ff0bf1f..c31810490 100644 --- a/.gitignore +++ b/.gitignore @@ -10,8 +10,11 @@ package dist client apps/api/db/*.db -local-serve apps/api/db/migration.db-journal apps/api/core* +apps/backup/backups/* +!apps/backup/backups/.gitkeep logs others/certificates +backups/* +!backups/.gitkeep \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 858d6ae2c..65a483d79 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,11 +1,22 @@ { - "i18n-ally.localesPaths": ["src/lib/locales"], + "i18n-ally.localesPaths": [ + "src/lib/locales" + ], "i18n-ally.keystyle": "nested", "i18n-ally.extract.ignoredByFiles": { - "src\\routes\\__layout.svelte": ["Coolify", "coolLabs logo"] + "src\\routes\\__layout.svelte": [ + "Coolify", + "coolLabs logo" + ] }, "i18n-ally.sourceLanguage": "en", - "i18n-ally.enabledFrameworks": ["svelte"], - "i18n-ally.enabledParsers": ["js", "ts", "json"], + "i18n-ally.enabledFrameworks": [ + "svelte" + ], + "i18n-ally.enabledParsers": [ + "js", + "ts", + "json" + ], "i18n-ally.extract.autoDetect": true -} +} \ No newline at end of file diff --git a/CONTRIBUTION.md b/CONTRIBUTION.md index 886e0e441..9d869bb10 100644 --- a/CONTRIBUTION.md +++ b/CONTRIBUTION.md @@ -34,7 +34,7 @@ You'll need a set of skills to [get started](docs/contribution/GettingStarted.md ```sh # Or... Copy and paste commands bellow: -cp apps/api/.env.example apps/api.env +cp apps/api/.env.example apps/api/.env pnpm install pnpm db:push pnpm db:seed diff --git a/README.md b/README.md index e73e6b675..c6c509ad6 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,7 @@ Deploy your resource to: ### Services + - [Appwrite](https://appwrite.io) - [WordPress](https://docs.coollabs.io/coolify/services/wordpress) - [Ghost](https://ghost.org) @@ -103,11 +104,29 @@ Deploy your resource to: - Email: [andras@coollabs.io](mailto:andras@coollabs.io) - Discord: [Invitation](https://coollabs.io/discord) -## Development Contributions +--- -Coolify is developed under the Apache License and you can help to make it grow → [Start coding!](./CONTRIBUTION.md) +## ⚗️ Expertise Contributions -## Financial Contributors +Coolify is developed under the [Apache License](./LICENSE) and you can help to make it grow. +Our community will be glad to have you on board! + +Learn how to contribute to Coolify as as ... + +→ [👩🏾‍💻 Software developer](./CONTRIBUTION.md) + +→ [🧑🏻‍🏫 Translator](./docs/contribution/Translating.md) + + + +--- + +## 💰 Financial Contributors Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/coollabsio/contribute)] diff --git a/apps/api/.env.example b/apps/api/.env.example index 800c40929..598d659b3 100644 --- a/apps/api/.env.example +++ b/apps/api/.env.example @@ -1,10 +1,9 @@ -COOLIFY_APP_ID=local-dev -# 32 bits long secret key -COOLIFY_SECRET_KEY=12341234123412341234123412341234 -COOLIFY_DATABASE_URL=file:../db/dev.db -COOLIFY_SENTRY_DSN= - -COOLIFY_IS_ON=docker -COOLIFY_WHITE_LABELED=false -COOLIFY_WHITE_LABELED_ICON= -COOLIFY_AUTO_UPDATE= \ No newline at end of file +COOLIFY_APP_ID=local-dev +# 32 bits long secret key +COOLIFY_SECRET_KEY=12341234123412341234123412341234 +COOLIFY_DATABASE_URL=file:../db/dev.db + +COOLIFY_IS_ON=docker +COOLIFY_WHITE_LABELED=false +COOLIFY_WHITE_LABELED_ICON= +COOLIFY_AUTO_UPDATE= diff --git a/apps/api/devTags.json b/apps/api/devTags.json index f2903d128..0d84f6caa 100644 --- a/apps/api/devTags.json +++ b/apps/api/devTags.json @@ -1 +1 @@ -[{"name":"appsmith","image":"appsmith/appsmith-ce","tags":["v1.8.8","v1.8.6","v1.8.4","v1.8.2","v1.8.0","v1.7.8","v1.7.6","v1.7.4","v1.7.2","v1.7.13","v1.7.11","v1.7.1","v1.6.9","v1.6.7","v1.6.5","v1.6.3","v1.6.22","v1.6.20","v1.6.19","v1.6.17","v1.6.15","v1.6.13","v1.6.11","v1.6.1","v1.5.30","v1.5.28","v1.5.26","v1.5.24","v1.5.22"]},{"name":"appwrite","image":"appwrite/appwrite","tags":["1.1.0","1.0.3","1.0.1","1.0.0","0.9.3","0.9.1","0.8.0","0.7.1","0.6.2","0.6.0","0.5.2","0.5.0","0.3.1","0.2.0","0.15.2","0.15.0","0.14.2","0.14.0","0.13.4","0.13.2","0.13.0","0.12.3","0.12.1","0.12.0","0.11.2","0.11.0","0.10.4","0.10.2","0.10.0","0.1.8"]},{"name":"fider","image":"getfider/fider","tags":["stable","master","main","dev","SHA_ee6e83cfaadadaa56ab76e089e01f5631af3506f","SHA_deb4f9b4f561d890d8a80e6872fea9a98a265cc6","SHA_d5cc307909d43447200483d76b5db74d8ed8349e","SHA_d1674476577a7fd3c88fc29f91c3f35f5bd6a260","SHA_d107cbb157abca6576110080736213efe0955cff","SHA_c9c55b2f5b33a76015241b97e03cfac1254b42a7","SHA_bcf451a3cb02d5c8a489fd30309249296057b084","SHA_bbfe419639514f949a042807addf0fde7d4de225","SHA_adc3afc4c7bcf96931a5f90cab65c282d860dbfd","SHA_ab5283ae95334f10b5041402dce79e333c472015","SHA_a3f4cb5ed0a4ee2d726705fc426636364aac17a1","SHA_a18224142bf51bc6463c3d22f45f62287902e9a6","SHA_7851f9da566132d87fa2a63004e78c3bc9c09c6c","SHA_6c0f2bed1754e9d579eb9575129a6e3dbc529c32","SHA_603508c8790d6a6fb1e852df1a58ead8e5b3ea6c","SHA_55efacf164a4749b50ee68ae8925e7dc9dfa3a0c","SHA_4bdd291ce61e5f5dfc063fa1b2d9be8c9ff1d4c4","SHA_3fba9cb6a9ceab0c78c6cff3220610f591f657cb","SHA_3d635b57606a9885babe91fe975b11429e0f2c38","SHA_3b794edbd9789a8aa38ecd3714bc536a675d3058","SHA_3570c454ad3252b690608f7bf8051737d8519f8a","SHA_263e2709fd145f3ea511e5557e170102899995b0","SHA_17f92b16ef790003338f0926fc8d791a9a61333c","SHA_0cddae6b274f915aabf2c3a3cbacf5f524bc59a0","SHA_0c403665346acc3ba90998a28ca53e8f76e54247","SHA_097ca277b11aefdb4cbbffb8a1dbc6e64130a960"]},{"name":"ghost-mariadb","image":"bitnami/ghost","tags":["5.7.1","5.7.0","5.5.0","5.4.1","5.4.0","5.3.1","5.3.0","5.22.9","5.22.8","5.22.7","5.22.6","5.22.5","5.22.4","5.22.3","5.22.2","5.22.11","5.22.10","5.22.1","5.22.0","5.21.0","5.20.0","5.2.4","5.2.3","5.2.2","5.2.1","5.19.3","5.19.2","5.19.1","5.19.0","5.18.0","4.48.8"]},{"name":"ghost-mysql","image":"library/ghost","tags":["5.9.4","5.8.3","5.8.2","5.7.1","5.7.0","5.5.0","5.4.1","5.3.1","5.3.0","5.22.9","5.22.8","5.22.4","5.22.11","5.22.10","5.22.1","5.20.0","5.2.4","5.2.3","5.2.2","5.2.1","5.19.3","5.19.0","5.18.0","5.17.2","5.17.1","5.17.0","5.16.2","5.14.2","5.14.1","5.13.2"]},{"name":"ghost-only","image":"library/ghost","tags":["5.9.4","5.8.3","5.8.2","5.7.1","5.7.0","5.5.0","5.4.1","5.3.1","5.3.0","5.22.9","5.22.8","5.22.4","5.22.11","5.22.10","5.22.1","5.20.0","5.2.4","5.2.3","5.2.2","5.2.1","5.19.3","5.19.0","5.18.0","5.17.2","5.17.1","5.17.0","5.16.2","5.14.2","5.14.1","5.13.2"]},{"name":"gitea","image":"gitea/gitea","tags":["1.9.6","1.9.5","1.9.4","1.9.3","1.9.2","1.9.0","1.8.3","1.8.1","1.8.0","1.7.5","1.7.3","1.7.1","1.7.0","1.6.3","1.6.1","1.6.0","1.5.3","1.5.1","1.5.0","1.4.3","1.4.1","1.4.0","1.3.3","1.3.1","1.3.0","1.2.3","1.2.1","1.2.0","1.17.3","1.17.2"]},{"name":"glitchtip","image":"glitchtip/glitchtip","tags":["v2.0.7","v2.0.5","v2.0.2","v2.0.0","v1.9.2","v1.9.0","v1.8.4","v1.8.2","v1.8.0","v1.7.1","v1.6.4","v1.6.2","v1.6.0","v1.5.3","v1.5.1","v1.4.1","v1.3.3","v1.3.1","v1.2.6","v1.2.4","v1.2.2","v1.2.0","v1.12.4","v1.12.2","v1.12.0","v1.10.3","v1.10.1","v1.1.2","v1.1.0","v1.0.8"]},{"name":"grafana","image":"grafana/grafana","tags":["9.2.5","9.2.4","9.2.3","9.2.2","9.2.1","9.2.0","9.1.8","9.1.7","9.1.6","9.1.5","9.1.4","9.1.3","9.1.2","9.1.1","9.1.0","9.0.9","9.0.8","9.0.7","9.0.6","9.0.5","9.0.4","9.0.3","9.0.2","9.0.1","9.0.0","8.5.9","8.5.6","8.5.5","8.5.4","8.5.3"]},{"name":"hasura","image":"hasura/graphql-engine","tags":["v2.9.0","v2.8.4","v2.8.3","v2.8.2","v2.8.1","v2.8.0","v2.7.0","v2.6.2","v2.6.1","v2.6.0","v2.5.2","v2.5.1","v2.5.0","v2.4.0","v2.3.1","v2.3.0","v2.2.2","v2.2.1","v2.2.0","v2.15.1","v2.15.0","v2.14.0","v2.13.1","v2.13.0","v2.12.0","v2.11.2","v2.11.1","v2.11.0","v2.10.1","v2.10.0"]},{"name":"keycloak","image":"quay.io/keycloak/keycloak","tags":["9.0.3","9.0.0","8.0.1","7.0.0","6.0.1","6.0.0","20.0.1","20.0.0","19.0.3","19.0.1","19.0.0","18.0.1","18.0.0","17.0.1","17.0.0","16.1.0","15.1.1","15.0.2","15.0.0","13.0.1","12.0.4","12.0.2","12.0.0","11.0.2","11.0.0","10.0.1"]},{"name":"languagetool","image":"silviof/docker-languagetool","tags":["latest","5.8","5.7","5.6","5.5","5.4","5.3"]},{"name":"lavalink","image":"fredboat/lavalink","tags":["v3.6","v3-vda0b3a4b3916a7b1a2b79702de1143c3a6939810-SNAPSHOT","v3-vc92690c425390bd20f6c51643c67ba79ab85b7e0-SNAPSHOT","v3-vab81dcd46adf3e8a961dd57eacd2a1bde1233e6c-SNAPSHOT","v3-v9c9432704d6a4badfcbd06a57597c54bed8f4326-SNAPSHOT","v3-v3.0","v3-v3","v3-v124f8fae7dab299f9cdf1cb4c1715be455497286-SNAPSHOT","v3-","v3","v2.0.1","v2.0","v2","update-udpqueue-vb4a439d6147dbd8641ea4f265e8efc9f1e16e2d3-SNAPSHOT","update-udpqueue-","update-udpqueue","revert-713-fix-error-for-loading-jda-nas","refactor-github-actions","patch-update-github-actions","patch-more-configurable-github-actions","patch-lavaplayer-update","patch-lavaplayer-bump","patch-build-number","next-api-vd4db194cac7a839a3899857f1f6d7b910369309d-SNAPSHOT","next-api-vc2e018d5ffef54b2d17244b3d213e31723a084d6-SNAPSHOT","next-api-v42cb5f7c58e98d1911e87bffb35aee0a235b85f8-SNAPSHOT","next-api-v31a243bda80badbd7d643f68fc1f87e99639060f-SNAPSHOT","next-api-v17f6884434c2d70d1704b2322a951d9f07af8865-SNAPSHOT","next-api-","next-api"]},{"name":"meilisearch","image":"getmeili/meilisearch","tags":["v0.9.0","v0.8.3","v0.8.1","v0.29.1","v0.29.0","v0.28.1","v0.28.0","v0.27.1","v0.27.0","v0.26.1","v0.26.0","v0.25.1","v0.25.0","v0.23.1","v0.23.0","v0.21.1","v0.21.0","v0.20.0","v0.19.0","v0.18.1","v0.18.0","v0.17.0","v0.16.0","v0.14.1","v0.14.0","v0.12.0","v0.11.0","v0.10.0","0.14.1"]},{"name":"minio","image":"minio/minio","tags":["RELEASE.2022-11-17T23-20-09Z.fips","RELEASE.2022-11-11T03-44-20Z.fips","RELEASE.2022-11-10T18-20-21Z.fips","RELEASE.2022-11-08T05-27-07Z.fips","RELEASE.2022-10-29T06-21-33Z.fips","RELEASE.2022-10-24T18-35-07Z.fips","RELEASE.2022-10-21T22-37-48Z.fips","RELEASE.2022-10-20T00-55-09Z.fips","RELEASE.2022-10-15T19-57-03Z.fips","RELEASE.2022-10-08T20-11-00Z.fips","RELEASE.2022-10-05T14-58-27Z.fips","RELEASE.2022-10-02T19-29-29Z.fips","RELEASE.2022-09-25T15-44-53Z.fips","RELEASE.2022-09-22T18-57-27Z.fips","RELEASE.2022-09-17T00-09-45Z.hotfix.fc6d6fdbd","RELEASE.2022-09-17T00-09-45Z.hotfix.4bb22d5cd","RELEASE.2022-09-17T00-09-45Z","RELEASE.2022-09-07T22-25-02Z","RELEASE.2022-09-01T23-53-36Z","RELEASE.2022-08-26T19-53-15Z","RELEASE.2022-08-25T07-17-05Z","RELEASE.2022-08-22T23-53-06Z.fips","RELEASE.2022-08-13T21-54-44Z.fips","RELEASE.2022-08-11T04-37-28Z.fips","RELEASE.2022-08-08T18-34-09Z.fips","RELEASE.2022-08-05T23-27-09Z.fips","RELEASE.2022-08-02T23-59-16Z.fips","RELEASE.2022-07-30T05-21-40Z.test.fdec67a59","RELEASE.2022-07-30T05-21-40Z.fips","RELEASE.2022-07-29T19-40-48Z.fips"]},{"name":"n8n","image":"n8nio/n8n","tags":["0.99.1","0.99.0","0.98.0","0.97.0","0.96.0","0.95.1","0.95.0","0.94.1","0.94.0","0.93.0","0.92.0","0.91.0","0.9.0","0.89.2","0.88.1","0.88.0","0.87.2","0.87.1","0.87.0","0.86.1","0.86.0","0.85.0","0.84.4","0.84.3","0.84.1","0.84.0","0.83.0","0.82.1","0.82.0","0.81.0"]},{"name":"nocodb","image":"nocodb/nocodb","tags":["0.98.3","0.98.1","0.97.0","0.96.3","0.96.1","0.92.4","0.92.0","0.91.8","0.91.6","0.91.1","0.90.8","0.90.5","0.90.3","0.90.11","0.90.1","0.9.9","0.9.7","0.9.43","0.9.41","0.9.39","0.9.37","0.9.35","0.9.33","0.9.31","0.9.29","0.9.27","0.9.25","0.9.22","0.9.19","0.9.16"]},{"name":"plausibleanalytics","image":"plausible/analytics","tags":["v1.5.0-rc.1","v1.4.4","v1.4.3","v1.4.2","v1.4.1","v1.4.0.rc.0","v1.4.0-rc.0","v1.4.0","v1.4","v1.3.0-rc.1","v1.3.0-rc.0","v1.3.0","v1.3","v1.2.1","v1.2.0","v1.2-rc.1","v1.2-rc.0","v1.2","v1.1.1","v1.1.0","v1.1","v1.0.0","v1.0","v1","stable","master","loadtest","latest","1.5.0-rc.0"]},{"name":"searxng","image":"searxng/searxng","tags":["2022.11.11-e6345758","2022.11.11-3a765113","2022.11.10-117f69fa","2022.11.09-ee4475ff","2022.11.07-d3949269","2022.11.07-8f19bdaf","2022.11.06-ae54c7d5","2022.11.06-2dc5c0e1","2022.11.05-e9f42e1c","2022.11.05-d764d94a","2022.11.05-d3a7399e","2022.11.05-d37afb8a","2022.11.05-4fe54636","2022.10.29-fc9986de","2022.10.29-fa59ff9b","2022.10.29-d49ccb54","2022.10.29-a9deead1","2022.10.29-3f1d594c","2022.10.28-c26fa335","2022.10.28-5db4ed5d","2022.10.28-5a181ea1","2022.10.25-affd8f75","2022.10.25-4783d6c9","2022.10.21-710a3a00","2022.10.14-e2dd5a80","2022.10.14-72f6367e","2022.10.14-4d4dfc58","2022.10.14-2eb81701","2022.10.14-1a5b0965","2022.10.14-096d9def"]},{"name":"trilium","image":"zadam/trilium","tags":["0.56.1","0.55.1","0.54.2","0.53.2","0.52.4","0.52.2","0.51.2","0.50.3","0.50.1","0.49.5","0.49.3","0.48.9","0.48.7","0.48.4","0.48.2","0.47.8","0.47.6","0.47.4","0.47.2","0.46.7","0.46.5","0.45.9","0.45.7","0.45.5","0.45.3","0.45.10","0.44.8","0.44.6","0.44.4","0.43.3"]},{"name":"umami-postgresql","image":"ghcr.io/umami-software/umami","tags":["postgresql-v1.39.5","postgresql-v1.39.4","postgresql-v1.39.3","postgresql-v1.39.2","postgresql-v1.39.1","postgresql-v1.39.0","postgresql-v1.38.0","postgresql-v1.37.0","postgresql-v1.36.1","postgresql-v1.36.0","postgresql-v1.35.0","postgresql-v1.34.0","postgresql-v1.33.3","postgresql-latest","mysql-v1.39.5","mysql-v1.39.4","mysql-v1.39.3","mysql-v1.39.2","mysql-v1.39.1","mysql-v1.39.0","mysql-v1.38.0","mysql-v1.37.0","mysql-v1.36.1","mysql-v1.36.0","mysql-v1.35.0","mysql-v1.34.0","mysql-v1.33.3","mysql-latest"]},{"name":"umami","image":"ghcr.io/umami-software/umami","tags":["postgresql-v1.39.5","postgresql-v1.39.4","postgresql-v1.39.3","postgresql-v1.39.2","postgresql-v1.39.1","postgresql-v1.39.0","postgresql-v1.38.0","postgresql-v1.37.0","postgresql-v1.36.1","postgresql-v1.36.0","postgresql-v1.35.0","postgresql-v1.34.0","postgresql-v1.33.3","postgresql-latest","mysql-v1.39.5","mysql-v1.39.4","mysql-v1.39.3","mysql-v1.39.2","mysql-v1.39.1","mysql-v1.39.0","mysql-v1.38.0","mysql-v1.37.0","mysql-v1.36.1","mysql-v1.36.0","mysql-v1.35.0","mysql-v1.34.0","mysql-v1.33.3","mysql-latest"]},{"name":"uptimekuma","image":"louislam/uptime-kuma","tags":["1.9.2","1.9.1","1.9.0","1.8.0","1.7.3","1.7.1","1.7.0","1.6.3","1.6.2","1.6.1","1.6.0","1.5.3","1.5.2","1.5.0","1.3.1","1.2.0","1.18.5","1.18.4","1.18.3","1.18.2","1.18.1","1.18.0","1.17.1","1.17.0","1.16.1","1.16.0","1.15.1","1.15.0","1.14.1","1.14.0"]},{"name":"vaultwarden","image":"vaultwarden/server","tags":["1.26.0","1.25.2","1.25.1","1.25.0","1.24.0","1.23.1","1.23.0","1.22.2","1.22.1","1.22.0","1.21.0"]},{"name":"vscodeserver","image":"codercom/code-server","tags":["4.8.3","4.8.2","4.8.1","4.8.0","4.7.0","4.6.0","4.5.1","4.4.0","4.2.0","4.0.2","3.9.3","3.9.1","3.8.1","3.7.4","3.7.2","3.7.0","3.6.1","3.5.0","3.4.0","3.3.0","3.2.0","3.11.1","3.10.2","3.10.0","3.1.1","3.1.0","3.0.2","3.0.0"]},{"name":"weblate","image":"weblate/weblate","tags":["latest","edge-2022-11-15-cad0a043b32c1ee61611ab258db0f01c5e6d718f","edge-2022-11-10-bf41db3afbab22384e103718094738dcfdc1a270","edge-2022-11-09-9bc90ce8b873778d2f486eccd0163bb1bb65ca6e","edge-2022-11-08-36e221037ff7097f8cd2c88d779135b6c7d3f363","edge-2022-11-08-3568e3c6759a9e9b779d98cb98393526d451466a","edge-2022-11-08-261d197970ca0679514d32ff783467972e807061","edge-2022-11-05-fa5cb203d854a11cc7850868a2890168afa3e7da","edge-2022-11-05-d93ae789eef8f065240f9fb6feb3edb236a7e6f8","edge-2022-11-05-8fc2be8e9d22e5ca2da2773488da7f72c5927ec3","edge-2022-11-05-85da67e88a113bed65530f0695ad4cddec0ed05a","edge-2022-11-05-3f4d77b6f2cb16bf008a4ef587e843ccb9c0c5d0","edge-2022-11-05-226eed520a2b32c3583c6e3247109ec8950764e7","edge-2022-11-03-487f3255cb89415fbe0769fa4b7bd2a9209deca6","edge-2022-11-02-e4171e0c5657ca38341cce8ac31f5cbdf25389eb","edge-2022-11-02-6d886c40cd62eb23d21f7c0a1840b4a7a4c51ad0","edge-2022-11-01-608df4dd95a2d1f76c15cddd9e116bb4c3229168","edge-2022-11-01-54957be78eb76f602ceae50c0b01b64b20402b2a","edge-2022-10-31-c55c7302a6c82a160ee9d711893c12d67ecd3b27","edge-2022-10-26-c69cfdd83ed1fad4a4d57398552b8c70894a6586","edge-2022-10-26-410b3aff37de5bbfacbc47642ce28b2518bee506","edge-2022-10-25-e09e2c29ed3748eb0fa248453635dd27768e8dd9","edge-2022-10-25-a059748c178cce0bc30bdc915d4ff8f0d13ce25c","edge-2022-10-24-94ae069cdcf9a171e812da32d760a436fa9b37ad","edge-2022-10-24-71d2011a1f6d2b60bb65af9f775bf75d4beaf8fd","edge-2022-10-19-acb8e82a0ea2c86fb7ce8f5cb1a81e2bc634583a","edge-2022-10-18-f54b83a0898bef9d14b00d885f675c2e4ebb68ca","edge-2022-10-17-eb9dc248720581052b4be4e3f1f372bce37edd62","edge-2022-10-17-e5a5f6cfdf7e500b7b357566971172fad022c1dc","edge-2022-10-17-da6d01ea005c29453e0111a45956d16bf2262f54"]},{"name":"wordpress-only","image":"library/wordpress","tags":["php8.1-fpm-alpine","php8.1-fpm","php8.1-apache","php8.1","php8.0-fpm-alpine","php8.0-fpm","php8.0-apache","php8.0","php7.4-fpm-alpine","php7.4-fpm","php7.4-apache","php7.4","php7.3-fpm-alpine","php7.3-fpm","php7.3-apache","php7.3","php7.2-fpm-alpine","php7.2-fpm","php7.2-apache","php7.2","php7.1-fpm-alpine","php7.1-fpm","php7.1-apache","php7.1","php7.0-fpm-alpine","php7.0-fpm","php7.0-apache","php7.0","php5.6-fpm-alpine","php5.6-fpm"]},{"name":"wordpress","image":"library/wordpress","tags":["php8.1-fpm-alpine","php8.1-fpm","php8.1-apache","php8.1","php8.0-fpm-alpine","php8.0-fpm","php8.0-apache","php8.0","php7.4-fpm-alpine","php7.4-fpm","php7.4-apache","php7.4","php7.3-fpm-alpine","php7.3-fpm","php7.3-apache","php7.3","php7.2-fpm-alpine","php7.2-fpm","php7.2-apache","php7.2","php7.1-fpm-alpine","php7.1-fpm","php7.1-apache","php7.1","php7.0-fpm-alpine","php7.0-fpm","php7.0-apache","php7.0","php5.6-fpm-alpine","php5.6-fpm"]}] \ No newline at end of file +[{"name":"appsmith","image":"appsmith/appsmith-ce","tags":["v1.8.8","v1.8.6","v1.8.4","v1.8.2","v1.8.10","v1.8.0","v1.7.8","v1.7.6","v1.7.4","v1.7.2","v1.7.13","v1.7.11","v1.7.1","v1.6.9","v1.6.7","v1.6.5","v1.6.3","v1.6.22","v1.6.20","v1.6.19","v1.6.17","v1.6.15","v1.6.13","v1.6.11","v1.6.1","v1.5.30","v1.5.28","v1.5.26","v1.5.24","v1.5.22"]},{"name":"appwrite","image":"appwrite/appwrite","tags":["1.1.2","1.1.0","1.0.3","1.0.1","1.0.0","0.9.3","0.9.1","0.8.0","0.7.1","0.6.2","0.6.0","0.5.2","0.5.0","0.3.1","0.2.0","0.15.2","0.15.0","0.14.2","0.14.0","0.13.4","0.13.2","0.13.0","0.12.3","0.12.1","0.12.0","0.11.2","0.11.0","0.10.4","0.10.2","0.10.0"]},{"name":"fider","image":"getfider/fider","tags":["stable","master","main","dev","SHA_ee6e83cfaadadaa56ab76e089e01f5631af3506f","SHA_deb4f9b4f561d890d8a80e6872fea9a98a265cc6","SHA_d5cc307909d43447200483d76b5db74d8ed8349e","SHA_d1674476577a7fd3c88fc29f91c3f35f5bd6a260","SHA_d107cbb157abca6576110080736213efe0955cff","SHA_c9c55b2f5b33a76015241b97e03cfac1254b42a7","SHA_bcf451a3cb02d5c8a489fd30309249296057b084","SHA_bbfe419639514f949a042807addf0fde7d4de225","SHA_adc3afc4c7bcf96931a5f90cab65c282d860dbfd","SHA_ab5283ae95334f10b5041402dce79e333c472015","SHA_a3f4cb5ed0a4ee2d726705fc426636364aac17a1","SHA_a18224142bf51bc6463c3d22f45f62287902e9a6","SHA_8e5cff30d95963eaee2587488d351e0d658c8195","SHA_8cabe2817ce7ccaf2f0a9fdbb1b5d3411de87f81","SHA_7851f9da566132d87fa2a63004e78c3bc9c09c6c","SHA_6c0f2bed1754e9d579eb9575129a6e3dbc529c32","SHA_603508c8790d6a6fb1e852df1a58ead8e5b3ea6c","SHA_55efacf164a4749b50ee68ae8925e7dc9dfa3a0c","SHA_4bdd291ce61e5f5dfc063fa1b2d9be8c9ff1d4c4","SHA_3fba9cb6a9ceab0c78c6cff3220610f591f657cb","SHA_3d635b57606a9885babe91fe975b11429e0f2c38","SHA_3b794edbd9789a8aa38ecd3714bc536a675d3058","SHA_3570c454ad3252b690608f7bf8051737d8519f8a","SHA_263e2709fd145f3ea511e5557e170102899995b0","SHA_255c30ed012fc4c39ffc97efc1d3b00425b17c72","SHA_17f92b16ef790003338f0926fc8d791a9a61333c"]},{"name":"ghost-mariadb","image":"bitnami/ghost","tags":["5.7.1","5.7.0","5.5.0","5.4.1","5.4.0","5.3.1","5.3.0","5.24.2","5.24.1","5.24.0","5.23.0","5.22.9","5.22.8","5.22.7","5.22.6","5.22.5","5.22.4","5.22.3","5.22.2","5.22.11","5.22.10","5.22.1","5.22.0","5.21.0","5.20.0","5.2.4","5.2.3","5.2.2","5.2.1","5.19.3","4.48.8"]},{"name":"ghost-mysql","image":"library/ghost","tags":["5.9.4","5.8.3","5.8.2","5.7.1","5.7.0","5.5.0","5.4.1","5.3.1","5.3.0","5.24.2","5.24.1","5.23.0","5.22.9","5.22.8","5.22.4","5.22.11","5.22.10","5.22.1","5.20.0","5.2.4","5.2.3","5.2.2","5.2.1","5.19.3","5.19.0","5.18.0","5.17.2","5.17.1","5.17.0","5.16.2"]},{"name":"ghost-only","image":"library/ghost","tags":["5.9.4","5.8.3","5.8.2","5.7.1","5.7.0","5.5.0","5.4.1","5.3.1","5.3.0","5.24.2","5.24.1","5.23.0","5.22.9","5.22.8","5.22.4","5.22.11","5.22.10","5.22.1","5.20.0","5.2.4","5.2.3","5.2.2","5.2.1","5.19.3","5.19.0","5.18.0","5.17.2","5.17.1","5.17.0","5.16.2"]},{"name":"gitea","image":"gitea/gitea","tags":["1.9.6","1.9.5","1.9.4","1.9.3","1.9.2","1.9.0","1.8.3","1.8.1","1.8.0","1.7.5","1.7.3","1.7.1","1.7.0","1.6.3","1.6.1","1.6.0","1.5.3","1.5.1","1.5.0","1.4.3","1.4.1","1.4.0","1.3.3","1.3.1","1.3.0","1.2.3","1.2.1","1.2.0","1.17.3","1.17.2"]},{"name":"glitchtip","image":"glitchtip/glitchtip","tags":["v2.0.7","v2.0.5","v2.0.2","v2.0.0","v1.9.2","v1.9.0","v1.8.4","v1.8.2","v1.8.0","v1.7.1","v1.6.4","v1.6.2","v1.6.0","v1.5.3","v1.5.1","v1.4.1","v1.3.3","v1.3.1","v1.2.6","v1.2.4","v1.2.2","v1.2.0","v1.12.4","v1.12.2","v1.12.0","v1.10.3","v1.10.1","v1.1.2","v1.1.0","v1.0.8"]},{"name":"grafana","image":"grafana/grafana","tags":["9.3.1","9.3.0","9.2.7","9.2.6","9.2.5","9.2.4","9.2.3","9.2.2","9.2.1","9.2.0","9.1.8","9.1.7","9.1.6","9.1.5","9.1.4","9.1.3","9.1.2","9.1.1","9.1.0","9.0.9","9.0.8","9.0.7","9.0.6","9.0.5","9.0.4","9.0.3","9.0.2","9.0.1","9.0.0","8.5.9"]},{"name":"hasura","image":"hasura/graphql-engine","tags":["v2.9.0","v2.8.4","v2.8.3","v2.8.2","v2.8.1","v2.8.0","v2.7.0","v2.6.2","v2.6.1","v2.6.0","v2.5.2","v2.5.1","v2.5.0","v2.4.0","v2.3.1","v2.3.0","v2.2.2","v2.2.1","v2.2.0","v2.15.2","v2.15.1","v2.15.0","v2.14.1","v2.14.0","v2.13.2","v2.13.1","v2.13.0","v2.12.1","v2.12.0","v2.11.3"]},{"name":"keycloak","image":"quay.io/keycloak/keycloak","tags":["9.0.3","9.0.0","8.0.1","7.0.0","6.0.1","6.0.0","20.0.1","20.0.0","19.0.3","19.0.1","19.0.0","18.0.1","18.0.0","17.0.1","17.0.0","16.1.0","15.1.1","15.0.2","15.0.0","13.0.1","12.0.4","12.0.2","12.0.0","11.0.2","11.0.0","10.0.1"]},{"name":"languagetool","image":"silviof/docker-languagetool","tags":["latest","5.8","5.7","5.6","5.5","5.4","5.3"]},{"name":"lavalink","image":"fredboat/lavalink","tags":["v3.7","v3.6","v3-vda0b3a4b3916a7b1a2b79702de1143c3a6939810-SNAPSHOT","v3-vc92690c425390bd20f6c51643c67ba79ab85b7e0-SNAPSHOT","v3-vab81dcd46adf3e8a961dd57eacd2a1bde1233e6c-SNAPSHOT","v3-v9c9432704d6a4badfcbd06a57597c54bed8f4326-SNAPSHOT","v3-v3.0","v3-v3","v3-v124f8fae7dab299f9cdf1cb4c1715be455497286-SNAPSHOT","v3-","v3","v2.0.1","v2.0","v2","update-udpqueue-vb4a439d6147dbd8641ea4f265e8efc9f1e16e2d3-SNAPSHOT","update-udpqueue-","update-udpqueue","revert-713-fix-error-for-loading-jda-nas","refactor-github-actions","patch-update-github-actions","patch-more-configurable-github-actions","patch-lavaplayer-update","patch-lavaplayer-bump","patch-build-number","next-api-vd4db194cac7a839a3899857f1f6d7b910369309d-SNAPSHOT","next-api-vc2e018d5ffef54b2d17244b3d213e31723a084d6-SNAPSHOT","next-api-v42cb5f7c58e98d1911e87bffb35aee0a235b85f8-SNAPSHOT","next-api-v31a243bda80badbd7d643f68fc1f87e99639060f-SNAPSHOT","next-api-v17f6884434c2d70d1704b2322a951d9f07af8865-SNAPSHOT","next-api-"]},{"name":"meilisearch","image":"getmeili/meilisearch","tags":["v0.9.0","v0.8.3","v0.8.1","v0.30.0","v0.29.1","v0.29.0","v0.28.1","v0.28.0","v0.27.1","v0.27.0","v0.26.1","v0.26.0","v0.25.1","v0.25.0","v0.23.1","v0.23.0","v0.21.1","v0.21.0","v0.20.0","v0.19.0","v0.18.1","v0.18.0","v0.17.0","v0.16.0","v0.14.1","v0.14.0","v0.12.0","v0.11.0","v0.10.0","0.14.1"]},{"name":"minio","image":"minio/minio","tags":["RELEASE.2022-11-29T23-40-49Z.fips","RELEASE.2022-11-26T22-43-32Z.fips","RELEASE.2022-11-17T23-20-09Z.fips","RELEASE.2022-11-11T03-44-20Z.fips","RELEASE.2022-11-10T18-20-21Z.fips","RELEASE.2022-11-08T05-27-07Z.fips","RELEASE.2022-10-29T06-21-33Z.fips","RELEASE.2022-10-24T18-35-07Z.hotfix.ce525fdaf","RELEASE.2022-10-24T18-35-07Z.fips","RELEASE.2022-10-21T22-37-48Z.fips","RELEASE.2022-10-20T00-55-09Z.fips","RELEASE.2022-10-15T19-57-03Z.fips","RELEASE.2022-10-08T20-11-00Z.fips","RELEASE.2022-10-05T14-58-27Z.fips","RELEASE.2022-10-02T19-29-29Z.fips","RELEASE.2022-09-25T15-44-53Z.fips","RELEASE.2022-09-22T18-57-27Z.fips","RELEASE.2022-09-17T00-09-45Z.hotfix.fc6d6fdbd","RELEASE.2022-09-17T00-09-45Z.hotfix.4bb22d5cd","RELEASE.2022-09-17T00-09-45Z","RELEASE.2022-09-07T22-25-02Z","RELEASE.2022-09-01T23-53-36Z","RELEASE.2022-08-26T19-53-15Z","RELEASE.2022-08-25T07-17-05Z","RELEASE.2022-08-22T23-53-06Z.fips","RELEASE.2022-08-13T21-54-44Z.fips","RELEASE.2022-08-11T04-37-28Z.fips","RELEASE.2022-08-08T18-34-09Z.fips","RELEASE.2022-08-05T23-27-09Z.fips","RELEASE.2022-08-02T23-59-16Z.fips"]},{"name":"n8n","image":"n8nio/n8n","tags":["0.99.1","0.99.0","0.98.0","0.97.0","0.96.0","0.95.1","0.95.0","0.94.1","0.94.0","0.93.0","0.92.0","0.91.0","0.9.0","0.89.2","0.88.1","0.88.0","0.87.2","0.87.1","0.87.0","0.86.1","0.86.0","0.85.0","0.84.4","0.84.3","0.84.1","0.84.0","0.83.0","0.82.1","0.82.0","0.81.0"]},{"name":"nocodb","image":"nocodb/nocodb","tags":["0.99.2","0.99.0","0.98.3","0.98.1","0.97.0","0.96.3","0.96.1","0.92.4","0.92.0","0.91.8","0.91.6","0.91.1","0.90.8","0.90.5","0.90.3","0.90.11","0.90.1","0.9.9","0.9.7","0.9.43","0.9.41","0.9.39","0.9.37","0.9.35","0.9.33","0.9.31","0.9.29","0.9.27","0.9.25","0.9.22"]},{"name":"plausibleanalytics-arm","image":"plausible/analytics","tags":["v1.5.0-rc.2","v1.5.0-rc.1","v1.4.4","v1.4.3","v1.4.2","v1.4.1","v1.4.0.rc.0","v1.4.0-rc.0","v1.4.0","v1.4","v1.3.0-rc.1","v1.3.0-rc.0","v1.3.0","v1.3","v1.2.1","v1.2.0","v1.2-rc.1","v1.2-rc.0","v1.2","v1.1.1","v1.1.0","v1.1","v1.0.0","v1.0","v1","stable","master","loadtest","latest","1.5.0-rc.0"]},{"name":"plausibleanalytics","image":"plausible/analytics","tags":["v1.5.0-rc.2","v1.5.0-rc.1","v1.4.4","v1.4.3","v1.4.2","v1.4.1","v1.4.0.rc.0","v1.4.0-rc.0","v1.4.0","v1.4","v1.3.0-rc.1","v1.3.0-rc.0","v1.3.0","v1.3","v1.2.1","v1.2.0","v1.2-rc.1","v1.2-rc.0","v1.2","v1.1.1","v1.1.0","v1.1","v1.0.0","v1.0","v1","stable","master","loadtest","latest","1.5.0-rc.0"]},{"name":"pocketbase","image":"coollabsio/pocketbase","tags":["0.8.0-arm64","0.8.0-amd64","0.8.0-aarch64","0.8.0"]},{"name":"searxng","image":"searxng/searxng","tags":["2022.12.02-ffb72dfd","2022.12.02-890d63b9","2022.12.02-4970db05","2022.12.02-317fe0a2","2022.11.30-f19837cf","2022.11.30-44d4a171","2022.11.30-0361f836","2022.11.29-a8359dd4","2022.11.29-82af2f44","2022.11.29-768659f2","2022.11.29-5b19f892","2022.11.29-3579a38a","2022.11.29-1b2f1c17","2022.11.25-5ca6868c","2022.11.25-28ae469f","2022.11.25-1314c1c5","2022.11.19-b5371b7a","2022.11.18-fe8b0472","2022.11.18-1cdadf4b","2022.11.11-e6345758","2022.11.11-3a765113","2022.11.10-117f69fa","2022.11.09-ee4475ff","2022.11.07-d3949269","2022.11.07-8f19bdaf","2022.11.06-ae54c7d5","2022.11.06-2dc5c0e1","2022.11.05-e9f42e1c","2022.11.05-d764d94a","2022.11.05-d3a7399e"]},{"name":"trilium","image":"zadam/trilium","tags":["0.57.2","0.56.1","0.55.1","0.54.2","0.53.2","0.52.4","0.52.2","0.51.2","0.50.3","0.50.1","0.49.5","0.49.3","0.48.9","0.48.7","0.48.4","0.48.2","0.47.8","0.47.6","0.47.4","0.47.2","0.46.7","0.46.5","0.45.9","0.45.7","0.45.5","0.45.3","0.45.10","0.44.8","0.44.6","0.44.4"]},{"name":"umami-postgresql","image":"ghcr.io/umami-software/umami","tags":["postgresql-v1.39.5","postgresql-v1.39.4","postgresql-v1.39.3","postgresql-v1.39.2","postgresql-v1.39.1","postgresql-v1.39.0","postgresql-v1.38.0","postgresql-v1.37.0","postgresql-v1.36.1","postgresql-v1.36.0","postgresql-v1.35.0","postgresql-v1.34.0","postgresql-v1.33.3","postgresql-latest","mysql-v1.39.5","mysql-v1.39.4","mysql-v1.39.3","mysql-v1.39.2","mysql-v1.39.1","mysql-v1.39.0","mysql-v1.38.0","mysql-v1.37.0","mysql-v1.36.1","mysql-v1.36.0","mysql-v1.35.0","mysql-v1.34.0","mysql-v1.33.3","mysql-latest"]},{"name":"umami","image":"ghcr.io/umami-software/umami","tags":["postgresql-v1.39.5","postgresql-v1.39.4","postgresql-v1.39.3","postgresql-v1.39.2","postgresql-v1.39.1","postgresql-v1.39.0","postgresql-v1.38.0","postgresql-v1.37.0","postgresql-v1.36.1","postgresql-v1.36.0","postgresql-v1.35.0","postgresql-v1.34.0","postgresql-v1.33.3","postgresql-latest","mysql-v1.39.5","mysql-v1.39.4","mysql-v1.39.3","mysql-v1.39.2","mysql-v1.39.1","mysql-v1.39.0","mysql-v1.38.0","mysql-v1.37.0","mysql-v1.36.1","mysql-v1.36.0","mysql-v1.35.0","mysql-v1.34.0","mysql-v1.33.3","mysql-latest"]},{"name":"uptimekuma","image":"louislam/uptime-kuma","tags":["1.9.2","1.9.1","1.9.0","1.8.0","1.7.3","1.7.1","1.7.0","1.6.3","1.6.2","1.6.1","1.6.0","1.5.3","1.5.2","1.5.0","1.3.1","1.2.0","1.18.5","1.18.4","1.18.3","1.18.2","1.18.1","1.18.0","1.17.1","1.17.0","1.16.1","1.16.0","1.15.1","1.15.0","1.14.1","1.14.0"]},{"name":"vaultwarden","image":"vaultwarden/server","tags":["1.26.0","1.25.2","1.25.1","1.25.0","1.24.0","1.23.1","1.23.0","1.22.2","1.22.1","1.22.0","1.21.0"]},{"name":"vscodeserver","image":"codercom/code-server","tags":["4.8.3","4.8.2","4.8.1","4.8.0","4.7.0","4.6.0","4.5.1","4.4.0","4.2.0","4.0.2","3.9.3","3.9.1","3.8.1","3.7.4","3.7.2","3.7.0","3.6.1","3.5.0","3.4.0","3.3.0","3.2.0","3.11.1","3.10.2","3.10.0","3.1.1","3.1.0","3.0.2","3.0.0"]},{"name":"weblate","image":"weblate/weblate","tags":["latest","edge-2022-12-01-0295bd44d4d9da0e0836b9152319fba173a0825e","edge-2022-11-28-f28431a1e78f88bf49ccf539fbc00afe0925542d","edge-2022-11-26-558811de16025b83de43d2747f1fe209a5b829f1","edge-2022-11-23-4a1fe25c7b70e49156e02183a8deec3b357b9030","edge-2022-11-22-9a178e7f5c2e387329592a1dd7700671f64f6682","edge-2022-11-21-eb741ebad70211ecb1babdfd23e4f43c5a59fc7b","edge-2022-11-21-4580d37f616650cf5b0851fee051651f785e8852","edge-2022-11-21-0f74d6c4d3777dbf28affd09b45c69c85ed01d84","edge-2022-11-15-cad0a043b32c1ee61611ab258db0f01c5e6d718f","edge-2022-11-10-bf41db3afbab22384e103718094738dcfdc1a270","edge-2022-11-09-9bc90ce8b873778d2f486eccd0163bb1bb65ca6e","edge-2022-11-08-36e221037ff7097f8cd2c88d779135b6c7d3f363","edge-2022-11-08-3568e3c6759a9e9b779d98cb98393526d451466a","edge-2022-11-08-261d197970ca0679514d32ff783467972e807061","edge-2022-11-05-fa5cb203d854a11cc7850868a2890168afa3e7da","edge-2022-11-05-d93ae789eef8f065240f9fb6feb3edb236a7e6f8","edge-2022-11-05-8fc2be8e9d22e5ca2da2773488da7f72c5927ec3","edge-2022-11-05-85da67e88a113bed65530f0695ad4cddec0ed05a","edge-2022-11-05-3f4d77b6f2cb16bf008a4ef587e843ccb9c0c5d0","edge-2022-11-05-226eed520a2b32c3583c6e3247109ec8950764e7","edge-2022-11-03-487f3255cb89415fbe0769fa4b7bd2a9209deca6","edge-2022-11-02-e4171e0c5657ca38341cce8ac31f5cbdf25389eb","edge-2022-11-02-6d886c40cd62eb23d21f7c0a1840b4a7a4c51ad0","edge-2022-11-01-608df4dd95a2d1f76c15cddd9e116bb4c3229168","edge-2022-11-01-54957be78eb76f602ceae50c0b01b64b20402b2a","edge-2022-10-31-c55c7302a6c82a160ee9d711893c12d67ecd3b27","edge-2022-10-26-c69cfdd83ed1fad4a4d57398552b8c70894a6586","edge-2022-10-26-410b3aff37de5bbfacbc47642ce28b2518bee506","edge-2022-10-25-e09e2c29ed3748eb0fa248453635dd27768e8dd9"]},{"name":"wordpress-only","image":"library/wordpress","tags":["php8.1-fpm-alpine","php8.1-fpm","php8.1-apache","php8.1","php8.0-fpm-alpine","php8.0-fpm","php8.0-apache","php8.0","php7.4-fpm-alpine","php7.4-fpm","php7.4-apache","php7.4","php7.3-fpm-alpine","php7.3-fpm","php7.3-apache","php7.3","php7.2-fpm-alpine","php7.2-fpm","php7.2-apache","php7.2","php7.1-fpm-alpine","php7.1-fpm","php7.1-apache","php7.1","php7.0-fpm-alpine","php7.0-fpm","php7.0-apache","php7.0","php5.6-fpm-alpine","php5.6-fpm"]},{"name":"wordpress","image":"library/wordpress","tags":["php8.1-fpm-alpine","php8.1-fpm","php8.1-apache","php8.1","php8.0-fpm-alpine","php8.0-fpm","php8.0-apache","php8.0","php7.4-fpm-alpine","php7.4-fpm","php7.4-apache","php7.4","php7.3-fpm-alpine","php7.3-fpm","php7.3-apache","php7.3","php7.2-fpm-alpine","php7.2-fpm","php7.2-apache","php7.2","php7.1-fpm-alpine","php7.1-fpm","php7.1-apache","php7.1","php7.0-fpm-alpine","php7.0-fpm","php7.0-apache","php7.0","php5.6-fpm-alpine","php5.6-fpm"]}] \ No newline at end of file diff --git a/apps/api/devTemplates.yaml b/apps/api/devTemplates.yaml index e678a1633..975b7b397 100644 --- a/apps/api/devTemplates.yaml +++ b/apps/api/devTemplates.yaml @@ -1,3 +1,161 @@ +- templateVersion: 1.0.0 + defaultVersion: '0.8.0' + documentation: https://pocketbase.io/docs/ + type: pocketbase + name: Pocketbase + description: "Open Source realtime backend in 1 file" + services: + $$id: + image: coollabsio/pocketbase:$$core_version + volumes: + - $$id-data:/app/pb_data + ports: + - "8080" +- templateVersion: 1.0.0 + defaultVersion: 1.5.0-rc.0 + documentation: https://plausible.io/doc/ + type: plausibleanalytics-arm + name: Plausible Analytics (ARM) + description: A lightweight and open-source website analytics tool. + labels: + - analytics + - statistics + - plausible + - gdpr + - no-cookie + - google analytics + services: + $$id: + name: Plausible Analytics + command: >- + sh -c "sleep 10 && /entrypoint.sh db createdb && /entrypoint.sh db migrate + && /entrypoint.sh db init-admin && /entrypoint.sh run" + depends_on: + - $$id-postgresql + - $$id-clickhouse + image: plausible/analytics:$$core_version + environment: + - ADMIN_USER_EMAIL=$$config_admin_user_email + - ADMIN_USER_NAME=$$config_admin_user_name + - ADMIN_USER_PWD=$$secret_admin_user_pwd + - BASE_URL=$$config_base_url + - SECRET_KEY_BASE=$$secret_secret_key_base + - DISABLE_AUTH=$$config_disable_auth + - DISABLE_REGISTRATION=$$config_disable_registration + - DATABASE_URL=$$secret_database_url + - CLICKHOUSE_DATABASE_URL=$$secret_clickhouse_database_url + ports: + - "8000" + $$id-postgresql: + name: PostgreSQL + image: postgres:14-alpine + volumes: + - $$id-postgresql-data:/var/lib/postgresql/data + environment: + - POSTGRES_PASSWORD=$$secret_postgres_password + - POSTGRES_USER=$$config_postgres_user + - POSTGRES_DB=$$config_postgres_db + $$id-clickhouse: + name: Clickhouse + volumes: + - $$id-clickhouse-data:/var/lib/clickhouse + image: clickhouse/clickhouse-server:22.6-alpine + ulimits: + nofile: + soft: 262144 + hard: 262144 + files: + - location: /etc/clickhouse-server/users.d/logging.xml + content: >- + warningtrue + - location: /etc/clickhouse-server/config.d/logging.xml + content: >- + 00 + - location: /docker-entrypoint-initdb.d/init.query + content: CREATE DATABASE IF NOT EXISTS plausible; + - location: /docker-entrypoint-initdb.d/init-db.sh + content: >- + clickhouse client --queries-file + /docker-entrypoint-initdb.d/init.query + variables: + - id: $$config_base_url + name: BASE_URL + label: Base URL + defaultValue: $$generate_fqdn + description: >- + You must set this to the FQDN of the Plausible Analytics instance. This is + used to generate the links to the Plausible Analytics instance. + - id: $$secret_database_url + name: DATABASE_URL + label: Database URL for PostgreSQL + defaultValue: >- + postgresql://$$config_postgres_user:$$secret_postgres_password@$$id-postgresql:5432/$$config_postgres_db + description: "" + - id: $$secret_clickhouse_database_url + name: CLICKHOUSE_DATABASE_URL + label: Database URL for Clickhouse + defaultValue: http://$$id-clickhouse:8123/plausible + description: "" + - id: $$config_admin_user_email + name: ADMIN_USER_EMAIL + label: Admin Email Address + defaultValue: admin@example.com + description: This is the admin email. Please change it. + - id: $$config_admin_user_name + name: ADMIN_USER_NAME + label: Admin User Name + defaultValue: $$generate_username + description: This is the admin username. Please change it. + - id: $$secret_admin_user_pwd + name: ADMIN_USER_PWD + label: Admin User Password + defaultValue: $$generate_password + description: This is the admin password. Please change it. + showOnConfiguration: true + - id: $$secret_secret_key_base + name: SECRET_KEY_BASE + label: Secret Key Base + defaultValue: $$generate_hex(64) + description: "" + - id: $$config_disable_auth + name: DISABLE_AUTH + label: Disable Authentication + defaultValue: "false" + description: "" + - id: $$config_disable_registration + name: DISABLE_REGISTRATION + label: Disable Registration + defaultValue: "true" + description: "" + - id: $$config_postgres_user + main: $$id-postgresql + name: POSTGRES_USER + label: PostgreSQL Username + defaultValue: postgresql + description: "" + - id: $$secret_postgres_password + main: $$id-postgresql + name: POSTGRES_PASSWORD + label: PostgreSQL Password + defaultValue: $$generate_password + description: "" + showOnConfiguration: true + - id: $$config_postgres_db + main: $$id-postgresql + name: POSTGRES_DB + label: PostgreSQL Database + defaultValue: plausible + description: "" + - id: $$config_scriptName + name: SCRIPT_NAME + label: Custom Script Name + defaultValue: plausible.js + description: This is the default script name. - templateVersion: 1.0.0 defaultVersion: "1.17" documentation: https://docs.gitea.io @@ -2317,6 +2475,7 @@ ignore: true defaultVersion: latest documentation: https://docs.ghost.org + arch: amd64 type: ghost-mariadb name: Ghost subname: (MariaDB) @@ -2979,6 +3138,7 @@ - templateVersion: 1.0.0 defaultVersion: stable documentation: https://plausible.io/doc/ + arch: amd64 type: plausibleanalytics name: Plausible Analytics description: A lightweight and open-source website analytics tool. @@ -3013,7 +3173,7 @@ - "8000" $$id-postgresql: name: PostgreSQL - image: "bitnami/postgresql:13.2.0" + image: "bitnami/postgresql:13" volumes: - "$$id-postgresql-data:/bitnami/postgresql" environment: @@ -3024,7 +3184,7 @@ name: Clickhouse volumes: - "$$id-clickhouse-data:/var/lib/clickhouse" - image: "yandex/clickhouse-server:21.3.2.5" + image: "clickhouse/clickhouse-server:22.6-alpine" ulimits: nofile: soft: 262144 diff --git a/apps/api/nodemon.json b/apps/api/nodemon.json index 796707b71..8825a2262 100644 --- a/apps/api/nodemon.json +++ b/apps/api/nodemon.json @@ -1,7 +1,11 @@ { - "watch": ["src"], - "ignore": ["src/**/*.test.ts"], - "ext": "ts,mjs,json,graphql", - "exec": "rimraf build && esbuild `find src \\( -name '*.ts' \\)` --minify=true --platform=node --outdir=build --format=cjs && node build", - "legacyWatch": true - } \ No newline at end of file + "watch": [ + "src" + ], + "ignore": [ + "src/**/*.test.ts" + ], + "ext": "ts,mjs,json,graphql", + "exec": "rimraf build && esbuild `find src \\( -name '*.ts' \\)` --platform=node --outdir=build --format=cjs && node build", + "legacyWatch": true +} \ No newline at end of file diff --git a/apps/api/package.json b/apps/api/package.json index 89c075e27..ad987b719 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -9,7 +9,7 @@ "db:studio": "prisma studio", "db:migrate": "COOLIFY_DATABASE_URL=file:../db/migration.db prisma migrate dev --skip-seed --name", "dev": "nodemon", - "build": "rimraf build && esbuild `find src \\( -name '*.ts' \\)| grep -v client/` --minify=true --platform=node --outdir=build --format=cjs", + "build": "rimraf build && esbuild `find src \\( -name '*.ts' \\)| grep -v client/` --platform=node --outdir=build --format=cjs", "format": "prettier --write 'src/**/*.{js,ts,json,md}'", "lint": "prettier --check 'src/**/*.{js,ts,json,md}' && eslint --ignore-path .eslintignore .", "start": "NODE_ENV=production pnpm prisma migrate deploy && pnpm prisma generate && pnpm prisma db seed && node index.js" @@ -26,9 +26,11 @@ "@iarna/toml": "2.2.5", "@ladjs/graceful": "3.0.2", "@prisma/client": "4.6.1", + "@sentry/node": "7.21.1", + "@sentry/tracing": "7.21.1", + "axe": "11.0.0", "bcryptjs": "2.4.3", "bree": "9.1.2", - "axe":"11.0.0", "cabin": "11.0.1", "compare-versions": "5.0.1", "csv-parse": "5.3.2", @@ -47,6 +49,7 @@ "is-port-reachable": "4.0.0", "js-yaml": "4.1.0", "jsonwebtoken": "8.5.1", + "minimist": "^1.2.7", "node-forge": "1.3.1", "node-os-utils": "1.3.7", "p-all": "4.0.0", @@ -54,6 +57,7 @@ "prisma": "4.6.1", "public-ip": "6.0.1", "pump": "3.0.0", + "shell-quote": "^1.7.4", "socket.io": "4.5.3", "ssh-config": "4.1.6", "strip-ansi": "7.0.1", diff --git a/apps/api/prisma/migrations/20221123122143_remote_haproxy_from_db/migration.sql b/apps/api/prisma/migrations/20221123122143_remote_haproxy_from_db/migration.sql new file mode 100644 index 000000000..1acf261a5 --- /dev/null +++ b/apps/api/prisma/migrations/20221123122143_remote_haproxy_from_db/migration.sql @@ -0,0 +1,37 @@ +/* + Warnings: + + - You are about to drop the column `proxyHash` on the `Setting` table. All the data in the column will be lost. + - You are about to drop the column `proxyPassword` on the `Setting` table. All the data in the column will be lost. + - You are about to drop the column `proxyUser` on the `Setting` table. All the data in the column will be lost. + +*/ +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_Setting" ( + "id" TEXT NOT NULL PRIMARY KEY, + "fqdn" TEXT, + "dualCerts" BOOLEAN NOT NULL DEFAULT false, + "minPort" INTEGER NOT NULL DEFAULT 9000, + "maxPort" INTEGER NOT NULL DEFAULT 9100, + "DNSServers" TEXT, + "ipv4" TEXT, + "ipv6" TEXT, + "arch" TEXT, + "concurrentBuilds" INTEGER NOT NULL DEFAULT 1, + "applicationStoragePathMigrationFinished" BOOLEAN NOT NULL DEFAULT false, + "proxyDefaultRedirect" TEXT, + "isAPIDebuggingEnabled" BOOLEAN DEFAULT false, + "isRegistrationEnabled" BOOLEAN NOT NULL DEFAULT false, + "isAutoUpdateEnabled" BOOLEAN NOT NULL DEFAULT false, + "isDNSCheckEnabled" BOOLEAN NOT NULL DEFAULT true, + "isTraefikUsed" BOOLEAN NOT NULL DEFAULT true, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); +INSERT INTO "new_Setting" ("DNSServers", "applicationStoragePathMigrationFinished", "arch", "concurrentBuilds", "createdAt", "dualCerts", "fqdn", "id", "ipv4", "ipv6", "isAPIDebuggingEnabled", "isAutoUpdateEnabled", "isDNSCheckEnabled", "isRegistrationEnabled", "isTraefikUsed", "maxPort", "minPort", "proxyDefaultRedirect", "updatedAt") SELECT "DNSServers", "applicationStoragePathMigrationFinished", "arch", "concurrentBuilds", "createdAt", "dualCerts", "fqdn", "id", "ipv4", "ipv6", "isAPIDebuggingEnabled", "isAutoUpdateEnabled", "isDNSCheckEnabled", "isRegistrationEnabled", "isTraefikUsed", "maxPort", "minPort", "proxyDefaultRedirect", "updatedAt" FROM "Setting"; +DROP TABLE "Setting"; +ALTER TABLE "new_Setting" RENAME TO "Setting"; +CREATE UNIQUE INDEX "Setting_fqdn_key" ON "Setting"("fqdn"); +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; diff --git a/apps/api/prisma/migrations/20221123133429_docker_registries/migration.sql b/apps/api/prisma/migrations/20221123133429_docker_registries/migration.sql new file mode 100644 index 000000000..abebc5514 --- /dev/null +++ b/apps/api/prisma/migrations/20221123133429_docker_registries/migration.sql @@ -0,0 +1,59 @@ +-- CreateTable +CREATE TABLE "DockerRegistry" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "url" TEXT NOT NULL, + "username" TEXT, + "password" TEXT, + "isSystemWide" BOOLEAN NOT NULL DEFAULT false, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "teamId" TEXT, + CONSTRAINT "DockerRegistry_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "Team" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_Application" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "fqdn" TEXT, + "repository" TEXT, + "configHash" TEXT, + "branch" TEXT, + "buildPack" TEXT, + "projectId" INTEGER, + "port" INTEGER, + "exposePort" INTEGER, + "installCommand" TEXT, + "buildCommand" TEXT, + "startCommand" TEXT, + "baseDirectory" TEXT, + "publishDirectory" TEXT, + "deploymentType" TEXT, + "phpModules" TEXT, + "pythonWSGI" TEXT, + "pythonModule" TEXT, + "pythonVariable" TEXT, + "dockerFileLocation" TEXT, + "denoMainFile" TEXT, + "denoOptions" TEXT, + "dockerComposeFile" TEXT, + "dockerComposeFileLocation" TEXT, + "dockerComposeConfiguration" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "destinationDockerId" TEXT, + "gitSourceId" TEXT, + "baseImage" TEXT, + "baseBuildImage" TEXT, + "dockerRegistryId" TEXT NOT NULL DEFAULT '0', + CONSTRAINT "Application_gitSourceId_fkey" FOREIGN KEY ("gitSourceId") REFERENCES "GitSource" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "Application_destinationDockerId_fkey" FOREIGN KEY ("destinationDockerId") REFERENCES "DestinationDocker" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "Application_dockerRegistryId_fkey" FOREIGN KEY ("dockerRegistryId") REFERENCES "DockerRegistry" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); +INSERT INTO "new_Application" ("baseBuildImage", "baseDirectory", "baseImage", "branch", "buildCommand", "buildPack", "configHash", "createdAt", "denoMainFile", "denoOptions", "deploymentType", "destinationDockerId", "dockerComposeConfiguration", "dockerComposeFile", "dockerComposeFileLocation", "dockerFileLocation", "exposePort", "fqdn", "gitSourceId", "id", "installCommand", "name", "phpModules", "port", "projectId", "publishDirectory", "pythonModule", "pythonVariable", "pythonWSGI", "repository", "startCommand", "updatedAt") SELECT "baseBuildImage", "baseDirectory", "baseImage", "branch", "buildCommand", "buildPack", "configHash", "createdAt", "denoMainFile", "denoOptions", "deploymentType", "destinationDockerId", "dockerComposeConfiguration", "dockerComposeFile", "dockerComposeFileLocation", "dockerFileLocation", "exposePort", "fqdn", "gitSourceId", "id", "installCommand", "name", "phpModules", "port", "projectId", "publishDirectory", "pythonModule", "pythonVariable", "pythonWSGI", "repository", "startCommand", "updatedAt" FROM "Application"; +DROP TABLE "Application"; +ALTER TABLE "new_Application" RENAME TO "Application"; +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; diff --git a/apps/api/prisma/migrations/20221128104158_do_not_track/migration.sql b/apps/api/prisma/migrations/20221128104158_do_not_track/migration.sql new file mode 100644 index 000000000..9cf26d8a8 --- /dev/null +++ b/apps/api/prisma/migrations/20221128104158_do_not_track/migration.sql @@ -0,0 +1,30 @@ +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_Setting" ( + "id" TEXT NOT NULL PRIMARY KEY, + "fqdn" TEXT, + "dualCerts" BOOLEAN NOT NULL DEFAULT false, + "minPort" INTEGER NOT NULL DEFAULT 9000, + "maxPort" INTEGER NOT NULL DEFAULT 9100, + "DNSServers" TEXT, + "ipv4" TEXT, + "ipv6" TEXT, + "arch" TEXT, + "concurrentBuilds" INTEGER NOT NULL DEFAULT 1, + "applicationStoragePathMigrationFinished" BOOLEAN NOT NULL DEFAULT false, + "proxyDefaultRedirect" TEXT, + "doNotTrack" BOOLEAN NOT NULL DEFAULT false, + "isAPIDebuggingEnabled" BOOLEAN DEFAULT false, + "isRegistrationEnabled" BOOLEAN NOT NULL DEFAULT false, + "isAutoUpdateEnabled" BOOLEAN NOT NULL DEFAULT false, + "isDNSCheckEnabled" BOOLEAN NOT NULL DEFAULT true, + "isTraefikUsed" BOOLEAN NOT NULL DEFAULT true, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); +INSERT INTO "new_Setting" ("DNSServers", "applicationStoragePathMigrationFinished", "arch", "concurrentBuilds", "createdAt", "dualCerts", "fqdn", "id", "ipv4", "ipv6", "isAPIDebuggingEnabled", "isAutoUpdateEnabled", "isDNSCheckEnabled", "isRegistrationEnabled", "isTraefikUsed", "maxPort", "minPort", "proxyDefaultRedirect", "updatedAt") SELECT "DNSServers", "applicationStoragePathMigrationFinished", "arch", "concurrentBuilds", "createdAt", "dualCerts", "fqdn", "id", "ipv4", "ipv6", "isAPIDebuggingEnabled", "isAutoUpdateEnabled", "isDNSCheckEnabled", "isRegistrationEnabled", "isTraefikUsed", "maxPort", "minPort", "proxyDefaultRedirect", "updatedAt" FROM "Setting"; +DROP TABLE "Setting"; +ALTER TABLE "new_Setting" RENAME TO "Setting"; +CREATE UNIQUE INDEX "Setting_fqdn_key" ON "Setting"("fqdn"); +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; diff --git a/apps/api/prisma/migrations/20221128104718_fix_defaults/migration.sql b/apps/api/prisma/migrations/20221128104718_fix_defaults/migration.sql new file mode 100644 index 000000000..c1152cdc3 --- /dev/null +++ b/apps/api/prisma/migrations/20221128104718_fix_defaults/migration.sql @@ -0,0 +1,60 @@ +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_Setting" ( + "id" TEXT NOT NULL PRIMARY KEY, + "fqdn" TEXT, + "dualCerts" BOOLEAN NOT NULL DEFAULT false, + "minPort" INTEGER NOT NULL DEFAULT 9000, + "maxPort" INTEGER NOT NULL DEFAULT 9100, + "DNSServers" TEXT, + "ipv4" TEXT, + "ipv6" TEXT, + "arch" TEXT, + "concurrentBuilds" INTEGER NOT NULL DEFAULT 1, + "applicationStoragePathMigrationFinished" BOOLEAN NOT NULL DEFAULT false, + "proxyDefaultRedirect" TEXT, + "doNotTrack" BOOLEAN NOT NULL DEFAULT false, + "isAPIDebuggingEnabled" BOOLEAN NOT NULL DEFAULT false, + "isRegistrationEnabled" BOOLEAN NOT NULL DEFAULT false, + "isAutoUpdateEnabled" BOOLEAN NOT NULL DEFAULT false, + "isDNSCheckEnabled" BOOLEAN NOT NULL DEFAULT true, + "isTraefikUsed" BOOLEAN NOT NULL DEFAULT true, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); +INSERT INTO "new_Setting" ("DNSServers", "applicationStoragePathMigrationFinished", "arch", "concurrentBuilds", "createdAt", "doNotTrack", "dualCerts", "fqdn", "id", "ipv4", "ipv6", "isAPIDebuggingEnabled", "isAutoUpdateEnabled", "isDNSCheckEnabled", "isRegistrationEnabled", "isTraefikUsed", "maxPort", "minPort", "proxyDefaultRedirect", "updatedAt") SELECT "DNSServers", "applicationStoragePathMigrationFinished", "arch", "concurrentBuilds", "createdAt", "doNotTrack", "dualCerts", "fqdn", "id", "ipv4", "ipv6", coalesce("isAPIDebuggingEnabled", false) AS "isAPIDebuggingEnabled", "isAutoUpdateEnabled", "isDNSCheckEnabled", "isRegistrationEnabled", "isTraefikUsed", "maxPort", "minPort", "proxyDefaultRedirect", "updatedAt" FROM "Setting"; +DROP TABLE "Setting"; +ALTER TABLE "new_Setting" RENAME TO "Setting"; +CREATE UNIQUE INDEX "Setting_fqdn_key" ON "Setting"("fqdn"); +CREATE TABLE "new_GlitchTip" ( + "id" TEXT NOT NULL PRIMARY KEY, + "postgresqlUser" TEXT NOT NULL, + "postgresqlPassword" TEXT NOT NULL, + "postgresqlDatabase" TEXT NOT NULL, + "postgresqlPublicPort" INTEGER, + "secretKeyBase" TEXT, + "defaultEmail" TEXT NOT NULL, + "defaultUsername" TEXT NOT NULL, + "defaultPassword" TEXT NOT NULL, + "defaultEmailFrom" TEXT NOT NULL DEFAULT 'glitchtip@domain.tdl', + "emailSmtpHost" TEXT DEFAULT 'domain.tdl', + "emailSmtpPort" INTEGER DEFAULT 25, + "emailSmtpUser" TEXT, + "emailSmtpPassword" TEXT, + "emailSmtpUseTls" BOOLEAN NOT NULL DEFAULT false, + "emailSmtpUseSsl" BOOLEAN NOT NULL DEFAULT false, + "emailBackend" TEXT, + "mailgunApiKey" TEXT, + "sendgridApiKey" TEXT, + "enableOpenUserRegistration" BOOLEAN NOT NULL DEFAULT true, + "serviceId" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "GlitchTip_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); +INSERT INTO "new_GlitchTip" ("createdAt", "defaultEmail", "defaultEmailFrom", "defaultPassword", "defaultUsername", "emailBackend", "emailSmtpHost", "emailSmtpPassword", "emailSmtpPort", "emailSmtpUseSsl", "emailSmtpUseTls", "emailSmtpUser", "enableOpenUserRegistration", "id", "mailgunApiKey", "postgresqlDatabase", "postgresqlPassword", "postgresqlPublicPort", "postgresqlUser", "secretKeyBase", "sendgridApiKey", "serviceId", "updatedAt") SELECT "createdAt", "defaultEmail", "defaultEmailFrom", "defaultPassword", "defaultUsername", "emailBackend", "emailSmtpHost", "emailSmtpPassword", "emailSmtpPort", coalesce("emailSmtpUseSsl", false) AS "emailSmtpUseSsl", coalesce("emailSmtpUseTls", false) AS "emailSmtpUseTls", "emailSmtpUser", "enableOpenUserRegistration", "id", "mailgunApiKey", "postgresqlDatabase", "postgresqlPassword", "postgresqlPublicPort", "postgresqlUser", "secretKeyBase", "sendgridApiKey", "serviceId", "updatedAt" FROM "GlitchTip"; +DROP TABLE "GlitchTip"; +ALTER TABLE "new_GlitchTip" RENAME TO "GlitchTip"; +CREATE UNIQUE INDEX "GlitchTip_serviceId_key" ON "GlitchTip"("serviceId"); +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; diff --git a/apps/api/prisma/migrations/20221128105615_custom_sentry/migration.sql b/apps/api/prisma/migrations/20221128105615_custom_sentry/migration.sql new file mode 100644 index 000000000..00857eb0d --- /dev/null +++ b/apps/api/prisma/migrations/20221128105615_custom_sentry/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Setting" ADD COLUMN "sentryDSN" TEXT; diff --git a/apps/api/prisma/migrations/20221129081832_fix_defaults/migration.sql b/apps/api/prisma/migrations/20221129081832_fix_defaults/migration.sql new file mode 100644 index 000000000..96f6de3fb --- /dev/null +++ b/apps/api/prisma/migrations/20221129081832_fix_defaults/migration.sql @@ -0,0 +1,31 @@ +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_Setting" ( + "id" TEXT NOT NULL PRIMARY KEY, + "fqdn" TEXT, + "dualCerts" BOOLEAN NOT NULL DEFAULT false, + "minPort" INTEGER NOT NULL DEFAULT 9000, + "maxPort" INTEGER NOT NULL DEFAULT 9100, + "DNSServers" TEXT NOT NULL DEFAULT '1.1.1.1,8.8.8.8', + "ipv4" TEXT, + "ipv6" TEXT, + "arch" TEXT, + "concurrentBuilds" INTEGER NOT NULL DEFAULT 1, + "applicationStoragePathMigrationFinished" BOOLEAN NOT NULL DEFAULT false, + "proxyDefaultRedirect" TEXT, + "doNotTrack" BOOLEAN NOT NULL DEFAULT false, + "sentryDSN" TEXT, + "isAPIDebuggingEnabled" BOOLEAN NOT NULL DEFAULT false, + "isRegistrationEnabled" BOOLEAN NOT NULL DEFAULT true, + "isAutoUpdateEnabled" BOOLEAN NOT NULL DEFAULT false, + "isDNSCheckEnabled" BOOLEAN NOT NULL DEFAULT true, + "isTraefikUsed" BOOLEAN NOT NULL DEFAULT true, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); +INSERT INTO "new_Setting" ("DNSServers", "applicationStoragePathMigrationFinished", "arch", "concurrentBuilds", "createdAt", "doNotTrack", "dualCerts", "fqdn", "id", "ipv4", "ipv6", "isAPIDebuggingEnabled", "isAutoUpdateEnabled", "isDNSCheckEnabled", "isRegistrationEnabled", "isTraefikUsed", "maxPort", "minPort", "proxyDefaultRedirect", "sentryDSN", "updatedAt") SELECT coalesce("DNSServers", '1.1.1.1,8.8.8.8') AS "DNSServers", "applicationStoragePathMigrationFinished", "arch", "concurrentBuilds", "createdAt", "doNotTrack", "dualCerts", "fqdn", "id", "ipv4", "ipv6", "isAPIDebuggingEnabled", "isAutoUpdateEnabled", "isDNSCheckEnabled", "isRegistrationEnabled", "isTraefikUsed", "maxPort", "minPort", "proxyDefaultRedirect", "sentryDSN", "updatedAt" FROM "Setting"; +DROP TABLE "Setting"; +ALTER TABLE "new_Setting" RENAME TO "Setting"; +CREATE UNIQUE INDEX "Setting_fqdn_key" ON "Setting"("fqdn"); +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; diff --git a/apps/api/prisma/migrations/20221129121702_preview_separator/migration.sql b/apps/api/prisma/migrations/20221129121702_preview_separator/migration.sql new file mode 100644 index 000000000..2fdff0eca --- /dev/null +++ b/apps/api/prisma/migrations/20221129121702_preview_separator/migration.sql @@ -0,0 +1,33 @@ +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_Setting" ( + "id" TEXT NOT NULL PRIMARY KEY, + "fqdn" TEXT, + "dualCerts" BOOLEAN NOT NULL DEFAULT false, + "minPort" INTEGER NOT NULL DEFAULT 9000, + "maxPort" INTEGER NOT NULL DEFAULT 9100, + "DNSServers" TEXT NOT NULL DEFAULT '1.1.1.1,8.8.8.8', + "ipv4" TEXT, + "ipv6" TEXT, + "arch" TEXT, + "concurrentBuilds" INTEGER NOT NULL DEFAULT 1, + "applicationStoragePathMigrationFinished" BOOLEAN NOT NULL DEFAULT false, + "numberOfDockerImagesKeptLocally" INTEGER NOT NULL DEFAULT 3, + "proxyDefaultRedirect" TEXT, + "doNotTrack" BOOLEAN NOT NULL DEFAULT false, + "sentryDSN" TEXT, + "previewSeparator" TEXT NOT NULL DEFAULT '.', + "isAPIDebuggingEnabled" BOOLEAN NOT NULL DEFAULT false, + "isRegistrationEnabled" BOOLEAN NOT NULL DEFAULT true, + "isAutoUpdateEnabled" BOOLEAN NOT NULL DEFAULT false, + "isDNSCheckEnabled" BOOLEAN NOT NULL DEFAULT true, + "isTraefikUsed" BOOLEAN NOT NULL DEFAULT true, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); +INSERT INTO "new_Setting" ("DNSServers", "applicationStoragePathMigrationFinished", "arch", "concurrentBuilds", "createdAt", "doNotTrack", "dualCerts", "fqdn", "id", "ipv4", "ipv6", "isAPIDebuggingEnabled", "isAutoUpdateEnabled", "isDNSCheckEnabled", "isRegistrationEnabled", "isTraefikUsed", "maxPort", "minPort", "numberOfDockerImagesKeptLocally", "proxyDefaultRedirect", "sentryDSN", "updatedAt") SELECT "DNSServers", "applicationStoragePathMigrationFinished", "arch", "concurrentBuilds", "createdAt", "doNotTrack", "dualCerts", "fqdn", "id", "ipv4", "ipv6", "isAPIDebuggingEnabled", "isAutoUpdateEnabled", "isDNSCheckEnabled", "isRegistrationEnabled", "isTraefikUsed", "maxPort", "minPort", "numberOfDockerImagesKeptLocally", "proxyDefaultRedirect", "sentryDSN", "updatedAt" FROM "Setting"; +DROP TABLE "Setting"; +ALTER TABLE "new_Setting" RENAME TO "Setting"; +CREATE UNIQUE INDEX "Setting_fqdn_key" ON "Setting"("fqdn"); +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; diff --git a/apps/api/prisma/migrations/20221129130036_keep_local_docker_images/migration.sql b/apps/api/prisma/migrations/20221129130036_keep_local_docker_images/migration.sql new file mode 100644 index 000000000..aa8136ed8 --- /dev/null +++ b/apps/api/prisma/migrations/20221129130036_keep_local_docker_images/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Application" ADD COLUMN "gitCommitHash" TEXT; diff --git a/apps/api/prisma/migrations/20221130142058_reconfigure_docker_registries/migration.sql b/apps/api/prisma/migrations/20221130142058_reconfigure_docker_registries/migration.sql new file mode 100644 index 000000000..c77afb687 --- /dev/null +++ b/apps/api/prisma/migrations/20221130142058_reconfigure_docker_registries/migration.sql @@ -0,0 +1,66 @@ +/* + Warnings: + + - You are about to drop the column `isSystemWide` on the `DockerRegistry` table. All the data in the column will be lost. + +*/ +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_DockerRegistry" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "url" TEXT NOT NULL, + "username" TEXT, + "password" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "teamId" TEXT, + CONSTRAINT "DockerRegistry_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "Team" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); +INSERT INTO "new_DockerRegistry" ("createdAt", "id", "name", "password", "teamId", "updatedAt", "url", "username") SELECT "createdAt", "id", "name", "password", "teamId", "updatedAt", "url", "username" FROM "DockerRegistry"; +DROP TABLE "DockerRegistry"; +ALTER TABLE "new_DockerRegistry" RENAME TO "DockerRegistry"; +CREATE TABLE "new_Application" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "fqdn" TEXT, + "repository" TEXT, + "configHash" TEXT, + "branch" TEXT, + "buildPack" TEXT, + "projectId" INTEGER, + "port" INTEGER, + "exposePort" INTEGER, + "installCommand" TEXT, + "buildCommand" TEXT, + "startCommand" TEXT, + "baseDirectory" TEXT, + "publishDirectory" TEXT, + "deploymentType" TEXT, + "phpModules" TEXT, + "pythonWSGI" TEXT, + "pythonModule" TEXT, + "pythonVariable" TEXT, + "dockerFileLocation" TEXT, + "denoMainFile" TEXT, + "denoOptions" TEXT, + "dockerComposeFile" TEXT, + "dockerComposeFileLocation" TEXT, + "dockerComposeConfiguration" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "destinationDockerId" TEXT, + "gitSourceId" TEXT, + "gitCommitHash" TEXT, + "baseImage" TEXT, + "baseBuildImage" TEXT, + "dockerRegistryId" TEXT, + CONSTRAINT "Application_gitSourceId_fkey" FOREIGN KEY ("gitSourceId") REFERENCES "GitSource" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "Application_destinationDockerId_fkey" FOREIGN KEY ("destinationDockerId") REFERENCES "DestinationDocker" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "Application_dockerRegistryId_fkey" FOREIGN KEY ("dockerRegistryId") REFERENCES "DockerRegistry" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); +INSERT INTO "new_Application" ("baseBuildImage", "baseDirectory", "baseImage", "branch", "buildCommand", "buildPack", "configHash", "createdAt", "denoMainFile", "denoOptions", "deploymentType", "destinationDockerId", "dockerComposeConfiguration", "dockerComposeFile", "dockerComposeFileLocation", "dockerFileLocation", "dockerRegistryId", "exposePort", "fqdn", "gitCommitHash", "gitSourceId", "id", "installCommand", "name", "phpModules", "port", "projectId", "publishDirectory", "pythonModule", "pythonVariable", "pythonWSGI", "repository", "startCommand", "updatedAt") SELECT "baseBuildImage", "baseDirectory", "baseImage", "branch", "buildCommand", "buildPack", "configHash", "createdAt", "denoMainFile", "denoOptions", "deploymentType", "destinationDockerId", "dockerComposeConfiguration", "dockerComposeFile", "dockerComposeFileLocation", "dockerFileLocation", "dockerRegistryId", "exposePort", "fqdn", "gitCommitHash", "gitSourceId", "id", "installCommand", "name", "phpModules", "port", "projectId", "publishDirectory", "pythonModule", "pythonVariable", "pythonWSGI", "repository", "startCommand", "updatedAt" FROM "Application"; +DROP TABLE "Application"; +ALTER TABLE "new_Application" RENAME TO "Application"; +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; diff --git a/apps/api/prisma/migrations/20221201115801_simple_dockerfile_deployment/migration.sql b/apps/api/prisma/migrations/20221201115801_simple_dockerfile_deployment/migration.sql new file mode 100644 index 000000000..b3406e59e --- /dev/null +++ b/apps/api/prisma/migrations/20221201115801_simple_dockerfile_deployment/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Application" ADD COLUMN "simpleDockerfile" TEXT; diff --git a/apps/api/prisma/migrations/20221201133847_push_image_to_docker_registry/migration.sql b/apps/api/prisma/migrations/20221201133847_push_image_to_docker_registry/migration.sql new file mode 100644 index 000000000..9f85d3518 --- /dev/null +++ b/apps/api/prisma/migrations/20221201133847_push_image_to_docker_registry/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Application" ADD COLUMN "dockerRegistryImageName" TEXT; diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index 49d40694d..54eefc504 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -21,26 +21,27 @@ model Certificate { model Setting { id String @id @default(cuid()) fqdn String? @unique - isAPIDebuggingEnabled Boolean? @default(false) - isRegistrationEnabled Boolean @default(false) dualCerts Boolean @default(false) minPort Int @default(9000) maxPort Int @default(9100) - proxyPassword String - proxyUser String - proxyHash String? - proxyDefaultRedirect String? - isAutoUpdateEnabled Boolean @default(false) - isDNSCheckEnabled Boolean @default(true) - DNSServers String? - isTraefikUsed Boolean @default(true) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + DNSServers String @default("1.1.1.1,8.8.8.8") ipv4 String? ipv6 String? arch String? concurrentBuilds Int @default(1) applicationStoragePathMigrationFinished Boolean @default(false) + numberOfDockerImagesKeptLocally Int @default(3) + proxyDefaultRedirect String? + doNotTrack Boolean @default(false) + sentryDSN String? + previewSeparator String @default(".") + isAPIDebuggingEnabled Boolean @default(false) + isRegistrationEnabled Boolean @default(true) + isAutoUpdateEnabled Boolean @default(false) + isDNSCheckEnabled Boolean @default(true) + isTraefikUsed Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt } model User { @@ -83,6 +84,7 @@ model Team { service Service[] users User[] certificate Certificate[] + dockerRegistry DockerRegistry[] } model TeamInvitation { @@ -96,7 +98,7 @@ model TeamInvitation { } model Application { - id String @id @default(cuid()) + id String @id @default(cuid()) name String fqdn String? repository String? @@ -122,20 +124,26 @@ model Application { dockerComposeFile String? dockerComposeFileLocation String? dockerComposeConfiguration String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt destinationDockerId String? gitSourceId String? + gitCommitHash String? baseImage String? baseBuildImage String? - gitSource GitSource? @relation(fields: [gitSourceId], references: [id]) - destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id]) - persistentStorage ApplicationPersistentStorage[] settings ApplicationSettings? - secrets Secret[] - teams Team[] - connectedDatabase ApplicationConnectedDatabase? - previewApplication PreviewApplication[] + dockerRegistryId String? + dockerRegistryImageName String? + simpleDockerfile String? + + persistentStorage ApplicationPersistentStorage[] + secrets Secret[] + teams Team[] + connectedDatabase ApplicationConnectedDatabase? + previewApplication PreviewApplication[] + gitSource GitSource? @relation(fields: [gitSourceId], references: [id]) + destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id]) + dockerRegistry DockerRegistry? @relation(fields: [dockerRegistryId], references: [id]) } model PreviewApplication { @@ -296,6 +304,19 @@ model SshKey { destinationDocker DestinationDocker[] } +model DockerRegistry { + id String @id @default(cuid()) + name String + url String + username String? + password String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + teamId String? + team Team? @relation(fields: [teamId], references: [id]) + application Application[] +} + model GitSource { id String @id @default(cuid()) name String @@ -626,8 +647,8 @@ model GlitchTip { emailSmtpPort Int? @default(25) emailSmtpUser String? emailSmtpPassword String? - emailSmtpUseTls Boolean? @default(false) - emailSmtpUseSsl Boolean? @default(false) + emailSmtpUseTls Boolean @default(false) + emailSmtpUseSsl Boolean @default(false) emailBackend String? mailgunApiKey String? sendgridApiKey String? diff --git a/apps/api/prisma/seed.js b/apps/api/prisma/seed.js index 39240123c..3d4b16a28 100644 --- a/apps/api/prisma/seed.js +++ b/apps/api/prisma/seed.js @@ -1,18 +1,8 @@ const dotEnvExtended = require('dotenv-extended'); dotEnvExtended.load(); const crypto = require('crypto'); -const generator = require('generate-password'); -const cuid = require('cuid'); const { PrismaClient } = require('@prisma/client'); const prisma = new PrismaClient(); - -function generatePassword(length = 24) { - return generator.generate({ - length, - numbers: true, - strict: true - }); -} const algorithm = 'aes-256-ctr'; async function main() { @@ -21,11 +11,8 @@ async function main() { if (!settingsFound) { await prisma.setting.create({ data: { - isRegistrationEnabled: true, - proxyPassword: encrypt(generatePassword()), - proxyUser: cuid(), + id: '0', arch: process.arch, - DNSServers: '1.1.1.1,8.8.8.8' } }); } else { @@ -34,11 +21,11 @@ async function main() { id: settingsFound.id }, data: { - isTraefikUsed: true, - proxyHash: null + id: '0' } }); } + // Create local docker engine const localDocker = await prisma.destinationDocker.findFirst({ where: { engine: '/var/run/docker.sock' } }); @@ -55,23 +42,18 @@ async function main() { // Set auto-update based on env variable const isAutoUpdateEnabled = process.env['COOLIFY_AUTO_UPDATE'] === 'true'; - const settings = await prisma.setting.findFirst({}); - if (settings) { - await prisma.setting.update({ - where: { - id: settings.id - }, - data: { - isAutoUpdateEnabled - } - }); - } + await prisma.setting.update({ + where: { + id: '0' + }, + data: { + isAutoUpdateEnabled + } + }); + // Create public github source const github = await prisma.gitSource.findFirst({ where: { htmlUrl: 'https://github.com', forPublic: true } }); - const gitlab = await prisma.gitSource.findFirst({ - where: { htmlUrl: 'https://gitlab.com', forPublic: true } - }); if (!github) { await prisma.gitSource.create({ data: { @@ -83,6 +65,10 @@ async function main() { } }); } + // Create public gitlab source + const gitlab = await prisma.gitSource.findFirst({ + where: { htmlUrl: 'https://gitlab.com', forPublic: true } + }); if (!gitlab) { await prisma.gitSource.create({ data: { diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index 4ae4cd9cc..b1d830adf 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -9,7 +9,7 @@ import autoLoad from '@fastify/autoload'; import socketIO from 'fastify-socket.io' import socketIOServer from './realtime' -import { asyncExecShell, cleanupDockerStorage, createRemoteEngineConfiguration, decrypt, encrypt, executeDockerCmd, executeSSHCmd, generateDatabaseConfiguration, isDev, listSettings, prisma, startTraefikProxy, startTraefikTCPProxy, version } from './lib/common'; +import { cleanupDockerStorage, createRemoteEngineConfiguration, decrypt, executeCommand, generateDatabaseConfiguration, isDev, listSettings, prisma, sentryDSN, startTraefikProxy, startTraefikTCPProxy, version } from './lib/common'; import { scheduler } from './lib/scheduler'; import { compareVersions } from 'compare-versions'; import Graceful from '@ladjs/graceful' @@ -19,14 +19,13 @@ import { verifyRemoteDockerEngineFn } from './routes/api/v1/destinations/handler import { checkContainer } from './lib/docker'; import { migrateApplicationPersistentStorage, migrateServicesToNewTemplate } from './lib'; import { refreshTags, refreshTemplates } from './routes/api/v1/handlers'; - +import * as Sentry from '@sentry/node'; declare module 'fastify' { interface FastifyInstance { config: { COOLIFY_APP_ID: string, COOLIFY_SECRET_KEY: string, COOLIFY_DATABASE_URL: string, - COOLIFY_SENTRY_DSN: string, COOLIFY_IS_ON: string, COOLIFY_WHITE_LABELED: string, COOLIFY_WHITE_LABELED_ICON: string | null, @@ -37,6 +36,7 @@ declare module 'fastify' { const port = isDev ? 3001 : 3000; const host = '0.0.0.0'; + (async () => { const settings = await prisma.setting.findFirst() const fastify = Fastify({ @@ -58,10 +58,6 @@ const host = '0.0.0.0'; type: 'string', default: 'file:../db/dev.db' }, - COOLIFY_SENTRY_DSN: { - type: 'string', - default: null - }, COOLIFY_IS_ON: { type: 'string', default: 'docker' @@ -114,7 +110,6 @@ const host = '0.0.0.0'; origin: isDev ? "*" : '' } }) - // To detect allowed origins // fastify.addHook('onRequest', async (request, reply) => { // console.log(request.headers.host) @@ -182,7 +177,7 @@ const host = '0.0.0.0'; setInterval(async () => { await migrateServicesToNewTemplate() - }, isDev ? 1000 : 60000) + }, isDev ? 10000 : 60000) setInterval(async () => { await copySSLCertificates(); @@ -207,14 +202,14 @@ async function getIPAddress() { try { const settings = await listSettings(); if (!settings.ipv4) { - console.log(`Getting public IPv4 address...`); const ipv4 = await publicIpv4({ timeout: 2000 }) + console.log(`Getting public IPv4 address...`); await prisma.setting.update({ where: { id: settings.id }, data: { ipv4 } }) } if (!settings.ipv6) { - console.log(`Getting public IPv6 address...`); const ipv6 = await publicIpv6({ timeout: 2000 }) + console.log(`Getting public IPv6 address...`); await prisma.setting.update({ where: { id: settings.id }, data: { ipv6 } }) } @@ -228,13 +223,13 @@ async function getTagsTemplates() { const tags = await fs.readFile('./devTags.json', 'utf8') await fs.writeFile('./templates.json', JSON.stringify(yaml.load(templates))) await fs.writeFile('./tags.json', tags) - console.log('Tags and templates loaded in dev mode...') + console.log('[004] Tags and templates loaded in dev mode...') } else { const tags = await got.get('https://get.coollabs.io/coolify/service-tags.json').text() const response = await got.get('https://get.coollabs.io/coolify/service-templates.yaml').text() await fs.writeFile('/app/templates.json', JSON.stringify(yaml.load(response))) await fs.writeFile('/app/tags.json', tags) - console.log('Tags and templates loaded...') + console.log('[004] Tags and templates loaded...') } } catch (error) { @@ -243,16 +238,44 @@ async function getTagsTemplates() { } } async function initServer() { + const appId = process.env['COOLIFY_APP_ID']; + const settings = await prisma.setting.findUnique({ where: { id: '0' } }) try { - console.log(`Initializing server...`); - await asyncExecShell(`docker network create --attachable coolify`); + if (settings.doNotTrack === true) { + console.log('[000] Telemetry disabled...') + + } else { + if (settings.sentryDSN !== sentryDSN) { + await prisma.setting.update({ where: { id: '0' }, data: { sentryDSN } }) + } + // Initialize Sentry + // Sentry.init({ + // dsn: sentryDSN, + // environment: isDev ? 'development' : 'production', + // release: version + // }); + // console.log('[000] Sentry initialized...') + } + } catch (error) { + console.error(error) + } + try { + console.log(`[001] Initializing server...`); + await executeCommand({ command: `docker network create --attachable coolify` }); } catch (error) { } try { + console.log(`[002] Cleanup stucked builds...`); const isOlder = compareVersions('3.8.1', version); if (isOlder === 1) { await prisma.build.updateMany({ where: { status: { in: ['running', 'queued'] } }, data: { status: 'failed' } }); } } catch (error) { } + try { + console.log('[003] Cleaning up old build sources under /tmp/build-sources/...'); + await fs.rm('/tmp/build-sources', { recursive: true, force: true }) + } catch (error) { + console.log(error) + } } async function getArch() { @@ -300,14 +323,10 @@ async function autoUpdater() { if (!isDev) { const { isAutoUpdateEnabled } = await prisma.setting.findFirst(); if (isAutoUpdateEnabled) { - await asyncExecShell(`docker pull coollabsio/coolify:${latestVersion}`); - await asyncExecShell(`env | grep '^COOLIFY' > .env`); - await asyncExecShell( - `sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .env` - ); - await asyncExecShell( - `docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db coollabsio/coolify:${latestVersion} /bin/sh -c "env | grep COOLIFY > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify coolify-fluentbit && docker rm coolify coolify-fluentbit && docker compose pull && docker compose up -d --force-recreate"` - ); + await executeCommand({ command: `docker pull coollabsio/coolify:${latestVersion}` }) + await executeCommand({ shell: true, command: `env | grep '^COOLIFY' > .env` }) + await executeCommand({ command: `sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .env` }) + await executeCommand({ shell: true, command: `docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db coollabsio/coolify:${latestVersion} /bin/sh -c "env | grep COOLIFY > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify coolify-fluentbit && docker rm coolify coolify-fluentbit && docker compose pull && docker compose up -d --force-recreate"` }) } } else { console.log('Updating (not really in dev mode).'); @@ -328,8 +347,8 @@ async function checkFluentBit() { }); const { found } = await checkContainer({ dockerId: id, container: 'coolify-fluentbit', remove: true }); if (!found) { - await asyncExecShell(`env | grep '^COOLIFY' > .env`); - await asyncExecShell(`docker compose up -d fluent-bit`); + await executeCommand({ shell: true, command: `env | grep '^COOLIFY' > .env` }); + await executeCommand({ command: `docker compose up -d fluent-bit` }); } } } catch (error) { @@ -439,25 +458,25 @@ async function copySSLCertificates() { } catch (error) { console.log(error) } finally { - await asyncExecShell(`find /tmp/ -maxdepth 1 -type f -name '*-*.pem' -delete`) + await executeCommand({ command: `find /tmp/ -maxdepth 1 -type f -name '*-*.pem' -delete` }) } } async function copyRemoteCertificates(id: string, dockerId: string, remoteIpAddress: string) { try { - await asyncExecShell(`scp /tmp/${id}-cert.pem /tmp/${id}-key.pem ${remoteIpAddress}:/tmp/`) - await executeSSHCmd({ dockerId, command: `docker exec coolify-proxy sh -c 'test -d /etc/traefik/acme/custom/ || mkdir -p /etc/traefik/acme/custom/'` }) - await executeSSHCmd({ dockerId, command: `docker cp /tmp/${id}-key.pem coolify-proxy:/etc/traefik/acme/custom/` }) - await executeSSHCmd({ dockerId, command: `docker cp /tmp/${id}-cert.pem coolify-proxy:/etc/traefik/acme/custom/` }) + await executeCommand({ command: `scp /tmp/${id}-cert.pem /tmp/${id}-key.pem ${remoteIpAddress}:/tmp/` }) + await executeCommand({ sshCommand: true, shell: true, dockerId, command: `docker exec coolify-proxy sh -c 'test -d /etc/traefik/acme/custom/ || mkdir -p /etc/traefik/acme/custom/'` }) + await executeCommand({ sshCommand: true, dockerId, command: `docker cp /tmp/${id}-key.pem coolify-proxy:/etc/traefik/acme/custom/` }) + await executeCommand({ sshCommand: true, dockerId, command: `docker cp /tmp/${id}-cert.pem coolify-proxy:/etc/traefik/acme/custom/` }) } catch (error) { console.log({ error }) } } async function copyLocalCertificates(id: string) { try { - await asyncExecShell(`docker exec coolify-proxy sh -c 'test -d /etc/traefik/acme/custom/ || mkdir -p /etc/traefik/acme/custom/'`) - await asyncExecShell(`docker cp /tmp/${id}-key.pem coolify-proxy:/etc/traefik/acme/custom/`) - await asyncExecShell(`docker cp /tmp/${id}-cert.pem coolify-proxy:/etc/traefik/acme/custom/`) + await executeCommand({ command: `docker exec coolify-proxy sh -c 'test -d /etc/traefik/acme/custom/ || mkdir -p /etc/traefik/acme/custom/'`, shell: true }) + await executeCommand({ command: `docker cp /tmp/${id}-key.pem coolify-proxy:/etc/traefik/acme/custom/` }) + await executeCommand({ command: `docker cp /tmp/${id}-cert.pem coolify-proxy:/etc/traefik/acme/custom/` }) } catch (error) { console.log({ error }) } @@ -475,12 +494,13 @@ async function cleanupStorage() { try { let stdout = null if (!isDev) { - const output = await executeDockerCmd({ dockerId: destination.id, command: `CONTAINER=$(docker ps -lq | head -1) && docker exec $CONTAINER sh -c 'df -kPT /'` }) + const output = await executeCommand({ dockerId: destination.id, command: `CONTAINER=$(docker ps -lq | head -1) && docker exec $CONTAINER sh -c 'df -kPT /'`, shell: true }) stdout = output.stdout; } else { - const output = await asyncExecShell( - `df -kPT /` - ); + const output = await executeCommand({ + command: + `df -kPT /` + }); stdout = output.stdout; } let lines = stdout.trim().split('\n'); diff --git a/apps/api/src/jobs/deployApplication.ts b/apps/api/src/jobs/deployApplication.ts index edc5c4864..90afea210 100644 --- a/apps/api/src/jobs/deployApplication.ts +++ b/apps/api/src/jobs/deployApplication.ts @@ -3,8 +3,8 @@ import crypto from 'crypto'; import fs from 'fs/promises'; import yaml from 'js-yaml'; -import { copyBaseConfigurationFiles, makeLabelForStandaloneApplication, saveBuildLog, setDefaultConfiguration } from '../lib/buildPacks/common'; -import { createDirectories, decrypt, defaultComposeConfiguration, executeDockerCmd, getDomain, prisma, decryptApplication } from '../lib/common'; +import { copyBaseConfigurationFiles, makeLabelForSimpleDockerfile, makeLabelForStandaloneApplication, saveBuildLog, saveDockerRegistryCredentials, setDefaultConfiguration } from '../lib/buildPacks/common'; +import { createDirectories, decrypt, defaultComposeConfiguration, getDomain, prisma, decryptApplication, isDev, pushToRegistry, executeCommand } from '../lib/common'; import * as importers from '../lib/importers'; import * as buildpacks from '../lib/buildPacks'; @@ -37,57 +37,257 @@ import * as buildpacks from '../lib/buildPacks'; for (const queueBuild of queuedBuilds) { actions.push(async () => { - let application = await prisma.application.findUnique({ where: { id: queueBuild.applicationId }, include: { destinationDocker: true, gitSource: { include: { githubApp: true, gitlabApp: true } }, persistentStorage: true, secrets: true, settings: true, teams: true } }) - let { id: buildId, type, sourceBranch = null, pullmergeRequestId = null, previewApplicationId = null, forceRebuild, sourceRepository = null } = queueBuild + let application = await prisma.application.findUnique({ where: { id: queueBuild.applicationId }, include: { dockerRegistry: true, destinationDocker: true, gitSource: { include: { githubApp: true, gitlabApp: true } }, persistentStorage: true, secrets: true, settings: true, teams: true } }) + + let { id: buildId, type, gitSourceId, sourceBranch = null, pullmergeRequestId = null, previewApplicationId = null, forceRebuild, sourceRepository = null } = queueBuild application = decryptApplication(application) + + if (!gitSourceId && application.simpleDockerfile) { + const { + id: applicationId, + destinationDocker, + destinationDockerId, + secrets, + port, + persistentStorage, + exposePort, + simpleDockerfile, + dockerRegistry + } = application + const { workdir } = await createDirectories({ repository: applicationId, buildId }); + try { + if (queueBuild.status === 'running') { + await saveBuildLog({ line: 'Building halted, restarting...', buildId, applicationId: application.id }); + } + const volumes = + persistentStorage?.map((storage) => { + if (storage.oldPath) { + return `${applicationId}${storage.path.replace(/\//gi, '-').replace('-app', '')}:${storage.path}`; + } + return `${applicationId}${storage.path.replace(/\//gi, '-')}:${storage.path}`; + }) || []; + + if (destinationDockerId) { + await prisma.build.update({ where: { id: buildId }, data: { status: 'running' } }); + try { + const { stdout: containers } = await executeCommand({ + dockerId: destinationDockerId, + command: `docker ps -a --filter 'label=com.docker.compose.service=${applicationId}' --format {{.ID}}` + }) + if (containers) { + const containerArray = containers.split('\n'); + if (containerArray.length > 0) { + for (const container of containerArray) { + await executeCommand({ dockerId: destinationDockerId, command: `docker stop -t 0 ${container}` }) + await executeCommand({ dockerId: destinationDockerId, command: `docker rm --force ${container}` }) + } + } + } + } catch (error) { + // + } + const envs = [ + `PORT=${port}` + ]; + if (secrets.length > 0) { + secrets.forEach((secret) => { + if (pullmergeRequestId) { + const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret) + if (isSecretFound.length > 0) { + envs.push(`${secret.name}=${isSecretFound[0].value}`); + } else { + envs.push(`${secret.name}=${secret.value}`); + } + } else { + if (!secret.isPRMRSecret) { + envs.push(`${secret.name}=${secret.value}`); + } + } + }); + } + await fs.writeFile(`${workdir}/.env`, envs.join('\n')); + + let envFound = false; + try { + envFound = !!(await fs.stat(`${workdir}/.env`)); + } catch (error) { + // + } + + await fs.writeFile(`${workdir}/Dockerfile`, simpleDockerfile); + if (dockerRegistry) { + const { url, username, password } = dockerRegistry + await saveDockerRegistryCredentials({ url, username, password, workdir }) + } + + const labels = makeLabelForSimpleDockerfile({ + applicationId, + type, + port: exposePort ? `${exposePort}:${port}` : port, + }); + try { + const composeVolumes = volumes.map((volume) => { + return { + [`${volume.split(':')[0]}`]: { + name: volume.split(':')[0] + } + }; + }); + const composeFile = { + version: '3.8', + services: { + [applicationId]: { + build: { + context: workdir, + }, + image: `${applicationId}:${buildId}`, + container_name: applicationId, + volumes, + labels, + env_file: envFound ? [`${workdir}/.env`] : [], + depends_on: [], + expose: [port], + ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), + ...defaultComposeConfiguration(destinationDocker.network), + } + }, + networks: { + [destinationDocker.network]: { + external: true + } + }, + volumes: Object.assign({}, ...composeVolumes) + }; + await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(composeFile)); + await executeCommand({ debug: true, dockerId: destinationDocker.id, command: `docker compose --project-directory ${workdir} up -d` }) + await saveBuildLog({ line: 'Deployed 🎉', buildId, applicationId }); + } catch (error) { + await saveBuildLog({ line: error, buildId, applicationId }); + const foundBuild = await prisma.build.findUnique({ where: { id: buildId } }) + if (foundBuild) { + await prisma.build.update({ + where: { id: buildId }, + data: { + status: 'failed' + } + }); + } + throw new Error(error); + } + + } + } catch (error) { + const foundBuild = await prisma.build.findUnique({ where: { id: buildId } }) + if (foundBuild) { + await prisma.build.update({ + where: { id: buildId }, + data: { + status: 'failed' + } + }); + } + if (error !== 1) { + await saveBuildLog({ line: error, buildId, applicationId: application.id }); + } + if (error instanceof Error) { + await saveBuildLog({ line: error.message, buildId, applicationId: application.id }); + } + await fs.rm(workdir, { recursive: true, force: true }); + return; + } + try { + if (application.dockerRegistryImageName) { + const customTag = application.dockerRegistryImageName.split(':')[1] || buildId; + const imageName = application.dockerRegistryImageName.split(':')[0]; + await saveBuildLog({ line: `Pushing ${imageName}:${customTag} to Docker Registry... It could take a while...`, buildId, applicationId: application.id }); + await pushToRegistry(application, workdir, buildId, imageName, customTag) + await saveBuildLog({ line: "Success", buildId, applicationId: application.id }); + } + } catch (error) { + if (error.stdout) { + await saveBuildLog({ line: error.stdout, buildId, applicationId }); + } + if (error.stderr) { + await saveBuildLog({ line: error.stderr, buildId, applicationId }); + } + } finally { + await fs.rm(workdir, { recursive: true, force: true }); + await prisma.build.update({ where: { id: buildId }, data: { status: 'success' } }); + } + return; + } + const originalApplicationId = application.id + const { + id: applicationId, + name, + destinationDocker, + destinationDockerId, + gitSource, + configHash, + fqdn, + projectId, + secrets, + phpModules, + settings, + persistentStorage, + pythonWSGI, + pythonModule, + pythonVariable, + denoOptions, + exposePort, + baseImage, + baseBuildImage, + deploymentType, + gitCommitHash, + dockerRegistry + } = application + + let { + branch, + repository, + buildPack, + port, + installCommand, + buildCommand, + startCommand, + baseDirectory, + publishDirectory, + dockerFileLocation, + dockerComposeFileLocation, + dockerComposeConfiguration, + denoMainFile + } = application + + let imageId = applicationId; + let domain = getDomain(fqdn); + + let location = null; + + let tag = null; + let customTag = null; + let imageName = null; + + let imageFoundLocally = false; + let imageFoundRemotely = false; + if (pullmergeRequestId) { const previewApplications = await prisma.previewApplication.findMany({ where: { applicationId: originalApplicationId, pullmergeRequestId } }) if (previewApplications.length > 0) { previewApplicationId = previewApplications[0].id } + // Previews, we need to get the source branch and set subdomain + branch = sourceBranch; + domain = `${pullmergeRequestId}.${domain}`; + imageId = `${applicationId}-${pullmergeRequestId}`; + repository = sourceRepository || repository; } - const usableApplicationId = previewApplicationId || originalApplicationId + const { workdir, repodir } = await createDirectories({ repository, buildId }); try { if (queueBuild.status === 'running') { await saveBuildLog({ line: 'Building halted, restarting...', buildId, applicationId: application.id }); } - const { - id: applicationId, - name, - destinationDocker, - destinationDockerId, - gitSource, - configHash, - fqdn, - projectId, - secrets, - phpModules, - settings, - persistentStorage, - pythonWSGI, - pythonModule, - pythonVariable, - denoOptions, - exposePort, - baseImage, - baseBuildImage, - deploymentType, - } = application - let { - branch, - repository, - buildPack, - port, - installCommand, - buildCommand, - startCommand, - baseDirectory, - publishDirectory, - dockerFileLocation, - dockerComposeConfiguration, - denoMainFile - } = application + const currentHash = crypto .createHash('sha256') .update( @@ -113,22 +313,21 @@ import * as buildpacks from '../lib/buildPacks'; ) .digest('hex'); const { debug } = settings; - let imageId = applicationId; - let domain = getDomain(fqdn); + if (!debug) { + await saveBuildLog({ + line: `Debug logging is disabled. Enable it above if necessary!`, + buildId, + applicationId + }); + } const volumes = persistentStorage?.map((storage) => { if (storage.oldPath) { - return `${applicationId}${storage.path.replace(/\//gi, '-').replace('-app','')}:${storage.path}`; + return `${applicationId}${storage.path.replace(/\//gi, '-').replace('-app', '')}:${storage.path}`; } return `${applicationId}${storage.path.replace(/\//gi, '-')}:${storage.path}`; }) || []; - // Previews, we need to get the source branch and set subdomain - if (pullmergeRequestId) { - branch = sourceBranch; - domain = `${pullmergeRequestId}.${domain}`; - imageId = `${applicationId}-${pullmergeRequestId}`; - repository = sourceRepository || repository; - } + try { dockerComposeConfiguration = JSON.parse(dockerComposeConfiguration) @@ -141,7 +340,7 @@ import * as buildpacks from '../lib/buildPacks'; } if (destinationType === 'docker') { await prisma.build.update({ where: { id: buildId }, data: { status: 'running' } }); - const { workdir, repodir } = await createDirectories({ repository, buildId }); + const configuration = await setDefaultConfiguration(application); buildPack = configuration.buildPack; @@ -152,6 +351,7 @@ import * as buildpacks from '../lib/buildPacks'; publishDirectory = configuration.publishDirectory; baseDirectory = configuration.baseDirectory || ''; dockerFileLocation = configuration.dockerFileLocation; + dockerComposeFileLocation = configuration.dockerComposeFileLocation; denoMainFile = configuration.denoMainFile; const commit = await importers[gitSource.type]({ applicationId, @@ -161,6 +361,8 @@ import * as buildpacks from '../lib/buildPacks'; githubAppId: gitSource.githubApp?.id, gitlabAppId: gitSource.gitlabApp?.id, customPort: gitSource.customPort, + gitCommitHash, + configuration, repository, branch, buildId, @@ -174,10 +376,21 @@ import * as buildpacks from '../lib/buildPacks'; if (!commit) { throw new Error('No commit found?'); } - let tag = commit.slice(0, 7); + tag = commit.slice(0, 7); if (pullmergeRequestId) { tag = `${commit.slice(0, 7)}-${pullmergeRequestId}`; } + if (application.dockerRegistryImageName) { + imageName = application.dockerRegistryImageName.split(':')[0] + customTag = application.dockerRegistryImageName.split(':')[1] || tag + } else { + customTag = tag + imageName = applicationId; + } + + if (pullmergeRequestId) { + customTag = `${customTag}-${pullmergeRequestId}`; + } try { await prisma.build.update({ where: { id: buildId }, data: { commit } }); @@ -187,7 +400,7 @@ import * as buildpacks from '../lib/buildPacks'; if (configHash !== currentHash) { deployNeeded = true; if (configHash) { - await saveBuildLog({ line: 'Configuration changed.', buildId, applicationId }); + await saveBuildLog({ line: 'Configuration changed', buildId, applicationId }); } } else { deployNeeded = false; @@ -196,16 +409,33 @@ import * as buildpacks from '../lib/buildPacks'; deployNeeded = true; } - let imageFound = false; try { - await executeDockerCmd({ + await executeCommand({ dockerId: destinationDocker.id, command: `docker image inspect ${applicationId}:${tag}` }) - imageFound = true; + imageFoundLocally = true; } catch (error) { // } + if (dockerRegistry) { + const { url, username, password } = dockerRegistry + location = await saveDockerRegistryCredentials({ url, username, password, workdir }) + } + + try { + await executeCommand({ + dockerId: destinationDocker.id, + command: `docker ${location ? `--config ${location}` : ''} pull ${imageName}:${customTag}` + }) + imageFoundRemotely = true; + } catch (error) { + // + } + let imageFound = `${applicationId}:${tag}` + if (imageFoundRemotely) { + imageFound = `${imageName}:${customTag}` + } await copyBaseConfigurationFiles(buildPack, workdir, buildId, applicationId, baseImage); const labels = makeLabelForStandaloneApplication({ applicationId, @@ -226,7 +456,7 @@ import * as buildpacks from '../lib/buildPacks'; publishDirectory }); if (forceRebuild) deployNeeded = true - if (!imageFound || deployNeeded) { + if ((!imageFoundLocally && !imageFoundRemotely) || deployNeeded) { if (buildpacks[buildPack]) await buildpacks[buildPack]({ dockerId: destinationDocker.id, @@ -260,6 +490,7 @@ import * as buildpacks from '../lib/buildPacks'; pythonVariable, dockerFileLocation, dockerComposeConfiguration, + dockerComposeFileLocation, denoMainFile, denoOptions, baseImage, @@ -271,26 +502,37 @@ import * as buildpacks from '../lib/buildPacks'; throw new Error(`Build pack ${buildPack} not found.`); } } else { - await saveBuildLog({ line: 'Build image already available - no rebuild required.', buildId, applicationId }); + if (imageFoundRemotely || deployNeeded) { + await saveBuildLog({ line: `Container image ${imageFound} found in Docker Registry - reuising it`, buildId, applicationId }); + } else { + if (imageFoundLocally || deployNeeded) { + await saveBuildLog({ line: `Container image ${imageFound} found locally - reuising it`, buildId, applicationId }); + } + } } if (buildPack === 'compose') { try { - await executeDockerCmd({ + const { stdout: containers } = await executeCommand({ dockerId: destinationDockerId, - command: `docker ps -a --filter 'label=coolify.applicationId=${applicationId}' --format {{.ID}}|xargs -r -n 1 docker stop -t 0` - }) - await executeDockerCmd({ - dockerId: destinationDockerId, - command: `docker ps -a --filter 'label=coolify.applicationId=${applicationId}' --format {{.ID}}|xargs -r -n 1 docker rm --force` + command: `docker ps -a --filter 'label=coolify.applicationId=${applicationId}' --format {{.ID}}` }) + if (containers) { + const containerArray = containers.split('\n'); + if (containerArray.length > 0) { + for (const container of containerArray) { + await executeCommand({ dockerId: destinationDockerId, command: `docker stop -t 0 ${container}` }) + await executeCommand({ dockerId: destinationDockerId, command: `docker rm --force ${container}` }) + } + } + } } catch (error) { // } try { - await executeDockerCmd({ debug, buildId, applicationId, dockerId: destinationDocker.id, command: `docker compose --project-directory ${workdir} up -d` }) - await saveBuildLog({ line: 'Deployment successful!', buildId, applicationId }); - await saveBuildLog({ line: 'Proxy will be updated shortly.', buildId, applicationId }); + console.log({ debug }) + await executeCommand({ debug, buildId, applicationId, dockerId: destinationDocker.id, command: `docker compose --project-directory ${workdir} up -d` }) + await saveBuildLog({ line: 'Deployed 🎉', buildId, applicationId }); await prisma.build.update({ where: { id: buildId }, data: { status: 'success' } }); await prisma.application.update({ where: { id: applicationId }, @@ -312,14 +554,19 @@ import * as buildpacks from '../lib/buildPacks'; } else { try { - await executeDockerCmd({ + const { stdout: containers } = await executeCommand({ dockerId: destinationDockerId, - command: `docker ps -a --filter 'label=com.docker.compose.service=${pullmergeRequestId ? imageId : applicationId}' --format {{.ID}}|xargs -r -n 1 docker stop -t 0` - }) - await executeDockerCmd({ - dockerId: destinationDockerId, - command: `docker ps -a --filter 'label=com.docker.compose.service=${pullmergeRequestId ? imageId : applicationId}' --format {{.ID}}|xargs -r -n 1 docker rm --force` + command: `docker ps -a --filter 'label=com.docker.compose.service=${pullmergeRequestId ? imageId : applicationId}' --format {{.ID}}` }) + if (containers) { + const containerArray = containers.split('\n'); + if (containerArray.length > 0) { + for (const container of containerArray) { + await executeCommand({ dockerId: destinationDockerId, command: `docker stop -t 0 ${container}` }) + await executeCommand({ dockerId: destinationDockerId, command: `docker rm --force ${container}` }) + } + } + } } catch (error) { // } @@ -343,6 +590,10 @@ import * as buildpacks from '../lib/buildPacks'; }); } await fs.writeFile(`${workdir}/.env`, envs.join('\n')); + if (dockerRegistry) { + const { url, username, password } = dockerRegistry + await saveDockerRegistryCredentials({ url, username, password, workdir }) + } let envFound = false; try { @@ -351,7 +602,6 @@ import * as buildpacks from '../lib/buildPacks'; // } try { - await saveBuildLog({ line: 'Deployment started.', buildId, applicationId }); const composeVolumes = volumes.map((volume) => { return { [`${volume.split(':')[0]}`]: { @@ -363,7 +613,7 @@ import * as buildpacks from '../lib/buildPacks'; version: '3.8', services: { [imageId]: { - image: `${applicationId}:${tag}`, + image: imageFound, container_name: imageId, volumes, env_file: envFound ? [`${workdir}/.env`] : [], @@ -382,8 +632,8 @@ import * as buildpacks from '../lib/buildPacks'; volumes: Object.assign({}, ...composeVolumes) }; await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(composeFile)); - await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose --project-directory ${workdir} up -d` }) - await saveBuildLog({ line: 'Deployment successful!', buildId, applicationId }); + await executeCommand({ debug, dockerId: destinationDocker.id, command: `docker compose --project-directory ${workdir} up -d` }) + await saveBuildLog({ line: 'Deployed 🎉', buildId, applicationId }); } catch (error) { await saveBuildLog({ line: error, buildId, applicationId }); const foundBuild = await prisma.build.findUnique({ where: { id: buildId } }) @@ -397,16 +647,14 @@ import * as buildpacks from '../lib/buildPacks'; } throw new Error(error); } - await saveBuildLog({ line: 'Proxy will be updated shortly.', buildId, applicationId }); - await prisma.build.update({ where: { id: buildId }, data: { status: 'success' } }); + if (!pullmergeRequestId) await prisma.application.update({ where: { id: applicationId }, data: { configHash: currentHash } }); } } - } - catch (error) { + } catch (error) { const foundBuild = await prisma.build.findUnique({ where: { id: buildId } }) if (foundBuild) { await prisma.build.update({ @@ -419,6 +667,29 @@ import * as buildpacks from '../lib/buildPacks'; if (error !== 1) { await saveBuildLog({ line: error, buildId, applicationId: application.id }); } + if (error instanceof Error) { + await saveBuildLog({ line: error.message, buildId, applicationId: application.id }); + } + await fs.rm(workdir, { recursive: true, force: true }); + return; + } + try { + if (application.dockerRegistryImageName && (!imageFoundRemotely || forceRebuild)) { + await saveBuildLog({ line: `Pushing ${imageName}:${customTag} to Docker Registry... It could take a while...`, buildId, applicationId: application.id }); + await pushToRegistry(application, workdir, tag, imageName, customTag) + await saveBuildLog({ line: "Success", buildId, applicationId: application.id }); + } + } catch (error) { + if (error.stdout) { + await saveBuildLog({ line: error.stdout, buildId, applicationId }); + } + if (error.stderr) { + await saveBuildLog({ line: error.stderr, buildId, applicationId }); + } + + } finally { + await fs.rm(workdir, { recursive: true, force: true }); + await prisma.build.update({ where: { id: buildId }, data: { status: 'success' } }); } }); } diff --git a/apps/api/src/lib.ts b/apps/api/src/lib.ts index ad95c35c9..c4e02cb21 100644 --- a/apps/api/src/lib.ts +++ b/apps/api/src/lib.ts @@ -467,7 +467,6 @@ async function plausibleAnalytics(service: any, template: any) { // Disconnect old service data // await prisma.service.update({ where: { id: service.id }, data: { plausibleAnalytics: { disconnect: true } } }) } - async function migrateSettings(settings: any[], service: any, template: any) { for (const setting of settings) { try { @@ -528,4 +527,4 @@ async function createVolumes(service: any, template: any) { // console.log('Creating volume', volumeName, path, containerId, 'for service', service.id, ', service name:', service.name) await prisma.servicePersistentStorage.findFirst({ where: { volumeName, serviceId: service.id } }) || await prisma.servicePersistentStorage.create({ data: { volumeName, path, containerId, predefined: true, service: { connect: { id: service.id } } } }) } -} \ No newline at end of file +} diff --git a/apps/api/src/lib/buildPacks/common.ts b/apps/api/src/lib/buildPacks/common.ts index b2afafa1d..ba50b32b4 100644 --- a/apps/api/src/lib/buildPacks/common.ts +++ b/apps/api/src/lib/buildPacks/common.ts @@ -1,4 +1,4 @@ -import { base64Encode, encrypt, executeDockerCmd, generateTimestamp, getDomain, isDev, prisma, version } from "../common"; +import { base64Encode, decrypt, encrypt, executeCommand, generateTimestamp, getDomain, isARM, isDev, prisma, version } from "../common"; import { promises as fs } from 'fs'; import { day } from "../dayjs"; @@ -52,6 +52,14 @@ export function setDefaultBaseImage(buildPack: string | null, deploymentType: st { value: 'webdevops/apache:alpine', label: 'webdevops/apache:alpine' + }, + { + value: 'nginx:alpine', + label: 'nginx:alpine' + }, + { + value: 'httpd:alpine', + label: 'httpd:alpine (Apache)' } ]; const rustVersions = [ @@ -214,8 +222,20 @@ export function setDefaultBaseImage(buildPack: string | null, deploymentType: st label: 'webdevops/php-apache:7.1-alpine' }, { - value: 'webdevops/php-nginx:7.1-alpine', - label: 'webdevops/php-nginx:7.1-alpine' + value: 'php:8.1-fpm', + label: 'php:8.1-fpm' + }, + { + value: 'php:8.0-fpm', + label: 'php:8.0-fpm' + }, + { + value: 'php:8.1-fpm-alpine', + label: 'php:8.1-fpm-alpine' + }, + { + value: 'php:8.0-fpm-alpine', + label: 'php:8.0-fpm-alpine' } ]; const pythonVersions = [ @@ -306,8 +326,8 @@ export function setDefaultBaseImage(buildPack: string | null, deploymentType: st }; if (nodeBased.includes(buildPack)) { if (deploymentType === 'static') { - payload.baseImage = 'webdevops/nginx:alpine'; - payload.baseImages = staticVersions; + payload.baseImage = isARM(process.arch) ? 'nginx:alpine' : 'webdevops/nginx:alpine'; + payload.baseImages = isARM(process.arch) ? staticVersions.filter((version) => !version.value.includes('webdevops')) : staticVersions; payload.baseBuildImage = 'node:lts'; payload.baseBuildImages = nodeVersions; } else { @@ -318,8 +338,8 @@ export function setDefaultBaseImage(buildPack: string | null, deploymentType: st } } if (staticApps.includes(buildPack)) { - payload.baseImage = 'webdevops/nginx:alpine'; - payload.baseImages = staticVersions; + payload.baseImage = isARM(process.arch) ? 'nginx:alpine' : 'webdevops/nginx:alpine'; + payload.baseImages = isARM(process.arch) ? staticVersions.filter((version) => !version.value.includes('webdevops')) : staticVersions; payload.baseBuildImage = 'node:lts'; payload.baseBuildImages = nodeVersions; } @@ -337,12 +357,12 @@ export function setDefaultBaseImage(buildPack: string | null, deploymentType: st payload.baseImage = 'denoland/deno:latest'; } if (buildPack === 'php') { - payload.baseImage = 'webdevops/php-apache:8.2-alpine'; - payload.baseImages = phpVersions; + payload.baseImage = isARM(process.arch) ? 'php:8.1-fpm-alpine' : 'webdevops/php-apache:8.2-alpine'; + payload.baseImages = isARM(process.arch) ? phpVersions.filter((version) => !version.value.includes('webdevops')) : phpVersions } if (buildPack === 'laravel') { - payload.baseImage = 'webdevops/php-apache:8.2-alpine'; - payload.baseImages = phpVersions; + payload.baseImage = isARM(process.arch) ? 'php:8.1-fpm-alpine' : 'webdevops/php-apache:8.2-alpine'; + payload.baseImages = isARM(process.arch) ? phpVersions.filter((version) => !version.value.includes('webdevops')) : phpVersions payload.baseBuildImage = 'node:18'; payload.baseBuildImages = nodeVersions; } @@ -363,6 +383,7 @@ export const setDefaultConfiguration = async (data: any) => { publishDirectory, baseDirectory, dockerFileLocation, + dockerComposeFileLocation, denoMainFile } = data; //@ts-ignore @@ -392,6 +413,12 @@ export const setDefaultConfiguration = async (data: any) => { } else { dockerFileLocation = '/Dockerfile'; } + if (dockerComposeFileLocation) { + if (!dockerComposeFileLocation.startsWith('/')) dockerComposeFileLocation = `/${dockerComposeFileLocation}`; + if (dockerComposeFileLocation.endsWith('/')) dockerComposeFileLocation = dockerComposeFileLocation.slice(0, -1); + } else { + dockerComposeFileLocation = '/Dockerfile'; + } if (!denoMainFile) { denoMainFile = 'main.ts'; } @@ -405,6 +432,7 @@ export const setDefaultConfiguration = async (data: any) => { publishDirectory, baseDirectory, dockerFileLocation, + dockerComposeFileLocation, denoMainFile }; }; @@ -461,10 +489,12 @@ export const saveBuildLog = async ({ buildId: string; applicationId: string; }): Promise => { + if (buildId === 'undefined' || buildId === 'null' || !buildId) return; + if (applicationId === 'undefined' || applicationId === 'null' || !applicationId) return; const { default: got } = await import('got') if (typeof line === 'object' && line) { if (line.shortMessage) { - line = line.shortMessage + '\n' + line.stderr; + line = line.shortMessage + '\n' + line.stderr; } else { line = JSON.stringify(line); } @@ -564,6 +594,7 @@ export async function copyBaseConfigurationFiles( ` ); } + // TODO: Add more configuration files for other buildpacks, like apache2, etc. } catch (error) { throw new Error(error); } @@ -577,6 +608,29 @@ export function checkPnpm(installCommand = null, buildCommand = null, startComma ); } +export async function saveDockerRegistryCredentials({ url, username, password, workdir }) { + if (!username || !password) { + return null + } + + let decryptedPassword = decrypt(password); + const location = `${workdir}/.docker`; + + try { + await fs.mkdir(`${workdir}/.docker`); + } catch (error) { + console.log(error); + } + const payload = JSON.stringify({ + "auths": { + [url]: { + "auth": Buffer.from(`${username}:${decryptedPassword}`).toString('base64') + } + } + }) + await fs.writeFile(`${location}/config.json`, payload) + return location +} export async function buildImage({ applicationId, tag, @@ -589,33 +643,36 @@ export async function buildImage({ commit }) { if (isCache) { - await saveBuildLog({ line: `Building cache image started.`, buildId, applicationId }); + await saveBuildLog({ line: `Building cache image...`, buildId, applicationId }); } else { - await saveBuildLog({ line: `Building image started.`, buildId, applicationId }); - } - if (!debug) { - await saveBuildLog({ - line: `Debug logging is disabled. Enable it above if necessary!`, - buildId, - applicationId - }); + await saveBuildLog({ line: `Building production image...`, buildId, applicationId }); } const dockerFile = isCache ? `${dockerFileLocation}-cache` : `${dockerFileLocation}` const cache = `${applicationId}:${tag}${isCache ? '-cache' : ''}` - await executeDockerCmd({ debug, buildId, applicationId, dockerId, command: `docker build --progress plain -f ${workdir}/${dockerFile} -t ${cache} --build-arg SOURCE_COMMIT=${commit} ${workdir}` }) + let location = null + + const { dockerRegistry } = await prisma.application.findUnique({ where: { id: applicationId }, select: { dockerRegistry: true } }) + if (dockerRegistry) { + const { url, username, password } = dockerRegistry + location = await saveDockerRegistryCredentials({ url, username, password, workdir }) + } + + await executeCommand({ stream: true, debug, buildId, applicationId, dockerId, command: `docker ${location ? `--config ${location}` : ''} build --progress plain -f ${workdir}/${dockerFile} -t ${cache} --build-arg SOURCE_COMMIT=${commit} ${workdir}` }) const { status } = await prisma.build.findUnique({ where: { id: buildId } }) if (status === 'canceled') { - throw new Error('Deployment canceled.') - } - if (isCache) { - await saveBuildLog({ line: `Building cache image successful.`, buildId, applicationId }); - } else { - await saveBuildLog({ line: `Building image successful.`, buildId, applicationId }); + throw new Error('Canceled.') } } - +export function makeLabelForSimpleDockerfile({ applicationId, port, type }) { + return [ + 'coolify.managed=true', + `coolify.version=${version}`, + `coolify.applicationId=${applicationId}`, + `coolify.type=standalone-application` + ]; +} export function makeLabelForStandaloneApplication({ applicationId, fqdn, @@ -644,6 +701,7 @@ export function makeLabelForStandaloneApplication({ `coolify.version=${version}`, `coolify.applicationId=${applicationId}`, `coolify.type=standalone-application`, + `coolify.name=${name}`, `coolify.configuration=${base64Encode( JSON.stringify({ applicationId, diff --git a/apps/api/src/lib/buildPacks/compose.ts b/apps/api/src/lib/buildPacks/compose.ts index d57dba930..f180630dc 100644 --- a/apps/api/src/lib/buildPacks/compose.ts +++ b/apps/api/src/lib/buildPacks/compose.ts @@ -1,127 +1,126 @@ import { promises as fs } from 'fs'; -import { defaultComposeConfiguration, executeDockerCmd } from '../common'; -import { buildImage, saveBuildLog } from './common'; +import { defaultComposeConfiguration, executeCommand } from '../common'; +import { saveBuildLog } from './common'; import yaml from 'js-yaml'; export default async function (data) { - let { - applicationId, - debug, - buildId, - dockerId, - network, - volumes, - labels, - workdir, - baseDirectory, - secrets, - pullmergeRequestId, - port, - dockerComposeConfiguration - } = data - const fileYml = `${workdir}${baseDirectory}/docker-compose.yml`; - const fileYaml = `${workdir}${baseDirectory}/docker-compose.yaml`; - let dockerComposeRaw = null; - let isYml = false; - try { - dockerComposeRaw = await fs.readFile(`${fileYml}`, 'utf8') - isYml = true - } catch (error) { } - try { - dockerComposeRaw = await fs.readFile(`${fileYaml}`, 'utf8') - } catch (error) { } + let { + applicationId, + debug, + buildId, + dockerId, + network, + volumes, + labels, + workdir, + baseDirectory, + secrets, + pullmergeRequestId, + dockerComposeConfiguration, + dockerComposeFileLocation + } = data; + const fileYaml = `${workdir}${baseDirectory}${dockerComposeFileLocation}`; + const dockerComposeRaw = await fs.readFile(fileYaml, 'utf8'); + const dockerComposeYaml = yaml.load(dockerComposeRaw); + if (!dockerComposeYaml.services) { + throw 'No Services found in docker-compose file.'; + } + const envs = []; + if (secrets.length > 0) { + secrets.forEach((secret) => { + if (pullmergeRequestId) { + const isSecretFound = secrets.filter((s) => s.name === secret.name && s.isPRMRSecret); + if (isSecretFound.length > 0) { + envs.push(`${secret.name}=${isSecretFound[0].value}`); + } else { + envs.push(`${secret.name}=${secret.value}`); + } + } else { + if (!secret.isPRMRSecret) { + envs.push(`${secret.name}=${secret.value}`); + } + } + }); + } + await fs.writeFile(`${workdir}/.env`, envs.join('\n')); + let envFound = false; + try { + envFound = !!(await fs.stat(`${workdir}/.env`)); + } catch (error) { + // + } + const composeVolumes = []; + if (volumes.length > 0) { + for (const volume of volumes) { + let [v, path] = volume.split(':'); + composeVolumes[v] = { + name: v + }; + } + } - if (!dockerComposeRaw) { - throw ('docker-compose.yml or docker-compose.yaml are not found!'); - } - const dockerComposeYaml = yaml.load(dockerComposeRaw) - if (!dockerComposeYaml.services) { - throw 'No Services found in docker-compose file.' - } - const envs = []; - if (Object.entries(dockerComposeYaml.services).length === 1) { - envs.push(`PORT=${port}`) - } - if (secrets.length > 0) { - secrets.forEach((secret) => { - if (pullmergeRequestId) { - const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret) - if (isSecretFound.length > 0) { - envs.push(`${secret.name}=${isSecretFound[0].value}`); - } else { - envs.push(`${secret.name}=${secret.value}`); - } - } else { - if (!secret.isPRMRSecret) { - envs.push(`${secret.name}=${secret.value}`); - } - } - }); - } - await fs.writeFile(`${workdir}/.env`, envs.join('\n')); - let envFound = false; - try { - envFound = !!(await fs.stat(`${workdir}/.env`)); - } catch (error) { - // - } - const composeVolumes = []; - if (volumes.length > 0) { - for (const volume of volumes) { - let [v, path] = volume.split(':'); - composeVolumes[v] = { - name: v, - } - } - } - - let networks = {} - for (let [key, value] of Object.entries(dockerComposeYaml.services)) { - value['container_name'] = `${applicationId}-${key}` - value['env_file'] = envFound ? [`${workdir}/.env`] : [] - value['labels'] = labels - // TODO: If we support separated volume for each service, we need to add it here - if (value['volumes']?.length > 0) { - value['volumes'] = value['volumes'].map((volume) => { - let [v, path, permission] = volume.split(':'); - if (!path) { - path = v; - v = `${applicationId}${v.replace(/\//gi, '-').replace(/\./gi, '')}` - } else { - v = `${applicationId}${v.replace(/\//gi, '-').replace(/\./gi, '')}` - } - composeVolumes[v] = { - name: v - } - return `${v}:${path}${permission ? ':' + permission : ''}` - }) - } - if (volumes.length > 0) { - for (const volume of volumes) { - value['volumes'].push(volume) - } - } - if (dockerComposeConfiguration[key].port) { - value['expose'] = [dockerComposeConfiguration[key].port] - } - if (value['networks']?.length > 0) { - value['networks'].forEach((network) => { - networks[network] = { - name: network - } - }) - } - value['networks'] = [...value['networks'] || '', network] - dockerComposeYaml.services[key] = { ...dockerComposeYaml.services[key], restart: defaultComposeConfiguration(network).restart, deploy: defaultComposeConfiguration(network).deploy } - - } - if (Object.keys(composeVolumes).length > 0) { - dockerComposeYaml['volumes'] = { ...composeVolumes } - } - dockerComposeYaml['networks'] = Object.assign({ ...networks }, { [network]: { external: true } }) - await fs.writeFile(`${workdir}/docker-compose.${isYml ? 'yml' : 'yaml'}`, yaml.dump(dockerComposeYaml)); - await executeDockerCmd({ debug, buildId, applicationId, dockerId, command: `docker compose --project-directory ${workdir} pull` }) - await saveBuildLog({ line: 'Pulling images from Compose file.', buildId, applicationId }); - await executeDockerCmd({ debug, buildId, applicationId, dockerId, command: `docker compose --project-directory ${workdir} build --progress plain` }) - await saveBuildLog({ line: 'Building images from Compose file.', buildId, applicationId }); + let networks = {}; + for (let [key, value] of Object.entries(dockerComposeYaml.services)) { + value['container_name'] = `${applicationId}-${key}`; + value['env_file'] = envFound ? [`${workdir}/.env`] : []; + value['labels'] = labels; + // TODO: If we support separated volume for each service, we need to add it here + if (value['volumes']?.length > 0) { + value['volumes'] = value['volumes'].map((volume) => { + let [v, path, permission] = volume.split(':'); + if (!path) { + path = v; + v = `${applicationId}${v.replace(/\//gi, '-').replace(/\./gi, '')}`; + } else { + v = `${applicationId}${v.replace(/\//gi, '-').replace(/\./gi, '')}`; + } + composeVolumes[v] = { + name: v + }; + return `${v}:${path}${permission ? ':' + permission : ''}`; + }); + } + if (volumes.length > 0) { + for (const volume of volumes) { + value['volumes'].push(volume); + } + } + if (dockerComposeConfiguration[key].port) { + value['expose'] = [dockerComposeConfiguration[key].port]; + } + if (value['networks']?.length > 0) { + value['networks'].forEach((network) => { + networks[network] = { + name: network + }; + }); + } + value['networks'] = [...(value['networks'] || ''), network]; + dockerComposeYaml.services[key] = { + ...dockerComposeYaml.services[key], + restart: defaultComposeConfiguration(network).restart, + deploy: defaultComposeConfiguration(network).deploy + }; + } + if (Object.keys(composeVolumes).length > 0) { + dockerComposeYaml['volumes'] = { ...composeVolumes }; + } + dockerComposeYaml['networks'] = Object.assign({ ...networks }, { [network]: { external: true } }); + await fs.writeFile(fileYaml, yaml.dump(dockerComposeYaml)); + await executeCommand({ + debug, + buildId, + applicationId, + dockerId, + command: `docker compose --project-directory ${workdir} pull` + }); + await saveBuildLog({ line: 'Pulling images from Compose file...', buildId, applicationId }); + await executeCommand({ + debug, + buildId, + applicationId, + dockerId, + command: `docker compose --project-directory ${workdir} build --progress plain` + }); + await saveBuildLog({ line: 'Building images from Compose file...', buildId, applicationId }); } diff --git a/apps/api/src/lib/buildPacks/docker.ts b/apps/api/src/lib/buildPacks/docker.ts index 777dda157..902dd449b 100644 --- a/apps/api/src/lib/buildPacks/docker.ts +++ b/apps/api/src/lib/buildPacks/docker.ts @@ -20,7 +20,11 @@ export default async function (data) { .toString() .trim() .split('\n'); - Dockerfile.push(`LABEL coolify.buildId=${buildId}`); + Dockerfile.forEach((line, index) => { + if (line.startsWith('FROM')) { + Dockerfile.splice(index + 1, 0, `LABEL coolify.buildId=${buildId}`); + } + }); if (secrets.length > 0) { secrets.forEach((secret) => { if (secret.isBuildSecret) { @@ -28,11 +32,9 @@ export default async function (data) { (pullmergeRequestId && secret.isPRMRSecret) || (!pullmergeRequestId && !secret.isPRMRSecret) ) { - Dockerfile.unshift(`ARG ${secret.name}=${secret.value}`); - Dockerfile.forEach((line, index) => { if (line.startsWith('FROM')) { - Dockerfile.splice(index + 1, 0, `ARG ${secret.name}`); + Dockerfile.splice(index + 1, 0, `ARG ${secret.name}=${secret.value}`); } }); } diff --git a/apps/api/src/lib/buildPacks/heroku.ts b/apps/api/src/lib/buildPacks/heroku.ts index c108f3203..7ba131082 100644 --- a/apps/api/src/lib/buildPacks/heroku.ts +++ b/apps/api/src/lib/buildPacks/heroku.ts @@ -1,17 +1,16 @@ -import { executeDockerCmd, prisma } from "../common" +import { executeCommand } from "../common" import { saveBuildLog } from "./common"; export default async function (data: any): Promise { const { buildId, applicationId, tag, dockerId, debug, workdir, baseDirectory, baseImage } = data try { - await saveBuildLog({ line: `Building image started.`, buildId, applicationId }); - await executeDockerCmd({ + await saveBuildLog({ line: `Building production image...`, buildId, applicationId }); + await executeCommand({ buildId, debug, dockerId, command: `pack build -p ${workdir}${baseDirectory} ${applicationId}:${tag} --builder ${baseImage}` }) - await saveBuildLog({ line: `Building image successful.`, buildId, applicationId }); } catch (error) { throw error; } diff --git a/apps/api/src/lib/buildPacks/rust.ts b/apps/api/src/lib/buildPacks/rust.ts index 1af215869..931a4524a 100644 --- a/apps/api/src/lib/buildPacks/rust.ts +++ b/apps/api/src/lib/buildPacks/rust.ts @@ -1,6 +1,6 @@ import { promises as fs } from 'fs'; import TOML from '@iarna/toml'; -import { asyncExecShell } from '../common'; +import { executeCommand } from '../common'; import { buildCacheImageWithCargo, buildImage } from './common'; const createDockerfile = async (data, image, name): Promise => { @@ -28,7 +28,7 @@ const createDockerfile = async (data, image, name): Promise => { export default async function (data) { try { const { workdir, baseImage, baseBuildImage } = data; - const { stdout: cargoToml } = await asyncExecShell(`cat ${workdir}/Cargo.toml`); + const { stdout: cargoToml } = await executeCommand({ command: `cat ${workdir}/Cargo.toml` }); const parsedToml: any = TOML.parse(cargoToml); const name = parsedToml.package.name; await buildCacheImageWithCargo(data, baseBuildImage); diff --git a/apps/api/src/lib/buildPacks/static.ts b/apps/api/src/lib/buildPacks/static.ts index c727985ca..160098654 100644 --- a/apps/api/src/lib/buildPacks/static.ts +++ b/apps/api/src/lib/buildPacks/static.ts @@ -18,7 +18,11 @@ const createDockerfile = async (data, image): Promise => { const Dockerfile: Array = []; Dockerfile.push(`FROM ${image}`); - Dockerfile.push('WORKDIR /app'); + if (baseImage?.includes('httpd')) { + Dockerfile.push('WORKDIR /usr/local/apache2/htdocs/'); + } else { + Dockerfile.push('WORKDIR /app'); + } Dockerfile.push(`LABEL coolify.buildId=${buildId}`); if (secrets.length > 0) { secrets.forEach((secret) => { diff --git a/apps/api/src/lib/common.ts b/apps/api/src/lib/common.ts index 6c3bac117..70173a31e 100644 --- a/apps/api/src/lib/common.ts +++ b/apps/api/src/lib/common.ts @@ -8,18 +8,20 @@ import type { Config } from 'unique-names-generator'; import generator from 'generate-password'; import crypto from 'crypto'; import { promises as dns } from 'dns'; +import * as Sentry from '@sentry/node'; import { PrismaClient } from '@prisma/client'; import os from 'os'; import sshConfig from 'ssh-config'; import jsonwebtoken from 'jsonwebtoken'; import { checkContainer, removeContainer } from './docker'; import { day } from './dayjs'; -import { saveBuildLog } from './buildPacks/common'; +import { saveBuildLog, saveDockerRegistryCredentials } from './buildPacks/common'; import { scheduler } from './scheduler'; +import type { ExecaChildProcess } from 'execa'; -export const version = '3.11.13'; +export const version = '3.12.0'; export const isDev = process.env.NODE_ENV === 'development'; - +export const sentryDSN = 'https://409f09bcb7af47928d3e0f46b78987f3@o1082494.ingest.sentry.io/4504236622217216'; const algorithm = 'aes-256-ctr'; const customConfig: Config = { dictionaries: [adjectives, colors, animals], @@ -62,7 +64,6 @@ const otherTraefikEndpoint = isDev : 'http://coolify:3000/webhooks/traefik/other.json'; export const uniqueName = (): string => uniqueNamesGenerator(customConfig); -export const asyncExecShell = util.promisify(exec); export const asyncExecShellStream = async ({ debug, buildId, @@ -302,7 +303,7 @@ export async function isDomainConfigured({ export async function getContainerUsage(dockerId: string, container: string): Promise { try { - const { stdout } = await executeDockerCmd({ + const { stdout } = await executeCommand({ dockerId, command: `docker container stats ${container} --no-stream --no-trunc --format "{{json .}}"` }); @@ -507,36 +508,13 @@ export async function createRemoteEngineConfiguration(id: string) { remoteUser } = await prisma.destinationDocker.findFirst({ where: { id }, include: { sshKey: true } }); await fs.writeFile(sshKeyFile, decrypt(privateKey) + '\n', { encoding: 'utf8', mode: 400 }); - // Needed for remote docker compose - // const { stdout: numberOfSSHAgentsRunning } = await asyncExecShell( - // `ps ax | grep [s]sh-agent | grep coolify-ssh-agent.pid | grep -v grep | wc -l` - // ); - // if (numberOfSSHAgentsRunning !== '' && Number(numberOfSSHAgentsRunning.trim()) == 0) { - // try { - // await fs.stat(`/tmp/coolify-ssh-agent.pid`); - // await fs.rm(`/tmp/coolify-ssh-agent.pid`); - // } catch (error) { } - // await asyncExecShell(`eval $(ssh-agent -sa /tmp/coolify-ssh-agent.pid)`); - // } - // await asyncExecShell(`SSH_AUTH_SOCK=/tmp/coolify-ssh-agent.pid ssh-add -q ${sshKeyFile}`); - - // const { stdout: numberOfSSHTunnelsRunning } = await asyncExecShell( - // `ps ax | grep 'ssh -F /dev/null -o StrictHostKeyChecking no -fNL ${localPort}:localhost:${remotePort}' | grep -v grep | wc -l` - // ); - // if (numberOfSSHTunnelsRunning !== '' && Number(numberOfSSHTunnelsRunning.trim()) == 0) { - // try { - // await asyncExecShell( - // `SSH_AUTH_SOCK=/tmp/coolify-ssh-agent.pid ssh -F /dev/null -o "StrictHostKeyChecking no" -fNL ${localPort}:localhost:${remotePort} ${remoteUser}@${remoteIpAddress}` - // ); - // } catch (error) { } - // } const config = sshConfig.parse(''); const Host = `${remoteIpAddress}-remote` try { - await asyncExecShell(`ssh-keygen -R ${Host}`); - await asyncExecShell(`ssh-keygen -R ${remoteIpAddress}`); - await asyncExecShell(`ssh-keygen -R localhost:${localPort}`); + await executeCommand({ command: `ssh-keygen -R ${Host}` }); + await executeCommand({ command: `ssh-keygen -R ${remoteIpAddress}` }); + await executeCommand({ command: `ssh-keygen -R localhost:${localPort}` }); } catch (error) { } @@ -565,56 +543,130 @@ export async function createRemoteEngineConfiguration(id: string) { } return await fs.writeFile(`${homedir}/.ssh/config`, sshConfig.stringify(config)); } -export async function executeSSHCmd({ dockerId, command }) { - const { execaCommand } = await import('execa') - let { remoteEngine, remoteIpAddress } = await prisma.destinationDocker.findUnique({ where: { id: dockerId } }) - if (remoteEngine) { - await createRemoteEngineConfiguration(dockerId) - } - if (process.env.CODESANDBOX_HOST) { - if (command.startsWith('docker compose')) { - command = command.replace(/docker compose/gi, 'docker-compose') +export async function executeCommand({ command, dockerId = null, sshCommand = false, shell = false, stream = false, buildId, applicationId, debug }: { command: string, sshCommand?: boolean, shell?: boolean, stream?: boolean, dockerId?: string, buildId?: string, applicationId?: string, debug?: boolean }): Promise> { + const { execa, execaCommand } = await import('execa') + const { parse } = await import('shell-quote') + const parsedCommand = parse(command); + const dockerCommand = parsedCommand[0]; + const dockerArgs = parsedCommand.slice(1); + + if (dockerId) { + let { remoteEngine, remoteIpAddress, engine } = await prisma.destinationDocker.findUnique({ where: { id: dockerId } }) + if (remoteEngine) { + await createRemoteEngineConfiguration(dockerId); + engine = `ssh://${remoteIpAddress}-remote`; + } else { + engine = 'unix:///var/run/docker.sock'; } - } - return await execaCommand(`ssh ${remoteIpAddress}-remote ${command}`) -} -export async function executeDockerCmd({ debug, buildId, applicationId, dockerId, command }: { debug?: boolean, buildId?: string, applicationId?: string, dockerId: string, command: string }): Promise { - const { execaCommand } = await import('execa') - let { remoteEngine, remoteIpAddress, engine } = await prisma.destinationDocker.findUnique({ where: { id: dockerId } }) - if (remoteEngine) { - await createRemoteEngineConfiguration(dockerId); - engine = `ssh://${remoteIpAddress}-remote`; + if (process.env.CODESANDBOX_HOST) { + if (command.startsWith('docker compose')) { + command = command.replace(/docker compose/gi, 'docker-compose'); + } + } + if (sshCommand) { + if (shell) { + return execaCommand(`ssh ${remoteIpAddress}-remote ${command}`); + } + return await execa('ssh', [`${remoteIpAddress}-remote`, dockerCommand, ...dockerArgs]); + } + if (stream) { + return await new Promise(async (resolve, reject) => { + let subprocess = null; + if (shell) { + subprocess = execaCommand(command, { + env: { DOCKER_BUILDKIT: '1', DOCKER_HOST: engine } + }); + } else { + subprocess = execa(dockerCommand, dockerArgs, { + env: { DOCKER_BUILDKIT: '1', DOCKER_HOST: engine } + }); + } + const logs = []; + subprocess.stdout.on('data', async (data) => { + const stdout = data.toString(); + const array = stdout.split('\n'); + for (const line of array) { + if (line !== '\n' && line !== '') { + const log = { + line: `${line.replace('\n', '')}`, + buildId, + applicationId + } + logs.push(log); + if (debug) { + await saveBuildLog(log); + } + } + } + }); + subprocess.stderr.on('data', async (data) => { + const stderr = data.toString(); + const array = stderr.split('\n'); + for (const line of array) { + if (line !== '\n' && line !== '') { + const log = { + line: `${line.replace('\n', '')}`, + buildId, + applicationId + } + logs.push(log); + if (debug) { + await saveBuildLog(log); + } + } + } + }); + subprocess.on('exit', async (code) => { + if (code === 0) { + resolve(code); + } else { + if (!debug) { + for (const log of logs) { + await saveBuildLog(log); + } + } + reject(code); + } + }); + }) + } else { + if (shell) { + return await execaCommand(command, { + env: { DOCKER_BUILDKIT: '1', DOCKER_HOST: engine } + }); + } else { + return await execa(dockerCommand, dockerArgs, { + env: { DOCKER_BUILDKIT: '1', DOCKER_HOST: engine } + }); + } + } + } else { - engine = 'unix:///var/run/docker.sock'; - } - if (process.env.CODESANDBOX_HOST) { - if (command.startsWith('docker compose')) { - command = command.replace(/docker compose/gi, 'docker-compose'); + if (shell) { + return execaCommand(command, { shell: true }); } + return await execa(dockerCommand, dockerArgs); } - if (command.startsWith(`docker build`) || command.startsWith(`pack build`) || command.startsWith(`docker compose build`)) { - return await asyncExecShellStream({ debug, buildId, applicationId, command, engine }); - } - return await execaCommand(command, { env: { DOCKER_BUILDKIT: "1", DOCKER_HOST: engine }, shell: true }) } + export async function startTraefikProxy(id: string): Promise { const { engine, network, remoteEngine, remoteIpAddress } = await prisma.destinationDocker.findUnique({ where: { id } }) const { found } = await checkContainer({ dockerId: id, container: 'coolify-proxy', remove: true }); const { id: settingsId, ipv4, ipv6 } = await listSettings(); if (!found) { - const { stdout: coolifyNetwork } = await executeDockerCmd({ + const { stdout: coolifyNetwork } = await executeCommand({ dockerId: id, command: `docker network ls --filter 'name=coolify-infra' --no-trunc --format "{{json .}}"` }); if (!coolifyNetwork) { - await executeDockerCmd({ + await executeCommand({ dockerId: id, command: `docker network create --attachable coolify-infra` }); } - const { stdout: Config } = await executeDockerCmd({ + const { stdout: Config } = await executeCommand({ dockerId: id, command: `docker network inspect ${network} --format '{{json .IPAM.Config }}'` }); @@ -629,7 +681,7 @@ export async function startTraefikProxy(id: string): Promise { } traefikUrl = `${ip}/webhooks/traefik/remote/${id}`; } - await executeDockerCmd({ + await executeCommand({ dockerId: id, command: `docker run --restart always \ --add-host 'host.docker.internal:host-gateway' \ @@ -654,7 +706,6 @@ export async function startTraefikProxy(id: string): Promise { --certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web \ --log.level=error` }); - await prisma.setting.update({ where: { id: settingsId }, data: { proxyHash: null } }); await prisma.destinationDocker.update({ where: { id }, data: { isCoolifyProxyUsed: true } @@ -678,13 +729,13 @@ export async function startTraefikProxy(id: string): Promise { export async function configureNetworkTraefikProxy(destination: any): Promise { const { id } = destination; - const { stdout: networks } = await executeDockerCmd({ + const { stdout: networks } = await executeCommand({ dockerId: id, command: `docker ps -a --filter name=coolify-proxy --format '{{json .Networks}}'` }); const configuredNetworks = networks.replace(/"/g, '').replace('\n', '').split(','); if (!configuredNetworks.includes(destination.network)) { - await executeDockerCmd({ + await executeCommand({ dockerId: destination.id, command: `docker network connect ${destination.network} coolify-proxy` }); @@ -699,13 +750,12 @@ export async function stopTraefikProxy( where: { id }, data: { isCoolifyProxyUsed: false } }); - const { id: settingsId } = await prisma.setting.findFirst({}); - await prisma.setting.update({ where: { id: settingsId }, data: { proxyHash: null } }); try { if (found) { - await executeDockerCmd({ + await executeCommand({ dockerId: id, - command: `docker stop -t 0 coolify-proxy && docker rm coolify-proxy` + command: `docker stop -t 0 coolify-proxy && docker rm coolify-proxy`, + shell: true }); } } catch (error) { @@ -714,9 +764,7 @@ export async function stopTraefikProxy( } export async function listSettings(): Promise { - const settings = await prisma.setting.findFirst({}); - if (settings.proxyPassword) settings.proxyPassword = decrypt(settings.proxyPassword); - return settings; + return await prisma.setting.findUnique({ where: { id: '0' } }); } export function generateToken() { @@ -1075,6 +1123,7 @@ export async function makeLabelForStandaloneDatabase({ id, image, volume }) { 'coolify.managed=true', `coolify.version=${version}`, `coolify.type=standalone-database`, + `coolify.name=${database.name}`, `coolify.configuration=${base64Encode( JSON.stringify({ version, @@ -1092,7 +1141,7 @@ export const createDirectories = async ({ repository: string; buildId: string; }): Promise<{ workdir: string; repodir: string }> => { - repository = repository.replaceAll(' ', '') + if (repository) repository = repository.replaceAll(' ', '') const repodir = `/tmp/build-sources/${repository}/`; const workdir = `/tmp/build-sources/${repository}/${buildId}`; let workdirFound = false; @@ -1100,9 +1149,9 @@ export const createDirectories = async ({ workdirFound = !!(await fs.stat(workdir)); } catch (error) { } if (workdirFound) { - await asyncExecShell(`rm -fr ${workdir}`); + await executeCommand({ command: `rm -fr ${workdir}` }); } - await asyncExecShell(`mkdir -p ${workdir}`); + await executeCommand({ command: `mkdir -p ${workdir}` }); return { workdir, repodir @@ -1118,7 +1167,7 @@ export async function stopDatabaseContainer(database: any): Promise { } = database; if (destinationDockerId) { try { - const { stdout } = await executeDockerCmd({ + const { stdout } = await executeCommand({ dockerId, command: `docker inspect --format '{{json .State}}' ${id}` }); @@ -1146,9 +1195,10 @@ export async function stopTcpHttpProxy( const { found } = await checkContainer({ dockerId, container }); try { if (found) { - return await executeDockerCmd({ + return await executeCommand({ dockerId, - command: `docker stop -t 0 ${container} && docker rm ${container}` + command: `docker stop -t 0 ${container} && docker rm ${container}`, + shell: true }); } } catch (error) { @@ -1170,34 +1220,34 @@ export async function updatePasswordInDb(database, user, newPassword, isRoot) { } = database; if (destinationDockerId) { if (type === 'mysql') { - await executeDockerCmd({ + await executeCommand({ dockerId, command: `docker exec ${id} mysql -u ${rootUser} -p${rootUserPassword} -e \"ALTER USER '${user}'@'%' IDENTIFIED WITH caching_sha2_password BY '${newPassword}';\"` }); } else if (type === 'mariadb') { - await executeDockerCmd({ + await executeCommand({ dockerId, command: `docker exec ${id} mysql -u ${rootUser} -p${rootUserPassword} -e \"SET PASSWORD FOR '${user}'@'%' = PASSWORD('${newPassword}');\"` }); } else if (type === 'postgresql') { if (isRoot) { - await executeDockerCmd({ + await executeCommand({ dockerId, command: `docker exec ${id} psql postgresql://postgres:${rootUserPassword}@${id}:5432/${defaultDatabase} -c "ALTER role postgres WITH PASSWORD '${newPassword}'"` }); } else { - await executeDockerCmd({ + await executeCommand({ dockerId, command: `docker exec ${id} psql postgresql://${dbUser}:${dbUserPassword}@${id}:5432/${defaultDatabase} -c "ALTER role ${user} WITH PASSWORD '${newPassword}'"` }); } } else if (type === 'mongodb') { - await executeDockerCmd({ + await executeCommand({ dockerId, command: `docker exec ${id} mongo 'mongodb://${rootUser}:${rootUserPassword}@${id}:27017/admin?readPreference=primary&ssl=false' --eval "db.changeUserPassword('${user}','${newPassword}')"` }); } else if (type === 'redis') { - await executeDockerCmd({ + await executeCommand({ dockerId, command: `docker exec ${id} redis-cli -u redis://${dbUserPassword}@${id}:6379 --raw CONFIG SET requirepass ${newPassword}` }); @@ -1371,7 +1421,7 @@ export async function startTraefikTCPProxy( }); try { if (foundDependentContainer && !found) { - const { stdout: Config } = await executeDockerCmd({ + const { stdout: Config } = await executeCommand({ dockerId, command: `docker network inspect ${network} --format '{{json .IPAM.Config }}'` }); @@ -1418,16 +1468,17 @@ export async function startTraefikTCPProxy( } }; await fs.writeFile(`/tmp/docker-compose-${id}.yaml`, yaml.dump(tcpProxy)); - await executeDockerCmd({ + await executeCommand({ dockerId, command: `docker compose -f /tmp/docker-compose-${id}.yaml up -d` }); await fs.rm(`/tmp/docker-compose-${id}.yaml`); } if (!foundDependentContainer && found) { - await executeDockerCmd({ + await executeCommand({ dockerId, - command: `docker stop -t 0 ${container} && docker rm ${container}` + command: `docker stop -t 0 ${container} && docker rm ${container}`, + shell: true }); } } catch (error) { @@ -1487,12 +1538,17 @@ export function makeLabelForServices(type) { } export function errorHandler({ status = 500, - message = 'Unknown error.' + message = 'Unknown error.', + type = 'normal' }: { status: number; message: string | any; + type?: string | null; }) { if (message.message) message = message.message; + if (type === 'normal') { + Sentry.captureException(message); + } throw { status, message }; } export async function generateSshKeyPair(): Promise<{ publicKey: string; privateKey: string }> { @@ -1531,9 +1587,9 @@ export async function stopBuild(buildId, applicationId) { scheduler.workers.get('deployApplication').postMessage('cancel'); } await cleanupDB(buildId, applicationId); - return reject(new Error('Deployment canceled.')); + return reject(new Error('Canceled.')); } - const { stdout: buildContainers } = await executeDockerCmd({ + const { stdout: buildContainers } = await executeCommand({ dockerId, command: `docker container ls --filter "label=coolify.buildId=${buildId}" --format '{{json .}}'` }); @@ -1564,7 +1620,7 @@ async function cleanupDB(buildId: string, applicationId: string) { if (data?.status === 'queued' || data?.status === 'running') { await prisma.build.update({ where: { id: buildId }, data: { status: 'canceled' } }); } - await saveBuildLog({ line: 'Deployment canceled.', buildId, applicationId }); + await saveBuildLog({ line: 'Canceled.', buildId, applicationId }); } export function convertTolOldVolumeNames(type) { @@ -1576,36 +1632,60 @@ export function convertTolOldVolumeNames(type) { export async function cleanupDockerStorage(dockerId, lowDiskSpace, force) { // Cleanup old coolify images try { - let { stdout: images } = await executeDockerCmd({ + let { stdout: images } = await executeCommand({ dockerId, - command: `docker images coollabsio/coolify --filter before="coollabsio/coolify:${version}" -q | xargs -r` + command: `docker images coollabsio/coolify --filter before="coollabsio/coolify:${version}" -q | xargs -r`, + shell: true }); images = images.trim(); if (images) { - await executeDockerCmd({ dockerId, command: `docker rmi -f ${images}" -q | xargs -r` }); + await executeCommand({ dockerId, command: `docker rmi -f ${images}" -q | xargs -r`, shell: true }); } } catch (error) { } if (lowDiskSpace || force) { - // if (isDev) { - // if (!force) console.log(`[DEV MODE] Low disk space: ${lowDiskSpace}`); - // return; - // } + // Cleanup images that are not used try { - await executeDockerCmd({ + await executeCommand({ dockerId, command: `docker image prune -f` }); + } catch (error) { } + + const { numberOfDockerImagesKeptLocally } = await prisma.setting.findUnique({ where: { id: '0' } }) + const { stdout: images } = await executeCommand({ + dockerId, + command: `docker images | grep -v "" | grep -v REPOSITORY | awk '{print $1, $2}'`, + shell: true + }); + const imagesArray = images.trim().replaceAll(' ', ':').split('\n'); + const imagesSet = new Set(imagesArray.map((image) => image.split(':')[0])); + let deleteImage = [] + for (const image of imagesSet) { + let keepImage = [] + for (const image2 of imagesArray) { + if (image2.startsWith(image)) { + if (keepImage.length >= numberOfDockerImagesKeptLocally) { + deleteImage.push(image2) + } else { + keepImage.push(image2) + } + } + + } + } + for (const image of deleteImage) { + await executeCommand({ dockerId, command: `docker image rm -f ${image}` }); + } + + // Prune coolify managed containers + try { + await executeCommand({ dockerId, command: `docker container prune -f --filter "label=coolify.managed=true"` }); } catch (error) { } - try { - await executeDockerCmd({ dockerId, command: `docker image prune -f` }); - } catch (error) { } - try { - await executeDockerCmd({ dockerId, command: `docker image prune -a -f` }); - } catch (error) { } + // Cleanup build caches try { - await executeDockerCmd({ dockerId, command: `docker builder prune -a -f` }); + await executeCommand({ dockerId, command: `docker builder prune -a -f` }); } catch (error) { } } } @@ -1687,3 +1767,17 @@ export function decryptApplication(application: any) { return application; } } + +export async function pushToRegistry(application: any, workdir: string, tag: string, imageName: string, customTag: string) { + const location = `${workdir}/.docker` + const tagCommand = `docker tag ${application.id}:${tag} ${imageName}:${customTag}` + const pushCommand = `docker --config ${location} push ${imageName}:${customTag}` + await executeCommand({ + dockerId: application.destinationDockerId, + command: tagCommand + }) + await executeCommand({ + dockerId: application.destinationDockerId, + command: pushCommand + }) +} \ No newline at end of file diff --git a/apps/api/src/lib/docker.ts b/apps/api/src/lib/docker.ts index a4d36b7fc..0e2822d59 100644 --- a/apps/api/src/lib/docker.ts +++ b/apps/api/src/lib/docker.ts @@ -1,4 +1,4 @@ -import { executeDockerCmd } from './common'; +import { executeCommand } from './common'; export function formatLabelsOnDocker(data) { return data.trim().split('\n').map(a => JSON.parse(a)).map((container) => { @@ -16,7 +16,7 @@ export function formatLabelsOnDocker(data) { export async function checkContainer({ dockerId, container, remove = false }: { dockerId: string, container: string, remove?: boolean }): Promise<{ found: boolean, status?: { isExited: boolean, isRunning: boolean, isRestarting: boolean } }> { let containerFound = false; try { - const { stdout } = await executeDockerCmd({ + const { stdout } = await executeCommand({ dockerId, command: `docker inspect --format '{{json .State}}' ${container}` @@ -28,27 +28,26 @@ export async function checkContainer({ dockerId, container, remove = false }: { const isRestarting = status === 'restarting' const isExited = status === 'exited' if (status === 'created') { - await executeDockerCmd({ + await executeCommand({ dockerId, command: `docker rm ${container}` }); } if (remove && status === 'exited') { - await executeDockerCmd({ + await executeCommand({ dockerId, command: `docker rm ${container}` }); } - + return { found: containerFound, status: { isRunning, isRestarting, isExited - } }; } catch (err) { @@ -63,7 +62,7 @@ export async function checkContainer({ dockerId, container, remove = false }: { export async function isContainerExited(dockerId: string, containerName: string): Promise { let isExited = false; try { - const { stdout } = await executeDockerCmd({ dockerId, command: `docker inspect -f '{{.State.Status}}' ${containerName}` }) + const { stdout } = await executeCommand({ dockerId, command: `docker inspect -f '{{.State.Status}}' ${containerName}` }) if (stdout.trim() === 'exited') { isExited = true; } @@ -82,13 +81,13 @@ export async function removeContainer({ dockerId: string; }): Promise { try { - const { stdout } = await executeDockerCmd({ dockerId, command: `docker inspect --format '{{json .State}}' ${id}` }) + const { stdout } = await executeCommand({ dockerId, command: `docker inspect --format '{{json .State}}' ${id}` }) if (JSON.parse(stdout).Running) { - await executeDockerCmd({ dockerId, command: `docker stop -t 0 ${id}` }) - await executeDockerCmd({ dockerId, command: `docker rm ${id}` }) + await executeCommand({ dockerId, command: `docker stop -t 0 ${id}` }) + await executeCommand({ dockerId, command: `docker rm ${id}` }) } if (JSON.parse(stdout).Status === 'exited') { - await executeDockerCmd({ dockerId, command: `docker rm ${id}` }) + await executeCommand({ dockerId, command: `docker rm ${id}` }) } } catch (error) { throw error; diff --git a/apps/api/src/lib/importers/github.ts b/apps/api/src/lib/importers/github.ts index f42461939..1313b77c5 100644 --- a/apps/api/src/lib/importers/github.ts +++ b/apps/api/src/lib/importers/github.ts @@ -1,7 +1,7 @@ import jsonwebtoken from 'jsonwebtoken'; import { saveBuildLog } from '../buildPacks/common'; -import { asyncExecShell, decrypt, prisma } from '../common'; +import { decrypt, executeCommand, prisma } from '../common'; export default async function ({ applicationId, @@ -9,6 +9,7 @@ export default async function ({ githubAppId, repository, apiUrl, + gitCommitHash, htmlUrl, branch, buildId, @@ -20,6 +21,7 @@ export default async function ({ githubAppId: string; repository: string; apiUrl: string; + gitCommitHash?: string; htmlUrl: string; branch: string; buildId: string; @@ -28,16 +30,24 @@ export default async function ({ }): Promise { const { default: got } = await import('got') const url = htmlUrl.replace('https://', '').replace('http://', ''); - await saveBuildLog({ line: 'GitHub importer started.', buildId, applicationId }); if (forPublic) { await saveBuildLog({ - line: `Cloning ${repository}:${branch} branch.`, + line: `Cloning ${repository}:${branch}...`, buildId, applicationId }); - await asyncExecShell( - `git clone -q -b ${branch} https://${url}/${repository}.git ${workdir}/ && cd ${workdir} && git submodule update --init --recursive && git lfs pull && cd .. ` - ); + if (gitCommitHash) { + await saveBuildLog({ + line: `Checking out ${gitCommitHash} commit...`, + buildId, + applicationId + }); + } + await executeCommand({ + command: + `git clone -q -b ${branch} https://${url}/${repository}.git ${workdir}/ && cd ${workdir} && git checkout ${gitCommitHash || ""} && git submodule update --init --recursive && git lfs pull && cd .. `, + shell: true + }); } else { const body = await prisma.githubApp.findUnique({ where: { id: githubAppId } }); @@ -62,15 +72,23 @@ export default async function ({ }) .json(); await saveBuildLog({ - line: `Cloning ${repository}:${branch} branch.`, + line: `Cloning ${repository}:${branch}...`, buildId, applicationId }); - await asyncExecShell( - `git clone -q -b ${branch} https://x-access-token:${token}@${url}/${repository}.git --config core.sshCommand="ssh -p ${customPort}" ${workdir}/ && cd ${workdir} && git submodule update --init --recursive && git lfs pull && cd .. ` - ); + if (gitCommitHash) { + await saveBuildLog({ + line: `Checking out ${gitCommitHash} commit...`, + buildId, + applicationId + }); + } + await executeCommand({ + command: + `git clone -q -b ${branch} https://x-access-token:${token}@${url}/${repository}.git --config core.sshCommand="ssh -p ${customPort}" ${workdir}/ && cd ${workdir} && git checkout ${gitCommitHash || ""} && git submodule update --init --recursive && git lfs pull && cd .. `, + shell: true + }); } - const { stdout: commit } = await asyncExecShell(`cd ${workdir}/ && git rev-parse HEAD`); - + const { stdout: commit } = await executeCommand({ command: `cd ${workdir}/ && git rev-parse HEAD`, shell: true }); return commit.replace('\n', ''); } diff --git a/apps/api/src/lib/importers/gitlab.ts b/apps/api/src/lib/importers/gitlab.ts index 35fa3d8ce..eef9b62ef 100644 --- a/apps/api/src/lib/importers/gitlab.ts +++ b/apps/api/src/lib/importers/gitlab.ts @@ -1,11 +1,12 @@ import { saveBuildLog } from "../buildPacks/common"; -import { asyncExecShell } from "../common"; +import { executeCommand } from "../common"; export default async function ({ applicationId, workdir, repodir, htmlUrl, + gitCommitHash, repository, branch, buildId, @@ -20,34 +21,43 @@ export default async function ({ branch: string; buildId: string; repodir: string; + gitCommitHash: string; privateSshKey: string; customPort: number; forPublic: boolean; }): Promise { const url = htmlUrl.replace('https://', '').replace('http://', '').replace(/\/$/, ''); - await saveBuildLog({ line: 'GitLab importer started.', buildId, applicationId }); - if (!forPublic) { - await asyncExecShell(`echo '${privateSshKey}' > ${repodir}/id.rsa`); - await asyncExecShell(`chmod 600 ${repodir}/id.rsa`); + await executeCommand({ command: `echo '${privateSshKey}' > ${repodir}/id.rsa`, shell: true }); + await executeCommand({ command: `chmod 600 ${repodir}/id.rsa` }); } await saveBuildLog({ - line: `Cloning ${repository}:${branch} branch.`, + line: `Cloning ${repository}:${branch}...`, buildId, applicationId }); - + if (gitCommitHash) { + await saveBuildLog({ + line: `Checking out ${gitCommitHash} commit...`, + buildId, + applicationId + }); + } if (forPublic) { - await asyncExecShell( - `git clone -q -b ${branch} https://${url}/${repository}.git ${workdir}/ && cd ${workdir}/ && git submodule update --init --recursive && git lfs pull && cd .. ` + await executeCommand({ + command: + `git clone -q -b ${branch} https://${url}/${repository}.git ${workdir}/ && cd ${workdir}/ && git checkout ${gitCommitHash || ""} && git submodule update --init --recursive && git lfs pull && cd .. `, shell: true + } ); } else { - await asyncExecShell( - `git clone -q -b ${branch} git@${url}:${repository}.git --config core.sshCommand="ssh -p ${customPort} -q -i ${repodir}id.rsa -o StrictHostKeyChecking=no" ${workdir}/ && cd ${workdir}/ && git submodule update --init --recursive && git lfs pull && cd .. ` + await executeCommand({ + command: + `git clone -q -b ${branch} git@${url}:${repository}.git --config core.sshCommand="ssh -p ${customPort} -q -i ${repodir}id.rsa -o StrictHostKeyChecking=no" ${workdir}/ && cd ${workdir}/ && git checkout ${gitCommitHash || ""} && git submodule update --init --recursive && git lfs pull && cd .. `, shell: true + } ); } - - const { stdout: commit } = await asyncExecShell(`cd ${workdir}/ && git rev-parse HEAD`); + + const { stdout: commit } = await executeCommand({ command: `cd ${workdir}/ && git rev-parse HEAD`, shell: true }); return commit.replace('\n', ''); } diff --git a/apps/api/src/lib/services.ts b/apps/api/src/lib/services.ts index 7dfe6f0c8..b423cbe9c 100644 --- a/apps/api/src/lib/services.ts +++ b/apps/api/src/lib/services.ts @@ -1,12 +1,15 @@ -import { isDev } from "./common"; +import { isARM, isDev } from "./common"; import fs from 'fs/promises'; export async function getTemplates() { const templatePath = isDev ? './templates.json' : '/app/templates.json'; const open = await fs.open(templatePath, 'r'); - let data; try { - data = await open.readFile({ encoding: 'utf-8' }); - return JSON.parse(data); + let data = await open.readFile({ encoding: 'utf-8' }); + let jsonData = JSON.parse(data) + if (isARM(process.arch)) { + jsonData = jsonData.filter(d => d.arch !== 'amd64') + } + return jsonData; } catch (error) { return [] } finally { diff --git a/apps/api/src/lib/services/handlers.ts b/apps/api/src/lib/services/handlers.ts index ec015ea4f..866f9b17a 100644 --- a/apps/api/src/lib/services/handlers.ts +++ b/apps/api/src/lib/services/handlers.ts @@ -2,7 +2,7 @@ import type { FastifyReply, FastifyRequest } from 'fastify'; import fs from 'fs/promises'; import yaml from 'js-yaml'; import path from 'path'; -import { asyncSleep, ComposeFile, createDirectories, decrypt, defaultComposeConfiguration, errorHandler, executeDockerCmd, getServiceFromDB, isARM, makeLabelForServices, persistentVolumes, prisma, stopTcpHttpProxy } from '../common'; +import { asyncSleep, ComposeFile, createDirectories, decrypt, defaultComposeConfiguration, errorHandler, executeCommand, getServiceFromDB, isARM, makeLabelForServices, persistentVolumes, prisma, stopTcpHttpProxy } from '../common'; import { parseAndFindServiceTemplates } from '../../routes/api/v1/services/handlers'; import { ServiceStartStop } from '../../routes/api/v1/services/types'; @@ -15,14 +15,19 @@ export async function stopService(request: FastifyRequest) { const teamId = request.user.teamId; const { destinationDockerId } = await getServiceFromDB({ id, teamId }); if (destinationDockerId) { - await executeDockerCmd({ + const { stdout: containers } = await executeCommand({ dockerId: destinationDockerId, - command: `docker ps -a --filter 'label=com.docker.compose.project=${id}' --format {{.ID}}|xargs -r -n 1 docker stop -t 0` - }) - await executeDockerCmd({ - dockerId: destinationDockerId, - command: `docker ps -a --filter 'label=com.docker.compose.project=${id}' --format {{.ID}}|xargs -r -n 1 docker rm --force` + command: `docker ps -a --filter 'label=com.docker.compose.project=${id}' --format {{.ID}}` }) + if (containers) { + const containerArray = containers.split('\n'); + if (containerArray.length > 0) { + for (const container of containerArray) { + await executeCommand({ dockerId: destinationDockerId, command: `docker stop -t 0 ${container}` }) + await executeCommand({ dockerId: destinationDockerId, command: `docker rm --force ${container}` }) + } + } + } return {} } throw { status: 500, message: 'Could not stop containers.' } @@ -182,19 +187,36 @@ export async function startService(request: FastifyRequest, fa // Workaround: Stop old minio proxies if (service.type === 'minio') { try { - await executeDockerCmd({ + const { stdout: containers } = await executeCommand({ dockerId: destinationDocker.id, command: - `docker container ls -a --filter 'name=${id}-' --format {{.ID}}|xargs -r -n 1 docker container stop -t 0` + `docker container ls -a --filter 'name=${id}-' --format {{.ID}}` }); - + if (containers) { + const containerArray = containers.split('\n'); + if (containerArray.length > 0) { + for (const container of containerArray) { + await executeCommand({ dockerId: destinationDockerId, command: `docker stop -t 0 ${container}` }) + await executeCommand({ dockerId: destinationDockerId, command: `docker rm --force ${container}` }) + } + } + } } catch (error) { } try { - await executeDockerCmd({ + const { stdout: containers } = await executeCommand({ dockerId: destinationDocker.id, command: - `docker container ls -a --filter 'name=${id}-' --format {{.ID}}|xargs -r -n 1 docker container rm -f` + `docker container ls -a --filter 'name=${id}-' --format {{.ID}}` }); + if (containers) { + const containerArray = containers.split('\n'); + if (containerArray.length > 0) { + for (const container of containerArray) { + await executeCommand({ dockerId: destinationDockerId, command: `docker stop -t 0 ${container}` }) + await executeCommand({ dockerId: destinationDockerId, command: `docker rm --force ${container}` }) + } + } + } } catch (error) { } } return {} @@ -205,16 +227,16 @@ export async function startService(request: FastifyRequest, fa async function startServiceContainers(fastify, id, teamId, dockerId, composeFileDestination) { try { fastify.io.to(teamId).emit(`start-service`, { serviceId: id, state: 'Pulling images...' }) - await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} pull` }) + await executeCommand({ dockerId, command: `docker compose -f ${composeFileDestination} pull` }) } catch (error) { } fastify.io.to(teamId).emit(`start-service`, { serviceId: id, state: 'Building images...' }) - await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} build --no-cache` }) + await executeCommand({ dockerId, command: `docker compose -f ${composeFileDestination} build --no-cache` }) fastify.io.to(teamId).emit(`start-service`, { serviceId: id, state: 'Creating containers...' }) - await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} create` }) + await executeCommand({ dockerId, command: `docker compose -f ${composeFileDestination} create` }) fastify.io.to(teamId).emit(`start-service`, { serviceId: id, state: 'Starting containers...' }) - await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} start` }) + await executeCommand({ dockerId, command: `docker compose -f ${composeFileDestination} start` }) await asyncSleep(1000); - await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} up -d` }) + await executeCommand({ dockerId, command: `docker compose -f ${composeFileDestination} up -d` }) fastify.io.to(teamId).emit(`start-service`, { serviceId: id, state: 0 }) } export async function migrateAppwriteDB(request: FastifyRequest, reply: FastifyReply) { @@ -226,7 +248,7 @@ export async function migrateAppwriteDB(request: FastifyRequest, reply: destinationDocker, } = await getServiceFromDB({ id, teamId }); if (destinationDockerId) { - await executeDockerCmd({ + await executeCommand({ dockerId: destinationDocker.id, command: `docker exec ${id} migrate` }) diff --git a/apps/api/src/routes/api/v1/applications/handlers.ts b/apps/api/src/routes/api/v1/applications/handlers.ts index e166951a9..f43a5c536 100644 --- a/apps/api/src/routes/api/v1/applications/handlers.ts +++ b/apps/api/src/routes/api/v1/applications/handlers.ts @@ -7,12 +7,12 @@ import yaml from 'js-yaml'; import csv from 'csvtojson'; import { day } from '../../../../lib/dayjs'; -import { setDefaultBaseImage, setDefaultConfiguration } from '../../../../lib/buildPacks/common'; -import { checkDomainsIsValidInDNS, checkExposedPort, createDirectories, decrypt, defaultComposeConfiguration, encrypt, errorHandler, executeDockerCmd, generateSshKeyPair, getContainerUsage, getDomain, isDev, isDomainConfigured, listSettings, prisma, stopBuild, uniqueName } from '../../../../lib/common'; +import { saveDockerRegistryCredentials, setDefaultBaseImage, setDefaultConfiguration } from '../../../../lib/buildPacks/common'; +import { checkDomainsIsValidInDNS, checkExposedPort, createDirectories, decrypt, defaultComposeConfiguration, encrypt, errorHandler, executeCommand, generateSshKeyPair, getContainerUsage, getDomain, isDev, isDomainConfigured, listSettings, prisma, stopBuild, uniqueName } from '../../../../lib/common'; import { checkContainer, formatLabelsOnDocker, removeContainer } from '../../../../lib/docker'; import type { FastifyRequest } from 'fastify'; -import type { GetImages, CancelDeployment, CheckDNS, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, GetApplicationLogs, GetBuildIdLogs, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, DeployApplication, CheckDomain, StopPreviewApplication, RestartPreviewApplication, GetBuilds } from './types'; +import type { GetImages, CancelDeployment, CheckDNS, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, GetApplicationLogs, GetBuildIdLogs, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, DeployApplication, CheckDomain, StopPreviewApplication, RestartPreviewApplication, GetBuilds, RestartApplication } from './types'; import { OnlyId } from '../../../../types'; function filterObject(obj, callback) { @@ -78,7 +78,7 @@ export async function cleanupUnconfiguredApplications(request: FastifyRequest) { const application: any = await getApplicationFromDB(id, teamId); if (application?.destinationDockerId) { if (application.buildPack === 'compose') { - const { stdout: containers } = await executeDockerCmd({ + const { stdout: containers } = await executeCommand({ dockerId: application.destinationDocker.id, command: `docker ps -a --filter "label=coolify.applicationId=${id}" --format '{{json .}}'` @@ -241,7 +241,8 @@ export async function getApplicationFromDB(id: string, teamId: string) { secrets: true, persistentStorage: true, connectedDatabase: true, - previewApplication: true + previewApplication: true, + dockerRegistry: true } }); if (!application) { @@ -280,7 +281,7 @@ export async function getApplicationFromDBWebhook(projectId: number, branch: str } }); if (applications.length === 0) { - throw { status: 500, message: 'Application not configured.' } + throw { status: 500, message: 'Application not configured.', type: 'webhook' } } applications = applications.map((application: any) => { application = decryptApplication(application); @@ -302,8 +303,8 @@ export async function getApplicationFromDBWebhook(projectId: number, branch: str return applications; - } catch ({ status, message }) { - return errorHandler({ status, message }) + } catch ({ status, message, type }) { + return errorHandler({ status, message, type }) } } export async function saveApplication(request: FastifyRequest, reply: FastifyReply) { @@ -326,13 +327,16 @@ export async function saveApplication(request: FastifyRequest, dockerFileLocation, denoMainFile, denoOptions, + gitCommitHash, baseImage, baseBuildImage, deploymentType, baseDatabaseBranch, dockerComposeFile, dockerComposeFileLocation, - dockerComposeConfiguration + dockerComposeConfiguration, + simpleDockerfile, + dockerRegistryImageName } = request.body if (port) port = Number(port); if (exposePort) { @@ -350,6 +354,7 @@ export async function saveApplication(request: FastifyRequest, publishDirectory, baseDirectory, dockerFileLocation, + dockerComposeFileLocation, denoMainFile }); if (baseDatabaseBranch) { @@ -364,11 +369,14 @@ export async function saveApplication(request: FastifyRequest, pythonVariable, denoOptions, baseImage, + gitCommitHash, baseBuildImage, deploymentType, dockerComposeFile, dockerComposeFileLocation, dockerComposeConfiguration, + simpleDockerfile, + dockerRegistryImageName, ...defaultConfiguration, connectedDatabase: { update: { hostedDatabaseDBName: baseDatabaseBranch } } } @@ -382,6 +390,7 @@ export async function saveApplication(request: FastifyRequest, exposePort, pythonWSGI, pythonModule, + gitCommitHash, pythonVariable, denoOptions, baseImage, @@ -390,6 +399,8 @@ export async function saveApplication(request: FastifyRequest, dockerComposeFile, dockerComposeFileLocation, dockerComposeConfiguration, + simpleDockerfile, + dockerRegistryImageName, ...defaultConfiguration } }); @@ -438,16 +449,17 @@ export async function stopPreviewApplication(request: FastifyRequest, reply: FastifyReply) { +export async function restartApplication(request: FastifyRequest, reply: FastifyReply) { try { const { id } = request.params + const { imageId = null } = request.body const { teamId } = request.user let application: any = await getApplicationFromDB(id, teamId); if (application?.destinationDockerId) { const buildId = cuid(); const { id: dockerId, network } = application.destinationDocker; - const { secrets, pullmergeRequestId, port, repository, persistentStorage, id: applicationId, buildPack, exposePort } = application; - + const { dockerRegistry, secrets, pullmergeRequestId, port, repository, persistentStorage, id: applicationId, buildPack, exposePort } = application; + let location = null; const envs = [ `PORT=${port}` ]; @@ -470,28 +482,48 @@ export async function restartApplication(request: FastifyRequest, reply: const { workdir } = await createDirectories({ repository, buildId }); const labels = [] let image = null - const { stdout: container } = await executeDockerCmd({ dockerId, command: `docker container ls --filter 'label=com.docker.compose.service=${id}' --format '{{json .}}'` }) - const containersArray = container.trim().split('\n'); - for (const container of containersArray) { - const containerObj = formatLabelsOnDocker(container); - image = containerObj[0].Image - Object.keys(containerObj[0].Labels).forEach(function (key) { - if (key.startsWith('coolify')) { - labels.push(`${key}=${containerObj[0].Labels[key]}`) - } - }) + if (imageId) { + image = imageId + } else { + const { stdout: container } = await executeCommand({ dockerId, command: `docker container ls --filter 'label=com.docker.compose.service=${id}' --format '{{json .}}'` }) + const containersArray = container.trim().split('\n'); + for (const container of containersArray) { + const containerObj = formatLabelsOnDocker(container); + image = containerObj[0].Image + Object.keys(containerObj[0].Labels).forEach(function (key) { + if (key.startsWith('coolify')) { + labels.push(`${key}=${containerObj[0].Labels[key]}`) + } + }) + } } - let imageFound = false; + if (dockerRegistry) { + const { url, username, password } = dockerRegistry + location = await saveDockerRegistryCredentials({ url, username, password, workdir }) + } + + let imageFoundLocally = false; try { - await executeDockerCmd({ + await executeCommand({ dockerId, command: `docker image inspect ${image}` }) - imageFound = true; + imageFoundLocally = true; } catch (error) { // } - if (!imageFound) { + let imageFoundRemotely = false; + try { + await executeCommand({ + dockerId, + command: `docker ${location ? `--config ${location}` : ''} pull ${image}` + }) + imageFoundRemotely = true; + } catch (error) { + // + } + + if (!imageFoundLocally && !imageFoundRemotely) { throw { status: 500, message: 'Image not found, cannot restart application.' } } await fs.writeFile(`${workdir}/.env`, envs.join('\n')); @@ -537,9 +569,14 @@ export async function restartApplication(request: FastifyRequest, reply: volumes: Object.assign({}, ...composeVolumes) }; await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(composeFile)); - await executeDockerCmd({ dockerId, command: `docker stop -t 0 ${id}` }) - await executeDockerCmd({ dockerId, command: `docker rm ${id}` }) - await executeDockerCmd({ dockerId, command: `docker compose --project-directory ${workdir} up -d` }) + try { + await executeCommand({ dockerId, command: `docker stop -t 0 ${id}` }) + await executeCommand({ dockerId, command: `docker rm ${id}` }) + } catch (error) { + // + } + + await executeCommand({ dockerId, command: `docker compose --project-directory ${workdir} up -d` }) return reply.code(201).send(); } throw { status: 500, message: 'Application cannot be restarted.' } @@ -555,7 +592,7 @@ export async function stopApplication(request: FastifyRequest, reply: Fa if (application?.destinationDockerId) { const { id: dockerId } = application.destinationDocker; if (application.buildPack === 'compose') { - const { stdout: containers } = await executeDockerCmd({ + const { stdout: containers } = await executeCommand({ dockerId: application.destinationDocker.id, command: `docker ps -a --filter "label=coolify.applicationId=${id}" --format '{{json .}}'` @@ -590,7 +627,7 @@ export async function deleteApplication(request: FastifyRequest b.tag - a.tag); + + return { + imagesAvailables, + runningImage + } + } catch (error) { + return { + imagesAvailables, + } + } + + } catch ({ status, message }) { + + return errorHandler({ status, message }) + } +} export async function getUsageByContainer(request) { try { @@ -718,22 +796,37 @@ export async function deployApplication(request: FastifyRequest, reply: FastifyReply) { try { const { id } = request.params - const { gitSourceId, forPublic, type } = request.body + const { gitSourceId, forPublic, type, simpleDockerfile } = request.body if (forPublic) { const publicGit = await prisma.gitSource.findFirst({ where: { type, forPublic } }); await prisma.application.update({ where: { id }, data: { gitSource: { connect: { id: publicGit.id } } } }); - } else { + } + if (simpleDockerfile) { + await prisma.application.update({ + where: { id }, + data: { simpleDockerfile, settings: { update: { autodeploy: false } } } + }); + } + if (gitSourceId) { await prisma.application.update({ where: { id }, data: { gitSource: { connect: { id: gitSourceId } } } }); } + return reply.code(201).send() } catch ({ status, message }) { return errorHandler({ status, message }) @@ -819,7 +920,7 @@ export async function saveRepository(request, reply) { let { repository, branch, projectId, autodeploy, webhookToken, isPublicRepository = false } = request.body repository = repository.toLowerCase(); - + projectId = Number(projectId); if (webhookToken) { await prisma.application.update({ @@ -864,11 +965,11 @@ export async function getBuildPack(request) { const teamId = request.user?.teamId; const application: any = await getApplicationFromDB(id, teamId); return { - type: application.gitSource.type, + type: application.gitSource?.type || 'dockerRegistry', projectId: application.projectId, repository: application.repository, branch: application.branch, - apiUrl: application.gitSource.apiUrl, + apiUrl: application.gitSource?.apiUrl || null, isPublicRepository: application.settings.isPublicRepository } } catch ({ status, message }) { @@ -876,6 +977,16 @@ export async function getBuildPack(request) { } } +export async function saveRegistry(request, reply) { + try { + const { id } = request.params + const { registryId } = request.body + await prisma.application.update({ where: { id }, data: { dockerRegistry: { connect: { id: registryId } } } }); + return reply.code(201).send() + } catch ({ status, message }) { + return errorHandler({ status, message }) + } +} export async function saveBuildPack(request, reply) { try { const { id } = request.params @@ -1072,7 +1183,7 @@ export async function restartPreview(request: FastifyRequest) { try { const { id } = request.params const application = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } }); - const { stdout } = await executeDockerCmd({ dockerId: application.destinationDocker.id, command: `docker container ls --filter 'name=${id}-' --format "{{json .}}"` }) + const { stdout } = await executeCommand({ dockerId: application.destinationDocker.id, command: `docker container ls --filter 'name=${id}-' --format "{{json .}}"` }) if (stdout === '') { throw { status: 500, message: 'No previews found.' } } @@ -1257,7 +1368,7 @@ export async function getApplicationLogs(request: FastifyRequest ansi(l)).filter((a) => a); const stripLogsStderr = stderr.toString().split('\n').map((l) => ansi(l)).filter((a) => a); const logs = stripLogsStderr.concat(stripLogsStdout) @@ -1448,19 +1559,19 @@ export async function createdBranchDatabase(database: any, baseDatabaseBranch: s if (destinationDockerId) { if (type === 'postgresql') { const decryptedRootUserPassword = decrypt(rootUserPassword); - await executeDockerCmd({ + await executeCommand({ dockerId: destinationDockerId, command: `docker exec ${id} pg_dump -d "postgresql://postgres:${decryptedRootUserPassword}@${id}:5432/${baseDatabaseBranch}" --encoding=UTF8 --schema-only -f /tmp/${baseDatabaseBranch}.dump` }) - await executeDockerCmd({ + await executeCommand({ dockerId: destinationDockerId, command: `docker exec ${id} psql postgresql://postgres:${decryptedRootUserPassword}@${id}:5432 -c "CREATE DATABASE branch_${pullmergeRequestId}"` }) - await executeDockerCmd({ + await executeCommand({ dockerId: destinationDockerId, command: `docker exec ${id} psql -d "postgresql://postgres:${decryptedRootUserPassword}@${id}:5432/branch_${pullmergeRequestId}" -f /tmp/${baseDatabaseBranch}.dump` }) - await executeDockerCmd({ + await executeCommand({ dockerId: destinationDockerId, command: `docker exec ${id} psql postgresql://postgres:${decryptedRootUserPassword}@${id}:5432 -c "ALTER DATABASE branch_${pullmergeRequestId} OWNER TO ${dbUser}"` }) @@ -1479,12 +1590,12 @@ export async function removeBranchDatabase(database: any, pullmergeRequestId: st if (type === 'postgresql') { const decryptedRootUserPassword = decrypt(rootUserPassword); // Terminate all connections to the database - await executeDockerCmd({ + await executeCommand({ dockerId: destinationDockerId, command: `docker exec ${id} psql postgresql://postgres:${decryptedRootUserPassword}@${id}:5432 -c "SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = 'branch_${pullmergeRequestId}' AND pid <> pg_backend_pid();"` }) - await executeDockerCmd({ + await executeCommand({ dockerId: destinationDockerId, command: `docker exec ${id} psql postgresql://postgres:${decryptedRootUserPassword}@${id}:5432 -c "DROP DATABASE branch_${pullmergeRequestId}"` }) diff --git a/apps/api/src/routes/api/v1/applications/index.ts b/apps/api/src/routes/api/v1/applications/index.ts index 4ac98d895..9943dcee6 100644 --- a/apps/api/src/routes/api/v1/applications/index.ts +++ b/apps/api/src/routes/api/v1/applications/index.ts @@ -1,8 +1,8 @@ import { FastifyPluginAsync } from 'fastify'; import { OnlyId } from '../../../../types'; -import { cancelDeployment, checkDNS, checkDomain, checkRepository, cleanupUnconfiguredApplications, deleteApplication, deleteSecret, deleteStorage, deployApplication, getApplication, getApplicationLogs, getApplicationStatus, getBuildIdLogs, getBuildPack, getBuilds, getGitHubToken, getGitLabSSHKey, getImages, getPreviews, getPreviewStatus, getSecrets, getStorages, getUsage, getUsageByContainer, listApplications, loadPreviews, newApplication, restartApplication, restartPreview, saveApplication, saveApplicationSettings, saveApplicationSource, saveBuildPack, saveConnectedDatabase, saveDeployKey, saveDestination, saveGitLabSSHKey, saveRepository, saveSecret, saveStorage, stopApplication, stopPreviewApplication, updatePreviewSecret, updateSecret } from './handlers'; +import { cancelDeployment, checkDNS, checkDomain, checkRepository, cleanupUnconfiguredApplications, deleteApplication, deleteSecret, deleteStorage, deployApplication, getApplication, getApplicationLogs, getApplicationStatus, getBuildIdLogs, getBuildPack, getBuilds, getDockerImages, getGitHubToken, getGitLabSSHKey, getImages, getPreviews, getPreviewStatus, getSecrets, getStorages, getUsage, getUsageByContainer, listApplications, loadPreviews, newApplication, restartApplication, restartPreview, saveApplication, saveApplicationSettings, saveApplicationSource, saveBuildPack, saveConnectedDatabase, saveDeployKey, saveDestination, saveGitLabSSHKey, saveRegistry, saveRepository, saveSecret, saveStorage, stopApplication, stopPreviewApplication, updatePreviewSecret, updateSecret } from './handlers'; -import type { CancelDeployment, CheckDNS, CheckDomain, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, DeployApplication, GetApplicationLogs, GetBuildIdLogs, GetBuilds, GetImages, RestartPreviewApplication, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, StopPreviewApplication } from './types'; +import type { CancelDeployment, CheckDNS, CheckDomain, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, DeployApplication, GetApplicationLogs, GetBuildIdLogs, GetBuilds, GetImages, RestartApplication, RestartPreviewApplication, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, StopPreviewApplication } from './types'; const root: FastifyPluginAsync = async (fastify): Promise => { fastify.addHook('onRequest', async (request) => { @@ -21,7 +21,7 @@ const root: FastifyPluginAsync = async (fastify): Promise => { fastify.get('/:id/status', async (request) => await getApplicationStatus(request)); - fastify.post('/:id/restart', async (request, reply) => await restartApplication(request, reply)); + fastify.post('/:id/restart', async (request, reply) => await restartApplication(request, reply)); fastify.post('/:id/stop', async (request, reply) => await stopApplication(request, reply)); fastify.post('/:id/stop/preview', async (request, reply) => await stopPreviewApplication(request, reply)); @@ -45,7 +45,6 @@ const root: FastifyPluginAsync = async (fastify): Promise => { fastify.get('/:id/previews/:pullmergeRequestId/status', async (request) => await getPreviewStatus(request)); fastify.post('/:id/previews/:pullmergeRequestId/restart', async (request, reply) => await restartPreview(request, reply)); - // fastify.get('/:id/logs', async (request) => await getApplicationLogs(request)); fastify.get('/:id/logs/:containerId', async (request) => await getApplicationLogs(request)); fastify.get('/:id/logs/build', async (request) => await getBuilds(request)); fastify.get('/:id/logs/build/:buildId', async (request) => await getBuildIdLogs(request)); @@ -53,6 +52,8 @@ const root: FastifyPluginAsync = async (fastify): Promise => { fastify.get('/:id/usage', async (request) => await getUsage(request)) fastify.get('/:id/usage/:containerId', async (request) => await getUsageByContainer(request)) + fastify.get('/:id/images', async (request) => await getDockerImages(request)) + fastify.post('/:id/deploy', async (request) => await deployApplication(request)) fastify.post('/:id/cancel', async (request, reply) => await cancelDeployment(request, reply)); @@ -64,6 +65,8 @@ const root: FastifyPluginAsync = async (fastify): Promise => { fastify.get('/:id/configuration/buildpack', async (request) => await getBuildPack(request)); fastify.post('/:id/configuration/buildpack', async (request, reply) => await saveBuildPack(request, reply)); + fastify.post('/:id/configuration/registry', async (request, reply) => await saveRegistry(request, reply)); + fastify.post('/:id/configuration/database', async (request, reply) => await saveConnectedDatabase(request, reply)); fastify.get('/:id/configuration/sshkey', async (request) => await getGitLabSSHKey(request)); diff --git a/apps/api/src/routes/api/v1/applications/types.ts b/apps/api/src/routes/api/v1/applications/types.ts index b282b0647..ee8d19580 100644 --- a/apps/api/src/routes/api/v1/applications/types.ts +++ b/apps/api/src/routes/api/v1/applications/types.ts @@ -19,12 +19,15 @@ export interface SaveApplication extends OnlyId { denoMainFile: string, denoOptions: string, baseImage: string, + gitCommitHash: string, baseBuildImage: string, deploymentType: string, baseDatabaseBranch: string, dockerComposeFile: string, dockerComposeFileLocation: string, - dockerComposeConfiguration: string + dockerComposeConfiguration: string, + simpleDockerfile: string, + dockerRegistryImageName: string } } export interface SaveApplicationSettings extends OnlyId { @@ -55,7 +58,7 @@ export interface GetImages { Body: { buildPack: string, deploymentType: string } } export interface SaveApplicationSource extends OnlyId { - Body: { gitSourceId?: string | null, forPublic?: boolean, type?: string } + Body: { gitSourceId?: string | null, forPublic?: boolean, type?: string, simpleDockerfile?: string } } export interface CheckRepository extends OnlyId { Querystring: { repository: string, branch: string } @@ -140,4 +143,12 @@ export interface RestartPreviewApplication { id: string, pullmergeRequestId: string | null, } +} +export interface RestartApplication { + Params: { + id: string, + }, + Body: { + imageId: string | null, + } } \ No newline at end of file diff --git a/apps/api/src/routes/api/v1/base/index.ts b/apps/api/src/routes/api/v1/base/index.ts index 76854af8a..170a138f3 100644 --- a/apps/api/src/routes/api/v1/base/index.ts +++ b/apps/api/src/routes/api/v1/base/index.ts @@ -2,13 +2,20 @@ import { FastifyPluginAsync } from 'fastify'; import { errorHandler, listSettings, version } from '../../../../lib/common'; const root: FastifyPluginAsync = async (fastify): Promise => { + fastify.addHook('onRequest', async (request) => { + try { + await request.jwtVerify() + } catch(error) { + return + } + }); fastify.get('/', async (request) => { const teamId = request.user?.teamId; const settings = await listSettings() try { return { - ipv4: teamId ? settings.ipv4 : 'nope', - ipv6: teamId ? settings.ipv6 : 'nope', + ipv4: teamId ? settings.ipv4 : null, + ipv6: teamId ? settings.ipv6 : null, version, whiteLabeled: process.env.COOLIFY_WHITE_LABELED === 'true', whiteLabeledIcon: process.env.COOLIFY_WHITE_LABELED_ICON, diff --git a/apps/api/src/routes/api/v1/databases/handlers.ts b/apps/api/src/routes/api/v1/databases/handlers.ts index 48a29a900..20bf285b1 100644 --- a/apps/api/src/routes/api/v1/databases/handlers.ts +++ b/apps/api/src/routes/api/v1/databases/handlers.ts @@ -3,7 +3,7 @@ import type { FastifyRequest } from 'fastify'; import { FastifyReply } from 'fastify'; import yaml from 'js-yaml'; import fs from 'fs/promises'; -import { ComposeFile, createDirectories, decrypt, defaultComposeConfiguration, encrypt, errorHandler, executeDockerCmd, generateDatabaseConfiguration, generatePassword, getContainerUsage, getDatabaseImage, getDatabaseVersions, getFreePublicPort, listSettings, makeLabelForStandaloneDatabase, prisma, startTraefikTCPProxy, stopDatabaseContainer, stopTcpHttpProxy, supportedDatabaseTypesAndVersions, uniqueName, updatePasswordInDb } from '../../../../lib/common'; +import { ComposeFile, createDirectories, decrypt, defaultComposeConfiguration, encrypt, errorHandler, executeCommand, generateDatabaseConfiguration, generatePassword, getContainerUsage, getDatabaseImage, getDatabaseVersions, getFreePublicPort, listSettings, makeLabelForStandaloneDatabase, prisma, startTraefikTCPProxy, stopDatabaseContainer, stopTcpHttpProxy, supportedDatabaseTypesAndVersions, uniqueName, updatePasswordInDb } from '../../../../lib/common'; import { day } from '../../../../lib/dayjs'; import type { OnlyId } from '../../../../types'; @@ -89,7 +89,7 @@ export async function getDatabaseStatus(request: FastifyRequest) { const { destinationDockerId, destinationDocker } = database; if (destinationDockerId) { try { - const { stdout } = await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker inspect --format '{{json .State}}' ${id}` }) + const { stdout } = await executeCommand({ dockerId: destinationDocker.id, command: `docker inspect --format '{{json .State}}' ${id}` }) if (JSON.parse(stdout).Running) { isRunning = true; @@ -208,7 +208,7 @@ export async function saveDatabaseDestination(request: FastifyRequest) { }; const composeFileDestination = `${workdir}/docker-compose.yaml`; await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up -d` }) + await executeCommand({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up -d` }) if (isPublic) await startTraefikTCPProxy(destinationDocker, id, publicPort, privatePort); return {}; @@ -347,7 +347,7 @@ export async function getDatabaseLogs(request: FastifyRequest) // const found = await checkContainer({ dockerId, container: id }) // if (found) { const { default: ansi } = await import('strip-ansi') - const { stdout, stderr } = await executeDockerCmd({ dockerId, command: `docker logs --since ${since} --tail 5000 --timestamps ${id}` }) + const { stdout, stderr } = await executeCommand({ dockerId, command: `docker logs --since ${since} --tail 5000 --timestamps ${id}` }) const stripLogsStdout = stdout.toString().split('\n').map((l) => ansi(l)).filter((a) => a); const stripLogsStderr = stderr.toString().split('\n').map((l) => ansi(l)).filter((a) => a); const logs = stripLogsStderr.concat(stripLogsStdout) diff --git a/apps/api/src/routes/api/v1/destinations/handlers.ts b/apps/api/src/routes/api/v1/destinations/handlers.ts index 9006dc336..f5ccb2614 100644 --- a/apps/api/src/routes/api/v1/destinations/handlers.ts +++ b/apps/api/src/routes/api/v1/destinations/handlers.ts @@ -4,7 +4,7 @@ import sshConfig from 'ssh-config' import fs from 'fs/promises' import os from 'os'; -import { asyncExecShell, createRemoteEngineConfiguration, decrypt, errorHandler, executeDockerCmd, executeSSHCmd, listSettings, prisma, startTraefikProxy, stopTraefikProxy } from '../../../../lib/common'; +import { createRemoteEngineConfiguration, decrypt, errorHandler, executeCommand, listSettings, prisma, startTraefikProxy, stopTraefikProxy } from '../../../../lib/common'; import { checkContainer } from '../../../../lib/docker'; import type { OnlyId } from '../../../../types'; @@ -79,9 +79,9 @@ export async function newDestination(request: FastifyRequest, re let { name, network, engine, isCoolifyProxyUsed, remoteIpAddress, remoteUser, remotePort } = request.body if (id === 'new') { if (engine) { - const { stdout } = await asyncExecShell(`DOCKER_HOST=unix:///var/run/docker.sock docker network ls --filter 'name=^${network}$' --format '{{json .}}'`); + const { stdout } = await await executeCommand({ command: `docker network ls --filter 'name=^${network}$' --format '{{json .}}'` }); if (stdout === '') { - await asyncExecShell(`DOCKER_HOST=unix:///var/run/docker.sock docker network create --attachable ${network}`); + await await executeCommand({ command: `docker network create --attachable ${network}` }); } await prisma.destinationDocker.create({ data: { name, teams: { connect: { id: teamId } }, engine, network, isCoolifyProxyUsed } @@ -103,7 +103,7 @@ export async function newDestination(request: FastifyRequest, re return reply.code(201).send({ id: destination.id }); } else { const destination = await prisma.destinationDocker.create({ - data: { name, teams: { connect: { id: teamId } }, engine, network, isCoolifyProxyUsed, remoteEngine: true, remoteIpAddress, remoteUser, remotePort } + data: { name, teams: { connect: { id: teamId } }, engine, network, isCoolifyProxyUsed, remoteEngine: true, remoteIpAddress, remoteUser, remotePort: Number(remotePort) } }); return reply.code(201).send({ id: destination.id }) } @@ -122,13 +122,13 @@ export async function deleteDestination(request: FastifyRequest) { const { network, remoteVerified, engine, isCoolifyProxyUsed } = await prisma.destinationDocker.findUnique({ where: { id } }); if (isCoolifyProxyUsed) { if (engine || remoteVerified) { - const { stdout: found } = await executeDockerCmd({ + const { stdout: found } = await executeCommand({ dockerId: id, command: `docker ps -a --filter network=${network} --filter name=coolify-proxy --format '{{.}}'` }) if (found) { - await executeDockerCmd({ dockerId: id, command: `docker network disconnect ${network} coolify-proxy` }) - await executeDockerCmd({ dockerId: id, command: `docker network rm ${network}` }) + await executeCommand({ dockerId: id, command: `docker network disconnect ${network} coolify-proxy` }) + await executeCommand({ dockerId: id, command: `docker network rm ${network}` }) } } } @@ -203,22 +203,31 @@ export async function assignSSHKey(request: FastifyRequest) { } } export async function verifyRemoteDockerEngineFn(id: string) { - await createRemoteEngineConfiguration(id); const { remoteIpAddress, network, isCoolifyProxyUsed } = await prisma.destinationDocker.findFirst({ where: { id } }) - const host = `ssh://${remoteIpAddress}-remote` - const { stdout } = await asyncExecShell(`DOCKER_HOST=${host} docker network ls --filter 'name=${network}' --no-trunc --format "{{json .}}"`); - if (!stdout) { - await asyncExecShell(`DOCKER_HOST=${host} docker network create --attachable ${network}`); - } - const { stdout: coolifyNetwork } = await asyncExecShell(`DOCKER_HOST=${host} docker network ls --filter 'name=coolify-infra' --no-trunc --format "{{json .}}"`); - if (!coolifyNetwork) { - await asyncExecShell(`DOCKER_HOST=${host} docker network create --attachable coolify-infra`); - } - if (isCoolifyProxyUsed) await startTraefikProxy(id); + const daemonJson = `daemon-${id}.json` try { - const { stdout: daemonJson } = await executeSSHCmd({ dockerId: id, command: `cat /etc/docker/daemon.json` }); - let daemonJsonParsed = JSON.parse(daemonJson); - let isUpdated = false; + await executeCommand({ sshCommand: true, command: `docker network inspect ${network}`, dockerId: id }); + } catch (error) { + await executeCommand({ command: `docker network create --attachable ${network}`, dockerId: id }); + } + + try { + await executeCommand({ sshCommand: true, command: `docker network inspect coolify-infra`, dockerId: id }); + } catch (error) { + await executeCommand({ command: `docker network create --attachable coolify-infra`, dockerId: id }); + } + + if (isCoolifyProxyUsed) await startTraefikProxy(id); + let isUpdated = false; + let daemonJsonParsed = { + "live-restore": true, + "features": { + "buildkit": true + } + }; + try { + const { stdout: daemonJson } = await executeCommand({ sshCommand: true, dockerId: id, command: `cat /etc/docker/daemon.json` }); + daemonJsonParsed = JSON.parse(daemonJson); if (!daemonJsonParsed['live-restore'] || daemonJsonParsed['live-restore'] !== true) { isUpdated = true; daemonJsonParsed['live-restore'] = true @@ -230,21 +239,19 @@ export async function verifyRemoteDockerEngineFn(id: string) { buildkit: true } } - if (isUpdated) { - await executeSSHCmd({ dockerId: id, command: `echo '${JSON.stringify(daemonJsonParsed)}' > /etc/docker/daemon.json` }); - await executeSSHCmd({ dockerId: id, command: `systemctl restart docker` }); - } } catch (error) { - const daemonJsonParsed = { - "live-restore": true, - "features": { - "buildkit": true - } + isUpdated = true; + } + try { + if (isUpdated) { + await executeCommand({ shell: true, command: `echo '${JSON.stringify(daemonJsonParsed, null, 2)}' > /tmp/${daemonJson}` }) + await executeCommand({ dockerId: id, command: `scp /tmp/${daemonJson} ${remoteIpAddress}-remote:/etc/docker/daemon.json` }); + await executeCommand({ command: `rm /tmp/${daemonJson}` }) + await executeCommand({ sshCommand: true, dockerId: id, command: `systemctl restart docker` }); } - await executeSSHCmd({ dockerId: id, command: `echo '${JSON.stringify(daemonJsonParsed)}' > /etc/docker/daemon.json` }); - await executeSSHCmd({ dockerId: id, command: `systemctl restart docker` }); - } finally { await prisma.destinationDocker.update({ where: { id }, data: { remoteVerified: true } }) + } catch (error) { + throw new Error('Error while verifying remote docker engine') } } export async function verifyRemoteDockerEngine(request: FastifyRequest, reply: FastifyReply) { diff --git a/apps/api/src/routes/api/v1/handlers.ts b/apps/api/src/routes/api/v1/handlers.ts index ffd3ddb05..438d8a275 100644 --- a/apps/api/src/routes/api/v1/handlers.ts +++ b/apps/api/src/routes/api/v1/handlers.ts @@ -4,7 +4,6 @@ import bcrypt from "bcryptjs"; import fs from 'fs/promises'; import yaml from 'js-yaml'; import { - asyncExecShell, asyncSleep, cleanupDockerStorage, errorHandler, @@ -13,6 +12,8 @@ import { prisma, uniqueName, version, + sentryDSN, + executeCommand, } from "../../../lib/common"; import { scheduler } from "../../../lib/scheduler"; import type { FastifyReply, FastifyRequest } from "fastify"; @@ -24,6 +25,35 @@ export async function hashPassword(password: string): Promise { return bcrypt.hash(password, saltRounds); } +export async function backup(request: FastifyRequest) { + try { + const { backupData } = request.params; + let std = null; + const [id, backupType, type, zipped, storage] = backupData.split(':') + console.log(id, backupType, type, zipped, storage) + const database = await prisma.database.findUnique({ where: { id } }) + if (database) { + // await executeDockerCmd({ + // dockerId: database.destinationDockerId, + // command: `docker pull coollabsio/backup:latest`, + // }) + std = await executeCommand({ + dockerId: database.destinationDockerId, + command: `docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v coolify-local-backup:/app/backups -e CONTAINERS_TO_BACKUP="${backupData}" coollabsio/backup` + }) + + } + if (std.stdout) { + return std.stdout; + } + if (std.stderr) { + return std.stderr; + } + return 'nope'; + } catch ({ status, message }) { + return errorHandler({ status, message }); + } +} export async function cleanupManually(request: FastifyRequest) { try { const { serverId } = request.body; @@ -110,14 +140,10 @@ export async function update(request: FastifyRequest) { try { if (!isDev) { const { isAutoUpdateEnabled } = await prisma.setting.findFirst(); - await asyncExecShell(`docker pull coollabsio/coolify:${latestVersion}`); - await asyncExecShell(`env | grep COOLIFY > .env`); - await asyncExecShell( - `sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .env` - ); - await asyncExecShell( - `docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db coollabsio/coolify:${latestVersion} /bin/sh -c "env | grep COOLIFY > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify coolify-fluentbit && docker rm coolify coolify-fluentbit && docker compose pull && docker compose up -d --force-recreate"` - ); + await executeCommand({ command: `docker pull coollabsio/coolify:${latestVersion}` }); + await executeCommand({ shell: true, command: `env | grep COOLIFY > .env` }); + await executeCommand({ command: `sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .env` }); + await executeCommand({ shell: true, command: `docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db coollabsio/coolify:${latestVersion} /bin/sh -c "env | grep COOLIFY > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify coolify-fluentbit && docker rm coolify coolify-fluentbit && docker compose pull && docker compose up -d --force-recreate"` }); return {}; } else { await asyncSleep(2000); @@ -146,7 +172,7 @@ export async function restartCoolify(request: FastifyRequest) { const teamId = request.user.teamId; if (teamId === "0") { if (!isDev) { - asyncExecShell(`docker restart coolify`); + await executeCommand({ command: `docker restart coolify` }); return {}; } else { return {}; @@ -189,7 +215,7 @@ export async function showDashboard(request: FastifyRequest) { let foundUnconfiguredApplication = false; for (const application of applications) { - if (!application.buildPack || !application.destinationDockerId || !application.branch || (!application.settings?.isBot && !application?.fqdn) && application.buildPack !== "compose") { + if (((!application.buildPack || !application.branch) && !application.simpleDockerfile) || !application.destinationDockerId || (!application.settings?.isBot && !application?.fqdn) && application.buildPack !== "compose") { foundUnconfiguredApplication = true } } @@ -398,7 +424,8 @@ export async function getCurrentUser( } const pendingInvitations = await prisma.teamInvitation.findMany({ where: { uid: request.user.userId } }) return { - settings: await prisma.setting.findFirst(), + settings: await prisma.setting.findUnique({ where: { id: "0" } }), + sentryDSN, pendingInvitations, token, ...request.user, diff --git a/apps/api/src/routes/api/v1/index.ts b/apps/api/src/routes/api/v1/index.ts index 6ec94e479..243683a59 100644 --- a/apps/api/src/routes/api/v1/index.ts +++ b/apps/api/src/routes/api/v1/index.ts @@ -1,5 +1,5 @@ import { FastifyPluginAsync } from 'fastify'; -import { checkUpdate, login, showDashboard, update, resetQueue, getCurrentUser, cleanupManually, restartCoolify } from './handlers'; +import { checkUpdate, login, showDashboard, update, resetQueue, getCurrentUser, cleanupManually, restartCoolify, backup } from './handlers'; import { GetCurrentUser } from './types'; export interface Update { @@ -52,6 +52,10 @@ const root: FastifyPluginAsync = async (fastify): Promise => { fastify.post('/internal/cleanup', { onRequest: [fastify.authenticate] }, async (request) => await cleanupManually(request)); + + // fastify.get('/internal/backup/:backupData', { + // onRequest: [fastify.authenticate] + // }, async (request) => await backup(request)); }; export default root; diff --git a/apps/api/src/routes/api/v1/servers/handlers.ts b/apps/api/src/routes/api/v1/servers/handlers.ts index 874f5a9f7..9b163ab7b 100644 --- a/apps/api/src/routes/api/v1/servers/handlers.ts +++ b/apps/api/src/routes/api/v1/servers/handlers.ts @@ -1,5 +1,5 @@ import type { FastifyRequest } from 'fastify'; -import { errorHandler, executeDockerCmd, prisma, createRemoteEngineConfiguration, executeSSHCmd } from '../../../../lib/common'; +import { errorHandler, prisma, executeCommand } from '../../../../lib/common'; import os from 'node:os'; import osu from 'node-os-utils'; @@ -71,10 +71,10 @@ export async function showUsage(request: FastifyRequest) { let { remoteEngine } = request.query remoteEngine = remoteEngine === 'true' ? true : false if (remoteEngine) { - const { stdout: stats } = await executeSSHCmd({ dockerId: id, command: `vmstat -s` }) - const { stdout: disks } = await executeSSHCmd({ dockerId: id, command: `df -m / --output=size,used,pcent|grep -v 'Used'| xargs` }) - const { stdout: cpus } = await executeSSHCmd({ dockerId: id, command: `nproc --all` }) - const { stdout: cpuUsage } = await executeSSHCmd({ dockerId: id, command: `echo $[100-$(vmstat 1 2|tail -1|awk '{print $15}')]` }) + const { stdout: stats } = await executeCommand({ sshCommand: true, dockerId: id, command: `vmstat -s` }) + const { stdout: disks } = await executeCommand({ sshCommand: true, shell: true, dockerId: id, command: `df -m / --output=size,used,pcent|grep -v 'Used'| xargs` }) + const { stdout: cpus } = await executeCommand({ sshCommand: true, dockerId: id, command: `nproc --all` }) + const { stdout: cpuUsage } = await executeCommand({ sshCommand: true, shell: true, dockerId: id, command: `echo $[100-$(vmstat 1 2|tail -1|awk '{print $15}')]` }) const parsed: any = parseFromText(stats) return { usage: { diff --git a/apps/api/src/routes/api/v1/services/handlers.ts b/apps/api/src/routes/api/v1/services/handlers.ts index 27386c550..32333afc5 100644 --- a/apps/api/src/routes/api/v1/services/handlers.ts +++ b/apps/api/src/routes/api/v1/services/handlers.ts @@ -4,7 +4,7 @@ import yaml from 'js-yaml'; import bcrypt from 'bcryptjs'; import cuid from 'cuid'; -import { prisma, uniqueName, asyncExecShell, getServiceFromDB, getContainerUsage, isDomainConfigured, fixType, decrypt, encrypt, ComposeFile, getFreePublicPort, getDomain, errorHandler, generatePassword, isDev, stopTcpHttpProxy, executeDockerCmd, checkDomainsIsValidInDNS, checkExposedPort, listSettings, generateToken } from '../../../../lib/common'; +import { prisma, uniqueName, getServiceFromDB, getContainerUsage, isDomainConfigured, fixType, decrypt, encrypt, ComposeFile, getFreePublicPort, getDomain, errorHandler, generatePassword, isDev, stopTcpHttpProxy, checkDomainsIsValidInDNS, checkExposedPort, listSettings, generateToken, executeCommand } from '../../../../lib/common'; import { day } from '../../../../lib/dayjs'; import { checkContainer, } from '../../../../lib/docker'; import { removeService } from '../../../../lib/services/common'; @@ -48,14 +48,19 @@ export async function cleanupUnconfiguredServices(request: FastifyRequest) { for (const service of services) { if (!service.fqdn) { if (service.destinationDockerId) { - await executeDockerCmd({ + const { stdout: containers } = await executeCommand({ dockerId: service.destinationDockerId, - command: `docker ps -a --filter 'label=com.docker.compose.project=${service.id}' --format {{.ID}}|xargs -r -n 1 docker stop -t 0` - }) - await executeDockerCmd({ - dockerId: service.destinationDockerId, - command: `docker ps -a --filter 'label=com.docker.compose.project=${service.id}' --format {{.ID}}|xargs -r -n 1 docker rm --force` + command: `docker ps -a --filter 'label=com.docker.compose.project=${service.id}' --format {{.ID}}` }) + if (containers) { + const containerArray = containers.split('\n'); + if (containerArray.length > 0) { + for (const container of containerArray) { + await executeCommand({ dockerId: service.destinationDockerId, command: `docker stop -t 0 ${container}` }) + await executeCommand({ dockerId: service.destinationDockerId, command: `docker rm --force ${container}` }) + } + } + } } await removeService({ id: service.id }); } @@ -73,58 +78,61 @@ export async function getServiceStatus(request: FastifyRequest) { const { destinationDockerId, settings } = service; let payload = {} if (destinationDockerId) { - const { stdout: containers } = await executeDockerCmd({ + const { stdout: containers } = await executeCommand({ dockerId: service.destinationDocker.id, command: `docker ps -a --filter "label=com.docker.compose.project=${id}" --format '{{json .}}'` }); - const containersArray = containers.trim().split('\n'); - if (containersArray.length > 0 && containersArray[0] !== '') { - const templates = await getTemplates(); - let template = templates.find(t => t.type === service.type); - const templateStr = JSON.stringify(template) - if (templateStr) { - template = JSON.parse(templateStr.replaceAll('$$id', service.id)); - } - for (const container of containersArray) { - let isRunning = false; - let isExited = false; - let isRestarting = false; - let isExcluded = false; - const containerObj = JSON.parse(container); - const exclude = template?.services[containerObj.Names]?.exclude; - if (exclude) { + if (containers) { + const containersArray = containers.trim().split('\n'); + if (containersArray.length > 0 && containersArray[0] !== '') { + const templates = await getTemplates(); + let template = templates.find(t => t.type === service.type); + const templateStr = JSON.stringify(template) + if (templateStr) { + template = JSON.parse(templateStr.replaceAll('$$id', service.id)); + } + for (const container of containersArray) { + let isRunning = false; + let isExited = false; + let isRestarting = false; + let isExcluded = false; + const containerObj = JSON.parse(container); + const exclude = template?.services[containerObj.Names]?.exclude; + if (exclude) { + payload[containerObj.Names] = { + status: { + isExcluded: true, + isRunning: false, + isExited: false, + isRestarting: false, + } + } + continue; + } + + const status = containerObj.State + if (status === 'running') { + isRunning = true; + } + if (status === 'exited') { + isExited = true; + } + if (status === 'restarting') { + isRestarting = true; + } payload[containerObj.Names] = { status: { - isExcluded: true, - isRunning: false, - isExited: false, - isRestarting: false, + isExcluded, + isRunning, + isExited, + isRestarting } } - continue; - } - - const status = containerObj.State - if (status === 'running') { - isRunning = true; - } - if (status === 'exited') { - isExited = true; - } - if (status === 'restarting') { - isRestarting = true; - } - payload[containerObj.Names] = { - status: { - isExcluded, - isRunning, - isExited, - isRestarting - } } } } + } return payload } catch ({ status, message }) { @@ -239,13 +247,13 @@ export async function parseAndFindServiceTemplates(service: any, workdir?: strin if (value === '$$generate_fqdn') { strParsedTemplate = strParsedTemplate.replaceAll(regex, service.fqdn + '"' || '' + '"') } else if (value === '$$generate_fqdn_slash') { - strParsedTemplate = strParsedTemplate.replaceAll(regex, service.fqdn + '/' + '"') + strParsedTemplate = strParsedTemplate.replaceAll(regex, service.fqdn + '/' + '"') } else if (value === '$$generate_domain') { strParsedTemplate = strParsedTemplate.replaceAll(regex, getDomain(service.fqdn) + '"') } else if (service.destinationDocker?.network && value === '$$generate_network') { - strParsedTemplate = strParsedTemplate.replaceAll(regex, service.destinationDocker.network + '"') + strParsedTemplate = strParsedTemplate.replaceAll(regex, service.destinationDocker.network + '"') } else { - strParsedTemplate = strParsedTemplate.replaceAll(regex, value + '"') + strParsedTemplate = strParsedTemplate.replaceAll(regex, value + '"') } } } @@ -443,7 +451,7 @@ export async function getServiceLogs(request: FastifyRequest) { if (destinationDockerId) { try { const { default: ansi } = await import('strip-ansi') - const { stdout, stderr } = await executeDockerCmd({ dockerId, command: `docker logs --since ${since} --tail 5000 --timestamps ${containerId}` }) + const { stdout, stderr } = await executeCommand({ dockerId, command: `docker logs --since ${since} --tail 5000 --timestamps ${containerId}` }) const stripLogsStdout = stdout.toString().split('\n').map((l) => ansi(l)).filter((a) => a); const stripLogsStderr = stderr.toString().split('\n').map((l) => ansi(l)).filter((a) => a); const logs = stripLogsStderr.concat(stripLogsStdout) @@ -749,7 +757,7 @@ export async function activatePlausibleUsers(request: FastifyRequest, re if (destinationDockerId) { const databaseUrl = serviceSecret.find((secret) => secret.name === 'DATABASE_URL'); if (databaseUrl) { - await executeDockerCmd({ + await executeCommand({ dockerId: destinationDocker.id, command: `docker exec ${id}-postgresql psql -H ${databaseUrl.value} -c "UPDATE users SET email_verified = true;"` }) @@ -770,9 +778,10 @@ export async function cleanupPlausibleLogs(request: FastifyRequest, repl destinationDocker, } = await getServiceFromDB({ id, teamId }); if (destinationDockerId) { - await executeDockerCmd({ + await executeCommand({ dockerId: destinationDocker.id, - command: `docker exec ${id}-clickhouse /usr/bin/clickhouse-client -q \\"SELECT name FROM system.tables WHERE name LIKE '%log%';\\"| xargs -I{} /usr/bin/clickhouse-client -q \"TRUNCATE TABLE system.{};\"` + command: `docker exec ${id}-clickhouse /usr/bin/clickhouse-client -q \\"SELECT name FROM system.tables WHERE name LIKE '%log%';\\"| xargs -I{} /usr/bin/clickhouse-client -q \"TRUNCATE TABLE system.{};\"`, + shell: true }) return await reply.code(201).send() } @@ -812,36 +821,42 @@ export async function activateWordpressFtp(request: FastifyRequest ${hostkeyDir}/${id}.ed25519`); + await executeCommand({ command: `echo "${decrypt(ftpHostKey)}" > ${hostkeyDir}/${id}.ed25519`, shell: true }); } if (!ftpHostKeyPrivate) { - await asyncExecShell(`ssh-keygen -t rsa -b 4096 -N "" -f ${hostkeyDir}/${id}.rsa`); - const { stdout: ftpHostKeyPrivate } = await asyncExecShell(`cat ${hostkeyDir}/${id}.rsa`); + await executeCommand({ command: `ssh-keygen -t rsa -b 4096 -N "" -f ${hostkeyDir}/${id}.rsa` }); + const { stdout: ftpHostKeyPrivate } = await executeCommand({ command: `cat ${hostkeyDir}/${id}.rsa` }); await prisma.wordpress.update({ where: { serviceId: id }, data: { ftpHostKeyPrivate: encrypt(ftpHostKeyPrivate) } }); } else { - await asyncExecShell(`echo "${decrypt(ftpHostKeyPrivate)}" > ${hostkeyDir}/${id}.rsa`); + await executeCommand({ command: `echo "${decrypt(ftpHostKeyPrivate)}" > ${hostkeyDir}/${id}.rsa`, shell: true }); } await prisma.wordpress.update({ @@ -856,9 +871,10 @@ export async function activateWordpressFtp(request: FastifyRequest { + if (registry.password) { + registry.password = decrypt(registry.password) + } + return registry + }) const unencryptedKeys = [] if (sshKeys.length > 0) { for (const key of sshKeys) { @@ -27,7 +34,8 @@ export async function listAllSettings(request: FastifyRequest) { return { settings, certificates: cns, - sshKeys: unencryptedKeys + sshKeys: unencryptedKeys, + registries } } catch ({ status, message }) { return errorHandler({ status, message }) @@ -35,7 +43,10 @@ export async function listAllSettings(request: FastifyRequest) { } export async function saveSettings(request: FastifyRequest, reply: FastifyReply) { try { - const { + let { + previewSeparator, + numberOfDockerImagesKeptLocally, + doNotTrack, fqdn, isAPIDebuggingEnabled, isRegistrationEnabled, @@ -47,10 +58,29 @@ export async function saveSettings(request: FastifyRequest, reply: DNSServers, proxyDefaultRedirect } = request.body - const { id } = await listSettings(); + const { id, previewSeparator: SetPreviewSeparator } = await listSettings(); + if (numberOfDockerImagesKeptLocally) { + numberOfDockerImagesKeptLocally = Number(numberOfDockerImagesKeptLocally) + } + if (previewSeparator == '') { + previewSeparator = '.' + } + if (SetPreviewSeparator != previewSeparator) { + const applications = await prisma.application.findMany({ where: { previewApplication: { some: { id: { not: undefined } } } }, include: { previewApplication: true } }) + for (const application of applications) { + for (const preview of application.previewApplication) { + const { protocol } = new URL(preview.customDomain) + const { pullmergeRequestId } = preview + const { fqdn } = application + const newPreviewDomain = `${protocol}//${pullmergeRequestId}${previewSeparator}${getDomain(fqdn)}` + await prisma.previewApplication.update({ where: { id: preview.id }, data: { customDomain: newPreviewDomain } }) + } + } + } + await prisma.setting.update({ where: { id }, - data: { isRegistrationEnabled, dualCerts, isAutoUpdateEnabled, isDNSCheckEnabled, DNSServers, isAPIDebuggingEnabled, } + data: { previewSeparator, numberOfDockerImagesKeptLocally, doNotTrack, isRegistrationEnabled, dualCerts, isAutoUpdateEnabled, isDNSCheckEnabled, DNSServers, isAPIDebuggingEnabled } }); if (fqdn) { await prisma.setting.update({ where: { id }, data: { fqdn } }); @@ -59,6 +89,14 @@ export async function saveSettings(request: FastifyRequest, reply: if (minPort && maxPort) { await prisma.setting.update({ where: { id }, data: { minPort, maxPort } }); } + if (doNotTrack === false) { + // Sentry.init({ + // dsn: sentryDSN, + // environment: isDev ? 'development' : 'production', + // release: version + // }); + // console.log('Sentry initialized') + } return reply.code(201).send() } catch ({ status, message }) { return errorHandler({ status, message }) @@ -91,7 +129,7 @@ export async function checkDomain(request: FastifyRequest) { if (fqdn) fqdn = fqdn.toLowerCase(); const found = await isDomainConfigured({ id, fqdn }); if (found) { - throw "Domain already configured"; + throw { message: "Domain already configured" }; } if (isDNSCheckEnabled && !forceSave && !isDev) { const hostname = request.hostname.split(':')[0] @@ -131,8 +169,9 @@ export async function saveSSHKey(request: FastifyRequest, reply: Fas } export async function deleteSSHKey(request: FastifyRequest, reply: FastifyReply) { try { + const teamId = request.user.teamId; const { id } = request.body; - await prisma.sshKey.delete({ where: { id } }) + await prisma.sshKey.deleteMany({ where: { id, teamId } }) return reply.code(201).send() } catch ({ status, message }) { return errorHandler({ status, message }) @@ -141,9 +180,54 @@ export async function deleteSSHKey(request: FastifyRequest, reply: export async function deleteCertificates(request: FastifyRequest, reply: FastifyReply) { try { + const teamId = request.user.teamId; const { id } = request.body; - await asyncExecShell(`docker exec coolify-proxy sh -c 'rm -f /etc/traefik/acme/custom/${id}-key.pem /etc/traefik/acme/custom/${id}-cert.pem'`) - await prisma.certificate.delete({ where: { id } }) + await executeCommand({ command: `docker exec coolify-proxy sh -c 'rm -f /etc/traefik/acme/custom/${id}-key.pem /etc/traefik/acme/custom/${id}-cert.pem'`, shell: true }) + await prisma.certificate.deleteMany({ where: { id, teamId } }) + return reply.code(201).send() + } catch ({ status, message }) { + return errorHandler({ status, message }) + } +} + +export async function setDockerRegistry(request: FastifyRequest, reply: FastifyReply) { + try { + const teamId = request.user.teamId; + const { id, username, password } = request.body; + + let encryptedPassword = '' + if (password) encryptedPassword = encrypt(password) + + if (teamId === '0') { + await prisma.dockerRegistry.update({ where: { id }, data: { username, password: encryptedPassword } }) + } else { + await prisma.dockerRegistry.updateMany({ where: { id, teamId }, data: { username, password: encryptedPassword } }) + } + return reply.code(201).send() + } catch ({ status, message }) { + return errorHandler({ status, message }) + } +} +export async function addDockerRegistry(request: FastifyRequest, reply: FastifyReply) { + try { + const teamId = request.user.teamId; + const { name, url, username, password } = request.body; + + let encryptedPassword = '' + if (password) encryptedPassword = encrypt(password) + await prisma.dockerRegistry.create({ data: { name, url, username, password: encryptedPassword, team: { connect: { id: teamId } } } }) + + return reply.code(201).send() + } catch ({ status, message }) { + return errorHandler({ status, message }) + } +} +export async function deleteDockerRegistry(request: FastifyRequest, reply: FastifyReply) { + try { + const teamId = request.user.teamId; + const { id } = request.body; + await prisma.application.updateMany({ where: { dockerRegistryId: id }, data: { dockerRegistryId: null } }) + await prisma.dockerRegistry.deleteMany({ where: { id, teamId } }) return reply.code(201).send() } catch ({ status, message }) { return errorHandler({ status, message }) diff --git a/apps/api/src/routes/api/v1/settings/index.ts b/apps/api/src/routes/api/v1/settings/index.ts index 45e418b34..2ba8b178a 100644 --- a/apps/api/src/routes/api/v1/settings/index.ts +++ b/apps/api/src/routes/api/v1/settings/index.ts @@ -2,8 +2,8 @@ import { FastifyPluginAsync } from 'fastify'; import { X509Certificate } from 'node:crypto'; import { encrypt, errorHandler, prisma } from '../../../../lib/common'; -import { checkDNS, checkDomain, deleteCertificates, deleteDomain, deleteSSHKey, listAllSettings, saveSettings, saveSSHKey } from './handlers'; -import { CheckDNS, CheckDomain, DeleteDomain, OnlyIdInBody, SaveSettings, SaveSSHKey } from './types'; +import { addDockerRegistry, checkDNS, checkDomain, deleteCertificates, deleteDockerRegistry, deleteDomain, deleteSSHKey, listAllSettings, saveSettings, saveSSHKey, setDockerRegistry } from './handlers'; +import { AddDefaultRegistry, CheckDNS, CheckDomain, DeleteDomain, OnlyIdInBody, SaveSettings, SaveSSHKey, SetDefaultRegistry } from './types'; const root: FastifyPluginAsync = async (fastify): Promise => { @@ -20,6 +20,10 @@ const root: FastifyPluginAsync = async (fastify): Promise => { fastify.post('/sshKey', async (request, reply) => await saveSSHKey(request, reply)); fastify.delete('/sshKey', async (request, reply) => await deleteSSHKey(request, reply)); + fastify.post('/registry', async (request, reply) => await setDockerRegistry(request, reply)); + fastify.post('/registry/new', async (request, reply) => await addDockerRegistry(request, reply)); + fastify.delete('/registry', async (request, reply) => await deleteDockerRegistry(request, reply)); + fastify.post('/upload', async (request) => { try { const teamId = request.user.teamId; @@ -53,7 +57,6 @@ const root: FastifyPluginAsync = async (fastify): Promise => { }); fastify.delete('/certificate', async (request, reply) => await deleteCertificates(request, reply)) - // fastify.get('/certificates', async (request) => await getCertificates(request)) }; export default root; diff --git a/apps/api/src/routes/api/v1/settings/types.ts b/apps/api/src/routes/api/v1/settings/types.ts index d68a7bdd3..418f2661f 100644 --- a/apps/api/src/routes/api/v1/settings/types.ts +++ b/apps/api/src/routes/api/v1/settings/types.ts @@ -2,6 +2,9 @@ import { OnlyId } from "../../../../types" export interface SaveSettings { Body: { + previewSeparator: string, + numberOfDockerImagesKeptLocally: number, + doNotTrack: boolean, fqdn: string, isAPIDebuggingEnabled: boolean, isRegistrationEnabled: boolean, @@ -21,30 +24,46 @@ export interface DeleteDomain { } export interface CheckDomain extends OnlyId { Body: { - fqdn: string, - forceSave: boolean, - dualCerts: boolean, - isDNSCheckEnabled: boolean, + fqdn: string, + forceSave: boolean, + dualCerts: boolean, + isDNSCheckEnabled: boolean, } } export interface CheckDNS { Params: { - domain: string, + domain: string, } } export interface SaveSSHKey { Body: { - privateKey: string, + privateKey: string, name: string } } export interface DeleteSSHKey { Body: { - id: string + id: string } } export interface OnlyIdInBody { Body: { id: string - } + } +} + +export interface SetDefaultRegistry { + Body: { + id: string + username: string + password: string + } +} +export interface AddDefaultRegistry { + Body: { + url: string + name: string + username: string + password: string + } } \ No newline at end of file diff --git a/apps/api/src/routes/api/v1/sources/handlers.ts b/apps/api/src/routes/api/v1/sources/handlers.ts index b67ab7eb4..e33652571 100644 --- a/apps/api/src/routes/api/v1/sources/handlers.ts +++ b/apps/api/src/routes/api/v1/sources/handlers.ts @@ -37,9 +37,7 @@ export async function getSource(request: FastifyRequest) { try { const { id } = request.params const { teamId } = request.user - const settings = await prisma.setting.findFirst({}); - if (settings.proxyPassword) settings.proxyPassword = decrypt(settings.proxyPassword); if (id === 'new') { return { diff --git a/apps/api/src/routes/webhooks/github/handlers.ts b/apps/api/src/routes/webhooks/github/handlers.ts index d9345c04d..5e0f2bbba 100644 --- a/apps/api/src/routes/webhooks/github/handlers.ts +++ b/apps/api/src/routes/webhooks/github/handlers.ts @@ -71,7 +71,7 @@ export async function gitHubEvents(request: FastifyRequest): Promi const githubEvent = request.headers['x-github-event']?.toString().toLowerCase(); const githubSignature = request.headers['x-hub-signature-256']?.toString().toLowerCase(); if (!allowedGithubEvents.includes(githubEvent)) { - throw { status: 500, message: 'Event not allowed.' } + throw { status: 500, message: 'Event not allowed.', type: 'webhook' } } if (githubEvent === 'ping') { return { pong: 'cool' } @@ -89,9 +89,10 @@ export async function gitHubEvents(request: FastifyRequest): Promi branch = body.pull_request.base.ref } if (!projectId || !branch) { - throw { status: 500, message: 'Cannot parse projectId or branch from the webhook?!' } + throw { status: 500, message: 'Cannot parse projectId or branch from the webhook?!', type: 'webhook' } } const applicationsFound = await getApplicationFromDBWebhook(projectId, branch); + const settings = await prisma.setting.findUnique({ where: { id: '0' } }); if (applicationsFound && applicationsFound.length > 0) { for (const application of applicationsFound) { const buildId = cuid(); @@ -106,7 +107,7 @@ export async function gitHubEvents(request: FastifyRequest): Promi const checksum = Buffer.from(githubSignature, 'utf8'); //@ts-ignore if (checksum.length !== digest.length || !crypto.timingSafeEqual(digest, checksum)) { - throw { status: 500, message: 'SHA256 checksum failed. Are you doing something fishy?' } + throw { status: 500, message: 'SHA256 checksum failed. Are you doing something fishy?', type: 'webhook' } }; } @@ -156,7 +157,7 @@ export async function gitHubEvents(request: FastifyRequest): Promi const sourceBranch = body.pull_request.head.ref const sourceRepository = body.pull_request.head.repo.full_name if (!allowedActions.includes(pullmergeRequestAction)) { - throw { status: 500, message: 'Action not allowed.' } + throw { status: 500, message: 'Action not allowed.', type: 'webhook' } } if (application.settings.previews) { @@ -168,7 +169,7 @@ export async function gitHubEvents(request: FastifyRequest): Promi } ); if (!isRunning) { - throw { status: 500, message: 'Application not running.' } + throw { status: 500, message: 'Application not running.', type: 'webhook' } } } if ( @@ -192,7 +193,7 @@ export async function gitHubEvents(request: FastifyRequest): Promi data: { pullmergeRequestId, sourceBranch, - customDomain: `${protocol}${pullmergeRequestId}.${getDomain(application.fqdn)}`, + customDomain: `${protocol}${pullmergeRequestId}${settings.previewSeparator}${getDomain(application.fqdn)}`, application: { connect: { id: application.id } } } }) @@ -257,8 +258,8 @@ export async function gitHubEvents(request: FastifyRequest): Promi } } } - } catch ({ status, message }) { - return errorHandler({ status, message }) + } catch ({ status, message, type }) { + return errorHandler({ status, message, type }) } } \ No newline at end of file diff --git a/apps/api/src/routes/webhooks/gitlab/handlers.ts b/apps/api/src/routes/webhooks/gitlab/handlers.ts index d3a1a47b3..a3ac97705 100644 --- a/apps/api/src/routes/webhooks/gitlab/handlers.ts +++ b/apps/api/src/routes/webhooks/gitlab/handlers.ts @@ -44,8 +44,9 @@ export async function gitLabEvents(request: FastifyRequest) { const allowedActions = ['opened', 'reopen', 'close', 'open', 'update']; const webhookToken = request.headers['x-gitlab-token']; if (!webhookToken && !isDev) { - throw { status: 500, message: 'Invalid webhookToken.' } + throw { status: 500, message: 'Invalid webhookToken.', type: 'webhook' } } + const settings = await prisma.setting.findUnique({ where: { id: '0' } }); if (objectKind === 'push') { const projectId = Number(project_id); const branch = ref.split('/')[2]; @@ -95,10 +96,10 @@ export async function gitLabEvents(request: FastifyRequest) { const pullmergeRequestId = request.body.object_attributes.iid.toString(); const projectId = Number(id); if (!allowedActions.includes(action)) { - throw { status: 500, message: 'Action not allowed.' } + throw { status: 500, message: 'Action not allowed.', type: 'webhook' } } if (isDraft) { - throw { status: 500, message: 'Draft MR, do nothing.' } + throw { status: 500, message: 'Draft MR, do nothing.', type: 'webhook' } } const applicationsFound = await getApplicationFromDBWebhook(projectId, targetBranch); if (applicationsFound && applicationsFound.length > 0) { @@ -113,11 +114,11 @@ export async function gitLabEvents(request: FastifyRequest) { } ); if (!isRunning) { - throw { status: 500, message: 'Application not running.' } + throw { status: 500, message: 'Application not running.', type: 'webhook' } } } if (!isDev && application.gitSource.gitlabApp.webhookToken !== webhookToken) { - throw { status: 500, message: 'Invalid webhookToken. Are you doing something nasty?!' } + throw { status: 500, message: 'Invalid webhookToken. Are you doing something nasty?!', type: 'webhook' } } if ( action === 'opened' || @@ -140,7 +141,7 @@ export async function gitLabEvents(request: FastifyRequest) { data: { pullmergeRequestId, sourceBranch, - customDomain: `${protocol}${pullmergeRequestId}.${getDomain(application.fqdn)}`, + customDomain: `${protocol}${pullmergeRequestId}${settings.previewSeparator}${getDomain(application.fqdn)}`, application: { connect: { id: application.id } } } }) @@ -188,7 +189,7 @@ export async function gitLabEvents(request: FastifyRequest) { } } } - } catch ({ status, message }) { - return errorHandler({ status, message }) + } catch ({ status, message, type }) { + return errorHandler({ status, message, type }) } } \ No newline at end of file diff --git a/apps/api/src/routes/webhooks/traefik/handlers.ts b/apps/api/src/routes/webhooks/traefik/handlers.ts index 18bdcfebf..e46857e7e 100644 --- a/apps/api/src/routes/webhooks/traefik/handlers.ts +++ b/apps/api/src/routes/webhooks/traefik/handlers.ts @@ -1,5 +1,5 @@ import { FastifyRequest } from "fastify"; -import { errorHandler, getDomain, isDev, prisma, executeDockerCmd, fixType } from "../../../lib/common"; +import { errorHandler, getDomain, isDev, prisma, executeCommand } from "../../../lib/common"; import { getTemplates } from "../../../lib/services"; import { OnlyId } from "../../../types"; @@ -171,8 +171,8 @@ export async function proxyConfiguration(request: FastifyRequest, remote }; try { const { id = null } = request.params; - const settings = await prisma.setting.findFirst(); - if (settings.isTraefikUsed && settings.proxyDefaultRedirect) { + const coolifySettings = await prisma.setting.findFirst(); + if (coolifySettings.isTraefikUsed && coolifySettings.proxyDefaultRedirect) { traefik.http.routers['catchall-http'] = { entrypoints: ["web"], rule: "HostRegexp(`{catchall:.*}`)", @@ -190,7 +190,7 @@ export async function proxyConfiguration(request: FastifyRequest, remote traefik.http.middlewares['redirect-regexp'] = { redirectregex: { regex: '(.*)', - replacement: settings.proxyDefaultRedirect, + replacement: coolifySettings.proxyDefaultRedirect, permanent: false } } @@ -263,10 +263,12 @@ export async function proxyConfiguration(request: FastifyRequest, remote const runningContainers = {} applications.forEach((app) => dockerIds.add(app.destinationDocker.id)); for (const dockerId of dockerIds) { - const { stdout: container } = await executeDockerCmd({ dockerId, command: `docker container ls --filter 'label=coolify.managed=true' --format '{{ .Names}}'` }) - const containersArray = container.trim().split('\n'); - if (containersArray.length > 0) { - runningContainers[dockerId] = containersArray + const { stdout: container } = await executeCommand({ dockerId, command: `docker container ls --filter 'label=coolify.managed=true' --format '{{ .Names}}'` }) + if (container) { + const containersArray = container.trim().split('\n'); + if (containersArray.length > 0) { + runningContainers[dockerId] = containersArray + } } } for (const application of applications) { @@ -332,20 +334,22 @@ export async function proxyConfiguration(request: FastifyRequest, remote traefik.http.routers = { ...traefik.http.routers, ...generateRouters(serviceId, domain, nakedDomain, pathPrefix, isHttps, isWWW, dualCerts, isCustomSSL) } traefik.http.services = { ...traefik.http.services, ...generateServices(serviceId, id, port) } if (previews) { - const { stdout } = await executeDockerCmd({ dockerId, command: `docker container ls --filter="status=running" --filter="network=${network}" --filter="name=${id}-" --format="{{json .Names}}"` }) - const containers = stdout - .trim() - .split('\n') - .filter((a) => a) - .map((c) => c.replace(/"/g, '')); - if (containers.length > 0) { - for (const container of containers) { - const previewDomain = `${container.split('-')[1]}.${domain}`; - const nakedDomain = previewDomain.replace(/^www\./, ''); - const pathPrefix = '/' - const serviceId = `${container}-${port || 'default'}` - traefik.http.routers = { ...traefik.http.routers, ...generateRouters(serviceId, previewDomain, nakedDomain, pathPrefix, isHttps, isWWW, dualCerts, isCustomSSL) } - traefik.http.services = { ...traefik.http.services, ...generateServices(serviceId, container, port) } + const { stdout } = await executeCommand({ dockerId, command: `docker container ls --filter="status=running" --filter="network=${network}" --filter="name=${id}-" --format="{{json .Names}}"` }) + if (stdout) { + const containers = stdout + .trim() + .split('\n') + .filter((a) => a) + .map((c) => c.replace(/"/g, '')); + if (containers.length > 0) { + for (const container of containers) { + const previewDomain = `${container.split('-')[1]}${coolifySettings.previewSeparator}${domain}`; + const nakedDomain = previewDomain.replace(/^www\./, ''); + const pathPrefix = '/' + const serviceId = `${container}-${port || 'default'}` + traefik.http.routers = { ...traefik.http.routers, ...generateRouters(serviceId, previewDomain, nakedDomain, pathPrefix, isHttps, isWWW, dualCerts, isCustomSSL) } + traefik.http.services = { ...traefik.http.services, ...generateServices(serviceId, container, port) } + } } } } @@ -359,10 +363,12 @@ export async function proxyConfiguration(request: FastifyRequest, remote const runningContainers = {} services.forEach((app) => dockerIds.add(app.destinationDocker.id)); for (const dockerId of dockerIds) { - const { stdout: container } = await executeDockerCmd({ dockerId, command: `docker container ls --filter 'label=coolify.managed=true' --format '{{ .Names}}'` }) - const containersArray = container.trim().split('\n'); - if (containersArray.length > 0) { - runningContainers[dockerId] = containersArray + const { stdout: container } = await executeCommand({ dockerId, command: `docker container ls --filter 'label=coolify.managed=true' --format '{{ .Names}}'` }) + if (container) { + const containersArray = container.trim().split('\n'); + if (containersArray.length > 0) { + runningContainers[dockerId] = containersArray + } } } for (const service of services) { diff --git a/apps/api/tags.json b/apps/api/tags.json index f2903d128..0d84f6caa 100644 --- a/apps/api/tags.json +++ b/apps/api/tags.json @@ -1 +1 @@ -[{"name":"appsmith","image":"appsmith/appsmith-ce","tags":["v1.8.8","v1.8.6","v1.8.4","v1.8.2","v1.8.0","v1.7.8","v1.7.6","v1.7.4","v1.7.2","v1.7.13","v1.7.11","v1.7.1","v1.6.9","v1.6.7","v1.6.5","v1.6.3","v1.6.22","v1.6.20","v1.6.19","v1.6.17","v1.6.15","v1.6.13","v1.6.11","v1.6.1","v1.5.30","v1.5.28","v1.5.26","v1.5.24","v1.5.22"]},{"name":"appwrite","image":"appwrite/appwrite","tags":["1.1.0","1.0.3","1.0.1","1.0.0","0.9.3","0.9.1","0.8.0","0.7.1","0.6.2","0.6.0","0.5.2","0.5.0","0.3.1","0.2.0","0.15.2","0.15.0","0.14.2","0.14.0","0.13.4","0.13.2","0.13.0","0.12.3","0.12.1","0.12.0","0.11.2","0.11.0","0.10.4","0.10.2","0.10.0","0.1.8"]},{"name":"fider","image":"getfider/fider","tags":["stable","master","main","dev","SHA_ee6e83cfaadadaa56ab76e089e01f5631af3506f","SHA_deb4f9b4f561d890d8a80e6872fea9a98a265cc6","SHA_d5cc307909d43447200483d76b5db74d8ed8349e","SHA_d1674476577a7fd3c88fc29f91c3f35f5bd6a260","SHA_d107cbb157abca6576110080736213efe0955cff","SHA_c9c55b2f5b33a76015241b97e03cfac1254b42a7","SHA_bcf451a3cb02d5c8a489fd30309249296057b084","SHA_bbfe419639514f949a042807addf0fde7d4de225","SHA_adc3afc4c7bcf96931a5f90cab65c282d860dbfd","SHA_ab5283ae95334f10b5041402dce79e333c472015","SHA_a3f4cb5ed0a4ee2d726705fc426636364aac17a1","SHA_a18224142bf51bc6463c3d22f45f62287902e9a6","SHA_7851f9da566132d87fa2a63004e78c3bc9c09c6c","SHA_6c0f2bed1754e9d579eb9575129a6e3dbc529c32","SHA_603508c8790d6a6fb1e852df1a58ead8e5b3ea6c","SHA_55efacf164a4749b50ee68ae8925e7dc9dfa3a0c","SHA_4bdd291ce61e5f5dfc063fa1b2d9be8c9ff1d4c4","SHA_3fba9cb6a9ceab0c78c6cff3220610f591f657cb","SHA_3d635b57606a9885babe91fe975b11429e0f2c38","SHA_3b794edbd9789a8aa38ecd3714bc536a675d3058","SHA_3570c454ad3252b690608f7bf8051737d8519f8a","SHA_263e2709fd145f3ea511e5557e170102899995b0","SHA_17f92b16ef790003338f0926fc8d791a9a61333c","SHA_0cddae6b274f915aabf2c3a3cbacf5f524bc59a0","SHA_0c403665346acc3ba90998a28ca53e8f76e54247","SHA_097ca277b11aefdb4cbbffb8a1dbc6e64130a960"]},{"name":"ghost-mariadb","image":"bitnami/ghost","tags":["5.7.1","5.7.0","5.5.0","5.4.1","5.4.0","5.3.1","5.3.0","5.22.9","5.22.8","5.22.7","5.22.6","5.22.5","5.22.4","5.22.3","5.22.2","5.22.11","5.22.10","5.22.1","5.22.0","5.21.0","5.20.0","5.2.4","5.2.3","5.2.2","5.2.1","5.19.3","5.19.2","5.19.1","5.19.0","5.18.0","4.48.8"]},{"name":"ghost-mysql","image":"library/ghost","tags":["5.9.4","5.8.3","5.8.2","5.7.1","5.7.0","5.5.0","5.4.1","5.3.1","5.3.0","5.22.9","5.22.8","5.22.4","5.22.11","5.22.10","5.22.1","5.20.0","5.2.4","5.2.3","5.2.2","5.2.1","5.19.3","5.19.0","5.18.0","5.17.2","5.17.1","5.17.0","5.16.2","5.14.2","5.14.1","5.13.2"]},{"name":"ghost-only","image":"library/ghost","tags":["5.9.4","5.8.3","5.8.2","5.7.1","5.7.0","5.5.0","5.4.1","5.3.1","5.3.0","5.22.9","5.22.8","5.22.4","5.22.11","5.22.10","5.22.1","5.20.0","5.2.4","5.2.3","5.2.2","5.2.1","5.19.3","5.19.0","5.18.0","5.17.2","5.17.1","5.17.0","5.16.2","5.14.2","5.14.1","5.13.2"]},{"name":"gitea","image":"gitea/gitea","tags":["1.9.6","1.9.5","1.9.4","1.9.3","1.9.2","1.9.0","1.8.3","1.8.1","1.8.0","1.7.5","1.7.3","1.7.1","1.7.0","1.6.3","1.6.1","1.6.0","1.5.3","1.5.1","1.5.0","1.4.3","1.4.1","1.4.0","1.3.3","1.3.1","1.3.0","1.2.3","1.2.1","1.2.0","1.17.3","1.17.2"]},{"name":"glitchtip","image":"glitchtip/glitchtip","tags":["v2.0.7","v2.0.5","v2.0.2","v2.0.0","v1.9.2","v1.9.0","v1.8.4","v1.8.2","v1.8.0","v1.7.1","v1.6.4","v1.6.2","v1.6.0","v1.5.3","v1.5.1","v1.4.1","v1.3.3","v1.3.1","v1.2.6","v1.2.4","v1.2.2","v1.2.0","v1.12.4","v1.12.2","v1.12.0","v1.10.3","v1.10.1","v1.1.2","v1.1.0","v1.0.8"]},{"name":"grafana","image":"grafana/grafana","tags":["9.2.5","9.2.4","9.2.3","9.2.2","9.2.1","9.2.0","9.1.8","9.1.7","9.1.6","9.1.5","9.1.4","9.1.3","9.1.2","9.1.1","9.1.0","9.0.9","9.0.8","9.0.7","9.0.6","9.0.5","9.0.4","9.0.3","9.0.2","9.0.1","9.0.0","8.5.9","8.5.6","8.5.5","8.5.4","8.5.3"]},{"name":"hasura","image":"hasura/graphql-engine","tags":["v2.9.0","v2.8.4","v2.8.3","v2.8.2","v2.8.1","v2.8.0","v2.7.0","v2.6.2","v2.6.1","v2.6.0","v2.5.2","v2.5.1","v2.5.0","v2.4.0","v2.3.1","v2.3.0","v2.2.2","v2.2.1","v2.2.0","v2.15.1","v2.15.0","v2.14.0","v2.13.1","v2.13.0","v2.12.0","v2.11.2","v2.11.1","v2.11.0","v2.10.1","v2.10.0"]},{"name":"keycloak","image":"quay.io/keycloak/keycloak","tags":["9.0.3","9.0.0","8.0.1","7.0.0","6.0.1","6.0.0","20.0.1","20.0.0","19.0.3","19.0.1","19.0.0","18.0.1","18.0.0","17.0.1","17.0.0","16.1.0","15.1.1","15.0.2","15.0.0","13.0.1","12.0.4","12.0.2","12.0.0","11.0.2","11.0.0","10.0.1"]},{"name":"languagetool","image":"silviof/docker-languagetool","tags":["latest","5.8","5.7","5.6","5.5","5.4","5.3"]},{"name":"lavalink","image":"fredboat/lavalink","tags":["v3.6","v3-vda0b3a4b3916a7b1a2b79702de1143c3a6939810-SNAPSHOT","v3-vc92690c425390bd20f6c51643c67ba79ab85b7e0-SNAPSHOT","v3-vab81dcd46adf3e8a961dd57eacd2a1bde1233e6c-SNAPSHOT","v3-v9c9432704d6a4badfcbd06a57597c54bed8f4326-SNAPSHOT","v3-v3.0","v3-v3","v3-v124f8fae7dab299f9cdf1cb4c1715be455497286-SNAPSHOT","v3-","v3","v2.0.1","v2.0","v2","update-udpqueue-vb4a439d6147dbd8641ea4f265e8efc9f1e16e2d3-SNAPSHOT","update-udpqueue-","update-udpqueue","revert-713-fix-error-for-loading-jda-nas","refactor-github-actions","patch-update-github-actions","patch-more-configurable-github-actions","patch-lavaplayer-update","patch-lavaplayer-bump","patch-build-number","next-api-vd4db194cac7a839a3899857f1f6d7b910369309d-SNAPSHOT","next-api-vc2e018d5ffef54b2d17244b3d213e31723a084d6-SNAPSHOT","next-api-v42cb5f7c58e98d1911e87bffb35aee0a235b85f8-SNAPSHOT","next-api-v31a243bda80badbd7d643f68fc1f87e99639060f-SNAPSHOT","next-api-v17f6884434c2d70d1704b2322a951d9f07af8865-SNAPSHOT","next-api-","next-api"]},{"name":"meilisearch","image":"getmeili/meilisearch","tags":["v0.9.0","v0.8.3","v0.8.1","v0.29.1","v0.29.0","v0.28.1","v0.28.0","v0.27.1","v0.27.0","v0.26.1","v0.26.0","v0.25.1","v0.25.0","v0.23.1","v0.23.0","v0.21.1","v0.21.0","v0.20.0","v0.19.0","v0.18.1","v0.18.0","v0.17.0","v0.16.0","v0.14.1","v0.14.0","v0.12.0","v0.11.0","v0.10.0","0.14.1"]},{"name":"minio","image":"minio/minio","tags":["RELEASE.2022-11-17T23-20-09Z.fips","RELEASE.2022-11-11T03-44-20Z.fips","RELEASE.2022-11-10T18-20-21Z.fips","RELEASE.2022-11-08T05-27-07Z.fips","RELEASE.2022-10-29T06-21-33Z.fips","RELEASE.2022-10-24T18-35-07Z.fips","RELEASE.2022-10-21T22-37-48Z.fips","RELEASE.2022-10-20T00-55-09Z.fips","RELEASE.2022-10-15T19-57-03Z.fips","RELEASE.2022-10-08T20-11-00Z.fips","RELEASE.2022-10-05T14-58-27Z.fips","RELEASE.2022-10-02T19-29-29Z.fips","RELEASE.2022-09-25T15-44-53Z.fips","RELEASE.2022-09-22T18-57-27Z.fips","RELEASE.2022-09-17T00-09-45Z.hotfix.fc6d6fdbd","RELEASE.2022-09-17T00-09-45Z.hotfix.4bb22d5cd","RELEASE.2022-09-17T00-09-45Z","RELEASE.2022-09-07T22-25-02Z","RELEASE.2022-09-01T23-53-36Z","RELEASE.2022-08-26T19-53-15Z","RELEASE.2022-08-25T07-17-05Z","RELEASE.2022-08-22T23-53-06Z.fips","RELEASE.2022-08-13T21-54-44Z.fips","RELEASE.2022-08-11T04-37-28Z.fips","RELEASE.2022-08-08T18-34-09Z.fips","RELEASE.2022-08-05T23-27-09Z.fips","RELEASE.2022-08-02T23-59-16Z.fips","RELEASE.2022-07-30T05-21-40Z.test.fdec67a59","RELEASE.2022-07-30T05-21-40Z.fips","RELEASE.2022-07-29T19-40-48Z.fips"]},{"name":"n8n","image":"n8nio/n8n","tags":["0.99.1","0.99.0","0.98.0","0.97.0","0.96.0","0.95.1","0.95.0","0.94.1","0.94.0","0.93.0","0.92.0","0.91.0","0.9.0","0.89.2","0.88.1","0.88.0","0.87.2","0.87.1","0.87.0","0.86.1","0.86.0","0.85.0","0.84.4","0.84.3","0.84.1","0.84.0","0.83.0","0.82.1","0.82.0","0.81.0"]},{"name":"nocodb","image":"nocodb/nocodb","tags":["0.98.3","0.98.1","0.97.0","0.96.3","0.96.1","0.92.4","0.92.0","0.91.8","0.91.6","0.91.1","0.90.8","0.90.5","0.90.3","0.90.11","0.90.1","0.9.9","0.9.7","0.9.43","0.9.41","0.9.39","0.9.37","0.9.35","0.9.33","0.9.31","0.9.29","0.9.27","0.9.25","0.9.22","0.9.19","0.9.16"]},{"name":"plausibleanalytics","image":"plausible/analytics","tags":["v1.5.0-rc.1","v1.4.4","v1.4.3","v1.4.2","v1.4.1","v1.4.0.rc.0","v1.4.0-rc.0","v1.4.0","v1.4","v1.3.0-rc.1","v1.3.0-rc.0","v1.3.0","v1.3","v1.2.1","v1.2.0","v1.2-rc.1","v1.2-rc.0","v1.2","v1.1.1","v1.1.0","v1.1","v1.0.0","v1.0","v1","stable","master","loadtest","latest","1.5.0-rc.0"]},{"name":"searxng","image":"searxng/searxng","tags":["2022.11.11-e6345758","2022.11.11-3a765113","2022.11.10-117f69fa","2022.11.09-ee4475ff","2022.11.07-d3949269","2022.11.07-8f19bdaf","2022.11.06-ae54c7d5","2022.11.06-2dc5c0e1","2022.11.05-e9f42e1c","2022.11.05-d764d94a","2022.11.05-d3a7399e","2022.11.05-d37afb8a","2022.11.05-4fe54636","2022.10.29-fc9986de","2022.10.29-fa59ff9b","2022.10.29-d49ccb54","2022.10.29-a9deead1","2022.10.29-3f1d594c","2022.10.28-c26fa335","2022.10.28-5db4ed5d","2022.10.28-5a181ea1","2022.10.25-affd8f75","2022.10.25-4783d6c9","2022.10.21-710a3a00","2022.10.14-e2dd5a80","2022.10.14-72f6367e","2022.10.14-4d4dfc58","2022.10.14-2eb81701","2022.10.14-1a5b0965","2022.10.14-096d9def"]},{"name":"trilium","image":"zadam/trilium","tags":["0.56.1","0.55.1","0.54.2","0.53.2","0.52.4","0.52.2","0.51.2","0.50.3","0.50.1","0.49.5","0.49.3","0.48.9","0.48.7","0.48.4","0.48.2","0.47.8","0.47.6","0.47.4","0.47.2","0.46.7","0.46.5","0.45.9","0.45.7","0.45.5","0.45.3","0.45.10","0.44.8","0.44.6","0.44.4","0.43.3"]},{"name":"umami-postgresql","image":"ghcr.io/umami-software/umami","tags":["postgresql-v1.39.5","postgresql-v1.39.4","postgresql-v1.39.3","postgresql-v1.39.2","postgresql-v1.39.1","postgresql-v1.39.0","postgresql-v1.38.0","postgresql-v1.37.0","postgresql-v1.36.1","postgresql-v1.36.0","postgresql-v1.35.0","postgresql-v1.34.0","postgresql-v1.33.3","postgresql-latest","mysql-v1.39.5","mysql-v1.39.4","mysql-v1.39.3","mysql-v1.39.2","mysql-v1.39.1","mysql-v1.39.0","mysql-v1.38.0","mysql-v1.37.0","mysql-v1.36.1","mysql-v1.36.0","mysql-v1.35.0","mysql-v1.34.0","mysql-v1.33.3","mysql-latest"]},{"name":"umami","image":"ghcr.io/umami-software/umami","tags":["postgresql-v1.39.5","postgresql-v1.39.4","postgresql-v1.39.3","postgresql-v1.39.2","postgresql-v1.39.1","postgresql-v1.39.0","postgresql-v1.38.0","postgresql-v1.37.0","postgresql-v1.36.1","postgresql-v1.36.0","postgresql-v1.35.0","postgresql-v1.34.0","postgresql-v1.33.3","postgresql-latest","mysql-v1.39.5","mysql-v1.39.4","mysql-v1.39.3","mysql-v1.39.2","mysql-v1.39.1","mysql-v1.39.0","mysql-v1.38.0","mysql-v1.37.0","mysql-v1.36.1","mysql-v1.36.0","mysql-v1.35.0","mysql-v1.34.0","mysql-v1.33.3","mysql-latest"]},{"name":"uptimekuma","image":"louislam/uptime-kuma","tags":["1.9.2","1.9.1","1.9.0","1.8.0","1.7.3","1.7.1","1.7.0","1.6.3","1.6.2","1.6.1","1.6.0","1.5.3","1.5.2","1.5.0","1.3.1","1.2.0","1.18.5","1.18.4","1.18.3","1.18.2","1.18.1","1.18.0","1.17.1","1.17.0","1.16.1","1.16.0","1.15.1","1.15.0","1.14.1","1.14.0"]},{"name":"vaultwarden","image":"vaultwarden/server","tags":["1.26.0","1.25.2","1.25.1","1.25.0","1.24.0","1.23.1","1.23.0","1.22.2","1.22.1","1.22.0","1.21.0"]},{"name":"vscodeserver","image":"codercom/code-server","tags":["4.8.3","4.8.2","4.8.1","4.8.0","4.7.0","4.6.0","4.5.1","4.4.0","4.2.0","4.0.2","3.9.3","3.9.1","3.8.1","3.7.4","3.7.2","3.7.0","3.6.1","3.5.0","3.4.0","3.3.0","3.2.0","3.11.1","3.10.2","3.10.0","3.1.1","3.1.0","3.0.2","3.0.0"]},{"name":"weblate","image":"weblate/weblate","tags":["latest","edge-2022-11-15-cad0a043b32c1ee61611ab258db0f01c5e6d718f","edge-2022-11-10-bf41db3afbab22384e103718094738dcfdc1a270","edge-2022-11-09-9bc90ce8b873778d2f486eccd0163bb1bb65ca6e","edge-2022-11-08-36e221037ff7097f8cd2c88d779135b6c7d3f363","edge-2022-11-08-3568e3c6759a9e9b779d98cb98393526d451466a","edge-2022-11-08-261d197970ca0679514d32ff783467972e807061","edge-2022-11-05-fa5cb203d854a11cc7850868a2890168afa3e7da","edge-2022-11-05-d93ae789eef8f065240f9fb6feb3edb236a7e6f8","edge-2022-11-05-8fc2be8e9d22e5ca2da2773488da7f72c5927ec3","edge-2022-11-05-85da67e88a113bed65530f0695ad4cddec0ed05a","edge-2022-11-05-3f4d77b6f2cb16bf008a4ef587e843ccb9c0c5d0","edge-2022-11-05-226eed520a2b32c3583c6e3247109ec8950764e7","edge-2022-11-03-487f3255cb89415fbe0769fa4b7bd2a9209deca6","edge-2022-11-02-e4171e0c5657ca38341cce8ac31f5cbdf25389eb","edge-2022-11-02-6d886c40cd62eb23d21f7c0a1840b4a7a4c51ad0","edge-2022-11-01-608df4dd95a2d1f76c15cddd9e116bb4c3229168","edge-2022-11-01-54957be78eb76f602ceae50c0b01b64b20402b2a","edge-2022-10-31-c55c7302a6c82a160ee9d711893c12d67ecd3b27","edge-2022-10-26-c69cfdd83ed1fad4a4d57398552b8c70894a6586","edge-2022-10-26-410b3aff37de5bbfacbc47642ce28b2518bee506","edge-2022-10-25-e09e2c29ed3748eb0fa248453635dd27768e8dd9","edge-2022-10-25-a059748c178cce0bc30bdc915d4ff8f0d13ce25c","edge-2022-10-24-94ae069cdcf9a171e812da32d760a436fa9b37ad","edge-2022-10-24-71d2011a1f6d2b60bb65af9f775bf75d4beaf8fd","edge-2022-10-19-acb8e82a0ea2c86fb7ce8f5cb1a81e2bc634583a","edge-2022-10-18-f54b83a0898bef9d14b00d885f675c2e4ebb68ca","edge-2022-10-17-eb9dc248720581052b4be4e3f1f372bce37edd62","edge-2022-10-17-e5a5f6cfdf7e500b7b357566971172fad022c1dc","edge-2022-10-17-da6d01ea005c29453e0111a45956d16bf2262f54"]},{"name":"wordpress-only","image":"library/wordpress","tags":["php8.1-fpm-alpine","php8.1-fpm","php8.1-apache","php8.1","php8.0-fpm-alpine","php8.0-fpm","php8.0-apache","php8.0","php7.4-fpm-alpine","php7.4-fpm","php7.4-apache","php7.4","php7.3-fpm-alpine","php7.3-fpm","php7.3-apache","php7.3","php7.2-fpm-alpine","php7.2-fpm","php7.2-apache","php7.2","php7.1-fpm-alpine","php7.1-fpm","php7.1-apache","php7.1","php7.0-fpm-alpine","php7.0-fpm","php7.0-apache","php7.0","php5.6-fpm-alpine","php5.6-fpm"]},{"name":"wordpress","image":"library/wordpress","tags":["php8.1-fpm-alpine","php8.1-fpm","php8.1-apache","php8.1","php8.0-fpm-alpine","php8.0-fpm","php8.0-apache","php8.0","php7.4-fpm-alpine","php7.4-fpm","php7.4-apache","php7.4","php7.3-fpm-alpine","php7.3-fpm","php7.3-apache","php7.3","php7.2-fpm-alpine","php7.2-fpm","php7.2-apache","php7.2","php7.1-fpm-alpine","php7.1-fpm","php7.1-apache","php7.1","php7.0-fpm-alpine","php7.0-fpm","php7.0-apache","php7.0","php5.6-fpm-alpine","php5.6-fpm"]}] \ No newline at end of file +[{"name":"appsmith","image":"appsmith/appsmith-ce","tags":["v1.8.8","v1.8.6","v1.8.4","v1.8.2","v1.8.10","v1.8.0","v1.7.8","v1.7.6","v1.7.4","v1.7.2","v1.7.13","v1.7.11","v1.7.1","v1.6.9","v1.6.7","v1.6.5","v1.6.3","v1.6.22","v1.6.20","v1.6.19","v1.6.17","v1.6.15","v1.6.13","v1.6.11","v1.6.1","v1.5.30","v1.5.28","v1.5.26","v1.5.24","v1.5.22"]},{"name":"appwrite","image":"appwrite/appwrite","tags":["1.1.2","1.1.0","1.0.3","1.0.1","1.0.0","0.9.3","0.9.1","0.8.0","0.7.1","0.6.2","0.6.0","0.5.2","0.5.0","0.3.1","0.2.0","0.15.2","0.15.0","0.14.2","0.14.0","0.13.4","0.13.2","0.13.0","0.12.3","0.12.1","0.12.0","0.11.2","0.11.0","0.10.4","0.10.2","0.10.0"]},{"name":"fider","image":"getfider/fider","tags":["stable","master","main","dev","SHA_ee6e83cfaadadaa56ab76e089e01f5631af3506f","SHA_deb4f9b4f561d890d8a80e6872fea9a98a265cc6","SHA_d5cc307909d43447200483d76b5db74d8ed8349e","SHA_d1674476577a7fd3c88fc29f91c3f35f5bd6a260","SHA_d107cbb157abca6576110080736213efe0955cff","SHA_c9c55b2f5b33a76015241b97e03cfac1254b42a7","SHA_bcf451a3cb02d5c8a489fd30309249296057b084","SHA_bbfe419639514f949a042807addf0fde7d4de225","SHA_adc3afc4c7bcf96931a5f90cab65c282d860dbfd","SHA_ab5283ae95334f10b5041402dce79e333c472015","SHA_a3f4cb5ed0a4ee2d726705fc426636364aac17a1","SHA_a18224142bf51bc6463c3d22f45f62287902e9a6","SHA_8e5cff30d95963eaee2587488d351e0d658c8195","SHA_8cabe2817ce7ccaf2f0a9fdbb1b5d3411de87f81","SHA_7851f9da566132d87fa2a63004e78c3bc9c09c6c","SHA_6c0f2bed1754e9d579eb9575129a6e3dbc529c32","SHA_603508c8790d6a6fb1e852df1a58ead8e5b3ea6c","SHA_55efacf164a4749b50ee68ae8925e7dc9dfa3a0c","SHA_4bdd291ce61e5f5dfc063fa1b2d9be8c9ff1d4c4","SHA_3fba9cb6a9ceab0c78c6cff3220610f591f657cb","SHA_3d635b57606a9885babe91fe975b11429e0f2c38","SHA_3b794edbd9789a8aa38ecd3714bc536a675d3058","SHA_3570c454ad3252b690608f7bf8051737d8519f8a","SHA_263e2709fd145f3ea511e5557e170102899995b0","SHA_255c30ed012fc4c39ffc97efc1d3b00425b17c72","SHA_17f92b16ef790003338f0926fc8d791a9a61333c"]},{"name":"ghost-mariadb","image":"bitnami/ghost","tags":["5.7.1","5.7.0","5.5.0","5.4.1","5.4.0","5.3.1","5.3.0","5.24.2","5.24.1","5.24.0","5.23.0","5.22.9","5.22.8","5.22.7","5.22.6","5.22.5","5.22.4","5.22.3","5.22.2","5.22.11","5.22.10","5.22.1","5.22.0","5.21.0","5.20.0","5.2.4","5.2.3","5.2.2","5.2.1","5.19.3","4.48.8"]},{"name":"ghost-mysql","image":"library/ghost","tags":["5.9.4","5.8.3","5.8.2","5.7.1","5.7.0","5.5.0","5.4.1","5.3.1","5.3.0","5.24.2","5.24.1","5.23.0","5.22.9","5.22.8","5.22.4","5.22.11","5.22.10","5.22.1","5.20.0","5.2.4","5.2.3","5.2.2","5.2.1","5.19.3","5.19.0","5.18.0","5.17.2","5.17.1","5.17.0","5.16.2"]},{"name":"ghost-only","image":"library/ghost","tags":["5.9.4","5.8.3","5.8.2","5.7.1","5.7.0","5.5.0","5.4.1","5.3.1","5.3.0","5.24.2","5.24.1","5.23.0","5.22.9","5.22.8","5.22.4","5.22.11","5.22.10","5.22.1","5.20.0","5.2.4","5.2.3","5.2.2","5.2.1","5.19.3","5.19.0","5.18.0","5.17.2","5.17.1","5.17.0","5.16.2"]},{"name":"gitea","image":"gitea/gitea","tags":["1.9.6","1.9.5","1.9.4","1.9.3","1.9.2","1.9.0","1.8.3","1.8.1","1.8.0","1.7.5","1.7.3","1.7.1","1.7.0","1.6.3","1.6.1","1.6.0","1.5.3","1.5.1","1.5.0","1.4.3","1.4.1","1.4.0","1.3.3","1.3.1","1.3.0","1.2.3","1.2.1","1.2.0","1.17.3","1.17.2"]},{"name":"glitchtip","image":"glitchtip/glitchtip","tags":["v2.0.7","v2.0.5","v2.0.2","v2.0.0","v1.9.2","v1.9.0","v1.8.4","v1.8.2","v1.8.0","v1.7.1","v1.6.4","v1.6.2","v1.6.0","v1.5.3","v1.5.1","v1.4.1","v1.3.3","v1.3.1","v1.2.6","v1.2.4","v1.2.2","v1.2.0","v1.12.4","v1.12.2","v1.12.0","v1.10.3","v1.10.1","v1.1.2","v1.1.0","v1.0.8"]},{"name":"grafana","image":"grafana/grafana","tags":["9.3.1","9.3.0","9.2.7","9.2.6","9.2.5","9.2.4","9.2.3","9.2.2","9.2.1","9.2.0","9.1.8","9.1.7","9.1.6","9.1.5","9.1.4","9.1.3","9.1.2","9.1.1","9.1.0","9.0.9","9.0.8","9.0.7","9.0.6","9.0.5","9.0.4","9.0.3","9.0.2","9.0.1","9.0.0","8.5.9"]},{"name":"hasura","image":"hasura/graphql-engine","tags":["v2.9.0","v2.8.4","v2.8.3","v2.8.2","v2.8.1","v2.8.0","v2.7.0","v2.6.2","v2.6.1","v2.6.0","v2.5.2","v2.5.1","v2.5.0","v2.4.0","v2.3.1","v2.3.0","v2.2.2","v2.2.1","v2.2.0","v2.15.2","v2.15.1","v2.15.0","v2.14.1","v2.14.0","v2.13.2","v2.13.1","v2.13.0","v2.12.1","v2.12.0","v2.11.3"]},{"name":"keycloak","image":"quay.io/keycloak/keycloak","tags":["9.0.3","9.0.0","8.0.1","7.0.0","6.0.1","6.0.0","20.0.1","20.0.0","19.0.3","19.0.1","19.0.0","18.0.1","18.0.0","17.0.1","17.0.0","16.1.0","15.1.1","15.0.2","15.0.0","13.0.1","12.0.4","12.0.2","12.0.0","11.0.2","11.0.0","10.0.1"]},{"name":"languagetool","image":"silviof/docker-languagetool","tags":["latest","5.8","5.7","5.6","5.5","5.4","5.3"]},{"name":"lavalink","image":"fredboat/lavalink","tags":["v3.7","v3.6","v3-vda0b3a4b3916a7b1a2b79702de1143c3a6939810-SNAPSHOT","v3-vc92690c425390bd20f6c51643c67ba79ab85b7e0-SNAPSHOT","v3-vab81dcd46adf3e8a961dd57eacd2a1bde1233e6c-SNAPSHOT","v3-v9c9432704d6a4badfcbd06a57597c54bed8f4326-SNAPSHOT","v3-v3.0","v3-v3","v3-v124f8fae7dab299f9cdf1cb4c1715be455497286-SNAPSHOT","v3-","v3","v2.0.1","v2.0","v2","update-udpqueue-vb4a439d6147dbd8641ea4f265e8efc9f1e16e2d3-SNAPSHOT","update-udpqueue-","update-udpqueue","revert-713-fix-error-for-loading-jda-nas","refactor-github-actions","patch-update-github-actions","patch-more-configurable-github-actions","patch-lavaplayer-update","patch-lavaplayer-bump","patch-build-number","next-api-vd4db194cac7a839a3899857f1f6d7b910369309d-SNAPSHOT","next-api-vc2e018d5ffef54b2d17244b3d213e31723a084d6-SNAPSHOT","next-api-v42cb5f7c58e98d1911e87bffb35aee0a235b85f8-SNAPSHOT","next-api-v31a243bda80badbd7d643f68fc1f87e99639060f-SNAPSHOT","next-api-v17f6884434c2d70d1704b2322a951d9f07af8865-SNAPSHOT","next-api-"]},{"name":"meilisearch","image":"getmeili/meilisearch","tags":["v0.9.0","v0.8.3","v0.8.1","v0.30.0","v0.29.1","v0.29.0","v0.28.1","v0.28.0","v0.27.1","v0.27.0","v0.26.1","v0.26.0","v0.25.1","v0.25.0","v0.23.1","v0.23.0","v0.21.1","v0.21.0","v0.20.0","v0.19.0","v0.18.1","v0.18.0","v0.17.0","v0.16.0","v0.14.1","v0.14.0","v0.12.0","v0.11.0","v0.10.0","0.14.1"]},{"name":"minio","image":"minio/minio","tags":["RELEASE.2022-11-29T23-40-49Z.fips","RELEASE.2022-11-26T22-43-32Z.fips","RELEASE.2022-11-17T23-20-09Z.fips","RELEASE.2022-11-11T03-44-20Z.fips","RELEASE.2022-11-10T18-20-21Z.fips","RELEASE.2022-11-08T05-27-07Z.fips","RELEASE.2022-10-29T06-21-33Z.fips","RELEASE.2022-10-24T18-35-07Z.hotfix.ce525fdaf","RELEASE.2022-10-24T18-35-07Z.fips","RELEASE.2022-10-21T22-37-48Z.fips","RELEASE.2022-10-20T00-55-09Z.fips","RELEASE.2022-10-15T19-57-03Z.fips","RELEASE.2022-10-08T20-11-00Z.fips","RELEASE.2022-10-05T14-58-27Z.fips","RELEASE.2022-10-02T19-29-29Z.fips","RELEASE.2022-09-25T15-44-53Z.fips","RELEASE.2022-09-22T18-57-27Z.fips","RELEASE.2022-09-17T00-09-45Z.hotfix.fc6d6fdbd","RELEASE.2022-09-17T00-09-45Z.hotfix.4bb22d5cd","RELEASE.2022-09-17T00-09-45Z","RELEASE.2022-09-07T22-25-02Z","RELEASE.2022-09-01T23-53-36Z","RELEASE.2022-08-26T19-53-15Z","RELEASE.2022-08-25T07-17-05Z","RELEASE.2022-08-22T23-53-06Z.fips","RELEASE.2022-08-13T21-54-44Z.fips","RELEASE.2022-08-11T04-37-28Z.fips","RELEASE.2022-08-08T18-34-09Z.fips","RELEASE.2022-08-05T23-27-09Z.fips","RELEASE.2022-08-02T23-59-16Z.fips"]},{"name":"n8n","image":"n8nio/n8n","tags":["0.99.1","0.99.0","0.98.0","0.97.0","0.96.0","0.95.1","0.95.0","0.94.1","0.94.0","0.93.0","0.92.0","0.91.0","0.9.0","0.89.2","0.88.1","0.88.0","0.87.2","0.87.1","0.87.0","0.86.1","0.86.0","0.85.0","0.84.4","0.84.3","0.84.1","0.84.0","0.83.0","0.82.1","0.82.0","0.81.0"]},{"name":"nocodb","image":"nocodb/nocodb","tags":["0.99.2","0.99.0","0.98.3","0.98.1","0.97.0","0.96.3","0.96.1","0.92.4","0.92.0","0.91.8","0.91.6","0.91.1","0.90.8","0.90.5","0.90.3","0.90.11","0.90.1","0.9.9","0.9.7","0.9.43","0.9.41","0.9.39","0.9.37","0.9.35","0.9.33","0.9.31","0.9.29","0.9.27","0.9.25","0.9.22"]},{"name":"plausibleanalytics-arm","image":"plausible/analytics","tags":["v1.5.0-rc.2","v1.5.0-rc.1","v1.4.4","v1.4.3","v1.4.2","v1.4.1","v1.4.0.rc.0","v1.4.0-rc.0","v1.4.0","v1.4","v1.3.0-rc.1","v1.3.0-rc.0","v1.3.0","v1.3","v1.2.1","v1.2.0","v1.2-rc.1","v1.2-rc.0","v1.2","v1.1.1","v1.1.0","v1.1","v1.0.0","v1.0","v1","stable","master","loadtest","latest","1.5.0-rc.0"]},{"name":"plausibleanalytics","image":"plausible/analytics","tags":["v1.5.0-rc.2","v1.5.0-rc.1","v1.4.4","v1.4.3","v1.4.2","v1.4.1","v1.4.0.rc.0","v1.4.0-rc.0","v1.4.0","v1.4","v1.3.0-rc.1","v1.3.0-rc.0","v1.3.0","v1.3","v1.2.1","v1.2.0","v1.2-rc.1","v1.2-rc.0","v1.2","v1.1.1","v1.1.0","v1.1","v1.0.0","v1.0","v1","stable","master","loadtest","latest","1.5.0-rc.0"]},{"name":"pocketbase","image":"coollabsio/pocketbase","tags":["0.8.0-arm64","0.8.0-amd64","0.8.0-aarch64","0.8.0"]},{"name":"searxng","image":"searxng/searxng","tags":["2022.12.02-ffb72dfd","2022.12.02-890d63b9","2022.12.02-4970db05","2022.12.02-317fe0a2","2022.11.30-f19837cf","2022.11.30-44d4a171","2022.11.30-0361f836","2022.11.29-a8359dd4","2022.11.29-82af2f44","2022.11.29-768659f2","2022.11.29-5b19f892","2022.11.29-3579a38a","2022.11.29-1b2f1c17","2022.11.25-5ca6868c","2022.11.25-28ae469f","2022.11.25-1314c1c5","2022.11.19-b5371b7a","2022.11.18-fe8b0472","2022.11.18-1cdadf4b","2022.11.11-e6345758","2022.11.11-3a765113","2022.11.10-117f69fa","2022.11.09-ee4475ff","2022.11.07-d3949269","2022.11.07-8f19bdaf","2022.11.06-ae54c7d5","2022.11.06-2dc5c0e1","2022.11.05-e9f42e1c","2022.11.05-d764d94a","2022.11.05-d3a7399e"]},{"name":"trilium","image":"zadam/trilium","tags":["0.57.2","0.56.1","0.55.1","0.54.2","0.53.2","0.52.4","0.52.2","0.51.2","0.50.3","0.50.1","0.49.5","0.49.3","0.48.9","0.48.7","0.48.4","0.48.2","0.47.8","0.47.6","0.47.4","0.47.2","0.46.7","0.46.5","0.45.9","0.45.7","0.45.5","0.45.3","0.45.10","0.44.8","0.44.6","0.44.4"]},{"name":"umami-postgresql","image":"ghcr.io/umami-software/umami","tags":["postgresql-v1.39.5","postgresql-v1.39.4","postgresql-v1.39.3","postgresql-v1.39.2","postgresql-v1.39.1","postgresql-v1.39.0","postgresql-v1.38.0","postgresql-v1.37.0","postgresql-v1.36.1","postgresql-v1.36.0","postgresql-v1.35.0","postgresql-v1.34.0","postgresql-v1.33.3","postgresql-latest","mysql-v1.39.5","mysql-v1.39.4","mysql-v1.39.3","mysql-v1.39.2","mysql-v1.39.1","mysql-v1.39.0","mysql-v1.38.0","mysql-v1.37.0","mysql-v1.36.1","mysql-v1.36.0","mysql-v1.35.0","mysql-v1.34.0","mysql-v1.33.3","mysql-latest"]},{"name":"umami","image":"ghcr.io/umami-software/umami","tags":["postgresql-v1.39.5","postgresql-v1.39.4","postgresql-v1.39.3","postgresql-v1.39.2","postgresql-v1.39.1","postgresql-v1.39.0","postgresql-v1.38.0","postgresql-v1.37.0","postgresql-v1.36.1","postgresql-v1.36.0","postgresql-v1.35.0","postgresql-v1.34.0","postgresql-v1.33.3","postgresql-latest","mysql-v1.39.5","mysql-v1.39.4","mysql-v1.39.3","mysql-v1.39.2","mysql-v1.39.1","mysql-v1.39.0","mysql-v1.38.0","mysql-v1.37.0","mysql-v1.36.1","mysql-v1.36.0","mysql-v1.35.0","mysql-v1.34.0","mysql-v1.33.3","mysql-latest"]},{"name":"uptimekuma","image":"louislam/uptime-kuma","tags":["1.9.2","1.9.1","1.9.0","1.8.0","1.7.3","1.7.1","1.7.0","1.6.3","1.6.2","1.6.1","1.6.0","1.5.3","1.5.2","1.5.0","1.3.1","1.2.0","1.18.5","1.18.4","1.18.3","1.18.2","1.18.1","1.18.0","1.17.1","1.17.0","1.16.1","1.16.0","1.15.1","1.15.0","1.14.1","1.14.0"]},{"name":"vaultwarden","image":"vaultwarden/server","tags":["1.26.0","1.25.2","1.25.1","1.25.0","1.24.0","1.23.1","1.23.0","1.22.2","1.22.1","1.22.0","1.21.0"]},{"name":"vscodeserver","image":"codercom/code-server","tags":["4.8.3","4.8.2","4.8.1","4.8.0","4.7.0","4.6.0","4.5.1","4.4.0","4.2.0","4.0.2","3.9.3","3.9.1","3.8.1","3.7.4","3.7.2","3.7.0","3.6.1","3.5.0","3.4.0","3.3.0","3.2.0","3.11.1","3.10.2","3.10.0","3.1.1","3.1.0","3.0.2","3.0.0"]},{"name":"weblate","image":"weblate/weblate","tags":["latest","edge-2022-12-01-0295bd44d4d9da0e0836b9152319fba173a0825e","edge-2022-11-28-f28431a1e78f88bf49ccf539fbc00afe0925542d","edge-2022-11-26-558811de16025b83de43d2747f1fe209a5b829f1","edge-2022-11-23-4a1fe25c7b70e49156e02183a8deec3b357b9030","edge-2022-11-22-9a178e7f5c2e387329592a1dd7700671f64f6682","edge-2022-11-21-eb741ebad70211ecb1babdfd23e4f43c5a59fc7b","edge-2022-11-21-4580d37f616650cf5b0851fee051651f785e8852","edge-2022-11-21-0f74d6c4d3777dbf28affd09b45c69c85ed01d84","edge-2022-11-15-cad0a043b32c1ee61611ab258db0f01c5e6d718f","edge-2022-11-10-bf41db3afbab22384e103718094738dcfdc1a270","edge-2022-11-09-9bc90ce8b873778d2f486eccd0163bb1bb65ca6e","edge-2022-11-08-36e221037ff7097f8cd2c88d779135b6c7d3f363","edge-2022-11-08-3568e3c6759a9e9b779d98cb98393526d451466a","edge-2022-11-08-261d197970ca0679514d32ff783467972e807061","edge-2022-11-05-fa5cb203d854a11cc7850868a2890168afa3e7da","edge-2022-11-05-d93ae789eef8f065240f9fb6feb3edb236a7e6f8","edge-2022-11-05-8fc2be8e9d22e5ca2da2773488da7f72c5927ec3","edge-2022-11-05-85da67e88a113bed65530f0695ad4cddec0ed05a","edge-2022-11-05-3f4d77b6f2cb16bf008a4ef587e843ccb9c0c5d0","edge-2022-11-05-226eed520a2b32c3583c6e3247109ec8950764e7","edge-2022-11-03-487f3255cb89415fbe0769fa4b7bd2a9209deca6","edge-2022-11-02-e4171e0c5657ca38341cce8ac31f5cbdf25389eb","edge-2022-11-02-6d886c40cd62eb23d21f7c0a1840b4a7a4c51ad0","edge-2022-11-01-608df4dd95a2d1f76c15cddd9e116bb4c3229168","edge-2022-11-01-54957be78eb76f602ceae50c0b01b64b20402b2a","edge-2022-10-31-c55c7302a6c82a160ee9d711893c12d67ecd3b27","edge-2022-10-26-c69cfdd83ed1fad4a4d57398552b8c70894a6586","edge-2022-10-26-410b3aff37de5bbfacbc47642ce28b2518bee506","edge-2022-10-25-e09e2c29ed3748eb0fa248453635dd27768e8dd9"]},{"name":"wordpress-only","image":"library/wordpress","tags":["php8.1-fpm-alpine","php8.1-fpm","php8.1-apache","php8.1","php8.0-fpm-alpine","php8.0-fpm","php8.0-apache","php8.0","php7.4-fpm-alpine","php7.4-fpm","php7.4-apache","php7.4","php7.3-fpm-alpine","php7.3-fpm","php7.3-apache","php7.3","php7.2-fpm-alpine","php7.2-fpm","php7.2-apache","php7.2","php7.1-fpm-alpine","php7.1-fpm","php7.1-apache","php7.1","php7.0-fpm-alpine","php7.0-fpm","php7.0-apache","php7.0","php5.6-fpm-alpine","php5.6-fpm"]},{"name":"wordpress","image":"library/wordpress","tags":["php8.1-fpm-alpine","php8.1-fpm","php8.1-apache","php8.1","php8.0-fpm-alpine","php8.0-fpm","php8.0-apache","php8.0","php7.4-fpm-alpine","php7.4-fpm","php7.4-apache","php7.4","php7.3-fpm-alpine","php7.3-fpm","php7.3-apache","php7.3","php7.2-fpm-alpine","php7.2-fpm","php7.2-apache","php7.2","php7.1-fpm-alpine","php7.1-fpm","php7.1-apache","php7.1","php7.0-fpm-alpine","php7.0-fpm","php7.0-apache","php7.0","php5.6-fpm-alpine","php5.6-fpm"]}] \ No newline at end of file diff --git a/apps/api/templates.json b/apps/api/templates.json index 10930cde2..256cca609 100644 --- a/apps/api/templates.json +++ b/apps/api/templates.json @@ -1 +1 @@ -[{"templateVersion":"1.0.0","defaultVersion":"1.17","documentation":"https://docs.gitea.io","type":"gitea","name":"Gitea","description":"Gitea is a community managed lightweight code hosting solution written in Go.","labels":["storage","git"],"services":{"$$id":{"name":"Gitea","documentation":"https://docs.gitea.io","image":"gitea/gitea:$$core_version","volumes":["$$id-data:/data","/etc/timezone:/etc/timezone:ro","/etc/localtime:/etc/localtime:ro"],"environment":["USER_UID=1000","USER_GID=1000","DOMAIN=$$config_domain","SSH_DOMAIN=$$config_ssh_domain","ROOT_URL=$$config_root_url","SECRET_KEY=$$secret_secret_key","INTERNAL_TOKEN=$$secret_internal_token","SSH_PORT=22","START_SSH_SERVER=$$config_start_ssh_server"],"ports":["3000","22"],"proxy":[{"port":"22","hostPort":"$$config_hostport_ssh"}]}},"variables":[{"id":"$$config_hostport_ssh","name":"SSH_PORT","label":"SSH Port","defaultValue":"8022","description":"","required":true},{"id":"$$config_domain","name":"DOMAIN","label":"Domain","defaultValue":"$$generate_domain","description":""},{"id":"$$config_ssh_domain","name":"SSH_DOMAIN","label":"SSH Domain","defaultValue":"$$generate_domain","description":""},{"id":"$$config_start_ssh_server","name":"START_SSH_SERVER","label":"Start SSH Server","defaultValue":"true","description":""},{"id":"$$config_root_url","name":"ROOT_URL","label":"Root URL of Gitea","defaultValue":"$$generate_fqdn_slash","description":""},{"id":"$$secret_secret_key","name":"SECRET_KEY","label":"Secret Key","defaultValue":"$$generate_hex(32)","description":""},{"id":"$$secret_internal_token","name":"INTERNAL_TOKEN","label":"Internal JWT Token","defaultValue":"$$generate_token","description":""}]},{"templateVersion":"1.0.0","defaultVersion":"20.0","documentation":"https://www.keycloak.org/documentation","type":"keycloak","name":"Keycloak","description":"Keycloak provides user federation, strong authentication, user management, fine-grained authorization, and more.","labels":["authentication","authorization","oidconnect","saml2"],"services":{"$$id":{"name":"Keycloak","command":"start --db=postgres --features=token-exchange --import-realm","depends_on":["$$id-postgresql"],"image":"quay.io/keycloak/keycloak:$$core_version","volumes":["$$id-import:/opt/keycloak/data/import"],"environment":["KC_HEALTH_ENABLED=true","KC_PROXY=edge","KC_DB=postgres","KC_HOSTNAME=$$config_keycloak_domain","KEYCLOAK_ADMIN=$$config_admin_user","KEYCLOAK_ADMIN_PASSWORD=$$secret_keycloak_admin_password","KC_DB_PASSWORD=$$secret_postgres_password","KC_DB_USERNAME=$$config_postgres_user","KC_DB_URL=$$secret_keycloak_database_url"],"ports":["8080"]},"$$id-postgresql":{"name":"PostgreSQL","depends_on":[],"image":"postgres:14-alpine","volumes":["$$id-postgresql-data:/var/lib/postgresql/data"],"environment":["POSTGRES_USER=$$config_postgres_user","POSTGRES_PASSWORD=$$secret_postgres_password","POSTGRES_DB=$$config_postgres_db"],"ports":[]}},"variables":[{"id":"$$config_keycloak_domain","name":"KEYCLOAK_DOMAIN","label":"Keycloak Domain","defaultValue":"$$generate_domain","description":""},{"id":"$$secret_keycloak_database_url","name":"KEYCLOAK_DATABASE_URL","label":"Keycloak Database Url","defaultValue":"jdbc:postgresql://$$id-postgresql:5432/$$config_postgres_db","description":""},{"id":"$$config_admin_user","name":"KEYCLOAK_ADMIN","label":"Keycloak Admin User","defaultValue":"$$generate_username","description":""},{"id":"$$secret_keycloak_admin_password","name":"KEYCLOAK_ADMIN_PASSWORD","label":"Keycloak Admin Password","defaultValue":"$$generate_password","description":"","showOnConfiguration":true},{"id":"$$config_postgres_user","main":"$$id-postgresql","name":"POSTGRES_USER","label":"PostgreSQL User","defaultValue":"$$generate_username","description":""},{"id":"$$secret_postgres_password","main":"$$id-postgresql","name":"POSTGRES_PASSWORD","label":"PostgreSQL Password","defaultValue":"$$generate_password","description":"","showOnConfiguration":true},{"id":"$$config_postgres_db","main":"$$id-postgresql","name":"POSTGRES_DB","label":"PostgreSQL Database","defaultValue":"keycloak","description":""}]},{"templateVersion":"1.0.0","defaultVersion":"v3.6","documentation":"https://github.com/freyacodes/Lavalink","description":"Standalone audio sending node based on Lavaplayer.","type":"lavalink","name":"Lavalink","labels":["discord","discord bot","audio","lavalink","jda"],"services":{"$$id":{"name":"Lavalink","image":"fredboat/lavalink:$$core_version","environment":[],"volumes":["$$id-lavalink:/lavalink"],"ports":["2333"],"files":[{"location":"/opt/Lavalink/application.yml","content":"server:\n port: $$config_port\n address: 0.0.0.0\nlavalink:\n server:\n password: \"$$secret_password\"\n sources:\n youtube: true\n bandcamp: true\n soundcloud: true\n twitch: true\n vimeo: true\n http: true\n local: false\n\nlogging:\n file:\n path: ./logs/\n\n level:\n root: INFO\n lavalink: INFO\n\n logback:\n rollingpolicy:\n max-file-size: 1GB\n max-history: 30"}]}},"variables":[{"id":"$$config_port","name":"PORT","label":"Port","defaultValue":"2333","required":true},{"id":"$$secret_password","name":"PASSWORD","label":"Password","defaultValue":"$$generate_password","required":true}]},{"templateVersion":"1.0.0","defaultVersion":"v1.8.6","documentation":"https://docs.appsmith.com/getting-started/setup/instance-configuration/","type":"appsmith","name":"Appsmith","description":"Fastest way to build internal apps over any database or API.","services":{"$$id":{"image":"appsmith/appsmith-ce:$$core_version","environment":["APPSMITH_MAIL_ENABLED=$$config_appsmith_mail_enabled","APPSMITH_DISABLE_TELEMETRY=$$config_appsmith_disable_telemetry","APPSMITH_DISABLE_INTERCOM=$$config_appsmith_disable_intercom"],"volumes":["$$id-stacks-data:/appsmith-stacks"],"ports":["80"]}},"variables":[{"id":"$$config_appsmith_mail_enabled","name":"APPSMITH_MAIL_ENABLED","label":"Enable Mail","defaultValue":"false","description":""},{"id":"$$config_appsmith_disable_telemetry","name":"APPSMITH_DISABLE_TELEMETRY","label":"Disable Telemetry","defaultValue":"true","description":""},{"id":"$$config_appsmith_disable_intercom","name":"APPSMITH_DISABLE_INTERCOM","label":"Disable Intercom","defaultValue":"true","description":""}]},{"templateVersion":"1.0.0","defaultVersion":"0.56.2","documentation":"https://hub.docker.com/r/zadam/trilium","description":"A hierarchical note taking application with focus on building large personal knowledge bases.","labels":["personal","knowledge","notes","wiki"],"type":"trilium","name":"Trilium Notes","services":{"$$id":{"image":"zadam/trilium:$$core_version","environment":[],"volumes":["$$id-trilium:/home/node/trilium-data"],"ports":["8080"]}},"variables":[]},{"templateVersion":"1.0.0","defaultVersion":"1.9.2","documentation":"https://hub.docker.com/r/louislam/uptime-kuma","description":"A free & fancy self-hosted monitoring tool.","labels":["uptime"],"type":"uptimekuma","name":"UptimeKuma","services":{"$$id":{"image":"louislam/uptime-kuma:$$core_version","environment":[],"volumes":["$$id-uptimekuma:/app/data"],"ports":["3001"]}},"variables":[]},{"templateVersion":"1.0.0","defaultVersion":"5.8","documentation":"https://hub.docker.com/r/silviof/docker-languagetool","description":"A multilingual grammar, style and spell checker.","type":"languagetool","name":"LanguageTool","services":{"$$id":{"image":"silviof/docker-languagetool:$$core_version","environment":[],"volumes":["$$id-ngrams:/ngrams"],"ports":["8010"]}},"variables":[]},{"templateVersion":"1.0.0","defaultVersion":"1.26.0","documentation":"https://hub.docker.com/r/vaultwarden/server","description":"Bitwarden compatible server written in Rust.","type":"vaultwarden","name":"VaultWarden","labels":["bitwarden","password manager"],"services":{"$$id":{"image":"vaultwarden/server:$$core_version","environment":[],"volumes":["$$id-data:/data"],"ports":["80"]}},"variables":[]},{"templateVersion":"1.0.0","defaultVersion":"9.2.3","documentation":"https://hub.docker.com/r/grafana/grafana","type":"grafana","name":"Grafana","description":"Grafana allows you to query, visualize, alert on and understand your metrics.","labels":["monitoring","metrics","dashboard"],"services":{"$$id":{"image":"grafana/grafana:$$core_version","environment":[],"volumes":["$$id-config:/etc/grafana","$$id-grafana:/var/lib/grafana"],"ports":["3000"]}},"variables":[]},{"templateVersion":"1.0.0","defaultVersion":"1.0.3","documentation":"https://appwrite.io/docs","type":"appwrite","name":"Appwrite","description":"Secure Backend Server for Web, Mobile & Flutter Developers.","labels":["serverless","backend","storage","api"],"services":{"$$id":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_WORKER_PER_CORE=$$config__app_worker_per_core","_APP_LOCALE=$$config__app_locale","_APP_CONSOLE_WHITELIST_ROOT=$$config__app_console_whitelist_root","_APP_CONSOLE_WHITELIST_EMAILS=$$config__app_console_whitelist_emails","_APP_CONSOLE_WHITELIST_IPS=$$config__app_console_whitelist_ips","_APP_SYSTEM_EMAIL_NAME=$$config__app_system_email_name","_APP_SYSTEM_EMAIL_ADDRESS=$$config__app_system_email_address","_APP_SYSTEM_SECURITY_EMAIL_ADDRESS=$$config__app_system_security_email_address","_APP_SYSTEM_RESPONSE_FORMAT=$$config__app_system_response_format","_APP_OPTIONS_ABUSE=$$config__app_options_abuse","_APP_OPTIONS_FORCE_HTTPS=$$config__app_options_force_https","_APP_OPENSSL_KEY_V1=$$secret__app_openssl_key_v1","_APP_DOMAIN=$$config__app_domain","_APP_DOMAIN_TARGET=$$config__app_domain_target","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_REDIS_USER=$$config__app_redis_user","_APP_REDIS_PASS=$$secret__app_redis_pass","_APP_DB_HOST=$$config__app_db_host","_APP_DB_PORT=$$config__app_db_port","_APP_DB_SCHEMA=$$config__app_db_schema","_APP_DB_USER=$$config__app_db_user","_APP_DB_PASS=$$secret__app_db_pass","_APP_SMTP_HOST=$$config__app_smtp_host","_APP_SMTP_PORT=$$config__app_smtp_port","_APP_SMTP_SECURE=$$config__app_smtp_secure","_APP_SMTP_USERNAME=$$config__app_smtp_username","_APP_SMTP_PASSWORD=$$secret__app_smtp_password","_APP_USAGE_STATS=$$config__app_usage_stats","_APP_INFLUXDB_HOST=$$config__app_influxdb_host","_APP_INFLUXDB_PORT=$$config__app_influxdb_port","_APP_STORAGE_LIMIT=$$config__app_storage_limit","_APP_STORAGE_PREVIEW_LIMIT=$$config__app_storage_preview_limit","_APP_STORAGE_ANTIVIRUS=$$config__app_storage_antivirus_enabled","_APP_STORAGE_ANTIVIRUS_HOST=$$config__app_storage_antivirus_host","_APP_STORAGE_ANTIVIRUS_PORT=$$config__app_storage_antivirus_port","_APP_STORAGE_DEVICE=$$config__app_storage_device","_APP_STORAGE_S3_ACCESS_KEY=$$secret__app_storage_s3_access_key","_APP_STORAGE_S3_SECRET=$$secret__app_storage_s3_secret","_APP_STORAGE_S3_REGION=$$config__app_storage_s3_region","_APP_STORAGE_S3_BUCKET=$$config__app_storage_s3_bucket","_APP_STORAGE_DO_SPACES_ACCESS_KEY=$$secret__app_storage_do_spaces_access_key","_APP_STORAGE_DO_SPACES_SECRET=$$secret__app_storage_do_spaces_secret","_APP_STORAGE_DO_SPACES_REGION=$$config__app_storage_do_spaces_region","_APP_STORAGE_DO_SPACES_BUCKET=$$config__app_storage_do_spaces_bucket","_APP_STORAGE_BACKBLAZE_ACCESS_KEY=$$secret__app_storage_backblaze_access_key","_APP_STORAGE_BACKBLAZE_SECRET=$$secret__app_storage_backblaze_secret","_APP_STORAGE_BACKBLAZE_REGION=$$config__app_storage_backblaze_region","_APP_STORAGE_BACKBLAZE_BUCKET=$$config__app_storage_backblaze_bucket","_APP_STORAGE_LINODE_ACCESS_KEY=$$secret__app_storage_linode_access_key","_APP_STORAGE_LINODE_SECRET=$$secret__app_storage_linode_secret","_APP_STORAGE_LINODE_REGION=$$config__app_storage_linode_region","_APP_STORAGE_LINODE_BUCKET=$$config__app_storage_linode_bucket","_APP_STORAGE_WASABI_ACCESS_KEY=$$secret__app_storage_wasabi_access_key","_APP_STORAGE_WASABI_SECRET=$$secret__app_storage_wasabi_secret","_APP_STORAGE_WASABI_REGION=$$config__app_storage_wasabi_region","_APP_STORAGE_WASABI_BUCKET=$$config__app_storage_wasabi_bucket","_APP_FUNCTIONS_SIZE_LIMIT=$$config__app_functions_size_limit","_APP_FUNCTIONS_TIMEOUT=$$config__app_functions_timeout","_APP_FUNCTIONS_BUILD_TIMEOUT=$$config__app_functions_build_timeout","_APP_FUNCTIONS_CONTAINERS=$$config__app_functions_containers","_APP_FUNCTIONS_CPUS=$$config__app_functions_cpus","_APP_FUNCTIONS_MEMORY=$$config__app_functions_memory_allocated","_APP_FUNCTIONS_MEMORY_SWAP=$$config__app_functions_memory_swap","_APP_FUNCTIONS_RUNTIMES=$$config__app_functions_runtimes","_APP_EXECUTOR_SECRET=$$secret__app_executor_secret","_APP_EXECUTOR_HOST=$$config__app_executor_host","_APP_LOGGING_PROVIDER=$$config__app_logging_provider","_APP_LOGGING_CONFIG=$$config__app_logging_config","_APP_STATSD_HOST=$$config__app_statsd_host","_APP_STATSD_PORT=$$config__app_statsd_port","_APP_MAINTENANCE_INTERVAL=$$config__app_maintenance_interval","_APP_MAINTENANCE_RETENTION_EXECUTION=$$config__app_maintenance_retention_execution","_APP_MAINTENANCE_RETENTION_CACHE=$$config__app_maintenance_retention_cache","_APP_MAINTENANCE_RETENTION_ABUSE=$$config__app_maintenance_retention_abuse","_APP_MAINTENANCE_RETENTION_AUDIT=$$config__app_maintenance_retention_audit","_APP_SMS_PROVIDER=$$config__app_sms_provider","_APP_SMS_FROM=$$config__app_sms_from","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":["$$id-uploads:/storage/uploads","$$id-cache:/storage/cache","$$id-config:/storage/config","$$id-certificates:/storage/certificates","$$id-functions:/storage/functions"],"ports":["80"],"proxy":[{"port":"80"}]},"$$id-executor":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_FUNCTIONS_TIMEOUT=$$config__app_functions_timeout","_APP_FUNCTIONS_BUILD_TIMEOUT=$$config__app_functions_build_timeout","_APP_FUNCTIONS_CONTAINERS=$$config__app_functions_containers","_APP_FUNCTIONS_RUNTIMES=$$config__app_functions_runtimes","_APP_FUNCTIONS_CPUS=$$config__app_functions_cpus","_APP_FUNCTIONS_MEMORY=$$config__app_functions_memory_allocated","_APP_FUNCTIONS_MEMORY_SWAP=$$config__app_functions_memory_swap","_APP_FUNCTIONS_INACTIVE_THRESHOLD=$$config__app_functions_inactive_threshold","_APP_EXECUTOR_SECRET=$$secret__app_executor_secret","_APP_LOGGING_PROVIDER=$$config__app_logging_provider","_APP_LOGGING_CONFIG=$$config__app_logging_config","_APP_STORAGE_DEVICE=$$config__app_storage_device","_APP_STORAGE_S3_ACCESS_KEY=$$secret__app_storage_s3_access_key","_APP_STORAGE_S3_SECRET=$$secret__app_storage_s3_secret","_APP_STORAGE_S3_REGION=$$config__app_storage_s3_region","_APP_STORAGE_S3_BUCKET=$$config__app_storage_s3_bucket","_APP_STORAGE_DO_SPACES_ACCESS_KEY=$$secret__app_storage_do_spaces_access_key","_APP_STORAGE_DO_SPACES_SECRET=$$secret__app_storage_do_spaces_secret","_APP_STORAGE_DO_SPACES_REGION=$$config__app_storage_do_spaces_region","_APP_STORAGE_DO_SPACES_BUCKET=$$config__app_storage_do_spaces_bucket","_APP_STORAGE_BACKBLAZE_ACCESS_KEY=$$secret__app_storage_backblaze_access_key","_APP_STORAGE_BACKBLAZE_SECRET=$$secret__app_storage_backblaze_secret","_APP_STORAGE_BACKBLAZE_REGION=$$config__app_storage_backblaze_region","_APP_STORAGE_BACKBLAZE_BUCKET=$$config__app_storage_backblaze_bucket","_APP_STORAGE_LINODE_ACCESS_KEY=$$secret__app_storage_linode_access_key","_APP_STORAGE_LINODE_SECRET=$$secret__app_storage_linode_secret","_APP_STORAGE_LINODE_REGION=$$config__app_storage_linode_region","_APP_STORAGE_LINODE_BUCKET=$$config__app_storage_linode_bucket","_APP_STORAGE_WASABI_ACCESS_KEY=$$secret__app_storage_wasabi_access_key","_APP_STORAGE_WASABI_SECRET=$$secret__app_storage_wasabi_secret","_APP_STORAGE_WASABI_REGION=$$config__app_storage_wasabi_region","_APP_STORAGE_WASABI_BUCKET=$$config__app_storage_wasabi_bucket","DOCKERHUB_PULL_USERNAME=$$config_dockerhub_pull_username","DOCKERHUB_PULL_PASSWORD=$$secret_dockerhub_pull_password","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":["$$id-functions:/storage/functions","$$id-builds:/storage/builds","/var/run/docker.sock:/var/run/docker.sock"],"entrypoint":"executor"},"$$id-influxdb":{"image":"appwrite/influxdb:1.5.0","environment":[],"volumes":["$$id-influxdb:/var/lib/influxdb"]},"$$id-maintenance":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_OPENSSL_KEY_V1=$$secret__app_openssl_key_v1","_APP_DOMAIN=$$config__app_domain","_APP_DOMAIN_TARGET=$$config__app_domain_target","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_REDIS_USER=$$config__app_redis_user","_APP_REDIS_PASS=$$secret__app_redis_pass","_APP_DB_HOST=$$config__app_db_host","_APP_DB_PORT=$$config__app_db_port","_APP_DB_SCHEMA=$$config__app_db_schema","_APP_DB_USER=$$config__app_db_user","_APP_DB_PASS=$$secret__app_db_pass","_APP_MAINTENANCE_INTERVAL=$$config__app_maintenance_interval","_APP_MAINTENANCE_RETENTION_EXECUTION=$$config__app_maintenance_retention_execution","_APP_MAINTENANCE_RETENTION_CACHE=$$config__app_maintenance_retention_cache","_APP_MAINTENANCE_RETENTION_ABUSE=$$config__app_maintenance_retention_abuse","_APP_MAINTENANCE_RETENTION_AUDIT=$$config__app_maintenance_retention_audit","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":[],"entrypoint":"maintenance"},"$$id-mariadb":{"image":"mariadb:10.7","command":"--innodb-flush-method fsync","environment":["MARIADB_ROOT_PASSWORD=$$secret__app_db_root_pass","MARIADB_DATABASE=$$config__app_db_schema","MARIADB_USER=$$config__app_db_user","MARIADB_PASSWORD=$$secret__app_db_pass","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":["$$id-mariadb:/var/lib/mysql"]},"$$id-realtime":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_WORKER_PER_CORE=$$config__app_worker_per_core","_APP_OPTIONS_ABUSE=$$config__app_options_abuse","_APP_OPENSSL_KEY_V1=$$secret__app_openssl_key_v1","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_DB_HOST=$$config__app_db_host","_APP_DB_PORT=$$config__app_db_port","_APP_DB_SCHEMA=$$config__app_db_schema","_APP_DB_USER=$$config__app_db_user","_APP_DB_PASS=$$secret__app_db_pass","_APP_USAGE_STATS=$$config__app_usage_stats","_APP_LOGGING_PROVIDER=$$config__app_logging_provider","_APP_LOGGING_CONFIG=$$config__app_logging_config","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":[],"entrypoint":"realtime","proxy":[{"port":"80","pathPrefix":"/v1/realtime"}]},"$$id-redis":{"image":"redis:7.0.4-alpine","command":"--maxmemory 512mb --maxmemory-policy allkeys-lru --maxmemory-samples 5","environment":[],"volumes":["$$id-redis:/data"]},"$$id-schedule":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_REDIS_USER=$$config__app_redis_user","_APP_REDIS_PASS=$$secret__app_redis_pass","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":[],"entrypoint":"schedule"},"$$id-telegraf":{"image":"appwrite/telegraf:1.4.0","environment":["_APP_INFLUXDB_HOST=$$config__app_influxdb_host","_APP_INFLUXDB_PORT=$$config__app_influxdb_port","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":["$$id-influxdb:/var/lib/influxdb"]},"$$id-usage-database":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_OPENSSL_KEY_V1=$$secret__app_openssl_key_v1","_APP_DB_HOST=$$config__app_db_host","_APP_DB_PORT=$$config__app_db_port","_APP_DB_SCHEMA=$$config__app_db_schema","_APP_DB_USER=$$config__app_db_user","_APP_DB_PASS=$$secret__app_db_pass","_APP_INFLUXDB_HOST=$$config__app_influxdb_host","_APP_INFLUXDB_PORT=$$config__app_influxdb_port","_APP_USAGE_TIMESERIES_INTERVAL=$$config__app_usage_timeseries_interval","_APP_USAGE_DATABASE_INTERVAL=$$config__app_usage_database_interval","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_REDIS_USER=$$config__app_redis_user","_APP_REDIS_PASS=$$secret__app_redis_pass","_APP_LOGGING_PROVIDER=$$config__app_logging_provider","_APP_LOGGING_CONFIG=$$config__app_logging_config","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":[],"entrypoint":"usage --type database"},"$$id-usage":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_OPENSSL_KEY_V1=$$secret__app_openssl_key_v1","_APP_DB_HOST=$$config__app_db_host","_APP_DB_PORT=$$config__app_db_port","_APP_DB_SCHEMA=$$config__app_db_schema","_APP_DB_USER=$$config__app_db_user","_APP_DB_PASS=$$secret__app_db_pass","_APP_INFLUXDB_HOST=$$config__app_influxdb_host","_APP_INFLUXDB_PORT=$$config__app_influxdb_port","_APP_USAGE_TIMESERIES_INTERVAL=$$config__app_usage_timeseries_interval","_APP_USAGE_DATABASE_INTERVAL=$$config__app_usage_database_interval","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_REDIS_USER=$$config__app_redis_user","_APP_REDIS_PASS=$$secret__app_redis_pass","_APP_LOGGING_PROVIDER=$$config__app_logging_provider","_APP_LOGGING_CONFIG=$$config__app_logging_config","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":[],"entrypoint":"usage --type timeseries"},"$$id-worker-audits":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_OPENSSL_KEY_V1=$$secret__app_openssl_key_v1","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_REDIS_USER=$$config__app_redis_user","_APP_REDIS_PASS=$$secret__app_redis_pass","_APP_DB_HOST=$$config__app_db_host","_APP_DB_PORT=$$config__app_db_port","_APP_DB_SCHEMA=$$config__app_db_schema","_APP_DB_USER=$$config__app_db_user","_APP_DB_PASS=$$secret__app_db_pass","_APP_LOGGING_PROVIDER=$$config__app_logging_provider","_APP_LOGGING_CONFIG=$$config__app_logging_config","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":[],"entrypoint":"worker-audits"},"$$id-worker-builds":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_OPENSSL_KEY_V1=$$secret__app_openssl_key_v1","_APP_EXECUTOR_SECRET=$$secret__app_executor_secret","_APP_EXECUTOR_HOST=$$config__app_executor_host","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_REDIS_USER=$$config__app_redis_user","_APP_REDIS_PASS=$$secret__app_redis_pass","_APP_DB_HOST=$$config__app_db_host","_APP_DB_PORT=$$config__app_db_port","_APP_DB_SCHEMA=$$config__app_db_schema","_APP_DB_USER=$$config__app_db_user","_APP_DB_PASS=$$secret__app_db_pass","_APP_LOGGING_PROVIDER=$$config__app_logging_provider","_APP_LOGGING_CONFIG=$$config__app_logging_config","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":[],"entrypoint":"worker-builds"},"$$id-worker-certificates":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_OPENSSL_KEY_V1=$$secret__app_openssl_key_v1","_APP_DOMAIN=$$config__app_domain","_APP_DOMAIN_TARGET=$$config__app_domain_target","_APP_SYSTEM_SECURITY_EMAIL_ADDRESS=$$config__app_system_security_email_address","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_REDIS_USER=$$config__app_redis_user","_APP_REDIS_PASS=$$secret__app_redis_pass","_APP_DB_HOST=$$config__app_db_host","_APP_DB_PORT=$$config__app_db_port","_APP_DB_SCHEMA=$$config__app_db_schema","_APP_DB_USER=$$config__app_db_user","_APP_DB_PASS=$$secret__app_db_pass","_APP_LOGGING_PROVIDER=$$config__app_logging_provider","_APP_LOGGING_CONFIG=$$config__app_logging_config","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":["$$id-config:/storage/config","$$id-certificates:/storage/certificates"],"entrypoint":"worker-certificates"},"$$id-worker-databases":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_OPENSSL_KEY_V1=$$secret__app_openssl_key_v1","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_REDIS_USER=$$config__app_redis_user","_APP_REDIS_PASS=$$secret__app_redis_pass","_APP_DB_HOST=$$config__app_db_host","_APP_DB_PORT=$$config__app_db_port","_APP_DB_SCHEMA=$$config__app_db_schema","_APP_DB_USER=$$config__app_db_user","_APP_DB_PASS=$$secret__app_db_pass","_APP_LOGGING_PROVIDER=$$config__app_logging_provider","_APP_LOGGING_CONFIG=$$config__app_logging_config","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":[],"entrypoint":"worker-databases"},"$$id-worker-deletes":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_OPENSSL_KEY_V1=$$secret__app_openssl_key_v1","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_REDIS_USER=$$config__app_redis_user","_APP_REDIS_PASS=$$secret__app_redis_pass","_APP_DB_HOST=$$config__app_db_host","_APP_DB_PORT=$$config__app_db_port","_APP_DB_SCHEMA=$$config__app_db_schema","_APP_DB_USER=$$config__app_db_user","_APP_DB_PASS=$$secret__app_db_pass","_APP_STORAGE_DEVICE=$$config__app_storage_device","_APP_STORAGE_S3_ACCESS_KEY=$$secret__app_storage_s3_access_key","_APP_STORAGE_S3_SECRET=$$secret__app_storage_s3_secret","_APP_STORAGE_S3_REGION=$$config__app_storage_s3_region","_APP_STORAGE_S3_BUCKET=$$config__app_storage_s3_bucket","_APP_STORAGE_DO_SPACES_ACCESS_KEY=$$secret__app_storage_do_spaces_access_key","_APP_STORAGE_DO_SPACES_SECRET=$$secret__app_storage_do_spaces_secret","_APP_STORAGE_DO_SPACES_REGION=$$config__app_storage_do_spaces_region","_APP_STORAGE_DO_SPACES_BUCKET=$$config__app_storage_do_spaces_bucket","_APP_STORAGE_BACKBLAZE_ACCESS_KEY=$$secret__app_storage_backblaze_access_key","_APP_STORAGE_BACKBLAZE_SECRET=$$secret__app_storage_backblaze_secret","_APP_STORAGE_BACKBLAZE_REGION=$$config__app_storage_backblaze_region","_APP_STORAGE_BACKBLAZE_BUCKET=$$config__app_storage_backblaze_bucket","_APP_STORAGE_LINODE_ACCESS_KEY=$$secret__app_storage_linode_access_key","_APP_STORAGE_LINODE_SECRET=$$secret__app_storage_linode_secret","_APP_STORAGE_LINODE_REGION=$$config__app_storage_linode_region","_APP_STORAGE_LINODE_BUCKET=$$config__app_storage_linode_bucket","_APP_STORAGE_WASABI_ACCESS_KEY=$$secret__app_storage_wasabi_access_key","_APP_STORAGE_WASABI_SECRET=$$secret__app_storage_wasabi_secret","_APP_STORAGE_WASABI_REGION=$$config__app_storage_wasabi_region","_APP_STORAGE_WASABI_BUCKET=$$config__app_storage_wasabi_bucket","_APP_LOGGING_PROVIDER=$$config__app_logging_provider","_APP_LOGGING_CONFIG=$$config__app_logging_config","_APP_EXECUTOR_SECRET=$$secret__app_executor_secret","_APP_EXECUTOR_HOST=$$config__app_executor_host","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":["$$id-uploads:/storage/uploads","$$id-cache:/storage/cache","$$id-functions:/storage/functions","$$id-builds:/storage/builds","$$id-certificates:/storage/certificates"],"entrypoint":"worker-deletes"},"$$id-worker-functions":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_OPENSSL_KEY_V1=$$secret__app_openssl_key_v1","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_REDIS_USER=$$config__app_redis_user","_APP_REDIS_PASS=$$secret__app_redis_pass","_APP_DB_HOST=$$config__app_db_host","_APP_DB_PORT=$$config__app_db_port","_APP_DB_SCHEMA=$$config__app_db_schema","_APP_DB_USER=$$config__app_db_user","_APP_DB_PASS=$$secret__app_db_pass","_APP_FUNCTIONS_TIMEOUT=$$config__app_functions_timeout","_APP_EXECUTOR_SECRET=$$secret__app_executor_secret","_APP_EXECUTOR_HOST=$$config__app_executor_host","_APP_USAGE_STATS=$$config__app_usage_stats","DOCKERHUB_PULL_USERNAME=$$config_dockerhub_pull_username","DOCKERHUB_PULL_PASSWORD=$$secret_dockerhub_pull_password","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":[],"entrypoint":"worker-functions"},"$$id-worker-mails":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_OPENSSL_KEY_V1=$$secret__app_openssl_key_v1","_APP_SYSTEM_EMAIL_NAME=$$config__app_system_email_name","_APP_SYSTEM_EMAIL_ADDRESS=$$config__app_system_email_address","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_REDIS_USER=$$config__app_redis_user","_APP_REDIS_PASS=$$secret__app_redis_pass","_APP_SMTP_HOST=$$config__app_smtp_host","_APP_SMTP_PORT=$$config__app_smtp_port","_APP_SMTP_SECURE=$$config__app_smtp_secure","_APP_SMTP_USERNAME=$$config__app_smtp_username","_APP_SMTP_PASSWORD=$$secret__app_smtp_password","_APP_LOGGING_PROVIDER=$$config__app_logging_provider","_APP_LOGGING_CONFIG=$$config__app_logging_config","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":[],"entrypoint":"worker-mails"},"$$id-worker-messaging":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_REDIS_USER=$$config__app_redis_user","_APP_REDIS_PASS=$$secret__app_redis_pass","_APP_SMS_PROVIDER=$$config__app_sms_provider","_APP_SMS_FROM=$$config__app_sms_from","_APP_LOGGING_PROVIDER=$$config__app_logging_provider","_APP_LOGGING_CONFIG=$$config__app_logging_config","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":[],"entrypoint":"worker-messaging"},"$$id-worker-webhooks":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_OPENSSL_KEY_V1=$$secret__app_openssl_key_v1","_APP_SYSTEM_SECURITY_EMAIL_ADDRESS=$$config__app_system_security_email_address","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_REDIS_USER=$$config__app_redis_user","_APP_REDIS_PASS=$$secret__app_redis_pass","_APP_LOGGING_PROVIDER=$$config__app_logging_provider","_APP_LOGGING_CONFIG=$$config__app_logging_config","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":[],"entrypoint":"worker-webhooks"}},"variables":[{"id":"$$config__app_influxdb_host","name":"_APP_INFLUXDB_HOST","label":"InfluxDB | _APP_INFLUXDB_HOST","defaultValue":"$$id-influxdb","description":""},{"id":"$$config__app_influxdb_port","name":"_APP_INFLUXDB_PORT","label":"InfluxDB | _APP_INFLUXDB_PORT","defaultValue":"8086","description":"InfluxDB server TCP port."},{"id":"$$config__app_env","name":"_APP_ENV","label":"General | _APP_ENV","defaultValue":"production","description":"Set your server running environment."},{"id":"$$config__app_worker_per_core","name":"_APP_WORKER_PER_CORE","label":"General | _APP_WORKER_PER_CORE","defaultValue":"6","description":"Internal Worker per core for the API, Realtime and Executor containers. Can be configured to optimize performance."},{"id":"$$config__app_locale","name":"_APP_LOCALE","label":"General | _APP_LOCALE","defaultValue":"en","description":"Set your Appwrite's locale. By default, the locale is set to 'en'."},{"id":"$$config__app_console_whitelist_root","name":"_APP_CONSOLE_WHITELIST_ROOT","label":"General | _APP_CONSOLE_WHITELIST_ROOT","defaultValue":"enabled","description":"This option allows you to disable the creation of new users on the Appwrite console. When enabled only 1 user will be able to use the registration form. New users can be added by inviting them to your project. By default this option is enabled."},{"id":"$$config__app_console_whitelist_emails","name":"_APP_CONSOLE_WHITELIST_EMAILS","label":"General | _APP_CONSOLE_WHITELIST_EMAILS","defaultValue":"","description":"This option allows you to limit creation of new users on the Appwrite console. This option is very useful for small teams or sole developers. To enable it, pass a list of allowed email addresses separated by a comma."},{"id":"$$config__app_console_whitelist_ips","name":"_APP_CONSOLE_WHITELIST_IPS","label":"General | _APP_CONSOLE_WHITELIST_IPS","defaultValue":"","description":"This last option allows you to limit creation of users in Appwrite console for users sharing the same set of IP addresses. This option is very useful for team working with a VPN service or a company IP.\\n\\nTo enable/activate this option, pass a list of allowed IP addresses separated by a comma."},{"id":"$$config__app_system_email_name","name":"_APP_SYSTEM_EMAIL_NAME","label":"General | _APP_SYSTEM_EMAIL_NAME","defaultValue":"Appwrite","description":"This is the sender name value that will appear on email messages sent to developers from the Appwrite console. You can use url encoded strings for spaces and special chars."},{"id":"$$config__app_system_email_address","name":"_APP_SYSTEM_EMAIL_ADDRESS","label":"General | _APP_SYSTEM_EMAIL_ADDRESS","defaultValue":"team@appwrite.io","description":"This is the sender email address that will appear on email messages sent to developers from the Appwrite console. You should choose an email address that is allowed to be used from your SMTP server to avoid the server email ending in the users' SPAM folders."},{"id":"$$config__app_system_security_email_address","name":"_APP_SYSTEM_SECURITY_EMAIL_ADDRESS","label":"General | _APP_SYSTEM_SECURITY_EMAIL_ADDRESS","defaultValue":"certs@appwrite.io","description":"This is the email address used to issue SSL certificates for custom domains or the user agent in your webhooks payload."},{"id":"$$config__app_system_response_format","name":"_APP_SYSTEM_RESPONSE_FORMAT","label":"General | _APP_SYSTEM_RESPONSE_FORMAT","defaultValue":"","description":"Use this environment variable to set the default Appwrite HTTP response format to support an older version of Appwrite. This option is useful to overcome breaking changes between versions. You can also use the X-Appwrite-Response-Format HTTP request header to overwrite the response for a specific request. This variable accepts any valid Appwrite version. To use the current version format, leave the value of the variable empty."},{"id":"$$config__app_options_abuse","name":"_APP_OPTIONS_ABUSE","label":"General | _APP_OPTIONS_ABUSE","defaultValue":"enabled","description":"Allows you to disable abuse checks and API rate limiting. By default, set to 'enabled'. To cancel the abuse checking, set to 'disabled'. It is not recommended to disable this check-in a production environment."},{"id":"$$config__app_options_force_https","name":"_APP_OPTIONS_FORCE_HTTPS","label":"General | _APP_OPTIONS_FORCE_HTTPS","defaultValue":"disabled","description":"Allows you to force HTTPS connection to your API. This feature redirects any HTTP call to HTTPS and adds the 'Strict-Transport-Security' header to all HTTP responses."},{"id":"$$secret__app_openssl_key_v1","name":"_APP_OPENSSL_KEY_V1","label":"General | _APP_OPENSSL_KEY_V1","defaultValue":"$$generate_hex(256)","description":"This is your server private secret key that is used to encrypt all sensitive data on your server. Appwrite server encrypts all secret data on your server like webhooks, HTTP passwords, user sessions, and storage files. Keep it a secret and have a backup for it."},{"id":"$$config__app_domain","name":"_APP_DOMAIN","label":"General | _APP_DOMAIN","defaultValue":"$$generate_domain","description":"Your Appwrite domain address. When setting a public suffix domain, Appwrite will attempt to issue a valid SSL certificate automatically. When used with a dev domain, Appwrite will assign a self-signed SSL certificate. The default value is 'localhost'."},{"id":"$$config__app_domain_target","name":"_APP_DOMAIN_TARGET","label":"General | _APP_DOMAIN_TARGET","defaultValue":"$$generate_fqdn","description":"A DNS A record hostname to serve as a CNAME target for your Appwrite custom domains. You can use the same value as used for the Appwrite '_APP_DOMAIN' variable. The default value is 'localhost'."},{"id":"$$config__app_redis_host","name":"_APP_REDIS_HOST","label":"Redis | _APP_REDIS_HOST","defaultValue":"$$id-redis","description":""},{"id":"$$config__app_redis_port","name":"_APP_REDIS_PORT","label":"Redis | _APP_REDIS_PORT","defaultValue":"6379","description":"Redis server TCP port."},{"id":"$$config__app_redis_user","name":"_APP_REDIS_USER","label":"Redis | _APP_REDIS_USER","defaultValue":"","description":"Redis server user. This is an optional variable. Default value is an empty string."},{"id":"$$secret__app_redis_pass","name":"_APP_REDIS_PASS","label":"Redis | _APP_REDIS_PASS","defaultValue":"","description":"Redis server password. This is an optional variable. Default value is an empty string."},{"id":"$$config__app_db_host","name":"_APP_DB_HOST","label":"MariaDB | _APP_DB_HOST","defaultValue":"$$id-mariadb","description":""},{"id":"$$config__app_db_port","name":"_APP_DB_PORT","label":"MariaDB | _APP_DB_PORT","defaultValue":"3306","description":"MariaDB server TCP port."},{"id":"$$config__app_db_schema","name":"_APP_DB_SCHEMA","label":"MariaDB | _APP_DB_SCHEMA","defaultValue":"appwrite","description":"MariaDB server database schema."},{"id":"$$config__app_db_user","name":"_APP_DB_USER","label":"MariaDB | _APP_DB_USER","defaultValue":"user","description":"MariaDB server user name."},{"id":"$$secret__app_db_root_pass","name":"MARIADB_ROOT_PASSWORD","label":"MariaDB | MARIADB_ROOT_PASSWORD","defaultValue":"$$generate_hex(16)","description":"MariaDB server root user password."},{"id":"$$secret__app_db_pass","name":"_APP_DB_PASS","label":"MariaDB | _APP_DB_PASS","defaultValue":"$$generate_hex(16)","description":"MariaDB server user password."},{"id":"$$config__app_smtp_host","name":"_APP_SMTP_HOST","label":"SMTP | _APP_SMTP_HOST","defaultValue":"","description":"SMTP server host name address. Use an empty string to disable all mail sending from the server. The default value for this variable is an empty string."},{"id":"$$config__app_smtp_port","name":"_APP_SMTP_PORT","label":"SMTP | _APP_SMTP_PORT","defaultValue":"","description":"SMTP server TCP port. Empty by default."},{"id":"$$config__app_smtp_secure","name":"_APP_SMTP_SECURE","label":"SMTP | _APP_SMTP_SECURE","defaultValue":"","description":"SMTP secure connection protocol. Empty by default, change to 'tls' if running on a secure connection."},{"id":"$$config__app_smtp_username","name":"_APP_SMTP_USERNAME","label":"SMTP | _APP_SMTP_USERNAME","defaultValue":"","description":"SMTP server user name. Empty by default."},{"id":"$$secret__app_smtp_password","name":"_APP_SMTP_PASSWORD","label":"SMTP | _APP_SMTP_PASSWORD","defaultValue":"","description":"SMTP server user password. Empty by default."},{"id":"$$config__app_usage_stats","name":"_APP_USAGE_STATS","label":"General | _APP_USAGE_STATS","defaultValue":"enabled","description":"This variable allows you to disable the collection and displaying of usage stats. This value is set to 'enabled' by default, to disable the usage stats set the value to 'disabled'. When disabled, it's recommended to turn off the Worker Usage, Influxdb and Telegraf containers for better resource usage."},{"id":"$$config__app_storage_limit","name":"_APP_STORAGE_LIMIT","label":"Storage | _APP_STORAGE_LIMIT","defaultValue":"30000000","description":"Maximum file size allowed for file upload. The default value is 30MB. You should pass your size limit value in bytes."},{"id":"$$config__app_storage_preview_limit","name":"_APP_STORAGE_PREVIEW_LIMIT","label":"Storage | _APP_STORAGE_PREVIEW_LIMIT","defaultValue":"20000000","description":"Maximum file size allowed for file image preview. The default value is 20MB. You should pass your size limit value in bytes."},{"id":"$$config__app_storage_antivirus_enabled","name":"_APP_STORAGE_ANTIVIRUS","label":"Storage | _APP_STORAGE_ANTIVIRUS","defaultValue":"disabled","description":"This variable allows you to disable the internal anti-virus scans. This value is set to 'disabled' by default, to enable the scans set the value to 'enabled'. Before enabling, you must add the ClamAV service and depend on it on main Appwrite service."},{"id":"$$config__app_storage_antivirus_host","name":"_APP_STORAGE_ANTIVIRUS_HOST","label":"Storage | _APP_STORAGE_ANTIVIRUS_HOST","defaultValue":"clamav","description":"ClamAV server host name address."},{"id":"$$config__app_storage_antivirus_port","name":"_APP_STORAGE_ANTIVIRUS_PORT","label":"Storage | _APP_STORAGE_ANTIVIRUS_PORT","defaultValue":"3310","description":"ClamAV server TCP port."},{"id":"$$config__app_storage_device","name":"_APP_STORAGE_DEVICE","label":"Storage | _APP_STORAGE_DEVICE","defaultValue":"Local","description":"Select default storage device. The default value is 'Local'. List of supported adapters are 'Local', 'S3', 'DOSpaces', 'Backblaze', 'Linode' and 'Wasabi'."},{"id":"$$secret__app_storage_s3_access_key","name":"_APP_STORAGE_S3_ACCESS_KEY","label":"Storage | _APP_STORAGE_S3_ACCESS_KEY","defaultValue":"","description":"AWS S3 storage access key. Required when the storage adapter is set to S3. You can get your access key from your AWS console."},{"id":"$$secret__app_storage_s3_secret","name":"_APP_STORAGE_S3_SECRET","label":"Storage | _APP_STORAGE_S3_SECRET","defaultValue":"","description":"AWS S3 storage secret key. Required when the storage adapter is set to S3. You can get your secret key from your AWS console."},{"id":"$$config__app_storage_s3_region","name":"_APP_STORAGE_S3_REGION","label":"Storage | _APP_STORAGE_S3_REGION","defaultValue":"us-east-1","description":"AWS S3 storage region. Required when storage adapter is set to S3. You can find your region info for your bucket from AWS console."},{"id":"$$config__app_storage_s3_bucket","name":"_APP_STORAGE_S3_BUCKET","label":"Storage | _APP_STORAGE_S3_BUCKET","defaultValue":"","description":"AWS S3 storage bucket. Required when storage adapter is set to S3. You can create buckets in your AWS console."},{"id":"$$secret__app_storage_do_spaces_access_key","name":"_APP_STORAGE_DO_SPACES_ACCESS_KEY","label":"Storage | _APP_STORAGE_DO_SPACES_ACCESS_KEY","defaultValue":"","description":"DigitalOcean spaces access key. Required when the storage adapter is set to DOSpaces. You can get your access key from your DigitalOcean console."},{"id":"$$secret__app_storage_do_spaces_secret","name":"_APP_STORAGE_DO_SPACES_SECRET","label":"Storage | _APP_STORAGE_DO_SPACES_SECRET","defaultValue":"","description":"DigitalOcean spaces secret key. Required when the storage adapter is set to DOSpaces. You can get your secret key from your DigitalOcean console."},{"id":"$$config__app_storage_do_spaces_region","name":"_APP_STORAGE_DO_SPACES_REGION","label":"Storage | _APP_STORAGE_DO_SPACES_REGION","defaultValue":"us-east-1","description":"DigitalOcean spaces region. Required when storage adapter is set to DOSpaces. You can find your region info for your space from DigitalOcean console."},{"id":"$$config__app_storage_do_spaces_bucket","name":"_APP_STORAGE_DO_SPACES_BUCKET","label":"Storage | _APP_STORAGE_DO_SPACES_BUCKET","defaultValue":"","description":"DigitalOcean spaces bucket. Required when storage adapter is set to DOSpaces. You can create spaces in your DigitalOcean console."},{"id":"$$secret__app_storage_backblaze_access_key","name":"_APP_STORAGE_BACKBLAZE_ACCESS_KEY","label":"Storage | _APP_STORAGE_BACKBLAZE_ACCESS_KEY","defaultValue":"","description":"Backblaze access key. Required when the storage adapter is set to Backblaze. Your Backblaze keyID will be your access key. You can get your keyID from your Backblaze console."},{"id":"$$secret__app_storage_backblaze_secret","name":"_APP_STORAGE_BACKBLAZE_SECRET","label":"Storage | _APP_STORAGE_BACKBLAZE_SECRET","defaultValue":"","description":"Backblaze secret key. Required when the storage adapter is set to Backblaze. Your Backblaze applicationKey will be your secret key. You can get your applicationKey from your Backblaze console."},{"id":"$$config__app_storage_backblaze_region","name":"_APP_STORAGE_BACKBLAZE_REGION","label":"Storage | _APP_STORAGE_BACKBLAZE_REGION","defaultValue":"us-west-004","description":"Backblaze region. Required when storage adapter is set to Backblaze. You can find your region info from your Backblaze console."},{"id":"$$config__app_storage_backblaze_bucket","name":"_APP_STORAGE_BACKBLAZE_BUCKET","label":"Storage | _APP_STORAGE_BACKBLAZE_BUCKET","defaultValue":"","description":"Backblaze bucket. Required when storage adapter is set to Backblaze. You can create your bucket from your Backblaze console."},{"id":"$$secret__app_storage_linode_access_key","name":"_APP_STORAGE_LINODE_ACCESS_KEY","label":"Storage | _APP_STORAGE_LINODE_ACCESS_KEY","defaultValue":"","description":"Linode object storage access key. Required when the storage adapter is set to Linode. You can get your access key from your Linode console."},{"id":"$$secret__app_storage_linode_secret","name":"_APP_STORAGE_LINODE_SECRET","label":"Storage | _APP_STORAGE_LINODE_SECRET","defaultValue":"","description":"Linode object storage secret key. Required when the storage adapter is set to Linode. You can get your secret key from your Linode console."},{"id":"$$config__app_storage_linode_region","name":"_APP_STORAGE_LINODE_REGION","label":"Storage | _APP_STORAGE_LINODE_REGION","defaultValue":"eu-central-1","description":"Linode object storage region. Required when storage adapter is set to Linode. You can find your region info from your Linode console."},{"id":"$$config__app_storage_linode_bucket","name":"_APP_STORAGE_LINODE_BUCKET","label":"Storage | _APP_STORAGE_LINODE_BUCKET","defaultValue":"","description":"Linode object storage bucket. Required when storage adapter is set to Linode. You can create buckets in your Linode console."},{"id":"$$secret__app_storage_wasabi_access_key","name":"_APP_STORAGE_WASABI_ACCESS_KEY","label":"Storage | _APP_STORAGE_WASABI_ACCESS_KEY","defaultValue":"","description":"Wasabi access key. Required when the storage adapter is set to Wasabi. You can get your access key from your Wasabi console."},{"id":"$$secret__app_storage_wasabi_secret","name":"_APP_STORAGE_WASABI_SECRET","label":"Storage | _APP_STORAGE_WASABI_SECRET","defaultValue":"","description":"Wasabi secret key. Required when the storage adapter is set to Wasabi. You can get your secret key from your Wasabi console."},{"id":"$$config__app_storage_wasabi_region","name":"_APP_STORAGE_WASABI_REGION","label":"Storage | _APP_STORAGE_WASABI_REGION","defaultValue":"eu-central-1","description":"Wasabi region. Required when storage adapter is set to Wasabi. You can find your region info from your Wasabi console."},{"id":"$$config__app_storage_wasabi_bucket","name":"_APP_STORAGE_WASABI_BUCKET","label":"Storage | _APP_STORAGE_WASABI_BUCKET","defaultValue":"","description":"Wasabi bucket. Required when storage adapter is set to Wasabi. You can create buckets in your Wasabi console."},{"id":"$$config__app_functions_size_limit","name":"_APP_FUNCTIONS_SIZE_LIMIT","label":"Functions | _APP_FUNCTIONS_SIZE_LIMIT","defaultValue":"30000000","description":"The maximum size deployment in bytes. The default value is 30MB."},{"id":"$$config__app_functions_timeout","name":"_APP_FUNCTIONS_TIMEOUT","label":"Functions | _APP_FUNCTIONS_TIMEOUT","defaultValue":"900","description":"The maximum number of seconds allowed as a timeout value when creating a new function. The default value is 900 seconds."},{"id":"$$config__app_functions_build_timeout","name":"_APP_FUNCTIONS_BUILD_TIMEOUT","label":"Functions | _APP_FUNCTIONS_BUILD_TIMEOUT","defaultValue":"900","description":"The maximum number of seconds allowed as a timeout value when building a new function. The default value is 900 seconds."},{"id":"$$config__app_functions_containers","name":"_APP_FUNCTIONS_CONTAINERS","label":"Functions | _APP_FUNCTIONS_CONTAINERS","defaultValue":"10","description":"The maximum number of containers Appwrite is allowed to keep alive in the background for function environments. Running containers allow faster execution time as there is no need to recreate each container every time a function gets executed. The default value is 10."},{"id":"$$config__app_functions_cpus","name":"_APP_FUNCTIONS_CPUS","label":"Functions | _APP_FUNCTIONS_CPUS","defaultValue":"","description":"The maximum number of CPU core a single cloud function is allowed to use. Please note that setting a value higher than available cores will result in a function error, which might result in an error. The default value is empty. When it's empty, CPU limit will be disabled."},{"id":"$$config__app_functions_memory_allocated","name":"_APP_FUNCTIONS_MEMORY","label":"Functions | _APP_FUNCTIONS_MEMORY","defaultValue":"","description":"The maximum amount of memory a single cloud function is allowed to use in megabytes. The default value is empty. When it's empty, memory limit will be disabled."},{"id":"$$config__app_functions_memory_swap","name":"_APP_FUNCTIONS_MEMORY_SWAP","label":"Functions | _APP_FUNCTIONS_MEMORY_SWAP","defaultValue":"","description":"The maximum amount of swap memory a single cloud function is allowed to use in megabytes. The default value is empty. When it's empty, swap memory limit will be disabled."},{"id":"$$config__app_functions_runtimes","name":"_APP_FUNCTIONS_RUNTIMES","label":"Functions | _APP_FUNCTIONS_RUNTIMES","defaultValue":"node-18.0","description":"This option allows you to limit the available environments for cloud functions. This option is very useful for low-cost servers to safe disk space.\nTo enable/activate this option, pass a list of allowed environments separated by a comma.\nCurrently, supported environments are: node-14.5, node-16.0, node-18.0, php-8.0, php-8.1, ruby-3.0, ruby-3.1, python-3.8, python-3.9, python-3.10, deno-1.21, deno-1.24, dart-2.15, dart-2.16, dart-2.17, dotnet-3.1, dotnet-6.0, java-8.0, java-11.0, java-17.0, java-18.0, swift-5.5, kotlin-1.6, cpp-17.0"},{"id":"$$secret__app_executor_secret","name":"_APP_EXECUTOR_SECRET","label":"Functions | _APP_EXECUTOR_SECRET","defaultValue":"$$generate_hex(16)","description":"The secret key used by Appwrite to communicate with the function executor."},{"id":"$$config__app_executor_host","name":"_APP_EXECUTOR_HOST","label":"","defaultValue":"http://$$id-executor/v1","description":""},{"id":"$$config__app_logging_provider","name":"_APP_LOGGING_PROVIDER","label":"General | _APP_LOGGING_PROVIDER","defaultValue":"","description":"This variable allows you to enable logging errors to 3rd party providers. This value is empty by default, to enable the logger set the value to one of 'sentry', 'raygun', 'appsignal', 'logowl'"},{"id":"$$config__app_logging_config","name":"_APP_LOGGING_CONFIG","label":"General | _APP_LOGGING_CONFIG","defaultValue":"","description":"This variable configures authentication to 3rd party error logging providers. If using Sentry, this should be 'SENTRY_API_KEY;SENTRY_APP_ID'. If using Raygun, this should be Raygun API key. If using AppSignal, this should be AppSignal API key. If using LogOwl, this should be LogOwl Service Ticket."},{"id":"$$config__app_statsd_host","name":"_APP_STATSD_HOST","label":"","defaultValue":"$$id-telegraf","description":""},{"id":"$$config__app_statsd_port","name":"_APP_STATSD_PORT","label":"StatsD | _APP_STATSD_PORT","defaultValue":"8125","description":"StatsD server TCP port."},{"id":"$$config__app_maintenance_interval","name":"_APP_MAINTENANCE_INTERVAL","label":"Functions | _APP_MAINTENANCE_INTERVAL","defaultValue":"86400","description":"Interval value containing the number of seconds that the Appwrite maintenance process should wait before executing system cleanups and optimizations. The default value is 86400 seconds (1 day)."},{"id":"$$config__app_maintenance_retention_execution","name":"_APP_MAINTENANCE_RETENTION_EXECUTION","label":"Functions | _APP_MAINTENANCE_RETENTION_EXECUTION","defaultValue":"1209600","description":"The maximum duration (in seconds) upto which to retain execution logs. The default value is 1209600 seconds (14 days)."},{"id":"$$config__app_maintenance_retention_cache","name":"_APP_MAINTENANCE_RETENTION_CACHE","label":"Functions | _APP_MAINTENANCE_RETENTION_CACHE","defaultValue":"2592000","description":"The maximum duration (in seconds) upto which to retain cached files. The default value is 2592000 seconds (30 days)."},{"id":"$$config__app_maintenance_retention_abuse","name":"_APP_MAINTENANCE_RETENTION_ABUSE","label":"Functions | _APP_MAINTENANCE_RETENTION_ABUSE","defaultValue":"86400","description":"The maximum duration (in seconds) upto which to retain abuse logs. The default value is 86400 seconds (1 day)."},{"id":"$$config__app_maintenance_retention_audit","name":"_APP_MAINTENANCE_RETENTION_AUDIT","label":"Functions | _APP_MAINTENANCE_RETENTION_AUDIT","defaultValue":"1209600","description":"The maximum duration (in seconds) upto which to retain audit logs. The default value is 1209600 seconds (14 days)."},{"id":"$$config__app_sms_provider","name":"_APP_SMS_PROVIDER","label":"Phone | _APP_SMS_PROVIDER","defaultValue":"","description":"Provider used for delivering SMS for Phone authentication. Use the following format: 'sms://[USER]:[SECRET]@[PROVIDER]'. Available providers are twilio, text-magic, telesign, msg91, and vonage."},{"id":"$$config__app_sms_from","name":"_APP_SMS_FROM","label":"Phone | _APP_SMS_FROM","defaultValue":"","description":"Phone number used for sending out messages. Must start with a leading '+' and maximum of 15 digits without spaces (+123456789)."},{"id":"$$config__app_functions_inactive_threshold","name":"_APP_FUNCTIONS_INACTIVE_THRESHOLD","label":"Functions | _APP_FUNCTIONS_INACTIVE_THRESHOLD","defaultValue":"60","description":"The minimum time a function can be inactive before it's container is shutdown and put to sleep. The default value is 60 seconds"},{"id":"$$config_open_runtimes_network","name":"OPEN_RUNTIMES_NETWORK","label":"","defaultValue":"$$generate_network","description":""},{"id":"$$config_dockerhub_pull_username","name":"DOCKERHUB_PULL_USERNAME","label":"Functions | DOCKERHUB_PULL_USERNAME","defaultValue":"","description":"The username for hub.docker.com. This variable is used to pull images from hub.docker.com."},{"id":"$$secret_dockerhub_pull_password","name":"DOCKERHUB_PULL_PASSWORD","label":"Functions | DOCKERHUB_PULL_PASSWORD","defaultValue":"","description":"The password for hub.docker.com. This variable is used to pull images from hub.docker.com."},{"id":"$$config__app_usage_timeseries_interval","name":"_APP_USAGE_TIMESERIES_INTERVAL","label":"General | _APP_USAGE_TIMESERIES_INTERVAL","defaultValue":"30","description":"Interval value containing the number of seconds that the Appwrite usage process should wait before aggregating stats and syncing it to mariadb from InfluxDB. The default value is 30 seconds."},{"id":"$$config__app_usage_database_interval","name":"_APP_USAGE_DATABASE_INTERVAL","label":"General | _APP_USAGE_DATABASE_INTERVAL","defaultValue":"900","description":"Interval value containing the number of seconds that the Appwrite usage process should wait before aggregating stats from data in Appwrite Database. The default value is 15 minutes."}]},{"templateVersion":"1.0.0","defaultVersion":"latest","documentation":"https://docs.weblate.org/en/latest/admin/install/docker.html","description":"A copylefted libre software web-based continuous localization system.","type":"weblate","name":"Weblate","labels":["translate","localization"],"services":{"$$id":{"name":"Weblate","depends_on":["$$id-postgresql","$$id-redis"],"image":"weblate/weblate:$$core_version","volumes":["$$id-data:/app/data"],"environment":["WEBLATE_SITE_DOMAIN=$$config_weblate_site_domain","WEBLATE_ADMIN_PASSWORD=$$secret_weblate_admin_password","POSTGRES_PASSWORD=$$secret_postgres_password","POSTGRES_USER=$$config_postgres_user","POSTGRES_DATABASE=$$config_postgres_db","POSTGRES_HOST=$$id-postgresql","POSTGRES_PORT=5432","REDIS_HOST=$$id-redis"],"ports":["8080"]},"$$id-postgresql":{"name":"PostgreSQL","depends_on":[],"image":"postgres:14-alpine","volumes":["$$id-postgresql-data:/var/lib/postgresql/data"],"environment":["POSTGRES_USER=$$config_postgres_user","POSTGRES_PASSWORD=$$secret_postgres_password","POSTGRES_DB=$$config_postgres_db"],"ports":[]},"$$id-redis":{"name":"Redis","depends_on":[],"image":"redis:7-alpine","volumes":["$$id-redis-data:/data"],"environment":[],"ports":[]}},"variables":[{"id":"$$config_weblate_site_domain","name":"WEBLATE_SITE_DOMAIN","label":"Weblate Domain","defaultValue":"$$generate_domain","description":""},{"id":"$$secret_weblate_admin_password","name":"WEBLATE_ADMIN_PASSWORD","label":"Weblate Admin Password","defaultValue":"$$generate_password","description":"","showOnConfiguration":true},{"id":"$$config_postgres_user","main":"$$id-postgresql","name":"POSTGRES_USER","label":"PostgreSQL User","defaultValue":"$$generate_username","description":""},{"id":"$$secret_postgres_password","main":"$$id-postgresql","name":"POSTGRES_PASSWORD","label":"PostgreSQL Password","defaultValue":"$$generate_password","description":"","showOnConfiguration":true},{"id":"$$config_postgres_db","main":"$$id-postgresql","name":"POSTGRES_DB","label":"PostgreSQL Database","defaultValue":"weblate","description":""}]},{"templateVersion":"1.0.0","defaultVersion":"2022.10.14-1a5b0965","documentation":"https://docs.searxng.org/","type":"searxng","name":"SearXNG","description":"Free internet metasearch engine which aggregates results from more than 70 search services.","services":{"$$id":{"name":"SearXNG","depends_on":["$$id-redis"],"image":"searxng/searxng:$$core_version","volumes":["$$id-searxng:/etc/searxng"],"environment":["SEARXNG_BASE_URL=$$config_searxng_base_url"],"ports":["8080"],"cap_drop":["ALL"],"cap_add":["CHOWN","SETGID","SETUID","DAC_OVERRIDE"],"files":[{"location":"/etc/searxng/settings.yml","content":"\n # see https://docs.searxng.org/admin/engines/settings.html#use-default-settings\n use_default_settings: true\n server:\n secret_key: $$secret_secret_key\n limiter: true\n image_proxy: true\n ui:\n static_use_hash: true\n redis:\n url: redis://:$$secret_redis_password@$$id-redis:6379/0"}]},"$$id-redis":{"name":"Redis","command":"redis-server --requirepass $$secret_redis_password --save \"\" --appendonly \"no\"","depends_on":[],"image":"redis:7-alpine","volumes":["$$id-redis-data:/data"],"environment":["REDIS_PASSWORD=$$secret_redis_password"],"ports":[],"cap_drop":["ALL"],"cap_add":["SETGID","SETUID","DAC_OVERRIDE"]}},"variables":[{"id":"$$config_searxng_base_url","name":"SEARXNG_BASE_URL","label":"SearXNG Base URL","defaultValue":"$$generate_fqdn","description":""},{"id":"$$secret_secret_key","name":"SECRET_KEY","label":"Secret Key","defaultValue":"$$generate_hex(64)","description":""},{"id":"$$secret_redis_password","name":"REDIS_PASSWORD","label":"Redis Password","defaultValue":"$$generate_password","description":""}]},{"templateVersion":"1.0.0","defaultVersion":"v2.0.6","documentation":"https://glitchtip.com/documentation","type":"glitchtip","name":"GlitchTip","description":"Simple, open source error tracking.","labels":["sentry","bugsnag"],"services":{"$$id":{"name":"GlitchTip","depends_on":["$$id-postgresql","$$id-redis"],"image":"glitchtip/glitchtip:$$core_version","volumes":[],"environment":["PORT=$$config_port","GLITCHTIP_DOMAIN=$$config_glitchtip_domain","SECRET_KEY=$$secret_secret_key","DATABASE_URL=$$secret_database_url","REDIS_URL=$$secret_redis_url","DEFAULT_FROM_EMAIL=$$config_default_from_email","EMAIL_URL=$$secret_email_url","EMAIL_HOST=$$config_email_host","EMAIL_PORT=$$config_email_port","EMAIL_HOST_USER=$$config_email_host_user","EMAIL_HOST_PASSWORD=$$secret_email_host_password","EMAIL_USE_TLS=$$config_email_use_tls","EMAIL_USE_SSL=$$config_email_use_ssl","EMAIL_BACKEND=$$config_email_backend","MAILGUN_API_KEY=$$secret_mailgun_api_key","SENDGRID_API_KEY=$$secret_sendgrid_api_key","ENABLE_OPEN_USER_REGISTRATION=$$config_enable_open_user_registration","DJANGO_SUPERUSER_EMAIL=$$config_django_superuser_email","DJANGO_SUPERUSER_PASSWORD=$$secret_django_superuser_password","DJANGO_SUPERUSER_USERNAME=$$config_django_superuser_username","CELERY_WORKER_CONCURRENCY=$$config_celery_worker_concurrency"],"ports":["8000"]},"$$id-worker":{"name":"Celery Worker","command":"./bin/run-celery-with-beat.sh","depends_on":["$$id-postgresql","$$id-redis"],"image":"glitchtip/glitchtip:$$core_version","environment":["GLITCHTIP_DOMAIN=$$config_glitchtip_domain","SECRET_KEY=$$secret_secret_key","DATABASE_URL=$$secret_database_url","REDIS_URL=$$secret_redis_url","DEFAULT_FROM_EMAIL=$$config_default_from_email","EMAIL_URL=$$secret_email_url","CELERY_WORKER_CONCURRENCY=$$config_celery_worker_concurrency"],"ports":[]},"$$id-migrate":{"exclude":true,"name":"Migrate","command":"./manage.py migrate","depends_on":["$$id-postgresql","$$id-redis"],"image":"glitchtip/glitchtip:$$core_version","environment":["GLITCHTIP_DOMAIN=$$config_glitchtip_domain","SECRET_KEY=$$secret_secret_key","DATABASE_URL=$$secret_database_url","REDIS_URL=$$secret_redis_url","DEFAULT_FROM_EMAIL=$$config_default_from_email","EMAIL_URL=$$secret_email_url"],"ports":[]},"$$id-postgresql":{"name":"PostgreSQL","depends_on":[],"image":"postgres:14-alpine","volumes":["$$id-postgresql-data:/var/lib/postgresql/data"],"environment":["POSTGRES_USER=$$config_postgres_user","POSTGRES_PASSWORD=$$secret_postgres_password","POSTGRES_DB=$$config_postgres_db"],"ports":[]},"$$id-redis":{"name":"Redis","depends_on":[],"image":"redis:7-alpine","volumes":["$$id-postgresql-redis-data:/data"],"environment":[],"ports":[]}},"variables":[{"id":"$$config_django_superuser_username","name":"DJANGO_SUPERUSER_USERNAME","label":"Django Superuser Username","defaultValue":"$$generate_username","description":""},{"id":"$$secret_django_superuser_password","name":"DJANGO_SUPERUSER_PASSWORD","label":"Django Superuser Password","defaultValue":"$$generate_password","description":"","showOnConfiguration":true},{"id":"$$config_port","name":"PORT","label":"GlitchTip Port","defaultValue":"8000","description":""},{"id":"$$config_celery_worker_concurrency","main":"$$id-worker","name":"CELERY_WORKER_CONCURRENCY","label":"Celery Worker Concurrency","defaultValue":"2","description":""},{"id":"$$config_glitchtip_domain","name":"GLITCHTIP_DOMAIN","label":"GlitchTip Domain","defaultValue":"$$generate_fqdn","description":""},{"id":"$$secret_email_url","name":"EMAIL_URL","label":"SMTP Email URL","defaultValue":"smtp://$$config_email_host_user:$$secret_email_host_password@$$config_email_host:$$config_email_port","description":""},{"id":"$$secret_database_url","name":"DATABASE_URL","label":"Database URL for PostgreSQL","defaultValue":"postgresql://$$config_postgres_user:$$secret_postgres_password@$$id-postgresql:5432/$$config_postgres_db","description":""},{"id":"$$secret_redis_url","name":"REDIS_URL","label":"Redis URL","defaultValue":"redis://$$id-redis:6379/0","description":""},{"id":"$$config_default_from_email","name":"DEFAULT_FROM_EMAIL","label":"Default Email Address","defaultValue":"noreply@example.com","description":""},{"id":"$$config_email_host","name":"EMAIL_HOST","label":"Email SMTP Host","defaultValue":"","description":""},{"id":"$$config_email_port","name":"EMAIL_PORT","label":"Email SMTP Port","defaultValue":"25","description":""},{"id":"$$config_email_host_user","name":"EMAIL_HOST_USER","label":"Email SMTP User","defaultValue":"","description":""},{"id":"$$secret_email_host_password","name":"EMAIL_HOST_PASSWORD","label":"Email SMTP Password","defaultValue":"","description":""},{"id":"$$config_email_use_tls","name":"EMAIL_USE_TLS","label":"Email Use TLS","defaultValue":"false","description":""},{"id":"$$config_email_use_ssl","name":"EMAIL_USE_SSL","label":"Email Use SSL","defaultValue":"false","description":""},{"id":"$$secret_email_smtp_password","name":"EMAIL_SMTP_PASSWORD","label":"SMTP Password","defaultValue":"","description":""},{"id":"$$config_email_backend","name":"EMAIL_BACKEND","label":"Email Backend","defaultValue":"","description":""},{"id":"$$secret_mailgun_api_key","name":"MAILGUN_API_KEY","label":"Mailgun API Key","defaultValue":"","description":"","showOnConfiguration":true},{"id":"$$secret_sendgrid_api_key","name":"SENDGRID_API_KEY","label":"Sendgrid API Key","defaultValue":"","description":"","showOnConfiguration":true},{"id":"$$config_enable_open_user_registration","name":"ENABLE_OPEN_USER_REGISTRATION","label":"Enable Open User Registration","defaultValue":"true","description":""},{"id":"$$config_django_superuser_email","name":"DJANGO_SUPERUSER_EMAIL","label":"Django Superuser Email","defaultValue":"noreply@example.com","description":""},{"id":"$$config_postgres_user","main":"$$id-postgresql","name":"POSTGRES_USER","label":"PostgreSQL User","defaultValue":"$$generate_username","description":""},{"id":"$$secret_postgres_password","main":"$$id-postgresql","name":"POSTGRES_PASSWORD","label":"PostgreSQL Password","defaultValue":"$$generate_password","description":""},{"id":"$$config_postgres_db","main":"$$id-postgresql","name":"POSTGRES_DB","label":"PostgreSQL Database","defaultValue":"glitchtip","description":""}]},{"templateVersion":"1.0.0","defaultVersion":"v2.13.0","documentation":"https://hasura.io/docs/latest/index/","type":"hasura","name":"Hasura","description":"Instant realtime GraphQL APIs on any Postgres application, existing or new.","labels":["graphql","database"],"services":{"$$id":{"name":"Hasura","depends_on":["$$id-postgresql"],"image":"hasura/graphql-engine:$$core_version","volumes":[],"environment":["HASURA_GRAPHQL_ENABLE_CONSOLE=$$config_hasura_graphql_enable_console","HASURA_GRAPHQL_METADATA_DATABASE_URL=$$secret_hasura_graphql_metadata_database_url","HASURA_GRAPHQL_ADMIN_SECRET=$$secret_hasura_graphql_admin_secret"],"ports":["8080"]},"$$id-postgresql":{"name":"PostgreSQL","depends_on":[],"image":"postgres:12-alpine","volumes":["$$id-postgresql-data:/var/lib/postgresql/data"],"environment":["POSTGRES_USER=$$config_postgres_user","POSTGRES_PASSWORD=$$secret_postgres_password","POSTGRES_DB=$$config_postgres_db"],"ports":[]}},"variables":[{"id":"$$config_hasura_graphql_enable_console","name":"HASURA_GRAPHQL_ENABLE_CONSOLE","label":"Enable Hasura Console","defaultValue":"true","description":""},{"id":"$$secret_hasura_graphql_metadata_database_url","name":"HASURA_GRAPHQL_METADATA_DATABASE_URL","label":"Hasura Metadata Database URL","defaultValue":"postgresql://$$config_postgres_user:$$secret_postgres_password@$$id-postgresql:5432/$$config_postgres_db","description":""},{"id":"$$secret_hasura_graphql_admin_secret","name":"HASURA_GRAPHQL_ADMIN_SECRET","label":"Hasura Admin Password","defaultValue":"$$generate_password","description":"","showOnConfiguration":true},{"id":"$$config_postgres_user","name":"POSTGRES_USER","label":"PostgreSQL User","defaultValue":"$$generate_username","description":""},{"id":"$$secret_postgres_password","name":"POSTGRES_PASSWORD","label":"PostgreSQL Password","defaultValue":"$$generate_password","description":""},{"id":"$$config_postgres_db","name":"POSTGRES_DB","label":"PostgreSQL Database","defaultValue":"hasura","description":""}]},{"templateVersion":"1.0.0","defaultVersion":"postgresql-v1.38.0","documentation":"https://umami.is/docs/getting-started","type":"umami-postgresql","name":"Umami","subname":"(PostgreSQL)","description":"A simple, easy to use, self-hosted web analytics solution.","services":{"$$id":{"name":"Umami","depends_on":["$$id-postgresql"],"image":"ghcr.io/umami-software/umami:$$core_version","volumes":[],"environment":["ADMIN_PASSWORD=$$secret_admin_password","DATABASE_URL=$$secret_database_url","DATABASE_TYPE=$$config_database_type","HASH_SALT=$$secret_hash_salt"],"ports":["3000"]},"$$id-postgresql":{"name":"PostgreSQL","depends_on":[],"image":"postgres:12-alpine","volumes":["$$id-postgresql-data:/var/lib/postgresql/data"],"environment":["POSTGRES_USER=$$config_postgres_user","POSTGRES_PASSWORD=$$secret_postgres_password","POSTGRES_DB=$$config_postgres_db"],"ports":[],"files":[{"location":"/docker-entrypoint-initdb.d/schema.postgresql.sql","content":"\n -- CreateTable\n CREATE TABLE \"account\" (\n \"user_id\" SERIAL NOT NULL,\n \"username\" VARCHAR(255) NOT NULL,\n \"password\" VARCHAR(60) NOT NULL,\n \"is_admin\" BOOLEAN NOT NULL DEFAULT false,\n \"created_at\" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,\n \"updated_at\" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,\n \n PRIMARY KEY (\"user_id\")\n );\n \n -- CreateTable\n CREATE TABLE \"event\" (\n \"event_id\" SERIAL NOT NULL,\n \"website_id\" INTEGER NOT NULL,\n \"session_id\" INTEGER NOT NULL,\n \"created_at\" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,\n \"url\" VARCHAR(500) NOT NULL,\n \"event_type\" VARCHAR(50) NOT NULL,\n \"event_value\" VARCHAR(50) NOT NULL,\n \n PRIMARY KEY (\"event_id\")\n );\n \n -- CreateTable\n CREATE TABLE \"pageview\" (\n \"view_id\" SERIAL NOT NULL,\n \"website_id\" INTEGER NOT NULL,\n \"session_id\" INTEGER NOT NULL,\n \"created_at\" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,\n \"url\" VARCHAR(500) NOT NULL,\n \"referrer\" VARCHAR(500),\n \n PRIMARY KEY (\"view_id\")\n );\n \n -- CreateTable\n CREATE TABLE \"session\" (\n \"session_id\" SERIAL NOT NULL,\n \"session_uuid\" UUID NOT NULL,\n \"website_id\" INTEGER NOT NULL,\n \"created_at\" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,\n \"hostname\" VARCHAR(100),\n \"browser\" VARCHAR(20),\n \"os\" VARCHAR(20),\n \"device\" VARCHAR(20),\n \"screen\" VARCHAR(11),\n \"language\" VARCHAR(35),\n \"country\" CHAR(2),\n \n PRIMARY KEY (\"session_id\")\n );\n \n -- CreateTable\n CREATE TABLE \"website\" (\n \"website_id\" SERIAL NOT NULL,\n \"website_uuid\" UUID NOT NULL,\n \"user_id\" INTEGER NOT NULL,\n \"name\" VARCHAR(100) NOT NULL,\n \"domain\" VARCHAR(500),\n \"share_id\" VARCHAR(64),\n \"created_at\" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,\n \n PRIMARY KEY (\"website_id\")\n );\n \n -- CreateIndex\n CREATE UNIQUE INDEX \"account.username_unique\" ON \"account\"(\"username\");\n \n -- CreateIndex\n CREATE INDEX \"event_created_at_idx\" ON \"event\"(\"created_at\");\n \n -- CreateIndex\n CREATE INDEX \"event_session_id_idx\" ON \"event\"(\"session_id\");\n \n -- CreateIndex\n CREATE INDEX \"event_website_id_idx\" ON \"event\"(\"website_id\");\n \n -- CreateIndex\n CREATE INDEX \"pageview_created_at_idx\" ON \"pageview\"(\"created_at\");\n \n -- CreateIndex\n CREATE INDEX \"pageview_session_id_idx\" ON \"pageview\"(\"session_id\");\n \n -- CreateIndex\n CREATE INDEX \"pageview_website_id_created_at_idx\" ON \"pageview\"(\"website_id\", \"created_at\");\n \n -- CreateIndex\n CREATE INDEX \"pageview_website_id_idx\" ON \"pageview\"(\"website_id\");\n \n -- CreateIndex\n CREATE INDEX \"pageview_website_id_session_id_created_at_idx\" ON \"pageview\"(\"website_id\", \"session_id\", \"created_at\");\n \n -- CreateIndex\n CREATE UNIQUE INDEX \"session.session_uuid_unique\" ON \"session\"(\"session_uuid\");\n \n -- CreateIndex\n CREATE INDEX \"session_created_at_idx\" ON \"session\"(\"created_at\");\n \n -- CreateIndex\n CREATE INDEX \"session_website_id_idx\" ON \"session\"(\"website_id\");\n \n -- CreateIndex\n CREATE UNIQUE INDEX \"website.website_uuid_unique\" ON \"website\"(\"website_uuid\");\n \n -- CreateIndex\n CREATE UNIQUE INDEX \"website.share_id_unique\" ON \"website\"(\"share_id\");\n \n -- CreateIndex\n CREATE INDEX \"website_user_id_idx\" ON \"website\"(\"user_id\");\n \n -- AddForeignKey\n ALTER TABLE \"event\" ADD FOREIGN KEY (\"session_id\") REFERENCES \"session\"(\"session_id\") ON DELETE CASCADE ON UPDATE CASCADE;\n \n -- AddForeignKey\n ALTER TABLE \"event\" ADD FOREIGN KEY (\"website_id\") REFERENCES \"website\"(\"website_id\") ON DELETE CASCADE ON UPDATE CASCADE;\n \n -- AddForeignKey\n ALTER TABLE \"pageview\" ADD FOREIGN KEY (\"session_id\") REFERENCES \"session\"(\"session_id\") ON DELETE CASCADE ON UPDATE CASCADE;\n \n -- AddForeignKey\n ALTER TABLE \"pageview\" ADD FOREIGN KEY (\"website_id\") REFERENCES \"website\"(\"website_id\") ON DELETE CASCADE ON UPDATE CASCADE;\n \n -- AddForeignKey\n ALTER TABLE \"session\" ADD FOREIGN KEY (\"website_id\") REFERENCES \"website\"(\"website_id\") ON DELETE CASCADE ON UPDATE CASCADE;\n \n -- AddForeignKey\n ALTER TABLE \"website\" ADD FOREIGN KEY (\"user_id\") REFERENCES \"account\"(\"user_id\") ON DELETE CASCADE ON UPDATE CASCADE;\n \n insert into account (username, password, is_admin) values ('admin', '$$hashed$$secret_admin_password', true);"}]}},"variables":[{"id":"$$secret_database_url","name":"DATABASE_URL","label":"Database URL for PostgreSQL","defaultValue":"postgresql://$$config_postgres_user:$$secret_postgres_password@$$id-postgresql:5432/$$config_postgres_db","description":""},{"id":"$$secret_hash_salt","name":"HASH_SALT","label":"Hash Salt","defaultValue":"$$generate_hex(64)","description":""},{"id":"$$config_database_type","name":"DATABASE_TYPE","label":"Database Type","defaultValue":"postgresql","description":""},{"id":"$$config_postgres_user","name":"POSTGRES_USER","label":"PostgreSQL User","defaultValue":"$$generate_username","description":""},{"id":"$$secret_postgres_password","name":"POSTGRES_PASSWORD","label":"PostgreSQL Password","defaultValue":"$$generate_password","description":""},{"id":"$$config_postgres_db","name":"POSTGRES_DB","label":"PostgreSQL Database","defaultValue":"umami","description":""},{"id":"$$secret_admin_password","name":"ADMIN_PASSWORD","label":"Initial Admin Password","defaultValue":"$$generate_password","description":"","showOnConfiguration":true}]},{"templateVersion":"1.0.0","ignore":true,"defaultVersion":"postgresql-v1.38.0","documentation":"https://umami.is/docs/getting-started","type":"umami","name":"Umami","subname":"(PostgreSQL)","description":"A simple, easy to use, self-hosted web analytics solution.","services":{"$$id":{"name":"Umami","depends_on":["$$id-postgresql"],"image":"ghcr.io/umami-software/umami:$$core_version","volumes":[],"environment":["ADMIN_PASSWORD=$$secret_admin_password","DATABASE_URL=$$secret_database_url","DATABASE_TYPE=$$config_database_type","HASH_SALT=$$secret_hash_salt"],"ports":["3000"]},"$$id-postgresql":{"name":"PostgreSQL","depends_on":[],"image":"postgres:12-alpine","volumes":["$$id-postgresql-data:/var/lib/postgresql/data"],"environment":["POSTGRES_USER=$$config_postgres_user","POSTGRES_PASSWORD=$$secret_postgres_password","POSTGRES_DB=$$config_postgres_db"],"ports":[],"files":[{"location":"/docker-entrypoint-initdb.d/schema.postgresql.sql","content":"\n -- CreateTable\n CREATE TABLE \"account\" (\n \"user_id\" SERIAL NOT NULL,\n \"username\" VARCHAR(255) NOT NULL,\n \"password\" VARCHAR(60) NOT NULL,\n \"is_admin\" BOOLEAN NOT NULL DEFAULT false,\n \"created_at\" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,\n \"updated_at\" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,\n \n PRIMARY KEY (\"user_id\")\n );\n \n -- CreateTable\n CREATE TABLE \"event\" (\n \"event_id\" SERIAL NOT NULL,\n \"website_id\" INTEGER NOT NULL,\n \"session_id\" INTEGER NOT NULL,\n \"created_at\" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,\n \"url\" VARCHAR(500) NOT NULL,\n \"event_type\" VARCHAR(50) NOT NULL,\n \"event_value\" VARCHAR(50) NOT NULL,\n \n PRIMARY KEY (\"event_id\")\n );\n \n -- CreateTable\n CREATE TABLE \"pageview\" (\n \"view_id\" SERIAL NOT NULL,\n \"website_id\" INTEGER NOT NULL,\n \"session_id\" INTEGER NOT NULL,\n \"created_at\" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,\n \"url\" VARCHAR(500) NOT NULL,\n \"referrer\" VARCHAR(500),\n \n PRIMARY KEY (\"view_id\")\n );\n \n -- CreateTable\n CREATE TABLE \"session\" (\n \"session_id\" SERIAL NOT NULL,\n \"session_uuid\" UUID NOT NULL,\n \"website_id\" INTEGER NOT NULL,\n \"created_at\" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,\n \"hostname\" VARCHAR(100),\n \"browser\" VARCHAR(20),\n \"os\" VARCHAR(20),\n \"device\" VARCHAR(20),\n \"screen\" VARCHAR(11),\n \"language\" VARCHAR(35),\n \"country\" CHAR(2),\n \n PRIMARY KEY (\"session_id\")\n );\n \n -- CreateTable\n CREATE TABLE \"website\" (\n \"website_id\" SERIAL NOT NULL,\n \"website_uuid\" UUID NOT NULL,\n \"user_id\" INTEGER NOT NULL,\n \"name\" VARCHAR(100) NOT NULL,\n \"domain\" VARCHAR(500),\n \"share_id\" VARCHAR(64),\n \"created_at\" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,\n \n PRIMARY KEY (\"website_id\")\n );\n \n -- CreateIndex\n CREATE UNIQUE INDEX \"account.username_unique\" ON \"account\"(\"username\");\n \n -- CreateIndex\n CREATE INDEX \"event_created_at_idx\" ON \"event\"(\"created_at\");\n \n -- CreateIndex\n CREATE INDEX \"event_session_id_idx\" ON \"event\"(\"session_id\");\n \n -- CreateIndex\n CREATE INDEX \"event_website_id_idx\" ON \"event\"(\"website_id\");\n \n -- CreateIndex\n CREATE INDEX \"pageview_created_at_idx\" ON \"pageview\"(\"created_at\");\n \n -- CreateIndex\n CREATE INDEX \"pageview_session_id_idx\" ON \"pageview\"(\"session_id\");\n \n -- CreateIndex\n CREATE INDEX \"pageview_website_id_created_at_idx\" ON \"pageview\"(\"website_id\", \"created_at\");\n \n -- CreateIndex\n CREATE INDEX \"pageview_website_id_idx\" ON \"pageview\"(\"website_id\");\n \n -- CreateIndex\n CREATE INDEX \"pageview_website_id_session_id_created_at_idx\" ON \"pageview\"(\"website_id\", \"session_id\", \"created_at\");\n \n -- CreateIndex\n CREATE UNIQUE INDEX \"session.session_uuid_unique\" ON \"session\"(\"session_uuid\");\n \n -- CreateIndex\n CREATE INDEX \"session_created_at_idx\" ON \"session\"(\"created_at\");\n \n -- CreateIndex\n CREATE INDEX \"session_website_id_idx\" ON \"session\"(\"website_id\");\n \n -- CreateIndex\n CREATE UNIQUE INDEX \"website.website_uuid_unique\" ON \"website\"(\"website_uuid\");\n \n -- CreateIndex\n CREATE UNIQUE INDEX \"website.share_id_unique\" ON \"website\"(\"share_id\");\n \n -- CreateIndex\n CREATE INDEX \"website_user_id_idx\" ON \"website\"(\"user_id\");\n \n -- AddForeignKey\n ALTER TABLE \"event\" ADD FOREIGN KEY (\"session_id\") REFERENCES \"session\"(\"session_id\") ON DELETE CASCADE ON UPDATE CASCADE;\n \n -- AddForeignKey\n ALTER TABLE \"event\" ADD FOREIGN KEY (\"website_id\") REFERENCES \"website\"(\"website_id\") ON DELETE CASCADE ON UPDATE CASCADE;\n \n -- AddForeignKey\n ALTER TABLE \"pageview\" ADD FOREIGN KEY (\"session_id\") REFERENCES \"session\"(\"session_id\") ON DELETE CASCADE ON UPDATE CASCADE;\n \n -- AddForeignKey\n ALTER TABLE \"pageview\" ADD FOREIGN KEY (\"website_id\") REFERENCES \"website\"(\"website_id\") ON DELETE CASCADE ON UPDATE CASCADE;\n \n -- AddForeignKey\n ALTER TABLE \"session\" ADD FOREIGN KEY (\"website_id\") REFERENCES \"website\"(\"website_id\") ON DELETE CASCADE ON UPDATE CASCADE;\n \n -- AddForeignKey\n ALTER TABLE \"website\" ADD FOREIGN KEY (\"user_id\") REFERENCES \"account\"(\"user_id\") ON DELETE CASCADE ON UPDATE CASCADE;\n \n insert into account (username, password, is_admin) values ('admin', '$$hashed$$secret_admin_password', true);"}]}},"variables":[{"id":"$$secret_database_url","name":"DATABASE_URL","label":"Database URL for PostgreSQL","defaultValue":"postgresql://$$config_postgres_user:$$secret_postgres_password@$$id-postgresql:5432/$$config_postgres_db","description":""},{"id":"$$secret_hash_salt","name":"HASH_SALT","label":"Hash Salt","defaultValue":"$$generate_hex(64)","description":""},{"id":"$$config_database_type","name":"DATABASE_TYPE","label":"Database Type","defaultValue":"postgresql","description":""},{"id":"$$config_postgres_user","name":"POSTGRES_USER","label":"PostgreSQL User","defaultValue":"$$generate_username","description":""},{"id":"$$secret_postgres_password","name":"POSTGRES_PASSWORD","label":"PostgreSQL Password","defaultValue":"$$generate_password","description":""},{"id":"$$config_postgres_db","name":"POSTGRES_DB","label":"PostgreSQL Database","defaultValue":"umami","description":""},{"id":"$$secret_admin_password","name":"ADMIN_PASSWORD","label":"Initial Admin Password","defaultValue":"$$generate_password","description":"","showOnConfiguration":true}]},{"templateVersion":"1.0.0","defaultVersion":"v0.29.1","documentation":"https://docs.meilisearch.com/learn/getting_started/quick_start.html","type":"meilisearch","name":"MeiliSearch","description":"A lightning Fast, Ultra Relevant, and Typo-Tolerant Search Engine.","services":{"$$id":{"name":"MeiliSearch","documentation":"https://docs.meilisearch.com/","depends_on":[],"image":"getmeili/meilisearch:$$core_version","volumes":["$$id-datams:/meili_data/data.ms","$$id-data:/meili_data","$$id-snapshot:/snapshot","$$id-dump:/dumps"],"environment":["MEILI_MASTER_KEY=$$secret_meili_master_key"],"ports":["7700"]}},"variables":[{"id":"$$secret_meili_master_key","name":"MEILI_MASTER_KEY","label":"Master Key","defaultValue":"$$generate_hex(64)","description":"","showOnConfiguration":true}]},{"templateVersion":"1.0.0","ignore":true,"defaultVersion":"latest","documentation":"https://docs.ghost.org","type":"ghost-mariadb","name":"Ghost","subname":"(MariaDB)","description":"Free and open source blogging platform.","labels":["cms","blog"],"services":{"$$id":{"name":"Ghost","depends_on":["$$id-mariadb"],"image":"bitnami/ghost:$$core_version","volumes":["$$id-ghost:/bitnami/ghost"],"environment":["url=$$config_url","GHOST_HOST=$$config_ghost_host","GHOST_ENABLE_HTTPS=$$config_ghost_enable_https","GHOST_EMAIL=$$config_ghost_email","GHOST_PASSWORD=$$secret_ghost_password","GHOST_DATABASE_HOST=$$config_ghost_database_host","GHOST_DATABASE_USER=$$config_mariadb_user","GHOST_DATABASE_PASSWORD=$$secret_ghost_database_password","GHOST_DATABASE_NAME=$$config_mariadb_database","GHOST_DATABASE_PORT_NUMBER=3306"],"ports":["2368"]},"$$id-mariadb":{"name":"MariaDB","depends_on":[],"image":"bitnami/mariadb:latest","volumes":["$$id-mariadb:/bitnami/mariadb"],"environment":["MARIADB_USER=$$config_mariadb_user","MARIADB_PASSWORD=$$secret_mariadb_password","MARIADB_DATABASE=$$config_mariadb_database","MARIADB_ROOT_USER=$$config_mariadb_root_user","MARIADB_ROOT_PASSWORD=$$secret_mariadb_root_password"],"ports":[]}},"variables":[{"id":"$$config_url","name":"url","label":"URL","defaultValue":"$$generate_fqdn","description":""},{"id":"$$config_ghost_host","name":"GHOST_HOST","label":"Ghost Host","defaultValue":"$$generate_domain","description":""},{"id":"$$config_ghost_enable_https","name":"GHOST_ENABLE_HTTPS","label":"Ghost Enable HTTPS","defaultValue":"no","description":""},{"id":"$$config_ghost_email","name":"GHOST_EMAIL","label":"Ghost Default Email","defaultValue":"admin@example.com","description":""},{"id":"$$secret_ghost_password","name":"GHOST_PASSWORD","label":"Ghost Default Password","defaultValue":"$$generate_password","description":"","showOnConfiguration":true},{"id":"$$config_ghost_database_host","name":"GHOST_DATABASE_HOST","label":"Ghost Database Host","defaultValue":"$$id-mariadb","description":""},{"id":"$$config_ghost_database_user","name":"GHOST_DATABASE_USER","label":"MariaDB User","defaultValue":"$$config_mariadb_user","description":""},{"id":"$$secret_ghost_database_password","name":"GHOST_DATABASE_PASSWORD","label":"MariaDB Password","defaultValue":"$$secret_mariadb_password","description":""},{"id":"$$config_ghost_database_name","name":"GHOST_DATABASE_NAME","label":"MariaDB Database","defaultValue":"$$config_mariadb_database","description":""},{"id":"$$config_mariadb_user","name":"MARIADB_USER","label":"MariaDB User","defaultValue":"$$generate_username","description":""},{"id":"$$secret_mariadb_password","name":"MARIADB_PASSWORD","label":"MariaDB Password","defaultValue":"$$generate_password","description":""},{"id":"$$config_mariadb_database","name":"MARIADB_DATABASE","label":"MariaDB Database","defaultValue":"ghost","description":""},{"id":"$$config_mariadb_root_user","name":"MARIADB_ROOT_USER","label":"MariaDB Root User","defaultValue":"$$generate_username","description":""},{"id":"$$secret_mariadb_root_password","name":"MARIADB_ROOT_PASSWORD","label":"MariaDB Root Password","defaultValue":"$$generate_password","description":""}]},{"templateVersion":"1.0.0","defaultVersion":"5.22","documentation":"https://docs.ghost.org","type":"ghost-only","name":"Ghost","subname":"(without Database)","description":"Free and open source blogging platform.","services":{"$$id":{"name":"Ghost","image":"ghost:$$core_version","volumes":["$$id-ghost:/var/lib/ghost/content"],"environment":["url=$$config_url","database__client=$$config_database__client","database__connection__host=$$config_database__connection__host","database__connection__user=$$config_database__connection__user","database__connection__password=$$secret_database__connection__password","database__connection__database=$$config_database__connection__database"],"ports":["2368"]}},"variables":[{"id":"$$config_url","name":"url","label":"URL","defaultValue":"$$generate_fqdn","description":""},{"id":"$$config_database__client","name":"database__client","label":"Database Client","defaultValue":"mysql","description":"","required":true},{"id":"$$config_database__connection__host","name":"database__connection__host","label":"Database Host","defaultValue":"","description":"","required":true,"placeholder":"db.coolify.io"},{"id":"$$config_database__connection__user","name":"database__connection__user","label":"Database User","defaultValue":"","description":"","placeholder":"ghost","required":true},{"id":"$$secret_database__connection__password","name":"database__connection__password","label":"Database Password","defaultValue":"","description":"","placeholder":"superSecretP4ssword","showOnConfiguration":true,"required":true},{"id":"$$config_database__connection__database","name":"database__connection__database","label":"Database Name","defaultValue":"","description":"","placeholder":"ghost_db","required":true}]},{"templateVersion":"1.0.0","defaultVersion":"5.22","documentation":"https://docs.ghost.org","type":"ghost-mysql","name":"Ghost","subname":"(MySQL)","description":"Ghost is a free and open source blogging platform.","services":{"$$id":{"name":"Ghost","depends_on":["$$id-mysql"],"image":"ghost:$$core_version","volumes":["$$id-ghost:/var/lib/ghost/content"],"environment":["url=$$config_url","database__client=$$config_database__client","database__connection__host=$$config_database__connection__host","database__connection__user=$$config_mysql_user","database__connection__password=$$secret_mysql_password","database__connection__database=$$config_mysql_database"],"ports":["2368"]},"$$id-mysql":{"name":"MySQL","depends_on":[],"image":"mysql:8.0","volumes":["$$id-mysql:/var/lib/mysql"],"environment":["MYSQL_USER=$$config_mysql_user","MYSQL_PASSWORD=$$secret_mysql_password","MYSQL_DATABASE=$$config_mysql_database","MYSQL_ROOT_PASSWORD=$$secret_mysql_root_password"],"ports":[]}},"variables":[{"id":"$$config_url","name":"url","label":"URL","defaultValue":"$$generate_fqdn","description":""},{"id":"$$config_database__client","name":"database__client","label":"Database Client","defaultValue":"mysql","description":"","readOnly":true},{"id":"$$config_database__connection__host","name":"database__connection__host","label":"Database Host","defaultValue":"$$id-mysql","description":""},{"id":"$$config_mysql_user","main":"$$id-mysql","name":"MYSQL_USER","label":"MySQL User","defaultValue":"$$generate_username","description":""},{"id":"$$secret_mysql_password","main":"$$id-mysql","name":"MYSQL_PASSWORD","label":"MySQL Password","defaultValue":"$$generate_password","description":""},{"id":"$$config_mysql_database","main":"$$id-mysql","name":"MYSQL_DATABASE","label":"MySQL Database","defaultValue":"ghost","description":""},{"id":"$$secret_mysql_root_password","name":"MYSQL_ROOT_PASSWORD","label":"MySQL Root Password","defaultValue":"$$generate_password","description":""}]},{"templateVersion":"1.0.0","defaultVersion":"php8.1","documentation":"https://wordpress.org/","type":"wordpress","name":"WordPress","subname":"(MySQL)","description":"A content management system based on PHP.","labels":["wordpress","php","cms"],"services":{"$$id":{"name":"WordPress","depends_on":["$$id-mysql"],"image":"wordpress:$$core_version","volumes":["$$id-wordpress-data:/var/www/html"],"environment":["WORDPRESS_DB_HOST=$$config_wordpress_db_host","WORDPRESS_DB_USER=$$config_mysql_user","WORDPRESS_DB_PASSWORD=$$secret_mysql_password","WORDPRESS_DB_NAME=$$config_mysql_database","WORDPRESS_CONFIG_EXTRA=$$config_wordpress_config_extra"],"ports":["80"]},"$$id-mysql":{"name":"MySQL","depends_on":[],"image":"bitnami/mysql:5.7","imageArm":"mysql:8.0","volumes":["$$id-mysql-data:/bitnami/mysql/data"],"volumesArm":["$$id-mysql-data:/var/lib/mysql"],"environment":["MYSQL_ROOT_PASSWORD=$$secret_mysql_root_password","MYSQL_ROOT_USER=$$config_mysql_root_user","MYSQL_DATABASE=$$config_mysql_database","MYSQL_USER=$$config_mysql_user","MYSQL_PASSWORD=$$secret_mysql_password"]}},"variables":[{"id":"$$config_wordpress_db_host","name":"WORDPRESS_DB_HOST","label":"Database Host","defaultValue":"$$id-mysql","description":"","readOnly":true},{"id":"$$config_wordpress_config_extra","name":"WORDPRESS_CONFIG_EXTRA","label":"WordPress Config Extra","defaultValue":"","description":"","type":"textarea","placeholder":"define('WP_DEBUG', true);\ndefine('WP_DEBUG_LOG', true);\ndefine('WP_DEBUG_DISPLAY', false);\n@ini_set('display_errors', 0);\n"},{"id":"$$secret_mysql_root_password","name":"MYSQL_ROOT_PASSWORD","label":"MySQL Root Password","defaultValue":"$$generate_password","description":"","readOnly":true},{"id":"$$config_mysql_root_user","name":"MYSQL_ROOT_USER","label":"MySQL Root User","defaultValue":"$$generate_username","description":"","readOnly":true},{"id":"$$config_mysql_database","name":"MYSQL_DATABASE","label":"MySQL Database","defaultValue":"wordpress","description":"","readOnly":true},{"id":"$$config_mysql_user","name":"MYSQL_USER","label":"MySQL User","defaultValue":"$$generate_username","description":"","readOnly":true},{"id":"$$secret_mysql_password","name":"MYSQL_PASSWORD","label":"MySQL Password","defaultValue":"$$generate_password","description":"","readOnly":true}]},{"templateVersion":"1.0.0","defaultVersion":"php8.1","documentation":"https://wordpress.org/","type":"wordpress-only","name":"WordPress","subname":"(without DB)","description":"A content management system based on PHP.","labels":["wordpress","php","cms"],"services":{"$$id":{"name":"WordPress","image":"wordpress:$$core_version","volumes":["$$id-wordpress-data:/var/www/html"],"environment":["WORDPRESS_DB_HOST=$$config_wordpress_db_host","WORDPRESS_DB_PORT=$$config_wordpress_db_port","WORDPRESS_DB_USER=$$config_wordpress_db_user","WORDPRESS_DB_PASSWORD=$$secret_wordpress_db_password","WORDPRESS_DB_NAME=$$config_wordpress_db_name","WORDPRESS_CONFIG_EXTRA=$$config_wordpress_config_extra"],"ports":["80"]}},"variables":[{"id":"$$config_wordpress_db_host","name":"WORDPRESS_DB_HOST","label":"Database Host","defaultValue":"","description":"","placeholder":"db.coollabs.io","required":true},{"id":"$$config_wordpress_db_port","name":"WORDPRESS_DB_PORT","label":"Database Port","defaultValue":"","description":"","placeholder":"3306","required":true},{"id":"$$config_wordpress_db_user","name":"WORDPRESS_DB_USER","label":"Database User","defaultValue":"","description":"","placeholder":"wordpress","required":true},{"id":"$$secret_wordpress_db_password","name":"WORDPRESS_DB_PASSWORD","label":"Database Password","defaultValue":"","description":"","placeholder":"supers3cr3tpassw0rd!","required":true,"showOnConfiguration":true},{"id":"$$config_wordpress_db_name","name":"WORDPRESS_DB_NAME","label":"Database Name","defaultValue":"","description":"","placeholder":"wordpress","required":true},{"id":"$$config_wordpress_config_extra","name":"WORDPRESS_CONFIG_EXTRA","label":"Extra Config","defaultValue":"","description":"","type":"textarea","placeholder":"define('WP_DEBUG', true);\ndefine('WP_DEBUG_LOG', true);\ndefine('WP_DEBUG_DISPLAY', false);\n@ini_set('display_errors', 0);\n"}]},{"templateVersion":"1.0.0","defaultVersion":"4.7.1","documentation":"https://coder.com/docs/coder-oss/latest","type":"vscodeserver","name":"VSCode Server","description":"Visual Studio Code on a remote server, accessible through the browser.","labels":["vscode","ide"],"services":{"$$id":{"name":"VSCode Server","depends_on":[],"image":"codercom/code-server:$$core_version","volumes":["$$id-config-data:/home/coder/.local/share/code-server","$$id-vscodeserver-data:/home/coder","$$id-keys-directory:/root/.ssh","$$id-theme-and-plugin-directory:/root/.local/share/code-server"],"environment":["PASSWORD=$$secret_password"],"ports":["8080"]}},"variables":[{"id":"$$secret_password","name":"PASSWORD","label":"Password","defaultValue":"$$generate_password","description":"","showOnConfiguration":true}]},{"templateVersion":"1.0.0","defaultVersion":"RELEASE.2022-10-15T19-57-03Z","documentation":"https://min.io/docs/minio","type":"minio","name":"MinIO","description":"A cloud storage server compatible with Amazon S3.","labels":["storage","s3"],"services":{"$$id":{"name":"MinIO","command":"server /data --console-address :9001","depends_on":[],"image":"minio/minio:$$core_version","volumes":["$$id-minio-data:/data","$$id-data-write:/files"],"environment":["MINIO_SERVER_URL=$$config_coolify_fqdn_minio_console","MINIO_BROWSER_REDIRECT_URL=$$config_minio_browser_redirect_url","MINIO_DOMAIN=$$config_minio_domain","MINIO_ROOT_USER=$$config_minio_root_user","MINIO_ROOT_PASSWORD=$$secret_minio_root_password"],"ports":["9000","9001"],"proxy":[{"port":"9000","domain":"$$config_coolify_fqdn_minio_console"},{"port":"9001"}]}},"variables":[{"id":"$$config_coolify_fqdn_minio_console","name":"MINIO_SERVER_URL","label":"MinIO Server URL","defaultValue":"","description":"Specify the URL hostname the MinIO Console should use for connecting to the MinIO Server.","required":true},{"id":"$$config_minio_browser_redirect_url","name":"MINIO_BROWSER_REDIRECT_URL","label":"Browser Redirect URL","defaultValue":"$$generate_fqdn","description":""},{"id":"$$config_minio_domain","name":"MINIO_DOMAIN","label":"Domain","defaultValue":"$$generate_domain","description":""},{"id":"$$config_minio_root_user","name":"MINIO_ROOT_USER","label":"Root User","defaultValue":"$$generate_username","description":""},{"id":"$$secret_minio_root_password","name":"MINIO_ROOT_PASSWORD","label":"Root User Password","defaultValue":"$$generate_password","description":"","showOnConfiguration":true}]},{"templateVersion":"1.0.0","defaultVersion":"0.21.1","documentation":"https://fider.io/docs","type":"fider","name":"Fider","description":"A platform to collect and organize customer feedback.","labels":["suggestion","feedback"],"services":{"$$id":{"name":"Fider","image":"getfider/fider:$$core_version","depends_on":["$$id-postgresql"],"environment":["BASE_URL=$$config_base_url","DATABASE_URL=$$secret_database_url","JWT_SECRET=$$secret_jwt_secret","EMAIL_NOREPLY=$$config_email_noreply","EMAIL_MAILGUN_API=$$secret_email_mailgun_api","EMAIL_MAILGUN_REGION=$$config_email_mailgun_region","EMAIL_MAILGUN_DOMAIN=$$config_email_mailgun_domain","EMAIL_SMTP_HOST=$$config_email_smtp_host","EMAIL_SMTP_PORT=$$config_email_smtp_port","EMAIL_SMTP_USER=$$config_email_smtp_user","EMAIL_SMTP_PASSWORD=$$secret_email_smtp_password","EMAIL_SMTP_ENABLE_STARTTLS=$$config_email_smtp_enable_starttls"],"ports":["3000"]},"$$id-postgresql":{"name":"PostgreSQL","depends_on":[],"image":"postgres:12-alpine","volumes":["$$id-postgresql-data:/var/lib/postgresql/data"],"environment":["POSTGRES_USER=$$config_postgres_user","POSTGRES_PASSWORD=$$secret_postgres_password","POSTGRES_DB=$$config_postgres_db"]}},"variables":[{"id":"$$config_base_url","name":"BASE_URL","label":"Base URL","defaultValue":"$$generate_fqdn","description":""},{"id":"$$secret_database_url","name":"DATABASE_URL","label":"Database URL for PostgreSQL","defaultValue":"postgresql://$$config_postgres_user:$$secret_postgres_password@$$id-postgresql:5432/$$config_postgres_db?sslmode=disable","description":""},{"id":"$$secret_jwt_secret","name":"JWT_SECRET","label":"JWT Secret","defaultValue":"$$generate_hex(64)","description":""},{"id":"$$config_email_noreply","name":"EMAIL_NOREPLY","label":"No Reply Email Address","defaultValue":"noreply@example.com","description":""},{"id":"$$secret_email_mailgun_api","name":"EMAIL_MAILGUN_API","label":"Mailgun API Key","defaultValue":"","description":"","showOnConfiguration":true},{"id":"$$config_email_mailgun_region","name":"EMAIL_MAILGUN_REGION","label":"Mailgun Region","defaultValue":"EU","description":""},{"id":"$$config_email_mailgun_domain","name":"EMAIL_MAILGUN_DOMAIN","label":"Mailgun Domain","defaultValue":"","description":""},{"id":"$$config_email_smtp_host","name":"EMAIL_SMTP_HOST","label":"SMTP Host","defaultValue":"","description":""},{"id":"$$config_email_smtp_port","name":"EMAIL_SMTP_PORT","label":"SMTP Port","defaultValue":"587","description":""},{"id":"$$config_email_smtp_user","name":"EMAIL_SMTP_USER","label":"SMTP User","defaultValue":"","description":""},{"id":"$$secret_email_smtp_password","name":"EMAIL_SMTP_PASSWORD","label":"SMTP Password","defaultValue":"","description":"","showOnConfiguration":true},{"id":"$$config_email_smtp_enable_starttls","name":"EMAIL_SMTP_ENABLE_STARTTLS","label":"SMTP Enable StartTLS","defaultValue":"false","description":""},{"id":"$$config_postgres_user","name":"POSTGRES_USER","label":"PostgreSQL User","defaultValue":"$$generate_username","description":""},{"id":"$$secret_postgres_password","name":"POSTGRES_PASSWORD","label":"PostgreSQL Password","defaultValue":"$$generate_password","description":""},{"id":"$$config_postgres_db","name":"POSTGRES_DB","label":"PostgreSQL Database","defaultValue":"$$generate_username","description":""}]},{"templateVersion":"1.0.0","defaultVersion":"0.198.1","documentation":"https://docs.n8n.io","type":"n8n","name":"n8n.io","description":"A free and open node based Workflow Automation Tool.","labels":["workflow","automation","ifttt","zapier","nodered"],"services":{"$$id":{"name":"N8n","depends_on":[],"image":"n8nio/n8n:$$core_version","volumes":["$$id-data:/root/.n8n","$$id-data-write:/files","/var/run/docker.sock:/var/run/docker.sock"],"environment":["WEBHOOK_URL=$$config_webhook_url"],"ports":["5678"]}},"variables":[{"id":"$$config_webhook_url","name":"WEBHOOK_URL","label":"Webhook URL","defaultValue":"$$generate_fqdn","description":""}]},{"templateVersion":"1.0.0","defaultVersion":"stable","documentation":"https://plausible.io/doc/","type":"plausibleanalytics","name":"Plausible Analytics","description":"A lightweight and open-source website analytics tool.","labels":["analytics","statistics","plausible","gdpr","no-cookie","google analytics"],"services":{"$$id":{"name":"Plausible Analytics","command":"sh -c \"sleep 10 && /entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh db init-admin && /entrypoint.sh run\"","depends_on":["$$id-postgresql","$$id-clickhouse"],"image":"plausible/analytics:$$core_version","environment":["ADMIN_USER_EMAIL=$$config_admin_user_email","ADMIN_USER_NAME=$$config_admin_user_name","ADMIN_USER_PWD=$$secret_admin_user_pwd","BASE_URL=$$config_base_url","SECRET_KEY_BASE=$$secret_secret_key_base","DISABLE_AUTH=$$config_disable_auth","DISABLE_REGISTRATION=$$config_disable_registration","DATABASE_URL=$$secret_database_url","CLICKHOUSE_DATABASE_URL=$$secret_clickhouse_database_url"],"ports":["8000"]},"$$id-postgresql":{"name":"PostgreSQL","image":"bitnami/postgresql:13.2.0","volumes":["$$id-postgresql-data:/bitnami/postgresql"],"environment":["POSTGRESQL_PASSWORD=$$secret_postgresql_password","POSTGRESQL_USERNAME=$$config_postgresql_username","POSTGRESQL_DATABASE=$$config_postgresql_database"]},"$$id-clickhouse":{"name":"Clickhouse","volumes":["$$id-clickhouse-data:/var/lib/clickhouse"],"image":"yandex/clickhouse-server:21.3.2.5","ulimits":{"nofile":{"soft":262144,"hard":262144}},"files":[{"location":"/etc/clickhouse-server/users.d/logging.xml","content":"warningtrue"},{"location":"/etc/clickhouse-server/config.d/logging.xml","content":"00"},{"location":"/docker-entrypoint-initdb.d/init.query","content":"CREATE DATABASE IF NOT EXISTS plausible;"},{"location":"/docker-entrypoint-initdb.d/init-db.sh","content":"clickhouse client --queries-file /docker-entrypoint-initdb.d/init.query"}]}},"variables":[{"id":"$$config_base_url","name":"BASE_URL","label":"Base URL","defaultValue":"$$generate_fqdn","description":"You must set this to the FQDN of the Plausible Analytics instance. This is used to generate the links to the Plausible Analytics instance."},{"id":"$$secret_database_url","name":"DATABASE_URL","label":"Database URL for PostgreSQL","defaultValue":"postgresql://$$config_postgresql_username:$$secret_postgresql_password@$$id-postgresql:5432/$$config_postgresql_database","description":""},{"id":"$$secret_clickhouse_database_url","name":"CLICKHOUSE_DATABASE_URL","label":"Database URL for Clickhouse","defaultValue":"http://$$id-clickhouse:8123/plausible","description":""},{"id":"$$config_admin_user_email","name":"ADMIN_USER_EMAIL","label":"Admin Email Address","defaultValue":"admin@example.com","description":"This is the admin email. Please change it."},{"id":"$$config_admin_user_name","name":"ADMIN_USER_NAME","label":"Admin User Name","defaultValue":"$$generate_username","description":"This is the admin username. Please change it."},{"id":"$$secret_admin_user_pwd","name":"ADMIN_USER_PWD","label":"Admin User Password","defaultValue":"$$generate_password","description":"This is the admin password. Please change it.","showOnConfiguration":true},{"id":"$$secret_secret_key_base","name":"SECRET_KEY_BASE","label":"Secret Key Base","defaultValue":"$$generate_hex(64)","description":""},{"id":"$$config_disable_auth","name":"DISABLE_AUTH","label":"Disable Authentication","defaultValue":"false","description":""},{"id":"$$config_disable_registration","name":"DISABLE_REGISTRATION","label":"Disable Registration","defaultValue":"true","description":""},{"id":"$$config_postgresql_username","main":"$$id-postgresql","name":"POSTGRESQL_USERNAME","label":"PostgreSQL Username","defaultValue":"postgresql","description":""},{"id":"$$secret_postgresql_password","main":"$$id-postgresql","name":"POSTGRESQL_PASSWORD","label":"PostgreSQL Password","defaultValue":"$$generate_password","description":"","showOnConfiguration":true},{"id":"$$config_postgresql_database","main":"$$id-postgresql","name":"POSTGRESQL_DATABASE","label":"PostgreSQL Database","defaultValue":"plausible","description":""},{"id":"$$config_scriptName","name":"SCRIPT_NAME","label":"Custom Script Name","defaultValue":"plausible.js","description":"This is the default script name."}]},{"templateVersion":"1.0.0","defaultVersion":"0.98.1","documentation":"https://docs.nocodb.com","type":"nocodb","name":"NocoDB","description":"Turns any MySQL, PostgreSQL, SQL Server, SQLite & MariaDB into a smart-spreadsheet.","labels":["database","airtable","spreadsheet"],"services":{"$$id":{"name":"NocoDB","image":"nocodb/nocodb:$$core_version","environment":["PORT=$$config_port","NC_DB=$$config_nc_db","DATABASE_URL=$$secret_database_url","NC_PUBLIC_URL=$$config_public_url","NC_AUTH_JWT_SECRET=$$secret_auth_jwt_secret","NC_SENTRY_DSN=$$secret_sentry_dsn","NC_CONNECT_TO_EXTERNAL_DB_DISABLED=$$config_connect_to_external_db_disabled","NC_DISABLE_TELE=$$config_disable_tele"],"volumes":["$$id-data:/usr/app/data"],"ports":["8080"]}},"variables":[{"id":"$$config_nc_db","name":"NC_DB","label":"Database","defaultValue":"","description":"MySQL, PostgreSQL and MSSQL connection urls supported. If absent: A local SQLite will be created in root folder."},{"id":"$$config_port","name":"PORT","label":"Port","defaultValue":"8080","description":""},{"id":"$$secret_database_url","name":"DATABASE_URL","label":"Database URL","defaultValue":"","description":"JDBC URL Format. Can be used instead of NC_DB. Used in 1-Click Heroku deployment."},{"id":"$$config_public_url","name":"NC_PUBLIC_URL","label":"Public URL","defaultValue":"","description":"Used for sending Email invitations. If absent: Best guess from http request params."},{"id":"$$secret_auth_jwt_secret","name":"NC_AUTH_JWT_SECRET","label":"Auth JWT Secret","defaultValue":"$$generate_hex(64)","description":"JWT secret used for auth and storing other secrets. If absent: A Random secret will be generated."},{"id":"$$secret_sentry_dsn","name":"NC_SENTRY_DSN","label":"Sentry DSN","defaultValue":"","description":"For Sentry monitoring."},{"id":"$$config_connect_to_external_db_disabled","name":"NC_CONNECT_TO_EXTERNAL_DB_DISABLED","label":"Disable External Database","defaultValue":"0","description":"Disable Project creation with external database. (Enter \"1\" to disable)."},{"id":"$$config_disable_tele","name":"NC_DISABLE_TELE","label":"NocoDB Disable Telemetry","defaultValue":"1","description":"Disable telemetry (Enter \"1\" to disable)."}]}] \ No newline at end of file +[{"templateVersion":"1.0.0","defaultVersion":"0.8.0","documentation":"https://pocketbase.io/docs/","type":"pocketbase","name":"Pocketbase","description":"Open Source realtime backend in 1 file","services":{"$$id":{"image":"coollabsio/pocketbase:$$core_version","volumes":["$$id-data:/app/pb_data"],"ports":["8080"]}}},{"templateVersion":"1.0.0","defaultVersion":"1.5.0-rc.0","documentation":"https://plausible.io/doc/","type":"plausibleanalytics-arm","name":"Plausible Analytics (ARM)","description":"A lightweight and open-source website analytics tool.","labels":["analytics","statistics","plausible","gdpr","no-cookie","google analytics"],"services":{"$$id":{"name":"Plausible Analytics","command":"sh -c \"sleep 10 && /entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh db init-admin && /entrypoint.sh run\"","depends_on":["$$id-postgresql","$$id-clickhouse"],"image":"plausible/analytics:$$core_version","environment":["ADMIN_USER_EMAIL=$$config_admin_user_email","ADMIN_USER_NAME=$$config_admin_user_name","ADMIN_USER_PWD=$$secret_admin_user_pwd","BASE_URL=$$config_base_url","SECRET_KEY_BASE=$$secret_secret_key_base","DISABLE_AUTH=$$config_disable_auth","DISABLE_REGISTRATION=$$config_disable_registration","DATABASE_URL=$$secret_database_url","CLICKHOUSE_DATABASE_URL=$$secret_clickhouse_database_url"],"ports":["8000"]},"$$id-postgresql":{"name":"PostgreSQL","image":"postgres:14-alpine","volumes":["$$id-postgresql-data:/var/lib/postgresql/data"],"environment":["POSTGRES_PASSWORD=$$secret_postgres_password","POSTGRES_USER=$$config_postgres_user","POSTGRES_DB=$$config_postgres_db"]},"$$id-clickhouse":{"name":"Clickhouse","volumes":["$$id-clickhouse-data:/var/lib/clickhouse"],"image":"clickhouse/clickhouse-server:22.6-alpine","ulimits":{"nofile":{"soft":262144,"hard":262144}},"files":[{"location":"/etc/clickhouse-server/users.d/logging.xml","content":"warningtrue"},{"location":"/etc/clickhouse-server/config.d/logging.xml","content":"00"},{"location":"/docker-entrypoint-initdb.d/init.query","content":"CREATE DATABASE IF NOT EXISTS plausible;"},{"location":"/docker-entrypoint-initdb.d/init-db.sh","content":"clickhouse client --queries-file /docker-entrypoint-initdb.d/init.query"}]}},"variables":[{"id":"$$config_base_url","name":"BASE_URL","label":"Base URL","defaultValue":"$$generate_fqdn","description":"You must set this to the FQDN of the Plausible Analytics instance. This is used to generate the links to the Plausible Analytics instance."},{"id":"$$secret_database_url","name":"DATABASE_URL","label":"Database URL for PostgreSQL","defaultValue":"postgresql://$$config_postgres_user:$$secret_postgres_password@$$id-postgresql:5432/$$config_postgres_db","description":""},{"id":"$$secret_clickhouse_database_url","name":"CLICKHOUSE_DATABASE_URL","label":"Database URL for Clickhouse","defaultValue":"http://$$id-clickhouse:8123/plausible","description":""},{"id":"$$config_admin_user_email","name":"ADMIN_USER_EMAIL","label":"Admin Email Address","defaultValue":"admin@example.com","description":"This is the admin email. Please change it."},{"id":"$$config_admin_user_name","name":"ADMIN_USER_NAME","label":"Admin User Name","defaultValue":"$$generate_username","description":"This is the admin username. Please change it."},{"id":"$$secret_admin_user_pwd","name":"ADMIN_USER_PWD","label":"Admin User Password","defaultValue":"$$generate_password","description":"This is the admin password. Please change it.","showOnConfiguration":true},{"id":"$$secret_secret_key_base","name":"SECRET_KEY_BASE","label":"Secret Key Base","defaultValue":"$$generate_hex(64)","description":""},{"id":"$$config_disable_auth","name":"DISABLE_AUTH","label":"Disable Authentication","defaultValue":"false","description":""},{"id":"$$config_disable_registration","name":"DISABLE_REGISTRATION","label":"Disable Registration","defaultValue":"true","description":""},{"id":"$$config_postgres_user","main":"$$id-postgresql","name":"POSTGRES_USER","label":"PostgreSQL Username","defaultValue":"postgresql","description":""},{"id":"$$secret_postgres_password","main":"$$id-postgresql","name":"POSTGRES_PASSWORD","label":"PostgreSQL Password","defaultValue":"$$generate_password","description":"","showOnConfiguration":true},{"id":"$$config_postgres_db","main":"$$id-postgresql","name":"POSTGRES_DB","label":"PostgreSQL Database","defaultValue":"plausible","description":""},{"id":"$$config_scriptName","name":"SCRIPT_NAME","label":"Custom Script Name","defaultValue":"plausible.js","description":"This is the default script name."}]},{"templateVersion":"1.0.0","defaultVersion":"1.17","documentation":"https://docs.gitea.io","type":"gitea","name":"Gitea","description":"Gitea is a community managed lightweight code hosting solution written in Go.","labels":["storage","git"],"services":{"$$id":{"name":"Gitea","documentation":"https://docs.gitea.io","image":"gitea/gitea:$$core_version","volumes":["$$id-data:/data","/etc/timezone:/etc/timezone:ro","/etc/localtime:/etc/localtime:ro"],"environment":["USER_UID=1000","USER_GID=1000","DOMAIN=$$config_domain","SSH_DOMAIN=$$config_ssh_domain","ROOT_URL=$$config_root_url","SECRET_KEY=$$secret_secret_key","INTERNAL_TOKEN=$$secret_internal_token","SSH_PORT=22","START_SSH_SERVER=$$config_start_ssh_server"],"ports":["3000","22"],"proxy":[{"port":"22","hostPort":"$$config_hostport_ssh"}]}},"variables":[{"id":"$$config_hostport_ssh","name":"SSH_PORT","label":"SSH Port","defaultValue":"8022","description":"","required":true},{"id":"$$config_domain","name":"DOMAIN","label":"Domain","defaultValue":"$$generate_domain","description":""},{"id":"$$config_ssh_domain","name":"SSH_DOMAIN","label":"SSH Domain","defaultValue":"$$generate_domain","description":""},{"id":"$$config_start_ssh_server","name":"START_SSH_SERVER","label":"Start SSH Server","defaultValue":"true","description":""},{"id":"$$config_root_url","name":"ROOT_URL","label":"Root URL of Gitea","defaultValue":"$$generate_fqdn_slash","description":""},{"id":"$$secret_secret_key","name":"SECRET_KEY","label":"Secret Key","defaultValue":"$$generate_hex(32)","description":""},{"id":"$$secret_internal_token","name":"INTERNAL_TOKEN","label":"Internal JWT Token","defaultValue":"$$generate_token","description":""}]},{"templateVersion":"1.0.0","defaultVersion":"20.0","documentation":"https://www.keycloak.org/documentation","type":"keycloak","name":"Keycloak","description":"Keycloak provides user federation, strong authentication, user management, fine-grained authorization, and more.","labels":["authentication","authorization","oidconnect","saml2"],"services":{"$$id":{"name":"Keycloak","command":"start --db=postgres --features=token-exchange --import-realm","depends_on":["$$id-postgresql"],"image":"quay.io/keycloak/keycloak:$$core_version","volumes":["$$id-import:/opt/keycloak/data/import"],"environment":["KC_HEALTH_ENABLED=true","KC_PROXY=edge","KC_DB=postgres","KC_HOSTNAME=$$config_keycloak_domain","KEYCLOAK_ADMIN=$$config_admin_user","KEYCLOAK_ADMIN_PASSWORD=$$secret_keycloak_admin_password","KC_DB_PASSWORD=$$secret_postgres_password","KC_DB_USERNAME=$$config_postgres_user","KC_DB_URL=$$secret_keycloak_database_url"],"ports":["8080"]},"$$id-postgresql":{"name":"PostgreSQL","depends_on":[],"image":"postgres:14-alpine","volumes":["$$id-postgresql-data:/var/lib/postgresql/data"],"environment":["POSTGRES_USER=$$config_postgres_user","POSTGRES_PASSWORD=$$secret_postgres_password","POSTGRES_DB=$$config_postgres_db"],"ports":[]}},"variables":[{"id":"$$config_keycloak_domain","name":"KEYCLOAK_DOMAIN","label":"Keycloak Domain","defaultValue":"$$generate_domain","description":""},{"id":"$$secret_keycloak_database_url","name":"KEYCLOAK_DATABASE_URL","label":"Keycloak Database Url","defaultValue":"jdbc:postgresql://$$id-postgresql:5432/$$config_postgres_db","description":""},{"id":"$$config_admin_user","name":"KEYCLOAK_ADMIN","label":"Keycloak Admin User","defaultValue":"$$generate_username","description":""},{"id":"$$secret_keycloak_admin_password","name":"KEYCLOAK_ADMIN_PASSWORD","label":"Keycloak Admin Password","defaultValue":"$$generate_password","description":"","showOnConfiguration":true},{"id":"$$config_postgres_user","main":"$$id-postgresql","name":"POSTGRES_USER","label":"PostgreSQL User","defaultValue":"$$generate_username","description":""},{"id":"$$secret_postgres_password","main":"$$id-postgresql","name":"POSTGRES_PASSWORD","label":"PostgreSQL Password","defaultValue":"$$generate_password","description":"","showOnConfiguration":true},{"id":"$$config_postgres_db","main":"$$id-postgresql","name":"POSTGRES_DB","label":"PostgreSQL Database","defaultValue":"keycloak","description":""}]},{"templateVersion":"1.0.0","defaultVersion":"v3.6","documentation":"https://github.com/freyacodes/Lavalink","description":"Standalone audio sending node based on Lavaplayer.","type":"lavalink","name":"Lavalink","labels":["discord","discord bot","audio","lavalink","jda"],"services":{"$$id":{"name":"Lavalink","image":"fredboat/lavalink:$$core_version","environment":[],"volumes":["$$id-lavalink:/lavalink"],"ports":["2333"],"files":[{"location":"/opt/Lavalink/application.yml","content":"server:\n port: $$config_port\n address: 0.0.0.0\nlavalink:\n server:\n password: \"$$secret_password\"\n sources:\n youtube: true\n bandcamp: true\n soundcloud: true\n twitch: true\n vimeo: true\n http: true\n local: false\n\nlogging:\n file:\n path: ./logs/\n\n level:\n root: INFO\n lavalink: INFO\n\n logback:\n rollingpolicy:\n max-file-size: 1GB\n max-history: 30"}]}},"variables":[{"id":"$$config_port","name":"PORT","label":"Port","defaultValue":"2333","required":true},{"id":"$$secret_password","name":"PASSWORD","label":"Password","defaultValue":"$$generate_password","required":true}]},{"templateVersion":"1.0.0","defaultVersion":"v1.8.6","documentation":"https://docs.appsmith.com/getting-started/setup/instance-configuration/","type":"appsmith","name":"Appsmith","description":"Fastest way to build internal apps over any database or API.","services":{"$$id":{"image":"appsmith/appsmith-ce:$$core_version","environment":["APPSMITH_MAIL_ENABLED=$$config_appsmith_mail_enabled","APPSMITH_DISABLE_TELEMETRY=$$config_appsmith_disable_telemetry","APPSMITH_DISABLE_INTERCOM=$$config_appsmith_disable_intercom"],"volumes":["$$id-stacks-data:/appsmith-stacks"],"ports":["80"]}},"variables":[{"id":"$$config_appsmith_mail_enabled","name":"APPSMITH_MAIL_ENABLED","label":"Enable Mail","defaultValue":"false","description":""},{"id":"$$config_appsmith_disable_telemetry","name":"APPSMITH_DISABLE_TELEMETRY","label":"Disable Telemetry","defaultValue":"true","description":""},{"id":"$$config_appsmith_disable_intercom","name":"APPSMITH_DISABLE_INTERCOM","label":"Disable Intercom","defaultValue":"true","description":""}]},{"templateVersion":"1.0.0","defaultVersion":"0.56.2","documentation":"https://hub.docker.com/r/zadam/trilium","description":"A hierarchical note taking application with focus on building large personal knowledge bases.","labels":["personal","knowledge","notes","wiki"],"type":"trilium","name":"Trilium Notes","services":{"$$id":{"image":"zadam/trilium:$$core_version","environment":[],"volumes":["$$id-trilium:/home/node/trilium-data"],"ports":["8080"]}},"variables":[]},{"templateVersion":"1.0.0","defaultVersion":"1.9.2","documentation":"https://hub.docker.com/r/louislam/uptime-kuma","description":"A free & fancy self-hosted monitoring tool.","labels":["uptime"],"type":"uptimekuma","name":"UptimeKuma","services":{"$$id":{"image":"louislam/uptime-kuma:$$core_version","environment":[],"volumes":["$$id-uptimekuma:/app/data"],"ports":["3001"]}},"variables":[]},{"templateVersion":"1.0.0","defaultVersion":"5.8","documentation":"https://hub.docker.com/r/silviof/docker-languagetool","description":"A multilingual grammar, style and spell checker.","type":"languagetool","name":"LanguageTool","services":{"$$id":{"image":"silviof/docker-languagetool:$$core_version","environment":[],"volumes":["$$id-ngrams:/ngrams"],"ports":["8010"]}},"variables":[]},{"templateVersion":"1.0.0","defaultVersion":"1.26.0","documentation":"https://hub.docker.com/r/vaultwarden/server","description":"Bitwarden compatible server written in Rust.","type":"vaultwarden","name":"VaultWarden","labels":["bitwarden","password manager"],"services":{"$$id":{"image":"vaultwarden/server:$$core_version","environment":[],"volumes":["$$id-data:/data"],"ports":["80"]}},"variables":[]},{"templateVersion":"1.0.0","defaultVersion":"9.2.3","documentation":"https://hub.docker.com/r/grafana/grafana","type":"grafana","name":"Grafana","description":"Grafana allows you to query, visualize, alert on and understand your metrics.","labels":["monitoring","metrics","dashboard"],"services":{"$$id":{"image":"grafana/grafana:$$core_version","environment":[],"volumes":["$$id-config:/etc/grafana","$$id-grafana:/var/lib/grafana"],"ports":["3000"]}},"variables":[]},{"templateVersion":"1.0.0","defaultVersion":"1.0.3","documentation":"https://appwrite.io/docs","type":"appwrite","name":"Appwrite","description":"Secure Backend Server for Web, Mobile & Flutter Developers.","labels":["serverless","backend","storage","api"],"services":{"$$id":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_WORKER_PER_CORE=$$config__app_worker_per_core","_APP_LOCALE=$$config__app_locale","_APP_CONSOLE_WHITELIST_ROOT=$$config__app_console_whitelist_root","_APP_CONSOLE_WHITELIST_EMAILS=$$config__app_console_whitelist_emails","_APP_CONSOLE_WHITELIST_IPS=$$config__app_console_whitelist_ips","_APP_SYSTEM_EMAIL_NAME=$$config__app_system_email_name","_APP_SYSTEM_EMAIL_ADDRESS=$$config__app_system_email_address","_APP_SYSTEM_SECURITY_EMAIL_ADDRESS=$$config__app_system_security_email_address","_APP_SYSTEM_RESPONSE_FORMAT=$$config__app_system_response_format","_APP_OPTIONS_ABUSE=$$config__app_options_abuse","_APP_OPTIONS_FORCE_HTTPS=$$config__app_options_force_https","_APP_OPENSSL_KEY_V1=$$secret__app_openssl_key_v1","_APP_DOMAIN=$$config__app_domain","_APP_DOMAIN_TARGET=$$config__app_domain_target","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_REDIS_USER=$$config__app_redis_user","_APP_REDIS_PASS=$$secret__app_redis_pass","_APP_DB_HOST=$$config__app_db_host","_APP_DB_PORT=$$config__app_db_port","_APP_DB_SCHEMA=$$config__app_db_schema","_APP_DB_USER=$$config__app_db_user","_APP_DB_PASS=$$secret__app_db_pass","_APP_SMTP_HOST=$$config__app_smtp_host","_APP_SMTP_PORT=$$config__app_smtp_port","_APP_SMTP_SECURE=$$config__app_smtp_secure","_APP_SMTP_USERNAME=$$config__app_smtp_username","_APP_SMTP_PASSWORD=$$secret__app_smtp_password","_APP_USAGE_STATS=$$config__app_usage_stats","_APP_INFLUXDB_HOST=$$config__app_influxdb_host","_APP_INFLUXDB_PORT=$$config__app_influxdb_port","_APP_STORAGE_LIMIT=$$config__app_storage_limit","_APP_STORAGE_PREVIEW_LIMIT=$$config__app_storage_preview_limit","_APP_STORAGE_ANTIVIRUS=$$config__app_storage_antivirus_enabled","_APP_STORAGE_ANTIVIRUS_HOST=$$config__app_storage_antivirus_host","_APP_STORAGE_ANTIVIRUS_PORT=$$config__app_storage_antivirus_port","_APP_STORAGE_DEVICE=$$config__app_storage_device","_APP_STORAGE_S3_ACCESS_KEY=$$secret__app_storage_s3_access_key","_APP_STORAGE_S3_SECRET=$$secret__app_storage_s3_secret","_APP_STORAGE_S3_REGION=$$config__app_storage_s3_region","_APP_STORAGE_S3_BUCKET=$$config__app_storage_s3_bucket","_APP_STORAGE_DO_SPACES_ACCESS_KEY=$$secret__app_storage_do_spaces_access_key","_APP_STORAGE_DO_SPACES_SECRET=$$secret__app_storage_do_spaces_secret","_APP_STORAGE_DO_SPACES_REGION=$$config__app_storage_do_spaces_region","_APP_STORAGE_DO_SPACES_BUCKET=$$config__app_storage_do_spaces_bucket","_APP_STORAGE_BACKBLAZE_ACCESS_KEY=$$secret__app_storage_backblaze_access_key","_APP_STORAGE_BACKBLAZE_SECRET=$$secret__app_storage_backblaze_secret","_APP_STORAGE_BACKBLAZE_REGION=$$config__app_storage_backblaze_region","_APP_STORAGE_BACKBLAZE_BUCKET=$$config__app_storage_backblaze_bucket","_APP_STORAGE_LINODE_ACCESS_KEY=$$secret__app_storage_linode_access_key","_APP_STORAGE_LINODE_SECRET=$$secret__app_storage_linode_secret","_APP_STORAGE_LINODE_REGION=$$config__app_storage_linode_region","_APP_STORAGE_LINODE_BUCKET=$$config__app_storage_linode_bucket","_APP_STORAGE_WASABI_ACCESS_KEY=$$secret__app_storage_wasabi_access_key","_APP_STORAGE_WASABI_SECRET=$$secret__app_storage_wasabi_secret","_APP_STORAGE_WASABI_REGION=$$config__app_storage_wasabi_region","_APP_STORAGE_WASABI_BUCKET=$$config__app_storage_wasabi_bucket","_APP_FUNCTIONS_SIZE_LIMIT=$$config__app_functions_size_limit","_APP_FUNCTIONS_TIMEOUT=$$config__app_functions_timeout","_APP_FUNCTIONS_BUILD_TIMEOUT=$$config__app_functions_build_timeout","_APP_FUNCTIONS_CONTAINERS=$$config__app_functions_containers","_APP_FUNCTIONS_CPUS=$$config__app_functions_cpus","_APP_FUNCTIONS_MEMORY=$$config__app_functions_memory_allocated","_APP_FUNCTIONS_MEMORY_SWAP=$$config__app_functions_memory_swap","_APP_FUNCTIONS_RUNTIMES=$$config__app_functions_runtimes","_APP_EXECUTOR_SECRET=$$secret__app_executor_secret","_APP_EXECUTOR_HOST=$$config__app_executor_host","_APP_LOGGING_PROVIDER=$$config__app_logging_provider","_APP_LOGGING_CONFIG=$$config__app_logging_config","_APP_STATSD_HOST=$$config__app_statsd_host","_APP_STATSD_PORT=$$config__app_statsd_port","_APP_MAINTENANCE_INTERVAL=$$config__app_maintenance_interval","_APP_MAINTENANCE_RETENTION_EXECUTION=$$config__app_maintenance_retention_execution","_APP_MAINTENANCE_RETENTION_CACHE=$$config__app_maintenance_retention_cache","_APP_MAINTENANCE_RETENTION_ABUSE=$$config__app_maintenance_retention_abuse","_APP_MAINTENANCE_RETENTION_AUDIT=$$config__app_maintenance_retention_audit","_APP_SMS_PROVIDER=$$config__app_sms_provider","_APP_SMS_FROM=$$config__app_sms_from","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":["$$id-uploads:/storage/uploads","$$id-cache:/storage/cache","$$id-config:/storage/config","$$id-certificates:/storage/certificates","$$id-functions:/storage/functions"],"ports":["80"],"proxy":[{"port":"80"}]},"$$id-executor":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_FUNCTIONS_TIMEOUT=$$config__app_functions_timeout","_APP_FUNCTIONS_BUILD_TIMEOUT=$$config__app_functions_build_timeout","_APP_FUNCTIONS_CONTAINERS=$$config__app_functions_containers","_APP_FUNCTIONS_RUNTIMES=$$config__app_functions_runtimes","_APP_FUNCTIONS_CPUS=$$config__app_functions_cpus","_APP_FUNCTIONS_MEMORY=$$config__app_functions_memory_allocated","_APP_FUNCTIONS_MEMORY_SWAP=$$config__app_functions_memory_swap","_APP_FUNCTIONS_INACTIVE_THRESHOLD=$$config__app_functions_inactive_threshold","_APP_EXECUTOR_SECRET=$$secret__app_executor_secret","_APP_LOGGING_PROVIDER=$$config__app_logging_provider","_APP_LOGGING_CONFIG=$$config__app_logging_config","_APP_STORAGE_DEVICE=$$config__app_storage_device","_APP_STORAGE_S3_ACCESS_KEY=$$secret__app_storage_s3_access_key","_APP_STORAGE_S3_SECRET=$$secret__app_storage_s3_secret","_APP_STORAGE_S3_REGION=$$config__app_storage_s3_region","_APP_STORAGE_S3_BUCKET=$$config__app_storage_s3_bucket","_APP_STORAGE_DO_SPACES_ACCESS_KEY=$$secret__app_storage_do_spaces_access_key","_APP_STORAGE_DO_SPACES_SECRET=$$secret__app_storage_do_spaces_secret","_APP_STORAGE_DO_SPACES_REGION=$$config__app_storage_do_spaces_region","_APP_STORAGE_DO_SPACES_BUCKET=$$config__app_storage_do_spaces_bucket","_APP_STORAGE_BACKBLAZE_ACCESS_KEY=$$secret__app_storage_backblaze_access_key","_APP_STORAGE_BACKBLAZE_SECRET=$$secret__app_storage_backblaze_secret","_APP_STORAGE_BACKBLAZE_REGION=$$config__app_storage_backblaze_region","_APP_STORAGE_BACKBLAZE_BUCKET=$$config__app_storage_backblaze_bucket","_APP_STORAGE_LINODE_ACCESS_KEY=$$secret__app_storage_linode_access_key","_APP_STORAGE_LINODE_SECRET=$$secret__app_storage_linode_secret","_APP_STORAGE_LINODE_REGION=$$config__app_storage_linode_region","_APP_STORAGE_LINODE_BUCKET=$$config__app_storage_linode_bucket","_APP_STORAGE_WASABI_ACCESS_KEY=$$secret__app_storage_wasabi_access_key","_APP_STORAGE_WASABI_SECRET=$$secret__app_storage_wasabi_secret","_APP_STORAGE_WASABI_REGION=$$config__app_storage_wasabi_region","_APP_STORAGE_WASABI_BUCKET=$$config__app_storage_wasabi_bucket","DOCKERHUB_PULL_USERNAME=$$config_dockerhub_pull_username","DOCKERHUB_PULL_PASSWORD=$$secret_dockerhub_pull_password","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":["$$id-functions:/storage/functions","$$id-builds:/storage/builds","/var/run/docker.sock:/var/run/docker.sock"],"entrypoint":"executor"},"$$id-influxdb":{"image":"appwrite/influxdb:1.5.0","environment":[],"volumes":["$$id-influxdb:/var/lib/influxdb"]},"$$id-maintenance":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_OPENSSL_KEY_V1=$$secret__app_openssl_key_v1","_APP_DOMAIN=$$config__app_domain","_APP_DOMAIN_TARGET=$$config__app_domain_target","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_REDIS_USER=$$config__app_redis_user","_APP_REDIS_PASS=$$secret__app_redis_pass","_APP_DB_HOST=$$config__app_db_host","_APP_DB_PORT=$$config__app_db_port","_APP_DB_SCHEMA=$$config__app_db_schema","_APP_DB_USER=$$config__app_db_user","_APP_DB_PASS=$$secret__app_db_pass","_APP_MAINTENANCE_INTERVAL=$$config__app_maintenance_interval","_APP_MAINTENANCE_RETENTION_EXECUTION=$$config__app_maintenance_retention_execution","_APP_MAINTENANCE_RETENTION_CACHE=$$config__app_maintenance_retention_cache","_APP_MAINTENANCE_RETENTION_ABUSE=$$config__app_maintenance_retention_abuse","_APP_MAINTENANCE_RETENTION_AUDIT=$$config__app_maintenance_retention_audit","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":[],"entrypoint":"maintenance"},"$$id-mariadb":{"image":"mariadb:10.7","command":"--innodb-flush-method fsync","environment":["MARIADB_ROOT_PASSWORD=$$secret__app_db_root_pass","MARIADB_DATABASE=$$config__app_db_schema","MARIADB_USER=$$config__app_db_user","MARIADB_PASSWORD=$$secret__app_db_pass","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":["$$id-mariadb:/var/lib/mysql"]},"$$id-realtime":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_WORKER_PER_CORE=$$config__app_worker_per_core","_APP_OPTIONS_ABUSE=$$config__app_options_abuse","_APP_OPENSSL_KEY_V1=$$secret__app_openssl_key_v1","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_DB_HOST=$$config__app_db_host","_APP_DB_PORT=$$config__app_db_port","_APP_DB_SCHEMA=$$config__app_db_schema","_APP_DB_USER=$$config__app_db_user","_APP_DB_PASS=$$secret__app_db_pass","_APP_USAGE_STATS=$$config__app_usage_stats","_APP_LOGGING_PROVIDER=$$config__app_logging_provider","_APP_LOGGING_CONFIG=$$config__app_logging_config","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":[],"entrypoint":"realtime","proxy":[{"port":"80","pathPrefix":"/v1/realtime"}]},"$$id-redis":{"image":"redis:7.0.4-alpine","command":"--maxmemory 512mb --maxmemory-policy allkeys-lru --maxmemory-samples 5","environment":[],"volumes":["$$id-redis:/data"]},"$$id-schedule":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_REDIS_USER=$$config__app_redis_user","_APP_REDIS_PASS=$$secret__app_redis_pass","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":[],"entrypoint":"schedule"},"$$id-telegraf":{"image":"appwrite/telegraf:1.4.0","environment":["_APP_INFLUXDB_HOST=$$config__app_influxdb_host","_APP_INFLUXDB_PORT=$$config__app_influxdb_port","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":["$$id-influxdb:/var/lib/influxdb"]},"$$id-usage-database":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_OPENSSL_KEY_V1=$$secret__app_openssl_key_v1","_APP_DB_HOST=$$config__app_db_host","_APP_DB_PORT=$$config__app_db_port","_APP_DB_SCHEMA=$$config__app_db_schema","_APP_DB_USER=$$config__app_db_user","_APP_DB_PASS=$$secret__app_db_pass","_APP_INFLUXDB_HOST=$$config__app_influxdb_host","_APP_INFLUXDB_PORT=$$config__app_influxdb_port","_APP_USAGE_TIMESERIES_INTERVAL=$$config__app_usage_timeseries_interval","_APP_USAGE_DATABASE_INTERVAL=$$config__app_usage_database_interval","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_REDIS_USER=$$config__app_redis_user","_APP_REDIS_PASS=$$secret__app_redis_pass","_APP_LOGGING_PROVIDER=$$config__app_logging_provider","_APP_LOGGING_CONFIG=$$config__app_logging_config","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":[],"entrypoint":"usage --type database"},"$$id-usage":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_OPENSSL_KEY_V1=$$secret__app_openssl_key_v1","_APP_DB_HOST=$$config__app_db_host","_APP_DB_PORT=$$config__app_db_port","_APP_DB_SCHEMA=$$config__app_db_schema","_APP_DB_USER=$$config__app_db_user","_APP_DB_PASS=$$secret__app_db_pass","_APP_INFLUXDB_HOST=$$config__app_influxdb_host","_APP_INFLUXDB_PORT=$$config__app_influxdb_port","_APP_USAGE_TIMESERIES_INTERVAL=$$config__app_usage_timeseries_interval","_APP_USAGE_DATABASE_INTERVAL=$$config__app_usage_database_interval","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_REDIS_USER=$$config__app_redis_user","_APP_REDIS_PASS=$$secret__app_redis_pass","_APP_LOGGING_PROVIDER=$$config__app_logging_provider","_APP_LOGGING_CONFIG=$$config__app_logging_config","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":[],"entrypoint":"usage --type timeseries"},"$$id-worker-audits":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_OPENSSL_KEY_V1=$$secret__app_openssl_key_v1","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_REDIS_USER=$$config__app_redis_user","_APP_REDIS_PASS=$$secret__app_redis_pass","_APP_DB_HOST=$$config__app_db_host","_APP_DB_PORT=$$config__app_db_port","_APP_DB_SCHEMA=$$config__app_db_schema","_APP_DB_USER=$$config__app_db_user","_APP_DB_PASS=$$secret__app_db_pass","_APP_LOGGING_PROVIDER=$$config__app_logging_provider","_APP_LOGGING_CONFIG=$$config__app_logging_config","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":[],"entrypoint":"worker-audits"},"$$id-worker-builds":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_OPENSSL_KEY_V1=$$secret__app_openssl_key_v1","_APP_EXECUTOR_SECRET=$$secret__app_executor_secret","_APP_EXECUTOR_HOST=$$config__app_executor_host","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_REDIS_USER=$$config__app_redis_user","_APP_REDIS_PASS=$$secret__app_redis_pass","_APP_DB_HOST=$$config__app_db_host","_APP_DB_PORT=$$config__app_db_port","_APP_DB_SCHEMA=$$config__app_db_schema","_APP_DB_USER=$$config__app_db_user","_APP_DB_PASS=$$secret__app_db_pass","_APP_LOGGING_PROVIDER=$$config__app_logging_provider","_APP_LOGGING_CONFIG=$$config__app_logging_config","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":[],"entrypoint":"worker-builds"},"$$id-worker-certificates":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_OPENSSL_KEY_V1=$$secret__app_openssl_key_v1","_APP_DOMAIN=$$config__app_domain","_APP_DOMAIN_TARGET=$$config__app_domain_target","_APP_SYSTEM_SECURITY_EMAIL_ADDRESS=$$config__app_system_security_email_address","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_REDIS_USER=$$config__app_redis_user","_APP_REDIS_PASS=$$secret__app_redis_pass","_APP_DB_HOST=$$config__app_db_host","_APP_DB_PORT=$$config__app_db_port","_APP_DB_SCHEMA=$$config__app_db_schema","_APP_DB_USER=$$config__app_db_user","_APP_DB_PASS=$$secret__app_db_pass","_APP_LOGGING_PROVIDER=$$config__app_logging_provider","_APP_LOGGING_CONFIG=$$config__app_logging_config","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":["$$id-config:/storage/config","$$id-certificates:/storage/certificates"],"entrypoint":"worker-certificates"},"$$id-worker-databases":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_OPENSSL_KEY_V1=$$secret__app_openssl_key_v1","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_REDIS_USER=$$config__app_redis_user","_APP_REDIS_PASS=$$secret__app_redis_pass","_APP_DB_HOST=$$config__app_db_host","_APP_DB_PORT=$$config__app_db_port","_APP_DB_SCHEMA=$$config__app_db_schema","_APP_DB_USER=$$config__app_db_user","_APP_DB_PASS=$$secret__app_db_pass","_APP_LOGGING_PROVIDER=$$config__app_logging_provider","_APP_LOGGING_CONFIG=$$config__app_logging_config","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":[],"entrypoint":"worker-databases"},"$$id-worker-deletes":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_OPENSSL_KEY_V1=$$secret__app_openssl_key_v1","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_REDIS_USER=$$config__app_redis_user","_APP_REDIS_PASS=$$secret__app_redis_pass","_APP_DB_HOST=$$config__app_db_host","_APP_DB_PORT=$$config__app_db_port","_APP_DB_SCHEMA=$$config__app_db_schema","_APP_DB_USER=$$config__app_db_user","_APP_DB_PASS=$$secret__app_db_pass","_APP_STORAGE_DEVICE=$$config__app_storage_device","_APP_STORAGE_S3_ACCESS_KEY=$$secret__app_storage_s3_access_key","_APP_STORAGE_S3_SECRET=$$secret__app_storage_s3_secret","_APP_STORAGE_S3_REGION=$$config__app_storage_s3_region","_APP_STORAGE_S3_BUCKET=$$config__app_storage_s3_bucket","_APP_STORAGE_DO_SPACES_ACCESS_KEY=$$secret__app_storage_do_spaces_access_key","_APP_STORAGE_DO_SPACES_SECRET=$$secret__app_storage_do_spaces_secret","_APP_STORAGE_DO_SPACES_REGION=$$config__app_storage_do_spaces_region","_APP_STORAGE_DO_SPACES_BUCKET=$$config__app_storage_do_spaces_bucket","_APP_STORAGE_BACKBLAZE_ACCESS_KEY=$$secret__app_storage_backblaze_access_key","_APP_STORAGE_BACKBLAZE_SECRET=$$secret__app_storage_backblaze_secret","_APP_STORAGE_BACKBLAZE_REGION=$$config__app_storage_backblaze_region","_APP_STORAGE_BACKBLAZE_BUCKET=$$config__app_storage_backblaze_bucket","_APP_STORAGE_LINODE_ACCESS_KEY=$$secret__app_storage_linode_access_key","_APP_STORAGE_LINODE_SECRET=$$secret__app_storage_linode_secret","_APP_STORAGE_LINODE_REGION=$$config__app_storage_linode_region","_APP_STORAGE_LINODE_BUCKET=$$config__app_storage_linode_bucket","_APP_STORAGE_WASABI_ACCESS_KEY=$$secret__app_storage_wasabi_access_key","_APP_STORAGE_WASABI_SECRET=$$secret__app_storage_wasabi_secret","_APP_STORAGE_WASABI_REGION=$$config__app_storage_wasabi_region","_APP_STORAGE_WASABI_BUCKET=$$config__app_storage_wasabi_bucket","_APP_LOGGING_PROVIDER=$$config__app_logging_provider","_APP_LOGGING_CONFIG=$$config__app_logging_config","_APP_EXECUTOR_SECRET=$$secret__app_executor_secret","_APP_EXECUTOR_HOST=$$config__app_executor_host","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":["$$id-uploads:/storage/uploads","$$id-cache:/storage/cache","$$id-functions:/storage/functions","$$id-builds:/storage/builds","$$id-certificates:/storage/certificates"],"entrypoint":"worker-deletes"},"$$id-worker-functions":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_OPENSSL_KEY_V1=$$secret__app_openssl_key_v1","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_REDIS_USER=$$config__app_redis_user","_APP_REDIS_PASS=$$secret__app_redis_pass","_APP_DB_HOST=$$config__app_db_host","_APP_DB_PORT=$$config__app_db_port","_APP_DB_SCHEMA=$$config__app_db_schema","_APP_DB_USER=$$config__app_db_user","_APP_DB_PASS=$$secret__app_db_pass","_APP_FUNCTIONS_TIMEOUT=$$config__app_functions_timeout","_APP_EXECUTOR_SECRET=$$secret__app_executor_secret","_APP_EXECUTOR_HOST=$$config__app_executor_host","_APP_USAGE_STATS=$$config__app_usage_stats","DOCKERHUB_PULL_USERNAME=$$config_dockerhub_pull_username","DOCKERHUB_PULL_PASSWORD=$$secret_dockerhub_pull_password","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":[],"entrypoint":"worker-functions"},"$$id-worker-mails":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_OPENSSL_KEY_V1=$$secret__app_openssl_key_v1","_APP_SYSTEM_EMAIL_NAME=$$config__app_system_email_name","_APP_SYSTEM_EMAIL_ADDRESS=$$config__app_system_email_address","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_REDIS_USER=$$config__app_redis_user","_APP_REDIS_PASS=$$secret__app_redis_pass","_APP_SMTP_HOST=$$config__app_smtp_host","_APP_SMTP_PORT=$$config__app_smtp_port","_APP_SMTP_SECURE=$$config__app_smtp_secure","_APP_SMTP_USERNAME=$$config__app_smtp_username","_APP_SMTP_PASSWORD=$$secret__app_smtp_password","_APP_LOGGING_PROVIDER=$$config__app_logging_provider","_APP_LOGGING_CONFIG=$$config__app_logging_config","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":[],"entrypoint":"worker-mails"},"$$id-worker-messaging":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_REDIS_USER=$$config__app_redis_user","_APP_REDIS_PASS=$$secret__app_redis_pass","_APP_SMS_PROVIDER=$$config__app_sms_provider","_APP_SMS_FROM=$$config__app_sms_from","_APP_LOGGING_PROVIDER=$$config__app_logging_provider","_APP_LOGGING_CONFIG=$$config__app_logging_config","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":[],"entrypoint":"worker-messaging"},"$$id-worker-webhooks":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_OPENSSL_KEY_V1=$$secret__app_openssl_key_v1","_APP_SYSTEM_SECURITY_EMAIL_ADDRESS=$$config__app_system_security_email_address","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_REDIS_USER=$$config__app_redis_user","_APP_REDIS_PASS=$$secret__app_redis_pass","_APP_LOGGING_PROVIDER=$$config__app_logging_provider","_APP_LOGGING_CONFIG=$$config__app_logging_config","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":[],"entrypoint":"worker-webhooks"}},"variables":[{"id":"$$config__app_influxdb_host","name":"_APP_INFLUXDB_HOST","label":"InfluxDB | _APP_INFLUXDB_HOST","defaultValue":"$$id-influxdb","description":""},{"id":"$$config__app_influxdb_port","name":"_APP_INFLUXDB_PORT","label":"InfluxDB | _APP_INFLUXDB_PORT","defaultValue":"8086","description":"InfluxDB server TCP port."},{"id":"$$config__app_env","name":"_APP_ENV","label":"General | _APP_ENV","defaultValue":"production","description":"Set your server running environment."},{"id":"$$config__app_worker_per_core","name":"_APP_WORKER_PER_CORE","label":"General | _APP_WORKER_PER_CORE","defaultValue":"6","description":"Internal Worker per core for the API, Realtime and Executor containers. Can be configured to optimize performance."},{"id":"$$config__app_locale","name":"_APP_LOCALE","label":"General | _APP_LOCALE","defaultValue":"en","description":"Set your Appwrite's locale. By default, the locale is set to 'en'."},{"id":"$$config__app_console_whitelist_root","name":"_APP_CONSOLE_WHITELIST_ROOT","label":"General | _APP_CONSOLE_WHITELIST_ROOT","defaultValue":"enabled","description":"This option allows you to disable the creation of new users on the Appwrite console. When enabled only 1 user will be able to use the registration form. New users can be added by inviting them to your project. By default this option is enabled."},{"id":"$$config__app_console_whitelist_emails","name":"_APP_CONSOLE_WHITELIST_EMAILS","label":"General | _APP_CONSOLE_WHITELIST_EMAILS","defaultValue":"","description":"This option allows you to limit creation of new users on the Appwrite console. This option is very useful for small teams or sole developers. To enable it, pass a list of allowed email addresses separated by a comma."},{"id":"$$config__app_console_whitelist_ips","name":"_APP_CONSOLE_WHITELIST_IPS","label":"General | _APP_CONSOLE_WHITELIST_IPS","defaultValue":"","description":"This last option allows you to limit creation of users in Appwrite console for users sharing the same set of IP addresses. This option is very useful for team working with a VPN service or a company IP.\\n\\nTo enable/activate this option, pass a list of allowed IP addresses separated by a comma."},{"id":"$$config__app_system_email_name","name":"_APP_SYSTEM_EMAIL_NAME","label":"General | _APP_SYSTEM_EMAIL_NAME","defaultValue":"Appwrite","description":"This is the sender name value that will appear on email messages sent to developers from the Appwrite console. You can use url encoded strings for spaces and special chars."},{"id":"$$config__app_system_email_address","name":"_APP_SYSTEM_EMAIL_ADDRESS","label":"General | _APP_SYSTEM_EMAIL_ADDRESS","defaultValue":"team@appwrite.io","description":"This is the sender email address that will appear on email messages sent to developers from the Appwrite console. You should choose an email address that is allowed to be used from your SMTP server to avoid the server email ending in the users' SPAM folders."},{"id":"$$config__app_system_security_email_address","name":"_APP_SYSTEM_SECURITY_EMAIL_ADDRESS","label":"General | _APP_SYSTEM_SECURITY_EMAIL_ADDRESS","defaultValue":"certs@appwrite.io","description":"This is the email address used to issue SSL certificates for custom domains or the user agent in your webhooks payload."},{"id":"$$config__app_system_response_format","name":"_APP_SYSTEM_RESPONSE_FORMAT","label":"General | _APP_SYSTEM_RESPONSE_FORMAT","defaultValue":"","description":"Use this environment variable to set the default Appwrite HTTP response format to support an older version of Appwrite. This option is useful to overcome breaking changes between versions. You can also use the X-Appwrite-Response-Format HTTP request header to overwrite the response for a specific request. This variable accepts any valid Appwrite version. To use the current version format, leave the value of the variable empty."},{"id":"$$config__app_options_abuse","name":"_APP_OPTIONS_ABUSE","label":"General | _APP_OPTIONS_ABUSE","defaultValue":"enabled","description":"Allows you to disable abuse checks and API rate limiting. By default, set to 'enabled'. To cancel the abuse checking, set to 'disabled'. It is not recommended to disable this check-in a production environment."},{"id":"$$config__app_options_force_https","name":"_APP_OPTIONS_FORCE_HTTPS","label":"General | _APP_OPTIONS_FORCE_HTTPS","defaultValue":"disabled","description":"Allows you to force HTTPS connection to your API. This feature redirects any HTTP call to HTTPS and adds the 'Strict-Transport-Security' header to all HTTP responses."},{"id":"$$secret__app_openssl_key_v1","name":"_APP_OPENSSL_KEY_V1","label":"General | _APP_OPENSSL_KEY_V1","defaultValue":"$$generate_hex(256)","description":"This is your server private secret key that is used to encrypt all sensitive data on your server. Appwrite server encrypts all secret data on your server like webhooks, HTTP passwords, user sessions, and storage files. Keep it a secret and have a backup for it."},{"id":"$$config__app_domain","name":"_APP_DOMAIN","label":"General | _APP_DOMAIN","defaultValue":"$$generate_domain","description":"Your Appwrite domain address. When setting a public suffix domain, Appwrite will attempt to issue a valid SSL certificate automatically. When used with a dev domain, Appwrite will assign a self-signed SSL certificate. The default value is 'localhost'."},{"id":"$$config__app_domain_target","name":"_APP_DOMAIN_TARGET","label":"General | _APP_DOMAIN_TARGET","defaultValue":"$$generate_fqdn","description":"A DNS A record hostname to serve as a CNAME target for your Appwrite custom domains. You can use the same value as used for the Appwrite '_APP_DOMAIN' variable. The default value is 'localhost'."},{"id":"$$config__app_redis_host","name":"_APP_REDIS_HOST","label":"Redis | _APP_REDIS_HOST","defaultValue":"$$id-redis","description":""},{"id":"$$config__app_redis_port","name":"_APP_REDIS_PORT","label":"Redis | _APP_REDIS_PORT","defaultValue":"6379","description":"Redis server TCP port."},{"id":"$$config__app_redis_user","name":"_APP_REDIS_USER","label":"Redis | _APP_REDIS_USER","defaultValue":"","description":"Redis server user. This is an optional variable. Default value is an empty string."},{"id":"$$secret__app_redis_pass","name":"_APP_REDIS_PASS","label":"Redis | _APP_REDIS_PASS","defaultValue":"","description":"Redis server password. This is an optional variable. Default value is an empty string."},{"id":"$$config__app_db_host","name":"_APP_DB_HOST","label":"MariaDB | _APP_DB_HOST","defaultValue":"$$id-mariadb","description":""},{"id":"$$config__app_db_port","name":"_APP_DB_PORT","label":"MariaDB | _APP_DB_PORT","defaultValue":"3306","description":"MariaDB server TCP port."},{"id":"$$config__app_db_schema","name":"_APP_DB_SCHEMA","label":"MariaDB | _APP_DB_SCHEMA","defaultValue":"appwrite","description":"MariaDB server database schema."},{"id":"$$config__app_db_user","name":"_APP_DB_USER","label":"MariaDB | _APP_DB_USER","defaultValue":"user","description":"MariaDB server user name."},{"id":"$$secret__app_db_root_pass","name":"MARIADB_ROOT_PASSWORD","label":"MariaDB | MARIADB_ROOT_PASSWORD","defaultValue":"$$generate_hex(16)","description":"MariaDB server root user password."},{"id":"$$secret__app_db_pass","name":"_APP_DB_PASS","label":"MariaDB | _APP_DB_PASS","defaultValue":"$$generate_hex(16)","description":"MariaDB server user password."},{"id":"$$config__app_smtp_host","name":"_APP_SMTP_HOST","label":"SMTP | _APP_SMTP_HOST","defaultValue":"","description":"SMTP server host name address. Use an empty string to disable all mail sending from the server. The default value for this variable is an empty string."},{"id":"$$config__app_smtp_port","name":"_APP_SMTP_PORT","label":"SMTP | _APP_SMTP_PORT","defaultValue":"","description":"SMTP server TCP port. Empty by default."},{"id":"$$config__app_smtp_secure","name":"_APP_SMTP_SECURE","label":"SMTP | _APP_SMTP_SECURE","defaultValue":"","description":"SMTP secure connection protocol. Empty by default, change to 'tls' if running on a secure connection."},{"id":"$$config__app_smtp_username","name":"_APP_SMTP_USERNAME","label":"SMTP | _APP_SMTP_USERNAME","defaultValue":"","description":"SMTP server user name. Empty by default."},{"id":"$$secret__app_smtp_password","name":"_APP_SMTP_PASSWORD","label":"SMTP | _APP_SMTP_PASSWORD","defaultValue":"","description":"SMTP server user password. Empty by default."},{"id":"$$config__app_usage_stats","name":"_APP_USAGE_STATS","label":"General | _APP_USAGE_STATS","defaultValue":"enabled","description":"This variable allows you to disable the collection and displaying of usage stats. This value is set to 'enabled' by default, to disable the usage stats set the value to 'disabled'. When disabled, it's recommended to turn off the Worker Usage, Influxdb and Telegraf containers for better resource usage."},{"id":"$$config__app_storage_limit","name":"_APP_STORAGE_LIMIT","label":"Storage | _APP_STORAGE_LIMIT","defaultValue":"30000000","description":"Maximum file size allowed for file upload. The default value is 30MB. You should pass your size limit value in bytes."},{"id":"$$config__app_storage_preview_limit","name":"_APP_STORAGE_PREVIEW_LIMIT","label":"Storage | _APP_STORAGE_PREVIEW_LIMIT","defaultValue":"20000000","description":"Maximum file size allowed for file image preview. The default value is 20MB. You should pass your size limit value in bytes."},{"id":"$$config__app_storage_antivirus_enabled","name":"_APP_STORAGE_ANTIVIRUS","label":"Storage | _APP_STORAGE_ANTIVIRUS","defaultValue":"disabled","description":"This variable allows you to disable the internal anti-virus scans. This value is set to 'disabled' by default, to enable the scans set the value to 'enabled'. Before enabling, you must add the ClamAV service and depend on it on main Appwrite service."},{"id":"$$config__app_storage_antivirus_host","name":"_APP_STORAGE_ANTIVIRUS_HOST","label":"Storage | _APP_STORAGE_ANTIVIRUS_HOST","defaultValue":"clamav","description":"ClamAV server host name address."},{"id":"$$config__app_storage_antivirus_port","name":"_APP_STORAGE_ANTIVIRUS_PORT","label":"Storage | _APP_STORAGE_ANTIVIRUS_PORT","defaultValue":"3310","description":"ClamAV server TCP port."},{"id":"$$config__app_storage_device","name":"_APP_STORAGE_DEVICE","label":"Storage | _APP_STORAGE_DEVICE","defaultValue":"Local","description":"Select default storage device. The default value is 'Local'. List of supported adapters are 'Local', 'S3', 'DOSpaces', 'Backblaze', 'Linode' and 'Wasabi'."},{"id":"$$secret__app_storage_s3_access_key","name":"_APP_STORAGE_S3_ACCESS_KEY","label":"Storage | _APP_STORAGE_S3_ACCESS_KEY","defaultValue":"","description":"AWS S3 storage access key. Required when the storage adapter is set to S3. You can get your access key from your AWS console."},{"id":"$$secret__app_storage_s3_secret","name":"_APP_STORAGE_S3_SECRET","label":"Storage | _APP_STORAGE_S3_SECRET","defaultValue":"","description":"AWS S3 storage secret key. Required when the storage adapter is set to S3. You can get your secret key from your AWS console."},{"id":"$$config__app_storage_s3_region","name":"_APP_STORAGE_S3_REGION","label":"Storage | _APP_STORAGE_S3_REGION","defaultValue":"us-east-1","description":"AWS S3 storage region. Required when storage adapter is set to S3. You can find your region info for your bucket from AWS console."},{"id":"$$config__app_storage_s3_bucket","name":"_APP_STORAGE_S3_BUCKET","label":"Storage | _APP_STORAGE_S3_BUCKET","defaultValue":"","description":"AWS S3 storage bucket. Required when storage adapter is set to S3. You can create buckets in your AWS console."},{"id":"$$secret__app_storage_do_spaces_access_key","name":"_APP_STORAGE_DO_SPACES_ACCESS_KEY","label":"Storage | _APP_STORAGE_DO_SPACES_ACCESS_KEY","defaultValue":"","description":"DigitalOcean spaces access key. Required when the storage adapter is set to DOSpaces. You can get your access key from your DigitalOcean console."},{"id":"$$secret__app_storage_do_spaces_secret","name":"_APP_STORAGE_DO_SPACES_SECRET","label":"Storage | _APP_STORAGE_DO_SPACES_SECRET","defaultValue":"","description":"DigitalOcean spaces secret key. Required when the storage adapter is set to DOSpaces. You can get your secret key from your DigitalOcean console."},{"id":"$$config__app_storage_do_spaces_region","name":"_APP_STORAGE_DO_SPACES_REGION","label":"Storage | _APP_STORAGE_DO_SPACES_REGION","defaultValue":"us-east-1","description":"DigitalOcean spaces region. Required when storage adapter is set to DOSpaces. You can find your region info for your space from DigitalOcean console."},{"id":"$$config__app_storage_do_spaces_bucket","name":"_APP_STORAGE_DO_SPACES_BUCKET","label":"Storage | _APP_STORAGE_DO_SPACES_BUCKET","defaultValue":"","description":"DigitalOcean spaces bucket. Required when storage adapter is set to DOSpaces. You can create spaces in your DigitalOcean console."},{"id":"$$secret__app_storage_backblaze_access_key","name":"_APP_STORAGE_BACKBLAZE_ACCESS_KEY","label":"Storage | _APP_STORAGE_BACKBLAZE_ACCESS_KEY","defaultValue":"","description":"Backblaze access key. Required when the storage adapter is set to Backblaze. Your Backblaze keyID will be your access key. You can get your keyID from your Backblaze console."},{"id":"$$secret__app_storage_backblaze_secret","name":"_APP_STORAGE_BACKBLAZE_SECRET","label":"Storage | _APP_STORAGE_BACKBLAZE_SECRET","defaultValue":"","description":"Backblaze secret key. Required when the storage adapter is set to Backblaze. Your Backblaze applicationKey will be your secret key. You can get your applicationKey from your Backblaze console."},{"id":"$$config__app_storage_backblaze_region","name":"_APP_STORAGE_BACKBLAZE_REGION","label":"Storage | _APP_STORAGE_BACKBLAZE_REGION","defaultValue":"us-west-004","description":"Backblaze region. Required when storage adapter is set to Backblaze. You can find your region info from your Backblaze console."},{"id":"$$config__app_storage_backblaze_bucket","name":"_APP_STORAGE_BACKBLAZE_BUCKET","label":"Storage | _APP_STORAGE_BACKBLAZE_BUCKET","defaultValue":"","description":"Backblaze bucket. Required when storage adapter is set to Backblaze. You can create your bucket from your Backblaze console."},{"id":"$$secret__app_storage_linode_access_key","name":"_APP_STORAGE_LINODE_ACCESS_KEY","label":"Storage | _APP_STORAGE_LINODE_ACCESS_KEY","defaultValue":"","description":"Linode object storage access key. Required when the storage adapter is set to Linode. You can get your access key from your Linode console."},{"id":"$$secret__app_storage_linode_secret","name":"_APP_STORAGE_LINODE_SECRET","label":"Storage | _APP_STORAGE_LINODE_SECRET","defaultValue":"","description":"Linode object storage secret key. Required when the storage adapter is set to Linode. You can get your secret key from your Linode console."},{"id":"$$config__app_storage_linode_region","name":"_APP_STORAGE_LINODE_REGION","label":"Storage | _APP_STORAGE_LINODE_REGION","defaultValue":"eu-central-1","description":"Linode object storage region. Required when storage adapter is set to Linode. You can find your region info from your Linode console."},{"id":"$$config__app_storage_linode_bucket","name":"_APP_STORAGE_LINODE_BUCKET","label":"Storage | _APP_STORAGE_LINODE_BUCKET","defaultValue":"","description":"Linode object storage bucket. Required when storage adapter is set to Linode. You can create buckets in your Linode console."},{"id":"$$secret__app_storage_wasabi_access_key","name":"_APP_STORAGE_WASABI_ACCESS_KEY","label":"Storage | _APP_STORAGE_WASABI_ACCESS_KEY","defaultValue":"","description":"Wasabi access key. Required when the storage adapter is set to Wasabi. You can get your access key from your Wasabi console."},{"id":"$$secret__app_storage_wasabi_secret","name":"_APP_STORAGE_WASABI_SECRET","label":"Storage | _APP_STORAGE_WASABI_SECRET","defaultValue":"","description":"Wasabi secret key. Required when the storage adapter is set to Wasabi. You can get your secret key from your Wasabi console."},{"id":"$$config__app_storage_wasabi_region","name":"_APP_STORAGE_WASABI_REGION","label":"Storage | _APP_STORAGE_WASABI_REGION","defaultValue":"eu-central-1","description":"Wasabi region. Required when storage adapter is set to Wasabi. You can find your region info from your Wasabi console."},{"id":"$$config__app_storage_wasabi_bucket","name":"_APP_STORAGE_WASABI_BUCKET","label":"Storage | _APP_STORAGE_WASABI_BUCKET","defaultValue":"","description":"Wasabi bucket. Required when storage adapter is set to Wasabi. You can create buckets in your Wasabi console."},{"id":"$$config__app_functions_size_limit","name":"_APP_FUNCTIONS_SIZE_LIMIT","label":"Functions | _APP_FUNCTIONS_SIZE_LIMIT","defaultValue":"30000000","description":"The maximum size deployment in bytes. The default value is 30MB."},{"id":"$$config__app_functions_timeout","name":"_APP_FUNCTIONS_TIMEOUT","label":"Functions | _APP_FUNCTIONS_TIMEOUT","defaultValue":"900","description":"The maximum number of seconds allowed as a timeout value when creating a new function. The default value is 900 seconds."},{"id":"$$config__app_functions_build_timeout","name":"_APP_FUNCTIONS_BUILD_TIMEOUT","label":"Functions | _APP_FUNCTIONS_BUILD_TIMEOUT","defaultValue":"900","description":"The maximum number of seconds allowed as a timeout value when building a new function. The default value is 900 seconds."},{"id":"$$config__app_functions_containers","name":"_APP_FUNCTIONS_CONTAINERS","label":"Functions | _APP_FUNCTIONS_CONTAINERS","defaultValue":"10","description":"The maximum number of containers Appwrite is allowed to keep alive in the background for function environments. Running containers allow faster execution time as there is no need to recreate each container every time a function gets executed. The default value is 10."},{"id":"$$config__app_functions_cpus","name":"_APP_FUNCTIONS_CPUS","label":"Functions | _APP_FUNCTIONS_CPUS","defaultValue":"","description":"The maximum number of CPU core a single cloud function is allowed to use. Please note that setting a value higher than available cores will result in a function error, which might result in an error. The default value is empty. When it's empty, CPU limit will be disabled."},{"id":"$$config__app_functions_memory_allocated","name":"_APP_FUNCTIONS_MEMORY","label":"Functions | _APP_FUNCTIONS_MEMORY","defaultValue":"","description":"The maximum amount of memory a single cloud function is allowed to use in megabytes. The default value is empty. When it's empty, memory limit will be disabled."},{"id":"$$config__app_functions_memory_swap","name":"_APP_FUNCTIONS_MEMORY_SWAP","label":"Functions | _APP_FUNCTIONS_MEMORY_SWAP","defaultValue":"","description":"The maximum amount of swap memory a single cloud function is allowed to use in megabytes. The default value is empty. When it's empty, swap memory limit will be disabled."},{"id":"$$config__app_functions_runtimes","name":"_APP_FUNCTIONS_RUNTIMES","label":"Functions | _APP_FUNCTIONS_RUNTIMES","defaultValue":"node-18.0","description":"This option allows you to limit the available environments for cloud functions. This option is very useful for low-cost servers to safe disk space.\nTo enable/activate this option, pass a list of allowed environments separated by a comma.\nCurrently, supported environments are: node-14.5, node-16.0, node-18.0, php-8.0, php-8.1, ruby-3.0, ruby-3.1, python-3.8, python-3.9, python-3.10, deno-1.21, deno-1.24, dart-2.15, dart-2.16, dart-2.17, dotnet-3.1, dotnet-6.0, java-8.0, java-11.0, java-17.0, java-18.0, swift-5.5, kotlin-1.6, cpp-17.0"},{"id":"$$secret__app_executor_secret","name":"_APP_EXECUTOR_SECRET","label":"Functions | _APP_EXECUTOR_SECRET","defaultValue":"$$generate_hex(16)","description":"The secret key used by Appwrite to communicate with the function executor."},{"id":"$$config__app_executor_host","name":"_APP_EXECUTOR_HOST","label":"","defaultValue":"http://$$id-executor/v1","description":""},{"id":"$$config__app_logging_provider","name":"_APP_LOGGING_PROVIDER","label":"General | _APP_LOGGING_PROVIDER","defaultValue":"","description":"This variable allows you to enable logging errors to 3rd party providers. This value is empty by default, to enable the logger set the value to one of 'sentry', 'raygun', 'appsignal', 'logowl'"},{"id":"$$config__app_logging_config","name":"_APP_LOGGING_CONFIG","label":"General | _APP_LOGGING_CONFIG","defaultValue":"","description":"This variable configures authentication to 3rd party error logging providers. If using Sentry, this should be 'SENTRY_API_KEY;SENTRY_APP_ID'. If using Raygun, this should be Raygun API key. If using AppSignal, this should be AppSignal API key. If using LogOwl, this should be LogOwl Service Ticket."},{"id":"$$config__app_statsd_host","name":"_APP_STATSD_HOST","label":"","defaultValue":"$$id-telegraf","description":""},{"id":"$$config__app_statsd_port","name":"_APP_STATSD_PORT","label":"StatsD | _APP_STATSD_PORT","defaultValue":"8125","description":"StatsD server TCP port."},{"id":"$$config__app_maintenance_interval","name":"_APP_MAINTENANCE_INTERVAL","label":"Functions | _APP_MAINTENANCE_INTERVAL","defaultValue":"86400","description":"Interval value containing the number of seconds that the Appwrite maintenance process should wait before executing system cleanups and optimizations. The default value is 86400 seconds (1 day)."},{"id":"$$config__app_maintenance_retention_execution","name":"_APP_MAINTENANCE_RETENTION_EXECUTION","label":"Functions | _APP_MAINTENANCE_RETENTION_EXECUTION","defaultValue":"1209600","description":"The maximum duration (in seconds) upto which to retain execution logs. The default value is 1209600 seconds (14 days)."},{"id":"$$config__app_maintenance_retention_cache","name":"_APP_MAINTENANCE_RETENTION_CACHE","label":"Functions | _APP_MAINTENANCE_RETENTION_CACHE","defaultValue":"2592000","description":"The maximum duration (in seconds) upto which to retain cached files. The default value is 2592000 seconds (30 days)."},{"id":"$$config__app_maintenance_retention_abuse","name":"_APP_MAINTENANCE_RETENTION_ABUSE","label":"Functions | _APP_MAINTENANCE_RETENTION_ABUSE","defaultValue":"86400","description":"The maximum duration (in seconds) upto which to retain abuse logs. The default value is 86400 seconds (1 day)."},{"id":"$$config__app_maintenance_retention_audit","name":"_APP_MAINTENANCE_RETENTION_AUDIT","label":"Functions | _APP_MAINTENANCE_RETENTION_AUDIT","defaultValue":"1209600","description":"The maximum duration (in seconds) upto which to retain audit logs. The default value is 1209600 seconds (14 days)."},{"id":"$$config__app_sms_provider","name":"_APP_SMS_PROVIDER","label":"Phone | _APP_SMS_PROVIDER","defaultValue":"","description":"Provider used for delivering SMS for Phone authentication. Use the following format: 'sms://[USER]:[SECRET]@[PROVIDER]'. Available providers are twilio, text-magic, telesign, msg91, and vonage."},{"id":"$$config__app_sms_from","name":"_APP_SMS_FROM","label":"Phone | _APP_SMS_FROM","defaultValue":"","description":"Phone number used for sending out messages. Must start with a leading '+' and maximum of 15 digits without spaces (+123456789)."},{"id":"$$config__app_functions_inactive_threshold","name":"_APP_FUNCTIONS_INACTIVE_THRESHOLD","label":"Functions | _APP_FUNCTIONS_INACTIVE_THRESHOLD","defaultValue":"60","description":"The minimum time a function can be inactive before it's container is shutdown and put to sleep. The default value is 60 seconds"},{"id":"$$config_open_runtimes_network","name":"OPEN_RUNTIMES_NETWORK","label":"","defaultValue":"$$generate_network","description":""},{"id":"$$config_dockerhub_pull_username","name":"DOCKERHUB_PULL_USERNAME","label":"Functions | DOCKERHUB_PULL_USERNAME","defaultValue":"","description":"The username for hub.docker.com. This variable is used to pull images from hub.docker.com."},{"id":"$$secret_dockerhub_pull_password","name":"DOCKERHUB_PULL_PASSWORD","label":"Functions | DOCKERHUB_PULL_PASSWORD","defaultValue":"","description":"The password for hub.docker.com. This variable is used to pull images from hub.docker.com."},{"id":"$$config__app_usage_timeseries_interval","name":"_APP_USAGE_TIMESERIES_INTERVAL","label":"General | _APP_USAGE_TIMESERIES_INTERVAL","defaultValue":"30","description":"Interval value containing the number of seconds that the Appwrite usage process should wait before aggregating stats and syncing it to mariadb from InfluxDB. The default value is 30 seconds."},{"id":"$$config__app_usage_database_interval","name":"_APP_USAGE_DATABASE_INTERVAL","label":"General | _APP_USAGE_DATABASE_INTERVAL","defaultValue":"900","description":"Interval value containing the number of seconds that the Appwrite usage process should wait before aggregating stats from data in Appwrite Database. The default value is 15 minutes."}]},{"templateVersion":"1.0.0","defaultVersion":"latest","documentation":"https://docs.weblate.org/en/latest/admin/install/docker.html","description":"A copylefted libre software web-based continuous localization system.","type":"weblate","name":"Weblate","labels":["translate","localization"],"services":{"$$id":{"name":"Weblate","depends_on":["$$id-postgresql","$$id-redis"],"image":"weblate/weblate:$$core_version","volumes":["$$id-data:/app/data"],"environment":["WEBLATE_SITE_DOMAIN=$$config_weblate_site_domain","WEBLATE_ADMIN_PASSWORD=$$secret_weblate_admin_password","POSTGRES_PASSWORD=$$secret_postgres_password","POSTGRES_USER=$$config_postgres_user","POSTGRES_DATABASE=$$config_postgres_db","POSTGRES_HOST=$$id-postgresql","POSTGRES_PORT=5432","REDIS_HOST=$$id-redis"],"ports":["8080"]},"$$id-postgresql":{"name":"PostgreSQL","depends_on":[],"image":"postgres:14-alpine","volumes":["$$id-postgresql-data:/var/lib/postgresql/data"],"environment":["POSTGRES_USER=$$config_postgres_user","POSTGRES_PASSWORD=$$secret_postgres_password","POSTGRES_DB=$$config_postgres_db"],"ports":[]},"$$id-redis":{"name":"Redis","depends_on":[],"image":"redis:7-alpine","volumes":["$$id-redis-data:/data"],"environment":[],"ports":[]}},"variables":[{"id":"$$config_weblate_site_domain","name":"WEBLATE_SITE_DOMAIN","label":"Weblate Domain","defaultValue":"$$generate_domain","description":""},{"id":"$$secret_weblate_admin_password","name":"WEBLATE_ADMIN_PASSWORD","label":"Weblate Admin Password","defaultValue":"$$generate_password","description":"","showOnConfiguration":true},{"id":"$$config_postgres_user","main":"$$id-postgresql","name":"POSTGRES_USER","label":"PostgreSQL User","defaultValue":"$$generate_username","description":""},{"id":"$$secret_postgres_password","main":"$$id-postgresql","name":"POSTGRES_PASSWORD","label":"PostgreSQL Password","defaultValue":"$$generate_password","description":"","showOnConfiguration":true},{"id":"$$config_postgres_db","main":"$$id-postgresql","name":"POSTGRES_DB","label":"PostgreSQL Database","defaultValue":"weblate","description":""}]},{"templateVersion":"1.0.0","defaultVersion":"2022.10.14-1a5b0965","documentation":"https://docs.searxng.org/","type":"searxng","name":"SearXNG","description":"Free internet metasearch engine which aggregates results from more than 70 search services.","services":{"$$id":{"name":"SearXNG","depends_on":["$$id-redis"],"image":"searxng/searxng:$$core_version","volumes":["$$id-searxng:/etc/searxng"],"environment":["SEARXNG_BASE_URL=$$config_searxng_base_url"],"ports":["8080"],"cap_drop":["ALL"],"cap_add":["CHOWN","SETGID","SETUID","DAC_OVERRIDE"],"files":[{"location":"/etc/searxng/settings.yml","content":"\n # see https://docs.searxng.org/admin/engines/settings.html#use-default-settings\n use_default_settings: true\n server:\n secret_key: $$secret_secret_key\n limiter: true\n image_proxy: true\n ui:\n static_use_hash: true\n redis:\n url: redis://:$$secret_redis_password@$$id-redis:6379/0"}]},"$$id-redis":{"name":"Redis","command":"redis-server --requirepass $$secret_redis_password --save \"\" --appendonly \"no\"","depends_on":[],"image":"redis:7-alpine","volumes":["$$id-redis-data:/data"],"environment":["REDIS_PASSWORD=$$secret_redis_password"],"ports":[],"cap_drop":["ALL"],"cap_add":["SETGID","SETUID","DAC_OVERRIDE"]}},"variables":[{"id":"$$config_searxng_base_url","name":"SEARXNG_BASE_URL","label":"SearXNG Base URL","defaultValue":"$$generate_fqdn","description":""},{"id":"$$secret_secret_key","name":"SECRET_KEY","label":"Secret Key","defaultValue":"$$generate_hex(64)","description":""},{"id":"$$secret_redis_password","name":"REDIS_PASSWORD","label":"Redis Password","defaultValue":"$$generate_password","description":""}]},{"templateVersion":"1.0.0","defaultVersion":"v2.0.6","documentation":"https://glitchtip.com/documentation","type":"glitchtip","name":"GlitchTip","description":"Simple, open source error tracking.","labels":["sentry","bugsnag"],"services":{"$$id":{"name":"GlitchTip","depends_on":["$$id-postgresql","$$id-redis"],"image":"glitchtip/glitchtip:$$core_version","volumes":[],"environment":["PORT=$$config_port","GLITCHTIP_DOMAIN=$$config_glitchtip_domain","SECRET_KEY=$$secret_secret_key","DATABASE_URL=$$secret_database_url","REDIS_URL=$$secret_redis_url","DEFAULT_FROM_EMAIL=$$config_default_from_email","EMAIL_URL=$$secret_email_url","EMAIL_HOST=$$config_email_host","EMAIL_PORT=$$config_email_port","EMAIL_HOST_USER=$$config_email_host_user","EMAIL_HOST_PASSWORD=$$secret_email_host_password","EMAIL_USE_TLS=$$config_email_use_tls","EMAIL_USE_SSL=$$config_email_use_ssl","EMAIL_BACKEND=$$config_email_backend","MAILGUN_API_KEY=$$secret_mailgun_api_key","SENDGRID_API_KEY=$$secret_sendgrid_api_key","ENABLE_OPEN_USER_REGISTRATION=$$config_enable_open_user_registration","DJANGO_SUPERUSER_EMAIL=$$config_django_superuser_email","DJANGO_SUPERUSER_PASSWORD=$$secret_django_superuser_password","DJANGO_SUPERUSER_USERNAME=$$config_django_superuser_username","CELERY_WORKER_CONCURRENCY=$$config_celery_worker_concurrency"],"ports":["8000"]},"$$id-worker":{"name":"Celery Worker","command":"./bin/run-celery-with-beat.sh","depends_on":["$$id-postgresql","$$id-redis"],"image":"glitchtip/glitchtip:$$core_version","environment":["GLITCHTIP_DOMAIN=$$config_glitchtip_domain","SECRET_KEY=$$secret_secret_key","DATABASE_URL=$$secret_database_url","REDIS_URL=$$secret_redis_url","DEFAULT_FROM_EMAIL=$$config_default_from_email","EMAIL_URL=$$secret_email_url","CELERY_WORKER_CONCURRENCY=$$config_celery_worker_concurrency"],"ports":[]},"$$id-migrate":{"exclude":true,"name":"Migrate","command":"./manage.py migrate","depends_on":["$$id-postgresql","$$id-redis"],"image":"glitchtip/glitchtip:$$core_version","environment":["GLITCHTIP_DOMAIN=$$config_glitchtip_domain","SECRET_KEY=$$secret_secret_key","DATABASE_URL=$$secret_database_url","REDIS_URL=$$secret_redis_url","DEFAULT_FROM_EMAIL=$$config_default_from_email","EMAIL_URL=$$secret_email_url"],"ports":[]},"$$id-postgresql":{"name":"PostgreSQL","depends_on":[],"image":"postgres:14-alpine","volumes":["$$id-postgresql-data:/var/lib/postgresql/data"],"environment":["POSTGRES_USER=$$config_postgres_user","POSTGRES_PASSWORD=$$secret_postgres_password","POSTGRES_DB=$$config_postgres_db"],"ports":[]},"$$id-redis":{"name":"Redis","depends_on":[],"image":"redis:7-alpine","volumes":["$$id-postgresql-redis-data:/data"],"environment":[],"ports":[]}},"variables":[{"id":"$$config_django_superuser_username","name":"DJANGO_SUPERUSER_USERNAME","label":"Django Superuser Username","defaultValue":"$$generate_username","description":""},{"id":"$$secret_django_superuser_password","name":"DJANGO_SUPERUSER_PASSWORD","label":"Django Superuser Password","defaultValue":"$$generate_password","description":"","showOnConfiguration":true},{"id":"$$config_port","name":"PORT","label":"GlitchTip Port","defaultValue":"8000","description":""},{"id":"$$config_celery_worker_concurrency","main":"$$id-worker","name":"CELERY_WORKER_CONCURRENCY","label":"Celery Worker Concurrency","defaultValue":"2","description":""},{"id":"$$config_glitchtip_domain","name":"GLITCHTIP_DOMAIN","label":"GlitchTip Domain","defaultValue":"$$generate_fqdn","description":""},{"id":"$$secret_email_url","name":"EMAIL_URL","label":"SMTP Email URL","defaultValue":"smtp://$$config_email_host_user:$$secret_email_host_password@$$config_email_host:$$config_email_port","description":""},{"id":"$$secret_database_url","name":"DATABASE_URL","label":"Database URL for PostgreSQL","defaultValue":"postgresql://$$config_postgres_user:$$secret_postgres_password@$$id-postgresql:5432/$$config_postgres_db","description":""},{"id":"$$secret_redis_url","name":"REDIS_URL","label":"Redis URL","defaultValue":"redis://$$id-redis:6379/0","description":""},{"id":"$$config_default_from_email","name":"DEFAULT_FROM_EMAIL","label":"Default Email Address","defaultValue":"noreply@example.com","description":""},{"id":"$$config_email_host","name":"EMAIL_HOST","label":"Email SMTP Host","defaultValue":"","description":""},{"id":"$$config_email_port","name":"EMAIL_PORT","label":"Email SMTP Port","defaultValue":"25","description":""},{"id":"$$config_email_host_user","name":"EMAIL_HOST_USER","label":"Email SMTP User","defaultValue":"","description":""},{"id":"$$secret_email_host_password","name":"EMAIL_HOST_PASSWORD","label":"Email SMTP Password","defaultValue":"","description":""},{"id":"$$config_email_use_tls","name":"EMAIL_USE_TLS","label":"Email Use TLS","defaultValue":"false","description":""},{"id":"$$config_email_use_ssl","name":"EMAIL_USE_SSL","label":"Email Use SSL","defaultValue":"false","description":""},{"id":"$$secret_email_smtp_password","name":"EMAIL_SMTP_PASSWORD","label":"SMTP Password","defaultValue":"","description":""},{"id":"$$config_email_backend","name":"EMAIL_BACKEND","label":"Email Backend","defaultValue":"","description":""},{"id":"$$secret_mailgun_api_key","name":"MAILGUN_API_KEY","label":"Mailgun API Key","defaultValue":"","description":"","showOnConfiguration":true},{"id":"$$secret_sendgrid_api_key","name":"SENDGRID_API_KEY","label":"Sendgrid API Key","defaultValue":"","description":"","showOnConfiguration":true},{"id":"$$config_enable_open_user_registration","name":"ENABLE_OPEN_USER_REGISTRATION","label":"Enable Open User Registration","defaultValue":"true","description":""},{"id":"$$config_django_superuser_email","name":"DJANGO_SUPERUSER_EMAIL","label":"Django Superuser Email","defaultValue":"noreply@example.com","description":""},{"id":"$$config_postgres_user","main":"$$id-postgresql","name":"POSTGRES_USER","label":"PostgreSQL User","defaultValue":"$$generate_username","description":""},{"id":"$$secret_postgres_password","main":"$$id-postgresql","name":"POSTGRES_PASSWORD","label":"PostgreSQL Password","defaultValue":"$$generate_password","description":""},{"id":"$$config_postgres_db","main":"$$id-postgresql","name":"POSTGRES_DB","label":"PostgreSQL Database","defaultValue":"glitchtip","description":""}]},{"templateVersion":"1.0.0","defaultVersion":"v2.13.0","documentation":"https://hasura.io/docs/latest/index/","type":"hasura","name":"Hasura","description":"Instant realtime GraphQL APIs on any Postgres application, existing or new.","labels":["graphql","database"],"services":{"$$id":{"name":"Hasura","depends_on":["$$id-postgresql"],"image":"hasura/graphql-engine:$$core_version","volumes":[],"environment":["HASURA_GRAPHQL_ENABLE_CONSOLE=$$config_hasura_graphql_enable_console","HASURA_GRAPHQL_METADATA_DATABASE_URL=$$secret_hasura_graphql_metadata_database_url","HASURA_GRAPHQL_ADMIN_SECRET=$$secret_hasura_graphql_admin_secret"],"ports":["8080"]},"$$id-postgresql":{"name":"PostgreSQL","depends_on":[],"image":"postgres:12-alpine","volumes":["$$id-postgresql-data:/var/lib/postgresql/data"],"environment":["POSTGRES_USER=$$config_postgres_user","POSTGRES_PASSWORD=$$secret_postgres_password","POSTGRES_DB=$$config_postgres_db"],"ports":[]}},"variables":[{"id":"$$config_hasura_graphql_enable_console","name":"HASURA_GRAPHQL_ENABLE_CONSOLE","label":"Enable Hasura Console","defaultValue":"true","description":""},{"id":"$$secret_hasura_graphql_metadata_database_url","name":"HASURA_GRAPHQL_METADATA_DATABASE_URL","label":"Hasura Metadata Database URL","defaultValue":"postgresql://$$config_postgres_user:$$secret_postgres_password@$$id-postgresql:5432/$$config_postgres_db","description":""},{"id":"$$secret_hasura_graphql_admin_secret","name":"HASURA_GRAPHQL_ADMIN_SECRET","label":"Hasura Admin Password","defaultValue":"$$generate_password","description":"","showOnConfiguration":true},{"id":"$$config_postgres_user","name":"POSTGRES_USER","label":"PostgreSQL User","defaultValue":"$$generate_username","description":""},{"id":"$$secret_postgres_password","name":"POSTGRES_PASSWORD","label":"PostgreSQL Password","defaultValue":"$$generate_password","description":""},{"id":"$$config_postgres_db","name":"POSTGRES_DB","label":"PostgreSQL Database","defaultValue":"hasura","description":""}]},{"templateVersion":"1.0.0","defaultVersion":"postgresql-v1.38.0","documentation":"https://umami.is/docs/getting-started","type":"umami-postgresql","name":"Umami","subname":"(PostgreSQL)","description":"A simple, easy to use, self-hosted web analytics solution.","services":{"$$id":{"name":"Umami","depends_on":["$$id-postgresql"],"image":"ghcr.io/umami-software/umami:$$core_version","volumes":[],"environment":["ADMIN_PASSWORD=$$secret_admin_password","DATABASE_URL=$$secret_database_url","DATABASE_TYPE=$$config_database_type","HASH_SALT=$$secret_hash_salt"],"ports":["3000"]},"$$id-postgresql":{"name":"PostgreSQL","depends_on":[],"image":"postgres:12-alpine","volumes":["$$id-postgresql-data:/var/lib/postgresql/data"],"environment":["POSTGRES_USER=$$config_postgres_user","POSTGRES_PASSWORD=$$secret_postgres_password","POSTGRES_DB=$$config_postgres_db"],"ports":[],"files":[{"location":"/docker-entrypoint-initdb.d/schema.postgresql.sql","content":"\n -- CreateTable\n CREATE TABLE \"account\" (\n \"user_id\" SERIAL NOT NULL,\n \"username\" VARCHAR(255) NOT NULL,\n \"password\" VARCHAR(60) NOT NULL,\n \"is_admin\" BOOLEAN NOT NULL DEFAULT false,\n \"created_at\" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,\n \"updated_at\" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,\n \n PRIMARY KEY (\"user_id\")\n );\n \n -- CreateTable\n CREATE TABLE \"event\" (\n \"event_id\" SERIAL NOT NULL,\n \"website_id\" INTEGER NOT NULL,\n \"session_id\" INTEGER NOT NULL,\n \"created_at\" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,\n \"url\" VARCHAR(500) NOT NULL,\n \"event_type\" VARCHAR(50) NOT NULL,\n \"event_value\" VARCHAR(50) NOT NULL,\n \n PRIMARY KEY (\"event_id\")\n );\n \n -- CreateTable\n CREATE TABLE \"pageview\" (\n \"view_id\" SERIAL NOT NULL,\n \"website_id\" INTEGER NOT NULL,\n \"session_id\" INTEGER NOT NULL,\n \"created_at\" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,\n \"url\" VARCHAR(500) NOT NULL,\n \"referrer\" VARCHAR(500),\n \n PRIMARY KEY (\"view_id\")\n );\n \n -- CreateTable\n CREATE TABLE \"session\" (\n \"session_id\" SERIAL NOT NULL,\n \"session_uuid\" UUID NOT NULL,\n \"website_id\" INTEGER NOT NULL,\n \"created_at\" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,\n \"hostname\" VARCHAR(100),\n \"browser\" VARCHAR(20),\n \"os\" VARCHAR(20),\n \"device\" VARCHAR(20),\n \"screen\" VARCHAR(11),\n \"language\" VARCHAR(35),\n \"country\" CHAR(2),\n \n PRIMARY KEY (\"session_id\")\n );\n \n -- CreateTable\n CREATE TABLE \"website\" (\n \"website_id\" SERIAL NOT NULL,\n \"website_uuid\" UUID NOT NULL,\n \"user_id\" INTEGER NOT NULL,\n \"name\" VARCHAR(100) NOT NULL,\n \"domain\" VARCHAR(500),\n \"share_id\" VARCHAR(64),\n \"created_at\" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,\n \n PRIMARY KEY (\"website_id\")\n );\n \n -- CreateIndex\n CREATE UNIQUE INDEX \"account.username_unique\" ON \"account\"(\"username\");\n \n -- CreateIndex\n CREATE INDEX \"event_created_at_idx\" ON \"event\"(\"created_at\");\n \n -- CreateIndex\n CREATE INDEX \"event_session_id_idx\" ON \"event\"(\"session_id\");\n \n -- CreateIndex\n CREATE INDEX \"event_website_id_idx\" ON \"event\"(\"website_id\");\n \n -- CreateIndex\n CREATE INDEX \"pageview_created_at_idx\" ON \"pageview\"(\"created_at\");\n \n -- CreateIndex\n CREATE INDEX \"pageview_session_id_idx\" ON \"pageview\"(\"session_id\");\n \n -- CreateIndex\n CREATE INDEX \"pageview_website_id_created_at_idx\" ON \"pageview\"(\"website_id\", \"created_at\");\n \n -- CreateIndex\n CREATE INDEX \"pageview_website_id_idx\" ON \"pageview\"(\"website_id\");\n \n -- CreateIndex\n CREATE INDEX \"pageview_website_id_session_id_created_at_idx\" ON \"pageview\"(\"website_id\", \"session_id\", \"created_at\");\n \n -- CreateIndex\n CREATE UNIQUE INDEX \"session.session_uuid_unique\" ON \"session\"(\"session_uuid\");\n \n -- CreateIndex\n CREATE INDEX \"session_created_at_idx\" ON \"session\"(\"created_at\");\n \n -- CreateIndex\n CREATE INDEX \"session_website_id_idx\" ON \"session\"(\"website_id\");\n \n -- CreateIndex\n CREATE UNIQUE INDEX \"website.website_uuid_unique\" ON \"website\"(\"website_uuid\");\n \n -- CreateIndex\n CREATE UNIQUE INDEX \"website.share_id_unique\" ON \"website\"(\"share_id\");\n \n -- CreateIndex\n CREATE INDEX \"website_user_id_idx\" ON \"website\"(\"user_id\");\n \n -- AddForeignKey\n ALTER TABLE \"event\" ADD FOREIGN KEY (\"session_id\") REFERENCES \"session\"(\"session_id\") ON DELETE CASCADE ON UPDATE CASCADE;\n \n -- AddForeignKey\n ALTER TABLE \"event\" ADD FOREIGN KEY (\"website_id\") REFERENCES \"website\"(\"website_id\") ON DELETE CASCADE ON UPDATE CASCADE;\n \n -- AddForeignKey\n ALTER TABLE \"pageview\" ADD FOREIGN KEY (\"session_id\") REFERENCES \"session\"(\"session_id\") ON DELETE CASCADE ON UPDATE CASCADE;\n \n -- AddForeignKey\n ALTER TABLE \"pageview\" ADD FOREIGN KEY (\"website_id\") REFERENCES \"website\"(\"website_id\") ON DELETE CASCADE ON UPDATE CASCADE;\n \n -- AddForeignKey\n ALTER TABLE \"session\" ADD FOREIGN KEY (\"website_id\") REFERENCES \"website\"(\"website_id\") ON DELETE CASCADE ON UPDATE CASCADE;\n \n -- AddForeignKey\n ALTER TABLE \"website\" ADD FOREIGN KEY (\"user_id\") REFERENCES \"account\"(\"user_id\") ON DELETE CASCADE ON UPDATE CASCADE;\n \n insert into account (username, password, is_admin) values ('admin', '$$hashed$$secret_admin_password', true);"}]}},"variables":[{"id":"$$secret_database_url","name":"DATABASE_URL","label":"Database URL for PostgreSQL","defaultValue":"postgresql://$$config_postgres_user:$$secret_postgres_password@$$id-postgresql:5432/$$config_postgres_db","description":""},{"id":"$$secret_hash_salt","name":"HASH_SALT","label":"Hash Salt","defaultValue":"$$generate_hex(64)","description":""},{"id":"$$config_database_type","name":"DATABASE_TYPE","label":"Database Type","defaultValue":"postgresql","description":""},{"id":"$$config_postgres_user","name":"POSTGRES_USER","label":"PostgreSQL User","defaultValue":"$$generate_username","description":""},{"id":"$$secret_postgres_password","name":"POSTGRES_PASSWORD","label":"PostgreSQL Password","defaultValue":"$$generate_password","description":""},{"id":"$$config_postgres_db","name":"POSTGRES_DB","label":"PostgreSQL Database","defaultValue":"umami","description":""},{"id":"$$secret_admin_password","name":"ADMIN_PASSWORD","label":"Initial Admin Password","defaultValue":"$$generate_password","description":"","showOnConfiguration":true}]},{"templateVersion":"1.0.0","ignore":true,"defaultVersion":"postgresql-v1.38.0","documentation":"https://umami.is/docs/getting-started","type":"umami","name":"Umami","subname":"(PostgreSQL)","description":"A simple, easy to use, self-hosted web analytics solution.","services":{"$$id":{"name":"Umami","depends_on":["$$id-postgresql"],"image":"ghcr.io/umami-software/umami:$$core_version","volumes":[],"environment":["ADMIN_PASSWORD=$$secret_admin_password","DATABASE_URL=$$secret_database_url","DATABASE_TYPE=$$config_database_type","HASH_SALT=$$secret_hash_salt"],"ports":["3000"]},"$$id-postgresql":{"name":"PostgreSQL","depends_on":[],"image":"postgres:12-alpine","volumes":["$$id-postgresql-data:/var/lib/postgresql/data"],"environment":["POSTGRES_USER=$$config_postgres_user","POSTGRES_PASSWORD=$$secret_postgres_password","POSTGRES_DB=$$config_postgres_db"],"ports":[],"files":[{"location":"/docker-entrypoint-initdb.d/schema.postgresql.sql","content":"\n -- CreateTable\n CREATE TABLE \"account\" (\n \"user_id\" SERIAL NOT NULL,\n \"username\" VARCHAR(255) NOT NULL,\n \"password\" VARCHAR(60) NOT NULL,\n \"is_admin\" BOOLEAN NOT NULL DEFAULT false,\n \"created_at\" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,\n \"updated_at\" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,\n \n PRIMARY KEY (\"user_id\")\n );\n \n -- CreateTable\n CREATE TABLE \"event\" (\n \"event_id\" SERIAL NOT NULL,\n \"website_id\" INTEGER NOT NULL,\n \"session_id\" INTEGER NOT NULL,\n \"created_at\" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,\n \"url\" VARCHAR(500) NOT NULL,\n \"event_type\" VARCHAR(50) NOT NULL,\n \"event_value\" VARCHAR(50) NOT NULL,\n \n PRIMARY KEY (\"event_id\")\n );\n \n -- CreateTable\n CREATE TABLE \"pageview\" (\n \"view_id\" SERIAL NOT NULL,\n \"website_id\" INTEGER NOT NULL,\n \"session_id\" INTEGER NOT NULL,\n \"created_at\" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,\n \"url\" VARCHAR(500) NOT NULL,\n \"referrer\" VARCHAR(500),\n \n PRIMARY KEY (\"view_id\")\n );\n \n -- CreateTable\n CREATE TABLE \"session\" (\n \"session_id\" SERIAL NOT NULL,\n \"session_uuid\" UUID NOT NULL,\n \"website_id\" INTEGER NOT NULL,\n \"created_at\" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,\n \"hostname\" VARCHAR(100),\n \"browser\" VARCHAR(20),\n \"os\" VARCHAR(20),\n \"device\" VARCHAR(20),\n \"screen\" VARCHAR(11),\n \"language\" VARCHAR(35),\n \"country\" CHAR(2),\n \n PRIMARY KEY (\"session_id\")\n );\n \n -- CreateTable\n CREATE TABLE \"website\" (\n \"website_id\" SERIAL NOT NULL,\n \"website_uuid\" UUID NOT NULL,\n \"user_id\" INTEGER NOT NULL,\n \"name\" VARCHAR(100) NOT NULL,\n \"domain\" VARCHAR(500),\n \"share_id\" VARCHAR(64),\n \"created_at\" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,\n \n PRIMARY KEY (\"website_id\")\n );\n \n -- CreateIndex\n CREATE UNIQUE INDEX \"account.username_unique\" ON \"account\"(\"username\");\n \n -- CreateIndex\n CREATE INDEX \"event_created_at_idx\" ON \"event\"(\"created_at\");\n \n -- CreateIndex\n CREATE INDEX \"event_session_id_idx\" ON \"event\"(\"session_id\");\n \n -- CreateIndex\n CREATE INDEX \"event_website_id_idx\" ON \"event\"(\"website_id\");\n \n -- CreateIndex\n CREATE INDEX \"pageview_created_at_idx\" ON \"pageview\"(\"created_at\");\n \n -- CreateIndex\n CREATE INDEX \"pageview_session_id_idx\" ON \"pageview\"(\"session_id\");\n \n -- CreateIndex\n CREATE INDEX \"pageview_website_id_created_at_idx\" ON \"pageview\"(\"website_id\", \"created_at\");\n \n -- CreateIndex\n CREATE INDEX \"pageview_website_id_idx\" ON \"pageview\"(\"website_id\");\n \n -- CreateIndex\n CREATE INDEX \"pageview_website_id_session_id_created_at_idx\" ON \"pageview\"(\"website_id\", \"session_id\", \"created_at\");\n \n -- CreateIndex\n CREATE UNIQUE INDEX \"session.session_uuid_unique\" ON \"session\"(\"session_uuid\");\n \n -- CreateIndex\n CREATE INDEX \"session_created_at_idx\" ON \"session\"(\"created_at\");\n \n -- CreateIndex\n CREATE INDEX \"session_website_id_idx\" ON \"session\"(\"website_id\");\n \n -- CreateIndex\n CREATE UNIQUE INDEX \"website.website_uuid_unique\" ON \"website\"(\"website_uuid\");\n \n -- CreateIndex\n CREATE UNIQUE INDEX \"website.share_id_unique\" ON \"website\"(\"share_id\");\n \n -- CreateIndex\n CREATE INDEX \"website_user_id_idx\" ON \"website\"(\"user_id\");\n \n -- AddForeignKey\n ALTER TABLE \"event\" ADD FOREIGN KEY (\"session_id\") REFERENCES \"session\"(\"session_id\") ON DELETE CASCADE ON UPDATE CASCADE;\n \n -- AddForeignKey\n ALTER TABLE \"event\" ADD FOREIGN KEY (\"website_id\") REFERENCES \"website\"(\"website_id\") ON DELETE CASCADE ON UPDATE CASCADE;\n \n -- AddForeignKey\n ALTER TABLE \"pageview\" ADD FOREIGN KEY (\"session_id\") REFERENCES \"session\"(\"session_id\") ON DELETE CASCADE ON UPDATE CASCADE;\n \n -- AddForeignKey\n ALTER TABLE \"pageview\" ADD FOREIGN KEY (\"website_id\") REFERENCES \"website\"(\"website_id\") ON DELETE CASCADE ON UPDATE CASCADE;\n \n -- AddForeignKey\n ALTER TABLE \"session\" ADD FOREIGN KEY (\"website_id\") REFERENCES \"website\"(\"website_id\") ON DELETE CASCADE ON UPDATE CASCADE;\n \n -- AddForeignKey\n ALTER TABLE \"website\" ADD FOREIGN KEY (\"user_id\") REFERENCES \"account\"(\"user_id\") ON DELETE CASCADE ON UPDATE CASCADE;\n \n insert into account (username, password, is_admin) values ('admin', '$$hashed$$secret_admin_password', true);"}]}},"variables":[{"id":"$$secret_database_url","name":"DATABASE_URL","label":"Database URL for PostgreSQL","defaultValue":"postgresql://$$config_postgres_user:$$secret_postgres_password@$$id-postgresql:5432/$$config_postgres_db","description":""},{"id":"$$secret_hash_salt","name":"HASH_SALT","label":"Hash Salt","defaultValue":"$$generate_hex(64)","description":""},{"id":"$$config_database_type","name":"DATABASE_TYPE","label":"Database Type","defaultValue":"postgresql","description":""},{"id":"$$config_postgres_user","name":"POSTGRES_USER","label":"PostgreSQL User","defaultValue":"$$generate_username","description":""},{"id":"$$secret_postgres_password","name":"POSTGRES_PASSWORD","label":"PostgreSQL Password","defaultValue":"$$generate_password","description":""},{"id":"$$config_postgres_db","name":"POSTGRES_DB","label":"PostgreSQL Database","defaultValue":"umami","description":""},{"id":"$$secret_admin_password","name":"ADMIN_PASSWORD","label":"Initial Admin Password","defaultValue":"$$generate_password","description":"","showOnConfiguration":true}]},{"templateVersion":"1.0.0","defaultVersion":"v0.29.1","documentation":"https://docs.meilisearch.com/learn/getting_started/quick_start.html","type":"meilisearch","name":"MeiliSearch","description":"A lightning Fast, Ultra Relevant, and Typo-Tolerant Search Engine.","services":{"$$id":{"name":"MeiliSearch","documentation":"https://docs.meilisearch.com/","depends_on":[],"image":"getmeili/meilisearch:$$core_version","volumes":["$$id-datams:/meili_data/data.ms","$$id-data:/meili_data","$$id-snapshot:/snapshot","$$id-dump:/dumps"],"environment":["MEILI_MASTER_KEY=$$secret_meili_master_key"],"ports":["7700"]}},"variables":[{"id":"$$secret_meili_master_key","name":"MEILI_MASTER_KEY","label":"Master Key","defaultValue":"$$generate_hex(64)","description":"","showOnConfiguration":true}]},{"templateVersion":"1.0.0","ignore":true,"defaultVersion":"latest","documentation":"https://docs.ghost.org","arch":"amd64","type":"ghost-mariadb","name":"Ghost","subname":"(MariaDB)","description":"Free and open source blogging platform.","labels":["cms","blog"],"services":{"$$id":{"name":"Ghost","depends_on":["$$id-mariadb"],"image":"bitnami/ghost:$$core_version","volumes":["$$id-ghost:/bitnami/ghost"],"environment":["url=$$config_url","GHOST_HOST=$$config_ghost_host","GHOST_ENABLE_HTTPS=$$config_ghost_enable_https","GHOST_EMAIL=$$config_ghost_email","GHOST_PASSWORD=$$secret_ghost_password","GHOST_DATABASE_HOST=$$config_ghost_database_host","GHOST_DATABASE_USER=$$config_mariadb_user","GHOST_DATABASE_PASSWORD=$$secret_ghost_database_password","GHOST_DATABASE_NAME=$$config_mariadb_database","GHOST_DATABASE_PORT_NUMBER=3306"],"ports":["2368"]},"$$id-mariadb":{"name":"MariaDB","depends_on":[],"image":"bitnami/mariadb:latest","volumes":["$$id-mariadb:/bitnami/mariadb"],"environment":["MARIADB_USER=$$config_mariadb_user","MARIADB_PASSWORD=$$secret_mariadb_password","MARIADB_DATABASE=$$config_mariadb_database","MARIADB_ROOT_USER=$$config_mariadb_root_user","MARIADB_ROOT_PASSWORD=$$secret_mariadb_root_password"],"ports":[]}},"variables":[{"id":"$$config_url","name":"url","label":"URL","defaultValue":"$$generate_fqdn","description":""},{"id":"$$config_ghost_host","name":"GHOST_HOST","label":"Ghost Host","defaultValue":"$$generate_domain","description":""},{"id":"$$config_ghost_enable_https","name":"GHOST_ENABLE_HTTPS","label":"Ghost Enable HTTPS","defaultValue":"no","description":""},{"id":"$$config_ghost_email","name":"GHOST_EMAIL","label":"Ghost Default Email","defaultValue":"admin@example.com","description":""},{"id":"$$secret_ghost_password","name":"GHOST_PASSWORD","label":"Ghost Default Password","defaultValue":"$$generate_password","description":"","showOnConfiguration":true},{"id":"$$config_ghost_database_host","name":"GHOST_DATABASE_HOST","label":"Ghost Database Host","defaultValue":"$$id-mariadb","description":""},{"id":"$$config_ghost_database_user","name":"GHOST_DATABASE_USER","label":"MariaDB User","defaultValue":"$$config_mariadb_user","description":""},{"id":"$$secret_ghost_database_password","name":"GHOST_DATABASE_PASSWORD","label":"MariaDB Password","defaultValue":"$$secret_mariadb_password","description":""},{"id":"$$config_ghost_database_name","name":"GHOST_DATABASE_NAME","label":"MariaDB Database","defaultValue":"$$config_mariadb_database","description":""},{"id":"$$config_mariadb_user","name":"MARIADB_USER","label":"MariaDB User","defaultValue":"$$generate_username","description":""},{"id":"$$secret_mariadb_password","name":"MARIADB_PASSWORD","label":"MariaDB Password","defaultValue":"$$generate_password","description":""},{"id":"$$config_mariadb_database","name":"MARIADB_DATABASE","label":"MariaDB Database","defaultValue":"ghost","description":""},{"id":"$$config_mariadb_root_user","name":"MARIADB_ROOT_USER","label":"MariaDB Root User","defaultValue":"$$generate_username","description":""},{"id":"$$secret_mariadb_root_password","name":"MARIADB_ROOT_PASSWORD","label":"MariaDB Root Password","defaultValue":"$$generate_password","description":""}]},{"templateVersion":"1.0.0","defaultVersion":"5.22","documentation":"https://docs.ghost.org","type":"ghost-only","name":"Ghost","subname":"(without Database)","description":"Free and open source blogging platform.","services":{"$$id":{"name":"Ghost","image":"ghost:$$core_version","volumes":["$$id-ghost:/var/lib/ghost/content"],"environment":["url=$$config_url","database__client=$$config_database__client","database__connection__host=$$config_database__connection__host","database__connection__user=$$config_database__connection__user","database__connection__password=$$secret_database__connection__password","database__connection__database=$$config_database__connection__database"],"ports":["2368"]}},"variables":[{"id":"$$config_url","name":"url","label":"URL","defaultValue":"$$generate_fqdn","description":""},{"id":"$$config_database__client","name":"database__client","label":"Database Client","defaultValue":"mysql","description":"","required":true},{"id":"$$config_database__connection__host","name":"database__connection__host","label":"Database Host","defaultValue":"","description":"","required":true,"placeholder":"db.coolify.io"},{"id":"$$config_database__connection__user","name":"database__connection__user","label":"Database User","defaultValue":"","description":"","placeholder":"ghost","required":true},{"id":"$$secret_database__connection__password","name":"database__connection__password","label":"Database Password","defaultValue":"","description":"","placeholder":"superSecretP4ssword","showOnConfiguration":true,"required":true},{"id":"$$config_database__connection__database","name":"database__connection__database","label":"Database Name","defaultValue":"","description":"","placeholder":"ghost_db","required":true}]},{"templateVersion":"1.0.0","defaultVersion":"5.22","documentation":"https://docs.ghost.org","type":"ghost-mysql","name":"Ghost","subname":"(MySQL)","description":"Ghost is a free and open source blogging platform.","services":{"$$id":{"name":"Ghost","depends_on":["$$id-mysql"],"image":"ghost:$$core_version","volumes":["$$id-ghost:/var/lib/ghost/content"],"environment":["url=$$config_url","database__client=$$config_database__client","database__connection__host=$$config_database__connection__host","database__connection__user=$$config_mysql_user","database__connection__password=$$secret_mysql_password","database__connection__database=$$config_mysql_database"],"ports":["2368"]},"$$id-mysql":{"name":"MySQL","depends_on":[],"image":"mysql:8.0","volumes":["$$id-mysql:/var/lib/mysql"],"environment":["MYSQL_USER=$$config_mysql_user","MYSQL_PASSWORD=$$secret_mysql_password","MYSQL_DATABASE=$$config_mysql_database","MYSQL_ROOT_PASSWORD=$$secret_mysql_root_password"],"ports":[]}},"variables":[{"id":"$$config_url","name":"url","label":"URL","defaultValue":"$$generate_fqdn","description":""},{"id":"$$config_database__client","name":"database__client","label":"Database Client","defaultValue":"mysql","description":"","readOnly":true},{"id":"$$config_database__connection__host","name":"database__connection__host","label":"Database Host","defaultValue":"$$id-mysql","description":""},{"id":"$$config_mysql_user","main":"$$id-mysql","name":"MYSQL_USER","label":"MySQL User","defaultValue":"$$generate_username","description":""},{"id":"$$secret_mysql_password","main":"$$id-mysql","name":"MYSQL_PASSWORD","label":"MySQL Password","defaultValue":"$$generate_password","description":""},{"id":"$$config_mysql_database","main":"$$id-mysql","name":"MYSQL_DATABASE","label":"MySQL Database","defaultValue":"ghost","description":""},{"id":"$$secret_mysql_root_password","name":"MYSQL_ROOT_PASSWORD","label":"MySQL Root Password","defaultValue":"$$generate_password","description":""}]},{"templateVersion":"1.0.0","defaultVersion":"php8.1","documentation":"https://wordpress.org/","type":"wordpress","name":"WordPress","subname":"(MySQL)","description":"A content management system based on PHP.","labels":["wordpress","php","cms"],"services":{"$$id":{"name":"WordPress","depends_on":["$$id-mysql"],"image":"wordpress:$$core_version","volumes":["$$id-wordpress-data:/var/www/html"],"environment":["WORDPRESS_DB_HOST=$$config_wordpress_db_host","WORDPRESS_DB_USER=$$config_mysql_user","WORDPRESS_DB_PASSWORD=$$secret_mysql_password","WORDPRESS_DB_NAME=$$config_mysql_database","WORDPRESS_CONFIG_EXTRA=$$config_wordpress_config_extra"],"ports":["80"]},"$$id-mysql":{"name":"MySQL","depends_on":[],"image":"bitnami/mysql:5.7","imageArm":"mysql:8.0","volumes":["$$id-mysql-data:/bitnami/mysql/data"],"volumesArm":["$$id-mysql-data:/var/lib/mysql"],"environment":["MYSQL_ROOT_PASSWORD=$$secret_mysql_root_password","MYSQL_ROOT_USER=$$config_mysql_root_user","MYSQL_DATABASE=$$config_mysql_database","MYSQL_USER=$$config_mysql_user","MYSQL_PASSWORD=$$secret_mysql_password"]}},"variables":[{"id":"$$config_wordpress_db_host","name":"WORDPRESS_DB_HOST","label":"Database Host","defaultValue":"$$id-mysql","description":"","readOnly":true},{"id":"$$config_wordpress_config_extra","name":"WORDPRESS_CONFIG_EXTRA","label":"WordPress Config Extra","defaultValue":"","description":"","type":"textarea","placeholder":"define('WP_DEBUG', true);\ndefine('WP_DEBUG_LOG', true);\ndefine('WP_DEBUG_DISPLAY', false);\n@ini_set('display_errors', 0);\n"},{"id":"$$secret_mysql_root_password","name":"MYSQL_ROOT_PASSWORD","label":"MySQL Root Password","defaultValue":"$$generate_password","description":"","readOnly":true},{"id":"$$config_mysql_root_user","name":"MYSQL_ROOT_USER","label":"MySQL Root User","defaultValue":"$$generate_username","description":"","readOnly":true},{"id":"$$config_mysql_database","name":"MYSQL_DATABASE","label":"MySQL Database","defaultValue":"wordpress","description":"","readOnly":true},{"id":"$$config_mysql_user","name":"MYSQL_USER","label":"MySQL User","defaultValue":"$$generate_username","description":"","readOnly":true},{"id":"$$secret_mysql_password","name":"MYSQL_PASSWORD","label":"MySQL Password","defaultValue":"$$generate_password","description":"","readOnly":true}]},{"templateVersion":"1.0.0","defaultVersion":"php8.1","documentation":"https://wordpress.org/","type":"wordpress-only","name":"WordPress","subname":"(without DB)","description":"A content management system based on PHP.","labels":["wordpress","php","cms"],"services":{"$$id":{"name":"WordPress","image":"wordpress:$$core_version","volumes":["$$id-wordpress-data:/var/www/html"],"environment":["WORDPRESS_DB_HOST=$$config_wordpress_db_host","WORDPRESS_DB_PORT=$$config_wordpress_db_port","WORDPRESS_DB_USER=$$config_wordpress_db_user","WORDPRESS_DB_PASSWORD=$$secret_wordpress_db_password","WORDPRESS_DB_NAME=$$config_wordpress_db_name","WORDPRESS_CONFIG_EXTRA=$$config_wordpress_config_extra"],"ports":["80"]}},"variables":[{"id":"$$config_wordpress_db_host","name":"WORDPRESS_DB_HOST","label":"Database Host","defaultValue":"","description":"","placeholder":"db.coollabs.io","required":true},{"id":"$$config_wordpress_db_port","name":"WORDPRESS_DB_PORT","label":"Database Port","defaultValue":"","description":"","placeholder":"3306","required":true},{"id":"$$config_wordpress_db_user","name":"WORDPRESS_DB_USER","label":"Database User","defaultValue":"","description":"","placeholder":"wordpress","required":true},{"id":"$$secret_wordpress_db_password","name":"WORDPRESS_DB_PASSWORD","label":"Database Password","defaultValue":"","description":"","placeholder":"supers3cr3tpassw0rd!","required":true,"showOnConfiguration":true},{"id":"$$config_wordpress_db_name","name":"WORDPRESS_DB_NAME","label":"Database Name","defaultValue":"","description":"","placeholder":"wordpress","required":true},{"id":"$$config_wordpress_config_extra","name":"WORDPRESS_CONFIG_EXTRA","label":"Extra Config","defaultValue":"","description":"","type":"textarea","placeholder":"define('WP_DEBUG', true);\ndefine('WP_DEBUG_LOG', true);\ndefine('WP_DEBUG_DISPLAY', false);\n@ini_set('display_errors', 0);\n"}]},{"templateVersion":"1.0.0","defaultVersion":"4.7.1","documentation":"https://coder.com/docs/coder-oss/latest","type":"vscodeserver","name":"VSCode Server","description":"Visual Studio Code on a remote server, accessible through the browser.","labels":["vscode","ide"],"services":{"$$id":{"name":"VSCode Server","depends_on":[],"image":"codercom/code-server:$$core_version","volumes":["$$id-config-data:/home/coder/.local/share/code-server","$$id-vscodeserver-data:/home/coder","$$id-keys-directory:/root/.ssh","$$id-theme-and-plugin-directory:/root/.local/share/code-server"],"environment":["PASSWORD=$$secret_password"],"ports":["8080"]}},"variables":[{"id":"$$secret_password","name":"PASSWORD","label":"Password","defaultValue":"$$generate_password","description":"","showOnConfiguration":true}]},{"templateVersion":"1.0.0","defaultVersion":"RELEASE.2022-10-15T19-57-03Z","documentation":"https://min.io/docs/minio","type":"minio","name":"MinIO","description":"A cloud storage server compatible with Amazon S3.","labels":["storage","s3"],"services":{"$$id":{"name":"MinIO","command":"server /data --console-address :9001","depends_on":[],"image":"minio/minio:$$core_version","volumes":["$$id-minio-data:/data","$$id-data-write:/files"],"environment":["MINIO_SERVER_URL=$$config_coolify_fqdn_minio_console","MINIO_BROWSER_REDIRECT_URL=$$config_minio_browser_redirect_url","MINIO_DOMAIN=$$config_minio_domain","MINIO_ROOT_USER=$$config_minio_root_user","MINIO_ROOT_PASSWORD=$$secret_minio_root_password"],"ports":["9000","9001"],"proxy":[{"port":"9000","domain":"$$config_coolify_fqdn_minio_console"},{"port":"9001"}]}},"variables":[{"id":"$$config_coolify_fqdn_minio_console","name":"MINIO_SERVER_URL","label":"MinIO Server URL","defaultValue":"","description":"Specify the URL hostname the MinIO Console should use for connecting to the MinIO Server.","required":true},{"id":"$$config_minio_browser_redirect_url","name":"MINIO_BROWSER_REDIRECT_URL","label":"Browser Redirect URL","defaultValue":"$$generate_fqdn","description":""},{"id":"$$config_minio_domain","name":"MINIO_DOMAIN","label":"Domain","defaultValue":"$$generate_domain","description":""},{"id":"$$config_minio_root_user","name":"MINIO_ROOT_USER","label":"Root User","defaultValue":"$$generate_username","description":""},{"id":"$$secret_minio_root_password","name":"MINIO_ROOT_PASSWORD","label":"Root User Password","defaultValue":"$$generate_password","description":"","showOnConfiguration":true}]},{"templateVersion":"1.0.0","defaultVersion":"0.21.1","documentation":"https://fider.io/docs","type":"fider","name":"Fider","description":"A platform to collect and organize customer feedback.","labels":["suggestion","feedback"],"services":{"$$id":{"name":"Fider","image":"getfider/fider:$$core_version","depends_on":["$$id-postgresql"],"environment":["BASE_URL=$$config_base_url","DATABASE_URL=$$secret_database_url","JWT_SECRET=$$secret_jwt_secret","EMAIL_NOREPLY=$$config_email_noreply","EMAIL_MAILGUN_API=$$secret_email_mailgun_api","EMAIL_MAILGUN_REGION=$$config_email_mailgun_region","EMAIL_MAILGUN_DOMAIN=$$config_email_mailgun_domain","EMAIL_SMTP_HOST=$$config_email_smtp_host","EMAIL_SMTP_PORT=$$config_email_smtp_port","EMAIL_SMTP_USER=$$config_email_smtp_user","EMAIL_SMTP_PASSWORD=$$secret_email_smtp_password","EMAIL_SMTP_ENABLE_STARTTLS=$$config_email_smtp_enable_starttls"],"ports":["3000"]},"$$id-postgresql":{"name":"PostgreSQL","depends_on":[],"image":"postgres:12-alpine","volumes":["$$id-postgresql-data:/var/lib/postgresql/data"],"environment":["POSTGRES_USER=$$config_postgres_user","POSTGRES_PASSWORD=$$secret_postgres_password","POSTGRES_DB=$$config_postgres_db"]}},"variables":[{"id":"$$config_base_url","name":"BASE_URL","label":"Base URL","defaultValue":"$$generate_fqdn","description":""},{"id":"$$secret_database_url","name":"DATABASE_URL","label":"Database URL for PostgreSQL","defaultValue":"postgresql://$$config_postgres_user:$$secret_postgres_password@$$id-postgresql:5432/$$config_postgres_db?sslmode=disable","description":""},{"id":"$$secret_jwt_secret","name":"JWT_SECRET","label":"JWT Secret","defaultValue":"$$generate_hex(64)","description":""},{"id":"$$config_email_noreply","name":"EMAIL_NOREPLY","label":"No Reply Email Address","defaultValue":"noreply@example.com","description":""},{"id":"$$secret_email_mailgun_api","name":"EMAIL_MAILGUN_API","label":"Mailgun API Key","defaultValue":"","description":"","showOnConfiguration":true},{"id":"$$config_email_mailgun_region","name":"EMAIL_MAILGUN_REGION","label":"Mailgun Region","defaultValue":"EU","description":""},{"id":"$$config_email_mailgun_domain","name":"EMAIL_MAILGUN_DOMAIN","label":"Mailgun Domain","defaultValue":"","description":""},{"id":"$$config_email_smtp_host","name":"EMAIL_SMTP_HOST","label":"SMTP Host","defaultValue":"","description":""},{"id":"$$config_email_smtp_port","name":"EMAIL_SMTP_PORT","label":"SMTP Port","defaultValue":"587","description":""},{"id":"$$config_email_smtp_user","name":"EMAIL_SMTP_USER","label":"SMTP User","defaultValue":"","description":""},{"id":"$$secret_email_smtp_password","name":"EMAIL_SMTP_PASSWORD","label":"SMTP Password","defaultValue":"","description":"","showOnConfiguration":true},{"id":"$$config_email_smtp_enable_starttls","name":"EMAIL_SMTP_ENABLE_STARTTLS","label":"SMTP Enable StartTLS","defaultValue":"false","description":""},{"id":"$$config_postgres_user","name":"POSTGRES_USER","label":"PostgreSQL User","defaultValue":"$$generate_username","description":""},{"id":"$$secret_postgres_password","name":"POSTGRES_PASSWORD","label":"PostgreSQL Password","defaultValue":"$$generate_password","description":""},{"id":"$$config_postgres_db","name":"POSTGRES_DB","label":"PostgreSQL Database","defaultValue":"$$generate_username","description":""}]},{"templateVersion":"1.0.0","defaultVersion":"0.198.1","documentation":"https://docs.n8n.io","type":"n8n","name":"n8n.io","description":"A free and open node based Workflow Automation Tool.","labels":["workflow","automation","ifttt","zapier","nodered"],"services":{"$$id":{"name":"N8n","depends_on":[],"image":"n8nio/n8n:$$core_version","volumes":["$$id-data:/root/.n8n","$$id-data-write:/files","/var/run/docker.sock:/var/run/docker.sock"],"environment":["WEBHOOK_URL=$$config_webhook_url"],"ports":["5678"]}},"variables":[{"id":"$$config_webhook_url","name":"WEBHOOK_URL","label":"Webhook URL","defaultValue":"$$generate_fqdn","description":""}]},{"templateVersion":"1.0.0","defaultVersion":"stable","documentation":"https://plausible.io/doc/","arch":"amd64","type":"plausibleanalytics","name":"Plausible Analytics","description":"A lightweight and open-source website analytics tool.","labels":["analytics","statistics","plausible","gdpr","no-cookie","google analytics"],"services":{"$$id":{"name":"Plausible Analytics","command":"sh -c \"sleep 10 && /entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh db init-admin && /entrypoint.sh run\"","depends_on":["$$id-postgresql","$$id-clickhouse"],"image":"plausible/analytics:$$core_version","environment":["ADMIN_USER_EMAIL=$$config_admin_user_email","ADMIN_USER_NAME=$$config_admin_user_name","ADMIN_USER_PWD=$$secret_admin_user_pwd","BASE_URL=$$config_base_url","SECRET_KEY_BASE=$$secret_secret_key_base","DISABLE_AUTH=$$config_disable_auth","DISABLE_REGISTRATION=$$config_disable_registration","DATABASE_URL=$$secret_database_url","CLICKHOUSE_DATABASE_URL=$$secret_clickhouse_database_url"],"ports":["8000"]},"$$id-postgresql":{"name":"PostgreSQL","image":"bitnami/postgresql:13","volumes":["$$id-postgresql-data:/bitnami/postgresql"],"environment":["POSTGRESQL_PASSWORD=$$secret_postgresql_password","POSTGRESQL_USERNAME=$$config_postgresql_username","POSTGRESQL_DATABASE=$$config_postgresql_database"]},"$$id-clickhouse":{"name":"Clickhouse","volumes":["$$id-clickhouse-data:/var/lib/clickhouse"],"image":"clickhouse/clickhouse-server:22.6-alpine","ulimits":{"nofile":{"soft":262144,"hard":262144}},"files":[{"location":"/etc/clickhouse-server/users.d/logging.xml","content":"warningtrue"},{"location":"/etc/clickhouse-server/config.d/logging.xml","content":"00"},{"location":"/docker-entrypoint-initdb.d/init.query","content":"CREATE DATABASE IF NOT EXISTS plausible;"},{"location":"/docker-entrypoint-initdb.d/init-db.sh","content":"clickhouse client --queries-file /docker-entrypoint-initdb.d/init.query"}]}},"variables":[{"id":"$$config_base_url","name":"BASE_URL","label":"Base URL","defaultValue":"$$generate_fqdn","description":"You must set this to the FQDN of the Plausible Analytics instance. This is used to generate the links to the Plausible Analytics instance."},{"id":"$$secret_database_url","name":"DATABASE_URL","label":"Database URL for PostgreSQL","defaultValue":"postgresql://$$config_postgresql_username:$$secret_postgresql_password@$$id-postgresql:5432/$$config_postgresql_database","description":""},{"id":"$$secret_clickhouse_database_url","name":"CLICKHOUSE_DATABASE_URL","label":"Database URL for Clickhouse","defaultValue":"http://$$id-clickhouse:8123/plausible","description":""},{"id":"$$config_admin_user_email","name":"ADMIN_USER_EMAIL","label":"Admin Email Address","defaultValue":"admin@example.com","description":"This is the admin email. Please change it."},{"id":"$$config_admin_user_name","name":"ADMIN_USER_NAME","label":"Admin User Name","defaultValue":"$$generate_username","description":"This is the admin username. Please change it."},{"id":"$$secret_admin_user_pwd","name":"ADMIN_USER_PWD","label":"Admin User Password","defaultValue":"$$generate_password","description":"This is the admin password. Please change it.","showOnConfiguration":true},{"id":"$$secret_secret_key_base","name":"SECRET_KEY_BASE","label":"Secret Key Base","defaultValue":"$$generate_hex(64)","description":""},{"id":"$$config_disable_auth","name":"DISABLE_AUTH","label":"Disable Authentication","defaultValue":"false","description":""},{"id":"$$config_disable_registration","name":"DISABLE_REGISTRATION","label":"Disable Registration","defaultValue":"true","description":""},{"id":"$$config_postgresql_username","main":"$$id-postgresql","name":"POSTGRESQL_USERNAME","label":"PostgreSQL Username","defaultValue":"postgresql","description":""},{"id":"$$secret_postgresql_password","main":"$$id-postgresql","name":"POSTGRESQL_PASSWORD","label":"PostgreSQL Password","defaultValue":"$$generate_password","description":"","showOnConfiguration":true},{"id":"$$config_postgresql_database","main":"$$id-postgresql","name":"POSTGRESQL_DATABASE","label":"PostgreSQL Database","defaultValue":"plausible","description":""},{"id":"$$config_scriptName","name":"SCRIPT_NAME","label":"Custom Script Name","defaultValue":"plausible.js","description":"This is the default script name."}]},{"templateVersion":"1.0.0","defaultVersion":"0.98.1","documentation":"https://docs.nocodb.com","type":"nocodb","name":"NocoDB","description":"Turns any MySQL, PostgreSQL, SQL Server, SQLite & MariaDB into a smart-spreadsheet.","labels":["database","airtable","spreadsheet"],"services":{"$$id":{"name":"NocoDB","image":"nocodb/nocodb:$$core_version","environment":["PORT=$$config_port","NC_DB=$$config_nc_db","DATABASE_URL=$$secret_database_url","NC_PUBLIC_URL=$$config_public_url","NC_AUTH_JWT_SECRET=$$secret_auth_jwt_secret","NC_SENTRY_DSN=$$secret_sentry_dsn","NC_CONNECT_TO_EXTERNAL_DB_DISABLED=$$config_connect_to_external_db_disabled","NC_DISABLE_TELE=$$config_disable_tele"],"volumes":["$$id-data:/usr/app/data"],"ports":["8080"]}},"variables":[{"id":"$$config_nc_db","name":"NC_DB","label":"Database","defaultValue":"","description":"MySQL, PostgreSQL and MSSQL connection urls supported. If absent: A local SQLite will be created in root folder."},{"id":"$$config_port","name":"PORT","label":"Port","defaultValue":"8080","description":""},{"id":"$$secret_database_url","name":"DATABASE_URL","label":"Database URL","defaultValue":"","description":"JDBC URL Format. Can be used instead of NC_DB. Used in 1-Click Heroku deployment."},{"id":"$$config_public_url","name":"NC_PUBLIC_URL","label":"Public URL","defaultValue":"","description":"Used for sending Email invitations. If absent: Best guess from http request params."},{"id":"$$secret_auth_jwt_secret","name":"NC_AUTH_JWT_SECRET","label":"Auth JWT Secret","defaultValue":"$$generate_hex(64)","description":"JWT secret used for auth and storing other secrets. If absent: A Random secret will be generated."},{"id":"$$secret_sentry_dsn","name":"NC_SENTRY_DSN","label":"Sentry DSN","defaultValue":"","description":"For Sentry monitoring."},{"id":"$$config_connect_to_external_db_disabled","name":"NC_CONNECT_TO_EXTERNAL_DB_DISABLED","label":"Disable External Database","defaultValue":"0","description":"Disable Project creation with external database. (Enter \"1\" to disable)."},{"id":"$$config_disable_tele","name":"NC_DISABLE_TELE","label":"NocoDB Disable Telemetry","defaultValue":"1","description":"Disable telemetry (Enter \"1\" to disable)."}]}] \ No newline at end of file diff --git a/apps/backup/.dockerignore b/apps/backup/.dockerignore new file mode 100644 index 000000000..1b8c356ca --- /dev/null +++ b/apps/backup/.dockerignore @@ -0,0 +1,2 @@ +node_modules +backup/* \ No newline at end of file diff --git a/apps/backup/Dockerfile b/apps/backup/Dockerfile new file mode 100644 index 000000000..178d70ab7 --- /dev/null +++ b/apps/backup/Dockerfile @@ -0,0 +1,27 @@ +ARG PNPM_VERSION=7.17.1 + +FROM node:18-slim as build +WORKDIR /app +RUN npm --no-update-notifier --no-fund --global install pnpm@${PNPM_VERSION} + +COPY ./package*.json . +RUN pnpm install -p +COPY . . + +# Production build +FROM node:18-slim +ARG DOCKER_VERSION=20.10.18 +ARG TARGETPLATFORM +ENV NODE_ENV production + +WORKDIR /app + +RUN apt update && apt -y install curl +RUN npm --no-update-notifier --no-fund --global install pnpm@${PNPM_VERSION} +RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/docker-$DOCKER_VERSION -o /usr/bin/docker +RUN chmod +x /usr/bin/docker +COPY --from=minio/mc:latest /usr/bin/mc /usr/bin/mc +COPY --from=build /app/ . + +ENV CHECKPOINT_DISABLE=1 +CMD node /app/src/index.mjs \ No newline at end of file diff --git a/apps/backup/backups/.gitkeep b/apps/backup/backups/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/apps/backup/package.json b/apps/backup/package.json new file mode 100644 index 000000000..cfabe22a5 --- /dev/null +++ b/apps/backup/package.json @@ -0,0 +1,24 @@ +{ + "name": "backup", + "version": "0.0.1", + "description": "", + "author": "Andras Bacsai", + "license": "Apache-2.0", + "main": "index.mjs", + "type": "module", + "scripts": { + "start": "NODE_ENV=production node src/index.mjs", + "dev": "pnpm cleanup && NODE_ENV=development node src/index.mjs", + "build": "docker build -t backup .", + "test": "pnpm build && docker run -ti --rm -v /var/run/docker.sock:/var/run/docker.sock -v /root/devel/coolify/apps/backup/backups:/app/backups -e CONTAINERS_TO_BACKUP='clatmhc6000008lvb5a5tnvsk:database:mysql:local' backup", + "cleanup": "rm -rf backups/*" + }, + "keywords": [], + "dependencies": { + "@aws-sdk/client-s3": "^3.222.0", + "@aws-sdk/lib-storage": "^3.222.0", + "cuid": "2.1.8", + "dotenv": "16.0.3", + "zx": "7.1.1" + } +} \ No newline at end of file diff --git a/apps/backup/src/index.mjs b/apps/backup/src/index.mjs new file mode 100644 index 000000000..4707d3a43 --- /dev/null +++ b/apps/backup/src/index.mjs @@ -0,0 +1,126 @@ +import * as dotenv from 'dotenv'; +dotenv.config() + +import 'zx/globals'; +import cuid from 'cuid'; +import { S3, PutObjectCommand } from "@aws-sdk/client-s3"; +import fs from 'fs'; + +const isDev = process.env.NODE_ENV === 'development' +$.verbose = !!isDev + +if (!process.env.CONTAINERS_TO_BACKUP && !isDev) { + console.log(chalk.red(`No containers to backup!`)) + process.exit(1) +} +const mysqlGzipLocal = 'clb6c9ue4000a8lputdd5g1cl:database:mysql:gzip:local'; +const mysqlRawLocal = 'clb6c9ue4000a8lputdd5g1cl:database:mysql:raw:local'; +const postgresqlGzipLocal = 'clb6c15yi00008lpuezop7cy0:database:postgresql:gzip:local'; +const postgresqlRawLocal = 'clb6c15yi00008lpuezop7cy0:database:postgresql:raw:local'; + +const minio = 'clb6c9ue4000a8lputdd5g1cl:database:mysql:gzip:minio|http|min.arm.coolify.io|backups||'; +const digitalOcean = 'clb6c9ue4000a8lputdd5g1cl:database:mysql:gzip:do|https|fra1.digitaloceanspaces.com|backups||'; + +const devContainers = [mysqlGzipLocal, mysqlRawLocal, postgresqlGzipLocal, postgresqlRawLocal] + +const containers = isDev + ? devContainers + : process.env.CONTAINERS_TO_BACKUP.split(',') + +const backup = async (container) => { + const id = cuid() + const [name, backupType, type, zipped, storage] = container.split(':') + const directory = `backups`; + const filename = zipped === 'raw' + ? `${name}-${type}-${backupType}-${new Date().getTime()}.sql` + : `${name}-${type}-${backupType}-${new Date().getTime()}.tgz` + const backup = `${directory}/${filename}`; + + try { + await $`docker inspect ${name.split(' ')[0]}`.quiet() + if (backupType === 'database') { + if (type === 'mysql') { + console.log(chalk.blue(`Backing up ${name}:${type}...`)) + const { stdout: rootPassword } = await $`docker exec ${name} printenv MYSQL_ROOT_PASSWORD`.quiet() + if (zipped === 'raw') { + await $`docker exec ${name} sh -c "exec mysqldump --all-databases -uroot -p${rootPassword.trim()}" > ${backup}` + } else if (zipped === 'gzip') { + await $`docker exec ${name} sh -c "exec mysqldump --all-databases -uroot -p${rootPassword.trim()}" | gzip > ${backup}` + } + } + if (type === 'postgresql') { + console.log(chalk.blue(`Backing up ${name}:${type}...`)) + const { stdout: userPassword } = await $`docker exec ${name} printenv POSTGRES_PASSWORD` + const { stdout: user } = await $`docker exec ${name} printenv POSTGRES_USER` + if (zipped === 'raw') { + await $`docker exec ${name} sh -c "exec pg_dumpall -c -U${user.trim()}" -W${userPassword.trim()}> ${backup}` + } else if (zipped === 'gzip') { + await $`docker exec ${name} sh -c "exec pg_dumpall -c -U${user.trim()}" -W${userPassword.trim()} | gzip > ${backup}` + } + } + const [storageType, ...storageArgs] = storage.split('|') + if (storageType !== 'local') { + let s3Protocol, s3Url, s3Bucket, s3Key, s3Secret = null + if (storageArgs.length > 0) { + [s3Protocol, s3Url, s3Bucket, s3Key, s3Secret] = storageArgs + } + if (storageType === 'minio') { + if (!s3Protocol || !s3Url || !s3Bucket || !s3Key || !s3Secret) { + console.log(chalk.red(`Invalid storage arguments for ${name}:${type}!`)) + return + } + await $`mc alias set ${id} ${s3Protocol}://${s3Url} ${s3Key} ${s3Secret}` + await $`mc stat ${id}` + await $`mc cp ${backup} ${id}/${s3Bucket}` + await $`rm ${backup}` + await $`mc alias rm ${id}` + } else if (storageType === 'do') { + if (!s3Protocol || !s3Url || !s3Bucket || !s3Key || !s3Secret) { + console.log(chalk.red(`Invalid storage arguments for ${name}:${type}!`)) + return + } + console.log({ s3Protocol, s3Url, s3Bucket, s3Key, s3Secret }) + console.log(chalk.blue(`Uploading ${name}:${type} to DigitalOcean Spaces...`)) + const readstream = fs.createReadStream(backup) + const bucketParams = { + Bucket: s3Bucket, + Key: filename, + Body: readstream + }; + const s3Client = new S3({ + forcePathStyle: false, + endpoint: `${s3Protocol}://${s3Url}`, + region: "us-east-1", + credentials: { + accessKeyId: s3Key, + secretAccessKey: s3Secret + }, + }); + try { + const data = await s3Client.send(new PutObjectCommand(bucketParams)); + console.log(chalk.green("Successfully uploaded backup: " + + bucketParams.Bucket + + "/" + + bucketParams.Key + ) + ); + return data; + } catch (err) { + console.log("Error", err); + } + } + } + } + + console.log(chalk.green(`Backup of ${name}:${type} complete!`)) + } catch (error) { + console.log(chalk.red(`Backup of ${name}:${type} failed!`)) + console.log(chalk.red(error)) + } +} +const promises = [] +for (const container of containers) { + // await backup(container); + promises.push(backup(container)) +} +await Promise.all(promises) \ No newline at end of file diff --git a/apps/ui/package.json b/apps/ui/package.json index 1cadeffd8..d40dba2a0 100644 --- a/apps/ui/package.json +++ b/apps/ui/package.json @@ -42,6 +42,8 @@ }, "type": "module", "dependencies": { + "@sentry/svelte": "7.21.1", + "@sentry/tracing": "7.21.1", "@sveltejs/adapter-static": "1.0.0-next.48", "@tailwindcss/typography": "0.5.8", "cuid": "2.1.8", diff --git a/apps/ui/src/hooks.ts b/apps/ui/src/hooks.ts index 30c352baa..df3284d5a 100644 --- a/apps/ui/src/hooks.ts +++ b/apps/ui/src/hooks.ts @@ -1,4 +1,13 @@ +import * as Sentry from '@sentry/svelte'; export async function handle({ event, resolve }) { const response = await resolve(event, { ssr: false }); return response; -} \ No newline at end of file +} +export const handleError = ({ error, event }) => { + Sentry.captureException(error, { event }); + + return { + message: 'Whoops!', + code: error?.code ?? 'UNKNOWN' + }; +}; \ No newline at end of file diff --git a/apps/ui/src/lib/common.ts b/apps/ui/src/lib/common.ts index c67a88929..9ed667f43 100644 --- a/apps/ui/src/lib/common.ts +++ b/apps/ui/src/lib/common.ts @@ -3,6 +3,8 @@ import { addToast } from '$lib/store'; export const asyncSleep = (delay: number) => new Promise((resolve) => setTimeout(resolve, delay)); +export let initials = (str:string) => (str||'').split(' ').map( (wrd) => wrd[0]).join('') + export function errorNotification(error: any | { message: string }): void { if (error.message) { if (error.message === 'Cannot read properties of undefined (reading \'postMessage\')') { @@ -87,4 +89,4 @@ export function handlerNotFoundLoad(error: any, url: URL) { export function getRndInteger(min: number, max: number) { return Math.floor(Math.random() * (max - min + 1)) + min; -} \ No newline at end of file +} diff --git a/apps/ui/src/lib/components/ContextMenu.svelte b/apps/ui/src/lib/components/ContextMenu.svelte new file mode 100644 index 000000000..fe4a2ce51 --- /dev/null +++ b/apps/ui/src/lib/components/ContextMenu.svelte @@ -0,0 +1,4 @@ + diff --git a/apps/ui/src/lib/components/CopyPasswordField.svelte b/apps/ui/src/lib/components/CopyPasswordField.svelte index f45adcd3d..b25817ac1 100644 --- a/apps/ui/src/lib/components/CopyPasswordField.svelte +++ b/apps/ui/src/lib/components/CopyPasswordField.svelte @@ -15,7 +15,7 @@ export let placeholder = ''; export let inputStyle = ''; - let disabledClass = 'bg-coolback disabled:bg-coolblack w-full'; + let disabledClass = 'input input-primary bg-coolback disabled:bg-coolblack w-full'; let isHttps = browser && window.location.protocol === 'https:'; function copyToClipboard() { diff --git a/apps/ui/src/lib/components/LocalePicker.svelte b/apps/ui/src/lib/components/LocalePicker.svelte new file mode 100644 index 000000000..6efdd176c --- /dev/null +++ b/apps/ui/src/lib/components/LocalePicker.svelte @@ -0,0 +1,11 @@ + + +
+ +
diff --git a/apps/ui/src/lib/components/badges/DestinationBadge.svelte b/apps/ui/src/lib/components/badges/DestinationBadge.svelte new file mode 100644 index 000000000..b566ef7bd --- /dev/null +++ b/apps/ui/src/lib/components/badges/DestinationBadge.svelte @@ -0,0 +1,14 @@ + + +{#if (name || '').length > 0} + + {initials(name)} + + {name} +{/if} diff --git a/apps/ui/src/lib/components/badges/PublicBadge.svelte b/apps/ui/src/lib/components/badges/PublicBadge.svelte new file mode 100644 index 000000000..6784c97e9 --- /dev/null +++ b/apps/ui/src/lib/components/badges/PublicBadge.svelte @@ -0,0 +1,19 @@ +
+ + + + + + + + +
diff --git a/apps/ui/src/lib/components/badges/StatusBadge.svelte b/apps/ui/src/lib/components/badges/StatusBadge.svelte new file mode 100644 index 000000000..01badcee2 --- /dev/null +++ b/apps/ui/src/lib/components/badges/StatusBadge.svelte @@ -0,0 +1,26 @@ + + +{#await getting} + ... +{:then status} + + {status} + +{/await} diff --git a/apps/ui/src/lib/components/badges/TeamsBadge.svelte b/apps/ui/src/lib/components/badges/TeamsBadge.svelte new file mode 100644 index 000000000..eab9d80d8 --- /dev/null +++ b/apps/ui/src/lib/components/badges/TeamsBadge.svelte @@ -0,0 +1,16 @@ + + + + {#each teams as team} + + Team: {initials(team.name)} + + {team.name} + {/each} + diff --git a/apps/ui/src/lib/components/grids/Grid3.svelte b/apps/ui/src/lib/components/grids/Grid3.svelte new file mode 100644 index 000000000..905655e04 --- /dev/null +++ b/apps/ui/src/lib/components/grids/Grid3.svelte @@ -0,0 +1,5 @@ +
+ +
diff --git a/apps/ui/src/lib/components/svg/applications/ApplicationIcons.svelte b/apps/ui/src/lib/components/svg/applications/ApplicationIcons.svelte index fc5dbe16c..387653c87 100644 --- a/apps/ui/src/lib/components/svg/applications/ApplicationIcons.svelte +++ b/apps/ui/src/lib/components/svg/applications/ApplicationIcons.svelte @@ -42,4 +42,6 @@ {:else if application.buildPack?.toLowerCase() === 'compose'} +{:else if application.simpleDockerfile} + {/if} diff --git a/apps/ui/src/lib/components/svg/servers/LocalDockerIcon.svelte b/apps/ui/src/lib/components/svg/servers/LocalDockerIcon.svelte new file mode 100644 index 000000000..f3ab3be56 --- /dev/null +++ b/apps/ui/src/lib/components/svg/servers/LocalDockerIcon.svelte @@ -0,0 +1,26 @@ + + + + + + + + + + + + + diff --git a/apps/ui/src/lib/components/svg/servers/RemoteDockerIcon.svelte b/apps/ui/src/lib/components/svg/servers/RemoteDockerIcon.svelte new file mode 100644 index 000000000..1d00a6900 --- /dev/null +++ b/apps/ui/src/lib/components/svg/servers/RemoteDockerIcon.svelte @@ -0,0 +1,16 @@ + + + + + + + diff --git a/apps/ui/src/lib/components/svg/services/ServiceIcons.svelte b/apps/ui/src/lib/components/svg/services/ServiceIcons.svelte index b03039f1d..7f146e3fb 100644 --- a/apps/ui/src/lib/components/svg/services/ServiceIcons.svelte +++ b/apps/ui/src/lib/components/svg/services/ServiceIcons.svelte @@ -5,6 +5,7 @@ const handleError = (ev: { target: { src: string } }) => (ev.target.src = fallback); let extension = 'png'; let svgs = [ + 'pocketbase', 'gitea', 'languagetool', 'meilisearch', diff --git a/apps/ui/src/lib/components/svg/sources/GithubIcon.svelte b/apps/ui/src/lib/components/svg/sources/GithubIcon.svelte new file mode 100644 index 000000000..b56210177 --- /dev/null +++ b/apps/ui/src/lib/components/svg/sources/GithubIcon.svelte @@ -0,0 +1,11 @@ + + + diff --git a/apps/ui/src/lib/components/svg/sources/GitlabIcon.svelte b/apps/ui/src/lib/components/svg/sources/GitlabIcon.svelte new file mode 100644 index 000000000..3e8c8a84c --- /dev/null +++ b/apps/ui/src/lib/components/svg/sources/GitlabIcon.svelte @@ -0,0 +1,21 @@ + + + diff --git a/apps/ui/src/lib/container/status.ts b/apps/ui/src/lib/container/status.ts new file mode 100644 index 000000000..a700e652b --- /dev/null +++ b/apps/ui/src/lib/container/status.ts @@ -0,0 +1,73 @@ +// +// Maps Container ID x Operation Status +// +// Example response of $status => {'123asdf': 'degraded', '124asdf': 'running'} + +import { writable, get as getStore } from 'svelte/store'; +import { get } from '$lib/api'; + +export let containerStatus = writable({}); + +let PERMITED_STATUS = ['loading', 'running', 'healthy', 'building', 'degraded', 'stopped', 'error']; + +// refreshStatus([{id}]) +export async function refreshStatus(list: Array) { + for (const item of list) { + setStatus(item.id, 'loading'); + getStatus(item, true); + } +} + +export async function getStatus(resource: any, force: boolean = false) { + const { id, buildPack, dualCerts, engine, simpleDockerfile } = resource; + let newStatus = 'stopped'; + + // Already set and we're not forcing + if (getStore(containerStatus)[id] && !force) return getStore(containerStatus)[id]; + + try { + if (buildPack || simpleDockerfile) { // Application + const response = await get(`/applications/${id}/status`); + newStatus = parseApplicationsResponse(response); + } else if (typeof dualCerts !== 'undefined') { // Service + const response = await get(`/services/${id}/status`); + newStatus = parseServiceResponse(response); + } else if (typeof engine !== 'undefined') { // Destination/Server + const response = await get(`/destinations/${id}/status`); + newStatus = response.isRunning ? 'running' : 'stopped'; + } else { // Database + const response = await get(`/databases/${id}/status`); + newStatus = response.isRunning ? 'running' : 'stopped'; + } + } catch (error) { + newStatus = 'error'; + } + + setStatus(id, newStatus); + // console.log("GOT:", id, newStatus) + return newStatus +} + +const setStatus = (thingId, newStatus) => { + if (!PERMITED_STATUS.includes(newStatus)) + throw (`Change to ${newStatus} is not permitted. Try: ${PERMITED_STATUS.join(', ')}`); + containerStatus.update(n => Object.assign(n, { thingId: newStatus })); +}; + +// -- Response Parsing + +function parseApplicationsResponse(list: Array) { + if (list.length === 0) return 'stopped'; + if (list.length === 1) return list[0].status.isRunning ? 'running' : 'stopped'; + return allWorking(list.map((el: any) => el.status.isRunning)) +} + +function parseServiceResponse(response: any) { + if (Object.keys(response).length === 0) return 'stopped'; + let list = Object.keys(response).map((el) => el.status.isRunning) + return allWorking(list) ? 'running' : 'degraded' +} + +function allWorking(list: Array) { + return list.reduce((acum: boolean, res: boolean) => acum && res) ? 'running' : 'degraded'; +} diff --git a/apps/ui/src/lib/lang.json b/apps/ui/src/lib/lang.json index fdfc5bf80..f5485ece0 100644 --- a/apps/ui/src/lib/lang.json +++ b/apps/ui/src/lib/lang.json @@ -1,4 +1,7 @@ { "fr": "Français", + "pt": "Português", + "es": "Espanhol", + "ko": "Korean", "en": "English" } diff --git a/apps/ui/src/lib/locales/es.json b/apps/ui/src/lib/locales/es.json new file mode 100644 index 000000000..54eb5f716 --- /dev/null +++ b/apps/ui/src/lib/locales/es.json @@ -0,0 +1,341 @@ +{ + "layout":{ + "update_done":"Actualización completada.", + "wait_new_version_startup":"Esperando que comience la nueva versión.", + "new_version":"Nueva versión accesible. Recargando.", + "switch_to_a_different_team":"Cambia a otro equipo.", + "update_available":"Actualización disponible" + }, + "error":{ + "you_can_find_your_way_back":"Puedes encontrar tu camino de vuelta", + "here":"Aquí.", + "you_are_lost":"¡Estás perdido! ¡Pero no tengas miedo!" + }, + "index":{ + "dashboard":"Dashboard", + "applications":"Aplicaciones", + "destinations":"Destinos", + "git_sources":"Fuentes Git", + "databases":"Bases de datos", + "services":"Servicios", + "teams":"Equipos", + "not_implemented_yet":"Aún no se ha aplicado", + "database":"Base de datos", + "settings":"Ajustes", + "global_settings":"Ajustes mundiales", + "secret":"Secret", + "team":"Equipo", + "logout":"Cerrar sesión" + }, + "login":{ + "already_logged_in":"Ya se ha registrado.", + "authenticating":"Autenticando.", + "login":"Iniciar sesión" + }, + "forms":{ + "password":"Contraseña", + "email":"Dirección de correo electrónico", + "passwords_not_match":"Las contraseñas no coinciden.", + "password_again":"Contraseña de nuevo", + "save":"Guardar", + "saving":"Salvando.", + "name":"Nombre", + "value":"Valor", + "action":"Acciones", + "is_required":"es necesario.", + "add":"Añadir", + "set":"Set", + "remove":"Retirar", + "path":"Camino", + "confirm_continue":"¿Estás seguro de continuar?", + "must_be_stopped_to_modify":"Debe ser detenido para modificar.", + "port":"Puerto", + "default":"Por defecto", + "base_directory":"Base Directory", + "publish_directory":"Publish Directory", + "generated_automatically_after_start":"Generado automáticamente después del inicio", + "roots_password":"La contraseña de Root", + "root_user":"Usuario raíz", + "eg":"eg", + "user":"Usuario", + "loading":"Carga.", + "version":"Versión", + "host":"Host", + "already_used_for":"##########################################################################################################################################################################################################################################################", + "configuration":"Configuración", + "engine":"Motor", + "network":"Red", + "ip_address":"Dirección IP", + "ssh_private_key":"SSH Clave privada", + "type":"Tipo", + "html_url":"URL", + "api_url":"API", + "organization":"Organización", + "new_password":"Nueva contraseña", + "super_secure_new_password":"Super seguro nueva contraseña", + "submit":"Submit", + "default_email_address":"Dirección de correo electrónico predeterminada", + "default_password":"Contraseña predeterminada", + "username":"Nombre de usuario", + "root_db_user":"Root DB Usuario", + "root_db_password":"Root DB Contraseña", + "api_port":"API Port", + "verifying":"Verificación", + "verify_emails_without_smtp":"Verificar correos electrónicos sin SMTP", + "extra_config":"Extra Config", + "select_a_service":"Seleccione un Servicio", + "select_a_service_version":"Seleccione una versión de servicio", + "removing":"Retirándose.", + "remove_domain":"Eliminar el dominio", + "public_port_range":"Public Port Range", + "public_port_range_explainer":"Puertos utilizados para exponer bases de datos/servicios/servicios internos. Añádalos a su cortafuegos (si es aplicable).Seguido se indicará una gama de puertos, por ejemplo: tachuelas clase='text-settings '9000-9100 seg/span", + "no_actions_available":"No se dispone de medidas", + "admin_api_key":"Clave de API de Admin" + }, + "register":{ + "register":"Registro", + "registering":"Registro.", + "first_user":"Está registrando al primer usuario. Será el administrador de tu instancia de Coolify." + }, + "reset":{ + "reset_password":"Reset", + "invalid_secret_key":"Una llave secreta inválida.", + "secret_key":"Secret Key", + "find_path_secret_key":"Puedes encontrarlo en ~coolify/.env (COOLIFY_SECRET_KEY)" + }, + "application":{ + "configuration":{ + "buildpack":{ + "choose_this_one":"Elige esta." + }, + "branch_already_in_use":"Esta rama ya es utilizada por otra aplicación. Webhooks no funcionará en este caso para ambas aplicaciones. ¿Seguro que quieres usarlo?", + "no_repositories_configured":"No hay repositorios configurados para su aplicación Git.", + "configure_it_now":"Configure ahora", + "loading_repositories":"Carga de repositorios ...", + "select_a_repository":"Seleccione un repositorio", + "loading_branches":"Cargando ramas ...", + "select_a_repository_first":"Por favor seleccione un repositorio primero", + "select_a_branch":"Por favor seleccione una rama", + "loading_groups":"Grupos de carga.", + "select_a_group":"Seleccione un grupo", + "loading_projects":"Cargando proyectos.", + "select_a_project":"Seleccione un proyecto", + "no_projects_found":"No se han encontrado proyectos", + "no_branches_found":"No hay ramas encontradas", + "configure_build_pack":"Configure Build Pack", + "scanning_repository_suggest_build_pack":"Repositorio de exploración para sugerir un paquete de construcción para usted.", + "found_lock_file":"encontrado archivo de bloqueo para {{packageManager}}.Seguido de comandos predefinidos.", + "configure_destination":"Configurar Destino", + "no_configurable_destination":"No hay destino configurable encontrado", + "select_a_repository_project":"Seleccione un Repositorio / Proyecto", + "select_a_git_source":"Seleccione una fuente de Git", + "no_configurable_git":"No se encontró una fuente de Git configurable", + "configuration_missing":"Falta de configuración" + }, + "build":{ + "queued_waiting_exec":"Queued and waiting for execution.", + "build_logs_of":"Construir registros de", + "running":"Corriendo", + "queued":"Queued", + "finished_in":"Terminado en", + "load_more":"Carga más", + "no_logs":"No hay registros encontrados", + "waiting_logs":"Esperando los registros." + }, + "preview":{ + "need_during_buildtime":"¿Necesitas durante el tiempo de construcción?", + "setup_secret_app_first":"Puede añadir secretos a las implementaciones PR/MR. Por favor, agregue secretos a la aplicación primero. √≠br]Useful for creating יspan class='text-settings 'staging won/span environments.", + "values_overwriting_app_secrets":"Estos valores sobrescriben los secretos de aplicación en las implementaciones PR/MR. Útil para la creación de clase 0'text-settings 'estaging significan ambientes / paño.", + "redeploy":"Redistribución", + "no_previews_available":"No hay vistas previas disponibles" + }, + "secrets":{ + "secret_saved":"Secreto salvado.", + "use_isbuildsecret":"Use isBuildSecret", + "secrets_for":"Secretos para" + }, + "storage":{ + "path_is_required":"Se requiere camino.", + "storage_saved":"Almacenamiento guardado.", + "storage_updated":"Almacenamiento actualizado.", + "storage_deleted":"Almacenamiento eliminado.", + "persistent_storage_explainer":"Puede especificar cualquier carpeta que desee ser persistente a través de las implementaciones.Seguido de la clase='text-settings 'ejemplo observado/span significa que preservará {{type}}이(가) 이미 사용됨", + "configuration":"구성", + "engine":"엔진", + "network":"회로망", + "ip_address":"IP 주소", + "ssh_private_key":"SSH 개인 키", + "type":"유형", + "html_url":"HTML URL", + "api_url":"API URL", + "organization":"조직", + "new_password":"새 비밀번호", + "super_secure_new_password":"매우 안전한 새 비밀번호", + "submit":"제출하다", + "default_email_address":"기본 이메일 주소", + "default_password":"기본 비밀번호", + "username":"사용자 이름", + "root_db_user":"루트 DB 사용자", + "root_db_password":"루트 DB 비밀번호", + "api_port":"API 포트", + "verifying":"확인 중", + "verify_emails_without_smtp":"SMTP 없이 이메일 확인", + "extra_config":"추가 구성", + "select_a_service":"서비스 선택", + "select_a_service_version":"서비스 버전 선택", + "removing":"풀이...", + "remove_domain":"도메인 제거", + "public_port_range":"공용 포트 범위", + "public_port_range_explainer":"데이터베이스/서비스/내부 서비스를 노출하는 데 사용되는 포트입니다.
방화벽에 추가합니다(해당되는 경우).

포트 범위를 지정할 수 있습니다(예: ). 9000-9100", + "no_actions_available":"사용 가능한 작업이 없습니다.", + "admin_api_key":"관리 API 키" + }, + "register":{ + "register":"등록하다", + "registering":"등록 중...", + "first_user":"첫 번째 사용자를 등록하고 있습니다. Coolify 인스턴스의 관리자가 됩니다." + }, + "reset":{ + "reset_password":"초기화", + "invalid_secret_key":"잘못된 비밀 키입니다.", + "secret_key":"비밀 키", + "find_path_secret_key":"~/coolify/.env(COOLIFY_SECRET_KEY)에서 찾을 수 있습니다." + }, + "application":{ + "configuration":{ + "buildpack":{ + "choose_this_one":"이걸 선택..." + }, + "branch_already_in_use":"이 분기는 이미 다른 응용 프로그램에서 사용하고 있습니다. 이 경우 두 애플리케이션 모두에 대해 Webhook이 작동하지 않습니다. 사용하시겠습니까?", + "no_repositories_configured":"Git 애플리케이션에 대해 구성된 저장소가 없습니다.", + "configure_it_now":"지금 구성", + "loading_repositories":"저장소 로드 중...", + "select_a_repository":"저장소를 선택하십시오", + "loading_branches":"브랜치 로드 중...", + "select_a_repository_first":"먼저 저장소를 선택하십시오", + "select_a_branch":"지점을 선택해 주세요", + "loading_groups":"그룹 로드 중...", + "select_a_group":"그룹을 선택하세요.", + "loading_projects":"프로젝트 로드 중...", + "select_a_project":"프로젝트를 선택하세요.", + "no_projects_found":"프로젝트를 찾을 수 없습니다.", + "no_branches_found":"지점을 찾을 수 없습니다", + "configure_build_pack":"빌드 팩 구성", + "scanning_repository_suggest_build_pack":"빌드 팩을 제안하기 위해 저장소를 검색하는 중...", + "found_lock_file":"{{packageManager}}에 대한 잠금 파일을 찾았습니다.
사전 정의된 명령 명령에 사용합니다.", + "configure_destination":"대상 구성", + "no_configurable_destination":"구성 가능한 대상을 찾을 수 없습니다.", + "select_a_repository_project":"리포지토리/프로젝트 선택", + "select_a_git_source":"Git 소스 선택", + "no_configurable_git":"구성 가능한 Git 소스를 찾을 수 없습니다.", + "configuration_missing":"구성 누락" + }, + "build":{ + "queued_waiting_exec":"큐에 넣고 실행을 기다리고 있습니다.", + "build_logs_of":"빌드 로그", + "running":"달리기", + "queued":"대기 중", + "finished_in":"완료", + "load_more":"더 찾아보기", + "no_logs":"로그를 찾을 수 없습니다.", + "waiting_logs":"로그를 기다리는 중..." + }, + "preview":{ + "need_during_buildtime":"빌드 시간에 필요하십니까?", + "setup_secret_app_first":"PR/MR 배포에 비밀을 추가할 수 있습니다. 먼저 응용 프로그램에 비밀을 추가하십시오.
스테이징 환경을 만드는 데 유용합니다.", + "values_overwriting_app_secrets":"이러한 값은 PR/MR 배포에서 애플리케이션 비밀을 덮어씁니다. 스테이징 환경을 만드는 데 유용합니다.", + "redeploy":"재배포", + "no_previews_available":"사용 가능한 미리보기가 없습니다." + }, + "secrets":{ + "secret_saved":"비밀이 저장되었습니다.", + "use_isbuildsecret":"isBuildSecret 사용", + "secrets_for":"비밀" + }, + "storage":{ + "path_is_required":"경로는 필수 항목입니다.", + "storage_saved":"저장용량이 저장되었습니다.", + "storage_updated":"스토리지가 업데이트되었습니다.", + "storage_deleted":"스토리지가 삭제되었습니다.", + "persistent_storage_explainer":"배포 간에 유지하려는 모든 폴더를 지정할 수 있습니다.
/example/app/를 보존함을 의미합니다. /app과 같은 컨테이너의 example은 애플리케이션의 루트 디렉토리입니다.

데이터베이스(SQLite) 또는 캐시와 같은 데이터를 저장하는 데 유용합니다." + }, + "deployment_queued":"배포가 대기 중입니다.", + "confirm_to_delete":"'{{name}}'을(를) 삭제하시겠습니까?", + "stop_application":"애플리케이션 중지", + "permission_denied_stop_application":"애플리케이션을 중지할 권한이 없습니다.", + "rebuild_application":"애플리케이션 재구축", + "permission_denied_rebuild_application":"애플리케이션을 다시 빌드할 권한이 없습니다.", + "build_and_start_application":"배포", + "permission_denied_build_and_start_application":"애플리케이션을 배포할 권한이 없습니다.", + "configurations":"구성", + "secret":"비밀", + "persistent_storage":"영구 스토리지", + "previews":"미리보기", + "logs":"애플리케이션 로그", + "build_logs":"빌드 로그", + "delete_application":"삭제", + "permission_denied_delete_application":"이 애플리케이션을 삭제할 권한이 없습니다.", + "domain_already_in_use":"도메인 {{domain}}은(는) 이미 사용 중입니다.", + "dns_not_set_error":"DNS가 올바르게 설정되지 않았거나 {{domain}}에 대해 전파되었습니다.

DNS 설정을 확인하십시오.", + "domain_required":"도메인은 필수 항목입니다.", + "settings_saved":"구성이 저장되었습니다.", + "dns_not_set_partial_error":"DNS가 설정되지 않았습니다.", + "domain_not_valid":"도메인을 확인할 수 없거나 서버 IP 주소를 가리키지 않습니다.

DNS 구성을 확인하고 다시 시도하십시오.", + "git_source":"힘내 소스", + "git_repository":"Git 저장소", + "build_pack":"빌드 팩", + "base_image":"배포 이미지", + "base_image_explainer":"배포에 사용할 이미지입니다.", + "base_build_image":"빌드 이미지", + "base_build_image_explainer":"빌드 프로세스 중에 사용될 이미지입니다.", + "destination":"목적지", + "application":"신청", + "url_fqdn":"URL(FQDN)", + "domain_fqdn":"도메인(FQDN)", + "https_explainer":"https를 지정하면 https를 통해서만 애플리케이션에 액세스할 수 있습니다. SSL 인증서가 생성됩니다.
www를 지정하면 애플리케이션이 www가 아닌 ​​곳에서 리디렉션(302)되거나 그 반대의 경우도 마찬가지입니다.
< br>도메인을 수정하려면 먼저 애플리케이션을 중지해야 합니다.

미리 DNS가 서버 IP를 가리키도록 설정해야 합니다.", + "ssl_www_and_non_www":"www 및 www가 없는 SSL을 생성하시겠습니까?", + "ssl_explainer":"www 및 non-www 모두에 대한 인증서를 생성합니다.
미리 두 DNS 항목을 설정해야 합니다.

두 DNS 항목 모두에 방문자가 있을 것으로 예상되는 경우 유용합니다.", + "install_command":"설치 명령", + "build_command":"빌드 명령", + "start_command":"시작 명령", + "directory_to_use_explainer":"모든 명령의 기반으로 사용할 디렉토리입니다.
monorepos와 함께 유용할 수 있습니다.", + "publish_directory_explainer":"배포를 위한 모든 자산이 포함된 디렉터리입니다.
예: dist,_site 또는 public< /스팬>.", + "features":"특징", + "enable_automatic_deployment":"자동 배포 활성화", + "enable_auto_deploy_webhooks":"웹훅을 통한 자동 배포를 활성화합니다.", + "enable_mr_pr_previews":"MR/PR 미리보기 활성화", + "expose_a_port":"포트 노출", + "enable_preview_deploy_mr_pr_requests":"끌어오기 또는 병합 요청에서 미리보기 배포를 활성화합니다.", + "debug_logs":"디버그 로그", + "enable_debug_log_during_build":"빌드 단계에서 디버그 로그를 활성화합니다.
민감한 정보가 표시되고 로그에 저장될 수 있습니다.", + "cant_activate_auto_deploy_without_repo":"이 리포지토리/분기에 대해 하나의 애플리케이션만 정의될 때까지 자동 배포를 활성화할 수 없습니다.", + "no_applications_found":"애플리케이션을 찾을 수 없습니다.", + "secret__batch_dot_env":".env 파일 붙여넣기", + "batch_secrets":"일괄 추가 비밀" + }, + "general":"일반적인", + "database":{ + "default_database":"기본 데이터베이스", + "generated_automatically_after_set_to_public":"public으로 설정 후 자동 생성", + "connection_string":"연결 문자열", + "set_public":"공개 설정", + "warning_database_public":"인터넷을 통해 데이터베이스에 연결할 수 있습니다.
이 경우 보안을 심각하게 생각하십시오!", + "change_append_only_mode":"추가 전용 모드 변경", + "warning_append_only":"백업에서 redis 데이터를 복원하려는 경우에 유용합니다.
데이터베이스를 다시 시작해야 합니다.", + "select_database_type":"데이터베이스 유형 선택", + "select_database_version":"데이터베이스 버전 선택", + "confirm_stop":"{{name}}을(를) 중지하시겠습니까?", + "stop_database":"중지", + "permission_denied_stop_database":"데이터베이스를 중지할 권한이 없습니다.", + "start_database":"시작", + "permission_denied_start_database":"데이터베이스를 시작할 권한이 없습니다.", + "delete_database":"삭제", + "permission_denied_delete_database":"데이터베이스를 삭제할 권한이 없습니다.", + "no_databases_found":"데이터베이스를 찾을 수 없습니다.", + "logs":"로그" + }, + "destination":{ + "delete_destination":"삭제", + "permission_denied_delete_destination":"이 목적지를 삭제할 권한이 없습니다.", + "add_to_coolify":"Coolify에 추가", + "coolify_proxy_stopped":"Coolify 프록시가 중지되었습니다!", + "coolify_proxy_started":"Coolify 프록시가 시작되었습니다!", + "confirm_restart_proxy":"프록시를 다시 시작하시겠습니까? 모든 것이 ~10초 안에 재구성됩니다.", + "coolify_proxy_restarting":"Coolify 프록시 다시 시작 중...", + "restarting_please_wait":"다시 시작 중입니다... 잠시만 기다려 주십시오...", + "force_restart_proxy":"강제 재시작 프록시", + "use_coolify_proxy":"Coolify 프록시를 사용하시겠습니까?", + "no_destination_found":"목적지를 찾을 수 없습니다", + "new_error_network_already_exists":"다른 팀에 대해 네트워크 {{network}}이(가) 이미 구성되었습니다!", + "new":{ + "saving_and_configuring_proxy":"절약...", + "install_proxy":"그러면 수동 구성 없이 애플리케이션과 서비스에 액세스할 수 있도록 대상에 프록시가 설치됩니다(Docker에 권장됨).

데이터베이스에는 자체 프록시가 있습니다.", + "add_new_destination":"새 목적지 추가", + "predefined_destinations":"사전 정의된 목적지" + } + }, + "sources":{ + "local_docker":"로컬 도커", + "remote_docker":"원격 도커", + "organization_explainer":"조직을 Git 소스로 사용하려면 입력하십시오. 그렇지 않으면 사용자가 사용됩니다." + }, + "source":{ + "new":{ + "git_source":"새 Git 소스 추가", + "official_providers":"공식 제공업체" + }, + "no_git_sources_found":"git 소스를 찾을 수 없습니다.", + "delete_git_source":"삭제", + "permission_denied":"Git 소스를 삭제할 권한이 없습니다.", + "create_new_app":"새 {{name}} 앱 만들기", + "change_app_settings":"{{name}} 앱 설정 변경", + "install_repositories":"저장소 설치", + "application_id":"애플리케이션 ID", + "group_name":"그룹 이름", + "oauth_id":"인증 ID", + "oauth_id_explainer":"OAuth ID는 GitLab 애플리케이션의 고유 식별자입니다.
GitLab OAuth 애플리케이션의 URL에서 찾을 수 있습니다.", + "register_oauth_gitlab":"GitLab에 새 OAuth 애플리케이션 등록", + "gitlab":{ + "self_hosted":"인스턴스 전체 애플리케이션(자체 호스팅)", + "user_owned":"사용자 소유 애플리케이션", + "group_owned":"그룹 소유 애플리케이션", + "gitlab_application_type":"GitLab 애플리케이션 유형", + "already_configured":"GitLab 앱이 이미 구성되어 있습니다." + }, + "github":{ + "redirecting":"Github으로 리디렉션 중..." + } + }, + "services":{ + "all_email_verified":"모든 이메일이 확인되었습니다. 지금 로그인할 수 있습니다.", + "generate_www_non_www_ssl":"www 및 non-www 모두에 대한 인증서를 생성합니다.
미리 두 DNS 항목을 설정해야 합니다.

서비스를 다시 시작해야 합니다." + }, + "service":{ + "stop_service":"중지", + "permission_denied_stop_service":"서비스를 중지할 권한이 없습니다.", + "start_service":"시작", + "permission_denied_start_service":"서비스를 시작할 권한이 없습니다.", + "delete_service":"삭제", + "permission_denied_delete_service":"서비스를 삭제할 권한이 없습니다.", + "no_service":"서비스를 찾을 수 없습니다.", + "logs":"로그" + }, + "setting":{ + "change_language":"언어 변경", + "permission_denied":"이 작업을 수행할 권한이 없습니다. \\n관리자에게 권한 수정을 요청하세요.", + "domain_removed":"도메인이 삭제됨", + "ssl_explainer":"https를 지정하면 Coolify는 https를 통해서만 액세스할 수 있습니다. SSL 인증서가 자동으로 생성됩니다.
www를 지정하면 Coolify가 www가 아닌 ​​곳에서 리디렉션(302)되거나 그 반대의 경우도 마찬가지입니다.

경고: 이미 설정된 도메인을 변경하면 웹훅 및 기타 통합이 중단됩니다! 수동으로 업데이트해야 합니다.", + "must_remove_domain_before_changing":"이 설정을 변경하려면 먼저 도메인을 제거해야 합니다.", + "registration_allowed":"등록이 허용됩니까?", + "registration_allowed_explainer":"애플리케이션에 대한 추가 등록을 허용합니다.
최초 등록 후에는 꺼져 있습니다.", + "coolify_proxy_settings":"Coolify 프록시 설정", + "credential_stat_explainer":"통계 페이지에 대한 자격 증명입니다.", + "auto_update_enabled":"자동 업데이트가 활성화되었습니까?", + "auto_update_enabled_explainer":"Coolify에 대한 자동 업데이트를 활성화합니다. 실행 중인 빌드 프로세스가 없는 경우 배후에서 자동으로 수행됩니다.", + "generate_www_non_www_ssl":"www 및 non-www 모두에 대한 인증서를 생성합니다.
미리 두 DNS 항목을 설정해야 합니다.", + "is_dns_check_enabled":"DNS 확인이 활성화되었습니까?", + "is_dns_check_enabled_explainer":"SSL 인증서를 생성하기 전에 DNS 확인을 비활성화할 수 있습니다.

Coolify가 역방향 프록시 또는 터널 뒤에 있을 때 비활성화하는 것이 유용합니다." + }, + "team":{ + "pending_invitations":"대기 중인 초대", + "accept":"수용하다", + "delete":"삭제", + "member":"회원", + "root":"(뿌리)", + "invited_with_permissions":"{{permission}} 권한으로 {{teamName}}에 초대되었습니다.", + "members":"회원", + "root_team_explainer":"루트 팀입니다. 즉, 이 그룹의 구성원은 인스턴스 전체 설정을 관리하고 Coolify의 모든 권한을 가질 수 있습니다(Linux의 루트 사용자와 같은 경우).", + "permission":"허가", + "you":"너", + "promote_to":"{{grade}}(으)로 승격", + "revoke_invitation":"초대 취소", + "pending_invitation":"대기 중인 초대", + "invite_new_member":"새 회원 초대", + "send_invitation":"초대장을 보내다", + "invite_only_register_explainer":"등록된 사용자만 초대할 수 있습니다.", + "admin":"관리자", + "read":"읽다" + } +} \ No newline at end of file diff --git a/apps/ui/src/lib/locales/pt.json b/apps/ui/src/lib/locales/pt.json new file mode 100644 index 000000000..0ac5deb46 --- /dev/null +++ b/apps/ui/src/lib/locales/pt.json @@ -0,0 +1,341 @@ +{ + "layout":{ + "update_done":"Atualização completa.", + "wait_new_version_startup":"Aguardando a nova versão iniciar...", + "new_version":"Nova versão acessível. Recarregando...", + "switch_to_a_different_team":"Mudar para uma equipa diferente...", + "update_available":"Atualização disponível" + }, + "error":{ + "you_can_find_your_way_back":"Você pode encontrar o seu caminho de volta", + "here":"aqui", + "you_are_lost":"Ooops você está perdido! Mas não tenha medo!" + }, + "index":{ + "dashboard":"Painel", + "applications":"Formulários", + "destinations":"Destinos", + "git_sources":"Fontes Git", + "databases":"Bancos de dados", + "services":"Serviços", + "teams":"Equipes", + "not_implemented_yet":"Ainda não implementado", + "database":"Base de dados", + "settings":"Definições", + "global_settings":"Configurações globais", + "secret":"Segredo", + "team":"Equipe", + "logout":"Sair" + }, + "login":{ + "already_logged_in":"Já logado...", + "authenticating":"Autenticando...", + "login":"Conecte-se" + }, + "forms":{ + "password":"Senha", + "email":"Endereço de email", + "passwords_not_match":"As senhas não coincidem.", + "password_again":"Senha novamente", + "save":"Salvar", + "saving":"Salvando...", + "name":"Nome", + "value":"Valor", + "action":"Ações", + "is_required":"É necessário.", + "add":"Adicionar", + "set":"Definir", + "remove":"Remover", + "path":"Caminho", + "confirm_continue":"Tem certeza de continuar?", + "must_be_stopped_to_modify":"Deve ser parado para modificar.", + "port":"Porta", + "default":"predefinição", + "base_directory":"Diretório base", + "publish_directory":"Publicar diretório", + "generated_automatically_after_start":"Gerado automaticamente após o início", + "roots_password":"Senha do Root", + "root_user":"Usuário raiz", + "eg":"por exemplo", + "user":"Do utilizador", + "loading":"Carregando...", + "version":"Versão", + "host":"Hospedeiro", + "already_used_for":"{{type}} já usado para", + "configuration":"Configuração", + "engine":"Motor", + "network":"Rede", + "ip_address":"Endereço de IP", + "ssh_private_key":"Chave privada SSH", + "type":"Modelo", + "html_url":"URL HTML", + "api_url":"URL da API", + "organization":"Organização", + "new_password":"Nova Senha", + "super_secure_new_password":"Nova senha super segura", + "submit":"Enviar", + "default_email_address":"Endereço de e-mail padrão", + "default_password":"Senha padrão", + "username":"Nome de usuário", + "root_db_user":"Usuário raiz do banco de dados", + "root_db_password":"Senha do banco de dados raiz", + "api_port":"Porta API", + "verifying":"Verificando", + "verify_emails_without_smtp":"Verifique e-mails sem SMTP", + "extra_config":"Configuração extra", + "select_a_service":"Selecione um serviço", + "select_a_service_version":"Selecione uma versão do serviço", + "removing":"Removendo...", + "remove_domain":"Remover domínio", + "public_port_range":"Intervalo de portas públicas", + "public_port_range_explainer":"Portas usadas para expor bancos de dados/serviços/serviços internos.
Adicione-os ao seu firewall (se aplicável).

Você pode especificar um intervalo de portas, por exemplo: 9000-9100", + "no_actions_available":"Nenhuma ação disponível", + "admin_api_key":"Chave de API de administrador" + }, + "register":{ + "register":"Registro", + "registering":"Registrando...", + "first_user":"Você está registrando o primeiro usuário. Será o administrador da sua instância Coolify." + }, + "reset":{ + "reset_password":"Redefinir", + "invalid_secret_key":"Chave secreta inválida.", + "secret_key":"Chave secreta", + "find_path_secret_key":"Você pode encontrá-lo em ~/coolify/.env (COOLIFY_SECRET_KEY)" + }, + "application":{ + "configuration":{ + "buildpack":{ + "choose_this_one":"Escolha este..." + }, + "branch_already_in_use":"Esta ramificação já é usada por outro aplicativo. Os webhooks não funcionarão neste caso para ambos os aplicativos. Tem certeza de que deseja usá-lo?", + "no_repositories_configured":"Nenhum repositório configurado para seu aplicativo Git.", + "configure_it_now":"Configure agora", + "loading_repositories":"Carregando repositórios...", + "select_a_repository":"Selecione um repositório", + "loading_branches":"Carregando ramos...", + "select_a_repository_first":"Selecione um repositório primeiro", + "select_a_branch":"Selecione uma filial", + "loading_groups":"Carregando grupos...", + "select_a_group":"Selecione um grupo", + "loading_projects":"Carregando projetos...", + "select_a_project":"Por favor selecione um projeto", + "no_projects_found":"Nenhum projeto encontrado", + "no_branches_found":"Nenhuma ramificação encontrada", + "configure_build_pack":"Configurar pacote de compilação", + "scanning_repository_suggest_build_pack":"Verificando repositório para sugerir um pacote de compilação para você...", + "found_lock_file":"Arquivo de bloqueio encontrado para {{packageManager}}.
Usando-o para comandos de comandos predefinidos.", + "configure_destination":"Configurar destino", + "no_configurable_destination":"Nenhum destino configurável encontrado", + "select_a_repository_project":"Selecione um Repositório/Projeto", + "select_a_git_source":"Selecione uma fonte Git", + "no_configurable_git":"Nenhuma fonte Git configurável encontrada", + "configuration_missing":"Configuração ausente" + }, + "build":{ + "queued_waiting_exec":"Na fila e aguardando execução.", + "build_logs_of":"Construir registros de", + "running":"Corrida", + "queued":"Enfileiradas", + "finished_in":"Terminando em", + "load_more":"Carregue mais", + "no_logs":"Nenhum registro encontrado", + "waiting_logs":"Aguardando os logs..." + }, + "preview":{ + "need_during_buildtime":"Precisa durante o tempo de construção?", + "setup_secret_app_first":"Você pode adicionar segredos a implantações de PR/MR. Por favor, adicione segredos ao aplicativo primeiro.
Útil para criar ambientes de preparação.", + "values_overwriting_app_secrets":"Esses valores substituem os segredos do aplicativo em implantações PR/MR. Útil para criar ambientes de preparação.", + "redeploy":"Reimplantar", + "no_previews_available":"Nenhuma visualização disponível" + }, + "secrets":{ + "secret_saved":"Segredo salvo.", + "use_isbuildsecret":"Use isBuildSecret", + "secrets_for":"Segredos para" + }, + "storage":{ + "path_is_required":"O caminho é obrigatório.", + "storage_saved":"Armazenamento salvo.", + "storage_updated":"Armazenamento atualizado.", + "storage_deleted":"Armazenamento excluído.", + "persistent_storage_explainer":"Você pode especificar qualquer pasta que deseja que seja persistente nas implantações.
/example significa que ela preservará /app/ example no contêiner, pois /app é o diretório raiz para seu aplicativo.

Isto é útil para armazenar dados como um banco de dados (SQLite) ou um cache." + }, + "deployment_queued":"Implantação em fila.", + "confirm_to_delete":"Tem certeza de que deseja excluir '{{name}}'?", + "stop_application":"Parar aplicativo", + "permission_denied_stop_application":"Você não tem permissão para parar o aplicativo.", + "rebuild_application":"Reconstruir aplicativo", + "permission_denied_rebuild_application":"Você não tem permissão para reconstruir o aplicativo.", + "build_and_start_application":"Implantar", + "permission_denied_build_and_start_application":"Você não tem permissão para implantar o aplicativo.", + "configurations":"Configurações", + "secret":"Segredos", + "persistent_storage":"Armazenamento persistente", + "previews":"Visualizações", + "logs":"Registros de aplicativos", + "build_logs":"Construir registros", + "delete_application":"Excluir", + "permission_denied_delete_application":"Você não tem permissão para excluir este aplicativo", + "domain_already_in_use":"O domínio {{domain}} já está em uso.", + "dns_not_set_error":"DNS não definido corretamente ou propagado para {{domain}}.

Verifique suas configurações de DNS.", + "domain_required":"O domínio é obrigatório.", + "settings_saved":"Configuração salva.", + "dns_not_set_partial_error":"DNS não definido", + "domain_not_valid":"Não foi possível resolver o domínio ou não está apontando para o endereço IP do servidor.

Verifique sua configuração de DNS e tente novamente.", + "git_source":"Fonte do Git", + "git_repository":"Repositório Git", + "build_pack":"Pacote de compilação", + "base_image":"Imagem de implantação", + "base_image_explainer":"Imagem que será usada para a implantação.", + "base_build_image":"Construir imagem", + "base_build_image_explainer":"Imagem que será usada durante o processo de compilação.", + "destination":"Destino", + "application":"Inscrição", + "url_fqdn":"URL (FQDN)", + "domain_fqdn":"Domínio (FQDN)", + "https_explainer":"Se você especificar https, o aplicativo será acessível apenas por https. O certificado SSL será gerado para você.
Se você especificar www, o aplicativo será redirecionado (302) de não www e vice-versa.
< br>Para modificar o domínio, você deve primeiro parar o aplicativo.

Você deve configurar seu DNS para apontar para o IP do servidor com antecedência.", + "ssl_www_and_non_www":"Gerar SSL para www e não www?", + "ssl_explainer":"Ele irá gerar certificados para www e não www.
Você precisa ter ambas as entradas DNS definidas com antecedência.

Útil se você espera receber visitantes em ambas.", + "install_command":"Comando de instalação", + "build_command":"Comando de compilação", + "start_command":"Comando Iniciar", + "directory_to_use_explainer":"Diretório a ser usado como base para todos os comandos.
Pode ser útil com monorepos.", + "publish_directory_explainer":"Diretório contendo todos os ativos para implantação.
Por exemplo: dist,_site ou public< /span>.", + "features":"Características", + "enable_automatic_deployment":"Ativar implantação automática", + "enable_auto_deploy_webhooks":"Habilite a implantação automática por meio de webhooks.", + "enable_mr_pr_previews":"Ativar visualizações de MR/PR", + "expose_a_port":"Expor uma porta", + "enable_preview_deploy_mr_pr_requests":"Habilite implantações de visualização de solicitações de pull ou mesclagem.", + "debug_logs":"Registros de depuração", + "enable_debug_log_during_build":"Ative os registros de depuração durante a fase de compilação.
Informações confidenciais podem ser visíveis e salvas nos registros.", + "cant_activate_auto_deploy_without_repo":"Não é possível ativar implantações automáticas até que apenas um aplicativo seja definido para este repositório/ramificação.", + "no_applications_found":"Nenhum aplicativo encontrado", + "secret__batch_dot_env":"Colar arquivo .env", + "batch_secrets":"Adicionar segredos em lote" + }, + "general":"Em geral", + "database":{ + "default_database":"Banco de dados padrão", + "generated_automatically_after_set_to_public":"Gerado automaticamente após definido como público", + "connection_string":"Cadeia de conexão", + "set_public":"Defina-o como público", + "warning_database_public":"Seu banco de dados estará acessível pela internet.
Leve a segurança a sério neste caso!", + "change_append_only_mode":"Alterar o modo somente anexar", + "warning_append_only":"Útil se você deseja restaurar dados redis de um backup.
É necessário reiniciar o banco de dados.", + "select_database_type":"Selecione um tipo de banco de dados", + "select_database_version":"Selecione uma versão do banco de dados", + "confirm_stop":"Tem certeza de que deseja parar {{name}}?", + "stop_database":"Pare", + "permission_denied_stop_database":"Você não tem permissão para parar o banco de dados.", + "start_database":"Começar", + "permission_denied_start_database":"Você não tem permissão para iniciar o banco de dados.", + "delete_database":"Excluir", + "permission_denied_delete_database":"Você não tem permissão para excluir um banco de dados", + "no_databases_found":"Nenhum banco de dados encontrado", + "logs":"Histórico" + }, + "destination":{ + "delete_destination":"Excluir", + "permission_denied_delete_destination":"Você não tem permissão para excluir este destino", + "add_to_coolify":"Adicionar ao Coolify", + "coolify_proxy_stopped":"Coolify Proxy parou!", + "coolify_proxy_started":"Coolify Proxy iniciado!", + "confirm_restart_proxy":"Tem certeza de que deseja reiniciar o proxy? Tudo será reconfigurado em ~10 segundos.", + "coolify_proxy_restarting":"Coolify Proxy reiniciando...", + "restarting_please_wait":"Reiniciando... aguarde...", + "force_restart_proxy":"Forçar reinicialização do proxy", + "use_coolify_proxy":"Usar Coolify Proxy?", + "no_destination_found":"Nenhum destino encontrado", + "new_error_network_already_exists":"Rede {{network}} já configurada para outra equipe!", + "new":{ + "saving_and_configuring_proxy":"Salvando...", + "install_proxy":"Isso instalará um proxy no destino para permitir que você acesse seus aplicativos e serviços sem qualquer configuração manual (recomendado para o Docker).

Os bancos de dados terão seu próprio proxy.", + "add_new_destination":"Adicionar novo destino", + "predefined_destinations":"Destinos predefinidos" + } + }, + "sources":{ + "local_docker":"Docker local", + "remote_docker":"Docker remoto", + "organization_explainer":"Preencha-o se quiser usar o de uma organização como seu Git Source. Caso contrário, seu usuário será usado." + }, + "source":{ + "new":{ + "git_source":"Adicionar nova fonte Git", + "official_providers":"Fornecedores oficiais" + }, + "no_git_sources_found":"Nenhuma fonte git encontrada", + "delete_git_source":"Excluir", + "permission_denied":"Você não tem permissão para excluir uma fonte Git", + "create_new_app":"Criar novo aplicativo {{name}}", + "change_app_settings":"Alterar {{name}} configurações do aplicativo", + "install_repositories":"Instalar repositórios", + "application_id":"ID do aplicativo", + "group_name":"Nome do grupo", + "oauth_id":"ID OAuth", + "oauth_id_explainer":"O OAuth ID é o identificador exclusivo do aplicativo GitLab.
Você pode encontrá-lo no URL do seu aplicativo GitLab OAuth.", + "register_oauth_gitlab":"Registre um novo aplicativo OAuth no GitLab", + "gitlab":{ + "self_hosted":"Aplicativo em toda a instância (auto-hospedado)", + "user_owned":"Aplicativo de propriedade do usuário", + "group_owned":"Aplicativo de propriedade do grupo", + "gitlab_application_type":"Tipo de aplicativo GitLab", + "already_configured":"O aplicativo GitLab já está configurado." + }, + "github":{ + "redirecting":"Redirecionando para o Github..." + } + }, + "services":{ + "all_email_verified":"Todos os e-mails são verificados. Você pode entrar agora.", + "generate_www_non_www_ssl":"Ele irá gerar certificados para www e não www.
Você precisa ter ambas as entradas DNS definidas com antecedência.

O serviço precisa ser reiniciado." + }, + "service":{ + "stop_service":"Pare", + "permission_denied_stop_service":"Você não tem permissão para interromper o serviço.", + "start_service":"Começar", + "permission_denied_start_service":"Você não tem permissão para iniciar o serviço.", + "delete_service":"Excluir", + "permission_denied_delete_service":"Você não tem permissão para excluir um serviço.", + "no_service":"Nenhum serviço encontrado", + "logs":"Histórico" + }, + "setting":{ + "change_language":"Mudar idioma", + "permission_denied":"Você não tem permissão para fazer isso. \\nPeça a um administrador para modificar suas permissões.", + "domain_removed":"Domínio removido", + "ssl_explainer":"Se você especificar https, o Coolify será acessível apenas por https. O certificado SSL será gerado para você.
Se você especificar www, Coolify será redirecionado (302) de não www e vice-versa.

AVISO: Se você alterar um domínio já definido, isso interromperá webhooks e outras integrações! Você precisa atualizá-los manualmente.", + "must_remove_domain_before_changing":"Deve remover o domínio antes de alterar esta configuração.", + "registration_allowed":"Registro permitido?", + "registration_allowed_explainer":"Permitir mais registros no aplicativo.
É desligado após o primeiro registro.", + "coolify_proxy_settings":"Configurações de proxy do Coolify", + "credential_stat_explainer":"Credenciais para a página de estatísticas.", + "auto_update_enabled":"Atualização automática habilitada?", + "auto_update_enabled_explainer":"Habilite atualizações automáticas para Coolify. Isso será feito automaticamente nos bastidores, se não houver nenhum processo de compilação em execução.", + "generate_www_non_www_ssl":"Ele irá gerar certificados para www e não www.
Você precisa ter ambas as entradas de DNS configuradas antecipadamente.", + "is_dns_check_enabled":"Verificação de DNS habilitada?", + "is_dns_check_enabled_explainer":"Você pode desabilitar a verificação de DNS antes de criar certificados SSL.

Desligá-la é útil quando o Coolify está atrás de um proxy reverso ou túnel." + }, + "team":{ + "pending_invitations":"Convites pendentes", + "accept":"Aceitar", + "delete":"Excluir", + "member":"membros)", + "root":"(raiz)", + "invited_with_permissions":"Convidado para {{teamName}} com permissão {{permission}}.", + "members":"Membros", + "root_team_explainer":"Esta é a equipe raiz. Isso significa que os membros deste grupo podem gerenciar as configurações de toda a instância e ter todos os privilégios no Coolify (imagine como usuário root no Linux).", + "permission":"Permissão", + "you":"Você", + "promote_to":"Promover para {{grade}}", + "revoke_invitation":"Revogar convite", + "pending_invitation":"Convite pendente", + "invite_new_member":"Convidar novo membro", + "send_invitation":"Enviar convite", + "invite_only_register_explainer":"Você só pode convidar usuários registrados.", + "admin":"Administrador", + "read":"Ler" + } +} \ No newline at end of file diff --git a/apps/ui/src/lib/store.ts b/apps/ui/src/lib/store.ts index ed5b386b9..f779eb1dc 100644 --- a/apps/ui/src/lib/store.ts +++ b/apps/ui/src/lib/store.ts @@ -57,14 +57,15 @@ export const appSession: Writable = writable({ export const disabledButton: Writable = writable(false); export const isDeploymentEnabled: Writable = writable(false); export function checkIfDeploymentEnabledApplications(isAdmin: boolean, application: any) { - return ( + return !!( isAdmin && (application.buildPack === 'compose') || (application.fqdn || application.settings.isBot) && - application.gitSource && - application.repository && - application.destinationDocker && - application.buildPack + ((application.gitSource && + application.repository && + application.buildPack) || application.simpleDockerfile) && + application.destinationDocker + ); } export function checkIfDeploymentEnabledServices(isAdmin: boolean, service: any) { @@ -81,6 +82,7 @@ export const status: Writable = writable({ statuses: [], overallStatus: 'stopped', loading: false, + restarting: false, initialLoading: true }, service: { diff --git a/apps/ui/src/lib/translations.ts b/apps/ui/src/lib/translations.ts index 3ead4e1ec..9cc71a118 100644 --- a/apps/ui/src/lib/translations.ts +++ b/apps/ui/src/lib/translations.ts @@ -1,11 +1,18 @@ import i18n from 'sveltekit-i18n'; +import { derived, writable } from "svelte/store"; import lang from './lang.json'; +export let currentLocale = writable("en"); +export let debugTranslation = writable(false); + /** @type {import('sveltekit-i18n').Config} */ export const config = { fallbackLocale: 'en', translations: { en: { lang }, + es: { lang }, + pt: { lang }, + ko: { lang }, fr: { lang } }, loaders: [ @@ -14,12 +21,27 @@ export const config = { key: '', loader: async () => (await import('./locales/en.json')).default }, + { + locale: 'es', + key: '', + loader: async () => (await import('./locales/es.json')).default + }, + { + locale: 'pt', + key: '', + loader: async () => (await import('./locales/pt.json')).default + }, { locale: 'fr', key: '', loader: async () => (await import('./locales/fr.json')).default + }, + { + locale: 'ko', + key: '', + loader: async () => (await import('./locales/ko.json')).default } ] }; -export const { t, locales, locale, loadTranslations } = new i18n(config); +export const { t, locales, locale, loadTranslations } = new i18n(config); \ No newline at end of file diff --git a/apps/ui/src/routes/__layout.svelte b/apps/ui/src/routes/__layout.svelte index 4663e6d9f..2efaa8a4e 100644 --- a/apps/ui/src/routes/__layout.svelte +++ b/apps/ui/src/routes/__layout.svelte @@ -64,6 +64,8 @@ @@ -136,10 +152,16 @@ {/if} +
{#if $appSession.userId} + IAM + Settings + Logout