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..3825ddaf3 100644 --- a/CONTRIBUTION.md +++ b/CONTRIBUTION.md @@ -53,71 +53,5 @@ Optional: - **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 +## How to add a new service? +You can find all details [here](https://github.com/coollabsio/coolify-community-templates) \ 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/apps/api/devTags.json b/apps/api/devTags.json new file mode 100644 index 000000000..47f6c6cb5 --- /dev/null +++ b/apps/api/devTags.json @@ -0,0 +1 @@ +[{"name":"appsmith","image":"appsmith/appsmith-ce","tags":["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","latest"]},{"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","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","latest"]},{"name":"languagetool","image":"silviof/docker-languagetool","tags":["latest","5.8","5.7","5.6","5.5","5.4","5.3"]},{"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","latest"]},{"name":"grafana","image":"grafana/grafana","tags":["9.2.3","9.2.2","9.2.1","9.2.0","9.1.8","9.1.7","9.1.6","9.1.5","9.1.4","9.1.3","9.1.2","9.1.1","9.1.0","9.0.9","9.0.8","9.0.7","9.0.6","9.0.5","9.0.4","9.0.3","9.0.2","9.0.1","9.0.0","8.5.9","8.5.6","8.5.5","8.5.4","8.5.3","8.5.2","8.5.14","latest"]},{"name":"appwrite","image":"appwrite/appwrite","tags":["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","0.1.6","latest"]},{"name":"weblate","image":"weblate/weblate","tags":["latest","edge-2022-11-02-e4171e0c5657ca38341cce8ac31f5cbdf25389eb","edge-2022-11-02-6d886c40cd62eb23d21f7c0a1840b4a7a4c51ad0","edge-2022-11-01-608df4dd95a2d1f76c15cddd9e116bb4c3229168","edge-2022-11-01-54957be78eb76f602ceae50c0b01b64b20402b2a","edge-2022-10-31-c55c7302a6c82a160ee9d711893c12d67ecd3b27","edge-2022-10-26-c69cfdd83ed1fad4a4d57398552b8c70894a6586","edge-2022-10-26-410b3aff37de5bbfacbc47642ce28b2518bee506","edge-2022-10-25-e09e2c29ed3748eb0fa248453635dd27768e8dd9","edge-2022-10-25-a059748c178cce0bc30bdc915d4ff8f0d13ce25c","edge-2022-10-24-94ae069cdcf9a171e812da32d760a436fa9b37ad","edge-2022-10-24-71d2011a1f6d2b60bb65af9f775bf75d4beaf8fd","edge-2022-10-19-acb8e82a0ea2c86fb7ce8f5cb1a81e2bc634583a","edge-2022-10-18-f54b83a0898bef9d14b00d885f675c2e4ebb68ca","edge-2022-10-17-eb9dc248720581052b4be4e3f1f372bce37edd62","edge-2022-10-17-e5a5f6cfdf7e500b7b357566971172fad022c1dc","edge-2022-10-17-da6d01ea005c29453e0111a45956d16bf2262f54","edge-2022-10-17-7a28d7f727778dd05b163fa40ec7903e2e07b20c","edge-2022-10-13-899ea7d8aef900ef216e231433fe15e8dc9a2af8","edge-2022-10-13-385a55156bdc918b0e78ce337322b8996455027a","edge-2022-10-12-5356be32da4b8beb16ba3ec0aa8763f6e08294ab","edge-2022-10-10-d087b31ef728a3a9c3563de9aa4b2ea189755fdf","edge-2022-10-07-f6d43c61fee0e58eb18dd6f80404fba272280c3f","edge-2022-10-05-b86e34a866f40446dc8cb1b48f5d56e621904a0c","edge-2022-10-05-82096af0a688d0104c1c5b973dfc1c6eef6f9d85","edge-2022-10-05-495225a1fd551be41301b53dcd5b10d0c97927bb","edge-2022-10-05-0bb3091700132894e51d806a140145a03489fc09","edge-2022-10-03-9255921fca690c5350ba16960d856c68b910e586","edge-2022-10-03-4b9d377a8289ede1a16b05836eeed0c3b0570dd9","edge-2022-10-01-482ba9c3f7862bca8b1e70d3d0a02806e075d120"]},{"name":"searxng","image":"searxng/searxng","tags":["2022.10.29-fc9986de","2022.10.29-fa59ff9b","2022.10.29-d49ccb54","2022.10.29-a9deead1","2022.10.29-3f1d594c","2022.10.28-c26fa335","2022.10.28-5db4ed5d","2022.10.28-5a181ea1","2022.10.25-affd8f75","2022.10.25-4783d6c9","2022.10.21-710a3a00","2022.10.14-e2dd5a80","2022.10.14-72f6367e","2022.10.14-4d4dfc58","2022.10.14-2eb81701","2022.10.14-1a5b0965","2022.10.14-096d9def","2022.10.11-a7337612","2022.10.07-84f61af8","2022.10.07-666cd1f6","2022.10.01-e9af772b","2022.10.01-901143f0","2022.10.01-14d0fb2c","2022.09.30-f4281b16","2022.09.30-62324655","2022.09.29-f3d25f9c","2022.09.29-a7d69323","2022.09.29-5f340154","2022.09.29-520a873a","2022.09.29-50607324","latest"]},{"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","latest"]},{"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.14.0","v2.13.1","v2.13.0","v2.12.0","v2.11.2","v2.11.1","v2.11.0","v2.10.1","v2.10.0","v2.1.1","v2.1.0","latest"]},{"name":"umami-postgresql","image":"ghcr.io/umami-software/umami","tags":["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.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","latest"]},{"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","latest"]},{"name":"ghost-mariadb","image":"bitnami/ghost","tags":["5.7.1","5.7.0","5.5.0","5.4.1","5.4.0","5.3.1","5.3.0","5.22.4","5.22.3","5.22.2","5.22.1","5.22.0","5.21.0","5.20.0","5.2.4","5.2.3","5.2.2","5.2.1","5.19.3","5.19.2","5.19.1","5.19.0","5.18.0","5.17.2","5.17.1","5.17.0","5.16.2","5.16.1","5.16.0","5.15.0","latest"]},{"name":"ghost-only","image":"library/ghost","tags":["5.9.4","5.8.3","5.8.2","5.7.1","5.7.0","5.5.0","5.4.1","5.3.1","5.3.0","5.22.4","5.22.1","5.20.0","5.2.4","5.2.3","5.2.2","5.2.1","5.19.3","5.19.0","5.18.0","5.17.2","5.17.1","5.17.0","5.16.2","5.14.2","5.14.1","5.13.2","5.12.3","5.12.2","5.12.0","5.11.0","latest"]},{"name":"ghost-mysql","image":"library/ghost","tags":["5.9.4","5.8.3","5.8.2","5.7.1","5.7.0","5.5.0","5.4.1","5.3.1","5.3.0","5.22.4","5.22.1","5.20.0","5.2.4","5.2.3","5.2.2","5.2.1","5.19.3","5.19.0","5.18.0","5.17.2","5.17.1","5.17.0","5.16.2","5.14.2","5.14.1","5.13.2","5.12.3","5.12.2","5.12.0","5.11.0","latest"]},{"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","latest"]},{"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","latest"]},{"name":"vscodeserver","image":"codercom/code-server","tags":["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","latest"]},{"name":"minio","image":"minio/minio","tags":["RELEASE.2022-10-29T06-21-33Z","RELEASE.2022-10-24T18-35-07Z","RELEASE.2022-10-21T22-37-48Z","RELEASE.2022-10-20T00-55-09Z","RELEASE.2022-10-15T19-57-03Z","RELEASE.2022-10-08T20-11-00Z","RELEASE.2022-10-05T14-58-27Z","RELEASE.2022-10-02T19-29-29Z","RELEASE.2022-09-25T15-44-53Z","RELEASE.2022-09-22T18-57-27Z","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","RELEASE.2022-07-26T00-53-03Z.fips","RELEASE.2022-07-24T17-09-31Z.fips","RELEASE.2022-07-24T01-54-52Z.fips","RELEASE.2022-07-17T15-43-14Z.fips","RELEASE.2022-07-15T03-44-22Z.fips","RELEASE.2022-07-13T23-29-44Z.hotfix.9184eff65","latest"]},{"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","latest"]},{"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","latest"]},{"name":"plausibleanalytics","image":"plausible/analytics","tags":["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":"nocodb","image":"nocodb/nocodb","tags":["0.98.3","0.98.1","0.97.0","0.96.3","0.96.1","0.92.4","0.92.0","0.91.8","0.91.6","0.91.1","0.90.8","0.90.5","0.90.3","0.90.11","0.90.1","0.9.9","0.9.7","0.9.43","0.9.41","0.9.39","0.9.37","0.9.35","0.9.33","0.9.31","0.9.29","0.9.27","0.9.25","0.9.22","0.9.19","0.9.16","latest"]}] \ No newline at end of file diff --git a/apps/api/devTemplates.yaml b/apps/api/devTemplates.yaml new file mode 100644 index 000000000..6b432ead4 --- /dev/null +++ b/apps/api/devTemplates.yaml @@ -0,0 +1,2785 @@ +- 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_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_PASSWORD=$$secret_hasura_graphql_admin_password + 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_password + name: HASURA_GRAPHQL_ADMIN_PASSWORD + 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 + documentation: "Official docs are [here](https://umami.is/docs/getting-started)" + 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 + documentation: "Official docs are [here](https://umami.is/docs/getting-started)" + 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 + defaultVersion: latest + documentation: https://ghost.org/resources/ + type: ghost-mariadb + name: Ghost + subname: (MariaDB) + description: >- + Free and open source blogging platform. + labels: + - cms + - blog + services: + $$id: + name: Ghost + documentation: "Taken from https://docs.ghost.org/" + 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://ghost.org/resources/ + type: ghost-only + name: Ghost + subname: (without Database) + description: >- + Free and open source blogging platform. + services: + $$id: + name: Ghost + documentation: "Taken from https://docs.ghost.org/" + 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://ghost.org/resources/ + type: ghost-mysql + name: Ghost + subname: (MySQL) + description: >- + Ghost is a free and open source blogging platform. + services: + $$id: + name: Ghost + documentation: "Taken from https://docs.ghost.org/" + 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 + documentation: " Taken from https://docs.docker.com/compose/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:5.7" + 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 + documentation: "Taken from https://docs.docker.com/compose/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 + documentation: "Taken from https://github.com/coder/code-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" + documentation: "Taken from https://docs.min.io/docs/minio-docker-quickstart-guide.html" + depends_on: [] + image: "minio/minio:$$core_version" + volumes: + - "$$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" + documentation: "Taken from https://hub.docker.com/r/getfider/fider/" + 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 + documentation: "Taken from https://hub.docker.com/r/getfider/fider/" + 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 + documentation: "Taken from https://hub.docker.com/r/n8nio/n8n" + depends_on: [] + image: "n8nio/n8n:$$core_version" + volumes: + - "$$id-data:/root/.n8n" + - "$$id-data-write:/files" + - "/var/run/docker.sock:/var/run/docker.sock" + environment: + - WEBHOOK_URL=$$config_webhook_url + ports: + - "5678" + variables: + - id: $$config_webhook_url + name: WEBHOOK_URL + label: Webhook URL + defaultValue: $$generate_fqdn + description: "" +- templateVersion: 1.0.0 + defaultVersion: stable + documentation: https://plausible.io/doc/ + type: plausibleanalytics + name: Plausible Analytics + description: A lightweight and open-source website analytics tool. + labels: + - analytics + - statistics + - plausible + - gdpr + - no-cookie + - google analytics + services: + $$id: + name: Plausible Analytics + documentation: "Taken from https://plausible.io/" + 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 + documentation: "Taken from https://plausible.io/" + image: "bitnami/postgresql:13.2.0" + volumes: + - "$$id-postgresql-data:/bitnami/postgresql" + environment: + - POSTGRESQL_PASSWORD=$$secret_postgresql_password + - POSTGRESQL_USERNAME=$$config_postgresql_username + - POSTGRESQL_DATABASE=$$config_postgresql_database + $$id-clickhouse: + name: Clickhouse + documentation: "Taken from https://plausible.io/" + volumes: + - "$$id-clickhouse-data:/var/lib/clickhouse" + image: "yandex/clickhouse-server:21.3.2.5" + ulimits: + nofile: + soft: 262144 + hard: 262144 + files: + - location: /etc/clickhouse-server/users.d/logging.xml + content: >- + warningtrue + - location: /etc/clickhouse-server/config.d/logging.xml + content: >- + 00 + - location: /docker-entrypoint-initdb.d/init.query + content: CREATE DATABASE IF NOT EXISTS plausible; + - location: /docker-entrypoint-initdb.d/init-db.sh + content: >- + clickhouse client --queries-file + /docker-entrypoint-initdb.d/init.query + variables: + - id: $$config_base_url + name: BASE_URL + label: Base URL + defaultValue: $$generate_fqdn + description: >- + You must set this to the FQDN of the Plausible Analytics instance. This + is used to generate the links to the Plausible Analytics instance. + - id: $$secret_database_url + name: DATABASE_URL + label: Database URL for PostgreSQL + defaultValue: >- + postgresql://$$config_postgresql_username:$$secret_postgresql_password@$$id-postgresql:5432/$$config_postgresql_database + description: "" + - id: $$secret_clickhouse_database_url + name: CLICKHOUSE_DATABASE_URL + label: Database URL for Clickhouse + defaultValue: "http://$$id-clickhouse:8123/plausible" + description: "" + - id: $$config_admin_user_email + name: ADMIN_USER_EMAIL + label: Admin Email Address + defaultValue: admin@example.com + description: This is the admin email. Please change it. + - id: $$config_admin_user_name + name: ADMIN_USER_NAME + label: Admin User Name + defaultValue: $$generate_username + description: This is the admin username. Please change it. + - id: $$secret_admin_user_pwd + name: ADMIN_USER_PWD + label: Admin User Password + defaultValue: $$generate_password + description: This is the admin password. Please change it. + showOnConfiguration: true + - id: $$secret_secret_key_base + name: SECRET_KEY_BASE + label: Secret Key Base + defaultValue: $$generate_hex(64) + description: "" + - id: $$config_disable_auth + name: DISABLE_AUTH + label: Disable Authentication + defaultValue: "false" + description: "" + - id: $$config_disable_registration + name: DISABLE_REGISTRATION + label: Disable Registration + defaultValue: "true" + description: "" + - id: $$config_postgresql_username + main: $$id-postgresql + name: POSTGRESQL_USERNAME + label: PostgreSQL Username + defaultValue: postgresql + description: "" + - id: $$secret_postgresql_password + main: $$id-postgresql + name: POSTGRESQL_PASSWORD + label: PostgreSQL Password + defaultValue: $$generate_password + description: "" + showOnConfiguration: true + - id: $$config_postgresql_database + main: $$id-postgresql + name: POSTGRESQL_DATABASE + label: PostgreSQL Database + defaultValue: plausible + description: "" + - id: $$config_scriptName + name: SCRIPT_NAME + label: Custom Script Name + defaultValue: plausible.js + description: This is the default script name. +- templateVersion: 1.0.0 + defaultVersion: 0.98.1 + documentation: https://docs.nocodb.com + type: nocodb + name: NocoDB + description: >- + Turns any MySQL, PostgreSQL, SQL Server, SQLite & MariaDB into a smart-spreadsheet. + labels: + - database + - airtable + - spreadsheet + services: + $$id: + name: NocoDB + image: nocodb/nocodb:$$core_version + environment: + - PORT=$$config_port + - NC_DB=$$config_nc_db + - DATABASE_URL=$$secret_database_url + - NC_PUBLIC_URL=$$config_public_url + - NC_AUTH_JWT_SECRET=$$secret_auth_jwt_secret + - NC_SENTRY_DSN=$$secret_sentry_dsn + - >- + NC_CONNECT_TO_EXTERNAL_DB_DISABLED=$$config_connect_to_external_db_disabled + - NC_DISABLE_TELE=$$config_disable_tele + volumes: + - $$id-data:/usr/app/data + ports: + - "8080" + variables: + - id: $$config_nc_db + name: NC_DB + label: Database + defaultValue: "" + description: >- + MySQL, PostgreSQL and MSSQL connection urls supported. If absent: A + local SQLite will be created in root folder. + - id: $$config_port + name: PORT + label: Port + defaultValue: "8080" + description: >- + + - id: $$secret_database_url + name: DATABASE_URL + label: Database URL + defaultValue: "" + description: >- + JDBC URL Format. Can be used instead of NC_DB. Used in 1-Click Heroku + deployment. + - id: $$config_public_url + name: NC_PUBLIC_URL + label: Public URL + defaultValue: "" + description: >- + Used for sending Email invitations. If absent: Best guess from http + request params. + - id: $$secret_auth_jwt_secret + name: NC_AUTH_JWT_SECRET + label: Auth JWT Secret + defaultValue: $$generate_hex(64) + description: >- + JWT secret used for auth and storing other secrets. If absent: A Random + secret will be generated. + - id: $$secret_sentry_dsn + name: NC_SENTRY_DSN + label: Sentry DSN + defaultValue: "" + description: For Sentry monitoring. + - id: $$config_connect_to_external_db_disabled + name: NC_CONNECT_TO_EXTERNAL_DB_DISABLED + label: Disable External Database + defaultValue: "0" + description: Disable Project creation with external database. (Enter "1" to disable). + - id: $$config_disable_tele + name: NC_DISABLE_TELE + label: NocoDB Disable Telemetry + defaultValue: "1" + description: Disable telemetry (Enter "1" to disable). diff --git a/apps/api/package.json b/apps/api/package.json index 49d5f34be..a2c988d73 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -3,7 +3,7 @@ "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", @@ -16,18 +16,16 @@ }, "dependencies": { "@breejs/ts-worker": "2.0.0", - "@fastify/autoload": "5.4.0", + "@fastify/autoload": "5.4.1", "@fastify/cookie": "8.3.0", - "@fastify/cors": "8.1.0", + "@fastify/cors": "8.1.1", "@fastify/env": "4.1.0", "@fastify/jwt": "6.3.2", - "@fastify/multipart": "7.2.0", + "@fastify/multipart": "7.3.0", "@fastify/static": "6.5.0", "@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.5.0", "bcryptjs": "2.4.3", "bree": "9.1.2", "cabin": "9.1.2", @@ -35,12 +33,13 @@ "csv-parse": "5.3.1", "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.9.2", + "fastify-plugin": "4.3.0", + "fastify-socket.io": "4.0.0", "generate-password": "1.7.0", "got": "12.5.2", "is-ip": "5.0.0", @@ -51,26 +50,28 @@ "node-os-utils": "1.3.7", "p-all": "4.0.0", "p-throttle": "5.0.0", + "prisma": "4.5.0", "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.6", "@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.41.0", + "@typescript-eslint/parser": "5.41.0", + "esbuild": "0.15.12", + "eslint": "8.26.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", + "types-fastify-socket.io": "0.0.1", "typescript": "4.8.4" }, "prisma": { 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/schema.prisma b/apps/api/prisma/schema.prisma index d782bceae..b2de9d23d 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -29,6 +29,7 @@ model Setting { proxyPassword String proxyUser String proxyHash String? + proxyDefaultRedirect String? isAutoUpdateEnabled Boolean @default(false) isDNSCheckEnabled Boolean @default(true) DNSServers String? @@ -193,14 +194,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 { @@ -392,12 +396,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? @@ -417,6 +423,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? @@ -462,10 +481,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) 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..c760116b0 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -6,14 +6,20 @@ 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, encrypt, executeDockerCmd, executeSSHCmd, generateDatabaseConfiguration, isDev, listSettings, prisma, 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 { migrateServicesToNewTemplate } from './lib'; +import { refreshTags, refreshTemplates } from './routes/api/v1/handlers'; + declare module 'fastify' { interface FastifyInstance { config: { @@ -103,27 +109,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 initServer(); const graceful = new Graceful({ brees: [scheduler] }); @@ -145,21 +164,30 @@ 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() + await refreshTags() + await migrateServicesToNewTemplate() + }, 60000) setInterval(async () => { await copySSLCertificates(); - }, 2000) + }, 10000) await Promise.all([ + getTagsTemplates(), getArch(), getIPAddress(), configureRemoteDockers(), ]) + } catch (error) { console.error(error); process.exit(1); @@ -185,6 +213,28 @@ async function getIPAddress() { } catch (error) { } } +async function getTagsTemplates() { + const { default: got } = await import('got') + try { + 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('Tags and templates loaded in dev mode...') + } else { + const tags = await got.get('https://get.coollabs.io/coolify/service-tags.json').text() + const response = await got.get('https://get.coollabs.io/coolify/service-templates.yaml').text() + await fs.writeFile('/app/templates.json', JSON.stringify(yaml.load(response))) + await fs.writeFile('/app/tags.json', tags) + console.log('Tags and templates loaded...') + } + + } catch (error) { + console.log("Couldn't get latest templates.") + console.log(error) + } +} async function initServer() { try { console.log(`Initializing server...`); @@ -197,6 +247,7 @@ async function initServer() { } } catch (error) { } } + async function getArch() { try { const settings = await prisma.setting.findFirst({}) @@ -226,17 +277,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 +307,9 @@ async function autoUpdater() { } } } - } catch (error) { } + } catch (error) { + console.log(error) + } } async function checkFluentBit() { @@ -338,17 +389,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 b2d2c8ec9..42e1bcef8 100644 --- a/apps/api/src/jobs/deployApplication.ts +++ b/apps/api/src/jobs/deployApplication.ts @@ -131,7 +131,6 @@ import * as buildpacks from '../lib/buildPacks'; try { dockerComposeConfiguration = JSON.parse(dockerComposeConfiguration) } catch (error) { } - let deployNeeded = true; let destinationType; @@ -313,11 +312,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) { // diff --git a/apps/api/src/lib.ts b/apps/api/src/lib.ts new file mode 100644 index 000000000..c3d29d062 --- /dev/null +++ b/apps/api/src/lib.ts @@ -0,0 +1,479 @@ +import cuid from "cuid"; +import { decrypt, encrypt, fixType, generatePassword, getDomain, prisma } from "./lib/common"; +import { getTemplates } from "./lib/services"; + +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) { + 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) + + await createVolumes(service, template); + + 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 { + 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 service of Object.keys(template.services)) { + if (template.services[service].volumes) { + for (const volume of template.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: { templateVersion: template.templateVersion } }) + } + + } + } 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_PASSWORD@@@${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); + + // 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) { + 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 } } } }) + } +} +async function migrateSecrets(secrets: any[], service: any) { + for (const secret of secrets) { + 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 } } } }) + } +} +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) { + const volumeName = volume.split(':')[0] + const volumePath = volume.split(':')[1] + const volumeService = s + 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..32e498845 100644 --- a/apps/api/src/lib/buildPacks/common.ts +++ b/apps/api/src/lib/buildPacks/common.ts @@ -480,7 +480,6 @@ export const saveBuildLog = async ({ } }) } catch (error) { - if (isDev) return return await prisma.buildLog.create({ data: { line: addTimestamp, buildId, time: Number(day().valueOf()), applicationId 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..4bca464ec 100644 --- a/apps/api/src/lib/common.ts +++ b/apps/api/src/lib/common.ts @@ -14,13 +14,10 @@ import sshConfig from 'ssh-config'; 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.11.0'; export const isDev = process.env.NODE_ENV === 'development'; const algorithm = 'aes-256-ctr'; @@ -44,7 +41,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 +195,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 +241,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 +280,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 +395,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 +504,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 +570,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'; } @@ -1095,6 +1090,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 +1395,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 +1443,18 @@ 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 + } }); 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 +1462,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', @@ -1681,7 +1613,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('/var/run/docker.sock')) { + volumeSet.add(volume); + } } } } diff --git a/apps/api/src/lib/services.ts b/apps/api/src/lib/services.ts index d7f0fd75e..7e249ace4 100644 --- a/apps/api/src/lib/services.ts +++ b/apps/api/src/lib/services.ts @@ -1,20 +1,170 @@ -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 { isDev } from "./common"; +import fs from 'fs/promises'; +export async function getTemplates() { + let templates: any = []; + if (isDev) { + templates = JSON.parse(await (await fs.readFile('./templates.json')).toString()) + } else { + templates = JSON.parse(await (await fs.readFile('/app/templates.json')).toString()) } - return { ...service, network, port, workdir, image, secrets } -} \ No newline at end of file + // if (!isDev) { + // templates.push({ + // "templateVersion": "1.0.0", + // "defaultVersion": "latest", + // "name": "Test-Fake-Service", + // "description": "", + // "services": { + // "$$id": { + // "name": "Test-Fake-Service", + // "depends_on": [ + // "$$id-postgresql", + // "$$id-redis" + // ], + // "image": "weblate/weblate:$$core_version", + // "volumes": [ + // "$$id-data:/app/data", + // ], + // "environment": [ + // `POSTGRES_SECRET=$$secret_postgres_secret`, + // `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", + // "main": "$$id", + // "name": "WEBLATE_SITE_DOMAIN", + // "label": "Weblate Domain", + // "defaultValue": "$$generate_domain", + // "description": "", + // }, + // { + // "id": "$$secret_weblate_admin_password", + // "main": "$$id", + // "name": "WEBLATE_ADMIN_PASSWORD", + // "label": "Weblate Admin Password", + // "defaultValue": "$$generate_password", + // "description": "", + // "extras": { + // "isVisibleOnUI": true, + // } + // }, + // { + // "id": "$$secret_weblate_admin_password2", + // "name": "WEBLATE_ADMIN_PASSWORD2", + // "label": "Weblate Admin Password2", + // "defaultValue": "$$generate_password", + // "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(32)", + // "description": "", + // }, + // { + // "id": "$$secret_postgres_password_hex32", + // "name": "POSTGRES_PASSWORD_hex32", + // "label": "PostgreSQL Password hex32", + // "defaultValue": "$$generate_hex(32)", + // "description": "", + // }, + // { + // "id": "$$config_postgres_something_hex32", + // "name": "POSTGRES_SOMETHING_HEX32", + // "label": "PostgreSQL Something hex32", + // "defaultValue": "$$generate_hex(32)", + // "description": "", + // }, + // { + // "id": "$$config_postgres_db", + // "main": "$$id-postgresql", + // "name": "POSTGRES_DB", + // "label": "PostgreSQL Database", + // "defaultValue": "weblate", + // "description": "", + // }, + // { + // "id": "$$secret_postgres_secret", + // "name": "POSTGRES_SECRET", + // "label": "PostgreSQL Secret", + // "defaultValue": "", + // "description": "", + // }, + // ] + // }) + // } + return templates +} +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) { + if (type) { + let tags: any = []; + if (isDev) { + tags = JSON.parse(await (await fs.readFile('./tags.json')).toString()) + } else { + tags = JSON.parse(await (await fs.readFile('/app/tags.json')).toString()) + } + tags = tags.find((tag: any) => tag.name.includes(type)) + tags.tags = tags.tags.sort(compareSemanticVersions).reverse(); + return tags + } + return [] +} diff --git a/apps/api/src/lib/services/common.ts b/apps/api/src/lib/services/common.ts index 716dc44c0..528e4ef12 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 { 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 } }); diff --git a/apps/api/src/lib/services/handlers.ts b/apps/api/src/lib/services/handlers.ts index bcaf07d36..ee6755a76 100644 --- a/apps/api/src/lib/services/handlers.ts +++ b/apps/api/src/lib/services/handlers.ts @@ -1,1907 +1,14 @@ 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'; -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 +29,156 @@ 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 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` + const secrets = await prisma.serviceSecret.findMany({ where: { serviceId: 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}=${decrypt(value)}`) + } + } + } + const customVolumes = await prisma.servicePersistentStorage.findMany({ where: { serviceId: id } }) + let volumes = arm ? template.services[s].volumesArm : template.services[s].volumes + if (customVolumes.length > 0) { + for (const customVolume of customVolumes) { + const { volumeName, path, containerId } = customVolume + if (volumes && volumes.length > 0 && !volumes.includes(`${volumeName}:${path}`) && containerId === service) { + volumes.push(`${volumeName}:${path}`) + } + } + } - const Dockerfile = ` - FROM ${config.searxng.image} - COPY ./settings.yml /etc/searxng/settings.yml`; + config[s] = { + container_name: s, + build: template.services[s].build || undefined, + command: template.services[s].command, + entrypoint: template.services[s]?.entrypoint, + image: arm ? template.services[s].imageArm : template.services[s].image, + expose: template.services[s].ports, + ...(exposePort ? { ports: [`${exposePort}:${exposePort}`] } : {}), + 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), + } - if (serviceSecret.length > 0) { - serviceSecret.forEach((secret) => { - config.searxng.environmentVariables[secret.name] = secret.value; - }); + // 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) + 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 d179aabfa..e166951a9 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'; @@ -771,6 +770,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); @@ -782,13 +782,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 }) @@ -819,7 +819,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({ @@ -970,6 +970,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/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..eff8532a3 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,68 @@ 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) + throw { + status: 500, + message: 'Could not fetch templates from get.coollabs.io' + }; + } + + 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) + throw { + status: 500, + message: 'Could not fetch templates from get.coollabs.io' + }; + } + + 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 +409,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..7f0c6c61c 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 { checkUpdate, login, showDashboard, update, resetQueue, getCurrentUser, cleanupManually, restartCoolify, refreshTemplates } 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 } @@ -55,6 +52,10 @@ const root: FastifyPluginAsync = async (fastify): Promise => { fastify.post('/internal/cleanup', { onRequest: [fastify.authenticate] }, async (request) => await cleanupManually(request)); + + fastify.post('/internal/refreshTemplates', { + onRequest: [fastify.authenticate] + }, async () => await refreshTemplates()); }; export default root; diff --git a/apps/api/src/routes/api/v1/services/handlers.ts b/apps/api/src/routes/api/v1/services/handlers.ts index 2dcc434b6..71dcf3628 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 } 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,186 @@ 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); + template = JSON.parse(JSON.stringify(template).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] = { + name, + documentation: value.documentation || foundTemplate.documentation || 'https://docs.coollabs.io', + image: value.image, + environment: [], + fqdns: [], + proxy: {} + } + if (value.environment?.length > 0) { + for (const env of value.environment) { + let [envKey, ...envValue] = env.split('=') + envValue = envValue.join("=") + const 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 } + ) + } + + } + } + } + } + } 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 $$fqdn + 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_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) { + const { name, value } = secret + 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 +258,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 +277,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 +287,79 @@ 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.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 { + 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 +406,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 +417,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 +426,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 +479,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 +510,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 +550,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 +579,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 +662,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 +719,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..4aa5905f0 100644 --- a/apps/api/src/routes/api/v1/settings/handlers.ts +++ b/apps/api/src/routes/api/v1/settings/handlers.ts @@ -2,7 +2,7 @@ import { promises as dns } from 'dns'; import { X509Certificate } from 'node:crypto'; import type { FastifyReply, FastifyRequest } from 'fastify'; -import { asyncExecShell, checkDomainsIsValidInDNS, decrypt, encrypt, errorHandler, isDNSValid, isDomainConfigured, listSettings, prisma } from '../../../../lib/common'; +import { asyncExecShell, checkDomainsIsValidInDNS, decrypt, encrypt, errorHandler, isDev, isDNSValid, isDomainConfigured, listSettings, prisma } from '../../../../lib/common'; import { CheckDNS, CheckDomain, DeleteDomain, OnlyIdInBody, SaveSettings, SaveSSHKey } from './types'; @@ -44,16 +44,18 @@ 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: { 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 } }); } @@ -91,7 +93,7 @@ export async function checkDomain(request: FastifyRequest) { if (found) { throw "Domain already configured"; } - if (isDNSCheckEnabled && !forceSave) { + if (isDNSCheckEnabled && !forceSave && !isDev) { const hostname = request.hostname.split(':')[0] return await checkDomainsIsValidInDNS({ hostname, fqdn, dualCerts }); } diff --git a/apps/api/src/routes/api/v1/settings/types.ts b/apps/api/src/routes/api/v1/settings/types.ts index 618101bba..d68a7bdd3 100644 --- a/apps/api/src/routes/api/v1/settings/types.ts +++ b/apps/api/src/routes/api/v1/settings/types.ts @@ -10,7 +10,8 @@ export interface SaveSettings { maxPort: number, isAutoUpdateEnabled: boolean, isDNSCheckEnabled: boolean, - DNSServers: string + DNSServers: string, + proxyDefaultRedirect: string } } export interface DeleteDomain { 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; + } + if ( + !runningContainers[destinationDockerId] || + runningContainers[destinationDockerId].length === 0 || + !runningContainers[destinationDockerId].includes(id) + ) { + continue } - continue; - } - if (fqdn) { + 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; + + traefik.http.routers = { ...traefik.http.routers, ...generateRouters(`${id}-${port || 'default'}`, domain, nakedDomain, pathPrefix, isHttps, isWWW, dualCerts, isCustomSSL) } + traefik.http.services = { ...traefik.http.services, ...generateServices(id, 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 = '/' + + traefik.http.routers = { ...traefik.http.routers, ...generateRouters(`${id}-${port || 'default'}`, domain, nakedDomain, pathPrefix, isHttps, isWWW, dualCerts, isCustomSSL) } + traefik.http.services = { ...traefik.http.services, ...generateServices(id, 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,148 @@ 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 = '/' + + traefik.http.routers = { ...traefik.http.routers, ...generateRouters(`${container}-${port || 'default'}`, previewDomain, nakedDomain, pathPrefix, isHttps, isWWW, dualCerts, isCustomSSL) } + traefik.http.services = { ...traefik.http.services, ...generateServices(container, 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 isProxyConfiguration = found.services[oneService].proxy; + if (isProxyConfiguration) { + 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 || null; + 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; + traefik.http.routers = { ...traefik.http.routers, ...generateRouters(`${id}-${port || 'default'}`, domain, nakedDomain, pathPrefix, isHttps, isWWW, dualCerts, isCustomSSL) } + traefik.http.services = { ...traefik.http.services, ...generateServices(id, id, 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) { + let port = found.services[oneService].ports[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 + + traefik.http.routers = { ...traefik.http.routers, ...generateRouters(`${id}-${port || 'default'}`, domain, nakedDomain, pathPrefix, isHttps, isWWW, dualCerts, isCustomSSL) } + traefik.http.services = { ...traefik.http.services, ...generateServices(id, 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; + const id = isDev ? 'host.docker.internal' : 'coolify' + const container = isDev ? 'host.docker.internal' : 'coolify' + const port = 3000 + const pathPrefix = '/' + const isCustomSSL = false - 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); + traefik.http.routers = { ...traefik.http.routers, ...generateRouters(`${id}-${port || 'default'}`, domain, nakedDomain, pathPrefix, isHttps, isWWW, dualCerts, isCustomSSL) } + traefik.http.services = { ...traefik.http.services, ...generateServices(id, 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 +511,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..47f6c6cb5 --- /dev/null +++ b/apps/api/tags.json @@ -0,0 +1 @@ +[{"name":"appsmith","image":"appsmith/appsmith-ce","tags":["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","latest"]},{"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","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","latest"]},{"name":"languagetool","image":"silviof/docker-languagetool","tags":["latest","5.8","5.7","5.6","5.5","5.4","5.3"]},{"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","latest"]},{"name":"grafana","image":"grafana/grafana","tags":["9.2.3","9.2.2","9.2.1","9.2.0","9.1.8","9.1.7","9.1.6","9.1.5","9.1.4","9.1.3","9.1.2","9.1.1","9.1.0","9.0.9","9.0.8","9.0.7","9.0.6","9.0.5","9.0.4","9.0.3","9.0.2","9.0.1","9.0.0","8.5.9","8.5.6","8.5.5","8.5.4","8.5.3","8.5.2","8.5.14","latest"]},{"name":"appwrite","image":"appwrite/appwrite","tags":["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","0.1.6","latest"]},{"name":"weblate","image":"weblate/weblate","tags":["latest","edge-2022-11-02-e4171e0c5657ca38341cce8ac31f5cbdf25389eb","edge-2022-11-02-6d886c40cd62eb23d21f7c0a1840b4a7a4c51ad0","edge-2022-11-01-608df4dd95a2d1f76c15cddd9e116bb4c3229168","edge-2022-11-01-54957be78eb76f602ceae50c0b01b64b20402b2a","edge-2022-10-31-c55c7302a6c82a160ee9d711893c12d67ecd3b27","edge-2022-10-26-c69cfdd83ed1fad4a4d57398552b8c70894a6586","edge-2022-10-26-410b3aff37de5bbfacbc47642ce28b2518bee506","edge-2022-10-25-e09e2c29ed3748eb0fa248453635dd27768e8dd9","edge-2022-10-25-a059748c178cce0bc30bdc915d4ff8f0d13ce25c","edge-2022-10-24-94ae069cdcf9a171e812da32d760a436fa9b37ad","edge-2022-10-24-71d2011a1f6d2b60bb65af9f775bf75d4beaf8fd","edge-2022-10-19-acb8e82a0ea2c86fb7ce8f5cb1a81e2bc634583a","edge-2022-10-18-f54b83a0898bef9d14b00d885f675c2e4ebb68ca","edge-2022-10-17-eb9dc248720581052b4be4e3f1f372bce37edd62","edge-2022-10-17-e5a5f6cfdf7e500b7b357566971172fad022c1dc","edge-2022-10-17-da6d01ea005c29453e0111a45956d16bf2262f54","edge-2022-10-17-7a28d7f727778dd05b163fa40ec7903e2e07b20c","edge-2022-10-13-899ea7d8aef900ef216e231433fe15e8dc9a2af8","edge-2022-10-13-385a55156bdc918b0e78ce337322b8996455027a","edge-2022-10-12-5356be32da4b8beb16ba3ec0aa8763f6e08294ab","edge-2022-10-10-d087b31ef728a3a9c3563de9aa4b2ea189755fdf","edge-2022-10-07-f6d43c61fee0e58eb18dd6f80404fba272280c3f","edge-2022-10-05-b86e34a866f40446dc8cb1b48f5d56e621904a0c","edge-2022-10-05-82096af0a688d0104c1c5b973dfc1c6eef6f9d85","edge-2022-10-05-495225a1fd551be41301b53dcd5b10d0c97927bb","edge-2022-10-05-0bb3091700132894e51d806a140145a03489fc09","edge-2022-10-03-9255921fca690c5350ba16960d856c68b910e586","edge-2022-10-03-4b9d377a8289ede1a16b05836eeed0c3b0570dd9","edge-2022-10-01-482ba9c3f7862bca8b1e70d3d0a02806e075d120"]},{"name":"searxng","image":"searxng/searxng","tags":["2022.10.29-fc9986de","2022.10.29-fa59ff9b","2022.10.29-d49ccb54","2022.10.29-a9deead1","2022.10.29-3f1d594c","2022.10.28-c26fa335","2022.10.28-5db4ed5d","2022.10.28-5a181ea1","2022.10.25-affd8f75","2022.10.25-4783d6c9","2022.10.21-710a3a00","2022.10.14-e2dd5a80","2022.10.14-72f6367e","2022.10.14-4d4dfc58","2022.10.14-2eb81701","2022.10.14-1a5b0965","2022.10.14-096d9def","2022.10.11-a7337612","2022.10.07-84f61af8","2022.10.07-666cd1f6","2022.10.01-e9af772b","2022.10.01-901143f0","2022.10.01-14d0fb2c","2022.09.30-f4281b16","2022.09.30-62324655","2022.09.29-f3d25f9c","2022.09.29-a7d69323","2022.09.29-5f340154","2022.09.29-520a873a","2022.09.29-50607324","latest"]},{"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","latest"]},{"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.14.0","v2.13.1","v2.13.0","v2.12.0","v2.11.2","v2.11.1","v2.11.0","v2.10.1","v2.10.0","v2.1.1","v2.1.0","latest"]},{"name":"umami-postgresql","image":"ghcr.io/umami-software/umami","tags":["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.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","latest"]},{"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","latest"]},{"name":"ghost-mariadb","image":"bitnami/ghost","tags":["5.7.1","5.7.0","5.5.0","5.4.1","5.4.0","5.3.1","5.3.0","5.22.4","5.22.3","5.22.2","5.22.1","5.22.0","5.21.0","5.20.0","5.2.4","5.2.3","5.2.2","5.2.1","5.19.3","5.19.2","5.19.1","5.19.0","5.18.0","5.17.2","5.17.1","5.17.0","5.16.2","5.16.1","5.16.0","5.15.0","latest"]},{"name":"ghost-only","image":"library/ghost","tags":["5.9.4","5.8.3","5.8.2","5.7.1","5.7.0","5.5.0","5.4.1","5.3.1","5.3.0","5.22.4","5.22.1","5.20.0","5.2.4","5.2.3","5.2.2","5.2.1","5.19.3","5.19.0","5.18.0","5.17.2","5.17.1","5.17.0","5.16.2","5.14.2","5.14.1","5.13.2","5.12.3","5.12.2","5.12.0","5.11.0","latest"]},{"name":"ghost-mysql","image":"library/ghost","tags":["5.9.4","5.8.3","5.8.2","5.7.1","5.7.0","5.5.0","5.4.1","5.3.1","5.3.0","5.22.4","5.22.1","5.20.0","5.2.4","5.2.3","5.2.2","5.2.1","5.19.3","5.19.0","5.18.0","5.17.2","5.17.1","5.17.0","5.16.2","5.14.2","5.14.1","5.13.2","5.12.3","5.12.2","5.12.0","5.11.0","latest"]},{"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","latest"]},{"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","latest"]},{"name":"vscodeserver","image":"codercom/code-server","tags":["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","latest"]},{"name":"minio","image":"minio/minio","tags":["RELEASE.2022-10-29T06-21-33Z","RELEASE.2022-10-24T18-35-07Z","RELEASE.2022-10-21T22-37-48Z","RELEASE.2022-10-20T00-55-09Z","RELEASE.2022-10-15T19-57-03Z","RELEASE.2022-10-08T20-11-00Z","RELEASE.2022-10-05T14-58-27Z","RELEASE.2022-10-02T19-29-29Z","RELEASE.2022-09-25T15-44-53Z","RELEASE.2022-09-22T18-57-27Z","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","RELEASE.2022-07-26T00-53-03Z.fips","RELEASE.2022-07-24T17-09-31Z.fips","RELEASE.2022-07-24T01-54-52Z.fips","RELEASE.2022-07-17T15-43-14Z.fips","RELEASE.2022-07-15T03-44-22Z.fips","RELEASE.2022-07-13T23-29-44Z.hotfix.9184eff65","latest"]},{"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","latest"]},{"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","latest"]},{"name":"plausibleanalytics","image":"plausible/analytics","tags":["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":"nocodb","image":"nocodb/nocodb","tags":["0.98.3","0.98.1","0.97.0","0.96.3","0.96.1","0.92.4","0.92.0","0.91.8","0.91.6","0.91.1","0.90.8","0.90.5","0.90.3","0.90.11","0.90.1","0.9.9","0.9.7","0.9.43","0.9.41","0.9.39","0.9.37","0.9.35","0.9.33","0.9.31","0.9.29","0.9.27","0.9.25","0.9.22","0.9.19","0.9.16","latest"]}] \ No newline at end of file diff --git a/apps/api/templates.json b/apps/api/templates.json new file mode 100644 index 000000000..40bb5ed5c --- /dev/null +++ b/apps/api/templates.json @@ -0,0 +1 @@ +[{"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_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_PASSWORD=$$secret_hasura_graphql_admin_password"],"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_password","name":"HASURA_GRAPHQL_ADMIN_PASSWORD","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","documentation":"Official docs are [here](https://umami.is/docs/getting-started)","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","documentation":"Official docs are [here](https://umami.is/docs/getting-started)","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","defaultVersion":"latest","documentation":"https://ghost.org/resources/","type":"ghost-mariadb","name":"Ghost","subname":"(MariaDB)","description":"Free and open source blogging platform.","labels":["cms","blog"],"services":{"$$id":{"name":"Ghost","documentation":"Taken from https://docs.ghost.org/","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://ghost.org/resources/","type":"ghost-only","name":"Ghost","subname":"(without Database)","description":"Free and open source blogging platform.","services":{"$$id":{"name":"Ghost","documentation":"Taken from https://docs.ghost.org/","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://ghost.org/resources/","type":"ghost-mysql","name":"Ghost","subname":"(MySQL)","description":"Ghost is a free and open source blogging platform.","services":{"$$id":{"name":"Ghost","documentation":"Taken from https://docs.ghost.org/","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","documentation":" Taken from https://docs.docker.com/compose/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:5.7","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","documentation":"Taken from https://docs.docker.com/compose/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","documentation":"Taken from https://github.com/coder/code-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","documentation":"Taken from https://docs.min.io/docs/minio-docker-quickstart-guide.html","depends_on":[],"image":"minio/minio:$$core_version","volumes":["$$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","documentation":"Taken from https://hub.docker.com/r/getfider/fider/","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","documentation":"Taken from https://hub.docker.com/r/getfider/fider/","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","documentation":"Taken from https://hub.docker.com/r/n8nio/n8n","depends_on":[],"image":"n8nio/n8n:$$core_version","volumes":["$$id-data:/root/.n8n","$$id-data-write:/files","/var/run/docker.sock:/var/run/docker.sock"],"environment":["WEBHOOK_URL=$$config_webhook_url"],"ports":["5678"]}},"variables":[{"id":"$$config_webhook_url","name":"WEBHOOK_URL","label":"Webhook URL","defaultValue":"$$generate_fqdn","description":""}]},{"templateVersion":"1.0.0","defaultVersion":"stable","documentation":"https://plausible.io/doc/","type":"plausibleanalytics","name":"Plausible Analytics","description":"A lightweight and open-source website analytics tool.","labels":["analytics","statistics","plausible","gdpr","no-cookie","google analytics"],"services":{"$$id":{"name":"Plausible Analytics","documentation":"Taken from https://plausible.io/","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","documentation":"Taken from https://plausible.io/","image":"bitnami/postgresql:13.2.0","volumes":["$$id-postgresql-data:/bitnami/postgresql"],"environment":["POSTGRESQL_PASSWORD=$$secret_postgresql_password","POSTGRESQL_USERNAME=$$config_postgresql_username","POSTGRESQL_DATABASE=$$config_postgresql_database"]},"$$id-clickhouse":{"name":"Clickhouse","documentation":"Taken from https://plausible.io/","volumes":["$$id-clickhouse-data:/var/lib/clickhouse"],"image":"yandex/clickhouse-server:21.3.2.5","ulimits":{"nofile":{"soft":262144,"hard":262144}},"files":[{"location":"/etc/clickhouse-server/users.d/logging.xml","content":"warningtrue"},{"location":"/etc/clickhouse-server/config.d/logging.xml","content":"00"},{"location":"/docker-entrypoint-initdb.d/init.query","content":"CREATE DATABASE IF NOT EXISTS plausible;"},{"location":"/docker-entrypoint-initdb.d/init-db.sh","content":"clickhouse client --queries-file /docker-entrypoint-initdb.d/init.query"}]}},"variables":[{"id":"$$config_base_url","name":"BASE_URL","label":"Base URL","defaultValue":"$$generate_fqdn","description":"You must set this to the FQDN of the Plausible Analytics instance. This is used to generate the links to the Plausible Analytics instance."},{"id":"$$secret_database_url","name":"DATABASE_URL","label":"Database URL for PostgreSQL","defaultValue":"postgresql://$$config_postgresql_username:$$secret_postgresql_password@$$id-postgresql:5432/$$config_postgresql_database","description":""},{"id":"$$secret_clickhouse_database_url","name":"CLICKHOUSE_DATABASE_URL","label":"Database URL for Clickhouse","defaultValue":"http://$$id-clickhouse:8123/plausible","description":""},{"id":"$$config_admin_user_email","name":"ADMIN_USER_EMAIL","label":"Admin Email Address","defaultValue":"admin@example.com","description":"This is the admin email. Please change it."},{"id":"$$config_admin_user_name","name":"ADMIN_USER_NAME","label":"Admin User Name","defaultValue":"$$generate_username","description":"This is the admin username. Please change it."},{"id":"$$secret_admin_user_pwd","name":"ADMIN_USER_PWD","label":"Admin User Password","defaultValue":"$$generate_password","description":"This is the admin password. Please change it.","showOnConfiguration":true},{"id":"$$secret_secret_key_base","name":"SECRET_KEY_BASE","label":"Secret Key Base","defaultValue":"$$generate_hex(64)","description":""},{"id":"$$config_disable_auth","name":"DISABLE_AUTH","label":"Disable Authentication","defaultValue":"false","description":""},{"id":"$$config_disable_registration","name":"DISABLE_REGISTRATION","label":"Disable Registration","defaultValue":"true","description":""},{"id":"$$config_postgresql_username","main":"$$id-postgresql","name":"POSTGRESQL_USERNAME","label":"PostgreSQL Username","defaultValue":"postgresql","description":""},{"id":"$$secret_postgresql_password","main":"$$id-postgresql","name":"POSTGRESQL_PASSWORD","label":"PostgreSQL Password","defaultValue":"$$generate_password","description":"","showOnConfiguration":true},{"id":"$$config_postgresql_database","main":"$$id-postgresql","name":"POSTGRESQL_DATABASE","label":"PostgreSQL Database","defaultValue":"plausible","description":""},{"id":"$$config_scriptName","name":"SCRIPT_NAME","label":"Custom Script Name","defaultValue":"plausible.js","description":"This is the default script name."}]},{"templateVersion":"1.0.0","defaultVersion":"0.98.1","documentation":"https://docs.nocodb.com","type":"nocodb","name":"NocoDB","description":"Turns any MySQL, PostgreSQL, SQL Server, SQLite & MariaDB into a smart-spreadsheet.","labels":["database","airtable","spreadsheet"],"services":{"$$id":{"name":"NocoDB","image":"nocodb/nocodb:$$core_version","environment":["PORT=$$config_port","NC_DB=$$config_nc_db","DATABASE_URL=$$secret_database_url","NC_PUBLIC_URL=$$config_public_url","NC_AUTH_JWT_SECRET=$$secret_auth_jwt_secret","NC_SENTRY_DSN=$$secret_sentry_dsn","NC_CONNECT_TO_EXTERNAL_DB_DISABLED=$$config_connect_to_external_db_disabled","NC_DISABLE_TELE=$$config_disable_tele"],"volumes":["$$id-data:/usr/app/data"],"ports":["8080"]}},"variables":[{"id":"$$config_nc_db","name":"NC_DB","label":"Database","defaultValue":"","description":"MySQL, PostgreSQL and MSSQL connection urls supported. If absent: A local SQLite will be created in root folder."},{"id":"$$config_port","name":"PORT","label":"Port","defaultValue":"8080","description":""},{"id":"$$secret_database_url","name":"DATABASE_URL","label":"Database URL","defaultValue":"","description":"JDBC URL Format. Can be used instead of NC_DB. Used in 1-Click Heroku deployment."},{"id":"$$config_public_url","name":"NC_PUBLIC_URL","label":"Public URL","defaultValue":"","description":"Used for sending Email invitations. If absent: Best guess from http request params."},{"id":"$$secret_auth_jwt_secret","name":"NC_AUTH_JWT_SECRET","label":"Auth JWT Secret","defaultValue":"$$generate_hex(64)","description":"JWT secret used for auth and storing other secrets. If absent: A Random secret will be generated."},{"id":"$$secret_sentry_dsn","name":"NC_SENTRY_DSN","label":"Sentry DSN","defaultValue":"","description":"For Sentry monitoring."},{"id":"$$config_connect_to_external_db_disabled","name":"NC_CONNECT_TO_EXTERNAL_DB_DISABLED","label":"Disable External Database","defaultValue":"0","description":"Disable Project creation with external database. (Enter \"1\" to disable)."},{"id":"$$config_disable_tele","name":"NC_DISABLE_TELE","label":"NocoDB Disable Telemetry","defaultValue":"1","description":"Disable telemetry (Enter \"1\" to disable)."}]}] \ No newline at end of file diff --git a/apps/ui/package.json b/apps/ui/package.json index fdf37b8aa..168613d09 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.3", + "@playwright/test": "1.27.1", "@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.41.0", + "@typescript-eslint/parser": "5.41.0", + "autoprefixer": "10.4.12", + "classnames": "2.3.2", + "eslint": "8.26.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.3", + "flowbite-svelte": "0.27.11", + "postcss": "8.4.18", "prettier": "2.7.1", - "prettier-plugin-svelte": "2.7.0", - "svelte": "3.50.0", - "svelte-check": "2.9.0", + "prettier-plugin-svelte": "2.8.0", + "svelte": "3.52.0", + "svelte-check": "2.9.2", "svelte-preprocess": "4.10.7", - "tailwindcss": "3.1.8", + "tailwindcss": "3.2.1", "tailwindcss-scrollbar": "0.1.0", "tslib": "2.4.0", - "typescript": "4.8.2", - "vite": "3.1.0" + "typescript": "4.8.4", + "vite": "3.2.0" }, "type": "module", "dependencies": { - "@sveltejs/adapter-static": "1.0.0-next.39", - "@tailwindcss/typography": "^0.5.7", + "@sveltejs/adapter-static": "1.0.0-next.46", + "@tailwindcss/typography": "0.5.7", "cuid": "2.1.8", - "daisyui": "2.24.2", - "dayjs": "1.11.5", + "daisyui": "2.33.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/components/CopyPasswordField.svelte b/apps/ui/src/lib/components/CopyPasswordField.svelte index 9083fa47c..f45adcd3d 100644 --- a/apps/ui/src/lib/components/CopyPasswordField.svelte +++ b/apps/ui/src/lib/components/CopyPasswordField.svelte @@ -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/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}
-