diff --git a/.dockerignore b/.dockerignore index 7e5eecf8e..66ff0bf1f 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,5 +1,6 @@ .DS_Store node_modules +.pnpm-store build .svelte-kit package @@ -9,4 +10,8 @@ package dist client apps/api/db/*.db -local-serve \ No newline at end of file +local-serve +apps/api/db/migration.db-journal +apps/api/core* +logs +others/certificates diff --git a/.github/workflows/fluent-bit-release.yml b/.github/workflows/fluent-bit-release.yml new file mode 100644 index 000000000..77be7bb95 --- /dev/null +++ b/.github/workflows/fluent-bit-release.yml @@ -0,0 +1,93 @@ +name: fluent-bit-release + +on: + push: + paths: + - "others/fluentbit" + - ".github/workflows/fluent-bit-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/fluentbit/ + platforms: linux/arm64 + push: true + tags: coollabsio/coolify-fluent-bit:1.0.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/fluentbit/ + platforms: linux/amd64 + push: true + tags: coollabsio/coolify-fluent-bit:1.0.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/fluentbit/ + platforms: linux/aarch64 + push: true + tags: coollabsio/coolify-fluent-bit:1.0.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/coolify-fluent-bit:1.0.0 --amend coollabsio/coolify-fluent-bit:1.0.0-amd64 --amend coollabsio/coolify-fluent-bit:1.0.0-arm64 --amend coollabsio/coolify-fluent-bit:1.0.0-aarch64 + docker manifest push coollabsio/coolify-fluent-bit:1.0.0 diff --git a/.github/workflows/staging-release.yml b/.github/workflows/staging-release.yml index 2280167d8..fa6b09473 100644 --- a/.github/workflows/staging-release.yml +++ b/.github/workflows/staging-release.yml @@ -2,6 +2,10 @@ name: staging-release on: push: + paths: + - '**' + - "!others/fluentbit" + - "!.github/workflows/fluent-bit-release.yml" branches: - next diff --git a/.gitignore b/.gitignore index ddc188f8c..66ff0bf1f 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,4 @@ local-serve apps/api/db/migration.db-journal apps/api/core* logs -others/certificates \ No newline at end of file +others/certificates diff --git a/CONTRIBUTION.md b/CONTRIBUTION.md index 9f4bf9321..9d869bb10 100644 --- a/CONTRIBUTION.md +++ b/CONTRIBUTION.md @@ -1,123 +1,48 @@ -# Contribution +# Contributing -First, thanks for considering to contribute to my project. It really means a lot! :) +> "First, thanks for considering to contribute to my project. + It really means a lot! 😁" - [@andrasbacsai](https://github.com/andrasbacsai) -You can ask for guidance anytime on our Discord server in the #contribution channel. +You can ask for guidance anytime on our +[Discord server](https://coollabs.io/discord) in the `#contribution` channel. -## Setup your development environment -### Container based development flow (recommended and the easiest) -All you need is to intall [Docker Engine 20.11+](https://docs.docker.com/engine/install/) on your local machine and run `pnpm dev:container`. It will build the base image for Coolify and start the development server inside Docker. All required ports (3000, 3001) will be exposed to your host. +You'll need a set of skills to [get started](docs/contribution/GettingStarted.md). -### Github codespaces +## 1) Setup your development environment -If you have github codespaces enabled then you can just create a codespace and run `pnpm dev` to run your the dev environment. All the required dependencies and packages has been configured for you already. +- 🌟 [Container based](docs/dev_setup/Container.md) ← *Recomended* +- 📦 [DockerContainer](docs/dev_setup/DockerContiner.md) *WIP +- 🐙 [Github Codespaces](docs/dev_setup/GithubCodespaces.md) +- ☁️ [GitPod](docs/dev_setup/GitPod.md) +- 🍏 [Local Mac](docs/dev_setup/Mac.md) -### Gitpod -1. Use [container based development flow](#container-based-development-flow-easiest) -2. Or setup your workspace manually: +## 2) Basic requirements -Create a workspace from this repository, run `pnpm install && pnpm db:push && pnpm db:seed` and then `pnpm dev`. All the required dependencies and packages has been configured for you already. +- [Install Pnpm](https://pnpm.io/installation) +- [Install Docker Engine](https://docs.docker.com/engine/install/) +- [Setup Docker Compose Plugin](https://docs.docker.com/compose/install/compose-plugin/) +- [Setup GIT LFS Support](https://git-lfs.github.com/) -> Some packages, just `pack` are not installed in this way. You cannot test all the features. Please use the [container based development flow](#container-based-development-flow-easiest). +## 3) Setup Coolify -### Local Machine -> At the moment, Coolify `doesn't support Windows`. You must use `Linux` or `MacOS` or consider using Gitpod or Github Codespaces. +- Copy `apps/api/.env.example` to `apps/api/.env` +- Edit `apps/api/.env`, set the `COOLIFY_APP_ID` environment variable to something cool. +- Run `pnpm install` to install dependencies. +- Run `pnpm db:push` to o create a local SQlite database. This will apply all migrations at `db/dev.db`. +- Run `pnpm db:seed` seed the database. +- Run `pnpm dev` start coding. -Install all the prerequisites manually to your host system. If you would not like to install anything, I suggest to use the [container based development flow](#container-based-development-flow-easiest). +```sh +# Or... Copy and paste commands bellow: +cp apps/api/.env.example apps/api/.env +pnpm install +pnpm db:push +pnpm db:seed +pnpm dev +``` -- Due to the lock file, this repository is best with [pnpm](https://pnpm.io). I recommend you try and use `pnpm` because it is cool and efficient! -- You need to have [Docker Engine](https://docs.docker.com/engine/install/) installed locally. -- You need to have [Docker Compose Plugin](https://docs.docker.com/compose/install/compose-plugin/) installed locally. -- You need to have [GIT LFS Support](https://git-lfs.github.com/) installed locally. +## 4) Start Coding -Optional: -- To test Heroku buildpacks, you need [pack](https://github.com/buildpacks/pack) binary installed locally. +You should be able to access `http://localhost:3000`. -### Inside a Docker container -`WIP` - -## Setup Coolify -- Copy `apps/api/.env.template` to `apps/api/.env.template` and set the `COOLIFY_APP_ID` environment variable to something cool. -- `pnpm install` to install dependencies. -- `pnpm db:push` to o create a local SQlite database. - - This will apply all migrations at `db/dev.db`. - -- `pnpm db:seed` seed the database. -- `pnpm dev` start coding. - -## Technical skills required - -- **Languages**: Node.js / Javascript / Typescript -- **Framework JS/TS**: [SvelteKit](https://kit.svelte.dev/) & [Fastify](https://www.fastify.io/) -- **Database ORM**: [Prisma.io](https://www.prisma.io/) -- **Docker Engine API** - -## Add a new service -### Which service is eligable to add to Coolify? -The following statements needs to be true: - -- Self-hostable -- Open-source -- Maintained (I do not want to add software full of bugs) - -### Create Prisma / Database schema for the new service. -All data that needs to be persist for a service should be saved to the database in `cleartext` or `encrypted`. - -very password/api key/passphrase needs to be encrypted. If you are not sure, whether it should be encrypted or not, just encrypt it. - -Update Prisma schema in [src/apps/api/prisma/schema.prisma](https://github.com/coollabsio/coolify/blob/main/apps/api/prisma/schema.prisma). - -- Add new model with the new service name. -- Make a relationship with `Service` model. -- In the `Service` model, the name of the new field should be with low-capital. -- If the service needs a database, define a `publicPort` field to be able to make it's database public, example field name in case of PostgreSQL: `postgresqlPublicPort`. It should be a optional field. - -Once done, create Prisma schema with `pnpm db:push`. -> You may also need to restart `Typescript Language Server` in your IDE to get the new types. - -### Add available versions - -Versions are hardcoded into Coolify at the moment and based on Docker image tags. -- Update `supportedServiceTypesAndVersions` function [here](apps/api/src/lib/services/supportedVersions.ts) - -### Include the new service in queries - -At [here](apps/api/src/lib/services/common.ts) in `includeServices` function add the new table name, so it will be included in all places in the database queries where it is required. - -### Define auto-generated fields - -At [here](apps/api/src/lib/services/common.ts) in `configureServiceType` function add the initial auto-generated details such as password, users etc, and the encryption process of secrets (if applicable). - -### Define input field details - -At [here](apps/api/src/lib/services/serviceFields.ts) add details about the input fields shown in the UI, so every component (API/UI) will know what to do with the values (decrypt/show it by default/readonly/etc). - -### Define the start process - -- At [here](apps/api/src/lib/services/handlers.ts), define how the service should start. It could be complex and based on `docker-compose` definitions. - -> See `startUmamiService()` function as example. - -- At [here](apps/api/src/routes/api/v1/services/handlers.ts), add the new start service process to `startService` function. - -### Define the deletion process - -[Here](apps/api/src/lib/services/common.ts) in `removeService` add the database deletion process. - -### Custom logo - -- At [here](apps/ui/src/lib/components/svg/services) add the service custom log as a Svelte component and export it [here](apps/ui/src/lib/components/svg/services/index.ts). - -> SVG is recommended, but you can use PNG as well. It should have the `isAbsolute` variable with the suitable CSS classes, primarily for sizing and positioning. - -- At [here](apps/ui/src/lib/components/svg/services/ServiceIcons.svelte) include the new logo with `isAbsolute` property. - -- At [here](apps/ui/src/routes/services/[id]/_ServiceLinks.svelte) add links to the documentation of the service. - -### Custom fields on the UI -By default the URL and name are shown on the UI. Everything else needs to be added [here](apps/ui/src/routes/services/[id]/_Services/_Services.svelte) - -> If you need to show more details on the frontend, such as users/passwords, you need to add Svelte component [here](apps/ui/src/routes/services/[id]/_Services) with an underscore. For example, see other [here](apps/ui/src/routes/services/[id]/_Services/_Umami.svelte). - -Good job! 👏 \ No newline at end of file +1. Click `Register` and setup your first user. \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 8cbc68037..6638bb202 100644 --- a/Dockerfile +++ b/Dockerfile @@ -43,6 +43,8 @@ COPY --from=build /app/apps/ui/build/ ./public COPY --from=build /app/apps/api/prisma/ ./prisma COPY --from=build /app/apps/api/package.json . COPY --from=build /app/docker-compose.yaml . +COPY --from=build /app/apps/api/tags.json . +COPY --from=build /app/apps/api/templates.json . RUN pnpm install -p diff --git a/README.md b/README.md index 2c106d047..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) @@ -93,19 +94,39 @@ Deploy your resource to: - [Fider](https://fider.io) - [Hasura](https://hasura.io) - [GlitchTip](https://glitchtip.com) - -## Migration from v1 - -A fresh installation is necessary. v2 and v3 are not compatible with v1. +- And more... ## Support -- Twitter: [@andrasbacsai](https://twitter.com/andrasbacsai) +- Mastodon: [@andrasbacsai@fosstodon.org](https://fosstodon.org/@andrasbacsai) - Telegram: [@andrasbacsai](https://t.me/andrasbacsai) +- Twitter: [@andrasbacsai](https://twitter.com/andrasbacsai) - Email: [andras@coollabs.io](mailto:andras@coollabs.io) - Discord: [Invitation](https://coollabs.io/discord) -## Financial Contributors +--- + +## ⚗️ Expertise Contributions + +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 a36dfcac9..598d659b3 100644 --- a/apps/api/.env.example +++ b/apps/api/.env.example @@ -2,7 +2,6 @@ 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 diff --git a/apps/api/devTags.json b/apps/api/devTags.json new file mode 100644 index 000000000..e75e9dd0d --- /dev/null +++ b/apps/api/devTags.json @@ -0,0 +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.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","5.19.2","5.19.1","5.19.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.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","5.14.2","5.14.1"]},{"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.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","5.14.2","5.14.1"]},{"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.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","8.5.6","8.5.5","8.5.4"]},{"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.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.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","0.9.19"]},{"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.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","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"]},{"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-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","edge-2022-10-25-a059748c178cce0bc30bdc915d4ff8f0d13ce25c","edge-2022-10-24-94ae069cdcf9a171e812da32d760a436fa9b37ad","edge-2022-10-24-71d2011a1f6d2b60bb65af9f775bf75d4beaf8fd"]},{"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 new file mode 100644 index 000000000..3a7a07b29 --- /dev/null +++ b/apps/api/devTemplates.yaml @@ -0,0 +1,3349 @@ +- 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: + port: $$config_port + address: 0.0.0.0 + lavalink: + server: + password: "$$secret_password" + sources: + youtube: true + bandcamp: true + soundcloud: true + twitch: true + vimeo: true + http: true + local: false + + logging: + file: + path: ./logs/ + + level: + root: INFO + lavalink: INFO + + logback: + rollingpolicy: + max-file-size: 1GB + 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. + To enable/activate this option, pass a list of allowed environments separated by a comma. + Currently, 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: |2- + + # see https://docs.searxng.org/admin/engines/settings.html#use-default-settings + use_default_settings: true + server: + secret_key: $$secret_secret_key + limiter: true + image_proxy: true + ui: + static_use_hash: true + redis: + 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: |2- + + -- CreateTable + CREATE TABLE "account" ( + "user_id" SERIAL NOT NULL, + "username" VARCHAR(255) NOT NULL, + "password" VARCHAR(60) NOT NULL, + "is_admin" BOOLEAN NOT NULL DEFAULT false, + "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, + + PRIMARY KEY ("user_id") + ); + + -- CreateTable + CREATE TABLE "event" ( + "event_id" SERIAL NOT NULL, + "website_id" INTEGER NOT NULL, + "session_id" INTEGER NOT NULL, + "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, + "url" VARCHAR(500) NOT NULL, + "event_type" VARCHAR(50) NOT NULL, + "event_value" VARCHAR(50) NOT NULL, + + PRIMARY KEY ("event_id") + ); + + -- CreateTable + CREATE TABLE "pageview" ( + "view_id" SERIAL NOT NULL, + "website_id" INTEGER NOT NULL, + "session_id" INTEGER NOT NULL, + "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, + "url" VARCHAR(500) NOT NULL, + "referrer" VARCHAR(500), + + PRIMARY KEY ("view_id") + ); + + -- CreateTable + CREATE TABLE "session" ( + "session_id" SERIAL NOT NULL, + "session_uuid" UUID NOT NULL, + "website_id" INTEGER NOT NULL, + "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, + "hostname" VARCHAR(100), + "browser" VARCHAR(20), + "os" VARCHAR(20), + "device" VARCHAR(20), + "screen" VARCHAR(11), + "language" VARCHAR(35), + "country" CHAR(2), + + PRIMARY KEY ("session_id") + ); + + -- CreateTable + CREATE TABLE "website" ( + "website_id" SERIAL NOT NULL, + "website_uuid" UUID NOT NULL, + "user_id" INTEGER NOT NULL, + "name" VARCHAR(100) NOT NULL, + "domain" VARCHAR(500), + "share_id" VARCHAR(64), + "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, + + PRIMARY KEY ("website_id") + ); + + -- CreateIndex + CREATE UNIQUE INDEX "account.username_unique" ON "account"("username"); + + -- CreateIndex + CREATE INDEX "event_created_at_idx" ON "event"("created_at"); + + -- CreateIndex + CREATE INDEX "event_session_id_idx" ON "event"("session_id"); + + -- CreateIndex + CREATE INDEX "event_website_id_idx" ON "event"("website_id"); + + -- CreateIndex + CREATE INDEX "pageview_created_at_idx" ON "pageview"("created_at"); + + -- CreateIndex + CREATE INDEX "pageview_session_id_idx" ON "pageview"("session_id"); + + -- CreateIndex + CREATE INDEX "pageview_website_id_created_at_idx" ON "pageview"("website_id", "created_at"); + + -- CreateIndex + CREATE INDEX "pageview_website_id_idx" ON "pageview"("website_id"); + + -- CreateIndex + CREATE INDEX "pageview_website_id_session_id_created_at_idx" ON "pageview"("website_id", "session_id", "created_at"); + + -- CreateIndex + CREATE UNIQUE INDEX "session.session_uuid_unique" ON "session"("session_uuid"); + + -- CreateIndex + CREATE INDEX "session_created_at_idx" ON "session"("created_at"); + + -- CreateIndex + CREATE INDEX "session_website_id_idx" ON "session"("website_id"); + + -- CreateIndex + CREATE UNIQUE INDEX "website.website_uuid_unique" ON "website"("website_uuid"); + + -- CreateIndex + CREATE UNIQUE INDEX "website.share_id_unique" ON "website"("share_id"); + + -- CreateIndex + CREATE INDEX "website_user_id_idx" ON "website"("user_id"); + + -- AddForeignKey + ALTER TABLE "event" ADD FOREIGN KEY ("session_id") REFERENCES "session"("session_id") ON DELETE CASCADE ON UPDATE CASCADE; + + -- AddForeignKey + ALTER TABLE "event" ADD FOREIGN KEY ("website_id") REFERENCES "website"("website_id") ON DELETE CASCADE ON UPDATE CASCADE; + + -- AddForeignKey + ALTER TABLE "pageview" ADD FOREIGN KEY ("session_id") REFERENCES "session"("session_id") ON DELETE CASCADE ON UPDATE CASCADE; + + -- AddForeignKey + ALTER TABLE "pageview" ADD FOREIGN KEY ("website_id") REFERENCES "website"("website_id") ON DELETE CASCADE ON UPDATE CASCADE; + + -- AddForeignKey + ALTER TABLE "session" ADD FOREIGN KEY ("website_id") REFERENCES "website"("website_id") ON DELETE CASCADE ON UPDATE CASCADE; + + -- AddForeignKey + ALTER TABLE "website" ADD FOREIGN KEY ("user_id") REFERENCES "account"("user_id") ON DELETE CASCADE ON UPDATE CASCADE; + + 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: |2- + + -- CreateTable + CREATE TABLE "account" ( + "user_id" SERIAL NOT NULL, + "username" VARCHAR(255) NOT NULL, + "password" VARCHAR(60) NOT NULL, + "is_admin" BOOLEAN NOT NULL DEFAULT false, + "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, + + PRIMARY KEY ("user_id") + ); + + -- CreateTable + CREATE TABLE "event" ( + "event_id" SERIAL NOT NULL, + "website_id" INTEGER NOT NULL, + "session_id" INTEGER NOT NULL, + "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, + "url" VARCHAR(500) NOT NULL, + "event_type" VARCHAR(50) NOT NULL, + "event_value" VARCHAR(50) NOT NULL, + + PRIMARY KEY ("event_id") + ); + + -- CreateTable + CREATE TABLE "pageview" ( + "view_id" SERIAL NOT NULL, + "website_id" INTEGER NOT NULL, + "session_id" INTEGER NOT NULL, + "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, + "url" VARCHAR(500) NOT NULL, + "referrer" VARCHAR(500), + + PRIMARY KEY ("view_id") + ); + + -- CreateTable + CREATE TABLE "session" ( + "session_id" SERIAL NOT NULL, + "session_uuid" UUID NOT NULL, + "website_id" INTEGER NOT NULL, + "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, + "hostname" VARCHAR(100), + "browser" VARCHAR(20), + "os" VARCHAR(20), + "device" VARCHAR(20), + "screen" VARCHAR(11), + "language" VARCHAR(35), + "country" CHAR(2), + + PRIMARY KEY ("session_id") + ); + + -- CreateTable + CREATE TABLE "website" ( + "website_id" SERIAL NOT NULL, + "website_uuid" UUID NOT NULL, + "user_id" INTEGER NOT NULL, + "name" VARCHAR(100) NOT NULL, + "domain" VARCHAR(500), + "share_id" VARCHAR(64), + "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, + + PRIMARY KEY ("website_id") + ); + + -- CreateIndex + CREATE UNIQUE INDEX "account.username_unique" ON "account"("username"); + + -- CreateIndex + CREATE INDEX "event_created_at_idx" ON "event"("created_at"); + + -- CreateIndex + CREATE INDEX "event_session_id_idx" ON "event"("session_id"); + + -- CreateIndex + CREATE INDEX "event_website_id_idx" ON "event"("website_id"); + + -- CreateIndex + CREATE INDEX "pageview_created_at_idx" ON "pageview"("created_at"); + + -- CreateIndex + CREATE INDEX "pageview_session_id_idx" ON "pageview"("session_id"); + + -- CreateIndex + CREATE INDEX "pageview_website_id_created_at_idx" ON "pageview"("website_id", "created_at"); + + -- CreateIndex + CREATE INDEX "pageview_website_id_idx" ON "pageview"("website_id"); + + -- CreateIndex + CREATE INDEX "pageview_website_id_session_id_created_at_idx" ON "pageview"("website_id", "session_id", "created_at"); + + -- CreateIndex + CREATE UNIQUE INDEX "session.session_uuid_unique" ON "session"("session_uuid"); + + -- CreateIndex + CREATE INDEX "session_created_at_idx" ON "session"("created_at"); + + -- CreateIndex + CREATE INDEX "session_website_id_idx" ON "session"("website_id"); + + -- CreateIndex + CREATE UNIQUE INDEX "website.website_uuid_unique" ON "website"("website_uuid"); + + -- CreateIndex + CREATE UNIQUE INDEX "website.share_id_unique" ON "website"("share_id"); + + -- CreateIndex + CREATE INDEX "website_user_id_idx" ON "website"("user_id"); + + -- AddForeignKey + ALTER TABLE "event" ADD FOREIGN KEY ("session_id") REFERENCES "session"("session_id") ON DELETE CASCADE ON UPDATE CASCADE; + + -- AddForeignKey + ALTER TABLE "event" ADD FOREIGN KEY ("website_id") REFERENCES "website"("website_id") ON DELETE CASCADE ON UPDATE CASCADE; + + -- AddForeignKey + ALTER TABLE "pageview" ADD FOREIGN KEY ("session_id") REFERENCES "session"("session_id") ON DELETE CASCADE ON UPDATE CASCADE; + + -- AddForeignKey + ALTER TABLE "pageview" ADD FOREIGN KEY ("website_id") REFERENCES "website"("website_id") ON DELETE CASCADE ON UPDATE CASCADE; + + -- AddForeignKey + ALTER TABLE "session" ADD FOREIGN KEY ("website_id") REFERENCES "website"("website_id") ON DELETE CASCADE ON UPDATE CASCADE; + + -- AddForeignKey + ALTER TABLE "website" ADD FOREIGN KEY ("user_id") REFERENCES "account"("user_id") ON DELETE CASCADE ON UPDATE CASCADE; + + 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); + define('WP_DEBUG_LOG', true); + define('WP_DEBUG_DISPLAY', false); + @ini_set('display_errors', 0); + - 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); + define('WP_DEBUG_LOG', true); + define('WP_DEBUG_DISPLAY', false); + @ini_set('display_errors', 0); +- 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:14" + 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). 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 49d5f34be..62da72325 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -3,46 +3,48 @@ "description": "Coolify's Fastify API", "license": "Apache-2.0", "scripts": { - "db:generate":"prisma generate", + "db:generate": "prisma generate", "db:push": "prisma db push && prisma generate", "db:seed": "prisma db seed", "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" }, "dependencies": { "@breejs/ts-worker": "2.0.0", - "@fastify/autoload": "5.4.0", + "@fastify/autoload": "5.5.0", "@fastify/cookie": "8.3.0", - "@fastify/cors": "8.1.0", + "@fastify/cors": "8.2.0", "@fastify/env": "4.1.0", - "@fastify/jwt": "6.3.2", - "@fastify/multipart": "7.2.0", - "@fastify/static": "6.5.0", + "@fastify/jwt": "6.3.3", + "@fastify/multipart": "7.3.0", + "@fastify/static": "6.5.1", "@iarna/toml": "2.2.5", "@ladjs/graceful": "3.0.2", - "@prisma/client": "4.4.0", - "prisma": "4.4.0", - "axios": "0.27.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", - "cabin": "9.1.2", + "cabin": "11.0.1", "compare-versions": "5.0.1", - "csv-parse": "5.3.1", + "csv-parse": "5.3.2", "csvtojson": "2.0.10", "cuid": "2.1.8", - "dayjs": "1.11.5", + "dayjs": "1.11.6", "dockerode": "3.3.4", "dotenv-extended": "2.9.0", "execa": "6.1.0", - "fastify": "4.8.1", - "fastify-plugin": "4.2.1", + "fastify": "4.10.2", + "fastify-plugin": "4.3.0", + "fastify-socket.io": "4.0.0", "generate-password": "1.7.0", - "got": "12.5.2", + "got": "12.5.3", "is-ip": "5.0.0", "is-port-reachable": "4.0.0", "js-yaml": "4.1.0", @@ -51,27 +53,29 @@ "node-os-utils": "1.3.7", "p-all": "4.0.0", "p-throttle": "5.0.0", + "prisma": "4.6.1", "public-ip": "6.0.1", - "pump": "^3.0.0", + "pump": "3.0.0", + "socket.io": "4.5.3", "ssh-config": "4.1.6", "strip-ansi": "7.0.1", "unique-names-generator": "4.7.1" }, "devDependencies": { - "@types/node": "18.8.5", + "@types/node": "18.11.9", "@types/node-os-utils": "1.3.0", - "@typescript-eslint/eslint-plugin": "5.38.1", - "@typescript-eslint/parser": "5.38.1", - "esbuild": "0.15.10", - "eslint": "8.25.0", + "@typescript-eslint/eslint-plugin": "5.44.0", + "@typescript-eslint/parser": "5.44.0", + "esbuild": "0.15.15", + "eslint": "8.28.0", "eslint-config-prettier": "8.5.0", "eslint-plugin-prettier": "4.2.1", "nodemon": "2.0.20", "prettier": "2.7.1", - "rimraf": "3.0.2", "tsconfig-paths": "4.1.0", - "typescript": "4.8.4" + "types-fastify-socket.io": "0.0.1", + "typescript": "4.9.3" }, "prisma": { "seed": "node prisma/seed.js" diff --git a/apps/api/prisma/migrations/20221017134342_standardized_service_configs/migration.sql b/apps/api/prisma/migrations/20221017134342_standardized_service_configs/migration.sql new file mode 100644 index 000000000..837017435 --- /dev/null +++ b/apps/api/prisma/migrations/20221017134342_standardized_service_configs/migration.sql @@ -0,0 +1,13 @@ +-- CreateTable +CREATE TABLE "ServiceSetting" ( + "id" TEXT NOT NULL PRIMARY KEY, + "serviceId" TEXT NOT NULL, + "name" TEXT NOT NULL, + "value" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "ServiceSetting_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateIndex +CREATE UNIQUE INDEX "ServiceSetting_serviceId_name_key" ON "ServiceSetting"("serviceId", "name"); diff --git a/apps/api/prisma/migrations/20221018090939_service_peristent_volumes_predefined/migration.sql b/apps/api/prisma/migrations/20221018090939_service_peristent_volumes_predefined/migration.sql new file mode 100644 index 000000000..c7b935eb7 --- /dev/null +++ b/apps/api/prisma/migrations/20221018090939_service_peristent_volumes_predefined/migration.sql @@ -0,0 +1,19 @@ +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_ServicePersistentStorage" ( + "id" TEXT NOT NULL PRIMARY KEY, + "serviceId" TEXT NOT NULL, + "path" TEXT NOT NULL, + "volumeName" TEXT, + "predefined" BOOLEAN NOT NULL DEFAULT false, + "containerId" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "ServicePersistentStorage_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); +INSERT INTO "new_ServicePersistentStorage" ("createdAt", "id", "path", "serviceId", "updatedAt") SELECT "createdAt", "id", "path", "serviceId", "updatedAt" FROM "ServicePersistentStorage"; +DROP TABLE "ServicePersistentStorage"; +ALTER TABLE "new_ServicePersistentStorage" RENAME TO "ServicePersistentStorage"; +CREATE UNIQUE INDEX "ServicePersistentStorage_serviceId_path_key" ON "ServicePersistentStorage"("serviceId", "path"); +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; diff --git a/apps/api/prisma/migrations/20221021112429_serivce_settings_extended/migration.sql b/apps/api/prisma/migrations/20221021112429_serivce_settings_extended/migration.sql new file mode 100644 index 000000000..b2eddcfd9 --- /dev/null +++ b/apps/api/prisma/migrations/20221021112429_serivce_settings_extended/migration.sql @@ -0,0 +1,24 @@ +/* + Warnings: + + - Added the required column `variableName` to the `ServiceSetting` table without a default value. This is not possible if the table is not empty. + +*/ +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_ServiceSetting" ( + "id" TEXT NOT NULL PRIMARY KEY, + "serviceId" TEXT NOT NULL, + "name" TEXT NOT NULL, + "value" TEXT NOT NULL, + "variableName" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "ServiceSetting_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); +INSERT INTO "new_ServiceSetting" ("createdAt", "id", "name", "serviceId", "updatedAt", "value") SELECT "createdAt", "id", "name", "serviceId", "updatedAt", "value" FROM "ServiceSetting"; +DROP TABLE "ServiceSetting"; +ALTER TABLE "new_ServiceSetting" RENAME TO "ServiceSetting"; +CREATE UNIQUE INDEX "ServiceSetting_serviceId_name_key" ON "ServiceSetting"("serviceId", "name"); +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; diff --git a/apps/api/prisma/migrations/20221021185630_service_template_version/migration.sql b/apps/api/prisma/migrations/20221021185630_service_template_version/migration.sql new file mode 100644 index 000000000..f716a2340 --- /dev/null +++ b/apps/api/prisma/migrations/20221021185630_service_template_version/migration.sql @@ -0,0 +1,21 @@ +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_Service" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "fqdn" TEXT, + "exposePort" INTEGER, + "dualCerts" BOOLEAN NOT NULL DEFAULT false, + "type" TEXT, + "version" TEXT, + "templateVersion" TEXT NOT NULL DEFAULT '0.0.0', + "destinationDockerId" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "Service_destinationDockerId_fkey" FOREIGN KEY ("destinationDockerId") REFERENCES "DestinationDocker" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); +INSERT INTO "new_Service" ("createdAt", "destinationDockerId", "dualCerts", "exposePort", "fqdn", "id", "name", "type", "updatedAt", "version") SELECT "createdAt", "destinationDockerId", "dualCerts", "exposePort", "fqdn", "id", "name", "type", "updatedAt", "version" FROM "Service"; +DROP TABLE "Service"; +ALTER TABLE "new_Service" RENAME TO "Service"; +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; diff --git a/apps/api/prisma/migrations/20221026115123_service_persistent_unique/migration.sql b/apps/api/prisma/migrations/20221026115123_service_persistent_unique/migration.sql new file mode 100644 index 000000000..dae80a76a --- /dev/null +++ b/apps/api/prisma/migrations/20221026115123_service_persistent_unique/migration.sql @@ -0,0 +1,11 @@ +/* + Warnings: + + - A unique constraint covering the columns `[serviceId,containerId,path]` on the table `ServicePersistentStorage` will be added. If there are existing duplicate values, this will fail. + +*/ +-- DropIndex +DROP INDEX "ServicePersistentStorage_serviceId_path_key"; + +-- CreateIndex +CREATE UNIQUE INDEX "ServicePersistentStorage_serviceId_containerId_path_key" ON "ServicePersistentStorage"("serviceId", "containerId", "path"); diff --git a/apps/api/prisma/migrations/20221028074301_wordpress_optional_fields/migration.sql b/apps/api/prisma/migrations/20221028074301_wordpress_optional_fields/migration.sql new file mode 100644 index 000000000..217039d97 --- /dev/null +++ b/apps/api/prisma/migrations/20221028074301_wordpress_optional_fields/migration.sql @@ -0,0 +1,32 @@ +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_Wordpress" ( + "id" TEXT NOT NULL PRIMARY KEY, + "extraConfig" TEXT, + "tablePrefix" TEXT, + "ownMysql" BOOLEAN NOT NULL DEFAULT false, + "mysqlHost" TEXT, + "mysqlPort" INTEGER, + "mysqlUser" TEXT, + "mysqlPassword" TEXT, + "mysqlRootUser" TEXT, + "mysqlRootUserPassword" TEXT, + "mysqlDatabase" TEXT, + "mysqlPublicPort" INTEGER, + "ftpEnabled" BOOLEAN NOT NULL DEFAULT false, + "ftpUser" TEXT, + "ftpPassword" TEXT, + "ftpPublicPort" INTEGER, + "ftpHostKey" TEXT, + "ftpHostKeyPrivate" TEXT, + "serviceId" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "Wordpress_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); +INSERT INTO "new_Wordpress" ("createdAt", "extraConfig", "ftpEnabled", "ftpHostKey", "ftpHostKeyPrivate", "ftpPassword", "ftpPublicPort", "ftpUser", "id", "mysqlDatabase", "mysqlHost", "mysqlPassword", "mysqlPort", "mysqlPublicPort", "mysqlRootUser", "mysqlRootUserPassword", "mysqlUser", "ownMysql", "serviceId", "tablePrefix", "updatedAt") SELECT "createdAt", "extraConfig", "ftpEnabled", "ftpHostKey", "ftpHostKeyPrivate", "ftpPassword", "ftpPublicPort", "ftpUser", "id", "mysqlDatabase", "mysqlHost", "mysqlPassword", "mysqlPort", "mysqlPublicPort", "mysqlRootUser", "mysqlRootUserPassword", "mysqlUser", "ownMysql", "serviceId", "tablePrefix", "updatedAt" FROM "Wordpress"; +DROP TABLE "Wordpress"; +ALTER TABLE "new_Wordpress" RENAME TO "Wordpress"; +CREATE UNIQUE INDEX "Wordpress_serviceId_key" ON "Wordpress"("serviceId"); +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; diff --git a/apps/api/prisma/migrations/20221104092223_default_redirect_proxy/migration.sql b/apps/api/prisma/migrations/20221104092223_default_redirect_proxy/migration.sql new file mode 100644 index 000000000..8584fe25c --- /dev/null +++ b/apps/api/prisma/migrations/20221104092223_default_redirect_proxy/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Setting" ADD COLUMN "proxyDefaultRedirect" TEXT; diff --git a/apps/api/prisma/migrations/20221114093217_application_storage_path_migration/migration.sql b/apps/api/prisma/migrations/20221114093217_application_storage_path_migration/migration.sql new file mode 100644 index 000000000..6913c8b00 --- /dev/null +++ b/apps/api/prisma/migrations/20221114093217_application_storage_path_migration/migration.sql @@ -0,0 +1,45 @@ +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_Setting" ( + "id" TEXT NOT NULL PRIMARY KEY, + "fqdn" TEXT, + "isAPIDebuggingEnabled" BOOLEAN DEFAULT false, + "isRegistrationEnabled" BOOLEAN NOT NULL DEFAULT false, + "dualCerts" BOOLEAN NOT NULL DEFAULT false, + "minPort" INTEGER NOT NULL DEFAULT 9000, + "maxPort" INTEGER NOT NULL DEFAULT 9100, + "proxyPassword" TEXT NOT NULL, + "proxyUser" TEXT NOT NULL, + "proxyHash" TEXT, + "proxyDefaultRedirect" TEXT, + "isAutoUpdateEnabled" BOOLEAN NOT NULL DEFAULT false, + "isDNSCheckEnabled" BOOLEAN NOT NULL DEFAULT true, + "DNSServers" TEXT, + "isTraefikUsed" BOOLEAN NOT NULL DEFAULT true, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "ipv4" TEXT, + "ipv6" TEXT, + "arch" TEXT, + "concurrentBuilds" INTEGER NOT NULL DEFAULT 1, + "applicationStoragePathMigrationFinished" BOOLEAN NOT NULL DEFAULT false +); +INSERT INTO "new_Setting" ("DNSServers", "arch", "concurrentBuilds", "createdAt", "dualCerts", "fqdn", "id", "ipv4", "ipv6", "isAPIDebuggingEnabled", "isAutoUpdateEnabled", "isDNSCheckEnabled", "isRegistrationEnabled", "isTraefikUsed", "maxPort", "minPort", "proxyDefaultRedirect", "proxyHash", "proxyPassword", "proxyUser", "updatedAt") SELECT "DNSServers", "arch", "concurrentBuilds", "createdAt", "dualCerts", "fqdn", "id", "ipv4", "ipv6", "isAPIDebuggingEnabled", "isAutoUpdateEnabled", "isDNSCheckEnabled", "isRegistrationEnabled", "isTraefikUsed", "maxPort", "minPort", "proxyDefaultRedirect", "proxyHash", "proxyPassword", "proxyUser", "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_ApplicationPersistentStorage" ( + "id" TEXT NOT NULL PRIMARY KEY, + "applicationId" TEXT NOT NULL, + "path" TEXT NOT NULL, + "oldPath" BOOLEAN NOT NULL DEFAULT false, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "ApplicationPersistentStorage_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "Application" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); +INSERT INTO "new_ApplicationPersistentStorage" ("applicationId", "createdAt", "id", "path", "updatedAt") SELECT "applicationId", "createdAt", "id", "path", "updatedAt" FROM "ApplicationPersistentStorage"; +DROP TABLE "ApplicationPersistentStorage"; +ALTER TABLE "new_ApplicationPersistentStorage" RENAME TO "ApplicationPersistentStorage"; +CREATE UNIQUE INDEX "ApplicationPersistentStorage_applicationId_path_key" ON "ApplicationPersistentStorage"("applicationId", "path"); +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; 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/schema.prisma b/apps/api/prisma/schema.prisma index 19e2b3481..9d5485deb 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -19,26 +19,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? - isAutoUpdateEnabled Boolean @default(false) - isDNSCheckEnabled Boolean @default(true) - DNSServers String? - isTraefikUsed Boolean @default(true) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - ipv4 String? - ipv6 String? - arch String? - concurrentBuilds Int @default(1) + id String @id @default(cuid()) + fqdn String? @unique + dualCerts Boolean @default(false) + minPort Int @default(9000) + maxPort Int @default(9100) + 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) + proxyDefaultRedirect String? + doNotTrack Boolean @default(false) + sentryDSN String? + 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 { @@ -81,6 +82,7 @@ model Team { service Service[] users User[] certificate Certificate[] + dockerRegistry DockerRegistry[] } model TeamInvitation { @@ -135,6 +137,8 @@ model Application { teams Team[] connectedDatabase ApplicationConnectedDatabase? previewApplication PreviewApplication[] + dockerRegistryId String @default("0") + dockerRegistry DockerRegistry @relation(fields: [dockerRegistryId], references: [id]) } model PreviewApplication { @@ -186,6 +190,7 @@ model ApplicationPersistentStorage { id String @id @default(cuid()) applicationId String path String + oldPath Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt application Application @relation(fields: [applicationId], references: [id]) @@ -194,14 +199,17 @@ model ApplicationPersistentStorage { } model ServicePersistentStorage { - id String @id @default(cuid()) - serviceId String - path String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - service Service @relation(fields: [serviceId], references: [id]) + id String @id @default(cuid()) + serviceId String + path String + volumeName String? + predefined Boolean @default(false) + containerId String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + service Service @relation(fields: [serviceId], references: [id]) - @@unique([serviceId, path]) + @@unique([serviceId, containerId, path]) } model Secret { @@ -291,6 +299,20 @@ model SshKey { destinationDocker DestinationDocker[] } +model DockerRegistry { + id String @id @default(cuid()) + name String + url String + username String? + password String? + isSystemWide Boolean @default(false) + 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 @@ -393,12 +415,14 @@ model Service { dualCerts Boolean @default(false) type String? version String? + templateVersion String @default("0.0.0") destinationDockerId String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id]) persistentStorage ServicePersistentStorage[] serviceSecret ServiceSecret[] + serviceSetting ServiceSetting[] teams Team[] fider Fider? @@ -418,6 +442,19 @@ model Service { taiga Taiga? } +model ServiceSetting { + id String @id @default(cuid()) + serviceId String + name String + value String + variableName String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + service Service @relation(fields: [serviceId], references: [id]) + + @@unique([serviceId, name]) +} + model PlausibleAnalytics { id String @id @default(cuid()) email String? @@ -463,10 +500,10 @@ model Wordpress { ownMysql Boolean @default(false) mysqlHost String? mysqlPort Int? - mysqlUser String - mysqlPassword String - mysqlRootUser String - mysqlRootUserPassword String + mysqlUser String? + mysqlPassword String? + mysqlRootUser String? + mysqlRootUserPassword String? mysqlDatabase String? mysqlPublicPort Int? ftpEnabled Boolean @default(false) @@ -606,8 +643,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..93fcb477b 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: { @@ -104,6 +90,11 @@ async function main() { } } } + // Add default docker registry (dockerhub) + const registries = await prisma.dockerRegistry.findMany() + if (registries.length === 0) { + await prisma.dockerRegistry.create({ data: { id: "0", name: 'Docker Hub', url: 'https://index.docker.io/v1/', isSystemWide: true } }) + } } main() .catch((e) => { diff --git a/apps/api/scripts/generateTags.mjs b/apps/api/scripts/generateTags.mjs new file mode 100644 index 000000000..fc2c88540 --- /dev/null +++ b/apps/api/scripts/generateTags.mjs @@ -0,0 +1,67 @@ +import fs from 'fs/promises'; +import yaml from 'js-yaml'; +import got from 'got'; + +const repositories = []; +const templates = await fs.readFile('./apps/api/devTemplates.yaml', 'utf8'); +const devTemplates = yaml.load(templates); +for (const template of devTemplates) { + let image = template.services['$$id'].image.replaceAll(':$$core_version', ''); + if (!image.includes('/')) { + image = `library/${image}`; + } + repositories.push({ image, name: template.type }); +} +const services = [] +const numberOfTags = 30; +// const semverRegex = new RegExp(/^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/g) +for (const repository of repositories) { + console.log('Querying', repository.name, 'at', repository.image); + let semverRegex = new RegExp(/^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/g) + if (repository.name.startsWith('wordpress')) { + semverRegex = new RegExp(/^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)-php(0|[1-9]\d*)$/g) + } + if (repository.name.startsWith('minio')) { + semverRegex = new RegExp(/^RELEASE.*$/g) + } + if (repository.name.startsWith('fider')) { + semverRegex = new RegExp(/^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)-([0-9]+)$/g) + } + if (repository.name.startsWith('searxng')) { + semverRegex = new RegExp(/^\d{4}[\.\-](0?[1-9]|[12][0-9]|3[01])[\.\-](0?[1-9]|1[012]).*$/) + } + if (repository.name.startsWith('umami')) { + semverRegex = new RegExp(/^postgresql-v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)-([0-9]+)$/g) + } + if (repository.image.includes('ghcr.io')) { + const { execaCommand } = await import('execa'); + const { stdout } = await execaCommand(`docker run --rm quay.io/skopeo/stable list-tags docker://${repository.image}`); + if (stdout) { + const json = JSON.parse(stdout); + const semverTags = json.Tags.filter((tag) => semverRegex.test(tag)) + let tags = semverTags.length > 10 ? semverTags.sort().reverse().slice(0, numberOfTags) : json.Tags.sort().reverse().slice(0, numberOfTags) + if (!tags.includes('latest')) { + tags.push('latest') + } + services.push({ name: repository.name, image: repository.image, tags }) + } + } else { + const { token } = await got.get(`https://auth.docker.io/token?service=registry.docker.io&scope=repository:${repository.image}:pull`).json() + let data = await got.get(`https://registry-1.docker.io/v2/${repository.image}/tags/list`, { + headers: { + Authorization: `Bearer ${token}` + } + }).json() + const semverTags = data.tags.filter((tag) => semverRegex.test(tag)) + let tags = semverTags.length > 10 ? semverTags.sort().reverse().slice(0, numberOfTags) : data.tags.sort().reverse().slice(0, numberOfTags) + if (!tags.includes('latest')) { + tags.push('latest') + } + services.push({ + name: repository.name, + image: repository.image, + tags + }) + } +} +await fs.writeFile('./apps/api/devTags.json', JSON.stringify(services)); \ No newline at end of file diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index 21185a6d4..0303cdec9 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -6,21 +6,26 @@ import cookie from '@fastify/cookie'; import multipart from '@fastify/multipart'; import path, { join } from 'path'; import autoLoad from '@fastify/autoload'; -import { asyncExecShell, cleanupDockerStorage, createRemoteEngineConfiguration, decrypt, executeDockerCmd, executeSSHCmd, generateDatabaseConfiguration, getDomain, isDev, listSettings, prisma, startTraefikProxy, startTraefikTCPProxy, version } from './lib/common'; +import socketIO from 'fastify-socket.io' +import socketIOServer from './realtime' + +import { asyncExecShell, cleanupDockerStorage, createRemoteEngineConfiguration, decrypt, executeDockerCmd, executeSSHCmd, 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' -import axios from 'axios'; +import yaml from 'js-yaml' import fs from 'fs/promises'; import { verifyRemoteDockerEngineFn } from './routes/api/v1/destinations/handlers'; 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, @@ -31,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({ @@ -52,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' @@ -103,27 +105,40 @@ const host = '0.0.0.0'; }); fastify.register(cookie) fastify.register(cors); - fastify.addHook('onRequest', async (request, reply) => { - let allowedList = ['coolify:3000']; - const { ipv4, ipv6, fqdn } = await prisma.setting.findFirst({}) - - ipv4 && allowedList.push(`${ipv4}:3000`); - ipv6 && allowedList.push(ipv6); - fqdn && allowedList.push(getDomain(fqdn)); - isDev && allowedList.push('localhost:3000') && allowedList.push('localhost:3001') && allowedList.push('host.docker.internal:3001'); - const remotes = await prisma.destinationDocker.findMany({ where: { remoteEngine: true, remoteVerified: true } }) - if (remotes.length > 0) { - remotes.forEach(remote => { - allowedList.push(`${remote.remoteIpAddress}:3000`); - }) - } - if (!allowedList.includes(request.headers.host)) { - // console.log('not allowed', request.headers.host) + fastify.register(socketIO, { + cors: { + origin: isDev ? "*" : '' } }) + // To detect allowed origins + // fastify.addHook('onRequest', async (request, reply) => { + // console.log(request.headers.host) + // let allowedList = ['coolify:3000']; + // const { ipv4, ipv6, fqdn } = await prisma.setting.findFirst({}) + + // ipv4 && allowedList.push(`${ipv4}:3000`); + // ipv6 && allowedList.push(ipv6); + // fqdn && allowedList.push(getDomain(fqdn)); + // isDev && allowedList.push('localhost:3000') && allowedList.push('localhost:3001') && allowedList.push('host.docker.internal:3001'); + // const remotes = await prisma.destinationDocker.findMany({ where: { remoteEngine: true, remoteVerified: true } }) + // if (remotes.length > 0) { + // remotes.forEach(remote => { + // allowedList.push(`${remote.remoteIpAddress}:3000`); + // }) + // } + // if (!allowedList.includes(request.headers.host)) { + // // console.log('not allowed', request.headers.host) + // } + // }) + + try { await fastify.listen({ port, host }) + await socketIOServer(fastify) console.log(`Coolify's API is listening on ${host}:${port}`); + + migrateServicesToNewTemplate(); + await migrateApplicationPersistentStorage(); await initServer(); const graceful = new Graceful({ brees: [scheduler] }); @@ -145,21 +160,36 @@ const host = '0.0.0.0'; await cleanupStorage() }, 60000 * 10) - // checkProxies and checkFluentBit + // checkProxies, checkFluentBit & refresh templates setInterval(async () => { await checkProxies(); await checkFluentBit(); - }, 10000) + }, 60000) + + // Refresh and check templates + setInterval(async () => { + await refreshTemplates() + }, 60000) + + setInterval(async () => { + await refreshTags() + }, 60000) + + setInterval(async () => { + await migrateServicesToNewTemplate() + }, isDev ? 10000 : 60000) setInterval(async () => { await copySSLCertificates(); - }, 2000) + }, 10000) await Promise.all([ + getTagsTemplates(), getArch(), getIPAddress(), configureRemoteDockers(), ]) + } catch (error) { console.error(error); process.exit(1); @@ -172,31 +202,82 @@ 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 } }) } } catch (error) { } } -async function initServer() { +async function getTagsTemplates() { + const { default: got } = await import('got') try { - console.log(`Initializing server...`); + if (isDev) { + const templates = await fs.readFile('./devTemplates.yaml', 'utf8') + 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('[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('[004] Tags and templates loaded...') + } + + } catch (error) { + console.log("Couldn't get latest templates.") + console.log(error) + } +} +async function initServer() { + const appId = process.env['COOLIFY_APP_ID']; + const settings = await prisma.setting.findUnique({ where: { id: '0' } }) + try { + 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 asyncExecShell(`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() { try { const settings = await prisma.setting.findFirst({}) @@ -226,17 +307,15 @@ async function configureRemoteDockers() { async function autoUpdater() { try { + const { default: got } = await import('got') const currentVersion = version; - const { data: versions } = await axios - .get( - `https://get.coollabs.io/versions.json` - , { - params: { - appId: process.env['COOLIFY_APP_ID'] || undefined, - version: currentVersion - } - }) - const latestVersion = versions['coolify'].main.version; + const { coolify } = await got.get('https://get.coollabs.io/versions.json', { + searchParams: { + appId: process.env['COOLIFY_APP_ID'] || undefined, + version: currentVersion + } + }).json() + const latestVersion = coolify.main.version; const isUpdateAvailable = compareVersions(latestVersion, currentVersion); if (isUpdateAvailable === 1) { const activeCount = 0 @@ -258,7 +337,9 @@ async function autoUpdater() { } } } - } catch (error) { } + } catch (error) { + console.log(error) + } } async function checkFluentBit() { @@ -338,17 +419,17 @@ async function checkProxies() { } // HTTP Proxies - const minioInstances = await prisma.minio.findMany({ - where: { publicPort: { not: null } }, - include: { service: { include: { destinationDocker: true } } } - }); - for (const minio of minioInstances) { - const { service, publicPort } = minio; - const { destinationDockerId, destinationDocker, id } = service; - if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) { - await startTraefikTCPProxy(destinationDocker, id, publicPort, 9000); - } - } + // const minioInstances = await prisma.minio.findMany({ + // where: { publicPort: { not: null } }, + // include: { service: { include: { destinationDocker: true } } } + // }); + // for (const minio of minioInstances) { + // const { service, publicPort } = minio; + // const { destinationDockerId, destinationDocker, id } = service; + // if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) { + // await startTraefikTCPProxy(destinationDocker, id, publicPort, 9000); + // } + // } } catch (error) { } diff --git a/apps/api/src/jobs/deployApplication.ts b/apps/api/src/jobs/deployApplication.ts index f31c54967..09e92543a 100644 --- a/apps/api/src/jobs/deployApplication.ts +++ b/apps/api/src/jobs/deployApplication.ts @@ -4,7 +4,7 @@ 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 { createDirectories, decrypt, defaultComposeConfiguration, executeDockerCmd, getDomain, prisma, decryptApplication, isDev } from '../lib/common'; import * as importers from '../lib/importers'; import * as buildpacks from '../lib/buildPacks'; @@ -38,57 +38,71 @@ 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 + application = decryptApplication(application) + 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, + } = application + + let { + branch, + repository, + buildPack, + port, + installCommand, + buildCommand, + startCommand, + baseDirectory, + publishDirectory, + dockerFileLocation, + dockerComposeFileLocation, + dockerComposeConfiguration, + denoMainFile + } = application + + let imageId = applicationId; + let domain = getDomain(fqdn); + 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, - gitCommitHash, - 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( @@ -114,25 +128,18 @@ import * as buildpacks from '../lib/buildPacks'; ) .digest('hex'); const { debug } = settings; - let imageId = applicationId; - let domain = getDomain(fqdn); const volumes = persistentStorage?.map((storage) => { - return `${applicationId}${storage.path.replace(/\//gi, '-')}:${buildPack !== 'docker' ? '/app' : '' - }${storage.path}`; + if (storage.oldPath) { + 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) } catch (error) { } - let deployNeeded = true; let destinationType; @@ -141,7 +148,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 +159,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, @@ -262,6 +270,7 @@ import * as buildpacks from '../lib/buildPacks'; pythonVariable, dockerFileLocation, dockerComposeConfiguration, + dockerComposeFileLocation, denoMainFile, denoOptions, baseImage, @@ -316,11 +325,11 @@ import * as buildpacks from '../lib/buildPacks'; try { await executeDockerCmd({ dockerId: destinationDockerId, - command: `docker ps -a --filter 'label=com.docker.compose.service=${applicationId}' --format {{.ID}}|xargs -r -n 1 docker stop -t 0` + 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=${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}}|xargs -r -n 1 docker rm --force` }) } catch (error) { // @@ -421,6 +430,10 @@ import * as buildpacks from '../lib/buildPacks'; if (error !== 1) { await saveBuildLog({ line: error, buildId, applicationId: application.id }); } + } finally { + if (!isDev) { + await fs.rm(workdir, { recursive: true, force: true }); + } } }); } diff --git a/apps/api/src/lib.ts b/apps/api/src/lib.ts new file mode 100644 index 000000000..ad95c35c9 --- /dev/null +++ b/apps/api/src/lib.ts @@ -0,0 +1,531 @@ +import cuid from "cuid"; +import { decrypt, encrypt, fixType, generatePassword, generateToken, prisma } from "./lib/common"; +import { getTemplates } from "./lib/services"; + +export async function migrateApplicationPersistentStorage() { + const settings = await prisma.setting.findFirst() + if (settings) { + const { id: settingsId, applicationStoragePathMigrationFinished } = settings + try { + if (!applicationStoragePathMigrationFinished) { + const applications = await prisma.application.findMany({ include: { persistentStorage: true } }); + for (const application of applications) { + if (application.persistentStorage && application.persistentStorage.length > 0 && application?.buildPack !== 'docker') { + for (const storage of application.persistentStorage) { + let { id, path } = storage + if (!path.startsWith('/app')) { + path = `/app${path}` + await prisma.applicationPersistentStorage.update({ where: { id }, data: { path, oldPath: true } }) + } + } + } + } + } + } catch (error) { + console.log(error) + } finally { + await prisma.setting.update({ where: { id: settingsId }, data: { applicationStoragePathMigrationFinished: true } }) + } + } +} +export async function migrateServicesToNewTemplate() { + // This function migrates old hardcoded services to the new template based services + try { + let templates = await getTemplates() + const services: any = await prisma.service.findMany({ + include: { + destinationDocker: true, + persistentStorage: true, + serviceSecret: true, + serviceSetting: true, + minio: true, + plausibleAnalytics: true, + vscodeserver: true, + wordpress: true, + ghost: true, + meiliSearch: true, + umami: true, + hasura: true, + fider: true, + moodle: true, + appwrite: true, + glitchTip: true, + searxng: true, + weblate: true, + taiga: true, + } + }) + for (const service of services) { + try { + const { id } = service + if (!service.type) { + continue; + } + let template = templates.find(t => fixType(t.type) === fixType(service.type)); + if (template) { + template = JSON.parse(JSON.stringify(template).replaceAll('$$id', service.id)) + if (service.type === 'plausibleanalytics' && service.plausibleAnalytics) await plausibleAnalytics(service, template) + if (service.type === 'fider' && service.fider) await fider(service, template) + if (service.type === 'minio' && service.minio) await minio(service, template) + if (service.type === 'vscodeserver' && service.vscodeserver) await vscodeserver(service, template) + if (service.type === 'wordpress' && service.wordpress) await wordpress(service, template) + if (service.type === 'ghost' && service.ghost) await ghost(service, template) + if (service.type === 'meilisearch' && service.meiliSearch) await meilisearch(service, template) + if (service.type === 'umami' && service.umami) await umami(service, template) + if (service.type === 'hasura' && service.hasura) await hasura(service, template) + if (service.type === 'glitchTip' && service.glitchTip) await glitchtip(service, template) + if (service.type === 'searxng' && service.searxng) await searxng(service, template) + if (service.type === 'weblate' && service.weblate) await weblate(service, template) + if (service.type === 'appwrite' && service.appwrite) await appwrite(service, template) + + try { + await createVolumes(service, template); + } catch (error) { + console.log(error) + } + if (template.variables) { + if (template.variables.length > 0) { + for (const variable of template.variables) { + const { defaultValue } = variable; + const regex = /^\$\$.*\((\d+)\)$/g; + const length = Number(regex.exec(defaultValue)?.[1]) || undefined + if (variable.defaultValue.startsWith('$$generate_password')) { + variable.value = generatePassword({ length }); + } else if (variable.defaultValue.startsWith('$$generate_hex')) { + variable.value = generatePassword({ length, isHex: true }); + } else if (variable.defaultValue.startsWith('$$generate_username')) { + variable.value = cuid(); + } else if (variable.defaultValue.startsWith('$$generate_token')) { + variable.value = generateToken() + } else { + variable.value = variable.defaultValue || ''; + } + } + } + for (const variable of template.variables) { + if (variable.id.startsWith('$$secret_')) { + const found = await prisma.serviceSecret.findFirst({ where: { name: variable.name, serviceId: id } }) + if (!found) { + await prisma.serviceSecret.create({ + data: { name: variable.name, value: encrypt(variable.value) || '', service: { connect: { id } } } + }) + } + + } + if (variable.id.startsWith('$$config_')) { + const found = await prisma.serviceSetting.findFirst({ where: { name: variable.name, serviceId: id } }) + if (!found) { + await prisma.serviceSetting.create({ + data: { name: variable.name, value: variable.value.toString(), variableName: variable.id, service: { connect: { id } } } + }) + } + } + } + } + for (const s of Object.keys(template.services)) { + if (service.type === 'plausibleanalytics') { + continue; + } + if (template.services[s].volumes) { + for (const volume of template.services[s].volumes) { + const [volumeName, path] = volume.split(':') + if (!volumeName.startsWith('/')) { + const found = await prisma.servicePersistentStorage.findFirst({ where: { volumeName, serviceId: id } }) + if (!found) { + await prisma.servicePersistentStorage.create({ + data: { volumeName, path, containerId: s, predefined: true, service: { connect: { id } } } + }); + } + } + } + } + } + await prisma.service.update({ where: { id }, data: { templateVersion: template.templateVersion } }) + } + } catch (error) { + console.log(error) + } + } + } catch (error) { + console.log(error) + + } +} +async function appwrite(service: any, template: any) { + const { opensslKeyV1, executorSecret, mariadbHost, mariadbPort, mariadbUser, mariadbPassword, mariadbRootUserPassword, mariadbDatabase } = service.appwrite + + const secrets = [ + `_APP_EXECUTOR_SECRET@@@${executorSecret}`, + `_APP_OPENSSL_KEY_V1@@@${opensslKeyV1}`, + `_APP_DB_PASS@@@${mariadbPassword}`, + `_APP_DB_ROOT_PASS@@@${mariadbRootUserPassword}`, + ] + + const settings = [ + `_APP_DB_HOST@@@${mariadbHost}`, + `_APP_DB_PORT@@@${mariadbPort}`, + `_APP_DB_USER@@@${mariadbUser}`, + `_APP_DB_SCHEMA@@@${mariadbDatabase}`, + ] + await migrateSecrets(secrets, service); + await migrateSettings(settings, service, template); + + // Disconnect old service data + // await prisma.service.update({ where: { id: service.id }, data: { appwrite: { disconnect: true } } }) +} +async function weblate(service: any, template: any) { + const { adminPassword, postgresqlUser, postgresqlPassword, postgresqlDatabase } = service.weblate + + const secrets = [ + `WEBLATE_ADMIN_PASSWORD@@@${adminPassword}`, + `POSTGRES_PASSWORD@@@${postgresqlPassword}`, + ] + + const settings = [ + `WEBLATE_SITE_DOMAIN@@@$$generate_domain`, + `POSTGRES_USER@@@${postgresqlUser}`, + `POSTGRES_DATABASE@@@${postgresqlDatabase}`, + `POSTGRES_DB@@@${postgresqlDatabase}`, + `POSTGRES_HOST@@@$$id-postgres`, + `POSTGRES_PORT@@@5432`, + `REDIS_HOST@@@$$id-redis`, + ] + await migrateSettings(settings, service, template); + await migrateSecrets(secrets, service); + + // Disconnect old service data + // await prisma.service.update({ where: { id: service.id }, data: { weblate: { disconnect: true } } }) +} +async function searxng(service: any, template: any) { + const { secretKey, redisPassword } = service.searxng + + const secrets = [ + `SECRET_KEY@@@${secretKey}`, + `REDIS_PASSWORD@@@${redisPassword}`, + ] + + const settings = [ + `SEARXNG_BASE_URL@@@$$generate_fqdn` + ] + await migrateSettings(settings, service, template); + await migrateSecrets(secrets, service); + + // Disconnect old service data + // await prisma.service.update({ where: { id: service.id }, data: { searxng: { disconnect: true } } }) +} +async function glitchtip(service: any, template: any) { + const { postgresqlUser, postgresqlPassword, postgresqlDatabase, secretKeyBase, defaultEmail, defaultUsername, defaultPassword, defaultEmailFrom, emailSmtpHost, emailSmtpPort, emailSmtpUser, emailSmtpPassword, emailSmtpUseTls, emailSmtpUseSsl, emailBackend, mailgunApiKey, sendgridApiKey, enableOpenUserRegistration } = service.glitchTip + const { id } = service + + const secrets = [ + `POSTGRES_PASSWORD@@@${postgresqlPassword}`, + `SECRET_KEY@@@${secretKeyBase}`, + `MAILGUN_API_KEY@@@${mailgunApiKey}`, + `SENDGRID_API_KEY@@@${sendgridApiKey}`, + `DJANGO_SUPERUSER_PASSWORD@@@${defaultPassword}`, + emailSmtpUser && emailSmtpPassword && emailSmtpHost && emailSmtpPort && `EMAIL_URL@@@${encrypt(`smtp://${emailSmtpUser}:${decrypt(emailSmtpPassword)}@${emailSmtpHost}:${emailSmtpPort}`)} || ''`, + `DATABASE_URL@@@${encrypt(`postgres://${postgresqlUser}:${decrypt(postgresqlPassword)}@${id}-postgresql:5432/${postgresqlDatabase}`)}`, + `REDIS_URL@@@${encrypt(`redis://${id}-redis:6379`)}` + ] + const settings = [ + `POSTGRES_USER@@@${postgresqlUser}`, + `POSTGRES_DB@@@${postgresqlDatabase}`, + `DEFAULT_FROM_EMAIL@@@${defaultEmailFrom}`, + `EMAIL_USE_TLS@@@${emailSmtpUseTls}`, + `EMAIL_USE_SSL@@@${emailSmtpUseSsl}`, + `EMAIL_BACKEND@@@${emailBackend}`, + `ENABLE_OPEN_USER_REGISTRATION@@@${enableOpenUserRegistration}`, + `DJANGO_SUPERUSER_EMAIL@@@${defaultEmail}`, + `DJANGO_SUPERUSER_USERNAME@@@${defaultUsername}`, + ] + await migrateSettings(settings, service, template); + await migrateSecrets(secrets, service); + + await prisma.service.update({ where: { id: service.id }, data: { type: 'glitchtip' } }) + // Disconnect old service data + // await prisma.service.update({ where: { id: service.id }, data: { glitchTip: { disconnect: true } } }) +} +async function hasura(service: any, template: any) { + const { postgresqlUser, postgresqlPassword, postgresqlDatabase, graphQLAdminPassword } = service.hasura + const { id } = service + + const secrets = [ + `HASURA_GRAPHQL_ADMIN_SECRET@@@${graphQLAdminPassword}`, + `HASURA_GRAPHQL_METADATA_DATABASE_URL@@@${encrypt(`postgres://${postgresqlUser}:${decrypt(postgresqlPassword)}@${id}-postgresql:5432/${postgresqlDatabase}`)}`, + `POSTGRES_PASSWORD@@@${postgresqlPassword}`, + ] + const settings = [ + `POSTGRES_USER@@@${postgresqlUser}`, + `POSTGRES_DB@@@${postgresqlDatabase}`, + ] + await migrateSettings(settings, service, template); + await migrateSecrets(secrets, service); + + // Disconnect old service data + // await prisma.service.update({ where: { id: service.id }, data: { hasura: { disconnect: true } } }) +} +async function umami(service: any, template: any) { + const { postgresqlUser, postgresqlPassword, postgresqlDatabase, umamiAdminPassword, hashSalt } = service.umami + const { id } = service + const secrets = [ + `HASH_SALT@@@${hashSalt}`, + `POSTGRES_PASSWORD@@@${postgresqlPassword}`, + `ADMIN_PASSWORD@@@${umamiAdminPassword}`, + `DATABASE_URL@@@${encrypt(`postgres://${postgresqlUser}:${decrypt(postgresqlPassword)}@${id}-postgresql:5432/${postgresqlDatabase}`)}`, + ] + const settings = [ + `DATABASE_TYPE@@@postgresql`, + `POSTGRES_USER@@@${postgresqlUser}`, + `POSTGRES_DB@@@${postgresqlDatabase}`, + ] + await migrateSettings(settings, service, template); + await migrateSecrets(secrets, service); + + await prisma.service.update({ where: { id: service.id }, data: { type: "umami-postgresql" } }) + + // Disconnect old service data + // await prisma.service.update({ where: { id: service.id }, data: { umami: { disconnect: true } } }) +} +async function meilisearch(service: any, template: any) { + const { masterKey } = service.meiliSearch + + const secrets = [ + `MEILI_MASTER_KEY@@@${masterKey}`, + ] + + // await migrateSettings(settings, service, template); + await migrateSecrets(secrets, service); + + // Disconnect old service data + // await prisma.service.update({ where: { id: service.id }, data: { meiliSearch: { disconnect: true } } }) +} +async function ghost(service: any, template: any) { + const { defaultEmail, defaultPassword, mariadbUser, mariadbPassword, mariadbRootUser, mariadbRootUserPassword, mariadbDatabase } = service.ghost + const { fqdn } = service + + const isHttps = fqdn.startsWith('https://'); + + const secrets = [ + `GHOST_PASSWORD@@@${defaultPassword}`, + `MARIADB_PASSWORD@@@${mariadbPassword}`, + `MARIADB_ROOT_PASSWORD@@@${mariadbRootUserPassword}`, + `GHOST_DATABASE_PASSWORD@@@${mariadbPassword}`, + ] + const settings = [ + `GHOST_EMAIL@@@${defaultEmail}`, + `GHOST_DATABASE_HOST@@@${service.id}-mariadb`, + `GHOST_DATABASE_USER@@@${mariadbUser}`, + `GHOST_DATABASE_NAME@@@${mariadbDatabase}`, + `GHOST_DATABASE_PORT_NUMBER@@@3306`, + `MARIADB_USER@@@${mariadbUser}`, + `MARIADB_DATABASE@@@${mariadbDatabase}`, + `MARIADB_ROOT_USER@@@${mariadbRootUser}`, + `GHOST_HOST@@@$$generate_domain`, + `url@@@$$generate_fqdn`, + `GHOST_ENABLE_HTTPS@@@${isHttps ? 'yes' : 'no'}` + ] + await migrateSettings(settings, service, template); + await migrateSecrets(secrets, service); + + await prisma.service.update({ where: { id: service.id }, data: { type: "ghost-mariadb" } }) + + // Disconnect old service data + // await prisma.service.update({ where: { id: service.id }, data: { ghost: { disconnect: true } } }) +} +async function wordpress(service: any, template: any) { + const { extraConfig, tablePrefix, ownMysql, mysqlHost, mysqlPort, mysqlUser, mysqlPassword, mysqlRootUser, mysqlRootUserPassword, mysqlDatabase, ftpEnabled, ftpUser, ftpPassword, ftpPublicPort, ftpHostKey, ftpHostKeyPrivate } = service.wordpress + + let settings = [] + let secrets = [] + if (ownMysql) { + secrets = [ + `WORDPRESS_DB_PASSWORD@@@${mysqlPassword}`, + ftpPassword && `COOLIFY_FTP_PASSWORD@@@${ftpPassword}`, + ftpHostKeyPrivate && `COOLIFY_FTP_HOST_KEY_PRIVATE@@@${ftpHostKeyPrivate}`, + ftpHostKey && `COOLIFY_FTP_HOST_KEY@@@${ftpHostKey}`, + ] + settings = [ + `WORDPRESS_CONFIG_EXTRA@@@${extraConfig}`, + `WORDPRESS_DB_HOST@@@${mysqlHost}`, + `WORDPRESS_DB_PORT@@@${mysqlPort}`, + `WORDPRESS_DB_USER@@@${mysqlUser}`, + `WORDPRESS_DB_NAME@@@${mysqlDatabase}`, + ] + } else { + secrets = [ + `MYSQL_ROOT_PASSWORD@@@${mysqlRootUserPassword}`, + `MYSQL_PASSWORD@@@${mysqlPassword}`, + ftpPassword && `COOLIFY_FTP_PASSWORD@@@${ftpPassword}`, + ftpHostKeyPrivate && `COOLIFY_FTP_HOST_KEY_PRIVATE@@@${ftpHostKeyPrivate}`, + ftpHostKey && `COOLIFY_FTP_HOST_KEY@@@${ftpHostKey}`, + ] + settings = [ + `MYSQL_ROOT_USER@@@${mysqlRootUser}`, + `MYSQL_USER@@@${mysqlUser}`, + `MYSQL_DATABASE@@@${mysqlDatabase}`, + `MYSQL_HOST@@@${service.id}-mysql`, + `MYSQL_PORT@@@${mysqlPort}`, + `WORDPRESS_CONFIG_EXTRA@@@${extraConfig}`, + `WORDPRESS_TABLE_PREFIX@@@${tablePrefix}`, + `WORDPRESS_DB_HOST@@@${service.id}-mysql`, + `COOLIFY_OWN_DB@@@${ownMysql}`, + `COOLIFY_FTP_ENABLED@@@${ftpEnabled}`, + `COOLIFY_FTP_USER@@@${ftpUser}`, + `COOLIFY_FTP_PUBLIC_PORT@@@${ftpPublicPort}`, + ] + } + + await migrateSettings(settings, service, template); + await migrateSecrets(secrets, service); + if (ownMysql) { + await prisma.service.update({ where: { id: service.id }, data: { type: "wordpress-only" } }) + } + // Disconnect old service data + // await prisma.service.update({ where: { id: service.id }, data: { wordpress: { disconnect: true } } }) +} +async function vscodeserver(service: any, template: any) { + const { password } = service.vscodeserver + + const secrets = [ + `PASSWORD@@@${password}`, + ] + await migrateSecrets(secrets, service); + + // Disconnect old service data + // await prisma.service.update({ where: { id: service.id }, data: { vscodeserver: { disconnect: true } } }) +} +async function minio(service: any, template: any) { + const { rootUser, rootUserPassword, apiFqdn } = service.minio + const secrets = [ + `MINIO_ROOT_PASSWORD@@@${rootUserPassword}`, + ] + const settings = [ + `MINIO_ROOT_USER@@@${rootUser}`, + `MINIO_SERVER_URL@@@${apiFqdn}`, + `MINIO_BROWSER_REDIRECT_URL@@@$$generate_fqdn`, + `MINIO_DOMAIN@@@$$generate_domain`, + ] + await migrateSettings(settings, service, template); + await migrateSecrets(secrets, service); + + // Disconnect old service data + // await prisma.service.update({ where: { id: service.id }, data: { minio: { disconnect: true } } }) +} +async function fider(service: any, template: any) { + const { postgresqlUser, postgresqlPassword, postgresqlDatabase, jwtSecret, emailNoreply, emailMailgunApiKey, emailMailgunDomain, emailMailgunRegion, emailSmtpHost, emailSmtpPort, emailSmtpUser, emailSmtpPassword, emailSmtpEnableStartTls } = service.fider + const { id } = service + const secrets = [ + `JWT_SECRET@@@${jwtSecret}`, + emailMailgunApiKey && `EMAIL_MAILGUN_API@@@${emailMailgunApiKey}`, + emailSmtpPassword && `EMAIL_SMTP_PASSWORD@@@${emailSmtpPassword}`, + `POSTGRES_PASSWORD@@@${postgresqlPassword}`, + `DATABASE_URL@@@${encrypt(`postgresql://${postgresqlUser}:${decrypt(postgresqlPassword)}@${id}-postgresql:5432/${postgresqlDatabase}?sslmode=disable`)}` + ] + const settings = [ + `BASE_URL@@@$$generate_fqdn`, + `EMAIL_NOREPLY@@@${emailNoreply || 'noreply@example.com'}`, + `EMAIL_MAILGUN_DOMAIN@@@${emailMailgunDomain || ''}`, + `EMAIL_MAILGUN_REGION@@@${emailMailgunRegion || ''}`, + `EMAIL_SMTP_HOST@@@${emailSmtpHost || ''}`, + `EMAIL_SMTP_PORT@@@${emailSmtpPort || 587}`, + `EMAIL_SMTP_USER@@@${emailSmtpUser || ''}`, + `EMAIL_SMTP_PASSWORD@@@${emailSmtpPassword || ''}`, + `EMAIL_SMTP_ENABLE_STARTTLS@@@${emailSmtpEnableStartTls || 'false'}`, + `POSTGRES_USER@@@${postgresqlUser}`, + `POSTGRES_DB@@@${postgresqlDatabase}`, + ] + await migrateSettings(settings, service, template); + await migrateSecrets(secrets, service); + + // Disconnect old service data + // await prisma.service.update({ where: { id: service.id }, data: { fider: { disconnect: true } } }) + +} +async function plausibleAnalytics(service: any, template: any) { + const { email, username, password, postgresqlUser, postgresqlPassword, postgresqlDatabase, secretKeyBase, scriptName } = service.plausibleAnalytics; + const { id } = service + + const settings = [ + `BASE_URL@@@$$generate_fqdn`, + `ADMIN_USER_EMAIL@@@${email}`, + `ADMIN_USER_NAME@@@${username}`, + `DISABLE_AUTH@@@false`, + `DISABLE_REGISTRATION@@@true`, + `POSTGRESQL_USERNAME@@@${postgresqlUser}`, + `POSTGRESQL_DATABASE@@@${postgresqlDatabase}`, + `SCRIPT_NAME@@@${scriptName}`, + ] + const secrets = [ + `ADMIN_USER_PWD@@@${password}`, + `SECRET_KEY_BASE@@@${secretKeyBase}`, + `POSTGRESQL_PASSWORD@@@${postgresqlPassword}`, + `DATABASE_URL@@@${encrypt(`postgres://${postgresqlUser}:${decrypt(postgresqlPassword)}@${id}-postgresql:5432/${postgresqlDatabase}`)}`, + ] + await migrateSettings(settings, service, template); + await migrateSecrets(secrets, service); + + // 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 { + if (!setting) continue; + let [name, value] = setting.split('@@@') + let minio = name + if (name === 'MINIO_SERVER_URL') { + name = 'coolify_fqdn_minio_console' + } + if (!value || value === 'null') { + continue; + } + let variableName = template.variables.find((v: any) => v.name === name)?.id + if (!variableName) { + variableName = `$$config_${name.toLowerCase()}` + } + // console.log('Migrating setting', name, value, 'for service', service.id, ', service name:', service.name, 'variableName: ', variableName) + + await prisma.serviceSetting.findFirst({ where: { name: minio, serviceId: service.id } }) || await prisma.serviceSetting.create({ data: { name: minio, value, variableName, service: { connect: { id: service.id } } } }) + } catch (error) { + console.log(error) + } + } +} +async function migrateSecrets(secrets: any[], service: any) { + for (const secret of secrets) { + try { + if (!secret) continue; + let [name, value] = secret.split('@@@') + if (!value || value === 'null') { + continue + } + // console.log('Migrating secret', name, value, 'for service', service.id, ', service name:', service.name) + await prisma.serviceSecret.findFirst({ where: { name, serviceId: service.id } }) || await prisma.serviceSecret.create({ data: { name, value, service: { connect: { id: service.id } } } }) + } catch (error) { + console.log(error) + } + } +} +async function createVolumes(service: any, template: any) { + const volumes = []; + for (const s of Object.keys(template.services)) { + if (template.services[s].volumes && template.services[s].volumes.length > 0) { + for (const volume of template.services[s].volumes) { + let volumeName = volume.split(':')[0] + const volumePath = volume.split(':')[1] + let volumeService = s + if (service.type === 'plausibleanalytics' && service.plausibleAnalytics?.id) { + let volumeId = volumeName.split('-')[0] + volumeName = volumeName.replace(volumeId, service.plausibleAnalytics.id) + } + volumes.push(`${volumeName}@@@${volumePath}@@@${volumeService}`) + } + } + } + for (const volume of volumes) { + const [volumeName, path, containerId] = volume.split('@@@') + // 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 fd36a2ea6..c8c383564 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, executeDockerCmd, generateTimestamp, getDomain, isDev, prisma, version } from "../common"; import { promises as fs } from 'fs'; import { day } from "../dayjs"; @@ -363,6 +363,7 @@ export const setDefaultConfiguration = async (data: any) => { publishDirectory, baseDirectory, dockerFileLocation, + dockerComposeFileLocation, denoMainFile } = data; //@ts-ignore @@ -392,6 +393,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 +412,7 @@ export const setDefaultConfiguration = async (data: any) => { publishDirectory, baseDirectory, dockerFileLocation, + dockerComposeFileLocation, denoMainFile }; }; @@ -462,7 +470,13 @@ export const saveBuildLog = async ({ applicationId: string; }): Promise => { const { default: got } = await import('got') - + if (typeof line === 'object' && line) { + if (line.shortMessage) { + line = line.shortMessage + '\n' + line.stderr; + } else { + line = JSON.stringify(line); + } + } if (line && typeof line === 'string' && line.includes('ghs_')) { const regex = /ghs_.*@/g; line = line.replace(regex, '@'); @@ -480,7 +494,6 @@ export const saveBuildLog = async ({ } }) } catch (error) { - if (isDev) return return await prisma.buildLog.create({ data: { line: addTimestamp, buildId, time: Number(day().valueOf()), applicationId @@ -572,6 +585,29 @@ export function checkPnpm(installCommand = null, buildCommand = null, startComma ); } +export async function saveDockerRegistryCredentials({ url, username, password, workdir }) { + let decryptedPassword = decrypt(password); + const location = `${workdir}/.docker`; + + if (!username || !password) { + return null + } + + 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, @@ -590,15 +626,18 @@ export async function buildImage({ } if (!debug) { await saveBuildLog({ - line: `Debug turned off. To see more details, allow it in the features tab.`, + line: `Debug logging is disabled. Enable it above if necessary!`, 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}` }) + const { dockerRegistry: { url, username, password } } = await prisma.application.findUnique({ where: { id: applicationId }, select: { dockerRegistry: true } }) + const location = await saveDockerRegistryCredentials({ url, username, password, workdir }) + console.log(`docker ${location ? `--config ${location}` : ''} build --progress plain -f ${workdir}/${dockerFile} -t ${cache} --build-arg SOURCE_COMMIT=${commit} ${workdir}`) + + await executeDockerCmd({ 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') { diff --git a/apps/api/src/lib/buildPacks/compose.ts b/apps/api/src/lib/buildPacks/compose.ts index 26a946596..2bee70c3f 100644 --- a/apps/api/src/lib/buildPacks/compose.ts +++ b/apps/api/src/lib/buildPacks/compose.ts @@ -17,30 +17,19 @@ export default async function (data) { secrets, pullmergeRequestId, port, - dockerComposeConfiguration + dockerComposeConfiguration, + dockerComposeFileLocation } = 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) { } - - if (!dockerComposeRaw) { - throw ('docker-compose.yml or docker-compose.yaml are not found!'); - } + 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 = [ - `PORT=${port}` - ]; + const envs = []; + if (Object.entries(dockerComposeYaml.services).length === 1) { + envs.push(`PORT=${port}`) + } if (secrets.length > 0) { secrets.forEach((secret) => { if (pullmergeRequestId) { @@ -64,19 +53,42 @@ export default async function (data) { } catch (error) { // } - const composeVolumes = volumes.map((volume) => { - return { - [`${volume.split(':')[0]}`]: { - name: volume.split(':')[0] + 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 - value['volumes'] = volumes + // 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] } @@ -89,10 +101,13 @@ export default async function (data) { } 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['volumes'] = Object.assign({ ...dockerComposeYaml['volumes'] }, ...composeVolumes) dockerComposeYaml['networks'] = Object.assign({ ...networks }, { [network]: { external: true } }) - await fs.writeFile(`${workdir}/docker-compose.${isYml ? 'yml' : 'yaml'}`, yaml.dump(dockerComposeYaml)); + await fs.writeFile(fileYaml, 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` }) diff --git a/apps/api/src/lib/buildPacks/heroku.ts b/apps/api/src/lib/buildPacks/heroku.ts index c9608aba2..c108f3203 100644 --- a/apps/api/src/lib/buildPacks/heroku.ts +++ b/apps/api/src/lib/buildPacks/heroku.ts @@ -2,13 +2,14 @@ import { executeDockerCmd, prisma } from "../common" import { saveBuildLog } from "./common"; export default async function (data: any): Promise { - const { buildId, applicationId, tag, dockerId, debug, workdir, baseDirectory } = data + const { buildId, applicationId, tag, dockerId, debug, workdir, baseDirectory, baseImage } = data try { await saveBuildLog({ line: `Building image started.`, buildId, applicationId }); await executeDockerCmd({ + buildId, debug, dockerId, - command: `pack build -p ${workdir}${baseDirectory} ${applicationId}:${tag} --builder heroku/buildpacks:20` + command: `pack build -p ${workdir}${baseDirectory} ${applicationId}:${tag} --builder ${baseImage}` }) await saveBuildLog({ line: `Building image successful.`, buildId, applicationId }); } catch (error) { diff --git a/apps/api/src/lib/common.ts b/apps/api/src/lib/common.ts index 75e75947f..18153b8b3 100644 --- a/apps/api/src/lib/common.ts +++ b/apps/api/src/lib/common.ts @@ -8,21 +8,19 @@ 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 * as serviceFields from './services/serviceFields'; import { saveBuildLog } from './buildPacks/common'; import { scheduler } from './scheduler'; -import { supportedServiceTypesAndVersions } from './services/supportedVersions'; -import { includeServices } from './services/common'; -export const version = '3.10.16'; +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], @@ -31,9 +29,6 @@ const customConfig: Config = { length: 3 }; -export const defaultProxyImage = `coolify-haproxy-alpine:latest`; -export const defaultProxyImageTcp = `coolify-haproxy-tcp-alpine:latest`; -export const defaultProxyImageHttp = `coolify-haproxy-http-alpine:latest`; export const defaultTraefikImage = `traefik:v2.8`; export function getAPIUrl() { if (process.env.GITPOD_WORKSPACE_URL) { @@ -44,7 +39,7 @@ export function getAPIUrl() { if (process.env.CODESANDBOX_HOST) { return `https://${process.env.CODESANDBOX_HOST.replace(/\$PORT/, '3001')}`; } - return isDev ? 'http://localhost:3001' : 'http://localhost:3000'; + return isDev ? 'http://host.docker.internal:3001' : 'http://localhost:3000'; } export function getUIUrl() { @@ -198,7 +193,7 @@ export const encrypt = (text: string) => { if (text) { const iv = crypto.randomBytes(16); const cipher = crypto.createCipheriv(algorithm, process.env['COOLIFY_SECRET_KEY'], iv); - const encrypted = Buffer.concat([cipher.update(text), cipher.final()]); + const encrypted = Buffer.concat([cipher.update(text.trim()), cipher.final()]); return JSON.stringify({ iv: iv.toString('hex'), content: encrypted.toString('hex') @@ -244,7 +239,11 @@ export async function isDNSValid(hostname: any, domain: string): Promise { } export function getDomain(domain: string): string { - return domain?.replace('https://', '').replace('http://', ''); + if (domain) { + return domain?.replace('https://', '').replace('http://', ''); + } else { + return ''; + } } export async function isDomainConfigured({ @@ -279,9 +278,7 @@ export async function isDomainConfigured({ where: { OR: [ { fqdn: { endsWith: `//${nakedDomain}` } }, - { fqdn: { endsWith: `//www.${nakedDomain}` } }, - { minio: { apiFqdn: { endsWith: `//${nakedDomain}` } } }, - { minio: { apiFqdn: { endsWith: `//www.${nakedDomain}` } } } + { fqdn: { endsWith: `//www.${nakedDomain}` } } ], id: { not: checkOwn ? undefined : id }, destinationDocker: { @@ -396,12 +393,6 @@ export function generateTimestamp(): string { return `${day().format('HH:mm:ss.SSS')}`; } -export async function listServicesWithIncludes(): Promise { - return await prisma.service.findMany({ - include: includeServices, - orderBy: { createdAt: 'desc' } - }); -} export const supportedDatabaseTypesAndVersions = [ { @@ -511,56 +502,62 @@ export async function createRemoteEngineConfiguration(id: string) { const localPort = await getFreeSSHLocalPort(id); const { sshKey: { privateKey }, + network, remoteIpAddress, remotePort, 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: 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 { 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 foundWildcard = config.find({ Host: '*' }); - if (!foundWildcard) { - config.append({ - Host: '*', - StrictHostKeyChecking: 'no', - ControlMaster: 'auto', - ControlPath: `${homedir}/.ssh/coolify-%r@%h:%p`, - ControlPersist: '10m' - }) - } - const found = config.find({ Host: remoteIpAddress }); - if (!found) { - config.append({ - Host: remoteIpAddress, - Hostname: 'localhost', - Port: localPort.toString(), - User: remoteUser, - IdentityFile: sshKeyFile, - StrictHostKeyChecking: 'no' - }); - } + const Host = `${remoteIpAddress}-remote` + + try { + await asyncExecShell(`ssh-keygen -R ${Host}`); + await asyncExecShell(`ssh-keygen -R ${remoteIpAddress}`); + await asyncExecShell(`ssh-keygen -R localhost:${localPort}`); + } catch (error) { } + + + const found = config.find({ Host }); + const foundIp = config.find({ Host: remoteIpAddress }); + + if (found) config.remove({ Host }) + if (foundIp) config.remove({ Host: remoteIpAddress }) + + config.append({ + Host, + Hostname: remoteIpAddress, + Port: remotePort.toString(), + User: remoteUser, + StrictHostKeyChecking: 'no', + IdentityFile: sshKeyFile, + ControlMaster: 'auto', + ControlPath: `${homedir}/.ssh/coolify-${remoteIpAddress}-%r@%h:%p`, + ControlPersist: '10m' + }); try { await fs.stat(`${homedir}/.ssh/`); @@ -571,27 +568,23 @@ export async function createRemoteEngineConfiguration(id: string) { } export async function executeSSHCmd({ dockerId, command }) { const { execaCommand } = await import('execa') - let { remoteEngine, remoteIpAddress, engine, remoteUser } = await prisma.destinationDocker.findUnique({ where: { id: dockerId } }) + let { remoteEngine, remoteIpAddress } = await prisma.destinationDocker.findUnique({ where: { id: dockerId } }) if (remoteEngine) { await createRemoteEngineConfiguration(dockerId) - engine = `ssh://${remoteIpAddress}` - } 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') } } - command = `ssh ${remoteIpAddress} ${command}` - return await execaCommand(command) + 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, remoteUser } = await prisma.destinationDocker.findUnique({ where: { id: dockerId } }) + let { remoteEngine, remoteIpAddress, engine } = await prisma.destinationDocker.findUnique({ where: { id: dockerId } }) if (remoteEngine) { await createRemoteEngineConfiguration(dockerId); - engine = `ssh://${remoteIpAddress}`; + engine = `ssh://${remoteIpAddress}-remote`; } else { engine = 'unix:///var/run/docker.sock'; } @@ -722,11 +715,14 @@ 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.findFirst({}); } +export function generateToken() { + return jsonwebtoken.sign({ + nbf: Math.floor(Date.now() / 1000) - 30, + }, process.env['COOLIFY_SECRET_KEY']) +} export function generatePassword({ length = 24, symbols = false, @@ -977,7 +973,7 @@ export function generateDatabaseConfiguration(database: any, arch: string): Data } } export function isARM(arch: string) { - if (arch === 'arm' || arch === 'arm64') { + if (arch === 'arm' || arch === 'arm64' || arch === 'aarch' || arch === 'aarch64') { return true; } return false; @@ -1095,6 +1091,7 @@ export const createDirectories = async ({ repository: string; buildId: string; }): Promise<{ workdir: string; repodir: string }> => { + repository = repository.replaceAll(' ', '') const repodir = `/tmp/build-sources/${repository}/`; const workdir = `/tmp/build-sources/${repository}/${buildId}`; let workdirFound = false; @@ -1399,7 +1396,7 @@ export async function startTraefikTCPProxy( `--entrypoints.tcp.address=:${publicPort}`, `--entryPoints.tcp.forwardedHeaders.insecure=true`, `--providers.http.endpoint=${traefikUrl}?id=${id}&privatePort=${privatePort}&publicPort=${publicPort}&type=tcp&address=${dependentId}`, - '--providers.http.pollTimeout=2s', + '--providers.http.pollTimeout=10s', '--log.level=error' ], ports: [`${publicPort}:${publicPort}`], @@ -1447,13 +1444,19 @@ export async function getServiceFromDB({ const settings = await prisma.setting.findFirst(); const body = await prisma.service.findFirst({ where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } }, - include: includeServices + include: { + destinationDocker: true, + persistentStorage: true, + serviceSecret: true, + serviceSetting: true, + wordpress: true, + plausibleAnalytics: true, + } }); if (!body) { return null } - let { type } = body; - type = fixType(type); + // body.type = fixType(body.type); if (body?.serviceSecret.length > 0) { body.serviceSecret = body.serviceSecret.map((s) => { @@ -1461,87 +1464,18 @@ export async function getServiceFromDB({ return s; }); } + if (body.wordpress) { + body.wordpress.ftpPassword = decrypt(body.wordpress.ftpPassword); + } - body[type] = { ...body[type], ...getUpdateableFields(type, body[type]) }; return { ...body, settings }; } -export function getServiceImage(type: string): string { - const found = supportedServiceTypesAndVersions.find((t) => t.name === type); - if (found) { - return found.baseImage; - } - return ''; -} - -export function getServiceImages(type: string): string[] { - const found = supportedServiceTypesAndVersions.find((t) => t.name === type); - if (found) { - return found.images; - } - return []; -} - -export function saveUpdateableFields(type: string, data: any) { - const update = {}; - if (type && serviceFields[type]) { - serviceFields[type].map((k) => { - let temp = data[k.name]; - if (temp) { - if (k.isEncrypted) { - temp = encrypt(temp); - } - if (k.isLowerCase) { - temp = temp.toLowerCase(); - } - if (k.isNumber) { - temp = Number(temp); - } - if (k.isBoolean) { - temp = Boolean(temp); - } - } - if (k.isNumber && temp === '') { - temp = null; - } - update[k.name] = temp; - }); - } - return update; -} - -export function getUpdateableFields(type: string, data: any) { - const update = {}; - if (type && serviceFields[type]) { - serviceFields[type].map((k) => { - let temp = data[k.name]; - if (temp) { - if (k.isEncrypted) { - temp = decrypt(temp); - } - update[k.name] = temp; - } - update[k.name] = temp; - }); - } - return update; -} export function fixType(type) { - // Hack to fix the type case sensitivity... - if (type === 'plausibleanalytics') type = 'plausibleAnalytics'; - if (type === 'meilisearch') type = 'meiliSearch'; - return type; + return type?.replaceAll(' ', '').toLowerCase() || null; } -export const getServiceMainPort = (service: string) => { - const serviceType = supportedServiceTypesAndVersions.find((s) => s.name === service); - if (serviceType) { - return serviceType.ports.main; - } - return null; -}; - export function makeLabelForServices(type) { return [ 'coolify.managed=true', @@ -1558,6 +1492,7 @@ export function errorHandler({ message: string | any; }) { if (message.message) message = message.message; + Sentry.captureException(message); throw { status, message }; } export async function generateSshKeyPair(): Promise<{ publicKey: string; privateKey: string }> { @@ -1681,7 +1616,9 @@ export function persistentVolumes(id, persistentStorage, config) { for (const [key, value] of Object.entries(config)) { if (value.volumes) { for (const volume of value.volumes) { - volumeSet.add(volume); + if (!volume.startsWith('/')) { + volumeSet.add(volume); + } } } } diff --git a/apps/api/src/lib/scheduler.ts b/apps/api/src/lib/scheduler.ts index 20add2b06..b1d173120 100644 --- a/apps/api/src/lib/scheduler.ts +++ b/apps/api/src/lib/scheduler.ts @@ -9,7 +9,7 @@ Bree.extend(TSBree); const options: any = { defaultExtension: 'js', - logger: new Cabin(), + logger: false, // logger: false, // workerMessageHandler: async ({ name, message }) => { // if (name === 'deployApplication' && message?.deploying) { diff --git a/apps/api/src/lib/services.ts b/apps/api/src/lib/services.ts index d7f0fd75e..b423cbe9c 100644 --- a/apps/api/src/lib/services.ts +++ b/apps/api/src/lib/services.ts @@ -1,20 +1,51 @@ -import { createDirectories, getServiceFromDB, getServiceImage, getServiceMainPort, makeLabelForServices } from "./common"; - -export async function defaultServiceConfigurations({ id, teamId }) { - const service = await getServiceFromDB({ id, teamId }); - const { destinationDockerId, destinationDocker, type, serviceSecret } = service; - - const network = destinationDockerId && destinationDocker.network; - const port = getServiceMainPort(type); - - const { workdir } = await createDirectories({ repository: type, buildId: id }); - - const image = getServiceImage(type); - let secrets = []; - if (serviceSecret.length > 0) { - serviceSecret.forEach((secret) => { - secrets.push(`${secret.name}=${secret.value}`); - }); +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'); + try { + 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 { + await open?.close() } - return { ...service, network, port, workdir, image, secrets } -} \ No newline at end of file +} +const compareSemanticVersions = (a: string, b: string) => { + const a1 = a.split('.'); + const b1 = b.split('.'); + const len = Math.min(a1.length, b1.length); + for (let i = 0; i < len; i++) { + const a2 = +a1[i] || 0; + const b2 = +b1[i] || 0; + if (a2 !== b2) { + return a2 > b2 ? 1 : -1; + } + } + return b1.length - a1.length; +}; +export async function getTags(type: string) { + + try { + if (type) { + const tagsPath = isDev ? './tags.json' : '/app/tags.json'; + const data = await fs.readFile(tagsPath, 'utf8') + let tags = JSON.parse(data) + if (tags) { + tags = tags.find((tag: any) => tag.name.includes(type)) + tags.tags = tags.tags.sort(compareSemanticVersions).reverse(); + return tags + } + } + } catch (error) { + return [] + + } + + +} diff --git a/apps/api/src/lib/services/common.ts b/apps/api/src/lib/services/common.ts index 716dc44c0..0d7cec42e 100644 --- a/apps/api/src/lib/services/common.ts +++ b/apps/api/src/lib/services/common.ts @@ -1,367 +1,9 @@ -import cuid from 'cuid'; -import { encrypt, generatePassword, prisma } from '../common'; - -export const includeServices: any = { - destinationDocker: true, - persistentStorage: true, - serviceSecret: true, - minio: true, - plausibleAnalytics: true, - vscodeserver: true, - wordpress: true, - ghost: true, - meiliSearch: true, - umami: true, - hasura: true, - fider: true, - moodle: true, - appwrite: true, - glitchTip: true, - searxng: true, - weblate: true, - taiga: true, -}; -export async function configureServiceType({ - id, - type -}: { - id: string; - type: string; -}): Promise { - if (type === 'plausibleanalytics') { - const password = encrypt(generatePassword({})); - const postgresqlUser = cuid(); - const postgresqlPassword = encrypt(generatePassword({})); - const postgresqlDatabase = 'plausibleanalytics'; - const secretKeyBase = encrypt(generatePassword({ length: 64 })); - - await prisma.service.update({ - where: { id }, - data: { - type, - plausibleAnalytics: { - create: { - postgresqlDatabase, - postgresqlUser, - postgresqlPassword, - password, - secretKeyBase - } - } - } - }); - } else if (type === 'nocodb') { - await prisma.service.update({ - where: { id }, - data: { type } - }); - } else if (type === 'minio') { - const rootUser = cuid(); - const rootUserPassword = encrypt(generatePassword({})); - await prisma.service.update({ - where: { id }, - data: { type, minio: { create: { rootUser, rootUserPassword } } } - }); - } else if (type === 'vscodeserver') { - const password = encrypt(generatePassword({})); - await prisma.service.update({ - where: { id }, - data: { type, vscodeserver: { create: { password } } } - }); - } else if (type === 'wordpress') { - const mysqlUser = cuid(); - const mysqlPassword = encrypt(generatePassword({})); - const mysqlRootUser = cuid(); - const mysqlRootUserPassword = encrypt(generatePassword({})); - await prisma.service.update({ - where: { id }, - data: { - type, - wordpress: { create: { mysqlPassword, mysqlRootUserPassword, mysqlRootUser, mysqlUser } } - } - }); - } else if (type === 'vaultwarden') { - await prisma.service.update({ - where: { id }, - data: { - type - } - }); - } else if (type === 'languagetool') { - await prisma.service.update({ - where: { id }, - data: { - type - } - }); - } else if (type === 'n8n') { - await prisma.service.update({ - where: { id }, - data: { - type - } - }); - } else if (type === 'uptimekuma') { - await prisma.service.update({ - where: { id }, - data: { - type - } - }); - } else if (type === 'ghost') { - const defaultEmail = `${cuid()}@example.com`; - const defaultPassword = encrypt(generatePassword({})); - const mariadbUser = cuid(); - const mariadbPassword = encrypt(generatePassword({})); - const mariadbRootUser = cuid(); - const mariadbRootUserPassword = encrypt(generatePassword({})); - - await prisma.service.update({ - where: { id }, - data: { - type, - ghost: { - create: { - defaultEmail, - defaultPassword, - mariadbUser, - mariadbPassword, - mariadbRootUser, - mariadbRootUserPassword - } - } - } - }); - } else if (type === 'meilisearch') { - const masterKey = encrypt(generatePassword({ length: 32 })); - await prisma.service.update({ - where: { id }, - data: { - type, - meiliSearch: { create: { masterKey } } - } - }); - } else if (type === 'umami') { - const umamiAdminPassword = encrypt(generatePassword({})); - const postgresqlUser = cuid(); - const postgresqlPassword = encrypt(generatePassword({})); - const postgresqlDatabase = 'umami'; - const hashSalt = encrypt(generatePassword({ length: 64 })); - await prisma.service.update({ - where: { id }, - data: { - type, - umami: { - create: { - umamiAdminPassword, - postgresqlDatabase, - postgresqlPassword, - postgresqlUser, - hashSalt - } - } - } - }); - } else if (type === 'hasura') { - const postgresqlUser = cuid(); - const postgresqlPassword = encrypt(generatePassword({})); - const postgresqlDatabase = 'hasura'; - const graphQLAdminPassword = encrypt(generatePassword({})); - await prisma.service.update({ - where: { id }, - data: { - type, - hasura: { - create: { - postgresqlDatabase, - postgresqlPassword, - postgresqlUser, - graphQLAdminPassword - } - } - } - }); - } else if (type === 'fider') { - const postgresqlUser = cuid(); - const postgresqlPassword = encrypt(generatePassword({})); - const postgresqlDatabase = 'fider'; - const jwtSecret = encrypt(generatePassword({ length: 64, symbols: true })); - await prisma.service.update({ - where: { id }, - data: { - type, - fider: { - create: { - postgresqlDatabase, - postgresqlPassword, - postgresqlUser, - jwtSecret - } - } - } - }); - } else if (type === 'moodle') { - const defaultUsername = cuid(); - const defaultPassword = encrypt(generatePassword({})); - const defaultEmail = `${cuid()} @example.com`; - const mariadbUser = cuid(); - const mariadbPassword = encrypt(generatePassword({})); - const mariadbDatabase = 'moodle_db'; - const mariadbRootUser = cuid(); - const mariadbRootUserPassword = encrypt(generatePassword({})); - await prisma.service.update({ - where: { id }, - data: { - type, - moodle: { - create: { - defaultUsername, - defaultPassword, - defaultEmail, - mariadbUser, - mariadbPassword, - mariadbDatabase, - mariadbRootUser, - mariadbRootUserPassword - } - } - } - }); - } else if (type === 'appwrite') { - const opensslKeyV1 = encrypt(generatePassword({})); - const executorSecret = encrypt(generatePassword({})); - const redisPassword = encrypt(generatePassword({})); - const mariadbHost = `${id}-mariadb` - const mariadbUser = cuid(); - const mariadbPassword = encrypt(generatePassword({})); - const mariadbDatabase = 'appwrite'; - const mariadbRootUser = cuid(); - const mariadbRootUserPassword = encrypt(generatePassword({})); - await prisma.service.update({ - where: { id }, - data: { - type, - appwrite: { - create: { - opensslKeyV1, - executorSecret, - redisPassword, - mariadbHost, - mariadbUser, - mariadbPassword, - mariadbDatabase, - mariadbRootUser, - mariadbRootUserPassword - } - } - } - }); - } else if (type === 'glitchTip') { - const defaultUsername = cuid(); - const defaultEmail = `${defaultUsername}@example.com`; - const defaultPassword = encrypt(generatePassword({})); - const postgresqlUser = cuid(); - const postgresqlPassword = encrypt(generatePassword({})); - const postgresqlDatabase = 'glitchTip'; - const secretKeyBase = encrypt(generatePassword({ length: 64 })); - - await prisma.service.update({ - where: { id }, - data: { - type, - glitchTip: { - create: { - postgresqlDatabase, - postgresqlUser, - postgresqlPassword, - secretKeyBase, - defaultEmail, - defaultUsername, - defaultPassword, - } - } - } - }); - } else if (type === 'searxng') { - const secretKey = encrypt(generatePassword({ length: 32, isHex: true })) - const redisPassword = encrypt(generatePassword({})); - await prisma.service.update({ - where: { id }, - data: { - type, - searxng: { - create: { - secretKey, - redisPassword, - } - } - } - }); - } else if (type === 'weblate') { - const adminPassword = encrypt(generatePassword({})) - const postgresqlUser = cuid(); - const postgresqlPassword = encrypt(generatePassword({})); - const postgresqlDatabase = 'weblate'; - await prisma.service.update({ - where: { id }, - data: { - type, - weblate: { - create: { - adminPassword, - postgresqlHost: `${id}-postgresql`, - postgresqlPort: 5432, - postgresqlUser, - postgresqlPassword, - postgresqlDatabase, - } - } - } - }); - } else if (type === 'taiga') { - const secretKey = encrypt(generatePassword({})) - const erlangSecret = encrypt(generatePassword({})) - const rabbitMQUser = cuid(); - const djangoAdminUser = cuid(); - const djangoAdminPassword = encrypt(generatePassword({})) - const rabbitMQPassword = encrypt(generatePassword({})) - const postgresqlUser = cuid(); - const postgresqlPassword = encrypt(generatePassword({})); - const postgresqlDatabase = 'taiga'; - await prisma.service.update({ - where: { id }, - data: { - type, - taiga: { - create: { - secretKey, - erlangSecret, - djangoAdminUser, - djangoAdminPassword, - rabbitMQUser, - rabbitMQPassword, - postgresqlHost: `${id}-postgresql`, - postgresqlPort: 5432, - postgresqlUser, - postgresqlPassword, - postgresqlDatabase, - } - } - } - }); - } else { - await prisma.service.update({ - where: { id }, - data: { - type - } - }); - } -} +import { decrypt, prisma } from '../common'; export async function removeService({ id }: { id: string }): Promise { await prisma.serviceSecret.deleteMany({ where: { serviceId: id } }); + await prisma.serviceSetting.deleteMany({ where: { serviceId: id } }); await prisma.servicePersistentStorage.deleteMany({ where: { serviceId: id } }); await prisma.meiliSearch.deleteMany({ where: { serviceId: id } }); await prisma.fider.deleteMany({ where: { serviceId: id } }); @@ -380,4 +22,18 @@ export async function removeService({ id }: { id: string }): Promise { await prisma.taiga.deleteMany({ where: { serviceId: id } }); await prisma.service.delete({ where: { id } }); +} +export async function verifyAndDecryptServiceSecrets(id: string) { + const secrets = await prisma.serviceSecret.findMany({ where: { serviceId: id } }) + let decryptedSecrets = secrets.map(secret => { + const { name, value } = secret + if (value) { + let rawValue = decrypt(value) + rawValue = rawValue.replaceAll(/\$/gi, '$$$') + return { name, value: rawValue } + } + return { name, value } + + }) + return decryptedSecrets } \ No newline at end of file diff --git a/apps/api/src/lib/services/handlers.ts b/apps/api/src/lib/services/handlers.ts index bcaf07d36..ec015ea4f 100644 --- a/apps/api/src/lib/services/handlers.ts +++ b/apps/api/src/lib/services/handlers.ts @@ -1,1907 +1,15 @@ import type { FastifyReply, FastifyRequest } from 'fastify'; import fs from 'fs/promises'; import yaml from 'js-yaml'; -import bcrypt from 'bcryptjs'; +import path from 'path'; +import { asyncSleep, ComposeFile, createDirectories, decrypt, defaultComposeConfiguration, errorHandler, executeDockerCmd, getServiceFromDB, isARM, makeLabelForServices, persistentVolumes, prisma, stopTcpHttpProxy } from '../common'; +import { parseAndFindServiceTemplates } from '../../routes/api/v1/services/handlers'; + import { ServiceStartStop } from '../../routes/api/v1/services/types'; -import { asyncSleep, ComposeFile, createDirectories, defaultComposeConfiguration, errorHandler, executeDockerCmd, getDomain, getFreePublicPort, getServiceFromDB, getServiceImage, getServiceMainPort, isARM, isDev, makeLabelForServices, persistentVolumes, prisma } from '../common'; -import { defaultServiceConfigurations } from '../services'; import { OnlyId } from '../../types'; +import { verifyAndDecryptServiceSecrets } from './common'; -export async function startService(request: FastifyRequest) { - try { - const { type } = request.params - if (type === 'plausibleanalytics') { - return await startPlausibleAnalyticsService(request) - } - if (type === 'nocodb') { - return await startNocodbService(request) - } - if (type === 'minio') { - return await startMinioService(request) - } - if (type === 'vscodeserver') { - return await startVscodeService(request) - } - if (type === 'wordpress') { - return await startWordpressService(request) - } - if (type === 'vaultwarden') { - return await startVaultwardenService(request) - } - if (type === 'languagetool') { - return await startLanguageToolService(request) - } - if (type === 'n8n') { - return await startN8nService(request) - } - if (type === 'uptimekuma') { - return await startUptimekumaService(request) - } - if (type === 'ghost') { - return await startGhostService(request) - } - if (type === 'meilisearch') { - return await startMeilisearchService(request) - } - if (type === 'umami') { - return await startUmamiService(request) - } - if (type === 'hasura') { - return await startHasuraService(request) - } - if (type === 'fider') { - return await startFiderService(request) - } - if (type === 'moodle') { - return await startMoodleService(request) - } - if (type === 'appwrite') { - return await startAppWriteService(request) - } - if (type === 'glitchTip') { - return await startGlitchTipService(request) - } - if (type === 'searxng') { - return await startSearXNGService(request) - } - if (type === 'weblate') { - return await startWeblateService(request) - } - if (type === 'taiga') { - return await startTaigaService(request) - } - if (type === 'grafana') { - return await startGrafanaService(request) - } - if (type === 'trilium') { - return await startTriliumService(request) - } - - throw `Service type ${type} not supported.` - } catch (error) { - throw { status: 500, message: error?.message || error } - } -} export async function stopService(request: FastifyRequest) { - try { - return await stopServiceContainers(request) - } catch (error) { - throw { status: 500, message: error?.message || error } - } -} - -async function startPlausibleAnalyticsService(request: FastifyRequest) { - try { - const { id } = request.params - const teamId = request.user.teamId; - const service = await getServiceFromDB({ id, teamId }); - const { - type, - version, - fqdn, - destinationDockerId, - destinationDocker, - serviceSecret, - persistentStorage, - exposePort, - plausibleAnalytics: { - id: plausibleDbId, - username, - email, - password, - postgresqlDatabase, - postgresqlPassword, - postgresqlUser, - secretKeyBase - } - } = service; - const image = getServiceImage(type); - - const config = { - plausibleAnalytics: { - image: `${image}:${version}`, - environmentVariables: { - ADMIN_USER_EMAIL: email, - ADMIN_USER_NAME: username, - ADMIN_USER_PWD: password, - BASE_URL: fqdn, - SECRET_KEY_BASE: secretKeyBase, - DISABLE_AUTH: 'false', - DISABLE_REGISTRATION: 'true', - DATABASE_URL: `postgresql://${postgresqlUser}:${postgresqlPassword}@${id}-postgresql:5432/${postgresqlDatabase}`, - CLICKHOUSE_DATABASE_URL: `http://${id}-clickhouse:8123/plausible` - } - }, - postgresql: { - volumes: [`${plausibleDbId}-postgresql-data:/bitnami/postgresql/`], - image: 'bitnami/postgresql:13.2.0', - environmentVariables: { - POSTGRESQL_PASSWORD: postgresqlPassword, - POSTGRESQL_USERNAME: postgresqlUser, - POSTGRESQL_DATABASE: postgresqlDatabase - } - }, - clickhouse: { - volumes: [`${plausibleDbId}-clickhouse-data:/var/lib/clickhouse`], - image: 'yandex/clickhouse-server:21.3.2.5', - environmentVariables: {}, - ulimits: { - nofile: { - soft: 262144, - hard: 262144 - } - } - } - }; - if (serviceSecret.length > 0) { - serviceSecret.forEach((secret) => { - config.plausibleAnalytics.environmentVariables[secret.name] = secret.value; - }); - } - const network = destinationDockerId && destinationDocker.network; - const port = getServiceMainPort('plausibleanalytics'); - - const { workdir } = await createDirectories({ repository: type, buildId: id }); - - const clickhouseConfigXml = ` - - - warning - true - - - - - - - - - - - - `; - const clickhouseUserConfigXml = ` - - - - 0 - 0 - - - `; - - const initQuery = 'CREATE DATABASE IF NOT EXISTS plausible;'; - const initScript = 'clickhouse client --queries-file /docker-entrypoint-initdb.d/init.query'; - await fs.writeFile(`${workdir}/clickhouse-config.xml`, clickhouseConfigXml); - await fs.writeFile(`${workdir}/clickhouse-user-config.xml`, clickhouseUserConfigXml); - await fs.writeFile(`${workdir}/init.query`, initQuery); - await fs.writeFile(`${workdir}/init-db.sh`, initScript); - - const Dockerfile = ` -FROM ${config.clickhouse.image} -COPY ./clickhouse-config.xml /etc/clickhouse-server/users.d/logging.xml -COPY ./clickhouse-user-config.xml /etc/clickhouse-server/config.d/logging.xml -COPY ./init.query /docker-entrypoint-initdb.d/init.query -COPY ./init-db.sh /docker-entrypoint-initdb.d/init-db.sh`; - - await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile); - - const { volumeMounts } = persistentVolumes(id, persistentStorage, config) - - const composeFile: ComposeFile = { - version: '3.8', - services: { - [id]: { - container_name: id, - image: config.plausibleAnalytics.image, - command: - 'sh -c "sleep 10 && /entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh db init-admin && /entrypoint.sh run"', - environment: config.plausibleAnalytics.environmentVariables, - ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), - depends_on: [`${id}-postgresql`, `${id}-clickhouse`], - labels: makeLabelForServices('plausibleAnalytics'), - ...defaultComposeConfiguration(network), - }, - [`${id}-postgresql`]: { - container_name: `${id}-postgresql`, - image: config.postgresql.image, - environment: config.postgresql.environmentVariables, - volumes: config.postgresql.volumes, - ...defaultComposeConfiguration(network), - }, - [`${id}-clickhouse`]: { - build: workdir, - container_name: `${id}-clickhouse`, - environment: config.clickhouse.environmentVariables, - volumes: config.clickhouse.volumes, - ...defaultComposeConfiguration(network), - } - }, - networks: { - [network]: { - external: true - } - }, - volumes: volumeMounts - }; - const composeFileDestination = `${workdir}/docker-compose.yaml`; - await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - await startServiceContainers(destinationDocker.id, composeFileDestination) - return {} - } catch ({ status, message }) { - return errorHandler({ status, message }) - } -} - -async function startNocodbService(request: FastifyRequest) { - try { - const { id } = request.params; - const teamId = request.user.teamId; - const service = await getServiceFromDB({ id, teamId }); - const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort, persistentStorage } = - service; - const network = destinationDockerId && destinationDocker.network; - const port = getServiceMainPort('nocodb'); - - const { workdir } = await createDirectories({ repository: type, buildId: id }); - const image = getServiceImage(type); - - const config = { - nocodb: { - image: `${image}:${version}`, - volumes: [`${id}-nc:/usr/app/data`], - environmentVariables: {} - } - - }; - if (serviceSecret.length > 0) { - serviceSecret.forEach((secret) => { - config.nocodb.environmentVariables[secret.name] = secret.value; - }); - } - const { volumeMounts } = persistentVolumes(id, persistentStorage, config) - const composeFile: ComposeFile = { - version: '3.8', - services: { - [id]: { - container_name: id, - image: config.nocodb.image, - volumes: config.nocodb.volumes, - environment: config.nocodb.environmentVariables, - ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), - labels: makeLabelForServices('nocodb'), - ...defaultComposeConfiguration(network), - } - }, - networks: { - [network]: { - external: true - } - }, - volumes: volumeMounts - }; - const composeFileDestination = `${workdir}/docker-compose.yaml`; - await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - await startServiceContainers(destinationDocker.id, composeFileDestination) - return {} - } catch ({ status, message }) { - return errorHandler({ status, message }) - } -} - -async function startMinioService(request: FastifyRequest) { - try { - const { id } = request.params; - const teamId = request.user.teamId; - const service = await getServiceFromDB({ id, teamId }); - const { - type, - version, - fqdn, - destinationDockerId, - destinationDocker, - persistentStorage, - exposePort, - minio: { rootUser, rootUserPassword, apiFqdn }, - serviceSecret - } = service; - - const network = destinationDockerId && destinationDocker.network; - const port = getServiceMainPort('minio'); - - const { service: { destinationDocker: { remoteEngine, engine, remoteIpAddress } } } = await prisma.minio.findUnique({ where: { serviceId: id }, include: { service: { include: { destinationDocker: true } } } }) - const publicPort = await getFreePublicPort({ id, remoteEngine, engine, remoteIpAddress }); - - const consolePort = 9001; - const { workdir } = await createDirectories({ repository: type, buildId: id }); - const image = getServiceImage(type); - - const config = { - minio: { - image: `${image}:${version}`, - volumes: [`${id}-minio-data:/data`], - environmentVariables: { - MINIO_SERVER_URL: apiFqdn, - MINIO_DOMAIN: getDomain(fqdn), - MINIO_ROOT_USER: rootUser, - MINIO_ROOT_PASSWORD: rootUserPassword, - MINIO_BROWSER_REDIRECT_URL: fqdn - } - } - - }; - if (serviceSecret.length > 0) { - serviceSecret.forEach((secret) => { - config.minio.environmentVariables[secret.name] = secret.value; - }); - } - const { volumeMounts } = persistentVolumes(id, persistentStorage, config) - const composeFile: ComposeFile = { - version: '3.8', - services: { - [id]: { - container_name: id, - image: config.minio.image, - command: `server /data --console-address ":${consolePort}"`, - environment: config.minio.environmentVariables, - volumes: config.minio.volumes, - ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), - labels: makeLabelForServices('minio'), - ...defaultComposeConfiguration(network), - } - }, - networks: { - [network]: { - external: true - } - }, - volumes: volumeMounts - }; - const composeFileDestination = `${workdir}/docker-compose.yaml`; - await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - await startServiceContainers(destinationDocker.id, composeFileDestination) - await prisma.minio.update({ where: { serviceId: id }, data: { publicPort } }); - return {} - } catch ({ status, message }) { - return errorHandler({ status, message }) - } -} - -async function startVscodeService(request: FastifyRequest) { - try { - const { id } = request.params; - const teamId = request.user.teamId; - const service = await getServiceFromDB({ id, teamId }); - const { - type, - version, - destinationDockerId, - destinationDocker, - serviceSecret, - persistentStorage, - exposePort, - vscodeserver: { password } - } = service; - - const network = destinationDockerId && destinationDocker.network; - const port = getServiceMainPort('vscodeserver'); - - const { workdir } = await createDirectories({ repository: type, buildId: id }); - const image = getServiceImage(type); - - const config = { - vscodeserver: { - image: `${image}:${version}`, - volumes: [`${id}-vscodeserver-data:/home/coder`], - environmentVariables: { - PASSWORD: password - } - } - - }; - if (serviceSecret.length > 0) { - serviceSecret.forEach((secret) => { - config.vscodeserver.environmentVariables[secret.name] = secret.value; - }); - } - const { volumeMounts } = persistentVolumes(id, persistentStorage, config) - - const composeFile: ComposeFile = { - version: '3.8', - services: { - [id]: { - container_name: id, - image: config.vscodeserver.image, - environment: config.vscodeserver.environmentVariables, - volumes: config.vscodeserver.volumes, - ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), - labels: makeLabelForServices('vscodeServer'), - ...defaultComposeConfiguration(network), - } - }, - networks: { - [network]: { - external: true - } - }, - volumes: volumeMounts - }; - const composeFileDestination = `${workdir}/docker-compose.yaml`; - await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - await startServiceContainers(destinationDocker.id, composeFileDestination) - - const changePermissionOn = persistentStorage.map((p) => p.path); - if (changePermissionOn.length > 0) { - await executeDockerCmd({ - dockerId: destinationDocker.id, command: `docker exec -u root ${id} chown -R 1000:1000 ${changePermissionOn.join( - ' ' - )}` - }) - } - return {} - } catch ({ status, message }) { - return errorHandler({ status, message }) - } -} - -async function startWordpressService(request: FastifyRequest) { - try { - const { id } = request.params; - const teamId = request.user.teamId; - const service = await getServiceFromDB({ id, teamId }); - const { - arch, - type, - version, - destinationDockerId, - serviceSecret, - destinationDocker, - persistentStorage, - exposePort, - wordpress: { - mysqlDatabase, - mysqlHost, - mysqlPort, - mysqlUser, - mysqlPassword, - extraConfig, - mysqlRootUser, - mysqlRootUserPassword, - ownMysql - } - } = service; - - const network = destinationDockerId && destinationDocker.network; - const image = getServiceImage(type); - const port = getServiceMainPort('wordpress'); - - const { workdir } = await createDirectories({ repository: type, buildId: id }); - const config = { - wordpress: { - image: `${image}:${version}`, - volumes: [`${id}-wordpress-data:/var/www/html`], - environmentVariables: { - WORDPRESS_DB_HOST: ownMysql ? `${mysqlHost}:${mysqlPort}` : `${id}-mysql`, - WORDPRESS_DB_USER: mysqlUser, - WORDPRESS_DB_PASSWORD: mysqlPassword, - WORDPRESS_DB_NAME: mysqlDatabase, - WORDPRESS_CONFIG_EXTRA: extraConfig - } - }, - mysql: { - image: `bitnami/mysql:5.7`, - volumes: [`${id}-mysql-data:/bitnami/mysql/data`], - environmentVariables: { - MYSQL_ROOT_PASSWORD: mysqlRootUserPassword, - MYSQL_ROOT_USER: mysqlRootUser, - MYSQL_USER: mysqlUser, - MYSQL_PASSWORD: mysqlPassword, - MYSQL_DATABASE: mysqlDatabase - } - } - }; - if (isARM(arch)) { - config.mysql.image = 'mysql:5.7' - config.mysql.volumes = [`${id}-mysql-data:/var/lib/mysql`] - } - if (serviceSecret.length > 0) { - serviceSecret.forEach((secret) => { - config.wordpress.environmentVariables[secret.name] = secret.value; - }); - } - - const { volumeMounts } = persistentVolumes(id, persistentStorage, config) - - const composeFile: ComposeFile = { - version: '3.8', - services: { - [id]: { - container_name: id, - image: config.wordpress.image, - environment: config.wordpress.environmentVariables, - volumes: config.wordpress.volumes, - ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), - labels: makeLabelForServices('wordpress'), - ...defaultComposeConfiguration(network), - } - }, - networks: { - [network]: { - external: true - } - }, - volumes: volumeMounts - }; - if (!ownMysql) { - composeFile.services[id].depends_on = [`${id}-mysql`]; - composeFile.services[`${id}-mysql`] = { - container_name: `${id}-mysql`, - image: config.mysql.image, - volumes: config.mysql.volumes, - environment: config.mysql.environmentVariables, - ...defaultComposeConfiguration(network), - }; - } - const composeFileDestination = `${workdir}/docker-compose.yaml`; - await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - await startServiceContainers(destinationDocker.id, composeFileDestination) - return {} - } catch ({ status, message }) { - return errorHandler({ status, message }) - } -} - -async function startVaultwardenService(request: FastifyRequest) { - try { - const { id } = request.params; - const teamId = request.user.teamId; - const service = await getServiceFromDB({ id, teamId }); - const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort, persistentStorage } = - service; - - const network = destinationDockerId && destinationDocker.network; - const port = getServiceMainPort('vaultwarden'); - - const { workdir } = await createDirectories({ repository: type, buildId: id }); - const image = getServiceImage(type); - - const config = { - vaultwarden: { - image: `${image}:${version}`, - volumes: [`${id}-vaultwarden-data:/data/`], - environmentVariables: {} - } - - }; - if (serviceSecret.length > 0) { - serviceSecret.forEach((secret) => { - config.vaultwarden.environmentVariables[secret.name] = secret.value; - }); - } - const { volumeMounts } = persistentVolumes(id, persistentStorage, config) - const composeFile: ComposeFile = { - version: '3.8', - services: { - [id]: { - container_name: id, - image: config.vaultwarden.image, - environment: config.vaultwarden.environmentVariables, - volumes: config.vaultwarden.volumes, - ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), - labels: makeLabelForServices('vaultWarden'), - ...defaultComposeConfiguration(network), - } - }, - networks: { - [network]: { - external: true - } - }, - volumes: volumeMounts - }; - const composeFileDestination = `${workdir}/docker-compose.yaml`; - await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - await startServiceContainers(destinationDocker.id, composeFileDestination) - return {} - } catch ({ status, message }) { - return errorHandler({ status, message }) - } -} - -async function startLanguageToolService(request: FastifyRequest) { - try { - const { id } = request.params; - const teamId = request.user.teamId; - const service = await getServiceFromDB({ id, teamId }); - const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort, persistentStorage } = - service; - const network = destinationDockerId && destinationDocker.network; - const port = getServiceMainPort('languagetool'); - - const { workdir } = await createDirectories({ repository: type, buildId: id }); - const image = getServiceImage(type); - - const config = { - languagetool: { - image: `${image}:${version}`, - volumes: [`${id}-ngrams:/ngrams`], - environmentVariables: {} - } - }; - - if (serviceSecret.length > 0) { - serviceSecret.forEach((secret) => { - config.languagetool.environmentVariables[secret.name] = secret.value; - }); - } - const { volumeMounts } = persistentVolumes(id, persistentStorage, config) - const composeFile: ComposeFile = { - version: '3.8', - services: { - [id]: { - container_name: id, - image: config.languagetool.image, - environment: config.languagetool.environmentVariables, - ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), - volumes: config.languagetool.volumes, - labels: makeLabelForServices('languagetool'), - ...defaultComposeConfiguration(network), - } - }, - networks: { - [network]: { - external: true - } - }, - volumes: volumeMounts - }; - const composeFileDestination = `${workdir}/docker-compose.yaml`; - await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - await startServiceContainers(destinationDocker.id, composeFileDestination) - return {} - } catch ({ status, message }) { - return errorHandler({ status, message }) - } -} - -async function startN8nService(request: FastifyRequest) { - try { - const { id } = request.params; - const teamId = request.user.teamId; - const service = await getServiceFromDB({ id, teamId }); - const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort, persistentStorage } = - service; - const network = destinationDockerId && destinationDocker.network; - const port = getServiceMainPort('n8n'); - - const { workdir } = await createDirectories({ repository: type, buildId: id }); - const image = getServiceImage(type); - - const config = { - n8n: { - image: `${image}:${version}`, - volumes: [`${id}-n8n:/root/.n8n`], - environmentVariables: { - WEBHOOK_URL: `${service.fqdn}` - } - } - }; - if (serviceSecret.length > 0) { - serviceSecret.forEach((secret) => { - config.n8n.environmentVariables[secret.name] = secret.value; - }); - } - const { volumeMounts } = persistentVolumes(id, persistentStorage, config) - const composeFile: ComposeFile = { - version: '3.8', - services: { - [id]: { - container_name: id, - image: config.n8n.image, - volumes: config.n8n.volumes, - environment: config.n8n.environmentVariables, - labels: makeLabelForServices('n8n'), - ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), - ...defaultComposeConfiguration(network), - } - }, - networks: { - [network]: { - external: true - } - }, - volumes: volumeMounts - }; - const composeFileDestination = `${workdir}/docker-compose.yaml`; - await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - await startServiceContainers(destinationDocker.id, composeFileDestination) - return {} - } catch ({ status, message }) { - return errorHandler({ status, message }) - } -} - -async function startUptimekumaService(request: FastifyRequest) { - try { - const { id } = request.params; - const teamId = request.user.teamId; - const service = await getServiceFromDB({ id, teamId }); - const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort, persistentStorage } = - service; - const network = destinationDockerId && destinationDocker.network; - const port = getServiceMainPort('uptimekuma'); - - const { workdir } = await createDirectories({ repository: type, buildId: id }); - const image = getServiceImage(type); - - const config = { - uptimekuma: { - image: `${image}:${version}`, - volumes: [`${id}-uptimekuma:/app/data`], - environmentVariables: {} - } - }; - if (serviceSecret.length > 0) { - serviceSecret.forEach((secret) => { - config.uptimekuma.environmentVariables[secret.name] = secret.value; - }); - } - const { volumeMounts } = persistentVolumes(id, persistentStorage, config) - const composeFile: ComposeFile = { - version: '3.8', - services: { - [id]: { - container_name: id, - image: config.uptimekuma.image, - volumes: config.uptimekuma.volumes, - environment: config.uptimekuma.environmentVariables, - ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), - labels: makeLabelForServices('uptimekuma'), - ...defaultComposeConfiguration(network), - } - }, - networks: { - [network]: { - external: true - } - }, - volumes: volumeMounts - }; - const composeFileDestination = `${workdir}/docker-compose.yaml`; - await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - await startServiceContainers(destinationDocker.id, composeFileDestination) - return {} - } catch ({ status, message }) { - return errorHandler({ status, message }) - } -} - -async function startGhostService(request: FastifyRequest) { - try { - const { id } = request.params; - const teamId = request.user.teamId; - const service = await getServiceFromDB({ id, teamId }); - const { - type, - version, - destinationDockerId, - destinationDocker, - serviceSecret, - persistentStorage, - exposePort, - fqdn, - ghost: { - defaultEmail, - defaultPassword, - mariadbRootUser, - mariadbRootUserPassword, - mariadbDatabase, - mariadbPassword, - mariadbUser - } - } = service; - const network = destinationDockerId && destinationDocker.network; - - const { workdir } = await createDirectories({ repository: type, buildId: id }); - const image = getServiceImage(type); - const domain = getDomain(fqdn); - const port = getServiceMainPort('ghost'); - const isHttps = fqdn.startsWith('https://'); - const config = { - ghost: { - image: `${image}:${version}`, - volumes: [`${id}-ghost:/bitnami/ghost`], - environmentVariables: { - url: fqdn, - GHOST_HOST: domain, - GHOST_ENABLE_HTTPS: isHttps ? 'yes' : 'no', - GHOST_EMAIL: defaultEmail, - GHOST_PASSWORD: defaultPassword, - GHOST_DATABASE_HOST: `${id}-mariadb`, - GHOST_DATABASE_USER: mariadbUser, - GHOST_DATABASE_PASSWORD: mariadbPassword, - GHOST_DATABASE_NAME: mariadbDatabase, - GHOST_DATABASE_PORT_NUMBER: 3306 - } - }, - mariadb: { - image: `bitnami/mariadb:latest`, - volumes: [`${id}-mariadb:/bitnami/mariadb`], - environmentVariables: { - MARIADB_USER: mariadbUser, - MARIADB_PASSWORD: mariadbPassword, - MARIADB_DATABASE: mariadbDatabase, - MARIADB_ROOT_USER: mariadbRootUser, - MARIADB_ROOT_PASSWORD: mariadbRootUserPassword - } - } - }; - if (serviceSecret.length > 0) { - serviceSecret.forEach((secret) => { - config.ghost.environmentVariables[secret.name] = secret.value; - }); - } - - const { volumeMounts } = persistentVolumes(id, persistentStorage, config) - const composeFile: ComposeFile = { - version: '3.8', - services: { - [id]: { - container_name: id, - image: config.ghost.image, - volumes: config.ghost.volumes, - environment: config.ghost.environmentVariables, - ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), - labels: makeLabelForServices('ghost'), - depends_on: [`${id}-mariadb`], - ...defaultComposeConfiguration(network), - }, - [`${id}-mariadb`]: { - container_name: `${id}-mariadb`, - image: config.mariadb.image, - volumes: config.mariadb.volumes, - environment: config.mariadb.environmentVariables, - ...defaultComposeConfiguration(network), - } - }, - networks: { - [network]: { - external: true - } - }, - volumes: volumeMounts - }; - const composeFileDestination = `${workdir}/docker-compose.yaml`; - await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - await startServiceContainers(destinationDocker.id, composeFileDestination) - return {} - } catch ({ status, message }) { - return errorHandler({ status, message }) - } -} - -async function startMeilisearchService(request: FastifyRequest) { - try { - const { id } = request.params; - const teamId = request.user.teamId; - const service = await getServiceFromDB({ id, teamId }); - const { - meiliSearch: { masterKey } - } = service; - const { type, version, destinationDockerId, destinationDocker, - serviceSecret, exposePort, persistentStorage } = service; - const network = destinationDockerId && destinationDocker.network; - const port = getServiceMainPort('meilisearch'); - - const { workdir } = await createDirectories({ repository: type, buildId: id }); - const image = getServiceImage(type); - - const config = { - meilisearch: { - image: `${image}:${version}`, - volumes: [`${id}-datams:/meili_data/data.ms`, `${id}-data:/meili_data `], - environmentVariables: { - MEILI_MASTER_KEY: masterKey - } - } - }; - - if (serviceSecret.length > 0) { - serviceSecret.forEach((secret) => { - config.meilisearch.environmentVariables[secret.name] = secret.value; - }); - } - const { volumeMounts } = persistentVolumes(id, persistentStorage, config) - const composeFile: ComposeFile = { - version: '3.8', - services: { - [id]: { - container_name: id, - image: config.meilisearch.image, - environment: config.meilisearch.environmentVariables, - ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), - volumes: config.meilisearch.volumes, - labels: makeLabelForServices('meilisearch'), - ...defaultComposeConfiguration(network), - } - }, - networks: { - [network]: { - external: true - } - }, - volumes: volumeMounts - }; - const composeFileDestination = `${workdir}/docker-compose.yaml`; - await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - await startServiceContainers(destinationDocker.id, composeFileDestination) - return {} - } catch ({ status, message }) { - return errorHandler({ status, message }) - } -} - -async function startUmamiService(request: FastifyRequest) { - try { - const { id } = request.params; - const teamId = request.user.teamId; - const service = await getServiceFromDB({ id, teamId }); - const { - type, - version, - destinationDockerId, - destinationDocker, - serviceSecret, - persistentStorage, - exposePort, - umami: { - umamiAdminPassword, - postgresqlUser, - postgresqlPassword, - postgresqlDatabase, - hashSalt - } - } = service; - const network = destinationDockerId && destinationDocker.network; - const port = getServiceMainPort('umami'); - - const { workdir } = await createDirectories({ repository: type, buildId: id }); - const image = getServiceImage(type); - - const config = { - umami: { - image: `${image}:${version}`, - environmentVariables: { - DATABASE_URL: `postgresql://${postgresqlUser}:${postgresqlPassword}@${id}-postgresql:5432/${postgresqlDatabase}`, - DATABASE_TYPE: 'postgresql', - HASH_SALT: hashSalt - } - }, - postgresql: { - image: 'postgres:12-alpine', - volumes: [`${id}-postgresql-data:/var/lib/postgresql/data`], - environmentVariables: { - POSTGRES_USER: postgresqlUser, - POSTGRES_PASSWORD: postgresqlPassword, - POSTGRES_DB: postgresqlDatabase - } - } - }; - if (serviceSecret.length > 0) { - serviceSecret.forEach((secret) => { - config.umami.environmentVariables[secret.name] = secret.value; - }); - } - - const initDbSQL = ` - -- CreateTable -CREATE TABLE "account" ( - "user_id" SERIAL NOT NULL, - "username" VARCHAR(255) NOT NULL, - "password" VARCHAR(60) NOT NULL, - "is_admin" BOOLEAN NOT NULL DEFAULT false, - "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, - - PRIMARY KEY ("user_id") -); - --- CreateTable -CREATE TABLE "event" ( - "event_id" SERIAL NOT NULL, - "website_id" INTEGER NOT NULL, - "session_id" INTEGER NOT NULL, - "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, - "url" VARCHAR(500) NOT NULL, - "event_type" VARCHAR(50) NOT NULL, - "event_value" VARCHAR(50) NOT NULL, - - PRIMARY KEY ("event_id") -); - --- CreateTable -CREATE TABLE "pageview" ( - "view_id" SERIAL NOT NULL, - "website_id" INTEGER NOT NULL, - "session_id" INTEGER NOT NULL, - "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, - "url" VARCHAR(500) NOT NULL, - "referrer" VARCHAR(500), - - PRIMARY KEY ("view_id") -); - --- CreateTable -CREATE TABLE "session" ( - "session_id" SERIAL NOT NULL, - "session_uuid" UUID NOT NULL, - "website_id" INTEGER NOT NULL, - "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, - "hostname" VARCHAR(100), - "browser" VARCHAR(20), - "os" VARCHAR(20), - "device" VARCHAR(20), - "screen" VARCHAR(11), - "language" VARCHAR(35), - "country" CHAR(2), - - PRIMARY KEY ("session_id") -); - --- CreateTable -CREATE TABLE "website" ( - "website_id" SERIAL NOT NULL, - "website_uuid" UUID NOT NULL, - "user_id" INTEGER NOT NULL, - "name" VARCHAR(100) NOT NULL, - "domain" VARCHAR(500), - "share_id" VARCHAR(64), - "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, - - PRIMARY KEY ("website_id") -); - --- CreateIndex -CREATE UNIQUE INDEX "account.username_unique" ON "account"("username"); - --- CreateIndex -CREATE INDEX "event_created_at_idx" ON "event"("created_at"); - --- CreateIndex -CREATE INDEX "event_session_id_idx" ON "event"("session_id"); - --- CreateIndex -CREATE INDEX "event_website_id_idx" ON "event"("website_id"); - --- CreateIndex -CREATE INDEX "pageview_created_at_idx" ON "pageview"("created_at"); - --- CreateIndex -CREATE INDEX "pageview_session_id_idx" ON "pageview"("session_id"); - --- CreateIndex -CREATE INDEX "pageview_website_id_created_at_idx" ON "pageview"("website_id", "created_at"); - --- CreateIndex -CREATE INDEX "pageview_website_id_idx" ON "pageview"("website_id"); - --- CreateIndex -CREATE INDEX "pageview_website_id_session_id_created_at_idx" ON "pageview"("website_id", "session_id", "created_at"); - --- CreateIndex -CREATE UNIQUE INDEX "session.session_uuid_unique" ON "session"("session_uuid"); - --- CreateIndex -CREATE INDEX "session_created_at_idx" ON "session"("created_at"); - --- CreateIndex -CREATE INDEX "session_website_id_idx" ON "session"("website_id"); - --- CreateIndex -CREATE UNIQUE INDEX "website.website_uuid_unique" ON "website"("website_uuid"); - --- CreateIndex -CREATE UNIQUE INDEX "website.share_id_unique" ON "website"("share_id"); - --- CreateIndex -CREATE INDEX "website_user_id_idx" ON "website"("user_id"); - --- AddForeignKey -ALTER TABLE "event" ADD FOREIGN KEY ("session_id") REFERENCES "session"("session_id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "event" ADD FOREIGN KEY ("website_id") REFERENCES "website"("website_id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "pageview" ADD FOREIGN KEY ("session_id") REFERENCES "session"("session_id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "pageview" ADD FOREIGN KEY ("website_id") REFERENCES "website"("website_id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "session" ADD FOREIGN KEY ("website_id") REFERENCES "website"("website_id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "website" ADD FOREIGN KEY ("user_id") REFERENCES "account"("user_id") ON DELETE CASCADE ON UPDATE CASCADE; - - insert into account (username, password, is_admin) values ('admin', '${bcrypt.hashSync( - umamiAdminPassword, - 10 - )}', true);`; - await fs.writeFile(`${workdir}/schema.postgresql.sql`, initDbSQL); - const Dockerfile = ` - FROM ${config.postgresql.image} - COPY ./schema.postgresql.sql /docker-entrypoint-initdb.d/schema.postgresql.sql`; - await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile); - const { volumeMounts } = persistentVolumes(id, persistentStorage, config) - const composeFile: ComposeFile = { - version: '3.8', - services: { - [id]: { - container_name: id, - image: config.umami.image, - environment: config.umami.environmentVariables, - ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), - labels: makeLabelForServices('umami'), - depends_on: [`${id}-postgresql`], - ...defaultComposeConfiguration(network), - }, - [`${id}-postgresql`]: { - build: workdir, - container_name: `${id}-postgresql`, - environment: config.postgresql.environmentVariables, - volumes: config.postgresql.volumes, - ...defaultComposeConfiguration(network), - } - }, - networks: { - [network]: { - external: true - } - }, - volumes: volumeMounts - }; - const composeFileDestination = `${workdir}/docker-compose.yaml`; - await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - await startServiceContainers(destinationDocker.id, composeFileDestination) - return {} - } catch ({ status, message }) { - return errorHandler({ status, message }) - } -} - -async function startHasuraService(request: FastifyRequest) { - try { - const { id } = request.params; - const teamId = request.user.teamId; - const service = await getServiceFromDB({ id, teamId }); - const { - type, - version, - destinationDockerId, - destinationDocker, - persistentStorage, - serviceSecret, - exposePort, - hasura: { postgresqlUser, postgresqlPassword, postgresqlDatabase } - } = service; - const network = destinationDockerId && destinationDocker.network; - const port = getServiceMainPort('hasura'); - - const { workdir } = await createDirectories({ repository: type, buildId: id }); - const image = getServiceImage(type); - - const config = { - hasura: { - image: `${image}:${version}`, - environmentVariables: { - HASURA_GRAPHQL_METADATA_DATABASE_URL: `postgresql://${postgresqlUser}:${postgresqlPassword}@${id}-postgresql:5432/${postgresqlDatabase}` - } - }, - postgresql: { - image: 'postgres:12-alpine', - volumes: [`${id}-postgresql-data:/var/lib/postgresql/data`], - environmentVariables: { - POSTGRES_USER: postgresqlUser, - POSTGRES_PASSWORD: postgresqlPassword, - POSTGRES_DB: postgresqlDatabase - } - } - }; - if (serviceSecret.length > 0) { - serviceSecret.forEach((secret) => { - config.hasura.environmentVariables[secret.name] = secret.value; - }); - } - - const { volumeMounts } = persistentVolumes(id, persistentStorage, config) - const composeFile: ComposeFile = { - version: '3.8', - services: { - [id]: { - container_name: id, - image: config.hasura.image, - environment: config.hasura.environmentVariables, - labels: makeLabelForServices('hasura'), - ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), - depends_on: [`${id}-postgresql`], - ...defaultComposeConfiguration(network), - }, - [`${id}-postgresql`]: { - image: config.postgresql.image, - container_name: `${id}-postgresql`, - environment: config.postgresql.environmentVariables, - volumes: config.postgresql.volumes, - ...defaultComposeConfiguration(network), - } - }, - networks: { - [network]: { - external: true - } - }, - volumes: volumeMounts - }; - const composeFileDestination = `${workdir}/docker-compose.yaml`; - await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - await startServiceContainers(destinationDocker.id, composeFileDestination) - return {} - } catch ({ status, message }) { - return errorHandler({ status, message }) - } -} - -async function startFiderService(request: FastifyRequest) { - try { - const { id } = request.params; - const teamId = request.user.teamId; - const service = await getServiceFromDB({ id, teamId }); - const { - type, - version, - fqdn, - destinationDockerId, - destinationDocker, - serviceSecret, - persistentStorage, - exposePort, - fider: { - postgresqlUser, - postgresqlPassword, - postgresqlDatabase, - jwtSecret, - emailNoreply, - emailMailgunApiKey, - emailMailgunDomain, - emailMailgunRegion, - emailSmtpHost, - emailSmtpPort, - emailSmtpUser, - emailSmtpPassword, - emailSmtpEnableStartTls - } - } = service; - const network = destinationDockerId && destinationDocker.network; - const port = getServiceMainPort('fider'); - - const { workdir } = await createDirectories({ repository: type, buildId: id }); - const image = getServiceImage(type); - const config = { - fider: { - image: `${image}:${version}`, - environmentVariables: { - BASE_URL: fqdn, - DATABASE_URL: `postgresql://${postgresqlUser}:${postgresqlPassword}@${id}-postgresql:5432/${postgresqlDatabase}?sslmode=disable`, - JWT_SECRET: `${jwtSecret.replace(/\$/g, '$$$')}`, - EMAIL_NOREPLY: emailNoreply, - EMAIL_MAILGUN_API: emailMailgunApiKey, - EMAIL_MAILGUN_REGION: emailMailgunRegion, - EMAIL_MAILGUN_DOMAIN: emailMailgunDomain, - EMAIL_SMTP_HOST: emailSmtpHost, - EMAIL_SMTP_PORT: emailSmtpPort, - EMAIL_SMTP_USER: emailSmtpUser, - EMAIL_SMTP_PASSWORD: emailSmtpPassword, - EMAIL_SMTP_ENABLE_STARTTLS: emailSmtpEnableStartTls - } - }, - postgresql: { - image: 'postgres:12-alpine', - volumes: [`${id}-postgresql-data:/var/lib/postgresql/data`], - environmentVariables: { - POSTGRES_USER: postgresqlUser, - POSTGRES_PASSWORD: postgresqlPassword, - POSTGRES_DB: postgresqlDatabase - } - } - }; - if (serviceSecret.length > 0) { - serviceSecret.forEach((secret) => { - config.fider.environmentVariables[secret.name] = secret.value; - }); - } - const { volumeMounts } = persistentVolumes(id, persistentStorage, config) - const composeFile: ComposeFile = { - version: '3.8', - services: { - [id]: { - container_name: id, - image: config.fider.image, - environment: config.fider.environmentVariables, - labels: makeLabelForServices('fider'), - ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), - depends_on: [`${id}-postgresql`], - ...defaultComposeConfiguration(network), - }, - [`${id}-postgresql`]: { - image: config.postgresql.image, - container_name: `${id}-postgresql`, - environment: config.postgresql.environmentVariables, - volumes: config.postgresql.volumes, - ...defaultComposeConfiguration(network), - } - }, - networks: { - [network]: { - external: true - } - }, - volumes: volumeMounts - }; - const composeFileDestination = `${workdir}/docker-compose.yaml`; - await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - await startServiceContainers(destinationDocker.id, composeFileDestination) - return {} - } catch ({ status, message }) { - return errorHandler({ status, message }) - } -} - -async function startAppWriteService(request: FastifyRequest) { - try { - const { id } = request.params; - const teamId = request.user.teamId; - const { version, fqdn, destinationDocker, secrets, exposePort, network, port, workdir, image, appwrite } = await defaultServiceConfigurations({ id, teamId }) - - const { - opensslKeyV1, - executorSecret, - mariadbHost, - mariadbPort, - mariadbUser, - mariadbPassword, - mariadbRootUser, - mariadbRootUserPassword, - mariadbDatabase - } = appwrite; - - const dockerCompose = { - [id]: { - image: `${image}:${version}`, - container_name: id, - labels: makeLabelForServices('appwrite'), - ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), - volumes: [ - `${id}-uploads:/storage/uploads:rw`, - `${id}-cache:/storage/cache:rw`, - `${id}-config:/storage/config:rw`, - `${id}-certificates:/storage/certificates:rw`, - `${id}-functions:/storage/functions:rw` - ], - depends_on: [ - `${id}-mariadb`, - `${id}-redis`, - `${id}-influxdb`, - ], - environment: [ - "_APP_ENV=production", - "_APP_LOCALE=en", - `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, - `_APP_DOMAIN=${fqdn}`, - `_APP_DOMAIN_TARGET=${fqdn}`, - `_APP_REDIS_HOST=${id}-redis`, - "_APP_REDIS_PORT=6379", - `_APP_DB_HOST=${mariadbHost}`, - `_APP_DB_PORT=${mariadbPort}`, - `_APP_DB_SCHEMA=${mariadbDatabase}`, - `_APP_DB_USER=${mariadbUser}`, - `_APP_DB_PASS=${mariadbPassword}`, - `_APP_INFLUXDB_HOST=${id}-influxdb`, - "_APP_INFLUXDB_PORT=8086", - `_APP_EXECUTOR_SECRET=${executorSecret}`, - `_APP_EXECUTOR_HOST=http://${id}-executor/v1`, - `_APP_STATSD_HOST=${id}-telegraf`, - "_APP_STATSD_PORT=8125", - `OPEN_RUNTIMES_NETWORK=${network}`, - ...secrets - ], - ...defaultComposeConfiguration(network), - }, - [`${id}-realtime`]: { - image: `${image}:${version}`, - container_name: `${id}-realtime`, - entrypoint: "realtime", - labels: makeLabelForServices('appwrite'), - depends_on: [ - `${id}-mariadb`, - `${id}-redis`, - ], - environment: [ - "_APP_ENV=production", - `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, - `_APP_REDIS_HOST=${id}-redis`, - "_APP_REDIS_PORT=6379", - `_APP_DB_HOST=${mariadbHost}`, - `_APP_DB_PORT=${mariadbPort}`, - `_APP_DB_SCHEMA=${mariadbDatabase}`, - `_APP_DB_USER=${mariadbUser}`, - `_APP_DB_PASS=${mariadbPassword}`, - `OPEN_RUNTIMES_NETWORK=${network}`, - ...secrets - ], - ...defaultComposeConfiguration(network), - }, - [`${id}-worker-audits`]: { - image: `${image}:${version}`, - container_name: `${id}-worker-audits`, - labels: makeLabelForServices('appwrite'), - entrypoint: "worker-audits", - depends_on: [ - `${id}-mariadb`, - `${id}-redis`, - ], - environment: [ - "_APP_ENV=production", - `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, - `_APP_REDIS_HOST=${id}-redis`, - "_APP_REDIS_PORT=6379", - `_APP_DB_HOST=${mariadbHost}`, - `_APP_DB_PORT=${mariadbPort}`, - `_APP_DB_SCHEMA=${mariadbDatabase}`, - `_APP_DB_USER=${mariadbUser}`, - `_APP_DB_PASS=${mariadbPassword}`, - `OPEN_RUNTIMES_NETWORK=${network}`, - ...secrets - ], - ...defaultComposeConfiguration(network), - }, - [`${id}-worker-webhooks`]: { - image: `${image}:${version}`, - container_name: `${id}-worker-webhooks`, - labels: makeLabelForServices('appwrite'), - entrypoint: "worker-webhooks", - depends_on: [ - `${id}-mariadb`, - `${id}-redis`, - ], - environment: [ - "_APP_ENV=production", - `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, - `_APP_REDIS_HOST=${id}-redis`, - "_APP_REDIS_PORT=6379", - `OPEN_RUNTIMES_NETWORK=${network}`, - ...secrets - ], - ...defaultComposeConfiguration(network), - }, - [`${id}-worker-deletes`]: { - image: `${image}:${version}`, - container_name: `${id}-worker-deletes`, - labels: makeLabelForServices('appwrite'), - entrypoint: "worker-deletes", - depends_on: [ - `${id}-mariadb`, - `${id}-redis`, - ], - volumes: [ - `${id}-uploads:/storage/uploads:rw`, - `${id}-cache:/storage/cache:rw`, - `${id}-config:/storage/config:rw`, - `${id}-certificates:/storage/certificates:rw`, - `${id}-functions:/storage/functions:rw`, - `${id}-builds:/storage/builds:rw`, - ], - "environment": [ - "_APP_ENV=production", - `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, - `_APP_REDIS_HOST=${id}-redis`, - "_APP_REDIS_PORT=6379", - `_APP_DB_HOST=${mariadbHost}`, - `_APP_DB_PORT=${mariadbPort}`, - `_APP_DB_SCHEMA=${mariadbDatabase}`, - `_APP_DB_USER=${mariadbUser}`, - `_APP_DB_PASS=${mariadbPassword}`, - `_APP_EXECUTOR_SECRET=${executorSecret}`, - `_APP_EXECUTOR_HOST=http://${id}-executor/v1`, - `OPEN_RUNTIMES_NETWORK=${network}`, - ...secrets - ], - ...defaultComposeConfiguration(network), - }, - [`${id}-worker-databases`]: { - image: `${image}:${version}`, - container_name: `${id}-worker-databases`, - labels: makeLabelForServices('appwrite'), - entrypoint: "worker-databases", - depends_on: [ - `${id}-mariadb`, - `${id}-redis`, - ], - environment: [ - "_APP_ENV=production", - `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, - `_APP_REDIS_HOST=${id}-redis`, - "_APP_REDIS_PORT=6379", - `_APP_DB_HOST=${mariadbHost}`, - `_APP_DB_PORT=${mariadbPort}`, - `_APP_DB_SCHEMA=${mariadbDatabase}`, - `_APP_DB_USER=${mariadbUser}`, - `_APP_DB_PASS=${mariadbPassword}`, - `OPEN_RUNTIMES_NETWORK=${network}`, - ...secrets - ], - ...defaultComposeConfiguration(network), - }, - [`${id}-worker-builds`]: { - image: `${image}:${version}`, - container_name: `${id}-worker-builds`, - labels: makeLabelForServices('appwrite'), - entrypoint: "worker-builds", - depends_on: [ - `${id}-mariadb`, - `${id}-redis`, - ], - environment: [ - "_APP_ENV=production", - `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, - `_APP_EXECUTOR_SECRET=${executorSecret}`, - `_APP_EXECUTOR_HOST=http://${id}-executor/v1`, - `_APP_REDIS_HOST=${id}-redis`, - "_APP_REDIS_PORT=6379", - `_APP_DB_HOST=${mariadbHost}`, - `_APP_DB_PORT=${mariadbPort}`, - `_APP_DB_SCHEMA=${mariadbDatabase}`, - `_APP_DB_USER=${mariadbUser}`, - `_APP_DB_PASS=${mariadbPassword}`, - `OPEN_RUNTIMES_NETWORK=${network}`, - ...secrets - ], - ...defaultComposeConfiguration(network), - }, - [`${id}-worker-certificates`]: { - image: `${image}:${version}`, - container_name: `${id}-worker-certificates`, - labels: makeLabelForServices('appwrite'), - entrypoint: "worker-certificates", - depends_on: [ - `${id}-mariadb`, - `${id}-redis`, - ], - volumes: [ - `${id}-config:/storage/config:rw`, - `${id}-certificates:/storage/certificates:rw`, - ], - environment: [ - "_APP_ENV=production", - `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, - `_APP_DOMAIN=${fqdn}`, - `_APP_DOMAIN_TARGET=${fqdn}`, - `_APP_REDIS_HOST=${id}-redis`, - "_APP_REDIS_PORT=6379", - `_APP_DB_HOST=${mariadbHost}`, - `_APP_DB_PORT=${mariadbPort}`, - `_APP_DB_SCHEMA=${mariadbDatabase}`, - `_APP_DB_USER=${mariadbUser}`, - `_APP_DB_PASS=${mariadbPassword}`, - `OPEN_RUNTIMES_NETWORK=${network}`, - ...secrets - ], - ...defaultComposeConfiguration(network), - }, - [`${id}-worker-functions`]: { - image: `${image}:${version}`, - container_name: `${id}-worker-functions`, - labels: makeLabelForServices('appwrite'), - entrypoint: "worker-functions", - depends_on: [ - `${id}-mariadb`, - `${id}-redis`, - `${id}-executor` - ], - environment: [ - "_APP_ENV=production", - `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, - `_APP_REDIS_HOST=${id}-redis`, - "_APP_REDIS_PORT=6379", - `_APP_DB_HOST=${mariadbHost}`, - `_APP_DB_PORT=${mariadbPort}`, - `_APP_DB_SCHEMA=${mariadbDatabase}`, - `_APP_DB_USER=${mariadbUser}`, - `_APP_DB_PASS=${mariadbPassword}`, - `_APP_EXECUTOR_SECRET=${executorSecret}`, - `_APP_EXECUTOR_HOST=http://${id}-executor/v1`, - `OPEN_RUNTIMES_NETWORK=${network}`, - ...secrets - ], - ...defaultComposeConfiguration(network), - }, - [`${id}-executor`]: { - image: `${image}:${version}`, - container_name: `${id}-executor`, - labels: makeLabelForServices('appwrite'), - entrypoint: "executor", - stop_signal: "SIGINT", - volumes: [ - `${id}-functions:/storage/functions:rw`, - `${id}-builds:/storage/builds:rw`, - "/var/run/docker.sock:/var/run/docker.sock", - "/tmp:/tmp:rw" - ], - depends_on: [ - `${id}-mariadb`, - `${id}-redis`, - `${id}` - ], - environment: [ - "_APP_ENV=production", - `_APP_EXECUTOR_SECRET=${executorSecret}`, - `OPEN_RUNTIMES_NETWORK=${network}`, - ...secrets - ], - ...defaultComposeConfiguration(network), - }, - [`${id}-worker-mails`]: { - image: `${image}:${version}`, - container_name: `${id}-worker-mails`, - labels: makeLabelForServices('appwrite'), - entrypoint: "worker-mails", - depends_on: [ - `${id}-redis`, - ], - environment: [ - "_APP_ENV=production", - `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, - `_APP_REDIS_HOST=${id}-redis`, - "_APP_REDIS_PORT=6379", - `OPEN_RUNTIMES_NETWORK=${network}`, - ...secrets - ], - ...defaultComposeConfiguration(network), - }, - [`${id}-worker-messaging`]: { - image: `${image}:${version}`, - container_name: `${id}-worker-messaging`, - labels: makeLabelForServices('appwrite'), - entrypoint: "worker-messaging", - depends_on: [ - `${id}-redis`, - ], - environment: [ - "_APP_ENV=production", - `_APP_REDIS_HOST=${id}-redis`, - "_APP_REDIS_PORT=6379", - `OPEN_RUNTIMES_NETWORK=${network}`, - ...secrets - ], - ...defaultComposeConfiguration(network), - }, - [`${id}-maintenance`]: { - image: `${image}:${version}`, - container_name: `${id}-maintenance`, - labels: makeLabelForServices('appwrite'), - entrypoint: "maintenance", - depends_on: [ - `${id}-redis`, - ], - environment: [ - "_APP_ENV=production", - `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, - `_APP_DOMAIN=${fqdn}`, - `_APP_DOMAIN_TARGET=${fqdn}`, - `_APP_REDIS_HOST=${id}-redis`, - "_APP_REDIS_PORT=6379", - `_APP_DB_HOST=${mariadbHost}`, - `_APP_DB_PORT=${mariadbPort}`, - `_APP_DB_SCHEMA=${mariadbDatabase}`, - `_APP_DB_USER=${mariadbUser}`, - `_APP_DB_PASS=${mariadbPassword}`, - `OPEN_RUNTIMES_NETWORK=${network}`, - ...secrets - ], - ...defaultComposeConfiguration(network), - }, - [`${id}-schedule`]: { - image: `${image}:${version}`, - container_name: `${id}-schedule`, - labels: makeLabelForServices('appwrite'), - entrypoint: "schedule", - depends_on: [ - `${id}-redis`, - ], - environment: [ - "_APP_ENV=production", - `_APP_REDIS_HOST=${id}-redis`, - "_APP_REDIS_PORT=6379", - `OPEN_RUNTIMES_NETWORK=${network}`, - ...secrets - ], - ...defaultComposeConfiguration(network), - }, - [`${id}-mariadb`]: { - image: "mariadb:10.7", - container_name: `${id}-mariadb`, - labels: makeLabelForServices('appwrite'), - volumes: [ - `${id}-mariadb:/var/lib/mysql:rw` - ], - environment: [ - `MYSQL_ROOT_USER=${mariadbRootUser}`, - `MYSQL_ROOT_PASSWORD=${mariadbRootUserPassword}`, - `MYSQL_USER=${mariadbUser}`, - `MYSQL_PASSWORD=${mariadbPassword}`, - `MYSQL_DATABASE=${mariadbDatabase}`, - `OPEN_RUNTIMES_NETWORK=${network}`, - ], - command: "mysqld --innodb-flush-method=fsync", - ...defaultComposeConfiguration(network), - }, - [`${id}-redis`]: { - image: "redis:6.2-alpine", - container_name: `${id}-redis`, - command: `redis-server --maxmemory 512mb --maxmemory-policy allkeys-lru --maxmemory-samples 5\n`, - volumes: [ - `${id}-redis:/data:rw` - ], - ...defaultComposeConfiguration(network), - }, - [`${id}-usage-timeseries`]: { - image: `${image}:${version}`, - container_name: `${id}-usage`, - labels: makeLabelForServices('appwrite'), - entrypoint: "usage --type=timeseries", - depends_on: [ - `${id}-mariadb`, - `${id}-influxdb`, - ], - environment: [ - "_APP_ENV=production", - `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, - `_APP_DB_HOST=${mariadbHost}`, - `_APP_DB_PORT=${mariadbPort}`, - `_APP_DB_SCHEMA=${mariadbDatabase}`, - `_APP_DB_USER=${mariadbUser}`, - `_APP_DB_PASS=${mariadbPassword}`, - `_APP_INFLUXDB_HOST=${id}-influxdb`, - "_APP_INFLUXDB_PORT=8086", - `_APP_REDIS_HOST=${id}-redis`, - "_APP_REDIS_PORT=6379", - `OPEN_RUNTIMES_NETWORK=${network}`, - ...secrets - ], - ...defaultComposeConfiguration(network), - }, - [`${id}-usage-database`]: { - image: `${image}:${version}`, - container_name: `${id}-usage-database`, - labels: makeLabelForServices('appwrite'), - entrypoint: "usage --type=database", - depends_on: [ - `${id}-mariadb`, - `${id}-influxdb`, - ], - environment: [ - "_APP_ENV=production", - `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, - `_APP_DB_HOST=${mariadbHost}`, - `_APP_DB_PORT=${mariadbPort}`, - `_APP_DB_SCHEMA=${mariadbDatabase}`, - `_APP_DB_USER=${mariadbUser}`, - `_APP_DB_PASS=${mariadbPassword}`, - `_APP_INFLUXDB_HOST=${id}-influxdb`, - "_APP_INFLUXDB_PORT=8086", - `_APP_REDIS_HOST=${id}-redis`, - "_APP_REDIS_PORT=6379", - `OPEN_RUNTIMES_NETWORK=${network}`, - ...secrets - ], - ...defaultComposeConfiguration(network), - }, - [`${id}-influxdb`]: { - image: "appwrite/influxdb:1.5.0", - container_name: `${id}-influxdb`, - volumes: [ - `${id}-influxdb:/var/lib/influxdb:rw` - ], - ...defaultComposeConfiguration(network), - }, - [`${id}-telegraf`]: { - image: "appwrite/telegraf:1.4.0", - container_name: `${id}-telegraf`, - environment: [ - `_APP_INFLUXDB_HOST=${id}-influxdb`, - "_APP_INFLUXDB_PORT=8086", - `OPEN_RUNTIMES_NETWORK=${network}`, - ], - ...defaultComposeConfiguration(network), - } - }; - const composeFile: any = { - version: '3.8', - services: dockerCompose, - networks: { - [network]: { - external: true - } - }, - volumes: { - [`${id}-uploads`]: { - name: `${id}-uploads` - }, - [`${id}-cache`]: { - name: `${id}-cache` - }, - [`${id}-config`]: { - name: `${id}-config` - }, - [`${id}-certificates`]: { - name: `${id}-certificates` - }, - [`${id}-functions`]: { - name: `${id}-functions` - }, - [`${id}-builds`]: { - name: `${id}-builds` - }, - [`${id}-mariadb`]: { - name: `${id}-mariadb` - }, - [`${id}-redis`]: { - name: `${id}-redis` - }, - [`${id}-influxdb`]: { - name: `${id}-influxdb` - } - } - - }; - const composeFileDestination = `${workdir}/docker-compose.yaml`; - await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - await startServiceContainers(destinationDocker.id, composeFileDestination) - return {} - } catch ({ status, message }) { - return errorHandler({ status, message }) - } -} -async function startServiceContainers(dockerId, composeFileDestination) { - try { - await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} pull` }) - } catch (error) { } - await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} build --no-cache` }) - await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} create` }) - await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} start` }) - await asyncSleep(1000); - await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} up -d` }) -} -async function stopServiceContainers(request: FastifyRequest) { try { const { id } = request.params; const teamId = request.user.teamId; @@ -1922,884 +30,193 @@ async function stopServiceContainers(request: FastifyRequest) return errorHandler({ status, message }) } } -async function startMoodleService(request: FastifyRequest) { +export async function startService(request: FastifyRequest, fastify: any) { try { const { id } = request.params; const teamId = request.user.teamId; const service = await getServiceFromDB({ id, teamId }); - const { - type, - version, - fqdn, - destinationDockerId, - destinationDocker, - serviceSecret, - persistentStorage, - exposePort, - moodle: { - defaultUsername, - defaultPassword, - defaultEmail, - mariadbRootUser, - mariadbRootUserPassword, - mariadbDatabase, - mariadbPassword, - mariadbUser - } - } = service; - const network = destinationDockerId && destinationDocker.network; - const port = getServiceMainPort('moodle'); - - const { workdir } = await createDirectories({ repository: type, buildId: id }); - const image = getServiceImage(type); - const config = { - moodle: { - image: `${image}:${version}`, - volumes: [`${id}-data:/bitnami/moodle`], - environmentVariables: { - MOODLE_USERNAME: defaultUsername, - MOODLE_PASSWORD: defaultPassword, - MOODLE_EMAIL: defaultEmail, - MOODLE_DATABASE_HOST: `${id}-mariadb`, - MOODLE_DATABASE_USER: mariadbUser, - MOODLE_DATABASE_PASSWORD: mariadbPassword, - MOODLE_DATABASE_NAME: mariadbDatabase, - MOODLE_REVERSEPROXY: 'yes' - } - }, - mariadb: { - image: 'bitnami/mariadb:latest', - volumes: [`${id}-mariadb-data:/bitnami/mariadb`], - environmentVariables: { - MARIADB_USER: mariadbUser, - MARIADB_PASSWORD: mariadbPassword, - MARIADB_DATABASE: mariadbDatabase, - MARIADB_ROOT_USER: mariadbRootUser, - MARIADB_ROOT_PASSWORD: mariadbRootUserPassword - } - } - }; - if (serviceSecret.length > 0) { - serviceSecret.forEach((secret) => { - config.moodle.environmentVariables[secret.name] = secret.value; - }); - } - const { volumeMounts } = persistentVolumes(id, persistentStorage, config) - const composeFile: ComposeFile = { - version: '3.8', - services: { - [id]: { - container_name: id, - image: config.moodle.image, - environment: config.moodle.environmentVariables, - volumes: config.moodle.volumes, - labels: makeLabelForServices('moodle'), - ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), - depends_on: [`${id}-mariadb`], - ...defaultComposeConfiguration(network), - }, - [`${id}-mariadb`]: { - container_name: `${id}-mariadb`, - image: config.mariadb.image, - environment: config.mariadb.environmentVariables, - volumes: config.mariadb.volumes, - ...defaultComposeConfiguration(network), - depends_on: [] - } - - }, - networks: { - [network]: { - external: true - } - }, - volumes: volumeMounts - - }; - const composeFileDestination = `${workdir}/docker-compose.yaml`; - await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - await startServiceContainers(destinationDocker.id, composeFileDestination) - return {} - } catch ({ status, message }) { - return errorHandler({ status, message }) - } -} - -async function startGlitchTipService(request: FastifyRequest) { - try { - const { id } = request.params; - const teamId = request.user.teamId; - const service = await getServiceFromDB({ id, teamId }); - const { - type, - version, - fqdn, - destinationDockerId, - destinationDocker, - serviceSecret, - persistentStorage, - exposePort, - glitchTip: { - postgresqlDatabase, - postgresqlPassword, - postgresqlUser, - secretKeyBase, - defaultEmail, - defaultUsername, - defaultPassword, - defaultFromEmail, - emailSmtpHost, - emailSmtpPort, - emailSmtpUser, - emailSmtpPassword, - emailSmtpUseTls, - emailSmtpUseSsl, - emailBackend, - mailgunApiKey, - sendgridApiKey, - enableOpenUserRegistration, - } - } = service; - const network = destinationDockerId && destinationDocker.network; - const port = getServiceMainPort('glitchTip'); - - const { workdir } = await createDirectories({ repository: type, buildId: id }); - const image = getServiceImage(type); - - const config = { - glitchTip: { - image: `${image}:${version}`, - environmentVariables: { - PORT: port, - GLITCHTIP_DOMAIN: fqdn, - SECRET_KEY: secretKeyBase, - DATABASE_URL: `postgresql://${postgresqlUser}:${postgresqlPassword}@${id}-postgresql:5432/${postgresqlDatabase}`, - REDIS_URL: `redis://${id}-redis:6379/0`, - DEFAULT_FROM_EMAIL: defaultFromEmail, - EMAIL_HOST: emailSmtpHost, - EMAIL_PORT: emailSmtpPort, - EMAIL_HOST_USER: emailSmtpUser, - EMAIL_HOST_PASSWORD: emailSmtpPassword, - EMAIL_USE_TLS: emailSmtpUseTls ? 'True' : 'False', - EMAIL_USE_SSL: emailSmtpUseSsl ? 'True' : 'False', - EMAIL_BACKEND: emailBackend, - MAILGUN_API_KEY: mailgunApiKey, - SENDGRID_API_KEY: sendgridApiKey, - ENABLE_OPEN_USER_REGISTRATION: enableOpenUserRegistration, - DJANGO_SUPERUSER_EMAIL: defaultEmail, - DJANGO_SUPERUSER_USERNAME: defaultUsername, - DJANGO_SUPERUSER_PASSWORD: defaultPassword, - } - }, - postgresql: { - image: 'postgres:14-alpine', - volumes: [`${id}-postgresql-data:/var/lib/postgresql/data`], - environmentVariables: { - POSTGRES_USER: postgresqlUser, - POSTGRES_PASSWORD: postgresqlPassword, - POSTGRES_DB: postgresqlDatabase - } - }, - redis: { - image: 'redis:7-alpine', - volumes: [`${id}-redis-data:/data`], - } - }; - if (serviceSecret.length > 0) { - serviceSecret.forEach((secret) => { - config.glitchTip.environmentVariables[secret.name] = secret.value; - }); - } - const { volumeMounts } = persistentVolumes(id, persistentStorage, config) - const composeFile: ComposeFile = { - version: '3.8', - services: { - [id]: { - container_name: id, - image: config.glitchTip.image, - environment: config.glitchTip.environmentVariables, - labels: makeLabelForServices('glitchTip'), - ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), - depends_on: [`${id}-postgresql`, `${id}-redis`], - ...defaultComposeConfiguration(network), - }, - [`${id}-worker`]: { - container_name: `${id}-worker`, - image: config.glitchTip.image, - command: './bin/run-celery-with-beat.sh', - environment: config.glitchTip.environmentVariables, - depends_on: [`${id}-postgresql`, `${id}-redis`], - ...defaultComposeConfiguration(network), - }, - [`${id}-setup`]: { - container_name: `${id}-setup`, - image: config.glitchTip.image, - command: 'sh -c "(./manage.py migrate || true) && (./manage.py createsuperuser --noinput || true)"', - environment: config.glitchTip.environmentVariables, - networks: [network], - restart: "no", - depends_on: [`${id}-postgresql`, `${id}-redis`] - }, - [`${id}-postgresql`]: { - image: config.postgresql.image, - container_name: `${id}-postgresql`, - environment: config.postgresql.environmentVariables, - volumes: config.postgresql.volumes, - ...defaultComposeConfiguration(network), - }, - [`${id}-redis`]: { - image: config.redis.image, - container_name: `${id}-redis`, - volumes: config.redis.volumes, - ...defaultComposeConfiguration(network), - } - }, - networks: { - [network]: { - external: true - } - }, - volumes: volumeMounts - }; - const composeFileDestination = `${workdir}/docker-compose.yaml`; - await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} pull` }) - await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up --build -d` }) - return {} - } catch ({ status, message }) { - return errorHandler({ status, message }) - } -} - -async function startSearXNGService(request: FastifyRequest) { - try { - const { id } = request.params; - const teamId = request.user.teamId; - const service = await getServiceFromDB({ id, teamId }); - const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort, persistentStorage, fqdn, searxng: { secretKey, redisPassword } } = + const arm = isARM(service.arch); + const { type, destinationDockerId, destinationDocker, persistentStorage, exposePort } = service; - const network = destinationDockerId && destinationDocker.network; - const port = getServiceMainPort('searxng'); const { workdir } = await createDirectories({ repository: type, buildId: id }); - const image = getServiceImage(type); - - const config = { - searxng: { - image: `${image}:${version}`, - volumes: [`${id}-searxng:/etc/searxng`], - environmentVariables: { - SEARXNG_BASE_URL: `${fqdn}` - }, - }, - redis: { - image: 'redis:7-alpine', + const template: any = await parseAndFindServiceTemplates(service, workdir, true) + const network = destinationDockerId && destinationDocker.network; + const config = {}; + for (const s in template.services) { + let newEnvironments = [] + if (arm) { + if (template.services[s]?.environmentArm?.length > 0) { + for (const environment of template.services[s].environmentArm) { + let [env, ...value] = environment.split("="); + value = value.join("=") + if (!value.startsWith('$$secret') && value !== '') { + newEnvironments.push(`${env}=${value}`) + } + } + } + } else { + if (template.services[s]?.environment?.length > 0) { + for (const environment of template.services[s].environment) { + let [env, ...value] = environment.split("="); + value = value.join("=") + if (!value.startsWith('$$secret') && value !== '') { + newEnvironments.push(`${env}=${value}`) + } + } + } + } + const secrets = await verifyAndDecryptServiceSecrets(id) + for (const secret of secrets) { + const { name, value } = secret + if (value) { + const foundEnv = !!template.services[s].environment?.find(env => env.startsWith(`${name}=`)) + const foundNewEnv = !!newEnvironments?.find(env => env.startsWith(`${name}=`)) + if (foundEnv && !foundNewEnv) { + newEnvironments.push(`${name}=${value}`) + } + if (!foundEnv && !foundNewEnv && s === id) { + newEnvironments.push(`${name}=${value}`) + } + } + } + const customVolumes = await prisma.servicePersistentStorage.findMany({ where: { serviceId: id } }) + let volumes = new Set() + if (arm) { + template.services[s]?.volumesArm && template.services[s].volumesArm.length > 0 && template.services[s].volumesArm.forEach(v => volumes.add(v)) + } else { + template.services[s]?.volumes && template.services[s].volumes.length > 0 && template.services[s].volumes.forEach(v => volumes.add(v)) } - }; - const settingsYml = ` - # see https://docs.searxng.org/admin/engines/settings.html#use-default-settings - use_default_settings: true - server: - secret_key: ${secretKey} - limiter: true - image_proxy: true - ui: - static_use_hash: true - redis: - url: redis://:${redisPassword}@${id}-redis:6379/0` + // Workaround: old plausible analytics service wrong volume id name + if (service.type === 'plausibleanalytics' && service.plausibleAnalytics?.id) { + let temp = Array.from(volumes) + temp.forEach(a => { + const t = a.replace(service.id, service.plausibleAnalytics.id) + volumes.delete(a) + volumes.add(t) + }) + } - const Dockerfile = ` - FROM ${config.searxng.image} - COPY ./settings.yml /etc/searxng/settings.yml`; - - if (serviceSecret.length > 0) { - serviceSecret.forEach((secret) => { - config.searxng.environmentVariables[secret.name] = secret.value; - }); + if (customVolumes.length > 0) { + for (const customVolume of customVolumes) { + const { volumeName, path, containerId } = customVolume + if (volumes && volumes.size > 0 && !volumes.has(`${volumeName}:${path}`) && containerId === service) { + volumes.add(`${volumeName}:${path}`) + } + } + } + let ports = [] + if (template.services[s].proxy?.length > 0) { + for (const proxy of template.services[s].proxy) { + if (proxy.hostPort) { + ports.push(`${proxy.hostPort}:${proxy.port}`) + } + } + } else { + if (template.services[s].ports?.length === 1) { + for (const port of template.services[s].ports) { + if (exposePort) { + ports.push(`${exposePort}:${port}`) + } + } + } + } + let image = template.services[s].image + if (arm && template.services[s].imageArm) { + image = template.services[s].imageArm + } + config[s] = { + container_name: s, + build: template.services[s].build || undefined, + command: template.services[s].command, + entrypoint: template.services[s]?.entrypoint, + image, + expose: template.services[s].ports, + ports: ports.length > 0 ? ports : undefined, + volumes: Array.from(volumes), + environment: newEnvironments, + depends_on: template.services[s]?.depends_on, + ulimits: template.services[s]?.ulimits, + cap_drop: template.services[s]?.cap_drop, + cap_add: template.services[s]?.cap_add, + labels: makeLabelForServices(type), + ...defaultComposeConfiguration(network), + } + // Generate files for builds + if (template.services[s]?.files?.length > 0) { + if (!config[s].build) { + config[s].build = { + context: workdir, + dockerfile: `Dockerfile.${s}` + } + } + let Dockerfile = ` + FROM ${template.services[s].image}` + for (const file of template.services[s].files) { + const { location, content } = file; + const source = path.join(workdir, location); + await fs.mkdir(path.dirname(source), { recursive: true }); + await fs.writeFile(source, content); + Dockerfile += ` + COPY .${location} ${location}` + } + await fs.writeFile(`${workdir}/Dockerfile.${s}`, Dockerfile); + } } const { volumeMounts } = persistentVolumes(id, persistentStorage, config) const composeFile: ComposeFile = { version: '3.8', - services: { - [id]: { - build: workdir, - container_name: id, - volumes: config.searxng.volumes, - environment: config.searxng.environmentVariables, - ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), - labels: makeLabelForServices('searxng'), - cap_drop: ['ALL'], - cap_add: ['CHOWN', 'SETGID', 'SETUID', 'DAC_OVERRIDE'], - depends_on: [`${id}-redis`], - ...defaultComposeConfiguration(network), - }, - [`${id}-redis`]: { - container_name: `${id}-redis`, - image: config.redis.image, - command: `redis-server --requirepass ${redisPassword} --save "" --appendonly "no"`, - labels: makeLabelForServices('searxng'), - cap_drop: ['ALL'], - cap_add: ['SETGID', 'SETUID', 'DAC_OVERRIDE'], - ...defaultComposeConfiguration(network), - }, - }, + services: config, networks: { [network]: { external: true } }, volumes: volumeMounts - }; + } const composeFileDestination = `${workdir}/docker-compose.yaml`; await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile); - await fs.writeFile(`${workdir}/settings.yml`, settingsYml); - await startServiceContainers(destinationDocker.id, composeFileDestination) + await startServiceContainers(fastify, id, teamId, destinationDocker.id, composeFileDestination) + + // Workaround: Stop old minio proxies + if (service.type === 'minio') { + try { + await executeDockerCmd({ + dockerId: destinationDocker.id, + command: + `docker container ls -a --filter 'name=${id}-' --format {{.ID}}|xargs -r -n 1 docker container stop -t 0` + }); + + } catch (error) { } + try { + await executeDockerCmd({ + dockerId: destinationDocker.id, + command: + `docker container ls -a --filter 'name=${id}-' --format {{.ID}}|xargs -r -n 1 docker container rm -f` + }); + } catch (error) { } + } return {} } catch ({ status, message }) { return errorHandler({ status, message }) } } - - -async function startWeblateService(request: FastifyRequest) { +async function startServiceContainers(fastify, id, teamId, dockerId, composeFileDestination) { try { - const { id } = request.params; - const teamId = request.user.teamId; - const service = await getServiceFromDB({ id, teamId }); - const { - weblate: { adminPassword, postgresqlHost, postgresqlPort, postgresqlUser, postgresqlPassword, postgresqlDatabase } - } = service; - const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort, persistentStorage, fqdn } = - service; - const network = destinationDockerId && destinationDocker.network; - const port = getServiceMainPort('weblate'); - - const { workdir } = await createDirectories({ repository: type, buildId: id }); - const image = getServiceImage(type); - - const config = { - weblate: { - image: `${image}:${version}`, - volumes: [`${id}-data:/app/data`], - environmentVariables: { - WEBLATE_SITE_DOMAIN: getDomain(fqdn), - WEBLATE_ADMIN_PASSWORD: adminPassword, - POSTGRES_PASSWORD: postgresqlPassword, - POSTGRES_USER: postgresqlUser, - POSTGRES_DATABASE: postgresqlDatabase, - POSTGRES_HOST: postgresqlHost, - POSTGRES_PORT: postgresqlPort, - REDIS_HOST: `${id}-redis`, - } - }, - postgresql: { - image: `postgres:14-alpine`, - volumes: [`${id}-postgresql-data:/var/lib/postgresql/data`], - environmentVariables: { - POSTGRES_PASSWORD: postgresqlPassword, - POSTGRES_USER: postgresqlUser, - POSTGRES_DB: postgresqlDatabase, - POSTGRES_HOST: postgresqlHost, - POSTGRES_PORT: postgresqlPort, - } - }, - redis: { - image: `redis:6-alpine`, - volumes: [`${id}-redis-data:/data`], - } - - }; - - if (serviceSecret.length > 0) { - serviceSecret.forEach((secret) => { - config.weblate.environmentVariables[secret.name] = secret.value; - }); - } - const { volumeMounts } = persistentVolumes(id, persistentStorage, config) - const composeFile: ComposeFile = { - version: '3.8', - services: { - [id]: { - container_name: id, - image: config.weblate.image, - environment: config.weblate.environmentVariables, - ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), - volumes: config.weblate.volumes, - labels: makeLabelForServices('weblate'), - ...defaultComposeConfiguration(network), - }, - [`${id}-postgresql`]: { - container_name: `${id}-postgresql`, - image: config.postgresql.image, - environment: config.postgresql.environmentVariables, - ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), - volumes: config.postgresql.volumes, - labels: makeLabelForServices('weblate'), - ...defaultComposeConfiguration(network), - }, - [`${id}-redis`]: { - container_name: `${id}-redis`, - image: config.redis.image, - ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), - volumes: config.redis.volumes, - labels: makeLabelForServices('weblate'), - ...defaultComposeConfiguration(network), - } - }, - networks: { - [network]: { - external: true - } - }, - volumes: volumeMounts - }; - const composeFileDestination = `${workdir}/docker-compose.yaml`; - await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - await startServiceContainers(destinationDocker.id, composeFileDestination) - return {} - } catch ({ status, message }) { - return errorHandler({ status, message }) - } + fastify.io.to(teamId).emit(`start-service`, { serviceId: id, state: 'Pulling images...' }) + await executeDockerCmd({ 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` }) + fastify.io.to(teamId).emit(`start-service`, { serviceId: id, state: 'Creating containers...' }) + await executeDockerCmd({ 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 asyncSleep(1000); + await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} up -d` }) + fastify.io.to(teamId).emit(`start-service`, { serviceId: id, state: 0 }) } - -async function startTaigaService(request: FastifyRequest) { - try { - const { id } = request.params; - const teamId = request.user.teamId; - const service = await getServiceFromDB({ id, teamId }); - const { - taiga: { secretKey, djangoAdminUser, djangoAdminPassword, erlangSecret, rabbitMQUser, rabbitMQPassword, postgresqlHost, postgresqlPort, postgresqlUser, postgresqlPassword, postgresqlDatabase } - } = service; - const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort, persistentStorage, fqdn } = - service; - const network = destinationDockerId && destinationDocker.network; - const port = getServiceMainPort('taiga'); - - const { workdir } = await createDirectories({ repository: type, buildId: id }); - const image = getServiceImage(type); - - const isHttps = fqdn.startsWith('https://'); - const superUserEntrypoint = `#!/bin/sh - set -e - python manage.py makemigrations - python manage.py migrate - - if [ "$DJANGO_SUPERUSER_USERNAME" ] - then - python manage.py createsuperuser \ - --noinput \ - --username $DJANGO_SUPERUSER_USERNAME \ - --email $DJANGO_SUPERUSER_EMAIL - fi - exec "$@"`; - const entrypoint = `#!/bin/sh - set -e - - /taiga-back/docker/entrypoint_superuser.sh || echo "Superuser creation failed, but continue" - /taiga-back/docker/entrypoint.sh - - exec "$@"`; - - const DockerfileBack = ` - FROM taigaio/taiga-back:latest - COPY ./entrypoint_superuser.sh /taiga-back/docker/entrypoint_superuser.sh - COPY ./entrypoint_coolify.sh /taiga-back/docker/entrypoint_coolify.sh - RUN ["chmod", "+x", "/taiga-back/docker/entrypoint_superuser.sh"] - RUN ["chmod", "+x", "/taiga-back/docker/entrypoint_coolify.sh"] - RUN ["chmod", "+x", "/taiga-back/docker/entrypoint.sh"]`; - - const DockerfileGateway = ` - FROM nginx:1.19-alpine - COPY ./nginx.conf /etc/nginx/conf.d/default.conf`; - - const nginxConf = `server { - listen 80 default_server; - - client_max_body_size 100M; - charset utf-8; - - # Frontend - location / { - proxy_pass http://${id}-taiga-front/; - proxy_pass_header Server; - proxy_set_header Host $http_host; - proxy_redirect off; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Scheme $scheme; - } - - # API - location /api/ { - proxy_pass http://${id}-taiga-back:8000/api/; - proxy_pass_header Server; - proxy_set_header Host $http_host; - proxy_redirect off; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Scheme $scheme; - } - - # Admin - location /admin/ { - proxy_pass http://${id}-taiga-back:8000/admin/; - proxy_pass_header Server; - proxy_set_header Host $http_host; - proxy_redirect off; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Scheme $scheme; - } - - # Static - location /static/ { - alias /taiga/static/; - } - - # Media - location /_protected/ { - internal; - alias /taiga/media/; - add_header Content-disposition "attachment"; - } - - # Unprotected section - location /media/exports/ { - alias /taiga/media/exports/; - add_header Content-disposition "attachment"; - } - - location /media/ { - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Scheme $scheme; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_pass http://${id}-taiga-protected:8003/; - proxy_redirect off; - } - - # Events - location /events { - proxy_pass http://${id}-taiga-events:8888/events; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_connect_timeout 7d; - proxy_send_timeout 7d; - proxy_read_timeout 7d; - } - }` - await fs.writeFile(`${workdir}/entrypoint_superuser.sh`, superUserEntrypoint); - await fs.writeFile(`${workdir}/entrypoint_coolify.sh`, entrypoint); - await fs.writeFile(`${workdir}/DockerfileBack`, DockerfileBack); - await fs.writeFile(`${workdir}/DockerfileGateway`, DockerfileGateway); - await fs.writeFile(`${workdir}/nginx.conf`, nginxConf); - - const config = { - ['taiga-gateway']: { - volumes: [`${id}-static-data:/taiga-back/static`, `${id}-media-data:/taiga-back/media`], - }, - ['taiga-front']: { - image: `${image}:${version}`, - environmentVariables: { - TAIGA_URL: fqdn, - TAIGA_WEBSOCKETS_URL: isHttps ? `wss://${getDomain(fqdn)}` : `ws://${getDomain(fqdn)}`, - TAIGA_SUBPATH: "", - PUBLIC_REGISTER_ENABLED: isDev ? "true" : "false", - } - }, - ['taiga-back']: { - volumes: [`${id}-static-data:/taiga-back/static`, `${id}-media-data:/taiga-back/media`], - environmentVariables: { - POSTGRES_DB: postgresqlDatabase, - POSTGRES_HOST: postgresqlHost, - POSTGRES_PORT: postgresqlPort, - POSTGRES_USER: postgresqlUser, - POSTGRES_PASSWORD: postgresqlPassword, - TAIGA_SECRET_KEY: secretKey, - TAIGA_SITES_SCHEME: isHttps ? 'https' : 'http', - TAIGA_SITES_DOMAIN: getDomain(fqdn), - TAIGA_SUBPATH: "", - EVENTS_PUSH_BACKEND_URL: `amqp://${rabbitMQUser}:${rabbitMQPassword}@${id}-taiga-rabbitmq:5672/taiga`, - CELERY_BROKER_URL: `amqp://${rabbitMQUser}:${rabbitMQPassword}@${id}-taiga-rabbitmq:5672/taiga`, - RABBITMQ_USER: rabbitMQUser, - RABBITMQ_PASS: rabbitMQPassword, - ENABLE_TELEMETRY: "False", - DJANGO_SUPERUSER_EMAIL: `admin@${getDomain(fqdn)}`, - DJANGO_SUPERUSER_PASSWORD: djangoAdminPassword, - DJANGO_SUPERUSER_USERNAME: djangoAdminUser, - PUBLIC_REGISTER_ENABLED: isDev ? "True" : "False", - SESSION_COOKIE_SECURE: isDev ? "False" : "True", - CSRF_COOKIE_SECURE: isDev ? "False" : "True", - - } - }, - ['taiga-async']: { - image: `taigaio/taiga-back:latest`, - volumes: [`${id}-static-data:/taiga-back/static`, `${id}-media-data:/taiga-back/media`], - environmentVariables: { - POSTGRES_DB: postgresqlDatabase, - POSTGRES_HOST: postgresqlHost, - POSTGRES_PORT: postgresqlPort, - POSTGRES_USER: postgresqlUser, - POSTGRES_PASSWORD: postgresqlPassword, - TAIGA_SECRET_KEY: secretKey, - TAIGA_SITES_SCHEME: isHttps ? 'https' : 'http', - TAIGA_SITES_DOMAIN: getDomain(fqdn), - TAIGA_SUBPATH: "", - RABBITMQ_USER: rabbitMQUser, - RABBITMQ_PASS: rabbitMQPassword, - ENABLE_TELEMETRY: "False", - } - }, - ['taiga-rabbitmq']: { - image: `rabbitmq:3.8-management-alpine`, - volumes: [`${id}-events:/var/lib/rabbitmq`], - environmentVariables: { - RABBITMQ_ERLANG_COOKIE: erlangSecret, - RABBITMQ_DEFAULT_USER: rabbitMQUser, - RABBITMQ_DEFAULT_PASS: rabbitMQPassword, - RABBITMQ_DEFAULT_VHOST: 'taiga' - } - }, - ['taiga-protected']: { - image: `taigaio/taiga-protected:latest`, - environmentVariables: { - MAX_AGE: 360, - SECRET_KEY: secretKey, - TAIGA_URL: fqdn - } - }, - ['taiga-events']: { - image: `taigaio/taiga-events:latest`, - environmentVariables: { - RABBITMQ_URL: `amqp://${rabbitMQUser}:${rabbitMQPassword}@${id}-taiga-rabbitmq:5672/taiga`, - RABBITMQ_USER: rabbitMQUser, - RABBITMQ_PASS: rabbitMQPassword, - TAIGA_SECRET_KEY: secretKey, - } - }, - - postgresql: { - image: `postgres:12.3`, - volumes: [`${id}-postgresql-data:/var/lib/postgresql/data`], - environmentVariables: { - POSTGRES_PASSWORD: postgresqlPassword, - POSTGRES_USER: postgresqlUser, - POSTGRES_DB: postgresqlDatabase - } - } - }; - - if (serviceSecret.length > 0) { - serviceSecret.forEach((secret) => { - config['taiga-back'].environmentVariables[secret.name] = secret.value; - }); - } - const { volumeMounts } = persistentVolumes(id, persistentStorage, config) - - const composeFile: ComposeFile = { - version: '3.8', - services: { - [id]: { - build: { - context: '.', - dockerfile: 'DockerfileGateway', - }, - container_name: id, - volumes: config['taiga-gateway'].volumes, - labels: makeLabelForServices('taiga'), - ...defaultComposeConfiguration(network), - }, - [`${id}-taiga-front`]: { - container_name: `${id}-taiga-front`, - image: config['taiga-front'].image, - environment: config['taiga-front'].environmentVariables, - labels: makeLabelForServices('taiga'), - ...defaultComposeConfiguration(network), - }, - [`${id}-taiga-back`]: { - build: { - context: '.', - dockerfile: 'DockerfileBack', - }, - entrypoint: '/taiga-back/docker/entrypoint_coolify.sh', - container_name: `${id}-taiga-back`, - environment: config['taiga-back'].environmentVariables, - ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), - volumes: config['taiga-back'].volumes, - labels: makeLabelForServices('taiga'), - ...defaultComposeConfiguration(network), - }, - - [`${id}-async`]: { - container_name: `${id}-taiga-async`, - image: config['taiga-async'].image, - entrypoint: ["/taiga-back/docker/async_entrypoint.sh"], - environment: config['taiga-async'].environmentVariables, - volumes: config['taiga-async'].volumes, - labels: makeLabelForServices('taiga'), - ...defaultComposeConfiguration(network), - }, - [`${id}-taiga-rabbitmq`]: { - container_name: `${id}-taiga-rabbitmq`, - image: config['taiga-rabbitmq'].image, - volumes: config['taiga-rabbitmq'].volumes, - environment: config['taiga-rabbitmq'].environmentVariables, - labels: makeLabelForServices('taiga'), - ...defaultComposeConfiguration(network), - }, - [`${id}-taiga-protected`]: { - container_name: `${id}-taiga-protected`, - image: config['taiga-protected'].image, - environment: config['taiga-protected'].environmentVariables, - labels: makeLabelForServices('taiga'), - ...defaultComposeConfiguration(network), - }, - [`${id}-taiga-events`]: { - container_name: `${id}-taiga-events`, - image: config['taiga-events'].image, - environment: config['taiga-events'].environmentVariables, - labels: makeLabelForServices('taiga'), - ...defaultComposeConfiguration(network), - }, - [`${id}-postgresql`]: { - container_name: `${id}-postgresql`, - image: config.postgresql.image, - environment: config.postgresql.environmentVariables, - ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), - volumes: config.postgresql.volumes, - labels: makeLabelForServices('taiga'), - ...defaultComposeConfiguration(network), - }, - - }, - networks: { - [network]: { - external: true - } - }, - volumes: volumeMounts - }; - const composeFileDestination = `${workdir}/docker-compose.yaml`; - await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - - await startServiceContainers(destinationDocker.id, composeFileDestination) - return {} - } catch ({ status, message }) { - return errorHandler({ status, message }) - } -} - -async function startGrafanaService(request: FastifyRequest) { - try { - const { id } = request.params; - const teamId = request.user.teamId; - const service = await getServiceFromDB({ id, teamId }); - const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort, persistentStorage } = - service; - const network = destinationDockerId && destinationDocker.network; - const port = getServiceMainPort('grafana'); - - const { workdir } = await createDirectories({ repository: type, buildId: id }); - const image = getServiceImage(type); - - const config = { - grafana: { - image: `${image}:${version}`, - volumes: [`${id}-grafana:/var/lib/grafana`], - environmentVariables: {} - } - }; - if (serviceSecret.length > 0) { - serviceSecret.forEach((secret) => { - config.grafana.environmentVariables[secret.name] = secret.value; - }); - } - const { volumeMounts } = persistentVolumes(id, persistentStorage, config) - const composeFile: ComposeFile = { - version: '3.8', - services: { - [id]: { - container_name: id, - image: config.grafana.image, - volumes: config.grafana.volumes, - environment: config.grafana.environmentVariables, - ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), - labels: makeLabelForServices('grafana'), - ...defaultComposeConfiguration(network), - } - }, - networks: { - [network]: { - external: true - } - }, - volumes: volumeMounts - }; - const composeFileDestination = `${workdir}/docker-compose.yaml`; - await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - await startServiceContainers(destinationDocker.id, composeFileDestination) - return {} - } catch ({ status, message }) { - return errorHandler({ status, message }) - } -} -async function startTriliumService(request: FastifyRequest) { - try { - const { id } = request.params; - const teamId = request.user.teamId; - const service = await getServiceFromDB({ id, teamId }); - const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort, persistentStorage } = - service; - const network = destinationDockerId && destinationDocker.network; - const port = getServiceMainPort('trilium'); - - const { workdir } = await createDirectories({ repository: type, buildId: id }); - const image = getServiceImage(type); - - const config = { - trilium: { - image: `${image}:${version}`, - volumes: [`${id}-trilium:/home/node/trilium-data`], - environmentVariables: {} - } - }; - if (serviceSecret.length > 0) { - serviceSecret.forEach((secret) => { - config.trilium.environmentVariables[secret.name] = secret.value; - }); - } - const { volumeMounts } = persistentVolumes(id, persistentStorage, config) - const composeFile: ComposeFile = { - version: '3.8', - services: { - [id]: { - container_name: id, - image: config.trilium.image, - volumes: config.trilium.volumes, - environment: config.trilium.environmentVariables, - ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), - labels: makeLabelForServices('trilium'), - ...defaultComposeConfiguration(network), - } - }, - networks: { - [network]: { - external: true - } - }, - volumes: volumeMounts - }; - const composeFileDestination = `${workdir}/docker-compose.yaml`; - await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - await startServiceContainers(destinationDocker.id, composeFileDestination) - return {} - } catch ({ status, message }) { - return errorHandler({ status, message }) - } -} - export async function migrateAppwriteDB(request: FastifyRequest, reply: FastifyReply) { try { const { id } = request.params diff --git a/apps/api/src/lib/services/supportedVersions.ts b/apps/api/src/lib/services/supportedVersions.ts deleted file mode 100644 index d84d1bd65..000000000 --- a/apps/api/src/lib/services/supportedVersions.ts +++ /dev/null @@ -1,278 +0,0 @@ -/* - Example of a supported version: -{ - // Name used to identify the service internally - name: 'umami', - // Fancier name to show to the user - fancyName: 'Umami', - // Docker base image for the service - baseImage: 'ghcr.io/mikecao/umami', - // Optional: If there is any dependent image, you should list it here - images: [], - // Usable tags - versions: ['postgresql-latest'], - // Which tag is the recommended - recommendedVersion: 'postgresql-latest', - // Application's default port, Umami listens on 3000 - ports: { - main: 3000 - } - } -*/ -export const supportedServiceTypesAndVersions = [ - { - name: 'plausibleanalytics', - fancyName: 'Plausible Analytics', - baseImage: 'plausible/analytics', - images: ['bitnami/postgresql:13.2.0', 'yandex/clickhouse-server:21.3.2.5'], - versions: ['latest', 'stable'], - recommendedVersion: 'stable', - ports: { - main: 8000 - }, - labels: ['analytics', 'plausible', 'plausible-analytics', 'gdpr', 'no-cookie'] - }, - { - name: 'nocodb', - fancyName: 'NocoDB', - baseImage: 'nocodb/nocodb', - versions: ['latest'], - recommendedVersion: 'latest', - ports: { - main: 8080 - }, - labels: ['nocodb', 'airtable', 'database'] - }, - { - name: 'minio', - fancyName: 'MinIO', - baseImage: 'minio/minio', - versions: ['latest'], - recommendedVersion: 'latest', - ports: { - main: 9001 - }, - labels: ['minio', 's3', 'storage'] - }, - { - name: 'vscodeserver', - fancyName: 'VSCode Server', - baseImage: 'codercom/code-server', - versions: ['latest'], - recommendedVersion: 'latest', - ports: { - main: 8080 - }, - labels: ['vscodeserver', 'vscode', 'code-server', 'ide'] - }, - { - name: 'wordpress', - fancyName: 'WordPress', - baseImage: 'wordpress', - images: ['bitnami/mysql:5.7'], - versions: ['latest', 'php8.1', 'php8.0', 'php7.4', 'php7.3'], - recommendedVersion: 'latest', - ports: { - main: 80 - }, - labels: ['wordpress', 'blog', 'cms'] - }, - { - name: 'vaultwarden', - fancyName: 'Vaultwarden', - baseImage: 'vaultwarden/server', - versions: ['latest'], - recommendedVersion: 'latest', - ports: { - main: 80 - }, - labels: ['vaultwarden', 'password-manager', 'passwords'] - }, - { - name: 'languagetool', - fancyName: 'LanguageTool', - baseImage: 'silviof/docker-languagetool', - versions: ['latest'], - recommendedVersion: 'latest', - ports: { - main: 8010 - }, - labels: ['languagetool', 'grammar', 'spell-checker'] - }, - { - name: 'n8n', - fancyName: 'n8n', - baseImage: 'n8nio/n8n', - versions: ['latest'], - recommendedVersion: 'latest', - ports: { - main: 5678 - }, - labels: ['n8n', 'workflow', 'automation', 'ifttt', 'zapier', 'nodered'] - }, - { - name: 'uptimekuma', - fancyName: 'Uptime Kuma', - baseImage: 'louislam/uptime-kuma', - versions: ['latest'], - recommendedVersion: 'latest', - ports: { - main: 3001 - }, - labels: ['uptimekuma', 'uptime', 'monitoring'] - }, - { - name: 'ghost', - fancyName: 'Ghost', - baseImage: 'bitnami/ghost', - images: ['bitnami/mariadb'], - versions: ['latest'], - recommendedVersion: 'latest', - ports: { - main: 2368 - }, - labels: ['ghost', 'blog', 'cms'] - }, - { - name: 'meilisearch', - fancyName: 'Meilisearch', - baseImage: 'getmeili/meilisearch', - images: [], - versions: ['latest'], - recommendedVersion: 'latest', - ports: { - main: 7700 - }, - labels: ['meilisearch', 'search', 'search-engine'] - }, - { - name: 'umami', - fancyName: 'Umami', - baseImage: 'ghcr.io/umami-software/umami', - images: ['postgres:12-alpine'], - versions: ['postgresql-latest'], - recommendedVersion: 'postgresql-latest', - ports: { - main: 3000 - }, - labels: ['umami', 'analytics', 'gdpr', 'no-cookie'] - }, - { - name: 'hasura', - fancyName: 'Hasura', - baseImage: 'hasura/graphql-engine', - images: ['postgres:12-alpine'], - versions: ['latest', 'v2.10.0', 'v2.5.1'], - recommendedVersion: 'v2.10.0', - ports: { - main: 8080 - }, - labels: ['hasura', 'graphql', 'database'] - }, - { - name: 'fider', - fancyName: 'Fider', - baseImage: 'getfider/fider', - images: ['postgres:12-alpine'], - versions: ['stable'], - recommendedVersion: 'stable', - ports: { - main: 3000 - }, - labels: ['fider', 'feedback', 'suggestions'] - }, - { - name: 'appwrite', - fancyName: 'Appwrite', - baseImage: 'appwrite/appwrite', - images: ['mariadb:10.7', 'redis:6.2-alpine', 'appwrite/telegraf:1.4.0'], - versions: ['latest', '1.0', '0.15.3'], - recommendedVersion: '1.0', - ports: { - main: 80 - }, - labels: ['appwrite', 'database', 'storage', 'api', 'serverless'] - }, - // { - // name: 'moodle', - // fancyName: 'Moodle', - // baseImage: 'bitnami/moodle', - // images: [], - // versions: ['latest', 'v4.0.2'], - // recommendedVersion: 'latest', - // ports: { - // main: 8080 - // } - // } - { - name: 'glitchTip', - fancyName: 'GlitchTip', - baseImage: 'glitchtip/glitchtip', - images: ['postgres:14-alpine', 'redis:7-alpine'], - versions: ['latest'], - recommendedVersion: 'latest', - ports: { - main: 8000 - }, - labels: ['glitchtip', 'error-reporting', 'error', 'sentry', 'bugsnag'] - }, - { - name: 'searxng', - fancyName: 'SearXNG', - baseImage: 'searxng/searxng', - images: [], - versions: ['latest'], - recommendedVersion: 'latest', - ports: { - main: 8080 - }, - labels: ['searxng', 'search', 'search-engine'] - }, - { - name: 'weblate', - fancyName: 'Weblate', - baseImage: 'weblate/weblate', - images: ['postgres:14-alpine', 'redis:6-alpine'], - versions: ['latest'], - recommendedVersion: 'latest', - ports: { - main: 8080 - }, - labels: ['weblate', 'translation', 'localization'] - }, - // { - // name: 'taiga', - // fancyName: 'Taiga', - // baseImage: 'taigaio/taiga-front', - // images: ['postgres:12.3', 'rabbitmq:3.8-management-alpine', 'taigaio/taiga-back', 'taigaio/taiga-events', 'taigaio/taiga-protected'], - // versions: ['latest'], - // recommendedVersion: 'latest', - // ports: { - // main: 80 - // } - // }, - { - name: 'grafana', - fancyName: 'Grafana', - baseImage: 'grafana/grafana', - images: [], - versions: ['latest', '9.1.3', '9.1.2', '9.0.8', '8.3.11', '8.4.11', '8.5.11'], - recommendedVersion: 'latest', - ports: { - main: 3000 - }, - labels: ['grafana', 'monitoring', 'metrics', 'dashboard'] - }, - { - name: 'trilium', - fancyName: 'Trilium Notes', - baseImage: 'zadam/trilium', - images: [], - versions: ['latest'], - recommendedVersion: 'latest', - ports: { - main: 8080 - }, - labels: ['trilium', 'notes', 'note-taking', 'wiki'] - }, -]; diff --git a/apps/api/src/realtime/index.ts b/apps/api/src/realtime/index.ts new file mode 100644 index 000000000..288c17bea --- /dev/null +++ b/apps/api/src/realtime/index.ts @@ -0,0 +1,29 @@ + +export default async (fastify) => { + fastify.io.use((socket, next) => { + const { token } = socket.handshake.auth; + if (token && fastify.jwt.verify(token)) { + next(); + } else { + return next(new Error("unauthorized event")); + } + }); + fastify.io.on('connection', (socket: any) => { + const { token } = socket.handshake.auth; + const { teamId } = fastify.jwt.decode(token); + socket.join(teamId); + // console.info('Socket connected!', socket.id) + // console.info('Socket joined team!', teamId) + // socket.on('message', (message) => { + // console.log(message) + // }) + // socket.on('error', (err) => { + // console.log(err) + // }) + }) + // fastify.io.on("error", (err) => { + // if (err && err.message === "unauthorized event") { + // fastify.io.disconnect(); + // } + // }); +} diff --git a/apps/api/src/routes/api/v1/applications/handlers.ts b/apps/api/src/routes/api/v1/applications/handlers.ts index e616aaa01..3ee5858ec 100644 --- a/apps/api/src/routes/api/v1/applications/handlers.ts +++ b/apps/api/src/routes/api/v1/applications/handlers.ts @@ -1,16 +1,15 @@ import cuid from 'cuid'; import crypto from 'node:crypto' import jsonwebtoken from 'jsonwebtoken'; -import axios from 'axios'; import { FastifyReply } from 'fastify'; import fs from 'fs/promises'; import yaml from 'js-yaml'; import csv from 'csvtojson'; import { day } from '../../../../lib/dayjs'; -import { makeLabelForStandaloneApplication, setDefaultBaseImage, setDefaultConfiguration } from '../../../../lib/buildPacks/common'; -import { checkDomainsIsValidInDNS, checkDoubleBranch, checkExposedPort, createDirectories, decrypt, defaultComposeConfiguration, encrypt, errorHandler, executeDockerCmd, generateSshKeyPair, getContainerUsage, getDomain, isDev, isDomainConfigured, listSettings, prisma, stopBuild, uniqueName } from '../../../../lib/common'; -import { checkContainer, formatLabelsOnDocker, isContainerExited, removeContainer } from '../../../../lib/docker'; +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 { 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'; @@ -242,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) { @@ -352,6 +352,7 @@ export async function saveApplication(request: FastifyRequest, publishDirectory, baseDirectory, dockerFileLocation, + dockerComposeFileLocation, denoMainFile }); if (baseDatabaseBranch) { @@ -774,6 +775,7 @@ export async function saveApplicationSource(request: FastifyRequest, reply: FastifyReply) { try { + const { default: got } = await import('got') const { id } = request.params const { teamId } = request.user const application: any = await getApplicationFromDB(id, teamId); @@ -785,13 +787,13 @@ export async function getGitHubToken(request: FastifyRequest, reply: Fas const githubToken = jsonwebtoken.sign(payload, application.gitSource.githubApp.privateKey, { algorithm: 'RS256' }); - const { data } = await axios.post(`${application.gitSource.apiUrl}/app/installations/${application.gitSource.githubApp.installationId}/access_tokens`, {}, { + const { token } = await got.post(`${application.gitSource.apiUrl}/app/installations/${application.gitSource.githubApp.installationId}/access_tokens`, { headers: { - Authorization: `Bearer ${githubToken}` + 'Authorization': `Bearer ${githubToken}`, } - }) + }).json() return reply.code(201).send({ - token: data.token + token }) } catch ({ status, message }) { return errorHandler({ status, message }) @@ -822,7 +824,7 @@ export async function saveRepository(request, reply) { let { repository, branch, projectId, autodeploy, webhookToken, isPublicRepository = false } = request.body repository = repository.toLowerCase(); - branch = branch.toLowerCase(); + projectId = Number(projectId); if (webhookToken) { await prisma.application.update({ @@ -879,6 +881,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 @@ -973,6 +985,10 @@ export async function saveSecret(request: FastifyRequest, reply: Fas try { const { id } = request.params const { name, value, isBuildSecret = false } = request.body + const found = await prisma.secret.findMany({ where: { applicationId: id, name } }) + if (found.length > 0) { + throw ({ message: 'Secret already exists.' }) + } await prisma.secret.create({ data: { name, value: encrypt(value.trim()), isBuildSecret, isPRMRSecret: false, application: { connect: { id } } } }); diff --git a/apps/api/src/routes/api/v1/applications/index.ts b/apps/api/src/routes/api/v1/applications/index.ts index 4ac98d895..750f7b765 100644 --- a/apps/api/src/routes/api/v1/applications/index.ts +++ b/apps/api/src/routes/api/v1/applications/index.ts @@ -1,6 +1,6 @@ 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, 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'; @@ -64,6 +64,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/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/destinations/handlers.ts b/apps/api/src/routes/api/v1/destinations/handlers.ts index 729426b2e..9006dc336 100644 --- a/apps/api/src/routes/api/v1/destinations/handlers.ts +++ b/apps/api/src/routes/api/v1/destinations/handlers.ts @@ -204,8 +204,8 @@ export async function assignSSHKey(request: FastifyRequest) { } export async function verifyRemoteDockerEngineFn(id: string) { await createRemoteEngineConfiguration(id); - const { remoteIpAddress, remoteUser, network, isCoolifyProxyUsed } = await prisma.destinationDocker.findFirst({ where: { id } }) - const host = `ssh://${remoteUser}@${remoteIpAddress}` + 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}`); @@ -215,8 +215,8 @@ export async function verifyRemoteDockerEngineFn(id: string) { await asyncExecShell(`DOCKER_HOST=${host} docker network create --attachable coolify-infra`); } if (isCoolifyProxyUsed) await startTraefikProxy(id); - const { stdout: daemonJson } = await executeSSHCmd({ dockerId: id, command: `cat /etc/docker/daemon.json` }); try { + const { stdout: daemonJson } = await executeSSHCmd({ dockerId: id, command: `cat /etc/docker/daemon.json` }); let daemonJsonParsed = JSON.parse(daemonJson); let isUpdated = false; if (!daemonJsonParsed['live-restore'] || daemonJsonParsed['live-restore'] !== true) { diff --git a/apps/api/src/routes/api/v1/handlers.ts b/apps/api/src/routes/api/v1/handlers.ts index f05b8acb9..ffd3ddb05 100644 --- a/apps/api/src/routes/api/v1/handlers.ts +++ b/apps/api/src/routes/api/v1/handlers.ts @@ -1,7 +1,8 @@ -import axios from "axios"; import { compareVersions } from "compare-versions"; import cuid from "cuid"; import bcrypt from "bcryptjs"; +import fs from 'fs/promises'; +import yaml from 'js-yaml'; import { asyncExecShell, asyncSleep, @@ -13,7 +14,6 @@ import { uniqueName, version, } from "../../../lib/common"; -import { supportedServiceTypesAndVersions } from "../../../lib/services/supportedVersions"; import { scheduler } from "../../../lib/scheduler"; import type { FastifyReply, FastifyRequest } from "fastify"; import type { Login, Update } from "."; @@ -36,16 +36,59 @@ export async function cleanupManually(request: FastifyRequest) { return errorHandler({ status, message }); } } +export async function refreshTags() { + try { + const { default: got } = await import('got') + try { + if (isDev) { + const tags = await fs.readFile('./devTags.json', 'utf8') + await fs.writeFile('./tags.json', tags) + } else { + const tags = await got.get('https://get.coollabs.io/coolify/service-tags.json').text() + await fs.writeFile('/app/tags.json', tags) + } + } catch (error) { + console.log(error) + } + + return {}; + } catch ({ status, message }) { + return errorHandler({ status, message }); + } +} +export async function refreshTemplates() { + try { + const { default: got } = await import('got') + try { + if (isDev) { + const response = await fs.readFile('./devTemplates.yaml', 'utf8') + await fs.writeFile('./templates.json', JSON.stringify(yaml.load(response))) + } else { + 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))) + } + } catch (error) { + console.log(error) + } + return {}; + } catch ({ status, message }) { + return errorHandler({ status, message }); + } +} export async function checkUpdate(request: FastifyRequest) { try { + const { default: got } = await import('got') const isStaging = request.hostname === "staging.coolify.io" || request.hostname === "arm.coolify.io"; const currentVersion = version; - const { data: versions } = await axios.get( - `https://get.coollabs.io/versions.json?appId=${process.env["COOLIFY_APP_ID"]}&version=${currentVersion}` - ); - const latestVersion = versions["coolify"].main.version; + const { coolify } = await got.get('https://get.coollabs.io/versions.json', { + searchParams: { + appId: process.env['COOLIFY_APP_ID'] || undefined, + version: currentVersion + } + }).json() + const latestVersion = coolify.main.version; const isUpdateAvailable = compareVersions(latestVersion, currentVersion); if (isStaging) { return { @@ -357,7 +400,6 @@ export async function getCurrentUser( return { settings: await prisma.setting.findFirst(), pendingInvitations, - supportedServiceTypesAndVersions, token, ...request.user, }; diff --git a/apps/api/src/routes/api/v1/index.ts b/apps/api/src/routes/api/v1/index.ts index 1f5ab0696..6ec94e479 100644 --- a/apps/api/src/routes/api/v1/index.ts +++ b/apps/api/src/routes/api/v1/index.ts @@ -1,9 +1,6 @@ import { FastifyPluginAsync } from 'fastify'; import { checkUpdate, login, showDashboard, update, resetQueue, getCurrentUser, cleanupManually, restartCoolify } from './handlers'; import { GetCurrentUser } from './types'; -import pump from 'pump' -import fs from 'fs' -import { asyncExecShell, encrypt, errorHandler, prisma } from '../../../lib/common'; export interface Update { Body: { latestVersion: string } diff --git a/apps/api/src/routes/api/v1/services/handlers.ts b/apps/api/src/routes/api/v1/services/handlers.ts index 2dcc434b6..659e02890 100644 --- a/apps/api/src/routes/api/v1/services/handlers.ts +++ b/apps/api/src/routes/api/v1/services/handlers.ts @@ -1,15 +1,17 @@ import type { FastifyReply, FastifyRequest } from 'fastify'; import fs from 'fs/promises'; import yaml from 'js-yaml'; -import { prisma, uniqueName, asyncExecShell, getServiceFromDB, getContainerUsage, isDomainConfigured, saveUpdateableFields, fixType, decrypt, encrypt, ComposeFile, getFreePublicPort, getDomain, errorHandler, generatePassword, isDev, stopTcpHttpProxy, executeDockerCmd, checkDomainsIsValidInDNS, checkExposedPort, listSettings } from '../../../../lib/common'; -import { day } from '../../../../lib/dayjs'; -import { checkContainer, isContainerExited } from '../../../../lib/docker'; +import bcrypt from 'bcryptjs'; import cuid from 'cuid'; -import type { OnlyId } from '../../../../types'; +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 { day } from '../../../../lib/dayjs'; +import { checkContainer, } from '../../../../lib/docker'; +import { removeService } from '../../../../lib/services/common'; +import { getTags, getTemplates } from '../../../../lib/services'; + import type { ActivateWordpressFtp, CheckService, CheckServiceDomain, DeleteServiceSecret, DeleteServiceStorage, GetServiceLogs, SaveService, SaveServiceDestination, SaveServiceSecret, SaveServiceSettings, SaveServiceStorage, SaveServiceType, SaveServiceVersion, ServiceStartStop, SetGlitchTipSettings, SetWordpressSettings } from './types'; -import { supportedServiceTypesAndVersions } from '../../../../lib/services/supportedVersions'; -import { configureServiceType, removeService } from '../../../../lib/services/common'; +import type { OnlyId } from '../../../../types'; export async function listServices(request: FastifyRequest) { try { @@ -67,30 +69,207 @@ export async function getServiceStatus(request: FastifyRequest) { try { const teamId = request.user.teamId; const { id } = request.params; - - let isRunning = false; - let isExited = false - let isRestarting = false; const service = await getServiceFromDB({ id, teamId }); const { destinationDockerId, settings } = service; - + let payload = {} if (destinationDockerId) { - const status = await checkContainer({ dockerId: service.destinationDocker.id, container: id }); - if (status?.found) { - isRunning = status.status.isRunning; - isExited = status.status.isExited; - isRestarting = status.status.isRestarting + const { stdout: containers } = await executeDockerCmd({ + 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) { + 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, + isRunning, + isExited, + isRestarting + } + } + } } } - return { - isRunning, - isExited, - settings - } + return payload } catch ({ status, message }) { return errorHandler({ status, message }) } } +export async function parseAndFindServiceTemplates(service: any, workdir?: string, isDeploy: boolean = false) { + const templates = await getTemplates() + const foundTemplate = templates.find(t => fixType(t.type) === service.type) + let parsedTemplate = {} + if (foundTemplate) { + if (!isDeploy) { + for (const [key, value] of Object.entries(foundTemplate.services)) { + const realKey = key.replace('$$id', service.id) + let name = value.name + if (!name) { + if (Object.keys(foundTemplate.services).length === 1) { + name = foundTemplate.name || service.name.toLowerCase() + } else { + if (key === '$$id') { + name = foundTemplate.name || key.replaceAll('$$id-', '') || service.name.toLowerCase() + } else { + name = key.replaceAll('$$id-', '') || service.name.toLowerCase() + } + } + } + parsedTemplate[realKey] = { + value, + name, + documentation: value.documentation || foundTemplate.documentation || 'https://docs.coollabs.io', + image: value.image, + files: value?.files, + environment: [], + fqdns: [], + hostPorts: [], + proxy: {} + } + if (value.environment?.length > 0) { + for (const env of value.environment) { + let [envKey, ...envValue] = env.split('=') + envValue = envValue.join("=") + let variable = null + if (foundTemplate?.variables) { + variable = foundTemplate?.variables.find(v => v.name === envKey) || foundTemplate?.variables.find(v => v.id === envValue) + } + if (variable) { + const id = variable.id.replaceAll('$$', '') + const label = variable?.label + const description = variable?.description + const defaultValue = variable?.defaultValue + const main = variable?.main || '$$id' + const type = variable?.type || 'input' + const placeholder = variable?.placeholder || '' + const readOnly = variable?.readOnly || false + const required = variable?.required || false + if (envValue.startsWith('$$config') || variable?.showOnConfiguration) { + if (envValue.startsWith('$$config_coolify')) { + continue + } + parsedTemplate[realKey].environment.push( + { id, name: envKey, value: envValue, main, label, description, defaultValue, type, placeholder, required, readOnly } + ) + } + } + + } + } + if (value?.proxy && value.proxy.length > 0) { + for (const proxyValue of value.proxy) { + if (proxyValue.domain) { + const variable = foundTemplate?.variables.find(v => v.id === proxyValue.domain) + if (variable) { + const { id, name, label, description, defaultValue, required = false } = variable + const found = await prisma.serviceSetting.findFirst({ where: { serviceId: service.id, variableName: proxyValue.domain } }) + parsedTemplate[realKey].fqdns.push( + { id, name, value: found?.value || '', label, description, defaultValue, required } + ) + } + } + if (proxyValue.hostPort) { + const variable = foundTemplate?.variables.find(v => v.id === proxyValue.hostPort) + if (variable) { + const { id, name, label, description, defaultValue, required = false } = variable + const found = await prisma.serviceSetting.findFirst({ where: { serviceId: service.id, variableName: proxyValue.hostPort } }) + parsedTemplate[realKey].hostPorts.push( + { id, name, value: found?.value || '', label, description, defaultValue, required } + ) + } + } + } + } + } + } else { + parsedTemplate = foundTemplate + } + let strParsedTemplate = JSON.stringify(parsedTemplate) + + // replace $$id and $$workdir + strParsedTemplate = strParsedTemplate.replaceAll('$$id', service.id) + strParsedTemplate = strParsedTemplate.replaceAll('$$core_version', service.version || foundTemplate.defaultVersion) + + // replace $$workdir + if (workdir) { + strParsedTemplate = strParsedTemplate.replaceAll('$$workdir', workdir) + } + + // replace $$config + if (service.serviceSetting.length > 0) { + for (const setting of service.serviceSetting) { + const { value, variableName } = setting + const regex = new RegExp(`\\$\\$config_${variableName.replace('$$config_', '')}\"`, 'gi') + if (value === '$$generate_fqdn') { + strParsedTemplate = strParsedTemplate.replaceAll(regex, service.fqdn + '"' || '' + '"') + } else if (value === '$$generate_fqdn_slash') { + 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 + '"') + } else { + strParsedTemplate = strParsedTemplate.replaceAll(regex, value + '"') + } + } + } + + // replace $$secret + if (service.serviceSecret.length > 0) { + for (const secret of service.serviceSecret) { + let { name, value } = secret + name = name.toLowerCase() + const regexHashed = new RegExp(`\\$\\$hashed\\$\\$secret_${name}\"`, 'gi') + const regex = new RegExp(`\\$\\$secret_${name}\"`, 'gi') + if (value) { + strParsedTemplate = strParsedTemplate.replaceAll(regexHashed, bcrypt.hashSync(value.replaceAll("\"", "\\\""), 10) + '"') + strParsedTemplate = strParsedTemplate.replaceAll(regex, value.replaceAll("\"", "\\\"") + '"') + } else { + strParsedTemplate = strParsedTemplate.replaceAll(regexHashed, '' + '"') + strParsedTemplate = strParsedTemplate.replaceAll(regex, '' + '"') + } + } + } + parsedTemplate = JSON.parse(strParsedTemplate) + } + return parsedTemplate +} export async function getService(request: FastifyRequest) { try { @@ -100,9 +279,17 @@ export async function getService(request: FastifyRequest) { if (!service) { throw { status: 404, message: 'Service not found.' } } + let template = {} + let tags = [] + if (service.type) { + template = await parseAndFindServiceTemplates(service) + tags = await getTags(service.type) + } return { settings: await listSettings(), - service + service, + template, + tags } } catch ({ status, message }) { return errorHandler({ status, message }) @@ -111,7 +298,7 @@ export async function getService(request: FastifyRequest) { export async function getServiceType(request: FastifyRequest) { try { return { - types: supportedServiceTypesAndVersions + services: await getTemplates() } } catch ({ status, message }) { return errorHandler({ status, message }) @@ -121,25 +308,83 @@ export async function saveServiceType(request: FastifyRequest, try { const { id } = request.params; const { type } = request.body; - await configureServiceType({ id, type }); - return reply.code(201).send() - } catch ({ status, message }) { - return errorHandler({ status, message }) - } -} -export async function getServiceVersions(request: FastifyRequest) { - try { - const teamId = request.user.teamId; - const { id } = request.params; - const { type } = await getServiceFromDB({ id, teamId }); - return { - type, - versions: supportedServiceTypesAndVersions.find((name) => name.name === type).versions + const templates = await getTemplates() + let foundTemplate = templates.find(t => fixType(t.type) === fixType(type)) + if (foundTemplate) { + foundTemplate = JSON.parse(JSON.stringify(foundTemplate).replaceAll('$$id', id)) + if (foundTemplate.variables) { + if (foundTemplate.variables.length > 0) { + for (const variable of foundTemplate.variables) { + const { defaultValue } = variable; + const regex = /^\$\$.*\((\d+)\)$/g; + const length = Number(regex.exec(defaultValue)?.[1]) || undefined + if (variable.defaultValue.startsWith('$$generate_password')) { + variable.value = generatePassword({ length }); + } else if (variable.defaultValue.startsWith('$$generate_hex')) { + variable.value = generatePassword({ length, isHex: true }); + } else if (variable.defaultValue.startsWith('$$generate_username')) { + variable.value = cuid(); + } else if (variable.defaultValue.startsWith('$$generate_token')) { + variable.value = generateToken() + } else { + variable.value = variable.defaultValue || ''; + } + const foundVariableSomewhereElse = foundTemplate.variables.find(v => v.defaultValue.includes(variable.id)) + if (foundVariableSomewhereElse) { + foundVariableSomewhereElse.value = foundVariableSomewhereElse.value.replaceAll(variable.id, variable.value) + } + } + } + for (const variable of foundTemplate.variables) { + if (variable.id.startsWith('$$secret_')) { + const found = await prisma.serviceSecret.findFirst({ where: { name: variable.name, serviceId: id } }) + if (!found) { + await prisma.serviceSecret.create({ + data: { name: variable.name, value: encrypt(variable.value) || '', service: { connect: { id } } } + }) + } + + } + if (variable.id.startsWith('$$config_')) { + const found = await prisma.serviceSetting.findFirst({ where: { name: variable.name, serviceId: id } }) + if (!found) { + await prisma.serviceSetting.create({ + data: { name: variable.name, value: variable.value.toString(), variableName: variable.id, service: { connect: { id } } } + }) + } + } + } + } + for (const service of Object.keys(foundTemplate.services)) { + if (foundTemplate.services[service].volumes) { + for (const volume of foundTemplate.services[service].volumes) { + const [volumeName, path] = volume.split(':') + if (!volumeName.startsWith('/')) { + const found = await prisma.servicePersistentStorage.findFirst({ where: { volumeName, serviceId: id } }) + if (!found) { + await prisma.servicePersistentStorage.create({ + data: { volumeName, path, containerId: service, predefined: true, service: { connect: { id } } } + }); + } + } + } + } + } + await prisma.service.update({ where: { id }, data: { type, version: foundTemplate.defaultVersion, templateVersion: foundTemplate.templateVersion } }) + + if (type.startsWith('wordpress')) { + await prisma.service.update({ where: { id }, data: { wordpress: { create: {} } } }) + } + return reply.code(201).send() + } else { + throw { status: 404, message: 'Service type not found.' } } + } catch ({ status, message }) { return errorHandler({ status, message }) } } + export async function saveServiceVersion(request: FastifyRequest, reply: FastifyReply) { try { const { id } = request.params; @@ -186,7 +431,7 @@ export async function getServiceUsage(request: FastifyRequest) { } export async function getServiceLogs(request: FastifyRequest) { try { - const { id } = request.params; + const { id, containerId } = request.params; let { since = 0 } = request.query if (since !== 0) { since = day(since).unix(); @@ -197,10 +442,8 @@ export async function getServiceLogs(request: FastifyRequest) { }); if (destinationDockerId) { try { - // 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 executeDockerCmd({ 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) @@ -208,7 +451,10 @@ export async function getServiceLogs(request: FastifyRequest) { return { logs: sortedLogs } // } } catch (error) { - const { statusCode } = error; + const { statusCode, stderr } = error; + if (stderr.startsWith('Error: No such container')) { + return { logs: [], noContainer: true } + } if (statusCode === 404) { return { logs: [] @@ -258,26 +504,22 @@ export async function checkServiceDomain(request: FastifyRequest) { try { const { id } = request.params; - let { fqdn, exposePort, forceSave, otherFqdns, dualCerts } = request.body; + let { fqdn, exposePort, forceSave, dualCerts, otherFqdn = false } = request.body; + + const domainsList = await prisma.serviceSetting.findMany({ where: { variableName: { startsWith: '$$config_coolify_fqdn' } } }) if (fqdn) fqdn = fqdn.toLowerCase(); - if (otherFqdns && otherFqdns.length > 0) otherFqdns = otherFqdns.map((f) => f.toLowerCase()); if (exposePort) exposePort = Number(exposePort); const { destinationDocker: { remoteIpAddress, remoteEngine, engine }, exposePort: configuredPort } = await prisma.service.findUnique({ where: { id }, include: { destinationDocker: true } }) const { isDNSCheckEnabled } = await prisma.setting.findFirst({}); - let found = await isDomainConfigured({ id, fqdn, remoteIpAddress }); + let found = await isDomainConfigured({ id, fqdn, remoteIpAddress, checkOwn: otherFqdn }); if (found) { throw { status: 500, message: `Domain ${getDomain(fqdn).replace('www.', '')} is already in use!` } } - if (otherFqdns && otherFqdns.length > 0) { - for (const ofqdn of otherFqdns) { - found = await isDomainConfigured({ id, fqdn: ofqdn, remoteIpAddress }); - if (found) { - throw { status: 500, message: `Domain ${getDomain(ofqdn).replace('www.', '')} is already in use!` } - } - } + if (domainsList.find(d => getDomain(d.value) === getDomain(fqdn))) { + throw { status: 500, message: `Domain ${getDomain(fqdn).replace('www.', '')} is already in use!` } } if (exposePort) await checkExposedPort({ id, configuredPort, exposePort, engine, remoteEngine, remoteIpAddress }) if (isDNSCheckEnabled && !isDev && !forceSave) { @@ -293,20 +535,33 @@ export async function checkService(request: FastifyRequest) { export async function saveService(request: FastifyRequest, reply: FastifyReply) { try { const { id } = request.params; - let { name, fqdn, exposePort, type } = request.body; - + let { name, fqdn, exposePort, type, serviceSetting, version } = request.body; if (fqdn) fqdn = fqdn.toLowerCase(); if (exposePort) exposePort = Number(exposePort); - type = fixType(type) - const update = saveUpdateableFields(type, request.body[type]) + const data = { fqdn, name, exposePort, + version, } - if (Object.keys(update).length > 0) { - data[type] = { update: update } + const templates = await getTemplates() + const service = await prisma.service.findUnique({ where: { id } }) + const foundTemplate = templates.find(t => fixType(t.type) === fixType(service.type)) + for (const setting of serviceSetting) { + let { id: settingId, name, value, changed = false, isNew = false, variableName } = setting + if (value) { + if (changed) { + await prisma.serviceSetting.update({ where: { id: settingId }, data: { value } }) + } + if (isNew) { + if (!variableName) { + variableName = foundTemplate?.variables.find(v => v.name === name).id + } + await prisma.serviceSetting.create({ data: { name, value, variableName, service: { connect: { id } } } }) + } + } } await prisma.service.update({ where: { id }, data @@ -320,11 +575,19 @@ export async function saveService(request: FastifyRequest, reply: F export async function getServiceSecrets(request: FastifyRequest) { try { const { id } = request.params + const teamId = request.user.teamId; + const service = await getServiceFromDB({ id, teamId }); let secrets = await prisma.serviceSecret.findMany({ where: { serviceId: id }, orderBy: { createdAt: 'desc' } }); + const templates = await getTemplates() + const foundTemplate = templates.find(t => fixType(t.type) === service.type) secrets = secrets.map((secret) => { + const foundVariable = foundTemplate?.variables.find(v => v.name === secret.name) || null + if (foundVariable) { + secret.readOnly = foundVariable.readOnly + } secret.value = decrypt(secret.value); return secret; }); @@ -341,7 +604,6 @@ export async function saveServiceSecret(request: FastifyRequest) { export async function saveServiceStorage(request: FastifyRequest, reply: FastifyReply) { try { const { id } = request.params - const { path, newStorage, storageId } = request.body + const { path, isNewStorage, storageId, containerId } = request.body - if (newStorage) { + if (isNewStorage) { + const volumeName = `${id}-custom${path.replace(/\//gi, '-')}` + const found = await prisma.servicePersistentStorage.findFirst({ where: { path, containerId } }); + if (found) { + throw { status: 500, message: 'Persistent storage already exists for this container and path.' } + } await prisma.servicePersistentStorage.create({ - data: { path, service: { connect: { id } } } + data: { path, volumeName, containerId, service: { connect: { id } } } }); } else { await prisma.servicePersistentStorage.update({ where: { id: storageId }, - data: { path } + data: { path, containerId } }); } return reply.code(201).send() @@ -420,9 +687,8 @@ export async function saveServiceStorage(request: FastifyRequest) { try { - const { id } = request.params - const { path } = request.body - await prisma.servicePersistentStorage.deleteMany({ where: { serviceId: id, path } }); + const { storageId } = request.body + await prisma.servicePersistentStorage.deleteMany({ where: { id: storageId } }); return {} } catch ({ status, message }) { return errorHandler({ status, message }) @@ -478,14 +744,17 @@ export async function activatePlausibleUsers(request: FastifyRequest, re const { destinationDockerId, destinationDocker, - plausibleAnalytics: { postgresqlUser, postgresqlPassword, postgresqlDatabase } + serviceSecret } = await getServiceFromDB({ id, teamId }); if (destinationDockerId) { - await executeDockerCmd({ - dockerId: destinationDocker.id, - command: `docker exec ${id}-postgresql psql -H postgresql://${postgresqlUser}:${postgresqlPassword}@localhost:5432/${postgresqlDatabase} -c "UPDATE users SET email_verified = true;"` - }) - return await reply.code(201).send() + const databaseUrl = serviceSecret.find((secret) => secret.name === 'DATABASE_URL'); + if (databaseUrl) { + await executeDockerCmd({ + dockerId: destinationDocker.id, + command: `docker exec ${id}-postgresql psql -H ${databaseUrl.value} -c "UPDATE users SET email_verified = true;"` + }) + return await reply.code(201).send() + } } throw { status: 500, message: 'Could not activate users.' } } catch ({ status, message }) { diff --git a/apps/api/src/routes/api/v1/services/index.ts b/apps/api/src/routes/api/v1/services/index.ts index bd0311b4d..dc23abe05 100644 --- a/apps/api/src/routes/api/v1/services/index.ts +++ b/apps/api/src/routes/api/v1/services/index.ts @@ -16,7 +16,6 @@ import { getServiceStorages, getServiceType, getServiceUsage, - getServiceVersions, listServices, newService, saveService, @@ -64,16 +63,15 @@ const root: FastifyPluginAsync = async (fastify): Promise => { fastify.get('/:id/configuration/type', async (request) => await getServiceType(request)); fastify.post('/:id/configuration/type', async (request, reply) => await saveServiceType(request, reply)); - fastify.get('/:id/configuration/version', async (request) => await getServiceVersions(request)); fastify.post('/:id/configuration/version', async (request, reply) => await saveServiceVersion(request, reply)); fastify.post('/:id/configuration/destination', async (request, reply) => await saveServiceDestination(request, reply)); fastify.get('/:id/usage', async (request) => await getServiceUsage(request)); - fastify.get('/:id/logs', async (request) => await getServiceLogs(request)); + fastify.get('/:id/logs/:containerId', async (request) => await getServiceLogs(request)); - fastify.post('/:id/:type/start', async (request) => await startService(request)); - fastify.post('/:id/:type/stop', async (request) => await stopService(request)); + fastify.post('/:id/start', async (request) => await startService(request, fastify)); + fastify.post('/:id/stop', async (request) => await stopService(request)); fastify.post('/:id/:type/settings', async (request, reply) => await setSettingsService(request, reply)); fastify.post('/:id/plausibleanalytics/activate', async (request, reply) => await activatePlausibleUsers(request, reply)); diff --git a/apps/api/src/routes/api/v1/services/types.ts b/apps/api/src/routes/api/v1/services/types.ts index 3de06fa57..253c5bcab 100644 --- a/apps/api/src/routes/api/v1/services/types.ts +++ b/apps/api/src/routes/api/v1/services/types.ts @@ -15,9 +15,13 @@ export interface SaveServiceDestination extends OnlyId { destinationId: string } } -export interface GetServiceLogs extends OnlyId { +export interface GetServiceLogs{ + Params: { + id: string, + containerId: string + }, Querystring: { - since: number + since: number, } } export interface SaveServiceSettings extends OnlyId { @@ -36,7 +40,7 @@ export interface CheckService extends OnlyId { forceSave: boolean, dualCerts: boolean, exposePort: number, - otherFqdns: Array + otherFqdn: boolean } } export interface SaveService extends OnlyId { @@ -44,6 +48,8 @@ export interface SaveService extends OnlyId { name: string, fqdn: string, exposePort: number, + version: string, + serviceSetting: any type: string } } @@ -62,14 +68,15 @@ export interface DeleteServiceSecret extends OnlyId { export interface SaveServiceStorage extends OnlyId { Body: { path: string, - newStorage: string, + containerId: string, storageId: string, + isNewStorage: boolean, } } export interface DeleteServiceStorage extends OnlyId { Body: { - path: string, + storageId: string, } } export interface ServiceStartStop { diff --git a/apps/api/src/routes/api/v1/settings/handlers.ts b/apps/api/src/routes/api/v1/settings/handlers.ts index f6364e91d..3a91449cc 100644 --- a/apps/api/src/routes/api/v1/settings/handlers.ts +++ b/apps/api/src/routes/api/v1/settings/handlers.ts @@ -1,9 +1,9 @@ import { promises as dns } from 'dns'; import { X509Certificate } from 'node:crypto'; - +import * as Sentry from '@sentry/node'; import type { FastifyReply, FastifyRequest } from 'fastify'; -import { asyncExecShell, checkDomainsIsValidInDNS, decrypt, encrypt, errorHandler, isDNSValid, isDomainConfigured, listSettings, prisma } from '../../../../lib/common'; -import { CheckDNS, CheckDomain, DeleteDomain, OnlyIdInBody, SaveSettings, SaveSSHKey } from './types'; +import { asyncExecShell, checkDomainsIsValidInDNS, decrypt, encrypt, errorHandler, isDev, isDNSValid, isDomainConfigured, listSettings, prisma, sentryDSN, version } from '../../../../lib/common'; +import { AddDefaultRegistry, CheckDNS, CheckDomain, DeleteDomain, OnlyIdInBody, SaveSettings, SaveSSHKey, SetDefaultRegistry } from './types'; export async function listAllSettings(request: FastifyRequest) { @@ -11,6 +11,20 @@ export async function listAllSettings(request: FastifyRequest) { const teamId = request.user.teamId; const settings = await listSettings(); const sshKeys = await prisma.sshKey.findMany({ where: { team: { id: teamId } } }) + let publicRegistries = await prisma.dockerRegistry.findMany({ where: { isSystemWide: true } }) + let privateRegistries = await prisma.dockerRegistry.findMany({ where: { team: { id: teamId }, isSystemWide: false } }) + publicRegistries = publicRegistries.map((registry) => { + if (registry.password) { + registry.password = decrypt(registry.password) + } + return registry + }) + privateRegistries = privateRegistries.map((registry) => { + if (registry.password) { + registry.password = decrypt(registry.password) + } + return registry + }) const unencryptedKeys = [] if (sshKeys.length > 0) { for (const key of sshKeys) { @@ -27,7 +41,11 @@ export async function listAllSettings(request: FastifyRequest) { return { settings, certificates: cns, - sshKeys: unencryptedKeys + sshKeys: unencryptedKeys, + registries: { + public: publicRegistries, + private: privateRegistries + } } } catch ({ status, message }) { return errorHandler({ status, message }) @@ -36,6 +54,7 @@ export async function listAllSettings(request: FastifyRequest) { export async function saveSettings(request: FastifyRequest, reply: FastifyReply) { try { const { + doNotTrack, fqdn, isAPIDebuggingEnabled, isRegistrationEnabled, @@ -44,19 +63,29 @@ export async function saveSettings(request: FastifyRequest, reply: maxPort, isAutoUpdateEnabled, isDNSCheckEnabled, - DNSServers + DNSServers, + proxyDefaultRedirect } = request.body const { id } = await listSettings(); await prisma.setting.update({ where: { id }, - data: { isRegistrationEnabled, dualCerts, isAutoUpdateEnabled, isDNSCheckEnabled, DNSServers, isAPIDebuggingEnabled } + data: { doNotTrack, isRegistrationEnabled, dualCerts, isAutoUpdateEnabled, isDNSCheckEnabled, DNSServers, isAPIDebuggingEnabled, } }); if (fqdn) { await prisma.setting.update({ where: { id }, data: { fqdn } }); } + await prisma.setting.update({ where: { id }, data: { proxyDefaultRedirect } }); 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 }) @@ -89,9 +118,9 @@ 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) { + if (isDNSCheckEnabled && !forceSave && !isDev) { const hostname = request.hostname.split(':')[0] return await checkDomainsIsValidInDNS({ hostname, fqdn, dualCerts }); } @@ -129,8 +158,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 }) @@ -139,9 +169,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 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, isSystemWide } = request.body; + + let encryptedPassword = '' + if (password) encryptedPassword = encrypt(password) + await prisma.dockerRegistry.create({ data: { name, url, username, password: encryptedPassword, isSystemWide, 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: '0' } }) + 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..d727c123c 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,11 @@ 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.delete<>('/registry', async (request, reply) => await deleteSSHKey(request, reply)); + fastify.post('/upload', async (request) => { try { const teamId = request.user.teamId; @@ -53,7 +58,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 618101bba..8e0acc6bc 100644 --- a/apps/api/src/routes/api/v1/settings/types.ts +++ b/apps/api/src/routes/api/v1/settings/types.ts @@ -2,6 +2,7 @@ import { OnlyId } from "../../../../types" export interface SaveSettings { Body: { + doNotTrack: boolean, fqdn: string, isAPIDebuggingEnabled: boolean, isRegistrationEnabled: boolean, @@ -10,7 +11,8 @@ export interface SaveSettings { maxPort: number, isAutoUpdateEnabled: boolean, isDNSCheckEnabled: boolean, - DNSServers: string + DNSServers: string, + proxyDefaultRedirect: string } } export interface DeleteDomain { @@ -20,30 +22,47 @@ 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 + isSystemWide: boolean + } } \ 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 26c400737..d9345c04d 100644 --- a/apps/api/src/routes/webhooks/github/handlers.ts +++ b/apps/api/src/routes/webhooks/github/handlers.ts @@ -1,4 +1,3 @@ -import axios from "axios"; import cuid from "cuid"; import crypto from "crypto"; import { encrypt, errorHandler, getDomain, getUIUrl, isDev, prisma } from "../../../lib/common"; @@ -32,13 +31,14 @@ export async function installGithub(request: FastifyRequest, repl } export async function configureGitHubApp(request, reply) { try { + const { default: got } = await import('got') const { code, state } = request.query; const { apiUrl } = await prisma.gitSource.findFirst({ where: { id: state }, include: { githubApp: true, gitlabApp: true } }); - const { data }: any = await axios.post(`${apiUrl}/app-manifests/${code}/conversions`); + const data: any = await got.post(`${apiUrl}/app-manifests/${code}/conversions`).json() const { id, client_id, slug, client_secret, pem, webhook_secret } = data const encryptedClientSecret = encrypt(client_secret); diff --git a/apps/api/src/routes/webhooks/gitlab/handlers.ts b/apps/api/src/routes/webhooks/gitlab/handlers.ts index aa60aef7b..d3a1a47b3 100644 --- a/apps/api/src/routes/webhooks/gitlab/handlers.ts +++ b/apps/api/src/routes/webhooks/gitlab/handlers.ts @@ -1,4 +1,3 @@ -import axios from "axios"; import cuid from "cuid"; import crypto from "crypto"; import type { FastifyReply, FastifyRequest } from "fastify"; @@ -10,6 +9,7 @@ import type { ConfigureGitLabApp, GitLabEvents } from "./types"; export async function configureGitLabApp(request: FastifyRequest, reply: FastifyReply) { try { + const { default: got } = await import('got') const { code, state } = request.query; const { fqdn } = await listSettings(); const { gitSource: { gitlabApp: { appId, appSecret }, htmlUrl } }: any = await getApplicationFromDB(state, undefined); @@ -19,19 +19,21 @@ export async function configureGitLabApp(request: FastifyRequest, remote: boolean = false) { + const traefik = { + tls: { + certificates: [] + }, + http: { + routers: {}, + services: {}, + middlewares: { + 'redirect-to-https': { + redirectscheme: { + scheme: 'https' + } + }, + 'redirect-to-http': { + redirectscheme: { + scheme: 'http' + } + }, + 'redirect-to-non-www': { + redirectregex: { + regex: '^https?://www\\.(.+)', + replacement: 'http://${1}' + } + }, + 'redirect-to-www': { + redirectregex: { + regex: '^https?://(?:www\\.)?(.+)', + replacement: 'http://www.${1}' + } + } + } + } + }; try { + const { id = null } = request.params; + const settings = await prisma.setting.findFirst(); + if (settings.isTraefikUsed && settings.proxyDefaultRedirect) { + traefik.http.routers['catchall-http'] = { + entrypoints: ["web"], + rule: "HostRegexp(`{catchall:.*}`)", + service: "noop", + priority: 1, + middlewares: ["redirect-regexp"] + } + traefik.http.routers['catchall-https'] = { + entrypoints: ["websecure"], + rule: "HostRegexp(`{catchall:.*}`)", + service: "noop", + priority: 1, + middlewares: ["redirect-regexp"] + } + traefik.http.middlewares['redirect-regexp'] = { + redirectregex: { + regex: '(.*)', + replacement: settings.proxyDefaultRedirect, + permanent: false + } + } + traefik.http.services['noop'] = { + loadBalancer: { + servers: [ + { + url: '' + } + ] + } + } + } const sslpath = '/etc/traefik/acme/custom'; - const certificates = await prisma.certificate.findMany({ where: { team: { applications: { some: { settings: { isCustomSSL: true } } }, destinationDocker: { some: { remoteEngine: false, isCoolifyProxyUsed: true } } } } }) + + let certificates = await prisma.certificate.findMany({ where: { team: { applications: { some: { settings: { isCustomSSL: true } } }, destinationDocker: { some: { remoteEngine: false, isCoolifyProxyUsed: true } } } } }) + + if (remote) { + certificates = await prisma.certificate.findMany({ where: { team: { applications: { some: { settings: { isCustomSSL: true } } }, destinationDocker: { some: { id, remoteEngine: true, isCoolifyProxyUsed: true, remoteVerified: true } } } } }) + } + let parsedCertificates = [] for (const certificate of certificates) { parsedCertificates.push({ @@ -187,108 +219,118 @@ export async function traefikConfiguration(request, reply) { keyFile: `${sslpath}/${certificate.id}-key.pem` }) } - const traefik = { - tls: { - certificates: parsedCertificates - }, - http: { - routers: {}, - services: {}, - middlewares: { - 'redirect-to-https': { - redirectscheme: { - scheme: 'https' - } - }, - 'redirect-to-http': { - redirectscheme: { - scheme: 'http' - } - }, - 'redirect-to-non-www': { - redirectregex: { - regex: '^https?://www\\.(.+)', - replacement: 'http://${1}' - } - }, - 'redirect-to-www': { - redirectregex: { - regex: '^https?://(?:www\\.)?(.+)', - replacement: 'http://www.${1}' - } - } + if (parsedCertificates.length > 0) { + traefik.tls.certificates = parsedCertificates + } + + let applications = []; + let services = []; + if (id) { + applications = await prisma.application.findMany({ + where: { destinationDocker: { id } }, + include: { destinationDocker: true, settings: true } + }); + services = await prisma.service.findMany({ + where: { destinationDocker: { id } }, + include: { + destinationDocker: true, + persistentStorage: true, + serviceSecret: true, + serviceSetting: true, + }, + orderBy: { createdAt: 'desc' } + }); + } else { + applications = await prisma.application.findMany({ + where: { destinationDocker: { remoteEngine: false } }, + include: { destinationDocker: true, settings: true } + }); + services = await prisma.service.findMany({ + where: { destinationDocker: { remoteEngine: false } }, + include: { + destinationDocker: true, + persistentStorage: true, + serviceSecret: true, + serviceSetting: true, + }, + orderBy: { createdAt: 'desc' }, + }); + } + + + if (applications.length > 0) { + const dockerIds = new Set() + 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 applications = await prisma.application.findMany({ - where: { destinationDocker: { remoteEngine: false } }, - include: { destinationDocker: true, settings: true } - }); - const data = { - applications: [], - services: [], - coolify: [] - }; - for (const application of applications) { - const { - fqdn, - id, - port, - buildPack, - dockerComposeConfiguration, - destinationDocker, - destinationDockerId, - settings: { previews, dualCerts, isCustomSSL } - } = application; - if (destinationDockerId) { - const { network, id: dockerId } = destinationDocker; - const isRunning = true; - if (buildPack === 'compose') { - const services = Object.entries(JSON.parse(dockerComposeConfiguration)) - for (const service of services) { - const [key, value] = service - const { port: customPort, fqdn } = value - if (fqdn) { - const domain = getDomain(fqdn); - const nakedDomain = domain.replace(/^www\./, ''); - const isHttps = fqdn.startsWith('https://'); - const isWWW = fqdn.includes('www.'); - data.applications.push({ - id: `${id}-${key}`, - container: `${id}-${key}`, - port: customPort ? customPort : port || 3000, - domain, - nakedDomain, - isRunning, - isHttps, - isWWW, - isDualCerts: dualCerts, - isCustomSSL - }); - } + for (const application of applications) { + try { + const { + fqdn, + id, + port, + buildPack, + dockerComposeConfiguration, + destinationDocker, + destinationDockerId, + settings + } = application; + if (!destinationDockerId) { + continue; } - continue; - } + if ( + !runningContainers[destinationDockerId] || + runningContainers[destinationDockerId].length === 0 || + runningContainers[destinationDockerId].filter((container) => container.startsWith(id)).length === 0 + ) { + continue + } + if (buildPack === 'compose') { + const services = Object.entries(JSON.parse(dockerComposeConfiguration)) + if (services.length > 0) { + for (const service of services) { + const [key, value] = service + if (key && value) { + if (!value.fqdn || !value.port) { + continue; + } + const { fqdn, port } = value + const containerId = `${id}-${key}` + const domain = getDomain(fqdn); + const nakedDomain = domain.replace(/^www\./, ''); + const isHttps = fqdn.startsWith('https://'); + const isWWW = fqdn.includes('www.'); + const pathPrefix = '/' + const isCustomSSL = false; + const dualCerts = false; + const serviceId = `${id}-${port || 'default'}` - if (fqdn) { + traefik.http.routers = { ...traefik.http.routers, ...generateRouters(serviceId, domain, nakedDomain, pathPrefix, isHttps, isWWW, dualCerts, isCustomSSL) } + traefik.http.services = { ...traefik.http.services, ...generateServices(serviceId, containerId, port) } + } + } + } + continue; + } + const { previews, dualCerts, isCustomSSL } = settings; + const { network, id: dockerId } = destinationDocker; + if (!fqdn) { + continue; + } const domain = getDomain(fqdn); const nakedDomain = domain.replace(/^www\./, ''); const isHttps = fqdn.startsWith('https://'); const isWWW = fqdn.includes('www.'); - if (isRunning) { - data.applications.push({ - id, - container: id, - port: port || 3000, - domain, - nakedDomain, - isRunning, - isHttps, - isWWW, - isDualCerts: dualCerts, - isCustomSSL - }); - } + const pathPrefix = '/' + const serviceId = `${id}-${port || 'default'}` + 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 @@ -300,156 +342,153 @@ export async function traefikConfiguration(request, reply) { for (const container of containers) { const previewDomain = `${container.split('-')[1]}.${domain}`; const nakedDomain = previewDomain.replace(/^www\./, ''); - data.applications.push({ - id: container, - container, - port: port || 3000, - domain: previewDomain, - isRunning, - nakedDomain, - isHttps, - isWWW, - isDualCerts: dualCerts, - isCustomSSL - }); + 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) } } } } + } catch (error) { + console.log(error) } } } - const services: any = await prisma.service.findMany({ - where: { destinationDocker: { remoteEngine: false } }, - include: includeServices, - orderBy: { createdAt: 'desc' }, - }); - - for (const service of services) { - const { - fqdn, - id, - type, - destinationDocker, - destinationDockerId, - dualCerts, - plausibleAnalytics - } = service; - if (destinationDockerId) { - const found = supportedServiceTypesAndVersions.find((a) => a.name === type); - if (found) { - const port = found.ports.main; - const publicPort = service[type]?.publicPort; - const isRunning = true; - if (fqdn) { - const domain = getDomain(fqdn); - const nakedDomain = domain.replace(/^www\./, ''); - const isHttps = fqdn.startsWith('https://'); - const isWWW = fqdn.includes('www.'); - if (isRunning) { - // Plausible Analytics custom script - let scriptName = false; - if (type === 'plausibleanalytics' && plausibleAnalytics.scriptName !== 'plausible.js') { - scriptName = plausibleAnalytics.scriptName; + if (services.length > 0) { + const dockerIds = new Set() + 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 + } + } + for (const service of services) { + try { + let { + fqdn, + id, + type, + destinationDockerId, + dualCerts, + serviceSetting + } = service; + if (!fqdn) { + continue; + } + if (!destinationDockerId) { + continue; + } + if ( + !runningContainers[destinationDockerId] || + runningContainers[destinationDockerId].length === 0 || + !runningContainers[destinationDockerId].includes(id) + ) { + continue + } + const templates = await getTemplates(); + let found = templates.find((a) => a.type === type); + if (!found) { + continue; + } + found = JSON.parse(JSON.stringify(found).replaceAll('$$id', id)); + for (const oneService of Object.keys(found.services)) { + const isDomainConfiguration = found?.services[oneService]?.proxy?.filter(p => p.domain) ?? []; + if (isDomainConfiguration.length > 0) { + const { proxy } = found.services[oneService]; + for (let configuration of proxy) { + if (configuration.domain) { + const setting = serviceSetting.find((a) => a.variableName === configuration.domain); + if (setting) { + configuration.domain = configuration.domain.replace(configuration.domain, setting.value); + } + } + const foundPortVariable = serviceSetting.find((a) => a.name.toLowerCase() === 'port') + if (foundPortVariable) { + configuration.port = foundPortVariable.value + } + let port, pathPrefix, customDomain; + if (configuration) { + port = configuration?.port; + pathPrefix = configuration?.pathPrefix || '/'; + customDomain = configuration?.domain + } + if (customDomain) { + fqdn = customDomain + } else { + fqdn = service.fqdn + } + const domain = getDomain(fqdn); + const nakedDomain = domain.replace(/^www\./, ''); + const isHttps = fqdn.startsWith('https://'); + const isWWW = fqdn.includes('www.'); + const isCustomSSL = false; + const serviceId = `${oneService}-${port || 'default'}` + traefik.http.routers = { ...traefik.http.routers, ...generateRouters(serviceId, domain, nakedDomain, pathPrefix, isHttps, isWWW, dualCerts, isCustomSSL) } + traefik.http.services = { ...traefik.http.services, ...generateServices(serviceId, oneService, port) } } - - let container = id; - let otherDomain = null; - let otherNakedDomain = null; - let otherIsHttps = null; - let otherIsWWW = null; - - if (type === 'minio' && service.minio.apiFqdn) { - otherDomain = getDomain(service.minio.apiFqdn); - otherNakedDomain = otherDomain.replace(/^www\./, ''); - otherIsHttps = service.minio.apiFqdn.startsWith('https://'); - otherIsWWW = service.minio.apiFqdn.includes('www.'); + } else { + if (found.services[oneService].ports && found.services[oneService].ports.length > 0) { + for (let [index, port] of found.services[oneService].ports.entries()) { + if (port == 22) continue; + if (index === 0) { + const foundPortVariable = serviceSetting.find((a) => a.name.toLowerCase() === 'port') + if (foundPortVariable) { + port = foundPortVariable.value + } + } + const domain = getDomain(fqdn); + const nakedDomain = domain.replace(/^www\./, ''); + const isHttps = fqdn.startsWith('https://'); + const isWWW = fqdn.includes('www.'); + const pathPrefix = '/' + const isCustomSSL = false + const serviceId = `${oneService}-${port || 'default'}` + 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) } + } } - data.services.push({ - id, - container, - type, - otherDomain, - otherNakedDomain, - otherIsHttps, - otherIsWWW, - port, - publicPort, - domain, - nakedDomain, - isRunning, - isHttps, - isWWW, - isDualCerts: dualCerts, - scriptName - }); } } + } catch (error) { + console.log(error) } } } - - const { fqdn, dualCerts } = await prisma.setting.findFirst(); - if (fqdn) { + if (!remote) { + const { fqdn, dualCerts } = await prisma.setting.findFirst(); + if (!fqdn) { + return + } const domain = getDomain(fqdn); const nakedDomain = domain.replace(/^www\./, ''); const isHttps = fqdn.startsWith('https://'); const isWWW = fqdn.includes('www.'); - data.coolify.push({ - id: isDev ? 'host.docker.internal' : 'coolify', - container: isDev ? 'host.docker.internal' : 'coolify', - port: 3000, - domain, - nakedDomain, - isHttps, - isWWW, - isDualCerts: dualCerts - }); - } - for (const application of data.applications) { - configureMiddleware(application, traefik); - } - for (const service of data.services) { - const { id, scriptName } = service; - - configureMiddleware(service, traefik); - if (service.type === 'minio') { - service.id = id + '-minio'; - service.container = id; - service.domain = service.otherDomain; - service.nakedDomain = service.otherNakedDomain; - service.isHttps = service.otherIsHttps; - service.isWWW = service.otherIsWWW; - service.port = 9000; - configureMiddleware(service, traefik); - } - - if (scriptName) { - traefik.http.middlewares[`${id}-redir`] = { - replacepathregex: { - regex: `/js/${scriptName}`, - replacement: '/js/plausible.js' - } - }; - } - } - for (const coolify of data.coolify) { - configureMiddleware(coolify, traefik); + const id = isDev ? 'host.docker.internal' : 'coolify' + const container = isDev ? 'host.docker.internal' : 'coolify' + const port = 3000 + const pathPrefix = '/' + const isCustomSSL = false + const serviceId = `${id}-${port || 'default'}` + traefik.http.routers = { ...traefik.http.routers, ...generateRouters(serviceId, domain, nakedDomain, pathPrefix, isHttps, isWWW, dualCerts, isCustomSSL) } + traefik.http.services = { ...traefik.http.services, ...generateServices(serviceId, container, port) } } + } catch (error) { + console.log(error) + } finally { if (Object.keys(traefik.http.routers).length === 0) { traefik.http.routers = null; } if (Object.keys(traefik.http.services).length === 0) { traefik.http.services = null; } - return { - ...traefik - } - } catch ({ status, message }) { - return errorHandler({ status, message }) + return traefik; } } -export async function traefikOtherConfiguration(request: FastifyRequest) { +export async function otherProxyConfiguration(request: FastifyRequest) { try { const { id } = request.query if (id) { @@ -477,84 +516,40 @@ export async function traefikOtherConfiguration(request: FastifyRequest) { - const { id } = request.params - try { - const sslpath = '/etc/traefik/acme/custom'; - const certificates = await prisma.certificate.findMany({ where: { team: { applications: { some: { settings: { isCustomSSL: true } } }, destinationDocker: { some: { id, remoteEngine: true, isCoolifyProxyUsed: true, remoteVerified: true } } } } }) - let parsedCertificates = [] - for (const certificate of certificates) { - parsedCertificates.push({ - certFile: `${sslpath}/${certificate.id}-cert.pem`, - keyFile: `${sslpath}/${certificate.id}-key.pem` - }) - } - const traefik = { - tls: { - certificates: parsedCertificates - }, - http: { - routers: {}, - services: {}, - middlewares: { - 'redirect-to-https': { - redirectscheme: { - scheme: 'https' - } - }, - 'redirect-to-http': { - redirectscheme: { - scheme: 'http' - } - }, - 'redirect-to-non-www': { - redirectregex: { - regex: '^https?://www\\.(.+)', - replacement: 'http://${1}' - } - }, - 'redirect-to-www': { - redirectregex: { - regex: '^https?://(?:www\\.)?(.+)', - replacement: 'http://www.${1}' - } - } - } - } - }; - const applications = await prisma.application.findMany({ - where: { destinationDocker: { id } }, - include: { destinationDocker: true, settings: true } - }); - const data = { - applications: [], - services: [], - coolify: [] - }; - for (const application of applications) { - const { - fqdn, - id, - port, - buildPack, - dockerComposeConfiguration, - destinationDocker, - destinationDockerId, - settings: { previews, dualCerts, isCustomSSL } - } = application; - if (destinationDockerId) { - const { id: dockerId, network } = destinationDocker; - const isRunning = true; - if (buildPack === 'compose') { - const services = Object.entries(JSON.parse(dockerComposeConfiguration)) - for (const service of services) { - const [key, value] = service - const { port: customPort, fqdn } = value - if (fqdn) { - const domain = getDomain(fqdn); - const nakedDomain = domain.replace(/^www\./, ''); - const isHttps = fqdn.startsWith('https://'); - const isWWW = fqdn.includes('www.'); - data.applications.push({ - id: `${id}-${key}`, - container: `${id}-${key}`, - port: customPort ? customPort : port || 3000, - domain, - nakedDomain, - isRunning, - isHttps, - isWWW, - isDualCerts: dualCerts, - isCustomSSL - }); - } - } - continue; - } - if (fqdn) { - const domain = getDomain(fqdn); - const nakedDomain = domain.replace(/^www\./, ''); - const isHttps = fqdn.startsWith('https://'); - const isWWW = fqdn.includes('www.'); - if (isRunning) { - data.applications.push({ - id, - container: id, - port: port || 3000, - domain, - nakedDomain, - isRunning, - isHttps, - isWWW, - isDualCerts: dualCerts, - isCustomSSL - }); - } - 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\./, ''); - data.applications.push({ - id: container, - container, - port: port || 3000, - domain: previewDomain, - isRunning, - nakedDomain, - isHttps, - isWWW, - isDualCerts: dualCerts, - isCustomSSL - }); - } - } - } - } - } - } - const services: any = await prisma.service.findMany({ - where: { destinationDocker: { id } }, - include: includeServices, - orderBy: { createdAt: 'desc' } - }); - - for (const service of services) { - const { - fqdn, - id, - type, - destinationDocker, - destinationDockerId, - dualCerts, - plausibleAnalytics - } = service; - if (destinationDockerId) { - const found = supportedServiceTypesAndVersions.find((a) => a.name === type); - if (found) { - const port = found.ports.main; - const publicPort = service[type]?.publicPort; - const isRunning = true; - if (fqdn) { - const domain = getDomain(fqdn); - const nakedDomain = domain.replace(/^www\./, ''); - const isHttps = fqdn.startsWith('https://'); - const isWWW = fqdn.includes('www.'); - if (isRunning) { - // Plausible Analytics custom script - let scriptName = false; - if (type === 'plausibleanalytics' && plausibleAnalytics.scriptName !== 'plausible.js') { - scriptName = plausibleAnalytics.scriptName; - } - - let container = id; - let otherDomain = null; - let otherNakedDomain = null; - let otherIsHttps = null; - let otherIsWWW = null; - - if (type === 'minio' && service.minio.apiFqdn) { - otherDomain = getDomain(service.minio.apiFqdn); - otherNakedDomain = otherDomain.replace(/^www\./, ''); - otherIsHttps = service.minio.apiFqdn.startsWith('https://'); - otherIsWWW = service.minio.apiFqdn.includes('www.'); - } - data.services.push({ - id, - container, - type, - otherDomain, - otherNakedDomain, - otherIsHttps, - otherIsWWW, - port, - publicPort, - domain, - nakedDomain, - isRunning, - isHttps, - isWWW, - isDualCerts: dualCerts, - scriptName - }); - } - } - } - } - } - - - for (const application of data.applications) { - configureMiddleware(application, traefik); - } - for (const service of data.services) { - const { id, scriptName } = service; - - configureMiddleware(service, traefik); - if (service.type === 'minio') { - service.id = id + '-minio'; - service.container = id; - service.domain = service.otherDomain; - service.nakedDomain = service.otherNakedDomain; - service.isHttps = service.otherIsHttps; - service.isWWW = service.otherIsWWW; - service.port = 9000; - configureMiddleware(service, traefik); - } - - if (scriptName) { - traefik.http.middlewares[`${id}-redir`] = { - replacepathregex: { - regex: `/js/${scriptName}`, - replacement: '/js/plausible.js' - } - }; - } - } - for (const coolify of data.coolify) { - configureMiddleware(coolify, traefik); - } - if (Object.keys(traefik.http.routers).length === 0) { - traefik.http.routers = null; - } - if (Object.keys(traefik.http.services).length === 0) { - traefik.http.services = null; - } - return { - ...traefik - } - } catch ({ status, message }) { - return errorHandler({ status, message }) - } -} \ No newline at end of file diff --git a/apps/api/src/routes/webhooks/traefik/index.ts b/apps/api/src/routes/webhooks/traefik/index.ts index 3769c8eb3..36ce68df0 100644 --- a/apps/api/src/routes/webhooks/traefik/index.ts +++ b/apps/api/src/routes/webhooks/traefik/index.ts @@ -1,13 +1,12 @@ import { FastifyPluginAsync } from 'fastify'; import { OnlyId } from '../../../types'; -import { remoteTraefikConfiguration, traefikConfiguration, traefikOtherConfiguration } from './handlers'; -import { TraefikOtherConfiguration } from './types'; +import { proxyConfiguration, otherProxyConfiguration } from './handlers'; +import { OtherProxyConfiguration } from './types'; const root: FastifyPluginAsync = async (fastify): Promise => { - fastify.get('/main.json', async (request, reply) => traefikConfiguration(request, reply)); - fastify.get('/other.json', async (request, reply) => traefikOtherConfiguration(request)); - - fastify.get('/remote/:id', async (request) => remoteTraefikConfiguration(request)); + fastify.get('/main.json', async (request, reply) => proxyConfiguration(request, false)); + fastify.get('/remote/:id', async (request) => proxyConfiguration(request, true)); + fastify.get('/other.json', async (request, reply) => otherProxyConfiguration(request)); }; export default root; diff --git a/apps/api/src/routes/webhooks/traefik/types.ts b/apps/api/src/routes/webhooks/traefik/types.ts index 237d746fe..141066111 100644 --- a/apps/api/src/routes/webhooks/traefik/types.ts +++ b/apps/api/src/routes/webhooks/traefik/types.ts @@ -1,4 +1,4 @@ -export interface TraefikOtherConfiguration { +export interface OtherProxyConfiguration { Querystring: { id: string, privatePort: number, diff --git a/apps/api/src/types.ts b/apps/api/src/types.ts index acb44c8e0..d4ab01953 100644 --- a/apps/api/src/types.ts +++ b/apps/api/src/types.ts @@ -1,4 +1,4 @@ export interface OnlyId { - Params: { id: string }, + Params: { id?: string }, } diff --git a/apps/api/tags.json b/apps/api/tags.json new file mode 100644 index 000000000..e75e9dd0d --- /dev/null +++ b/apps/api/tags.json @@ -0,0 +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.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","5.19.2","5.19.1","5.19.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.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","5.14.2","5.14.1"]},{"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.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","5.14.2","5.14.1"]},{"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.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","8.5.6","8.5.5","8.5.4"]},{"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.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.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","0.9.19"]},{"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.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","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"]},{"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-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","edge-2022-10-25-a059748c178cce0bc30bdc915d4ff8f0d13ce25c","edge-2022-10-24-94ae069cdcf9a171e812da32d760a436fa9b37ad","edge-2022-10-24-71d2011a1f6d2b60bb65af9f775bf75d4beaf8fd"]},{"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 new file mode 100644 index 000000000..99d40e200 --- /dev/null +++ b/apps/api/templates.json @@ -0,0 +1 @@ +[{"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:14","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/ui/package.json b/apps/ui/package.json index fdf37b8aa..1cadeffd8 100644 --- a/apps/ui/package.json +++ b/apps/ui/package.json @@ -14,43 +14,43 @@ "format": "prettier --write --plugin-search-dir=. ." }, "devDependencies": { - "@floating-ui/dom": "1.0.1", - "@playwright/test": "1.25.1", + "@floating-ui/dom": "1.0.6", + "@playwright/test": "1.28.0", "@popperjs/core": "2.11.6", "@sveltejs/kit": "1.0.0-next.405", "@types/js-cookie": "3.0.2", - "@typescript-eslint/eslint-plugin": "5.36.1", - "@typescript-eslint/parser": "5.36.1", - "autoprefixer": "10.4.8", - "classnames": "2.3.1", - "eslint": "8.23.0", + "@typescript-eslint/eslint-plugin": "5.44.0", + "@typescript-eslint/parser": "5.44.0", + "autoprefixer": "10.4.13", + "classnames": "2.3.2", + "eslint": "8.28.0", "eslint-config-prettier": "8.5.0", "eslint-plugin-svelte3": "4.0.0", - "flowbite": "1.5.2", - "flowbite-svelte": "0.26.2", - "postcss": "8.4.16", + "flowbite": "1.5.4", + "flowbite-svelte": "0.28.0", + "postcss": "8.4.19", "prettier": "2.7.1", - "prettier-plugin-svelte": "2.7.0", - "svelte": "3.50.0", - "svelte-check": "2.9.0", + "prettier-plugin-svelte": "2.8.1", + "svelte": "3.53.1", + "svelte-check": "2.9.2", "svelte-preprocess": "4.10.7", - "tailwindcss": "3.1.8", + "tailwindcss": "3.2.4", "tailwindcss-scrollbar": "0.1.0", - "tslib": "2.4.0", - "typescript": "4.8.2", - "vite": "3.1.0" + "tslib": "2.4.1", + "typescript": "4.9.3", + "vite": "3.2.4" }, "type": "module", "dependencies": { - "@sveltejs/adapter-static": "1.0.0-next.39", - "@tailwindcss/typography": "^0.5.7", + "@sveltejs/adapter-static": "1.0.0-next.48", + "@tailwindcss/typography": "0.5.8", "cuid": "2.1.8", - "daisyui": "2.24.2", - "dayjs": "1.11.5", + "daisyui": "2.41.0", + "dayjs": "1.11.6", "js-cookie": "3.0.1", "js-yaml": "4.1.0", "p-limit": "4.0.0", - "svelte-file-dropzone": "^1.0.0", + "socket.io-client": "4.5.3", "svelte-select": "4.4.7", "sveltekit-i18n": "2.2.2" } diff --git a/apps/ui/src/lib/api.ts b/apps/ui/src/lib/api.ts index 7341d580b..058ec083f 100644 --- a/apps/ui/src/lib/api.ts +++ b/apps/ui/src/lib/api.ts @@ -11,7 +11,7 @@ export function getAPIUrl() { return `https://${CODESANDBOX_HOST.replace(/\$PORT/, '3001')}`; } return dev - ? 'http://localhost:3001' + ? `http://${window.location.hostname}:3001` : 'http://localhost:3000'; } export function getWebhookUrl(type: string) { 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..8698e78bb --- /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 9083fa47c..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() { @@ -38,6 +38,8 @@ class={disabledClass} class:pr-10={true} class:pr-20={value && isHttps} + class:border={required && !value} + class:border-red-500={required && !value} {placeholder} type="text" {id} @@ -54,6 +56,8 @@ type="text" class:pr-10={true} class:pr-20={value && isHttps} + class:border={required && !value} + class:border-red-500={required && !value} {id} {name} {required} @@ -70,6 +74,8 @@ class={disabledClass} class:pr-10={true} class:pr-20={value && isHttps} + class:border={required && !value} + class:border-red-500={required && !value} type="password" {id} {name} @@ -85,6 +91,7 @@
{#if isPasswordField} +
(showPassword = !showPassword)}> {#if showPassword} {/if} {#if value && isHttps} +
+ import ExternalLink from './ExternalLink.svelte'; import Tooltip from './Tooltip.svelte'; export let url = 'https://docs.coollabs.io'; + export let text: any = ''; + export let isExternal = false; let id = 'cool-' + url @@ -10,10 +13,32 @@ .slice(-16); - - - - - + + + + + {text} + {#if isExternal} + + {/if} -See details in the documentation +{#if !text} + See details in the documentation +{/if} diff --git a/apps/ui/src/lib/components/Explainer.svelte b/apps/ui/src/lib/components/Explainer.svelte index 43986a39d..924ce70d6 100644 --- a/apps/ui/src/lib/components/Explainer.svelte +++ b/apps/ui/src/lib/components/Explainer.svelte @@ -3,7 +3,7 @@ // import Tooltip from './Tooltip.svelte'; export let explanation = ''; - export let position = 'dropdown-right' + export let position = 'dropdown-right'; // let id: any; // let self: any; // onMount(() => { @@ -13,32 +13,26 @@
+ + -
- diff --git a/apps/ui/src/lib/components/ExternalLink.svelte b/apps/ui/src/lib/components/ExternalLink.svelte new file mode 100644 index 000000000..62f2e312a --- /dev/null +++ b/apps/ui/src/lib/components/ExternalLink.svelte @@ -0,0 +1,10 @@ + + + diff --git a/apps/ui/src/lib/components/LocalePicker.svelte b/apps/ui/src/lib/components/LocalePicker.svelte new file mode 100644 index 000000000..dfd004165 --- /dev/null +++ b/apps/ui/src/lib/components/LocalePicker.svelte @@ -0,0 +1,11 @@ + + +
+ +
\ No newline at end of file diff --git a/apps/ui/src/lib/components/ServiceStatus.svelte b/apps/ui/src/lib/components/ServiceStatus.svelte new file mode 100644 index 000000000..f6cb092f5 --- /dev/null +++ b/apps/ui/src/lib/components/ServiceStatus.svelte @@ -0,0 +1,37 @@ + + +{#if serviceStatus.isExcluded} + Excluded +{:else if serviceStatus.isRunning} + Running +{:else if serviceStatus.isStopped || serviceStatus.isExited} + Stopped +{:else if serviceStatus.isRestarting} + Restarting +{/if} diff --git a/apps/ui/src/lib/components/Setting.svelte b/apps/ui/src/lib/components/Setting.svelte index ef5bab024..555323b37 100644 --- a/apps/ui/src/lib/components/Setting.svelte +++ b/apps/ui/src/lib/components/Setting.svelte @@ -8,7 +8,7 @@ export let setting: any; export let title: any; export let isBeta: any = false; - export let description: any; + export let description: any = null; export let isCenter = true; export let disabled = false; export let dataTooltip: any = null; @@ -31,6 +31,7 @@
+
+
dispatch('click')} on:mouseover={() => dispatch('pause')} on:focus={() => dispatch('pause')} on:mouseout={() => dispatch('resume')} on:blur={() => dispatch('resume')} - class={`flex flex-row alert shadow-lg text-white hover:scale-105 transition-all duration-100 cursor-pointer rounded ${success()}`} + class={` flex flex-row justify-center alert shadow-lg text-white hover:scale-105 transition-all duration-100 cursor-pointer rounded ${success()}`} class:alert-error={type === 'error'} class:alert-info={type === 'info'} > diff --git a/apps/ui/src/lib/components/Toasts.svelte b/apps/ui/src/lib/components/Toasts.svelte index a7bd9165c..031840b38 100644 --- a/apps/ui/src/lib/components/Toasts.svelte +++ b/apps/ui/src/lib/components/Toasts.svelte @@ -4,11 +4,11 @@ import { dismissToast, pauseToast, resumeToast, toasts } from '$lib/store'; -{#if $toasts} +{#if $toasts.length > 0}
-