From ea23fb658cef7968f3eae2360d92cc4a01f07784 Mon Sep 17 00:00:00 2001 From: Zakher Masri <46135573+zaaakher@users.noreply.github.com> Date: Wed, 8 Jul 2026 20:09:23 +0300 Subject: [PATCH 01/80] fix: meta.json of Poke blueprint The current github/docs links was broken before: https://codeberg.org/ashley/poke after: https://codeberg.org/ashleyirispuppy/poke --- blueprints/poke/meta.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/blueprints/poke/meta.json b/blueprints/poke/meta.json index bbb9bcba4..6766ff27a 100644 --- a/blueprints/poke/meta.json +++ b/blueprints/poke/meta.json @@ -5,9 +5,9 @@ "description": "Poke is an open-source, self-hosted alternative to YouTube. A privacy-focused video platform that allows you to watch and share videos without tracking.", "logo": "image.png", "links": { - "github": "https://codeberg.org/ashley/poke", + "github": "https://codeberg.org/ashleyirispuppy/poke", "website": "https://poketube.fun/", - "docs": "https://codeberg.org/ashley/poke" + "docs": "https://codeberg.org/ashleyirispuppy/poke" }, "tags": [ "video", From d7b9ac33dc09d994f471e316a0bd22654baf3cea Mon Sep 17 00:00:00 2001 From: zakher Date: Wed, 8 Jul 2026 21:04:58 +0300 Subject: [PATCH 02/80] fix: broken links in some blueprints --- blueprints/adventurelog/meta.json | 2 +- blueprints/affinepro/meta.json | 2 +- blueprints/anythingllm/meta.json | 4 ++-- blueprints/babybuddy/meta.json | 2 +- blueprints/bazarr/meta.json | 2 +- blueprints/bentopdf/meta.json | 4 ++-- blueprints/checkmate/meta.json | 2 +- blueprints/documenso/meta.json | 2 +- blueprints/maybe/meta.json | 2 +- blueprints/mumble/meta.json | 2 +- blueprints/paymenter/meta.json | 2 +- blueprints/rustrak/meta.json | 4 ++-- 12 files changed, 15 insertions(+), 15 deletions(-) diff --git a/blueprints/adventurelog/meta.json b/blueprints/adventurelog/meta.json index 83f1bc336..8c6109e66 100644 --- a/blueprints/adventurelog/meta.json +++ b/blueprints/adventurelog/meta.json @@ -7,7 +7,7 @@ "links": { "github": "https://github.com/seanmorley15/adventurelog", "website": "https://adventurelog.app/", - "docs": "https://adventurelog.app/docs/" + "docs": "https://adventurelog.app/docs/intro/adventurelog_overview.html" }, "tags": [ "activity", diff --git a/blueprints/affinepro/meta.json b/blueprints/affinepro/meta.json index 1a3da4a6f..7ec68577a 100644 --- a/blueprints/affinepro/meta.json +++ b/blueprints/affinepro/meta.json @@ -7,7 +7,7 @@ "links": { "github": "https://github.com/toeverything/Affine", "website": "https://affine.pro/", - "docs": "https://affine.pro/docs" + "docs": "https://docs.affine.pro/" }, "tags": [ "collaboration", diff --git a/blueprints/anythingllm/meta.json b/blueprints/anythingllm/meta.json index eff3e8c23..2a5258085 100644 --- a/blueprints/anythingllm/meta.json +++ b/blueprints/anythingllm/meta.json @@ -6,8 +6,8 @@ "logo": "logo.png", "links": { "github": "https://github.com/Mintplex-Labs/anything-llm", - "website": "https://useanything.com", - "docs": "https://github.com/Mintplex-Labs/anything-llm/tree/master/docs" + "website": "https://anythingllm.com/", + "docs": "https://docs.anythingllm.com/" }, "tags": [ "ai", diff --git a/blueprints/babybuddy/meta.json b/blueprints/babybuddy/meta.json index f564e4892..bbc59fc28 100644 --- a/blueprints/babybuddy/meta.json +++ b/blueprints/babybuddy/meta.json @@ -7,7 +7,7 @@ "links": { "github": "https://github.com/babybuddy/babybuddy", "website": "https://babybuddy.app", - "docs": "https://docs.babybuddy.app" + "docs": "https://docs.baby-buddy.net/" }, "tags": [ "parenting", diff --git a/blueprints/bazarr/meta.json b/blueprints/bazarr/meta.json index 473ed7d0a..e3c538b3a 100644 --- a/blueprints/bazarr/meta.json +++ b/blueprints/bazarr/meta.json @@ -7,7 +7,7 @@ "links": { "github": "https://github.com/morpheus65535/bazarr", "website": "https://www.bazarr.media/", - "docs": "https://www.bazarr.media/docs" + "docs": "https://wiki.bazarr.media/" }, "tags": [ "subtitles", diff --git a/blueprints/bentopdf/meta.json b/blueprints/bentopdf/meta.json index 2acc729b2..99ed44a2c 100644 --- a/blueprints/bentopdf/meta.json +++ b/blueprints/bentopdf/meta.json @@ -5,9 +5,9 @@ "description": "BentoPDF is a lightweight PDF conversion microservice that exposes a simple HTTP API for generating PDFs.", "logo": "image.png", "links": { - "github": "https://github.com/bentopdf/bentopdf", + "github": "https://github.com/alam00000/bentopdf/", "website": "https://bentopdf.com/", - "docs": "https://github.com/bentopdf/bentopdf#readme" + "docs": "https://www.bentopdf.com/docs/" }, "tags": [ "pdf", diff --git a/blueprints/checkmate/meta.json b/blueprints/checkmate/meta.json index b9f9d1555..5f4abc1b2 100644 --- a/blueprints/checkmate/meta.json +++ b/blueprints/checkmate/meta.json @@ -7,7 +7,7 @@ "links": { "github": "https://github.com/bluewave-labs/checkmate", "website": "https://checkmate.so/", - "docs": "https://docs.checkmate.so" + "docs": "https://checkmate.so/docs" }, "tags": [ "self-hosted", diff --git a/blueprints/documenso/meta.json b/blueprints/documenso/meta.json index 736eb2274..abf793f7d 100644 --- a/blueprints/documenso/meta.json +++ b/blueprints/documenso/meta.json @@ -6,7 +6,7 @@ "links": { "github": "https://github.com/documenso/documenso", "website": "https://documenso.com/", - "docs": "https://documenso.com/docs" + "docs": "https://docs.documenso.com/" }, "logo": "documenso.png", "tags": [ diff --git a/blueprints/maybe/meta.json b/blueprints/maybe/meta.json index a27e7f22f..1d7c54ae4 100644 --- a/blueprints/maybe/meta.json +++ b/blueprints/maybe/meta.json @@ -7,7 +7,7 @@ "links": { "github": "https://github.com/maybe-finance/maybe", "website": "https://maybe.finance/", - "docs": "https://docs.maybe.finance/" + "docs": "https://github.com/maybe-finance/maybe/blob/main/docs/hosting/docker.md" }, "tags": [ "finance", diff --git a/blueprints/mumble/meta.json b/blueprints/mumble/meta.json index 56783bb9b..05707dbf4 100644 --- a/blueprints/mumble/meta.json +++ b/blueprints/mumble/meta.json @@ -7,7 +7,7 @@ "links": { "github": "https://github.com/mumble-voip/mumble", "website": "https://www.mumble.info/", - "docs": "https://wiki.mumble.info/" + "docs": "https://www.mumble.info/documentation/" }, "tags": [ "voice-chat", diff --git a/blueprints/paymenter/meta.json b/blueprints/paymenter/meta.json index 2b1a3f198..6df437f11 100644 --- a/blueprints/paymenter/meta.json +++ b/blueprints/paymenter/meta.json @@ -7,7 +7,7 @@ "links": { "github": "https://github.com/Paymenter/Paymenter", "website": "https://paymenter.org/", - "docs": "https://paymenter.org/docs/" + "docs": "https://paymenter.org/docs/getting-started/introduction" }, "tags": [ "billing", diff --git a/blueprints/rustrak/meta.json b/blueprints/rustrak/meta.json index 9789fb2e4..b841c197a 100644 --- a/blueprints/rustrak/meta.json +++ b/blueprints/rustrak/meta.json @@ -6,8 +6,8 @@ "logo": "rustrak.svg", "links": { "github": "https://github.com/AbianS/rustrak", - "website": "https://abians.github.io/rustrak/", - "docs": "https://abians.github.io/rustrak/" + "website": "https://rustrak.github.io/rustrak/", + "docs": "https://rustrak.github.io/rustrak/getting-started/overview" }, "tags": [ "monitoring", From 6d56faf0ed0fcb7e8a77ecabd752dde90d9ebc4d Mon Sep 17 00:00:00 2001 From: Jinjinov Date: Mon, 13 Jul 2026 23:25:21 +0200 Subject: [PATCH 03/80] Add openhabittracker blueprint --- .../openhabittracker/docker-compose.yml | 18 +++++++++++++++++ blueprints/openhabittracker/meta.json | 19 ++++++++++++++++++ .../openhabittracker/openhabittracker.png | Bin 0 -> 36538 bytes blueprints/openhabittracker/template.toml | 16 +++++++++++++++ 4 files changed, 53 insertions(+) create mode 100644 blueprints/openhabittracker/docker-compose.yml create mode 100644 blueprints/openhabittracker/meta.json create mode 100755 blueprints/openhabittracker/openhabittracker.png create mode 100644 blueprints/openhabittracker/template.toml diff --git a/blueprints/openhabittracker/docker-compose.yml b/blueprints/openhabittracker/docker-compose.yml new file mode 100644 index 000000000..31cebec51 --- /dev/null +++ b/blueprints/openhabittracker/docker-compose.yml @@ -0,0 +1,18 @@ +version: "3" + +services: + openhabittracker: + image: jinjinov/openhabittracker:latest + ports: + - '8080' + environment: + AppSettings__UserName: ${OHT_USERNAME} + AppSettings__Email: ${OHT_EMAIL} + AppSettings__Password: ${OHT_PASSWORD} + AppSettings__JwtSecret: ${OHT_JWT_SECRET} + volumes: + - openhabittracker-data:/app/.OpenHabitTracker + restart: unless-stopped + +volumes: + openhabittracker-data: diff --git a/blueprints/openhabittracker/meta.json b/blueprints/openhabittracker/meta.json new file mode 100644 index 000000000..de635f7f6 --- /dev/null +++ b/blueprints/openhabittracker/meta.json @@ -0,0 +1,19 @@ +{ + "id": "openhabittracker", + "name": "OpenHabitTracker", + "version": "latest", + "description": "Take notes, plan tasks and track habits - free, ad-free, open source, with all data on your own server. Native apps for Windows, Linux, Android, iOS and macOS sync to it.", + "logo": "openhabittracker.png", + "links": { + "github": "https://github.com/Jinjinov/OpenHabitTracker", + "website": "https://openhabittracker.net", + "docs": "" + }, + "tags": [ + "productivity", + "notes", + "tasks", + "habits", + "self-hosted" + ] +} diff --git a/blueprints/openhabittracker/openhabittracker.png b/blueprints/openhabittracker/openhabittracker.png new file mode 100755 index 0000000000000000000000000000000000000000..92741bc1fd340f617ff0de2f4994445b2321735c GIT binary patch literal 36538 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 JP&u~!JIJ~c&k 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 S+>%7#A8mC5e2J>#t^Wt?-3n6x literal 0 HcmV?d00001 diff --git a/blueprints/openhabittracker/template.toml b/blueprints/openhabittracker/template.toml new file mode 100644 index 000000000..8aca131fa --- /dev/null +++ b/blueprints/openhabittracker/template.toml @@ -0,0 +1,16 @@ +[variables] +main_domain = "${domain}" +admin_password = "${password:24}" +jwt_secret = "${base64:48}" + +[config] +[[config.domains]] +serviceName = "openhabittracker" +port = 8_080 +host = "${main_domain}" + +[config.env] +OHT_USERNAME = "admin" +OHT_EMAIL = "admin@example.com" +OHT_PASSWORD = "${admin_password}" +OHT_JWT_SECRET = "${jwt_secret}" From 59770d4ae8b47eb0ea02278dfd2d475c7432091c Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 14 Jul 2026 10:55:42 -0600 Subject: [PATCH 04/80] fix(affinepro): update to current AFFiNE images The template pinned ghcr.io/toeverything/affine-graphql:stable-780dd83, an ephemeral per-commit tag on the old image name that has since been removed from GHCR, so the template could no longer pull and deploy. - Switch both app and migration services to ghcr.io/toeverything/affine:stable, the rolling stable tag recommended by the official self-host compose, so the template no longer rots when per-commit tags are garbage-collected - Move postgres to pgvector/pgvector:pg16 and set AFFINE_INDEXER_ENABLED=false, matching the current upstream self-host compose requirements - Add postgres/redis healthchecks and proper depends_on conditions so the self-host-predeploy migration job runs only once the database is ready, and the server starts only after migrations complete successfully Closes #777 Co-Authored-By: Claude Fable 5 --- blueprints/affinepro/docker-compose.yml | 37 +++++++++++++++++++------ blueprints/affinepro/meta.json | 2 +- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/blueprints/affinepro/docker-compose.yml b/blueprints/affinepro/docker-compose.yml index 93fa9da4f..e8a905d5d 100644 --- a/blueprints/affinepro/docker-compose.yml +++ b/blueprints/affinepro/docker-compose.yml @@ -1,7 +1,7 @@ version: "3.8" services: affinepro: - image: ghcr.io/toeverything/affine-graphql:stable-780dd83 + image: ghcr.io/toeverything/affine:stable restart: unless-stopped ports: - 3010 @@ -12,6 +12,7 @@ services: - REDIS_SERVER_HOST=redis - REDIS_SERVER_PASSWORD=${REDIS_PASSWORD} - DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@db:5432/affinepro + - AFFINE_INDEXER_ENABLED=false - AFFINE_SERVER_HOST=${DOMAIN} - MAILER_HOST=${MAILER_HOST} - MAILER_PORT=${MAILER_PORT} @@ -19,16 +20,21 @@ services: - MAILER_PASSWORD=${MAILER_PASSWORD} - MAILER_SENDER=${MAILER_SENDER} depends_on: - - db - - redis + db: + condition: service_healthy + redis: + condition: service_healthy + migration: + condition: service_completed_successfully migration: - image: ghcr.io/toeverything/affine-graphql:stable-780dd83 - command: node ./scripts/self-host-predeploy.js + image: ghcr.io/toeverything/affine:stable + command: ["sh", "-c", "node ./scripts/self-host-predeploy.js"] environment: - REDIS_SERVER_HOST=redis - REDIS_SERVER_PASSWORD=${REDIS_PASSWORD} - DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@db:5432/affinepro + - AFFINE_INDEXER_ENABLED=false - AFFINE_SERVER_HOST=${DOMAIN} - MAILER_HOST=${MAILER_HOST} - MAILER_PORT=${MAILER_PORT} @@ -39,17 +45,25 @@ services: - affine-storage:/root/.affine/storage - affine-config:/root/.affine/config depends_on: - - db - - redis + db: + condition: service_healthy + redis: + condition: service_healthy db: - image: postgres:15-alpine + image: pgvector/pgvector:pg16 restart: unless-stopped environment: - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} - POSTGRES_DB=affinepro + - POSTGRES_INITDB_ARGS=--data-checksums volumes: - postgres-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD", "pg_isready", "-U", "postgres", "-d", "affinepro"] + interval: 10s + timeout: 5s + retries: 5 redis: image: redis:7-alpine @@ -57,9 +71,14 @@ services: command: redis-server --requirepass ${REDIS_PASSWORD} volumes: - redis-data:/data + healthcheck: + test: ["CMD-SHELL", "redis-cli -a '${REDIS_PASSWORD}' ping | grep PONG"] + interval: 10s + timeout: 5s + retries: 5 volumes: affine-storage: {} affine-config: {} postgres-data: {} - redis-data: {} \ No newline at end of file + redis-data: {} diff --git a/blueprints/affinepro/meta.json b/blueprints/affinepro/meta.json index 7ec68577a..66e670547 100644 --- a/blueprints/affinepro/meta.json +++ b/blueprints/affinepro/meta.json @@ -1,7 +1,7 @@ { "id": "affinepro", "name": "Affine Pro", - "version": "stable-780dd83", + "version": "stable", "description": "Affine Pro is a modern, self-hosted platform designed for collaborative content creation and project management. It offers an intuitive interface, seamless real-time collaboration, and powerful tools for organizing tasks, notes, and ideas.", "logo": "logo.png", "links": { From 55aa82489d30e095981fba3a5ac6115527f80b38 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 14 Jul 2026 10:55:57 -0600 Subject: [PATCH 05/80] fix(cockpit): persist config and storage volumes per upstream docs The template mounted `html:/var/www/html` (freezing the app code from the first deploy) and `data:/var/www/html/storage/data`, which misses uploaded assets (stored under `storage/uploads`) and cache. Per the official Cockpit persistent-storage docs, mount `config:/var/www/html/config` and `storage:/var/www/html/storage` instead. Closes #549 Co-Authored-By: Claude Fable 5 --- blueprints/cockpit/docker-compose.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/blueprints/cockpit/docker-compose.yml b/blueprints/cockpit/docker-compose.yml index 3c8238978..fbc99a8e8 100644 --- a/blueprints/cockpit/docker-compose.yml +++ b/blueprints/cockpit/docker-compose.yml @@ -11,8 +11,8 @@ services: - COCKPIT_DATABASE_SERVER=mongodb://${MONGO_USERNAME}:${MONGO_PASSWORD}@mongo:27017 - COCKPIT_DATABASE_NAME=cockpit volumes: - - html:/var/www/html - - data:/var/www/html/storage/data + - config:/var/www/html/config + - storage:/var/www/html/storage depends_on: - mongo @@ -25,6 +25,6 @@ services: - mongo-data:/data/db volumes: - html: - data: + config: + storage: mongo-data: \ No newline at end of file From 744ff5277015bf62114140700c28681abcf3f41c Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 14 Jul 2026 10:56:19 -0600 Subject: [PATCH 06/80] fix(kener): define REDIS_URL and drop unused databases Kener v4 requires Redis for its BullMQ queues, caching and scheduler, so the app crashed on startup with "REDIS_URL is not defined in environment variables" right after printing that it was running (issue #976). The template also spun up both a postgres and a mariadb container while DATABASE_URL pointed at SQLite, so neither database was ever used (issue #282). Changes, aligned with the upstream docker-compose.yml (rajnandan1/kener): - Add a redis:7-alpine service with a healthcheck and persistent volume, wire REDIS_URL=redis://redis:6379 and make kener depend on redis being healthy - Remove the unused postgres and mysql services plus their env vars and generated passwords; keep SQLite (upstream default) persisted in the kener_db volume - Set ORIGIN to https:// instead of http://localhost:3000 so CSRF protection works behind the generated domain - Drop the stale /app/uploads bind mount (kener v4 only persists /app/database) and the obsolete compose version key Deploy-tested on a Dokploy instance: migrations and seed complete, schedulers start, "Kener version 4.1.2 is running!", HTTP 200 on the generated domain, both containers stable. Closes #976 Closes #282 Co-Authored-By: Claude Fable 5 --- blueprints/kener/docker-compose.yml | 34 +++++++++++++---------------- blueprints/kener/template.toml | 22 ++----------------- 2 files changed, 17 insertions(+), 39 deletions(-) diff --git a/blueprints/kener/docker-compose.yml b/blueprints/kener/docker-compose.yml index 01f9a689a..90dde8e20 100644 --- a/blueprints/kener/docker-compose.yml +++ b/blueprints/kener/docker-compose.yml @@ -1,39 +1,35 @@ -version: "3.8" - services: kener: image: rajnandan1/kener:latest environment: - TZ=${TZ} - KENER_SECRET_KEY=${KENER_SECRET_KEY} # 🔐 API key / secret + - ORIGIN=${ORIGIN} + - REDIS_URL=${REDIS_URL} - DATABASE_URL=${DATABASE_URL} - KENER_BASE_PATH=${KENER_BASE_PATH} - - ORIGIN=${ORIGIN} - RESEND_API_KEY=${RESEND_API_KEY} # 🔐 API key - RESEND_SENDER_EMAIL=${RESEND_SENDER_EMAIL} ports: - 3000 volumes: - kener_db:/app/database - - ../files/uploads:/app/uploads - restart: unless-stopped - - postgres: - image: postgres:alpine - environment: - - POSTGRES_USER=${POSTGRES_USER} - - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} # 🔐 DB password - - POSTGRES_DB=${POSTGRES_DB} + depends_on: + redis: + condition: service_healthy restart: unless-stopped - mysql: - image: mariadb:11 - environment: - - MYSQL_USER=${MYSQL_USER} - - MYSQL_PASSWORD=${MYSQL_PASSWORD} # 🔐 DB password - - MYSQL_DATABASE=${MYSQL_DATABASE} - - MYSQL_RANDOM_ROOT_PASSWORD=true + redis: + image: redis:7-alpine + volumes: + - kener_redis:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 restart: unless-stopped volumes: kener_db: {} + kener_redis: {} diff --git a/blueprints/kener/template.toml b/blueprints/kener/template.toml index 916f987e4..fba7cde4d 100644 --- a/blueprints/kener/template.toml +++ b/blueprints/kener/template.toml @@ -1,8 +1,6 @@ [variables] main_domain = "${domain}" KENER_SECRET_KEY = "${password:64}" -POSTGRES_PASSWORD = "${password:32}" -MYSQL_PASSWORD = "${password:32}" [config] [[config.domains]] @@ -13,25 +11,9 @@ host = "${main_domain}" [config.env] TZ = "Etc/UTC" KENER_SECRET_KEY = "${KENER_SECRET_KEY}" # 🔐 API key / secret +ORIGIN = "https://${main_domain}" +REDIS_URL = "redis://redis:6379" DATABASE_URL = "sqlite://./database/kener.sqlite.db" KENER_BASE_PATH = "" -ORIGIN = "http://localhost:3000" RESEND_API_KEY = "" RESEND_SENDER_EMAIL = "Accounts " -POSTGRES_USER = "user" -POSTGRES_DB = "kener_db" -MYSQL_USER = "user" -MYSQL_DATABASE = "kener_db" -MYSQL_RANDOM_ROOT_PASSWORD = "true" -MYSQL_PASSWORD = "${MYSQL_PASSWORD}" # 🔐 DB password -POSTGRES_PASSWORD = "${POSTGRES_PASSWORD}" # 🔐 DB password - -[[config.mounts]] -type = "volume" -source = "kener_db" -target = "/app/database" - -[[config.mounts]] -type = "bind" -source = "../files/uploads" -target = "/app/uploads" From c6a0b1b1b5d50c21c4b394d4f8d8ab941ae58a12 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 14 Jul 2026 10:58:33 -0600 Subject: [PATCH 07/80] fix(homarr): use named volume for /appdata and hex encryption key - Replace the relative bind mount `../homarr/appdata:/appdata` with a named volume `homarr_appdata:/appdata` so the data survives cleanups and can be backed up through Dokploy volume backups. - Generate SECRET_ENCRYPTION_KEY with `${hash:64}` instead of `${password:64}`: homarr requires a 64-character hex string (`openssl rand -hex 32`) and the password helper emits non-hex characters (g-z). Closes #738 Co-Authored-By: Claude Fable 5 --- blueprints/homarr/docker-compose.yml | 5 ++++- blueprints/homarr/template.toml | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/blueprints/homarr/docker-compose.yml b/blueprints/homarr/docker-compose.yml index 876ea3f6f..c43e62f3c 100644 --- a/blueprints/homarr/docker-compose.yml +++ b/blueprints/homarr/docker-compose.yml @@ -4,8 +4,11 @@ services: restart: unless-stopped volumes: # - /var/run/docker.sock:/var/run/docker.sock # Optional, only if you want docker integration - - ../homarr/appdata:/appdata + - homarr_appdata:/appdata environment: - SECRET_ENCRYPTION_KEY=${SECRET_ENCRYPTION_KEY} ports: - 7575 + +volumes: + homarr_appdata: diff --git a/blueprints/homarr/template.toml b/blueprints/homarr/template.toml index d73be9477..6cdf00cd8 100644 --- a/blueprints/homarr/template.toml +++ b/blueprints/homarr/template.toml @@ -1,6 +1,6 @@ [variables] main_domain = "${domain}" -secret_key = "${password:64}" +secret_key = "${hash:64}" [config] env = ["SECRET_ENCRYPTION_KEY=${secret_key}"] From 7ed8450cf8fc52b5af1dcf5f44499da134e9c202 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 14 Jul 2026 11:04:17 -0600 Subject: [PATCH 08/80] fix(logto): fix postgres role error and update to 1.41.0 The postgres healthcheck ran pg_isready without -U while POSTGRES_USER was set to logto, so every probe attempted to connect as role "postgres" and spammed FATAL: role "postgres" does not exist. Pass -U logto -d logto explicitly and set POSTGRES_DB. Also fix LOGTO_ENDPOINT pointing at the admin domain instead of the main domain, update Logto 1.27.0 -> 1.41.0, use ${password:32} for the generated password, drop the ephemeral host port publishing and add restart policies. Closes #130 Co-Authored-By: Claude Fable 5 --- blueprints/logto/docker-compose.yml | 14 +++++--------- blueprints/logto/meta.json | 2 +- blueprints/logto/template.toml | 4 ++-- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/blueprints/logto/docker-compose.yml b/blueprints/logto/docker-compose.yml index 04d7464f4..2e05a7b81 100644 --- a/blueprints/logto/docker-compose.yml +++ b/blueprints/logto/docker-compose.yml @@ -1,14 +1,11 @@ services: app: + image: ghcr.io/logto-io/logto:1.41.0 + restart: unless-stopped depends_on: postgres: condition: service_healthy - image: ghcr.io/logto-io/logto:1.27.0 entrypoint: ["sh", "-c", "npm run cli db seed -- --swe && npm start"] - ports: - - 3001 - - 3002 - environment: TRUST_PROXY_HEADER: 1 DB_URL: postgres://logto:${LOGTO_POSTGRES_PASSWORD}@postgres:5432/logto @@ -18,20 +15,19 @@ services: - logto-connectors:/etc/logto/packages/core/connectors postgres: image: postgres:17-alpine - user: postgres - + restart: unless-stopped environment: POSTGRES_USER: logto POSTGRES_PASSWORD: ${LOGTO_POSTGRES_PASSWORD} + POSTGRES_DB: logto volumes: - postgres-data:/var/lib/postgresql/data healthcheck: - test: ["CMD-SHELL", "pg_isready"] + test: ["CMD-SHELL", "pg_isready -U logto -d logto"] interval: 10s timeout: 5s retries: 5 - volumes: logto-connectors: postgres-data: diff --git a/blueprints/logto/meta.json b/blueprints/logto/meta.json index 03bd1ded7..60e7aedca 100644 --- a/blueprints/logto/meta.json +++ b/blueprints/logto/meta.json @@ -1,7 +1,7 @@ { "id": "logto", "name": "Logto", - "version": "1.27.0", + "version": "1.41.0", "description": "Logto is an open-source Identity and Access Management (IAM) platform designed to streamline Customer Identity and Access Management (CIAM) and Workforce Identity Management.", "logo": "logto.png", "links": { diff --git a/blueprints/logto/template.toml b/blueprints/logto/template.toml index 896a5d33c..e8e420e60 100644 --- a/blueprints/logto/template.toml +++ b/blueprints/logto/template.toml @@ -1,7 +1,7 @@ [variables] main_domain = "${domain}" admin_domain = "${domain}" -postgres_password = "${password}" +postgres_password = "${password:32}" [config] mounts = [] @@ -17,6 +17,6 @@ port = 3_002 host = "${admin_domain}" [config.env] -LOGTO_ENDPOINT = "http://${admin_domain}" +LOGTO_ENDPOINT = "http://${main_domain}" LOGTO_ADMIN_ENDPOINT = "http://${admin_domain}" LOGTO_POSTGRES_PASSWORD = "${postgres_password}" From 8f84a528553fdf9da6d7cb5c37ab5d3f3df65283 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 14 Jul 2026 11:11:25 -0600 Subject: [PATCH 09/80] fix(gitlab-ce): serve on the generated domain (remove removed unicorn config, pin omnibus) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The template set unicorn['worker_processes'] / unicorn['worker_timeout'] in GITLAB_OMNIBUS_CONFIG. Unicorn was removed in GitLab 14.0, so on current gitlab-ce images `gitlab-ctl reconfigure` aborts with "Removed configurations found in gitlab.rb" and GitLab never starts serving — the generated domain returned 502 Bad Gateway (issue #380). Rewrite the template as the official all-in-one Omnibus setup: - pin gitlab/gitlab-ce:19.1.2-ce.0 instead of :latest - external_url http:// with nginx listen_port 80 / listen_https false (TLS terminates at Traefik) - set gitlab_rails['initial_root_password'] from a generated template password - puma single mode + prometheus monitoring off to keep memory reasonable - drop the external postgres/redis containers (Omnibus bundles and manages its own) and the broken ports entries ("2224" published a random host port while sshd listens on 22) Verified on a live Dokploy instance: domain serves the GitLab sign-in page and root login with the generated password works. Co-Authored-By: Claude Fable 5 --- blueprints/gitlab-ce/docker-compose.yml | 53 +++---------------------- blueprints/gitlab-ce/meta.json | 2 +- blueprints/gitlab-ce/template.toml | 8 +--- 3 files changed, 9 insertions(+), 54 deletions(-) diff --git a/blueprints/gitlab-ce/docker-compose.yml b/blueprints/gitlab-ce/docker-compose.yml index 3100a1487..8e6c07347 100644 --- a/blueprints/gitlab-ce/docker-compose.yml +++ b/blueprints/gitlab-ce/docker-compose.yml @@ -1,65 +1,24 @@ services: gitlab: - image: gitlab/gitlab-ce:latest + image: gitlab/gitlab-ce:19.1.2-ce.0 restart: unless-stopped - hostname: gitlab.example.com + shm_size: "256m" environment: GITLAB_OMNIBUS_CONFIG: | external_url 'http://${GITLAB_HOST}' - gitlab_rails['gitlab_ssh_host'] = '${GITLAB_HOST}' - gitlab_rails['gitlab_shell_ssh_port'] = 2224 - gitlab_rails['db_adapter'] = 'postgresql' - gitlab_rails['db_host'] = 'postgresql' - gitlab_rails['db_port'] = '5432' - gitlab_rails['db_database'] = '${POSTGRES_DB}' - gitlab_rails['db_username'] = '${POSTGRES_USER}' - gitlab_rails['db_password'] = '${POSTGRES_PASSWORD}' - # Redis config for external TCP connection - gitlab_rails['redis_url'] = 'redis://redis:6379/0' - gitlab_rails['redis_host'] = 'redis' - gitlab_rails['redis_port'] = 6379 - gitlab_rails['redis_socket'] = nil - gitlab_rails['gitlab_email_enabled'] = false - gitlab_rails['gitlab_default_can_create_group'] = true - gitlab_rails['gitlab_username_changing_enabled'] = false - unicorn['worker_processes'] = 2 - unicorn['worker_timeout'] = 60 - postgresql['enable'] = false - redis['enable'] = false - nginx['enable'] = true nginx['listen_port'] = 80 nginx['listen_https'] = false + gitlab_rails['initial_root_password'] = '${GITLAB_ROOT_PASSWORD}' + gitlab_rails['gitlab_email_enabled'] = false + puma['worker_processes'] = 0 + sidekiq['concurrency'] = 10 prometheus_monitoring['enable'] = false - ports: - - "80" - - "2224" volumes: - gitlab_config:/etc/gitlab - gitlab_logs:/var/log/gitlab - gitlab_data:/var/opt/gitlab - depends_on: - - postgresql - - redis - - postgresql: - image: postgres:16-alpine - restart: unless-stopped - environment: - POSTGRES_DB: ${POSTGRES_DB} - POSTGRES_USER: ${POSTGRES_USER} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} - volumes: - - postgresql_data:/var/lib/postgresql/data - - redis: - image: redis:7-alpine - restart: unless-stopped - volumes: - - redis_data:/data volumes: gitlab_config: gitlab_logs: gitlab_data: - postgresql_data: - redis_data: diff --git a/blueprints/gitlab-ce/meta.json b/blueprints/gitlab-ce/meta.json index 8092fbd99..4ca3e9599 100644 --- a/blueprints/gitlab-ce/meta.json +++ b/blueprints/gitlab-ce/meta.json @@ -1,7 +1,7 @@ { "id": "gitlab-ce", "name": "GitLab CE", - "version": "latest", + "version": "19.1.2", "description": "GitLab Community Edition is a free and open source platform for managing Git repositories, CI/CD pipelines, and project management.", "logo": "gitlab-ce.svg", "links": { diff --git a/blueprints/gitlab-ce/template.toml b/blueprints/gitlab-ce/template.toml index 1bc24e874..d531d80d4 100644 --- a/blueprints/gitlab-ce/template.toml +++ b/blueprints/gitlab-ce/template.toml @@ -1,15 +1,11 @@ [variables] main_domain = "${domain}" -postgres_db = "gitlab" -postgres_user = "gitlab" -postgres_password = "${password:32}" +gitlab_root_password = "${password:32}" [config] env = [ "GITLAB_HOST=${main_domain}", - "POSTGRES_DB=${postgres_db}", - "POSTGRES_USER=${postgres_user}", - "POSTGRES_PASSWORD=${postgres_password}", + "GITLAB_ROOT_PASSWORD=${gitlab_root_password}", ] [[config.domains]] From 6786237720611c0342a8e5dd2773c2d762f4f5e2 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 14 Jul 2026 11:12:30 -0600 Subject: [PATCH 10/80] fix(zitadel): wire EXTERNALDOMAIN/PORT/SECURE to the generated domain Zitadel resolves its instance from the external domain/port/scheme, so the template's ZITADEL_EXTERNALPORT=8080 made every generated URL (OIDC issuer, login redirects) point at http://:8080 instead of the domain served by Traefik, ending in {"code":5,"message":"Not Found"} (#516). - Expose ZITADEL_EXTERNALDOMAIN / ZITADEL_EXTERNALPORT / ZITADEL_EXTERNALSECURE as template env vars, defaulting to the generated domain over HTTP (port 80, secure=false) so the routed domain and Zitadel's external config always match; HTTPS users flip them to 443/true in the Env tab. - Pin image to v4.16.0 (was latest) and keep the built-in login v1 via ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_REQUIRED=false so no separate login-v2 container is needed. - Add the official readiness healthcheck, drop the unused /app/data volume, bogus SMTP env keys, published port and obsolete compose version key. Closes #516 Co-Authored-By: Claude Fable 5 --- blueprints/zitadel/docker-compose.yml | 37 ++++++++++++++------------- blueprints/zitadel/meta.json | 2 +- blueprints/zitadel/template.toml | 14 +++++++--- 3 files changed, 31 insertions(+), 22 deletions(-) diff --git a/blueprints/zitadel/docker-compose.yml b/blueprints/zitadel/docker-compose.yml index aaa457c35..ba7322e20 100644 --- a/blueprints/zitadel/docker-compose.yml +++ b/blueprints/zitadel/docker-compose.yml @@ -1,9 +1,7 @@ -version: '3.8' - services: zitadel: restart: 'always' - image: 'ghcr.io/zitadel/zitadel:latest' + image: 'ghcr.io/zitadel/zitadel:v4.16.0' command: 'start-from-init --masterkey "${ZITADEL_MASTERKEY}" --tlsMode disabled' environment: # Database Configuration @@ -17,33 +15,37 @@ services: ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD: "${POSTGRES_PASSWORD}" ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE: disable - # External Configuration for HTTP only - TLS mode disabled - ZITADEL_EXTERNALSECURE: false - ZITADEL_EXTERNALPORT: 8080 - ZITADEL_EXTERNALDOMAIN: "${EXTERNAL_DOMAIN}" + # External access configuration. These MUST match the domain configured + # in the Domains tab and the scheme/port it is served on, otherwise + # Zitadel answers {"code":5,"message":"Not Found"}. + # HTTP -> ZITADEL_EXTERNALPORT=80, ZITADEL_EXTERNALSECURE=false + # HTTPS -> ZITADEL_EXTERNALPORT=443, ZITADEL_EXTERNALSECURE=true + ZITADEL_EXTERNALDOMAIN: "${ZITADEL_EXTERNALDOMAIN}" + ZITADEL_EXTERNALPORT: "${ZITADEL_EXTERNALPORT}" + ZITADEL_EXTERNALSECURE: "${ZITADEL_EXTERNALSECURE}" ZITADEL_TLS_ENABLED: false - # Disable Email Notifications - ZITADEL_NOTIFICATIONS_SMTP_HOST: "" - ZITADEL_NOTIFICATIONS_SMTP_PORT: "" - # Custom Admin User Configuration ZITADEL_FIRSTINSTANCE_ORG_HUMAN_USERNAME: "${ZITADEL_FIRSTINSTANCE_ORG_HUMAN_USERNAME}" ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORD: "${ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORD}" ZITADEL_FIRSTINSTANCE_ORG_HUMAN_EMAIL_ADDRESS: "${ZITADEL_FIRSTINSTANCE_ORG_HUMAN_EMAIL_ADDRESS}" ZITADEL_FIRSTINSTANCE_ORG_HUMAN_FIRSTNAME: "${ZITADEL_FIRSTINSTANCE_ORG_HUMAN_FIRSTNAME}" ZITADEL_FIRSTINSTANCE_ORG_HUMAN_LASTNAME: "${ZITADEL_FIRSTINSTANCE_ORG_HUMAN_LASTNAME}" - - # Default Instance Features + + # Use the login UI built into the API container (no extra login-v2 service needed) ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_REQUIRED: false + healthcheck: + test: ["CMD", "/app/zitadel", "ready"] + interval: '10s' + timeout: '30s' + retries: 12 + start_period: '30s' depends_on: db: condition: 'service_healthy' - ports: - - '8080' - volumes: - - zitadel_data:/app/data + expose: + - 8080 db: restart: 'always' @@ -63,4 +65,3 @@ services: volumes: postgres_data: - zitadel_data: \ No newline at end of file diff --git a/blueprints/zitadel/meta.json b/blueprints/zitadel/meta.json index 05917241e..ac66c4b50 100644 --- a/blueprints/zitadel/meta.json +++ b/blueprints/zitadel/meta.json @@ -1,7 +1,7 @@ { "id": "zitadel", "name": "Zitadel", - "version": "latest", + "version": "4.16.0", "description": "Open-source identity and access management platform with multi-tenancy, OpenID Connect, SAML, and OAuth 2.0 support.", "logo": "zitadel.png", "links": { diff --git a/blueprints/zitadel/template.toml b/blueprints/zitadel/template.toml index f04da0be3..44c177e57 100644 --- a/blueprints/zitadel/template.toml +++ b/blueprints/zitadel/template.toml @@ -7,6 +7,8 @@ admin_email = "${email}" admin_password = "AdminPassword123!" [config] +mounts = [] + [[config.domains]] serviceName = "zitadel" port = 8080 @@ -16,7 +18,15 @@ path = "/" [config.env] POSTGRES_PASSWORD = "${postgres_password}" ZITADEL_MASTERKEY = "${zitadel_masterkey}" -EXTERNAL_DOMAIN = "${main_domain}" + +# ZITADEL_EXTERNALDOMAIN must always match the domain configured in the +# Domains tab. If you change the domain, update this variable too (and do it +# BEFORE the first deploy — Zitadel binds its instance to this domain on +# first start). For a domain served over HTTPS set ZITADEL_EXTERNALPORT=443 +# and ZITADEL_EXTERNALSECURE=true. +ZITADEL_EXTERNALDOMAIN = "${main_domain}" +ZITADEL_EXTERNALPORT = "80" +ZITADEL_EXTERNALSECURE = "false" # Custom Admin User Configuration ZITADEL_FIRSTINSTANCE_ORG_HUMAN_USERNAME = "${admin_username}" @@ -24,5 +34,3 @@ ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORD = "${admin_password}" ZITADEL_FIRSTINSTANCE_ORG_HUMAN_EMAIL_ADDRESS = "${admin_email}" ZITADEL_FIRSTINSTANCE_ORG_HUMAN_FIRSTNAME = "Admin" ZITADEL_FIRSTINSTANCE_ORG_HUMAN_LASTNAME = "User" - -[[config.mounts]] From 31158e234045d1927449c4a6ae9d3b2308e9bdf6 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 14 Jul 2026 11:19:38 -0600 Subject: [PATCH 11/80] fix(immich): update to v3.0.2 Co-Authored-By: Claude Fable 5 --- blueprints/immich/docker-compose.yml | 50 ++++++---------------------- blueprints/immich/meta.json | 2 +- blueprints/immich/template.toml | 3 -- 3 files changed, 12 insertions(+), 43 deletions(-) diff --git a/blueprints/immich/docker-compose.yml b/blueprints/immich/docker-compose.yml index f385b329b..822117ca8 100644 --- a/blueprints/immich/docker-compose.yml +++ b/blueprints/immich/docker-compose.yml @@ -2,10 +2,10 @@ version: "3.9" services: immich-server: - image: ghcr.io/immich-app/immich-server:v2.1.0 + image: ghcr.io/immich-app/immich-server:v3.0.2 volumes: - - immich-library:/usr/src/app/upload + - immich-library:/data - /etc/localtime:/etc/localtime:ro depends_on: immich-redis: @@ -13,9 +13,6 @@ services: immich-database: condition: service_healthy environment: - PORT: 2283 - SERVER_URL: ${SERVER_URL} - FRONT_BASE_URL: ${FRONT_BASE_URL} # Database Configuration DB_HOSTNAME: ${DB_HOSTNAME} DB_PORT: ${DB_PORT} @@ -30,70 +27,45 @@ services: TZ: ${TZ} restart: unless-stopped healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:2283/server-info/ping"] - interval: 30s - timeout: 10s - retries: 3 + disable: false immich-machine-learning: - image: ghcr.io/immich-app/immich-machine-learning:v2.1.0 + image: ghcr.io/immich-app/immich-machine-learning:v3.0.2 volumes: - immich-model-cache:/cache - environment: - REDIS_HOSTNAME: ${REDIS_HOSTNAME} - REDIS_PORT: ${REDIS_PORT} - REDIS_DBINDEX: ${REDIS_DBINDEX} restart: unless-stopped healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:3003/ping"] - interval: 30s - timeout: 10s - retries: 3 + disable: false immich-redis: - image: redis:6.2-alpine + image: valkey/valkey:9 volumes: - immich-redis-data:/data healthcheck: - test: ["CMD", "redis-cli", "ping"] + test: redis-cli ping || exit 1 interval: 10s timeout: 5s retries: 5 restart: unless-stopped immich-database: - image: tensorchord/pgvecto-rs:pg14-v0.3.0 + image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0 volumes: - immich-postgres:/var/lib/postgresql/data environment: POSTGRES_PASSWORD: ${DB_PASSWORD} POSTGRES_USER: ${DB_USERNAME} - POSTGRES_DB: immich + POSTGRES_DB: ${DB_DATABASE_NAME} POSTGRES_INITDB_ARGS: '--data-checksums' + shm_size: 128mb healthcheck: - test: pg_isready -U ${DB_USERNAME} -d immich || exit 1 + test: pg_isready -U ${DB_USERNAME} -d ${DB_DATABASE_NAME} || exit 1 interval: 10s timeout: 5s retries: 5 - command: - [ - 'postgres', - '-c', - 'shared_preload_libraries=vectors.so', - '-c', - 'search_path="$$user", public, vectors', - '-c', - 'logging_collector=on', - '-c', - 'max_wal_size=2GB', - '-c', - 'shared_buffers=512MB', - '-c', - 'wal_compression=on', - ] restart: unless-stopped volumes: diff --git a/blueprints/immich/meta.json b/blueprints/immich/meta.json index bc3ecb6bd..cf1aa6344 100644 --- a/blueprints/immich/meta.json +++ b/blueprints/immich/meta.json @@ -1,7 +1,7 @@ { "id": "immich", "name": "Immich", - "version": "v1.121.0", + "version": "v3.0.2", "description": "High performance self-hosted photo and video backup solution directly from your mobile phone.", "logo": "immich.svg", "links": { diff --git a/blueprints/immich/template.toml b/blueprints/immich/template.toml index f2b910e72..a18ac2bb1 100644 --- a/blueprints/immich/template.toml +++ b/blueprints/immich/template.toml @@ -5,9 +5,6 @@ db_user = "immich" [config] env = [ - "IMMICH_HOST=${main_domain}", - "SERVER_URL=https://${main_domain}", - "FRONT_BASE_URL=https://${main_domain}", "DB_HOSTNAME=immich-database", "DB_PORT=5432", "DB_USERNAME=${db_user}", From 3c416eaba378c6574d336fe2b2b4b16a3ac56a0f Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 14 Jul 2026 11:28:22 -0600 Subject: [PATCH 12/80] fix(automatisch): switch to official image and wire up DOMAIN to fix Bad Gateway - Replace unofficial dockeriddonuts/automatisch:2.0 mirror (stale since Nov 2024) with the official automatischio/automatisch:0.15.0 image - Define DOMAIN in template.toml [config.env]: the compose referenced ${DOMAIN} but it was never provided, so HOST resolved empty and the app generated localhost URLs for the web app and webhooks - Set API_URL/WEB_APP_URL so generated URLs don't carry the internal :3000 port behind Traefik - Add postgres healthcheck and gate the app on service_healthy to avoid the boot-time migration race; worker now starts after main (official compose order) - Drop unnecessary published port and no-op REDIS_* env on the redis service - Bump meta.json version to 0.15.0 Closes #97 Co-Authored-By: Claude Fable 5 --- blueprints/automatisch/docker-compose.yml | 29 ++++++++++++----------- blueprints/automatisch/meta.json | 2 +- blueprints/automatisch/template.toml | 1 + 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/blueprints/automatisch/docker-compose.yml b/blueprints/automatisch/docker-compose.yml index 42f585c82..550c771d1 100644 --- a/blueprints/automatisch/docker-compose.yml +++ b/blueprints/automatisch/docker-compose.yml @@ -1,17 +1,15 @@ -version: "3.8" services: automatisch: - image: dockeriddonuts/automatisch:2.0 + image: automatischio/automatisch:0.15.0 restart: unless-stopped - ports: - - 3000 environment: - HOST=${DOMAIN} - PROTOCOL=http - PORT=3000 + - API_URL=http://${DOMAIN} + - WEB_APP_URL=http://${DOMAIN} - APP_ENV=production - REDIS_HOST=automatisch-redis - - REDIS_USERNAME=default - REDIS_PASSWORD=${REDIS_PASSWORD} - POSTGRES_HOST=automatisch-postgres - POSTGRES_DATABASE=automatisch @@ -23,11 +21,13 @@ services: volumes: - storage:/automatisch/storage depends_on: - - automatisch-postgres - - automatisch-redis + automatisch-postgres: + condition: service_healthy + automatisch-redis: + condition: service_started automatisch-worker: - image: dockeriddonuts/automatisch:2.0 + image: automatischio/automatisch:0.15.0 restart: unless-stopped environment: - APP_ENV=production @@ -44,8 +44,7 @@ services: volumes: - storage:/automatisch/storage depends_on: - - automatisch-postgres - - automatisch-redis + - automatisch automatisch-postgres: image: postgres:15-alpine @@ -56,18 +55,20 @@ services: - POSTGRES_DB=automatisch volumes: - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres -d automatisch"] + interval: 10s + timeout: 5s + retries: 5 automatisch-redis: image: redis:7-alpine restart: unless-stopped command: redis-server --requirepass ${REDIS_PASSWORD} - environment: - - REDIS_USERNAME=default - - REDIS_PASSWORD=${REDIS_PASSWORD} volumes: - redis_data:/data volumes: storage: {} postgres_data: {} - redis_data: {} \ No newline at end of file + redis_data: {} diff --git a/blueprints/automatisch/meta.json b/blueprints/automatisch/meta.json index 9acf2c66e..7750995f7 100644 --- a/blueprints/automatisch/meta.json +++ b/blueprints/automatisch/meta.json @@ -1,7 +1,7 @@ { "id": "automatisch", "name": "Automatisch", - "version": "2.0", + "version": "0.15.0", "description": "Automatisch is a powerful, self-hosted workflow automation tool designed for connecting your apps and automating repetitive tasks. With Automatisch, you can create workflows to sync data, send notifications, and perform various actions seamlessly across different services.", "logo": "logo.png", "links": { diff --git a/blueprints/automatisch/template.toml b/blueprints/automatisch/template.toml index 2e78ef006..a718ea0e5 100644 --- a/blueprints/automatisch/template.toml +++ b/blueprints/automatisch/template.toml @@ -13,6 +13,7 @@ port = 3000 host = "${main_domain}" [config.env] +DOMAIN = "${main_domain}" DB_PASSWORD = "${db_password}" REDIS_PASSWORD = "${redis_password}" ENCRYPTION_KEY = "${encryption_key}" From 1e486813b4d99448b7f033632284366579ec9ef4 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 14 Jul 2026 11:30:07 -0600 Subject: [PATCH 13/80] fix(signoz): fix ClickHouse hostname resolution and IPv6 binding - Override the ClickHouse image's config.d/docker_related_config.xml, which listens on the IPv6 wildcard address ([::]) first and fails to bind on hosts without IPv6 support; listen on 0.0.0.0 (IPv4) only and disable the internal DNS cache so transient startup DNS failures for zookeeper-1/clickhouse are not cached (issue #534). - Set hostname: clickhouse (instead of container_name) so the server identifies itself as the cluster host declared in cluster.xml. - Remove the explicit signoz-net network: Dokploy attaches its own networks automatically and the repo validator rejects explicit networks; a fixed-name shared network also breaks service DNS across deployments. - Fix mount mismatch: template.toml created files/signoz/prometheus.xml while the compose mounts ../files/signoz/prometheus.yml, so Docker silently mounted a directory as the signoz prometheus config. - Give the otel-collector domain its own generated host: both domains previously shared the same host and path "/", creating two conflicting Traefik routers where one service randomly won. Closes #534 Co-Authored-By: Claude Fable 5 --- blueprints/signoz/docker-compose.yml | 8 ++------ blueprints/signoz/template.toml | 21 +++++++++++++++++++-- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/blueprints/signoz/docker-compose.yml b/blueprints/signoz/docker-compose.yml index aaa6921a4..67ef17710 100644 --- a/blueprints/signoz/docker-compose.yml +++ b/blueprints/signoz/docker-compose.yml @@ -1,6 +1,4 @@ x-common: &common - networks: - - signoz-net restart: unless-stopped logging: options: @@ -89,11 +87,12 @@ services: - ZOO_PROMETHEUS_METRICS_PORT_NUMBER=9141 clickhouse: !!merge <<: *clickhouse-defaults - container_name: signoz-clickhouse + hostname: clickhouse volumes: - ../files/clickhouse/config.xml:/etc/clickhouse-server/config.xml - ../files/clickhouse/user_scripts:/var/lib/clickhouse/user_scripts/ - ../files/clickhouse/cluster.xml:/etc/clickhouse-server/config.d/cluster.xml + - ../files/clickhouse/docker_related_config.xml:/etc/clickhouse-server/config.d/docker_related_config.xml - clickhouse:/var/lib/clickhouse/ signoz: !!merge <<: *db-depend @@ -156,9 +155,6 @@ services: - --dsn=tcp://clickhouse:9000 - --up= restart: on-failure -networks: - signoz-net: - name: signoz-net volumes: clickhouse: name: signoz-clickhouse diff --git a/blueprints/signoz/template.toml b/blueprints/signoz/template.toml index 492a613ad..98839a942 100644 --- a/blueprints/signoz/template.toml +++ b/blueprints/signoz/template.toml @@ -1,5 +1,6 @@ [variables] main_domain = "${domain}" +otel_domain = "${domain}" jwt_secret = "${password:64}" [config] @@ -12,7 +13,7 @@ path = "/" [[config.domains]] serviceName = "otel-collector" port = 4318 -host = "${main_domain}" +host = "${otel_domain}" path = "/" [config.env] @@ -1246,7 +1247,23 @@ content = """ """ [[config.mounts]] -filePath = "/signoz/prometheus.xml" +filePath = "/clickhouse/docker_related_config.xml" +content = """ + + + + 0.0.0.0 + 1 + + 1 + +""" + +[[config.mounts]] +filePath = "/signoz/prometheus.yml" content = """ # my global config global: From 29a7970397313724c0f6414e2a75592ad4f76775 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 14 Jul 2026 11:30:46 -0600 Subject: [PATCH 14/80] fix(infisical): update to v0.162.6 - Bump image from v0.135.0-postgres to v0.162.6 (upstream dropped the -postgres tag suffix after v0.146.0; plain tags are the postgres build) - Remove the db-migration service: the standalone image runs database migrations automatically on boot (auto-start-migrations), matching the official docker-compose.prod.yml which no longer ships that service - Generate ENCRYPTION_KEY (${hash:32}) and AUTH_SECRET (${base64:32}) per deployment instead of shipping hardcoded example secrets - Fix SITE_URL to http://${main_domain} (no :8080; the domain is proxied) - Refresh SMTP env names (SMTP_FROM_ADDRESS/SMTP_FROM_NAME; SMTP_SECURE was removed upstream) and drop unused CLIENT_* env placeholders that were never passed to the container Closes #215 Co-Authored-By: Claude Fable 5 --- blueprints/infisical/docker-compose.yml | 59 ++++++------------------- blueprints/infisical/meta.json | 2 +- blueprints/infisical/template.toml | 47 ++++---------------- 3 files changed, 23 insertions(+), 85 deletions(-) diff --git a/blueprints/infisical/docker-compose.yml b/blueprints/infisical/docker-compose.yml index ac5718bcb..d28dd6868 100644 --- a/blueprints/infisical/docker-compose.yml +++ b/blueprints/infisical/docker-compose.yml @@ -1,58 +1,29 @@ services: - db-migration: - depends_on: - db: - condition: service_healthy - image: infisical/infisical:v0.135.0-postgres - environment: - - NODE_ENV=production - - ENCRYPTION_KEY - - AUTH_SECRET - - SITE_URL - - DB_CONNECTION_URI=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB} - - REDIS_URL=redis://redis:6379 - - SMTP_HOST - - SMTP_PORT - - SMTP_FROM_NAME - - SMTP_USERNAME - - SMTP_PASSWORD - - SMTP_SECURE=true - command: npm run migration:latest - pull_policy: always - backend: + image: infisical/infisical:v0.162.6 restart: unless-stopped depends_on: db: condition: service_healthy redis: condition: service_started - db-migration: - condition: service_completed_successfully - image: infisical/infisical:v0.135.0-postgres - pull_policy: always environment: - NODE_ENV=production - - ENCRYPTION_KEY - - AUTH_SECRET - - SITE_URL + - ENCRYPTION_KEY=${ENCRYPTION_KEY} + - AUTH_SECRET=${AUTH_SECRET} + - SITE_URL=${SITE_URL} - DB_CONNECTION_URI=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB} - REDIS_URL=redis://redis:6379 - - SMTP_HOST - - SMTP_PORT - - SMTP_FROM_NAME - - SMTP_USERNAME - - SMTP_PASSWORD - - SMTP_SECURE=true - + - SMTP_HOST=${SMTP_HOST} + - SMTP_PORT=${SMTP_PORT} + - SMTP_FROM_ADDRESS=${SMTP_FROM_ADDRESS} + - SMTP_FROM_NAME=${SMTP_FROM_NAME} + - SMTP_USERNAME=${SMTP_USERNAME} + - SMTP_PASSWORD=${SMTP_PASSWORD} redis: image: redis:7.4.1 - env_file: .env restart: always - environment: - - ALLOW_EMPTY_PASSWORD=yes - volumes: - redis_infisical_data:/data @@ -60,12 +31,11 @@ services: image: postgres:14-alpine restart: always environment: - - POSTGRES_PASSWORD - - POSTGRES_USER - - POSTGRES_DB + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - POSTGRES_USER=${POSTGRES_USER} + - POSTGRES_DB=${POSTGRES_DB} volumes: - pg_infisical_data:/var/lib/postgresql/data - healthcheck: test: "pg_isready --username=${POSTGRES_USER} && psql --username=${POSTGRES_USER} --list" interval: 5s @@ -75,6 +45,3 @@ services: volumes: pg_infisical_data: redis_infisical_data: - - - diff --git a/blueprints/infisical/meta.json b/blueprints/infisical/meta.json index b086baced..5f41cb137 100644 --- a/blueprints/infisical/meta.json +++ b/blueprints/infisical/meta.json @@ -1,7 +1,7 @@ { "id": "infisical", "name": "Infisical", - "version": "0.135.0", + "version": "0.162.6", "description": "All-in-one platform to securely manage application configuration and secrets across your team and infrastructure.", "logo": "infisical.jpg", "links": { diff --git a/blueprints/infisical/template.toml b/blueprints/infisical/template.toml index a96bfb6ca..4ea6af674 100644 --- a/blueprints/infisical/template.toml +++ b/blueprints/infisical/template.toml @@ -3,56 +3,27 @@ main_domain = "${domain}" postgres_password = "${password}" postgres_user = "infisical" postgres_db = "infisical" +encryption_key = "${hash:32}" +auth_secret = "${base64:32}" [config] env = [ - "ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218", - "AUTH_SECRET=5lrMXKKWCVocS/uerPsl7V+TX/aaUaI7iDkgl3tSmLE=", + "ENCRYPTION_KEY=${encryption_key}", + "AUTH_SECRET=${auth_secret}", "POSTGRES_PASSWORD=${postgres_password}", "POSTGRES_USER=${postgres_user}", "POSTGRES_DB=${postgres_db}", - "SITE_URL=http://${main_domain}:8080", + "SITE_URL=http://${main_domain}", "SMTP_HOST=", - "SMTP_PORT=", - "SMTP_NAME=", + "SMTP_PORT=587", + "SMTP_FROM_ADDRESS=", + "SMTP_FROM_NAME=Infisical", "SMTP_USERNAME=", "SMTP_PASSWORD=", - "CLIENT_ID_HEROKU=", - "CLIENT_ID_VERCEL=", - "CLIENT_ID_NETLIFY=", - "CLIENT_ID_GITHUB=", - "CLIENT_ID_GITHUB_APP=", - "CLIENT_SLUG_GITHUB_APP=", - "CLIENT_ID_GITLAB=", - "CLIENT_ID_BITBUCKET=", - "CLIENT_SECRET_HEROKU=", - "CLIENT_SECRET_VERCEL=", - "CLIENT_SECRET_NETLIFY=", - "CLIENT_SECRET_GITHUB=", - "CLIENT_SECRET_GITHUB_APP=", - "CLIENT_SECRET_GITLAB=", - "CLIENT_SECRET_BITBUCKET=", - "CLIENT_SLUG_VERCEL=", - "CLIENT_PRIVATE_KEY_GITHUB_APP=", - "CLIENT_APP_ID_GITHUB_APP=", - "SENTRY_DSN=", - "POSTHOG_HOST=", - "POSTHOG_PROJECT_API_KEY=", - "CLIENT_ID_GOOGLE_LOGIN=", - "CLIENT_SECRET_GOOGLE_LOGIN=", - "CLIENT_ID_GITHUB_LOGIN=", - "CLIENT_SECRET_GITHUB_LOGIN=", - "CLIENT_ID_GITLAB_LOGIN=", - "CLIENT_SECRET_GITLAB_LOGIN=", - "CAPTCHA_SECRET=", - "NEXT_PUBLIC_CAPTCHA_SITE_KEY=", - "PLAIN_API_KEY=", - "PLAIN_WISH_LABEL_IDS=", - "SSL_CLIENT_CERTIFICATE_HEADER_KEY=", ] mounts = [] [[config.domains]] serviceName = "backend" -port = 8_080 +port = 8080 host = "${main_domain}" From cd164419014610b920d87a3b37f589514316c9d7 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 14 Jul 2026 11:36:03 -0600 Subject: [PATCH 15/80] fix: add databasus (postgresus renamed upstream) Co-Authored-By: Claude Fable 5 --- blueprints/databasus/databasus.svg | 12 ++++++++++++ blueprints/databasus/docker-compose.yml | 22 ++++++++++++++++++++++ blueprints/databasus/meta.json | 17 +++++++++++++++++ blueprints/databasus/template.toml | 11 +++++++++++ blueprints/postgresus/docker-compose.yml | 4 +++- blueprints/postgresus/meta.json | 8 ++++---- 6 files changed, 69 insertions(+), 5 deletions(-) create mode 100644 blueprints/databasus/databasus.svg create mode 100644 blueprints/databasus/docker-compose.yml create mode 100644 blueprints/databasus/meta.json create mode 100644 blueprints/databasus/template.toml diff --git a/blueprints/databasus/databasus.svg b/blueprints/databasus/databasus.svg new file mode 100644 index 000000000..b41cdd402 --- /dev/null +++ b/blueprints/databasus/databasus.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/blueprints/databasus/docker-compose.yml b/blueprints/databasus/docker-compose.yml new file mode 100644 index 000000000..aafa7744a --- /dev/null +++ b/blueprints/databasus/docker-compose.yml @@ -0,0 +1,22 @@ +services: + databasus: + image: databasus/databasus:latest + ports: + - "4005" + volumes: + # Persistent data storage + - databasus-data:/databasus-data + restart: unless-stopped + environment: + # Optional: Set timezone + - TZ=UTC + healthcheck: + test: [ "CMD", "databasus", "healthcheck" ] + interval: 30s + timeout: 5s + retries: 3 + start_period: 60s + +volumes: + databasus-data: + driver: local diff --git a/blueprints/databasus/meta.json b/blueprints/databasus/meta.json new file mode 100644 index 000000000..9eb68a6fb --- /dev/null +++ b/blueprints/databasus/meta.json @@ -0,0 +1,17 @@ +{ + "id": "databasus", + "name": "Databasus", + "version": "latest", + "description": "Free, open source and self-hosted tool to backup PostgreSQL, MySQL, MariaDB and MongoDB. Multiple storages (S3, Google Drive, FTP, etc.), notifications and Point-in-Time Recovery. Successor of Postgresus", + "logo": "databasus.svg", + "links": { + "github": "https://github.com/databasus/databasus", + "website": "https://databasus.com", + "docs": "https://databasus.com/installation" + }, + "tags": [ + "postgres", + "backup", + "s3" + ] +} diff --git a/blueprints/databasus/template.toml b/blueprints/databasus/template.toml new file mode 100644 index 000000000..0a6dd3b9e --- /dev/null +++ b/blueprints/databasus/template.toml @@ -0,0 +1,11 @@ +[variables] +main_domain = "${domain}" + +[config] +env = [] +mounts = [] + +[[config.domains]] +serviceName = "databasus" +port = 4005 +host = "${main_domain}" diff --git a/blueprints/postgresus/docker-compose.yml b/blueprints/postgresus/docker-compose.yml index e0c573ee7..ebcdaabbd 100644 --- a/blueprints/postgresus/docker-compose.yml +++ b/blueprints/postgresus/docker-compose.yml @@ -1,6 +1,8 @@ services: postgresus: - image: rostislavdugin/postgresus:latest + # Postgresus was renamed upstream to Databasus (https://github.com/databasus/databasus). + # This image is frozen at its final release; use the "databasus" template for new deployments. + image: rostislavdugin/postgresus:v2.15.3 ports: - "4005" volumes: diff --git a/blueprints/postgresus/meta.json b/blueprints/postgresus/meta.json index 05d798902..d5fef9603 100644 --- a/blueprints/postgresus/meta.json +++ b/blueprints/postgresus/meta.json @@ -1,13 +1,13 @@ { "id": "postgresus", "name": "Postgresus", - "version": "latest", - "description": "Free, open source and self-hosted solution for automated PostgreSQL backups. With multiple storage options and notifications", + "version": "v2.15.3", + "description": "Deprecated: the project was renamed to Databasus, use the Databasus template instead. Free, open source and self-hosted solution for automated PostgreSQL backups, frozen at its final release", "logo": "postgresus.svg", "links": { "github": "https://github.com/RostislavDugin/postgresus", - "website": "https://postgresus.com", - "docs": "https://postgresus.com" + "website": "https://databasus.com", + "docs": "https://databasus.com" }, "tags": [ "postgres", From fb64197738a529e83e4fe4e384b95be4208a6aad Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 14 Jul 2026 11:38:27 -0600 Subject: [PATCH 16/80] fix(evolutionapi): enable integration flags and update version Adds the integration enable env flags introduced in Evolution API v2.3.x (N8N_ENABLED, OPENAI_ENABLED, EVOAI_ENABLED, DIFY_ENABLED, TYPEBOT_ENABLED, CHATWOOT_ENABLED) so integrations no longer fail with 'N8n is disabled', and pins the image to v2.3.7 instead of latest. Co-Authored-By: Claude Fable 5 --- blueprints/evolutionapi/docker-compose.yml | 9 ++++++++- blueprints/evolutionapi/meta.json | 2 +- blueprints/evolutionapi/template.toml | 7 +++++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/blueprints/evolutionapi/docker-compose.yml b/blueprints/evolutionapi/docker-compose.yml index 16f125318..5bdd30751 100644 --- a/blueprints/evolutionapi/docker-compose.yml +++ b/blueprints/evolutionapi/docker-compose.yml @@ -1,6 +1,6 @@ services: evolution-api: - image: evoapicloud/evolution-api:latest + image: evoapicloud/evolution-api:v2.3.7 restart: always volumes: - evolution-instances:/evolution/instances @@ -29,6 +29,13 @@ services: - CACHE_REDIS_URI=${CACHE_REDIS_URI} - CACHE_REDIS_PREFIX_KEY=${CACHE_REDIS_PREFIX_KEY} - CACHE_REDIS_SAVE_INSTANCES=${CACHE_REDIS_SAVE_INSTANCES} + - TYPEBOT_ENABLED=${TYPEBOT_ENABLED} + - TYPEBOT_API_VERSION=${TYPEBOT_API_VERSION} + - CHATWOOT_ENABLED=${CHATWOOT_ENABLED} + - OPENAI_ENABLED=${OPENAI_ENABLED} + - DIFY_ENABLED=${DIFY_ENABLED} + - N8N_ENABLED=${N8N_ENABLED} + - EVOAI_ENABLED=${EVOAI_ENABLED} evolution-postgres: image: postgres:16-alpine diff --git a/blueprints/evolutionapi/meta.json b/blueprints/evolutionapi/meta.json index 096ed0425..21cf2ce4f 100644 --- a/blueprints/evolutionapi/meta.json +++ b/blueprints/evolutionapi/meta.json @@ -1,7 +1,7 @@ { "id": "evolutionapi", "name": "Evolution API", - "version": "v2.1.2", + "version": "v2.3.7", "description": "Evolution API is a robust platform dedicated to empowering small businesses with limited resources, going beyond a simple messaging solution via WhatsApp.", "logo": "evolutionapi.png", "links": { diff --git a/blueprints/evolutionapi/template.toml b/blueprints/evolutionapi/template.toml index c547b9fd6..d32251a81 100644 --- a/blueprints/evolutionapi/template.toml +++ b/blueprints/evolutionapi/template.toml @@ -31,6 +31,13 @@ env = [ "CACHE_REDIS_URI=redis://evolution-redis:6379", "CACHE_REDIS_PREFIX_KEY=evolution", "CACHE_REDIS_SAVE_INSTANCES=true", + "TYPEBOT_ENABLED=true", + "TYPEBOT_API_VERSION=latest", + "CHATWOOT_ENABLED=true", + "OPENAI_ENABLED=true", + "DIFY_ENABLED=true", + "N8N_ENABLED=true", + "EVOAI_ENABLED=true", ] mounts = [] From 25563213f247feb819a9c48567ef95e702d6f507 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 14 Jul 2026 11:39:58 -0600 Subject: [PATCH 17/80] fix(rybbit): update to v2.7.0 The template shipped rybbit v1.5.1, which is no longer supported and is vulnerable to known React2Shell exploits (see #659). Update to the latest stable release v2.7.0 and align the stack with the upstream docker-compose: - Pin ghcr.io/rybbit-io/rybbit-backend and rybbit-client to v2.7.0 - Add the redis service now required by the backend (session tracking and BullMQ queues), with password auth, AOF persistence and noeviction policy per upstream - Drop the DOMAIN_NAME env var (only used by the upstream Caddy webserver, which Dokploy replaces with Traefik) - Add DISABLE_TELEMETRY env and generate BETTER_AUTH_SECRET as base64 Co-Authored-By: Claude Fable 5 --- blueprints/rybbit/docker-compose.yml | 52 ++++++++++++++++++++++++---- blueprints/rybbit/meta.json | 2 +- blueprints/rybbit/template.toml | 6 ++-- 3 files changed, 50 insertions(+), 10 deletions(-) diff --git a/blueprints/rybbit/docker-compose.yml b/blueprints/rybbit/docker-compose.yml index 2bd2d6b4c..4f13c5c63 100644 --- a/blueprints/rybbit/docker-compose.yml +++ b/blueprints/rybbit/docker-compose.yml @@ -1,13 +1,13 @@ # https://www.rybbit.io/docs/self-hosting-advanced # NOTE: there are two sample HTTP traefik domain entries created: -# - rybbit_backend (port 3001, path /api), +# - rybbit_backend (port 3001, path /api), # - rybbit_client (port 3002, path /) # # You should treat these as placeholders - Rybbit only supports HTTPS. # -# You should also update the `BASE_URL`, and `DOMAIN_NAME` environment -# variable when updating the domain entries with your custom domain. +# You should also update the `BASE_URL` environment variable when +# updating the domain entries with your custom domain. services: rybbit_clickhouse: @@ -50,8 +50,41 @@ services: retries: 3 restart: unless-stopped + rybbit_redis: + image: redis:7-alpine + volumes: + - redis_data:/data + # Backs session tracking and the BullMQ queues. noeviction is required by + # BullMQ (evicting job keys corrupts queues); AOF (everysec) keeps queues + # durable across restarts. + command: + - redis-server + - --requirepass + - ${REDIS_PASSWORD} + - --appendonly + - "yes" + - --appendfsync + - everysec + - --maxmemory-policy + - noeviction + healthcheck: + test: + [ + "CMD", + "redis-cli", + "-a", + "${REDIS_PASSWORD}", + "--no-auth-warning", + "ping", + ] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + restart: unless-stopped + rybbit_backend: - image: ghcr.io/rybbit-io/rybbit-backend:v1.5.1 + image: ghcr.io/rybbit-io/rybbit-backend:v2.7.0 environment: - NODE_ENV=production - CLICKHOUSE_HOST=http://rybbit_clickhouse:8123 @@ -63,15 +96,20 @@ services: - POSTGRES_DB=${POSTGRES_DB} - POSTGRES_USER=${POSTGRES_USER} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - REDIS_HOST=rybbit_redis + - REDIS_PORT=6379 + - REDIS_PASSWORD=${REDIS_PASSWORD} - BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET} - BASE_URL=${BASE_URL} - - DOMAIN_NAME=${DOMAIN_NAME} - DISABLE_SIGNUP=${DISABLE_SIGNUP} + - DISABLE_TELEMETRY=${DISABLE_TELEMETRY} depends_on: rybbit_clickhouse: condition: service_healthy rybbit_postgres: condition: service_started + rybbit_redis: + condition: service_healthy healthcheck: test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:3001/api/health"] interval: 30s @@ -81,11 +119,10 @@ services: restart: unless-stopped rybbit_client: - image: ghcr.io/rybbit-io/rybbit-client:v1.5.1 + image: ghcr.io/rybbit-io/rybbit-client:v2.7.0 environment: - NODE_ENV=production - NEXT_PUBLIC_BACKEND_URL=${BASE_URL} - - DOMAIN_NAME=${DOMAIN_NAME} - NEXT_PUBLIC_DISABLE_SIGNUP=${DISABLE_SIGNUP} depends_on: - rybbit_backend @@ -94,3 +131,4 @@ services: volumes: clickhouse_data: postgres_data: + redis_data: diff --git a/blueprints/rybbit/meta.json b/blueprints/rybbit/meta.json index bfac0f2cc..47765f562 100644 --- a/blueprints/rybbit/meta.json +++ b/blueprints/rybbit/meta.json @@ -1,7 +1,7 @@ { "id": "rybbit", "name": "Rybbit", - "version": "v1.5.1", + "version": "v2.7.0", "description": "Open-source and privacy-friendly alternative to Google Analytics that is 10x more intuitive", "logo": "rybbit.png", "links": { diff --git a/blueprints/rybbit/template.toml b/blueprints/rybbit/template.toml index c67e65239..c31bddaf5 100644 --- a/blueprints/rybbit/template.toml +++ b/blueprints/rybbit/template.toml @@ -1,8 +1,9 @@ [variables] main_domain = "${domain}" -better_auth_secret = "${password:32}" +better_auth_secret = "${base64:32}" clickhouse_password = "${password:32}" postgres_password = "${password:32}" +redis_password = "${password:32}" [[config.domains]] serviceName = "rybbit_backend" @@ -17,15 +18,16 @@ host = "${main_domain}" [config.env] BASE_URL = "http://${main_domain}" -DOMAIN_NAME= "${main_domain}" BETTER_AUTH_SECRET = "${better_auth_secret}" DISABLE_SIGNUP = "false" +DISABLE_TELEMETRY = "false" CLICKHOUSE_DB = "analytics" CLICKHOUSE_USER = "default" CLICKHOUSE_PASSWORD = "${clickhouse_password}" POSTGRES_DB = "analytics" POSTGRES_USER = "frog" POSTGRES_PASSWORD = "${postgres_password}" +REDIS_PASSWORD = "${redis_password}" [[config.mounts]] filePath = "./clickhouse_config/enable_json.xml" From 9bdbe266e52cc44f76626bba26dbe38c78d1d9c6 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 14 Jul 2026 11:47:20 -0600 Subject: [PATCH 18/80] fix(phpmyadmin): raise PHP upload limit so SQL imports over 2MB work Without UPLOAD_LIMIT the phpMyAdmin image keeps PHP's default 2M post_max_size/upload_max_filesize, so importing any SQL file larger than 2MB fails with 'POST Content-Length exceeds the limit of 2097152 bytes' followed by a session_start error page. - Add UPLOAD_LIMIT (512M) and MAX_EXECUTION_TIME (600) env vars, configurable from the template env - Wire the previously unused MYSQL_DATABASE env var into the compose file, replacing the hardcoded 'tu_base_de_datos' placeholder, and default it to 'phpmyadmin' instead of the system 'mysql' schema Closes #236 Co-Authored-By: Claude Fable 5 --- blueprints/phpmyadmin/docker-compose.yml | 4 +++- blueprints/phpmyadmin/template.toml | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/blueprints/phpmyadmin/docker-compose.yml b/blueprints/phpmyadmin/docker-compose.yml index 91674e87c..c45541843 100644 --- a/blueprints/phpmyadmin/docker-compose.yml +++ b/blueprints/phpmyadmin/docker-compose.yml @@ -5,7 +5,7 @@ services: image: mysql:5.7 environment: MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} - MYSQL_DATABASE: tu_base_de_datos + MYSQL_DATABASE: ${MYSQL_DATABASE} MYSQL_USER: ${MYSQL_USER} MYSQL_PASSWORD: ${MYSQL_PASSWORD} volumes: @@ -19,6 +19,8 @@ services: PMA_USER: ${MYSQL_USER} PMA_PASSWORD: ${MYSQL_PASSWORD} PMA_ARBITRARY: 1 + UPLOAD_LIMIT: ${UPLOAD_LIMIT} + MAX_EXECUTION_TIME: ${MAX_EXECUTION_TIME} depends_on: - db diff --git a/blueprints/phpmyadmin/template.toml b/blueprints/phpmyadmin/template.toml index 39c538457..cfbf8d008 100644 --- a/blueprints/phpmyadmin/template.toml +++ b/blueprints/phpmyadmin/template.toml @@ -13,6 +13,8 @@ host = "${main_domain}" [config.env] MYSQL_ROOT_PASSWORD = "${root_password}" -MYSQL_DATABASE = "mysql" +MYSQL_DATABASE = "phpmyadmin" MYSQL_USER = "phpmyadmin" MYSQL_PASSWORD = "${user_password}" +UPLOAD_LIMIT = "512M" +MAX_EXECUTION_TIME = "600" From f946051a6ced8b9ad170b2a9665d9f8e26db3cdb Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 14 Jul 2026 11:53:37 -0600 Subject: [PATCH 19/80] fix(openpanel): pin verified 2.2.1 images and fix ClickHouse interserver host MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Pin lindesvard/openpanel-api|dashboard|worker to 2.2.1 (verified working end-to-end on a Dokploy instance) instead of the mutable :2 tag, so a future broken 2.x release cannot silently break the template again — which is how #615 originated when 2.0.0 shipped. - Fix interserver_http_host to match the actual service name (op-ch), aligning with upstream self-hosting config. - Set meta.json version to the pinned 2.2.1. Co-Authored-By: Claude Fable 5 --- blueprints/openpanel/docker-compose.yml | 6 +++--- blueprints/openpanel/meta.json | 2 +- blueprints/openpanel/template.toml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/blueprints/openpanel/docker-compose.yml b/blueprints/openpanel/docker-compose.yml index 53c95bd29..34d030e4f 100644 --- a/blueprints/openpanel/docker-compose.yml +++ b/blueprints/openpanel/docker-compose.yml @@ -54,7 +54,7 @@ services: retries: 5 op-api: - image: lindesvard/openpanel-api:2 + image: lindesvard/openpanel-api:2.2.1 restart: always command: > sh -c " @@ -97,7 +97,7 @@ services: condition: service_healthy op-dashboard: - image: lindesvard/openpanel-dashboard:2 + image: lindesvard/openpanel-dashboard:2.2.1 restart: always depends_on: op-api: @@ -111,7 +111,7 @@ services: retries: 5 op-worker: - image: lindesvard/openpanel-worker:2 + image: lindesvard/openpanel-worker:2.2.1 restart: always depends_on: op-api: diff --git a/blueprints/openpanel/meta.json b/blueprints/openpanel/meta.json index e395573e4..ba3276eef 100644 --- a/blueprints/openpanel/meta.json +++ b/blueprints/openpanel/meta.json @@ -1,7 +1,7 @@ { "id": "openpanel", "name": "OpenPanel", - "version": "latest", + "version": "2.2.1", "description": "An open-source web and product analytics platform that combines the power of Mixpanel with the ease of Plausible and one of the best Google Analytics replacements.", "logo": "logo.svg", "links": { diff --git a/blueprints/openpanel/template.toml b/blueprints/openpanel/template.toml index 2a93c33c7..208df223d 100644 --- a/blueprints/openpanel/template.toml +++ b/blueprints/openpanel/template.toml @@ -26,7 +26,7 @@ content = """ 0.0.0.0 0.0.0.0 - opch + op-ch 0 From af162505610181633a70c7f448cff6d3a7039dd8 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 14 Jul 2026 11:55:19 -0600 Subject: [PATCH 20/80] fix(wordpress): route loopback requests through Traefik to fix cURL error 28 WordPress resolves its own public domain to the server's public IP from inside the container, so REST API / WP-Cron / Site Health loopback requests depend on hairpin NAT, which times out (cURL error 28) on many hosts. Pin the site domain to the dokploy-traefik container IP in /etc/hosts at startup (with retries, since Traefik joins the compose network shortly after the container starts) so loopback traffic stays inside the Docker network for both HTTP and HTTPS. No-op fallback when dokploy-traefik is not resolvable. Co-Authored-By: Claude Fable 5 --- blueprints/wordpress/docker-compose.yml | 22 ++++++++++++++++++++++ blueprints/wordpress/template.toml | 3 ++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/blueprints/wordpress/docker-compose.yml b/blueprints/wordpress/docker-compose.yml index 70b8aba35..75ab474a5 100644 --- a/blueprints/wordpress/docker-compose.yml +++ b/blueprints/wordpress/docker-compose.yml @@ -1,6 +1,27 @@ services: wordpress: image: wordpress:latest + # Resolve the site's own domain to the Traefik container instead of the + # server's public IP. Loopback requests (REST API, WP-Cron, Site Health) + # otherwise depend on hairpin NAT, which times out on many hosts + # (cURL error 28). See https://github.com/Dokploy/templates/issues/128 + command: + - bash + - -c + - | + if [ -n "$$WP_DOMAIN" ]; then + ( + for i in $$(seq 1 60); do + TRAEFIK_IP="$$(getent hosts dokploy-traefik | awk '{print $$1; exit}')" + if [ -n "$$TRAEFIK_IP" ]; then + grep -q "[[:space:]]$$WP_DOMAIN\$$" /etc/hosts || echo "$$TRAEFIK_IP $$WP_DOMAIN" >> /etc/hosts + break + fi + sleep 2 + done + ) & + fi + exec docker-entrypoint.sh apache2-foreground volumes: - wp_app:/var/www/html - ../files/uploads.ini:/usr/local/etc/php/conf.d/uploads.ini @@ -10,6 +31,7 @@ services: WORDPRESS_DB_USER: root WORDPRESS_DB_PASSWORD: $DB_PASSWORD WORDPRESS_DEBUG: ${WORDPRESS_DEBUG:-0} + WP_DOMAIN: ${WP_DOMAIN} WORDPRESS_CONFIG_EXTRA: | define('WP_MEMORY_LIMIT', '256M'); define('DISALLOW_FILE_EDIT', true); diff --git a/blueprints/wordpress/template.toml b/blueprints/wordpress/template.toml index 91d95e9c1..c11c63f2a 100644 --- a/blueprints/wordpress/template.toml +++ b/blueprints/wordpress/template.toml @@ -9,7 +9,8 @@ env = [ "WORDPRESS_DEBUG=0", "DB_NAME=${db_name}", "DB_USER=${db_user}", - "DB_PASSWORD=${db_password}" + "DB_PASSWORD=${db_password}", + "WP_DOMAIN=${main_domain}" ] [[config.domains]] From 030d9d1089aadaa248c3b896c17cdf239eff9ae5 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 14 Jul 2026 11:55:53 -0600 Subject: [PATCH 21/80] fix(minio): migrate to pgsty/minio community fork (upstream is source-only and archived) MinIO Inc. stopped publishing Docker images and pre-built binaries in October 2025 and archived the minio/minio repository in February 2026. The official minio/minio image is frozen at RELEASE.2025-09-07T16-13-09Z and no longer receives security fixes (the fix for CVE-2025-62506 was never published as an image). - Switch image to pgsty/minio:RELEASE.2026-06-18T00-00-00Z, the actively maintained community fork (drop-in replacement, AGPLv3, same env vars, same /data on-disk format, restores the full web console) - Route the S3 API (port 9000) through its own generated domain instead of publishing host port 9000, per repo conventions - Update meta.json links/description to point at the maintained fork Closes #469 Co-Authored-By: Claude Fable 5 --- blueprints/minio/docker-compose.yml | 21 ++++++++++++--------- blueprints/minio/meta.json | 10 +++++----- blueprints/minio/template.toml | 7 +++++++ 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/blueprints/minio/docker-compose.yml b/blueprints/minio/docker-compose.yml index a25e3aeec..575ddf953 100644 --- a/blueprints/minio/docker-compose.yml +++ b/blueprints/minio/docker-compose.yml @@ -1,9 +1,14 @@ services: minio: - # after RELEASE.2025-04-22T22-12-26Z, minio removed most of the admin UI, if you want to use the admin UI, uncomment the line below - # image: minio/minio:RELEASE.2025-04-22T22-12-26Z - # if you uncommented the line above, comment the line below - image: minio/minio + # MinIO Inc. stopped publishing Docker images and pre-built binaries in October 2025 + # (https://github.com/minio/minio/issues/21647) and archived the minio/minio repository + # in February 2026. The official minio/minio image is frozen at RELEASE.2025-09-07T16-13-09Z + # and no longer receives security fixes (e.g. the fix for CVE-2025-62506 was never published + # as an image). This template uses pgsty/minio, the actively maintained community fork + # (https://github.com/pgsty/minio, AGPLv3). It is a drop-in replacement: same environment + # variables, same `server` command, same /data on-disk format (existing minio-data volumes + # keep working), and it restores the full web console that upstream removed. + image: pgsty/minio:RELEASE.2026-06-18T00-00-00Z restart: unless-stopped volumes: # by default, the MinIO container will use a volume named minio-data @@ -19,12 +24,10 @@ services: - MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD} - MINIO_BROWSER_REDIRECT_URL=${MINIO_BROWSER_REDIRECT_URL} command: server /data --console-address ":9001" - ports: - # by default, the MinIO container will use port 9000 to expose its API - # and port 9001 to expose its web console - # minio requires port to be specified when making a request to the API - - 9000:9000 expose: + # port 9000 serves the S3 API (routed through the api domain defined in template.toml) + # port 9001 serves the web console (routed through the main domain) + - 9000 - 9001 # comment the line below if you specified a local directory in the volumes section of the minio service diff --git a/blueprints/minio/meta.json b/blueprints/minio/meta.json index 954272348..423c0c828 100644 --- a/blueprints/minio/meta.json +++ b/blueprints/minio/meta.json @@ -1,13 +1,13 @@ { "id": "minio", "name": "Minio", - "description": "Minio is an open source object storage server compatible with Amazon S3 cloud storage service.", + "description": "MinIO is an open source object storage server compatible with Amazon S3 cloud storage service. This template uses pgsty/minio, the actively maintained community fork, because MinIO Inc. stopped publishing Docker images in October 2025 and archived the upstream open source project in February 2026.", "logo": "minio.png", - "version": "latest", + "version": "RELEASE.2026-06-18T00-00-00Z", "links": { - "github": "https://github.com/minio/minio", - "website": "https://minio.io/", - "docs": "https://docs.minio.io/" + "github": "https://github.com/pgsty/minio", + "website": "https://silo.pigsty.io/", + "docs": "https://silo.pigsty.io/docs/" }, "tags": [ "storage" diff --git a/blueprints/minio/template.toml b/blueprints/minio/template.toml index ae6f17796..5a358b41f 100644 --- a/blueprints/minio/template.toml +++ b/blueprints/minio/template.toml @@ -5,11 +5,18 @@ api_domain = "${domain}" [config] mounts = [] +# Web console - log in with the generated root credentials below [[config.domains]] serviceName = "minio" port = 9_001 host = "${main_domain}" +# S3-compatible API - point AWS CLI / SDKs / rclone / backup tools at this domain +[[config.domains]] +serviceName = "minio" +port = 9_000 +host = "${api_domain}" + [config.env] MINIO_ROOT_USER = "minioadmin" MINIO_ROOT_PASSWORD = "${password:16}" From f036ddae928af8c02585159a1a57443b88d7f82d Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 14 Jul 2026 11:58:34 -0600 Subject: [PATCH 22/80] fix(nextcloud-aio): drop AIO branding, keep template id, pin nextcloud 33.0.6 The template ships plain nextcloud + mariadb + redis + cron, not the nextcloud/all-in-one mastercontainer. Display name and description no longer claim AIO; folder id is kept as nextcloud-aio to avoid breaking existing installs. Image pinned to the current stable tag (33.0.6). Co-Authored-By: Claude Fable 5 --- blueprints/nextcloud-aio/docker-compose.yml | 4 ++-- blueprints/nextcloud-aio/meta.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/blueprints/nextcloud-aio/docker-compose.yml b/blueprints/nextcloud-aio/docker-compose.yml index 7550c2523..954684a4a 100644 --- a/blueprints/nextcloud-aio/docker-compose.yml +++ b/blueprints/nextcloud-aio/docker-compose.yml @@ -1,6 +1,6 @@ services: nextcloud: - image: nextcloud:stable + image: nextcloud:33.0.6 restart: always volumes: - nextcloud_data:/var/www/html @@ -15,7 +15,7 @@ services: - nextcloud_redis nextcloud_cron: - image: nextcloud:stable + image: nextcloud:33.0.6 restart: always volumes: - nextcloud_data:/var/www/html diff --git a/blueprints/nextcloud-aio/meta.json b/blueprints/nextcloud-aio/meta.json index 4cb120ada..6d4001dec 100644 --- a/blueprints/nextcloud-aio/meta.json +++ b/blueprints/nextcloud-aio/meta.json @@ -1,7 +1,7 @@ { "id": "nextcloud-aio", "name": "Nextcloud", - "version": "stable", + "version": "33.0.6", "description": "Nextcloud is a self-hosted file storage and sync platform with powerful collaboration capabilities. It integrates Files, Talk, Groupware, Office, Assistant and more into a single platform for remote work and data protection.", "logo": "nextcloud.png", "links": { From b7c2d6c35d73e943a737eb3fe22e587474a353ee Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 14 Jul 2026 12:03:04 -0600 Subject: [PATCH 23/80] feat: add MailCatcher NG template Co-Authored-By: Claude Fable 5 --- blueprints/mailcatcher-ng/docker-compose.yml | 27 ++++++++++++++++++++ blueprints/mailcatcher-ng/mailcatcher-ng.svg | 10 ++++++++ blueprints/mailcatcher-ng/meta.json | 17 ++++++++++++ blueprints/mailcatcher-ng/template.toml | 13 ++++++++++ 4 files changed, 67 insertions(+) create mode 100644 blueprints/mailcatcher-ng/docker-compose.yml create mode 100644 blueprints/mailcatcher-ng/mailcatcher-ng.svg create mode 100644 blueprints/mailcatcher-ng/meta.json create mode 100644 blueprints/mailcatcher-ng/template.toml diff --git a/blueprints/mailcatcher-ng/docker-compose.yml b/blueprints/mailcatcher-ng/docker-compose.yml new file mode 100644 index 000000000..3a31b68a8 --- /dev/null +++ b/blueprints/mailcatcher-ng/docker-compose.yml @@ -0,0 +1,27 @@ +services: + mailcatcher-ng: + # Only the "latest" tag is published, so the image is pinned by digest + # (v2.3.9 of the image, MailCatcher NG 1.6.8). + image: stpaquet/alpinemailcatcher:latest@sha256:499c0eabca5e82b23d030706ba9d5b8b27bba5a5f34a43154ee5e68b07cba52c + restart: unless-stopped + # Same as the image default CMD, plus --persistence so caught mail + # survives container restarts (stored in the named volume below). + command: sh -c "mailcatcher --foreground --smtp-port=1025 --http-port=1080 --ip=0.0.0.0 --messages-limit=$$MAIL_LIMIT --persistence --no-quit" + environment: + - MAIL_LIMIT=${MAIL_LIMIT} + expose: + # SMTP: reachable from other services in the same Dokploy network + # at mailcatcher-ng:1025 (intentionally not published on the host). + - 1025 + # Web UI (routed through the domain below) + - 1080 + volumes: + - mailcatcher-data:/home/mailcatcher/.mailcatcher + healthcheck: + test: ['CMD', 'wget', '-q', '--spider', 'http://127.0.0.1:1080'] + interval: 10s + timeout: 5s + retries: 10 + +volumes: + mailcatcher-data: diff --git a/blueprints/mailcatcher-ng/mailcatcher-ng.svg b/blueprints/mailcatcher-ng/mailcatcher-ng.svg new file mode 100644 index 000000000..9ffcc2cf4 --- /dev/null +++ b/blueprints/mailcatcher-ng/mailcatcher-ng.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/blueprints/mailcatcher-ng/meta.json b/blueprints/mailcatcher-ng/meta.json new file mode 100644 index 000000000..814ea7819 --- /dev/null +++ b/blueprints/mailcatcher-ng/meta.json @@ -0,0 +1,17 @@ +{ + "id": "mailcatcher-ng", + "name": "MailCatcher NG", + "version": "1.6.8", + "description": "MailCatcher NG runs a super simple SMTP server which catches any message sent to it to display in a web interface. Point your app's SMTP settings at mailcatcher-ng:1025 and inspect every captured email in the browser.", + "logo": "mailcatcher-ng.svg", + "links": { + "github": "https://github.com/spaquet/mailcatcher", + "website": "https://spaquet.github.io/mailcatcher/", + "docs": "https://spaquet.github.io/docker-alpine-mailcatcher/" + }, + "tags": [ + "email", + "smtp", + "development" + ] +} diff --git a/blueprints/mailcatcher-ng/template.toml b/blueprints/mailcatcher-ng/template.toml new file mode 100644 index 000000000..2a72b979d --- /dev/null +++ b/blueprints/mailcatcher-ng/template.toml @@ -0,0 +1,13 @@ +[variables] +main_domain = "${domain}" + +[config] +env = [ + "MAIL_LIMIT=50", +] +mounts = [] + +[[config.domains]] +serviceName = "mailcatcher-ng" +port = 1_080 +host = "${main_domain}" From 03a60bb417fcbd6030dee00d2c3f4eca3c8d3dea Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 14 Jul 2026 12:04:30 -0600 Subject: [PATCH 24/80] docs: add per-template setup instructions (supabase, trigger.dev) Adds instructions.md files for the Supabase and Trigger.dev templates and renders them in the template browser as a new Instructions tab in the template dialog (fetched from blueprints//instructions.md, rendered with a small dependency-free markdown component). Also removes the placeholder blueprints/ackee/instructions.md so it does not surface as an empty Instructions tab. Co-Authored-By: Claude Fable 5 --- app/src/components/TemplateDialog.tsx | 37 ++++- app/src/components/TemplateGrid.tsx | 27 +++- app/src/components/ui/markdown.tsx | 177 +++++++++++++++++++++++ blueprints/ackee/instructions.md | 4 - blueprints/supabase/instructions.md | 61 ++++++++ blueprints/triggerdotdev/instructions.md | 54 +++++++ 6 files changed, 349 insertions(+), 11 deletions(-) create mode 100644 app/src/components/ui/markdown.tsx delete mode 100644 blueprints/ackee/instructions.md create mode 100644 blueprints/supabase/instructions.md create mode 100644 blueprints/triggerdotdev/instructions.md diff --git a/app/src/components/TemplateDialog.tsx b/app/src/components/TemplateDialog.tsx index b1e9b2f09..de10ce38a 100644 --- a/app/src/components/TemplateDialog.tsx +++ b/app/src/components/TemplateDialog.tsx @@ -14,6 +14,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs"; import { Label } from "./ui/label"; import { Clipboard } from "lucide-react"; import { Input } from "./ui/input"; +import Markdown from "./ui/markdown"; interface Template { id: string; @@ -32,6 +33,7 @@ interface Template { interface TemplateFiles { dockerCompose: string | null; config: string | null; + instructions: string | null; } interface TemplateDialogProps { @@ -169,8 +171,23 @@ const TemplateDialog: React.FC = ({ )} - + + {templateFiles?.instructions && ( + + Instructions + + )} = ({ Configuration + + {templateFiles?.instructions && ( + +
+ +
+ +
+
+
+ )} {templateFiles?.dockerCompose && (
diff --git a/app/src/components/TemplateGrid.tsx b/app/src/components/TemplateGrid.tsx index 8f51dd697..42e1a6555 100644 --- a/app/src/components/TemplateGrid.tsx +++ b/app/src/components/TemplateGrid.tsx @@ -28,6 +28,7 @@ interface Template { interface TemplateFiles { dockerCompose: string | null; config: string | null; + instructions: string | null; } interface TemplateGridProps { @@ -80,20 +81,34 @@ const TemplateGrid: React.FC = ({ view }) => { const fetchTemplateFiles = async (templateId: string) => { setModalLoading(true); try { - const [dockerComposeRes, configRes] = await Promise.all([ - fetch(`/blueprints/${templateId}/docker-compose.yml`), - fetch(`/blueprints/${templateId}/template.toml`), - ]); + const [dockerComposeRes, configRes, instructionsRes] = await Promise.all( + [ + fetch(`/blueprints/${templateId}/docker-compose.yml`), + fetch(`/blueprints/${templateId}/template.toml`), + fetch(`/blueprints/${templateId}/instructions.md`), + ] + ); const dockerCompose = dockerComposeRes.ok ? await dockerComposeRes.text() : null; const config = configRes.ok ? await configRes.text() : null; - setTemplateFiles({ dockerCompose, config }); + // Guard against SPA fallbacks that return index.html with a 200 status + // for templates that don't have an instructions.md file. + const instructionsIsMarkdown = + instructionsRes.ok && + !(instructionsRes.headers.get("content-type") || "").includes( + "text/html" + ); + const instructions = instructionsIsMarkdown + ? await instructionsRes.text() + : null; + + setTemplateFiles({ dockerCompose, config, instructions }); } catch (err) { console.error("Error fetching template files:", err); - setTemplateFiles({ dockerCompose: null, config: null }); + setTemplateFiles({ dockerCompose: null, config: null, instructions: null }); } finally { setModalLoading(false); } diff --git a/app/src/components/ui/markdown.tsx b/app/src/components/ui/markdown.tsx new file mode 100644 index 000000000..03c0ca87a --- /dev/null +++ b/app/src/components/ui/markdown.tsx @@ -0,0 +1,177 @@ +import React from "react"; + +// Minimal markdown renderer (no external dependencies) for the per-template +// instructions.md files. Supports headings, paragraphs, fenced code blocks, +// ordered/unordered lists, and inline code / bold / links. + +const INLINE_PATTERN = + /(`[^`]+`)|(\*\*[^*]+\*\*)|(\[[^\]]+\]\([^)\s]+\))/g; + +const renderInline = (text: string): React.ReactNode[] => { + const nodes: React.ReactNode[] = []; + let lastIndex = 0; + let key = 0; + let match: RegExpExecArray | null; + INLINE_PATTERN.lastIndex = 0; + + while ((match = INLINE_PATTERN.exec(text)) !== null) { + if (match.index > lastIndex) { + nodes.push(text.slice(lastIndex, match.index)); + } + const token = match[0]; + if (token.startsWith("`")) { + nodes.push( + + {token.slice(1, -1)} + + ); + } else if (token.startsWith("**")) { + nodes.push({token.slice(2, -2)}); + } else { + const link = /^\[([^\]]+)\]\(([^)\s]+)\)$/.exec(token); + if (link) { + nodes.push( + + {link[1]} + + ); + } else { + nodes.push(token); + } + } + lastIndex = match.index + token.length; + } + if (lastIndex < text.length) { + nodes.push(text.slice(lastIndex)); + } + return nodes; +}; + +const HEADING_CLASSES: Record = { + 1: "text-2xl font-bold mt-2", + 2: "text-xl font-semibold mt-6 border-b pb-1", + 3: "text-lg font-semibold mt-4", + 4: "text-base font-semibold mt-3", +}; + +interface MarkdownProps { + content: string; +} + +const Markdown: React.FC = ({ content }) => { + const lines = content.replace(/\r\n/g, "\n").split("\n"); + const blocks: React.ReactNode[] = []; + let key = 0; + let i = 0; + + while (i < lines.length) { + const line = lines[i]; + + // Blank line + if (line.trim() === "") { + i++; + continue; + } + + // Fenced code block + if (line.trim().startsWith("```")) { + const code: string[] = []; + i++; + while (i < lines.length && !lines[i].trim().startsWith("```")) { + code.push(lines[i]); + i++; + } + i++; // skip closing fence + blocks.push( +
+          {code.join("\n")}
+        
+ ); + continue; + } + + // Heading + const heading = /^(#{1,4})\s+(.*)$/.exec(line); + if (heading) { + const level = heading[1].length; + const Tag = `h${Math.min(level + 1, 6)}` as keyof React.JSX.IntrinsicElements; + blocks.push( + + {renderInline(heading[2])} + + ); + i++; + continue; + } + + // Unordered list + if (/^[-*]\s+/.test(line)) { + const items: string[] = []; + while (i < lines.length && /^[-*]\s+/.test(lines[i])) { + items.push(lines[i].replace(/^[-*]\s+/, "")); + i++; + } + blocks.push( +
    + {items.map((item, idx) => ( +
  • {renderInline(item)}
  • + ))} +
+ ); + continue; + } + + // Ordered list + if (/^\d+\.\s+/.test(line)) { + const items: string[] = []; + while (i < lines.length && /^\d+\.\s+/.test(lines[i])) { + items.push(lines[i].replace(/^\d+\.\s+/, "")); + i++; + } + blocks.push( +
    + {items.map((item, idx) => ( +
  1. {renderInline(item)}
  2. + ))} +
+ ); + continue; + } + + // Paragraph: consume consecutive plain lines + const paragraph: string[] = [line]; + i++; + while ( + i < lines.length && + lines[i].trim() !== "" && + !/^(#{1,4})\s+/.test(lines[i]) && + !/^[-*]\s+/.test(lines[i]) && + !/^\d+\.\s+/.test(lines[i]) && + !lines[i].trim().startsWith("```") + ) { + paragraph.push(lines[i]); + i++; + } + blocks.push( +

+ {renderInline(paragraph.join(" "))} +

+ ); + } + + return
{blocks}
; +}; + +export default Markdown; diff --git a/blueprints/ackee/instructions.md b/blueprints/ackee/instructions.md deleted file mode 100644 index b107d3602..000000000 --- a/blueprints/ackee/instructions.md +++ /dev/null @@ -1,4 +0,0 @@ - -## Instructions - -We don't have nothing to show here.... diff --git a/blueprints/supabase/instructions.md b/blueprints/supabase/instructions.md new file mode 100644 index 000000000..af95f2136 --- /dev/null +++ b/blueprints/supabase/instructions.md @@ -0,0 +1,61 @@ +# Supabase Setup Instructions + +## Deploy + +1. In Dokploy, create the service from the **Supabase** template (requires Dokploy `>= 0.22.5`). +2. Dokploy automatically generates all secrets for you (`POSTGRES_PASSWORD`, `JWT_SECRET`, `ANON_KEY`, `SERVICE_ROLE_KEY`, `DASHBOARD_PASSWORD`, etc.). You can review them in the **Environment** tab of the service. +3. Deploy and wait for all containers to become healthy. The first deploy can take several minutes while the Postgres database initializes. + +## Log in to Supabase Studio + +The main domain of the template points to the `kong` API gateway (port `8000`), which protects Supabase Studio with basic authentication: + +- **Username**: the value of `DASHBOARD_USERNAME` (default: `supabase`) +- **Password**: the value of `DASHBOARD_PASSWORD` + +Both values are in the **Environment** tab of the service in Dokploy. + +## API URL and keys + +To connect an application (for example with `supabase-js`): + +- **API URL**: `https://` (requests are routed through Kong) +- **anon key**: the value of `ANON_KEY` in the Environment tab +- **service_role key**: the value of `SERVICE_ROLE_KEY` in the Environment tab (server-side only, never expose it to browsers) + +## Recommended configuration + +Review these variables in the **Environment** tab before using Supabase in production: + +- `SUPABASE_PUBLIC_URL` and `API_EXTERNAL_URL`: must point to your Supabase domain with the correct `http`/`https` scheme (the template sets them from your domain automatically). +- `SITE_URL` and `ADDITIONAL_REDIRECT_URLS`: must point to the application that uses Supabase for authentication. +- `SMTP_HOST`, `SMTP_PORT`, `SMTP_USER`, `SMTP_PASS`, `SMTP_ADMIN_EMAIL`, `SMTP_SENDER_NAME`: required for auth emails (sign-up confirmations, password resets). The template ships with placeholder values, so no real emails are sent until you configure a real SMTP provider. + +## Warning: changing POSTGRES_PASSWORD after the first deploy + +The Postgres data directory (mounted at `files/volumes/db/data`) is initialized **once**, on the first deploy, using the value of `POSTGRES_PASSWORD` at that moment. The same password is also assigned to the internal Supabase roles (`authenticator`, `pgbouncer`, `supabase_auth_admin`, `supabase_functions_admin`, `supabase_storage_admin`) by an init script that only runs on first boot. + +If you later change `POSTGRES_PASSWORD` in the Environment tab and redeploy, the password stored **inside the database does not change**. The other services will start using the new password while the database still expects the old one, and you will see errors such as `invalid_password` or `password authentication failed`. + +To actually change the password, use one of these options: + +### Option A: change it inside the database (keeps your data) + +1. Open a terminal into the `db` container (in Dokploy: your Supabase service, `db` container, **Terminal**) and run `psql -U postgres`. +2. Execute the following, using your new password: + +```sql +ALTER USER postgres WITH PASSWORD 'your-new-password'; +ALTER USER supabase_admin WITH PASSWORD 'your-new-password'; +ALTER USER authenticator WITH PASSWORD 'your-new-password'; +ALTER USER pgbouncer WITH PASSWORD 'your-new-password'; +ALTER USER supabase_auth_admin WITH PASSWORD 'your-new-password'; +ALTER USER supabase_functions_admin WITH PASSWORD 'your-new-password'; +ALTER USER supabase_storage_admin WITH PASSWORD 'your-new-password'; +``` + +3. Update `POSTGRES_PASSWORD` in the Environment tab to the same value and redeploy. + +### Option B: reinitialize the database (deletes ALL data) + +Only if the instance has no data you care about: stop the service, delete the `files/volumes/db/data` directory of the service, set the new `POSTGRES_PASSWORD`, and deploy again. The database will be initialized from scratch with the new password. diff --git a/blueprints/triggerdotdev/instructions.md b/blueprints/triggerdotdev/instructions.md new file mode 100644 index 000000000..2f0415d18 --- /dev/null +++ b/blueprints/triggerdotdev/instructions.md @@ -0,0 +1,54 @@ +# Trigger.dev Setup Instructions + +## Deploy + +1. In Dokploy, create the service from the **Trigger.dev** template. +2. Dokploy generates all required secrets (`MAGIC_LINK_SECRET`, `SESSION_SECRET`, `ENCRYPTION_KEY`, database credentials, etc.) automatically. +3. Deploy and wait until the containers are running. The main domain points to the `webapp` service (port `3000`). + +If you serve the app over HTTPS, set `TRIGGER_PROTOCOL=https` in the **Environment** tab (the template defaults to `http`) so that login links use the correct scheme, then redeploy. + +## First login (no email server configured) + +Trigger.dev logs you in with **magic links** sent by email. The template does not configure an email transport by default, so the email is never actually sent. Instead, **the magic link is printed to the logs of the `webapp` container**: + +1. Open `https://` and enter your email address to request a magic link. +2. In Dokploy, go to your Trigger.dev service, open the **Logs** tab, and select the `webapp` container/service. +3. Look for a recent log entry containing a URL like `.../magic?token=...` (search for `magic`). +4. Copy that URL into your browser to complete the login. + +## Configure email sending (optional) + +To have magic links delivered by email, add these variables in the **Environment** tab and redeploy. + +Using SMTP: + +``` +EMAIL_TRANSPORT=smtp +FROM_EMAIL=trigger@example.com +REPLY_TO_EMAIL=trigger@example.com +SMTP_HOST=smtp.example.com +SMTP_PORT=587 +SMTP_SECURE=false +SMTP_USER=your-smtp-user +SMTP_PASSWORD=your-smtp-password +``` + +Using [Resend](https://resend.com): + +``` +EMAIL_TRANSPORT=resend +FROM_EMAIL=trigger@example.com +REPLY_TO_EMAIL=trigger@example.com +RESEND_API_KEY=your-resend-api-key +``` + +## Restrict who can sign up (recommended) + +By default anyone who can reach the webapp can request a magic link. Restrict access with a regex of allowed email addresses: + +``` +WHITELISTED_EMAILS=you@example\.com|teammate@example\.com +``` + +You can also grant admin rights by email with `ADMIN_EMAILS` (same regex format). From 76839478421ce9066f75efa0d077e7c66bccabd9 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 14 Jul 2026 12:07:41 -0600 Subject: [PATCH 25/80] feat: add Weblate template Co-Authored-By: Claude Fable 5 --- blueprints/weblate/docker-compose.yml | 66 +++++++++++++++++++++++++++ blueprints/weblate/meta.json | 18 ++++++++ blueprints/weblate/template.toml | 26 +++++++++++ blueprints/weblate/weblate.svg | 1 + 4 files changed, 111 insertions(+) create mode 100644 blueprints/weblate/docker-compose.yml create mode 100644 blueprints/weblate/meta.json create mode 100644 blueprints/weblate/template.toml create mode 100644 blueprints/weblate/weblate.svg diff --git a/blueprints/weblate/docker-compose.yml b/blueprints/weblate/docker-compose.yml new file mode 100644 index 000000000..f48a7cb34 --- /dev/null +++ b/blueprints/weblate/docker-compose.yml @@ -0,0 +1,66 @@ +services: + weblate: + image: weblate/weblate:2026.7.1.1 + restart: unless-stopped + depends_on: + database: + condition: service_healthy + cache: + condition: service_healthy + volumes: + - weblate-data:/app/data + - weblate-cache:/app/cache + tmpfs: + - /run + - /tmp + environment: + WEBLATE_SITE_DOMAIN: ${WEBLATE_SITE_DOMAIN} + WEBLATE_SITE_TITLE: ${WEBLATE_SITE_TITLE} + WEBLATE_ADMIN_NAME: ${WEBLATE_ADMIN_NAME} + WEBLATE_ADMIN_EMAIL: ${WEBLATE_ADMIN_EMAIL} + WEBLATE_ADMIN_PASSWORD: ${WEBLATE_ADMIN_PASSWORD} + WEBLATE_SERVER_EMAIL: ${WEBLATE_SERVER_EMAIL} + WEBLATE_DEFAULT_FROM_EMAIL: ${WEBLATE_DEFAULT_FROM_EMAIL} + WEBLATE_ENABLE_HTTPS: ${WEBLATE_ENABLE_HTTPS} + WEBLATE_ALLOWED_HOSTS: "*" + WEBLATE_REGISTRATION_OPEN: ${WEBLATE_REGISTRATION_OPEN} + POSTGRES_HOST: database + POSTGRES_PORT: 5432 + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DB} + REDIS_HOST: cache + REDIS_PORT: 6379 + + database: + image: postgres:18-alpine + restart: unless-stopped + volumes: + - weblate-postgres-data:/var/lib/postgresql + environment: + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DB} + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] + interval: 10s + timeout: 5s + retries: 5 + + cache: + image: redis:8-alpine + restart: unless-stopped + command: ["redis-server", "--save", "60", "1", "--loglevel", "warning"] + volumes: + - weblate-redis-data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + +volumes: + weblate-data: + weblate-cache: + weblate-postgres-data: + weblate-redis-data: diff --git a/blueprints/weblate/meta.json b/blueprints/weblate/meta.json new file mode 100644 index 000000000..1d2fe31f2 --- /dev/null +++ b/blueprints/weblate/meta.json @@ -0,0 +1,18 @@ +{ + "id": "weblate", + "name": "Weblate", + "version": "2026.7.1.1", + "description": "Weblate is a libre web-based continuous localization platform with tight version control integration, quality checks, and support for over 50 file formats including PO, XLIFF, JSON, and Android resources.", + "logo": "weblate.svg", + "links": { + "github": "https://github.com/WeblateOrg/weblate", + "website": "https://weblate.org/", + "docs": "https://docs.weblate.org/en/latest/admin/install/docker.html" + }, + "tags": [ + "localization", + "translation", + "i18n", + "self-hosted" + ] +} diff --git a/blueprints/weblate/template.toml b/blueprints/weblate/template.toml new file mode 100644 index 000000000..f960b5812 --- /dev/null +++ b/blueprints/weblate/template.toml @@ -0,0 +1,26 @@ +[variables] +main_domain = "${domain}" +admin_password = "${password:32}" +postgres_password = "${password:32}" + +[config] +env = [ + "WEBLATE_SITE_DOMAIN=${main_domain}", + "WEBLATE_SITE_TITLE=Weblate", + "WEBLATE_ADMIN_NAME=Weblate Admin", + "WEBLATE_ADMIN_EMAIL=admin@${main_domain}", + "WEBLATE_ADMIN_PASSWORD=${admin_password}", + "WEBLATE_SERVER_EMAIL=weblate@${main_domain}", + "WEBLATE_DEFAULT_FROM_EMAIL=weblate@${main_domain}", + "WEBLATE_ENABLE_HTTPS=0", + "WEBLATE_REGISTRATION_OPEN=1", + "POSTGRES_USER=weblate", + "POSTGRES_PASSWORD=${postgres_password}", + "POSTGRES_DB=weblate", +] +mounts = [] + +[[config.domains]] +serviceName = "weblate" +port = 8080 +host = "${main_domain}" diff --git a/blueprints/weblate/weblate.svg b/blueprints/weblate/weblate.svg new file mode 100644 index 000000000..b8ed62fe0 --- /dev/null +++ b/blueprints/weblate/weblate.svg @@ -0,0 +1 @@ + \ No newline at end of file From b23ddd0b9890df22b3e4b77d44049a94443673e2 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 14 Jul 2026 12:12:24 -0600 Subject: [PATCH 26/80] feat: add Synapse (Matrix homeserver) template Co-Authored-By: Claude Fable 5 --- blueprints/synapse/docker-compose.yml | 51 +++++++++++++++ blueprints/synapse/instructions.md | 39 +++++++++++ blueprints/synapse/meta.json | 16 +++++ blueprints/synapse/synapse.svg | 5 ++ blueprints/synapse/template.toml | 93 +++++++++++++++++++++++++++ 5 files changed, 204 insertions(+) create mode 100644 blueprints/synapse/docker-compose.yml create mode 100644 blueprints/synapse/instructions.md create mode 100644 blueprints/synapse/meta.json create mode 100644 blueprints/synapse/synapse.svg create mode 100644 blueprints/synapse/template.toml diff --git a/blueprints/synapse/docker-compose.yml b/blueprints/synapse/docker-compose.yml new file mode 100644 index 000000000..ee9c0bd8e --- /dev/null +++ b/blueprints/synapse/docker-compose.yml @@ -0,0 +1,51 @@ +services: + synapse: + image: matrixdotorg/synapse:v1.156.0 + restart: unless-stopped + # The stock entrypoint (/start.py) does not generate the signing key when a + # config file is provided, so generate any missing keys on first boot and + # make sure the data volume is owned by the synapse user (991) before + # handing over to the regular entrypoint. + entrypoint: + - /bin/sh + - -c + - | + python -m synapse.app.homeserver --config-path /config/homeserver.yaml --keys-directory /data --generate-keys + chown -R 991:991 /data + exec /start.py + environment: + SYNAPSE_CONFIG_PATH: /config/homeserver.yaml + volumes: + - synapse-data:/data + - ../files/homeserver.yaml:/config/homeserver.yaml:ro + - ../files/log.config:/config/log.config:ro + depends_on: + postgres: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "curl -fSs http://localhost:8008/health || exit 1"] + interval: 15s + timeout: 5s + retries: 10 + start_period: 90s + + postgres: + image: postgres:16-alpine + restart: unless-stopped + environment: + POSTGRES_USER: synapse + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: synapse + # Synapse requires a database with C collation + POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --lc-collate=C --lc-ctype=C" + volumes: + - postgres-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U synapse -d synapse"] + interval: 10s + timeout: 5s + retries: 5 + +volumes: + synapse-data: + postgres-data: diff --git a/blueprints/synapse/instructions.md b/blueprints/synapse/instructions.md new file mode 100644 index 000000000..91b308a22 --- /dev/null +++ b/blueprints/synapse/instructions.md @@ -0,0 +1,39 @@ +## Instructions + +Synapse is deployed with public registration disabled, so you need to create the +first user (your admin account) from the command line. + +### Create the first user + +Open a terminal on the server running Dokploy (or use the container terminal in +the Dokploy UI) and run the following inside the `synapse` service container: + +```bash +docker exec -it $(docker ps -qf "name=synapse" | head -n 1) \ + register_new_matrix_user http://localhost:8008 -c /config/homeserver.yaml +``` + +The tool prompts for a username and password, and asks whether the user should +be an admin. It authenticates against the server using the +`registration_shared_secret` that was generated for this deployment (stored in +the mounted `homeserver.yaml`), so no registration needs to be enabled. + +### Log in + +Point any Matrix client (for example Element Web at https://app.element.io) at +your homeserver URL `https://your-domain` and log in with the user you just +created. You can verify the server is up with: + +```bash +curl https://your-domain/_matrix/client/versions +``` + +### Notes + +- Data (signing keys, media uploads) is stored in the `synapse-data` volume and + the database in the `postgres-data` volume. +- Federation with other Matrix servers works through `.well-known` delegation + over HTTPS (port 443). The dedicated federation port 8448 is not exposed. +- To allow open registration instead, edit `enable_registration` in the mounted + `homeserver.yaml` (see Synapse docs for the required spam-check options such + as CAPTCHA or email verification). diff --git a/blueprints/synapse/meta.json b/blueprints/synapse/meta.json new file mode 100644 index 000000000..bd4dbd1fd --- /dev/null +++ b/blueprints/synapse/meta.json @@ -0,0 +1,16 @@ +{ + "id": "synapse", + "name": "Synapse", + "version": "v1.156.0", + "description": "Synapse is the reference homeserver implementation for Matrix, an open standard for secure, decentralised, real-time communication.", + "logo": "synapse.svg", + "links": { + "github": "https://github.com/element-hq/synapse", + "website": "https://element-hq.github.io/synapse/latest/", + "docs": "https://element-hq.github.io/synapse/latest/setup/installation.html" + }, + "tags": [ + "matrix", + "communication" + ] +} diff --git a/blueprints/synapse/synapse.svg b/blueprints/synapse/synapse.svg new file mode 100644 index 000000000..d2ab11dbc --- /dev/null +++ b/blueprints/synapse/synapse.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/blueprints/synapse/template.toml b/blueprints/synapse/template.toml new file mode 100644 index 000000000..f87bcdbbe --- /dev/null +++ b/blueprints/synapse/template.toml @@ -0,0 +1,93 @@ +[variables] +main_domain = "${domain}" +postgres_password = "${password:32}" +registration_shared_secret = "${password:32}" +macaroon_secret_key = "${password:32}" +form_secret = "${password:32}" + +[config] +env = [ + "POSTGRES_PASSWORD=${postgres_password}", +] + +[[config.domains]] +serviceName = "synapse" +port = 8008 +host = "${main_domain}" + +[[config.mounts]] +filePath = "homeserver.yaml" +content = """ +# Synapse homeserver configuration +# https://element-hq.github.io/synapse/latest/usage/configuration/config_documentation.html + +server_name: "${main_domain}" +public_baseurl: "https://${main_domain}/" +pid_file: /data/homeserver.pid +signing_key_path: "/data/signing.key" +log_config: "/config/log.config" +media_store_path: /data/media_store +report_stats: false + +# Serve /.well-known/matrix/server so other homeservers can federate with +# this server over port 443 (the dedicated federation port 8448 is not exposed). +serve_server_wellknown: true + +listeners: + - port: 8008 + tls: false + type: http + x_forwarded: true + bind_addresses: ['0.0.0.0'] + resources: + - names: [client, federation] + compress: false + +database: + name: psycopg2 + args: + user: synapse + password: "${postgres_password}" + database: synapse + host: postgres + port: 5432 + cp_min: 5 + cp_max: 10 + +# Public registration is disabled. Create users with the +# register_new_matrix_user tool (see the template instructions). +enable_registration: false +registration_shared_secret: "${registration_shared_secret}" + +macaroon_secret_key: "${macaroon_secret_key}" +form_secret: "${form_secret}" + +trusted_key_servers: + - server_name: "matrix.org" +suppress_key_server_warning: true +""" + +[[config.mounts]] +filePath = "log.config" +content = """ +version: 1 + +formatters: + precise: + format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s' + +handlers: + console: + class: logging.StreamHandler + formatter: precise + +loggers: + synapse.storage.SQL: + level: INFO + +root: + level: INFO + handlers: [console] + +disable_existing_loggers: false +""" From 1c2dea39c35548e417d918d9d56c4eec883c2eb1 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 14 Jul 2026 12:29:15 -0600 Subject: [PATCH 27/80] feat: add Dify template Dify 1.15.0 (open-source LLM app development platform): api + worker + worker_beat + web + pgvector PostgreSQL (also used as the vector store) + redis + sandbox + ssrf_proxy (squid) + plugin_daemon, fronted by an internal nginx gateway that mirrors the upstream path routing. Closes #88 Co-Authored-By: Claude Fable 5 --- blueprints/dify/docker-compose.yml | 256 +++++++++++++++++++++++++++++ blueprints/dify/instructions.md | 15 ++ blueprints/dify/logo.svg | 12 ++ blueprints/dify/meta.json | 17 ++ blueprints/dify/template.toml | 168 +++++++++++++++++++ 5 files changed, 468 insertions(+) create mode 100644 blueprints/dify/docker-compose.yml create mode 100644 blueprints/dify/instructions.md create mode 100644 blueprints/dify/logo.svg create mode 100644 blueprints/dify/meta.json create mode 100644 blueprints/dify/template.toml diff --git a/blueprints/dify/docker-compose.yml b/blueprints/dify/docker-compose.yml new file mode 100644 index 000000000..6c89415ba --- /dev/null +++ b/blueprints/dify/docker-compose.yml @@ -0,0 +1,256 @@ +# Dify - open-source LLM app development platform +# Adapted from the official docker/docker-compose.yaml (tag 1.15.0). +# Minimal functional stack: api + worker + worker_beat + web + postgres +# (pgvector, reused as the vector store) + redis + sandbox + ssrf_proxy +# + plugin_daemon, fronted by a lightweight internal nginx gateway that +# mirrors the upstream path routing (TLS terminates at Traefik). + +x-shared-api-env: &shared-api-env + LOG_LEVEL: INFO + DEPLOY_ENV: PRODUCTION + SECRET_KEY: ${SECRET_KEY} + MIGRATION_ENABLED: "true" + # Public URLs - a single domain, path-routed like the upstream nginx + CONSOLE_API_URL: https://${DIFY_DOMAIN} + CONSOLE_WEB_URL: https://${DIFY_DOMAIN} + SERVICE_API_URL: https://${DIFY_DOMAIN} + APP_API_URL: https://${DIFY_DOMAIN} + APP_WEB_URL: https://${DIFY_DOMAIN} + FILES_URL: https://${DIFY_DOMAIN} + INTERNAL_FILES_URL: http://api:5001 + TRIGGER_URL: https://${DIFY_DOMAIN} + ENDPOINT_URL_TEMPLATE: https://${DIFY_DOMAIN}/e/{hook_id} + # The dedicated api_websocket collaboration service is not deployed + ENABLE_COLLABORATION_MODE: "false" + WEB_API_CORS_ALLOW_ORIGINS: "*" + CONSOLE_CORS_ALLOW_ORIGINS: "*" + # PostgreSQL + DB_USERNAME: postgres + DB_PASSWORD: ${POSTGRES_PASSWORD} + DB_HOST: db + DB_PORT: "5432" + DB_DATABASE: dify + # Redis / Celery + REDIS_HOST: redis + REDIS_PORT: "6379" + REDIS_PASSWORD: ${REDIS_PASSWORD} + REDIS_DB: "0" + CELERY_BROKER_URL: redis://:${REDIS_PASSWORD}@redis:6379/1 + # File storage on a local volume + STORAGE_TYPE: opendal + OPENDAL_SCHEME: fs + OPENDAL_FS_ROOT: storage + # Vector store: pgvector, reusing the same PostgreSQL instance + VECTOR_STORE: pgvector + PGVECTOR_HOST: db + PGVECTOR_PORT: "5432" + PGVECTOR_USER: postgres + PGVECTOR_PASSWORD: ${POSTGRES_PASSWORD} + PGVECTOR_DATABASE: dify + PGVECTOR_MIN_CONNECTION: "1" + PGVECTOR_MAX_CONNECTION: "5" + # Code execution sandbox + CODE_EXECUTION_ENDPOINT: http://sandbox:8194 + CODE_EXECUTION_API_KEY: ${SANDBOX_API_KEY} + # SSRF proxy (squid) for user-triggered outbound requests + SSRF_PROXY_HTTP_URL: http://ssrf_proxy:3128 + SSRF_PROXY_HTTPS_URL: http://ssrf_proxy:3128 + # Plugin daemon (model providers / tools are plugins since Dify 1.0) + PLUGIN_DAEMON_URL: http://plugin_daemon:5002 + PLUGIN_DAEMON_KEY: ${PLUGIN_DAEMON_KEY} + INNER_API_KEY_FOR_PLUGIN: ${PLUGIN_INNER_API_KEY} + PLUGIN_MAX_PACKAGE_SIZE: "52428800" + PLUGIN_REMOTE_INSTALL_HOST: localhost + PLUGIN_REMOTE_INSTALL_PORT: "5003" + MARKETPLACE_ENABLED: "true" + MARKETPLACE_API_URL: https://marketplace.dify.ai + +services: + nginx: + image: nginx:1.27-alpine + restart: always + volumes: + - ../files/dify-nginx.conf:/etc/nginx/conf.d/default.conf:ro + depends_on: + api: + condition: service_healthy + web: + condition: service_started + plugin_daemon: + condition: service_started + + # The dify-api image runs as the non-root user 1001, so the shared + # storage volume has to be handed over to it once (same trick as the + # upstream init_permissions container). + init_permissions: + image: busybox:1.36 + command: + - sh + - -c + - | + FLAG_FILE="/app/api/storage/.init_permissions" + if [ -f "$$FLAG_FILE" ]; then + echo "Permissions already initialized." + exit 0 + fi + echo "Initializing permissions for /app/api/storage" + chown -R 1001:1001 /app/api/storage && touch "$$FLAG_FILE" + volumes: + - dify-app-storage:/app/api/storage + restart: "no" + + api: + image: langgenius/dify-api:1.15.0 + restart: always + environment: + <<: *shared-api-env + MODE: api + depends_on: + init_permissions: + condition: service_completed_successfully + db: + condition: service_healthy + redis: + condition: service_healthy + volumes: + - dify-app-storage:/app/api/storage + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:5001/health"] + interval: 15s + timeout: 5s + retries: 20 + start_period: 120s + + worker: + image: langgenius/dify-api:1.15.0 + restart: always + environment: + <<: *shared-api-env + MODE: worker + depends_on: + init_permissions: + condition: service_completed_successfully + db: + condition: service_healthy + redis: + condition: service_healthy + volumes: + - dify-app-storage:/app/api/storage + + worker_beat: + image: langgenius/dify-api:1.15.0 + restart: always + environment: + <<: *shared-api-env + MODE: beat + depends_on: + db: + condition: service_healthy + redis: + condition: service_healthy + + web: + image: langgenius/dify-web:1.15.0 + restart: always + environment: + CONSOLE_API_URL: https://${DIFY_DOMAIN} + APP_API_URL: https://${DIFY_DOMAIN} + SERVER_CONSOLE_API_URL: http://api:5001 + ENABLE_COLLABORATION_MODE: "false" + MARKETPLACE_API_URL: https://marketplace.dify.ai + MARKETPLACE_URL: https://marketplace.dify.ai + NEXT_TELEMETRY_DISABLED: "1" + + db: + image: pgvector/pgvector:pg16 + restart: always + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: dify + PGDATA: /var/lib/postgresql/data/pgdata + volumes: + - dify-db-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres -d dify"] + interval: 5s + timeout: 3s + retries: 30 + + redis: + image: redis:6-alpine + restart: always + environment: + REDISCLI_AUTH: ${REDIS_PASSWORD} + command: redis-server --requirepass ${REDIS_PASSWORD} + volumes: + - dify-redis-data:/data + healthcheck: + test: ["CMD-SHELL", "redis-cli ping | grep -q PONG"] + interval: 5s + timeout: 3s + retries: 30 + + sandbox: + image: langgenius/dify-sandbox:0.2.15 + restart: always + environment: + API_KEY: ${SANDBOX_API_KEY} + GIN_MODE: release + WORKER_TIMEOUT: "15" + ENABLE_NETWORK: "true" + HTTP_PROXY: http://ssrf_proxy:3128 + HTTPS_PROXY: http://ssrf_proxy:3128 + SANDBOX_PORT: "8194" + volumes: + - dify-sandbox-deps:/dependencies + + ssrf_proxy: + image: ubuntu/squid:latest + restart: always + volumes: + - ../files/dify-squid.conf:/etc/squid/squid.conf:ro + + plugin_daemon: + image: langgenius/dify-plugin-daemon:0.6.3-local + restart: always + environment: + DB_HOST: db + DB_PORT: "5432" + DB_USERNAME: postgres + DB_PASSWORD: ${POSTGRES_PASSWORD} + DB_DATABASE: dify_plugin + DB_SSL_MODE: disable + REDIS_HOST: redis + REDIS_PORT: "6379" + REDIS_PASSWORD: ${REDIS_PASSWORD} + SERVER_PORT: "5002" + SERVER_KEY: ${PLUGIN_DAEMON_KEY} + MAX_PLUGIN_PACKAGE_SIZE: "52428800" + DIFY_INNER_API_URL: http://api:5001 + DIFY_INNER_API_KEY: ${PLUGIN_INNER_API_KEY} + PLUGIN_REMOTE_INSTALLING_HOST: 0.0.0.0 + PLUGIN_REMOTE_INSTALLING_PORT: "5003" + PLUGIN_WORKING_PATH: /app/storage/cwd + FORCE_VERIFYING_SIGNATURE: "true" + PYTHON_ENV_INIT_TIMEOUT: "120" + PLUGIN_MAX_EXECUTION_TIMEOUT: "600" + PLUGIN_STORAGE_TYPE: local + PLUGIN_STORAGE_LOCAL_ROOT: /app/storage + PLUGIN_INSTALLED_PATH: plugin + PLUGIN_PACKAGE_CACHE_PATH: plugin_packages + PLUGIN_MEDIA_CACHE_PATH: assets + volumes: + - dify-plugin-storage:/app/storage + depends_on: + db: + condition: service_healthy + redis: + condition: service_healthy + +volumes: + dify-app-storage: + dify-db-data: + dify-redis-data: + dify-sandbox-deps: + dify-plugin-storage: diff --git a/blueprints/dify/instructions.md b/blueprints/dify/instructions.md new file mode 100644 index 000000000..f554f671d --- /dev/null +++ b/blueprints/dify/instructions.md @@ -0,0 +1,15 @@ +## Instructions + +### First setup + +- The first boot runs the database migrations, so it can take **2-4 minutes** before the application responds. +- Open your domain: you will be redirected to `/install`, where you create the admin (workspace owner) account. +- After signing in, go to **Settings -> Model Provider** and install a model provider plugin (OpenAI, Anthropic, Ollama, etc.) from the marketplace, then configure your API keys. + +### Notes + +- **HTTPS is required for the console login**: Dify issues `__Host-`/`Secure` session cookies, so make sure the domain has a certificate (e.g. Let's Encrypt in the domain settings). Over plain HTTP the browser will drop the session cookies and login will not persist. + +- The vector store is **pgvector**, running inside the bundled PostgreSQL instance (no separate Weaviate/Qdrant container needed). +- All routing (`/console/api`, `/api`, `/v1`, `/files`, `/e/`, ...) is handled by the bundled internal nginx gateway, mirroring the upstream deployment, so the Dify service API is available at `https:///v1`. +- Code execution runs in the isolated `sandbox` service, and user-triggered outbound HTTP requests go through the `ssrf_proxy` (squid) service, like in the official deployment. diff --git a/blueprints/dify/logo.svg b/blueprints/dify/logo.svg new file mode 100644 index 000000000..52ed710f5 --- /dev/null +++ b/blueprints/dify/logo.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/blueprints/dify/meta.json b/blueprints/dify/meta.json new file mode 100644 index 000000000..170013dda --- /dev/null +++ b/blueprints/dify/meta.json @@ -0,0 +1,17 @@ +{ + "id": "dify", + "name": "Dify", + "version": "1.15.0", + "description": "Dify is an open-source LLM app development platform. Build AI workflows, RAG pipelines, agents and chatbots with a visual interface and publish them as APIs or web apps.", + "logo": "logo.svg", + "links": { + "github": "https://github.com/langgenius/dify", + "website": "https://dify.ai/", + "docs": "https://docs.dify.ai/" + }, + "tags": [ + "ai", + "llm", + "rag" + ] +} diff --git a/blueprints/dify/template.toml b/blueprints/dify/template.toml new file mode 100644 index 000000000..ded696fb7 --- /dev/null +++ b/blueprints/dify/template.toml @@ -0,0 +1,168 @@ +[variables] +main_domain = "${domain}" +secret_key = "${base64:42}" +postgres_password = "${password:32}" +redis_password = "${password:32}" +sandbox_api_key = "${password:32}" +plugin_daemon_key = "${base64:42}" +plugin_inner_api_key = "${base64:42}" + +[config] +env = [ + "DIFY_DOMAIN=${main_domain}", + "SECRET_KEY=${secret_key}", + "POSTGRES_PASSWORD=${postgres_password}", + "REDIS_PASSWORD=${redis_password}", + "SANDBOX_API_KEY=${sandbox_api_key}", + "PLUGIN_DAEMON_KEY=${plugin_daemon_key}", + "PLUGIN_INNER_API_KEY=${plugin_inner_api_key}", +] + +[[config.domains]] +serviceName = "nginx" +port = 80 +host = "${main_domain}" + +# Internal nginx gateway replicating the upstream Dify nginx routing +# (TLS terminates at Traefik, which forwards everything to this nginx). +[[config.mounts]] +filePath = "dify-nginx.conf" +content = ''' +map $http_x_forwarded_proto $dify_forwarded_proto { + default $http_x_forwarded_proto; + "" $scheme; +} + +server { + listen 80; + server_name _; + + client_max_body_size 100M; + + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $dify_forwarded_proto; + proxy_http_version 1.1; + proxy_set_header Connection ""; + proxy_buffering off; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + + location /console/api { proxy_pass http://api:5001; } + location /api { proxy_pass http://api:5001; } + location /v1 { proxy_pass http://api:5001; } + location /openapi { proxy_pass http://api:5001; } + location /files { proxy_pass http://api:5001; } + location /mcp { proxy_pass http://api:5001; } + location /triggers { proxy_pass http://api:5001; } + location /explore { proxy_pass http://web:3000; } + + location /e/ { + proxy_pass http://plugin_daemon:5002; + # nginx drops inherited proxy_set_header directives as soon as a + # location defines its own, so the shared ones are repeated here. + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $dify_forwarded_proto; + proxy_set_header Connection ""; + proxy_set_header Dify-Hook-Url $dify_forwarded_proto://$host$request_uri; + } + + location / { proxy_pass http://web:3000; } +} +''' + +# Squid SSRF proxy configuration, rendered from the upstream +# docker/ssrf_proxy/squid.conf.template with its default values. +[[config.mounts]] +filePath = "dify-squid.conf" +content = ''' +acl client_localnet src 0.0.0.1-0.255.255.255 +acl client_localnet src 10.0.0.0/8 +acl client_localnet src 100.64.0.0/10 +acl client_localnet src 169.254.0.0/16 +acl client_localnet src 172.16.0.0/12 +acl client_localnet src 192.168.0.0/16 +acl client_localnet src fc00::/7 +acl client_localnet src fe80::/10 +acl to_private_networks dst 0.0.0.0/8 +acl to_private_networks dst 10.0.0.0/8 +acl to_private_networks dst 100.64.0.0/10 +acl to_private_networks dst 127.0.0.0/8 +acl to_private_networks dst 169.254.0.0/16 +acl to_private_networks dst 172.16.0.0/12 +acl to_private_networks dst 192.168.0.0/16 +acl to_private_networks dst 224.0.0.0/4 +acl to_private_networks dst 240.0.0.0/4 +acl to_private_networks dst ::/128 +acl to_private_networks dst ::1/128 +acl to_private_networks dst ::ffff:0:0/96 +acl to_private_networks dst ::/96 +acl to_private_networks dst fc00::/7 +acl to_private_networks dst fe80::/10 +acl SSL_ports port 443 +acl Safe_ports port 80 +acl Safe_ports port 21 +acl Safe_ports port 443 +acl Safe_ports port 70 +acl Safe_ports port 210 +acl Safe_ports port 1025-65535 +acl Safe_ports port 280 +acl Safe_ports port 488 +acl Safe_ports port 591 +acl Safe_ports port 777 +acl CONNECT method CONNECT +acl allowed_domains dstdomain .marketplace.dify.ai + +http_port 3128 + +http_access deny !Safe_ports +http_access deny CONNECT !SSL_ports +http_access allow localhost manager +http_access deny manager +http_access deny to_private_networks +http_access allow allowed_domains +http_access allow client_localnet +http_access allow localhost +http_access deny all +tcp_outgoing_address 0.0.0.0 + +coredump_dir /var/spool/squid +refresh_pattern ^ftp: 1440 20% 10080 +refresh_pattern ^gopher: 1440 0% 1440 +refresh_pattern -i (/cgi-bin/|\?) 0 0% 0 +refresh_pattern . 0 20% 4320 + +client_request_buffer_max_size 100 MB +max_filedescriptors 65536 + +connect_timeout 30 seconds +request_timeout 2 minutes +read_timeout 2 minutes +client_lifetime 5 minutes +shutdown_lifetime 30 seconds + +server_persistent_connections on +client_persistent_connections on +persistent_request_timeout 30 seconds +pconn_timeout 1 minute + +client_db on +server_idle_pconn_timeout 2 minutes +client_idle_pconn_timeout 2 minutes + +quick_abort_min 16 KB +quick_abort_max 16 MB +quick_abort_pct 95 + +memory_cache_mode disk +cache_mem 256 MB +maximum_object_size_in_memory 512 KB + +dns_timeout 30 seconds +dns_retransmit_interval 5 seconds + +logformat dify_log %ts.%03tu %6tr %>a %Ss/%03>Hs % Date: Tue, 14 Jul 2026 12:41:18 -0600 Subject: [PATCH 28/80] feat: add Sim Studio template Co-Authored-By: Claude Fable 5 --- blueprints/sim/docker-compose.yml | 76 +++++++++++++++++++++++++++++++ blueprints/sim/meta.json | 17 +++++++ blueprints/sim/sim.svg | 1 + blueprints/sim/template.toml | 28 ++++++++++++ 4 files changed, 122 insertions(+) create mode 100644 blueprints/sim/docker-compose.yml create mode 100644 blueprints/sim/meta.json create mode 100644 blueprints/sim/sim.svg create mode 100644 blueprints/sim/template.toml diff --git a/blueprints/sim/docker-compose.yml b/blueprints/sim/docker-compose.yml new file mode 100644 index 000000000..cc920b18d --- /dev/null +++ b/blueprints/sim/docker-compose.yml @@ -0,0 +1,76 @@ +services: + simstudio: + image: ghcr.io/simstudioai/simstudio:latest + restart: unless-stopped + environment: + - NODE_ENV=production + - DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@db:5432/simstudio + - BETTER_AUTH_URL=${SIM_APP_URL} + - NEXT_PUBLIC_APP_URL=${SIM_APP_URL} + - BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET} + - ENCRYPTION_KEY=${ENCRYPTION_KEY} + - API_ENCRYPTION_KEY=${API_ENCRYPTION_KEY} + - INTERNAL_API_SECRET=${INTERNAL_API_SECRET} + - SOCKET_SERVER_URL=http://realtime:3002 + depends_on: + db: + condition: service_healthy + migrations: + condition: service_completed_successfully + realtime: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-fsS", "http://127.0.0.1:3000"] + interval: 30s + timeout: 5s + retries: 5 + start_period: 30s + + realtime: + image: ghcr.io/simstudioai/realtime:latest + restart: unless-stopped + environment: + - NODE_ENV=production + - DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@db:5432/simstudio + - NEXT_PUBLIC_APP_URL=${SIM_APP_URL} + - BETTER_AUTH_URL=${SIM_APP_URL} + - BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET} + - INTERNAL_API_SECRET=${INTERNAL_API_SECRET} + depends_on: + db: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-fsS", "http://127.0.0.1:3002/health"] + interval: 30s + timeout: 5s + retries: 5 + start_period: 15s + + migrations: + image: ghcr.io/simstudioai/migrations:latest + working_dir: /app/packages/db + environment: + - DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@db:5432/simstudio + depends_on: + db: + condition: service_healthy + command: ["bun", "run", "db:migrate"] + restart: "no" + + db: + image: pgvector/pgvector:pg17 + restart: unless-stopped + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - POSTGRES_DB=simstudio + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 5 + +volumes: + postgres_data: diff --git a/blueprints/sim/meta.json b/blueprints/sim/meta.json new file mode 100644 index 000000000..1a7744688 --- /dev/null +++ b/blueprints/sim/meta.json @@ -0,0 +1,17 @@ +{ + "id": "sim", + "name": "Sim", + "version": "latest", + "description": "Open-source visual workflow builder for building and deploying AI agent workflows.", + "logo": "sim.svg", + "links": { + "github": "https://github.com/simstudioai/sim", + "website": "https://sim.ai", + "docs": "https://docs.sim.ai" + }, + "tags": [ + "ai", + "automation", + "workflow" + ] +} diff --git a/blueprints/sim/sim.svg b/blueprints/sim/sim.svg new file mode 100644 index 000000000..d1b94a8d2 --- /dev/null +++ b/blueprints/sim/sim.svg @@ -0,0 +1 @@ + diff --git a/blueprints/sim/template.toml b/blueprints/sim/template.toml new file mode 100644 index 000000000..b61ef6a3c --- /dev/null +++ b/blueprints/sim/template.toml @@ -0,0 +1,28 @@ +[variables] +main_domain = "${domain}" +better_auth_secret = "${password:32}" +encryption_key = "${hash:64}" +api_encryption_key = "${hash:64}" +internal_api_secret = "${password:32}" +postgres_password = "${password:32}" + +[config] + +[config.env] +SIM_APP_URL = "http://${main_domain}" +BETTER_AUTH_SECRET = "${better_auth_secret}" +ENCRYPTION_KEY = "${encryption_key}" +API_ENCRYPTION_KEY = "${api_encryption_key}" +INTERNAL_API_SECRET = "${internal_api_secret}" +POSTGRES_PASSWORD = "${postgres_password}" + +[[config.domains]] +serviceName = "simstudio" +port = 3000 +host = "${main_domain}" + +[[config.domains]] +serviceName = "realtime" +port = 3002 +host = "${main_domain}" +path = "/socket.io" From 62850c36962ffd9d07ef1d024e4ec5190d8759c7 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 14 Jul 2026 13:10:56 -0600 Subject: [PATCH 29/80] feat: add Steedos template Co-Authored-By: Claude Fable 5 --- blueprints/steedos/docker-compose.yml | 13 +++++++++++++ blueprints/steedos/meta.json | 16 ++++++++++++++++ blueprints/steedos/steedos.png | Bin 0 -> 4448 bytes blueprints/steedos/template.toml | 13 +++++++++++++ 4 files changed, 42 insertions(+) create mode 100644 blueprints/steedos/docker-compose.yml create mode 100644 blueprints/steedos/meta.json create mode 100644 blueprints/steedos/steedos.png create mode 100644 blueprints/steedos/template.toml diff --git a/blueprints/steedos/docker-compose.yml b/blueprints/steedos/docker-compose.yml new file mode 100644 index 000000000..0f2637fab --- /dev/null +++ b/blueprints/steedos/docker-compose.yml @@ -0,0 +1,13 @@ +version: "3.8" + +services: + steedos: + image: steedos/steedos-community:3.0.14 + restart: always + environment: + - ROOT_URL=http://${STEEDOS_HOST} + volumes: + - steedos-data:/steedos-storage + +volumes: + steedos-data: diff --git a/blueprints/steedos/meta.json b/blueprints/steedos/meta.json new file mode 100644 index 000000000..baabd0136 --- /dev/null +++ b/blueprints/steedos/meta.json @@ -0,0 +1,16 @@ +{ + "id": "steedos", + "name": "Steedos", + "version": "3.0.14", + "description": "Steedos is an open-source low-code platform and alternative to Salesforce, designed to help you build enterprise applications like CRM with clicks, not code.", + "logo": "steedos.png", + "links": { + "github": "https://github.com/steedos/steedos-platform", + "website": "https://www.steedos.com/", + "docs": "https://docs.steedos.com/" + }, + "tags": [ + "crm", + "low-code" + ] +} diff --git a/blueprints/steedos/steedos.png b/blueprints/steedos/steedos.png new file mode 100644 index 0000000000000000000000000000000000000000..64a306fd57b7bb1cc6bc25ab15276d3e72a84ee2 GIT binary patch literal 4448 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 It9RVr0rr*T-~a#s literal 0 HcmV?d00001 diff --git a/blueprints/steedos/template.toml b/blueprints/steedos/template.toml new file mode 100644 index 000000000..342571dc4 --- /dev/null +++ b/blueprints/steedos/template.toml @@ -0,0 +1,13 @@ +[variables] +main_domain = "${domain}" + +[config] +mounts = [] + +[[config.domains]] +serviceName = "steedos" +port = 80 +host = "${main_domain}" + +[config.env] +STEEDOS_HOST = "${main_domain}" From 0bfb50d62f3fafc87ac24cfafc3d49f5a70d2355 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 14 Jul 2026 13:19:16 -0600 Subject: [PATCH 30/80] feat: add Silex template Co-Authored-By: Claude Fable 5 --- blueprints/silex/docker-compose.yml | 20 ++++++++++++++++++++ blueprints/silex/logo.png | Bin 0 -> 13715 bytes blueprints/silex/meta.json | 17 +++++++++++++++++ blueprints/silex/template.toml | 13 +++++++++++++ 4 files changed, 50 insertions(+) create mode 100644 blueprints/silex/docker-compose.yml create mode 100644 blueprints/silex/logo.png create mode 100644 blueprints/silex/meta.json create mode 100644 blueprints/silex/template.toml diff --git a/blueprints/silex/docker-compose.yml b/blueprints/silex/docker-compose.yml new file mode 100644 index 000000000..bc821dca8 --- /dev/null +++ b/blueprints/silex/docker-compose.yml @@ -0,0 +1,20 @@ +services: + silex: + image: silexlabs/silex:3.7.0 + restart: unless-stopped + ports: + - 6805 + environment: + - SILEX_URL=${SILEX_URL} + - SILEX_SESSION_SECRET=${SILEX_SESSION_SECRET} + - STORAGE_CONNECTORS=fs + - HOSTING_CONNECTORS=fs,download + - SILEX_FS_ROOT=/silex/silex/storage + - SILEX_FS_HOSTING_ROOT=/silex/silex/hosting + volumes: + - silex-storage:/silex/silex/storage + - silex-hosting:/silex/silex/hosting + +volumes: + silex-storage: + silex-hosting: diff --git a/blueprints/silex/logo.png b/blueprints/silex/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..1a6544f9a7b178826fd324210c20d7cae1c46956 GIT binary patch literal 13715 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 Date: Tue, 14 Jul 2026 13:30:17 -0600 Subject: [PATCH 31/80] feat: add SpacetimeDB template Co-Authored-By: Claude Fable 5 --- blueprints/spacetimedb/docker-compose.yml | 13 +++++++ blueprints/spacetimedb/instructions.md | 45 ++++++++++++++++++++++ blueprints/spacetimedb/logo.png | Bin 0 -> 3683 bytes blueprints/spacetimedb/meta.json | 17 ++++++++ blueprints/spacetimedb/template.toml | 8 ++++ 5 files changed, 83 insertions(+) create mode 100644 blueprints/spacetimedb/docker-compose.yml create mode 100644 blueprints/spacetimedb/instructions.md create mode 100644 blueprints/spacetimedb/logo.png create mode 100644 blueprints/spacetimedb/meta.json create mode 100644 blueprints/spacetimedb/template.toml diff --git a/blueprints/spacetimedb/docker-compose.yml b/blueprints/spacetimedb/docker-compose.yml new file mode 100644 index 000000000..05d85869f --- /dev/null +++ b/blueprints/spacetimedb/docker-compose.yml @@ -0,0 +1,13 @@ +version: "3.8" +services: + spacetimedb: + image: clockworklabs/spacetime:v2.6.1 + restart: unless-stopped + command: start + ports: + - 3000 + volumes: + - spacetimedb-data:/home/spacetime + +volumes: + spacetimedb-data: {} diff --git a/blueprints/spacetimedb/instructions.md b/blueprints/spacetimedb/instructions.md new file mode 100644 index 000000000..676ced237 --- /dev/null +++ b/blueprints/spacetimedb/instructions.md @@ -0,0 +1,45 @@ +# SpacetimeDB + +SpacetimeDB is a database and server runtime in one: your clients connect directly to the database and your application logic runs inside it as WebAssembly modules. There is no web UI — the domain assigned to this service exposes the SpacetimeDB HTTP/WebSocket API (so opening `/` in a browser returns a plain 404; that is expected). + +## Verify the server is running + +```bash +curl -i https://your-domain.com/v1/ping +``` + +A `200 OK` response means the server is up. + +## Connect with the SpacetimeDB CLI + +1. Install the CLI on your local machine: + + ```bash + curl -sSf https://install.spacetimedb.com | sh + ``` + +2. Register your Dokploy instance as a server and make it the default: + + ```bash + spacetime server add --url https://your-domain.com dokploy --default + ``` + +3. Log in (or create an anonymous local identity) and publish a module: + + ```bash + spacetime login + spacetime publish --server dokploy my-database + ``` + +4. Check connectivity at any time: + + ```bash + spacetime server ping dokploy + ``` + +From your game or app, connect with any SpacetimeDB client SDK (Rust, C#, TypeScript) using `https://your-domain.com` as the host URI. See the [getting started guide](https://spacetimedb.com/docs/getting-started) for a full walkthrough. + +## Notes + +- All server state (databases, logs and the JWT signing keys used for identities) is persisted in the `spacetimedb-data` volume, so identities survive restarts and upgrades. +- Client connections use WebSockets over the same domain; no extra configuration is needed. diff --git a/blueprints/spacetimedb/logo.png b/blueprints/spacetimedb/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..77099a12a8d094a5dce6bc02c240285743256086 GIT binary patch literal 3683 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 literal 0 HcmV?d00001 diff --git a/blueprints/spacetimedb/meta.json b/blueprints/spacetimedb/meta.json new file mode 100644 index 000000000..824840efa --- /dev/null +++ b/blueprints/spacetimedb/meta.json @@ -0,0 +1,17 @@ +{ + "id": "spacetimedb", + "name": "SpacetimeDB", + "version": "v2.6.1", + "description": "SpacetimeDB is a relational database with a built-in server runtime. Clients connect directly to the database and run server-side logic (modules) inside it, making it ideal for real-time multiplayer games, chat and collaborative applications without a separate backend.", + "logo": "logo.png", + "links": { + "github": "https://github.com/clockworklabs/SpacetimeDB", + "website": "https://spacetimedb.com/", + "docs": "https://spacetimedb.com/docs" + }, + "tags": [ + "database", + "realtime", + "backend" + ] +} diff --git a/blueprints/spacetimedb/template.toml b/blueprints/spacetimedb/template.toml new file mode 100644 index 000000000..35d00d6f9 --- /dev/null +++ b/blueprints/spacetimedb/template.toml @@ -0,0 +1,8 @@ +[variables] +main_domain = "${domain}" + +[config] +[[config.domains]] +serviceName = "spacetimedb" +port = 3000 +host = "${main_domain}" From a266e36a4d6afb33cc61600a5fd3fe24d10176b5 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 14 Jul 2026 13:37:16 -0600 Subject: [PATCH 32/80] feat: add CertMate template Co-Authored-By: Claude Fable 5 --- blueprints/certmate/docker-compose.yml | 33 +++++++++++++++++++++++++ blueprints/certmate/instructions.md | 13 ++++++++++ blueprints/certmate/logo.png | Bin 0 -> 47464 bytes blueprints/certmate/meta.json | 18 ++++++++++++++ blueprints/certmate/template.toml | 20 +++++++++++++++ 5 files changed, 84 insertions(+) create mode 100644 blueprints/certmate/docker-compose.yml create mode 100644 blueprints/certmate/instructions.md create mode 100644 blueprints/certmate/logo.png create mode 100644 blueprints/certmate/meta.json create mode 100644 blueprints/certmate/template.toml diff --git a/blueprints/certmate/docker-compose.yml b/blueprints/certmate/docker-compose.yml new file mode 100644 index 000000000..d7dde9dcb --- /dev/null +++ b/blueprints/certmate/docker-compose.yml @@ -0,0 +1,33 @@ +version: "3.8" + +services: + certmate: + image: fabriziosalmi/certmate:2.21.3 + environment: + - FLASK_ENV=production + - SECRET_KEY=${SECRET_KEY} # 🔐 Flask session secret + - API_BEARER_TOKEN=${API_BEARER_TOKEN} # 🔐 API / web UI token + - BEHIND_PROXY=${BEHIND_PROXY} + - CLOUDFLARE_TOKEN=${CLOUDFLARE_TOKEN} + - LETSENCRYPT_EMAIL=${LETSENCRYPT_EMAIL} + - LOG_LEVEL=${LOG_LEVEL} + ports: + - 8000 + volumes: + - certmate_certificates:/app/certificates + - certmate_data:/app/data + - certmate_logs:/app/logs + - certmate_backups:/app/backups + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + +volumes: + certmate_certificates: + certmate_data: + certmate_logs: + certmate_backups: diff --git a/blueprints/certmate/instructions.md b/blueprints/certmate/instructions.md new file mode 100644 index 000000000..6a048e3ca --- /dev/null +++ b/blueprints/certmate/instructions.md @@ -0,0 +1,13 @@ +# CertMate + +## Getting started + +1. Deploy the template and open the app domain. +2. Log in with the **API bearer token**: it is auto-generated by the template and stored in the `API_BEARER_TOKEN` environment variable (Dokploy → your service → Environment). The same token authenticates REST API requests (`Authorization: Bearer `). +3. In **Settings**, set your Let's Encrypt email and add credentials for your DNS provider (Cloudflare, Route53, etc.) to issue certificates through DNS-01 challenges. + +## Notes + +- `CLOUDFLARE_TOKEN` can optionally be set as an environment variable instead of configuring it in the UI. +- Certificates, application data and backups are persisted in the `certmate_certificates`, `certmate_data` and `certmate_backups` volumes. +- API documentation is available at `/docs/` (Swagger) and `/redoc/` on your domain. diff --git a/blueprints/certmate/logo.png b/blueprints/certmate/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..1a7b34185024bf3d52a8d020f62bf217a695d7d1 GIT binary patch literal 47464 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 HEyDi~$kY6g literal 0 HcmV?d00001 diff --git a/blueprints/certmate/meta.json b/blueprints/certmate/meta.json new file mode 100644 index 000000000..f8be746c6 --- /dev/null +++ b/blueprints/certmate/meta.json @@ -0,0 +1,18 @@ +{ + "id": "certmate", + "name": "CertMate", + "version": "2.21.3", + "description": "CertMate is an SSL certificate management system with a web UI and REST API. It automates issuing and renewing Let's Encrypt certificates via DNS-01 challenges across 20+ DNS providers, with unified backups and multi-account support.", + "logo": "logo.png", + "links": { + "github": "https://github.com/fabriziosalmi/certmate", + "website": "https://www.certmate.org/", + "docs": "https://www.certmate.org/docs/" + }, + "tags": [ + "ssl", + "certificates", + "security", + "self-hosted" + ] +} diff --git a/blueprints/certmate/template.toml b/blueprints/certmate/template.toml new file mode 100644 index 000000000..48d6228d4 --- /dev/null +++ b/blueprints/certmate/template.toml @@ -0,0 +1,20 @@ +[variables] +main_domain = "${domain}" +SECRET_KEY = "${password:32}" +API_BEARER_TOKEN = "${password:32}" + +[config] +mounts = [] + +[[config.domains]] +serviceName = "certmate" +port = 8000 +host = "${main_domain}" + +[config.env] +SECRET_KEY = "${SECRET_KEY}" # 🔐 Flask session secret +API_BEARER_TOKEN = "${API_BEARER_TOKEN}" # 🔐 API / web UI token +BEHIND_PROXY = "true" +CLOUDFLARE_TOKEN = "" +LETSENCRYPT_EMAIL = "" +LOG_LEVEL = "INFO" From 7f3a08eb00067deda2e200c612e5dac639c94c58 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 14 Jul 2026 13:43:38 -0600 Subject: [PATCH 33/80] feat: add PostHog template Based on the official hobby deployment, trimmed to run on a single server: Django web + Celery worker, Node plugin-server (CDP, ingestion, session replay, recording API), Rust capture/replay-capture/ feature-flags/hypercache, livestream, Caddy path router, Postgres, Redis, Redpanda (Kafka), ZooKeeper, ClickHouse and MinIO. Images are pinned to a tested master commit (PostHog no longer publishes tagged releases); an init container fetches the matching ClickHouse configuration, Kafka table schemas, funnel UDF binaries and the GeoIP database at the same pinned ref. Co-Authored-By: Claude Fable 5 --- blueprints/posthog/docker-compose.yml | 634 ++++++++++++++++++++++++++ blueprints/posthog/instructions.md | 27 ++ blueprints/posthog/meta.json | 19 + blueprints/posthog/posthog.svg | 10 + blueprints/posthog/template.toml | 73 +++ 5 files changed, 763 insertions(+) create mode 100644 blueprints/posthog/docker-compose.yml create mode 100644 blueprints/posthog/instructions.md create mode 100644 blueprints/posthog/meta.json create mode 100644 blueprints/posthog/posthog.svg create mode 100644 blueprints/posthog/template.toml diff --git a/blueprints/posthog/docker-compose.yml b/blueprints/posthog/docker-compose.yml new file mode 100644 index 000000000..b5addc40d --- /dev/null +++ b/blueprints/posthog/docker-compose.yml @@ -0,0 +1,634 @@ +# PostHog self-hosted ("hobby" deployment, trimmed for a single VPS). +# Based on https://github.com/PostHog/posthog/blob/master/docker-compose.hobby.yml +# Omitted vs upstream hobby (heavy/optional): Temporal + Elasticsearch (data +# warehouse / batch exports), browserless (image exports / heatmap screenshots), +# personhog, cymbal + error-tracking/logs/traces ingestion, capture-ai and +# capture-logs. Everything needed for product analytics, feature flags, +# surveys, session replay and the CDP pipeline is included. + +x-logging: &default-logging + driver: json-file + options: + max-size: "50m" + max-file: "3" + +x-django-env: &django-env + # Postgres + DATABASE_URL: postgres://posthog:posthog@db:5432/posthog + PGHOST: db + PGUSER: posthog + PGPASSWORD: posthog + PERSONS_DB_WRITER_URL: postgres://posthog:posthog@db:5432/posthog + PERSONS_DB_READER_URL: postgres://posthog:posthog@db:5432/posthog + # ClickHouse (users match docker/clickhouse/users.xml from the PostHog repo) + CLICKHOUSE_HOST: clickhouse + CLICKHOUSE_DATABASE: posthog + CLICKHOUSE_SECURE: "false" + CLICKHOUSE_VERIFY: "false" + CLICKHOUSE_API_USER: api + CLICKHOUSE_API_PASSWORD: apipass + CLICKHOUSE_APP_USER: app + CLICKHOUSE_APP_PASSWORD: apppass + CLICKHOUSE_LOGS_CLUSTER_HOST: clickhouse + CLICKHOUSE_LOGS_CLUSTER_SECURE: "false" + # Redis / Kafka + REDIS_URL: redis://redis7:6379/ + KAFKA_HOSTS: kafka:9092 + FLAGS_REDIS_ENABLED: "false" + # Object storage (MinIO) + OBJECT_STORAGE_ENABLED: "true" + OBJECT_STORAGE_ENDPOINT: http://objectstorage:19000 + OBJECT_STORAGE_PUBLIC_ENDPOINT: ${POSTHOG_SITE_URL} + OBJECT_STORAGE_ACCESS_KEY_ID: object_storage_root_user + OBJECT_STORAGE_SECRET_ACCESS_KEY: object_storage_root_password + OBJECT_STORAGE_FORCE_PATH_STYLE: "true" + # Session replay storage + SESSION_RECORDING_V2_S3_ENDPOINT: http://objectstorage:19000 + SESSION_RECORDING_V2_S3_ACCESS_KEY_ID: object_storage_root_user + SESSION_RECORDING_V2_S3_SECRET_ACCESS_KEY: object_storage_root_password + RECORDING_API_URL: http://recording-api:6738 + # App + SITE_URL: ${POSTHOG_SITE_URL} + SECRET_KEY: ${POSTHOG_SECRET_KEY} + ENCRYPTION_SALT_KEYS: ${POSTHOG_ENCRYPTION_SALT_KEYS} + DEPLOYMENT: hobby + IS_BEHIND_PROXY: "true" + DISABLE_SECURE_SSL_REDIRECT: "true" + # "false" so login works over plain HTTP; set to "true" once the domain uses HTTPS + SECURE_COOKIES: ${POSTHOG_SECURE_COOKIES} + CDP_API_URL: http://plugins:6738 + FEATURE_FLAGS_SERVICE_URL: http://feature-flags:3001 + LIVESTREAM_HOST: ${POSTHOG_SITE_URL}/livestream + API_QUERIES_PER_TEAM: '{"1": 100}' + OTEL_SDK_DISABLED: "true" + OTEL_EXPORTER_OTLP_ENDPOINT: "" + +services: + # One-shot: fetches the ClickHouse configuration, Kafka table schemas and + # funnel UDFs from the PostHog repo (pinned to the same commit as the app + # image) plus the GeoLite2 GeoIP database. Idempotent across redeploys. + init-assets: + image: alpine:3.21 + restart: on-failure + logging: *default-logging + entrypoint: /bin/sh + command: + - -c + - | + set -e + apk add --no-cache git curl brotli >/dev/null + if [ ! -f /assets/.ok-${POSTHOG_VERSION} ]; then + echo "Fetching PostHog repo assets @ ${POSTHOG_VERSION}" + rm -rf /tmp/ph && mkdir -p /tmp/ph && cd /tmp/ph + git init -q . + git remote add origin https://github.com/PostHog/posthog.git + git sparse-checkout set --no-cone /docker/clickhouse/ /posthog/idl/ /posthog/user_scripts/ + git -c protocol.version=2 fetch -q --depth 1 --filter=blob:none origin ${POSTHOG_VERSION} + git checkout -q FETCH_HEAD + rm -rf /assets/clickhouse /assets/idl /assets/user_scripts /assets/.ok-* + cp -r docker/clickhouse /assets/clickhouse + cp -r posthog/idl /assets/idl + cp -r posthog/user_scripts /assets/user_scripts + touch /assets/.ok-${POSTHOG_VERSION} + echo "Repo assets ready" + fi + if [ ! -s /share/GeoLite2-City.mmdb ]; then + echo "Downloading GeoLite2 database" + curl -sL --http1.1 https://mmdbcdn.posthog.net/ | brotli --decompress --output=/share/GeoLite2-City.mmdb + chmod 644 /share/GeoLite2-City.mmdb + fi + echo "init-assets done" + volumes: + - posthog-assets:/assets + - geoip-data:/share + + db: + image: postgres:15.12-alpine + restart: unless-stopped + logging: *default-logging + environment: + POSTGRES_USER: posthog + POSTGRES_DB: posthog + POSTGRES_PASSWORD: posthog + healthcheck: + test: ["CMD-SHELL", "pg_isready -U posthog"] + interval: 5s + timeout: 10s + retries: 30 + start_period: 10s + volumes: + - postgres-data:/var/lib/postgresql/data + - ../files/db-init:/docker-entrypoint-initdb.d:ro + + redis7: + image: redis:7.2-alpine + restart: unless-stopped + logging: *default-logging + command: redis-server --maxmemory-policy allkeys-lru --maxmemory 200mb + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 10s + retries: 10 + volumes: + - redis-data:/data + + # Required by ClickHouse for distributed DDL (docker/clickhouse/config.xml + # points at zookeeper:2181) + zookeeper: + image: zookeeper:3.7.0 + restart: unless-stopped + logging: *default-logging + environment: + ZOO_AUTOPURGE_PURGEINTERVAL: 1 + ZOO_AUTOPURGE_SNAPRETAINCOUNT: 3 + volumes: + - zookeeper-data:/data + - zookeeper-datalog:/datalog + + # Kafka API provided by Redpanda (as upstream), tuned down for small servers + kafka: + image: docker.io/redpandadata/redpanda:v25.1.9 + restart: unless-stopped + logging: *default-logging + command: + - redpanda + - start + - --kafka-addr internal://0.0.0.0:9092 + - --advertise-kafka-addr internal://kafka:9092 + - --rpc-addr kafka:33145 + - --advertise-rpc-addr kafka:33145 + - --mode dev-container + - --smp 1 + - --memory 1G + - --reserve-memory 0M + - --overprovisioned + - --set redpanda.empty_seed_starts_cluster=false + - --seeds kafka:33145 + - --set redpanda.auto_create_topics_enabled=true + volumes: + - redpanda-data:/var/lib/redpanda/data + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:9644/v1/status/ready || exit 1"] + interval: 5s + timeout: 10s + retries: 30 + start_period: 20s + + # One-shot: pre-creates the Kafka topics that consumers check at startup + kafka-init: + image: docker.io/redpandadata/redpanda:v25.1.9 + restart: on-failure + logging: *default-logging + entrypoint: /bin/sh + depends_on: + kafka: + condition: service_healthy + command: + - -c + - | + set -e + TOPICS="clickhouse_events_json \ + clickhouse_ai_events_json \ + clickhouse_heatmap_events \ + clickhouse_ingestion_warnings \ + events_plugin_ingestion \ + events_plugin_ingestion_dlq \ + events_plugin_ingestion_overflow \ + events_plugin_ingestion_async \ + events_plugin_ingestion_historical \ + ingestion-clientwarnings-main-1 \ + heatmaps_ingestion \ + session_recording_snapshot_item_events \ + session_recording_snapshot_item_overflow \ + clickhouse_groups \ + clickhouse_person \ + clickhouse_person_distinct_id \ + clickhouse_app_metrics2 \ + log_entries \ + clickhouse_tophog" + for topic in $$TOPICS; do + rpk topic create "$$topic" --brokers kafka:9092 -p 1 -r 1 2>/dev/null \ + || rpk topic list --brokers kafka:9092 | grep -q "$$topic" + done + echo "Kafka topics ready" + + clickhouse: + image: clickhouse/clickhouse-server:26.6.1.1193 + restart: unless-stopped + logging: *default-logging + depends_on: + init-assets: + condition: service_completed_successfully + zookeeper: + condition: service_started + kafka: + condition: service_started + entrypoint: /bin/bash + command: + - -c + - | + set -e + cp /assets/clickhouse/config.xml /etc/clickhouse-server/config.xml + cp /assets/clickhouse/config.d/default.xml /etc/clickhouse-server/config.d/default.xml + cp /assets/clickhouse/users.xml /etc/clickhouse-server/users.xml + cp /assets/clickhouse/user_defined_function.xml /etc/clickhouse-server/user_defined_function.xml + mkdir -p /var/lib/clickhouse/format_schemas /var/lib/clickhouse/user_scripts + cp -r /assets/idl/. /var/lib/clickhouse/format_schemas/ + cp -r /assets/user_scripts/. /var/lib/clickhouse/user_scripts/ + chmod -R 755 /var/lib/clickhouse/user_scripts + chown -R clickhouse:clickhouse /var/lib/clickhouse/format_schemas /var/lib/clickhouse/user_scripts || true + # Materialize system log tables (system.crash_log is required by the + # custom_metrics ClickHouse migration), mirroring upstream init-db.sh + ( + i=0 + while [ $$i -lt 90 ]; do + if clickhouse-client --query "SELECT 1" >/dev/null 2>&1; then + clickhouse-client --query "SYSTEM FLUSH LOGS" && echo "system logs flushed" && break + fi + sleep 2 + i=$$((i+1)) + done + ) & + exec /entrypoint.sh + environment: + CLICKHOUSE_SKIP_USER_SETUP: 1 + KAFKA_HOSTS: kafka:9092 + volumes: + - posthog-assets:/assets:ro + - clickhouse-data:/var/lib/clickhouse + healthcheck: + test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:8123/ping || exit 1"] + interval: 5s + timeout: 10s + retries: 30 + start_period: 30s + + # Django app: runs migrations, then serves the API + UI on :8000 + web: + image: posthog/posthog:${POSTHOG_VERSION} + restart: unless-stopped + logging: *default-logging + command: ["sh", "-c", "./bin/migrate && exec ./bin/docker-server"] + depends_on: + db: + condition: service_healthy + redis7: + condition: service_healthy + clickhouse: + condition: service_healthy + kafka: + condition: service_healthy + kafka-init: + condition: service_completed_successfully + objectstorage: + condition: service_started + environment: + <<: *django-env + NGINX_UNIT_APP_PROCESSES: 2 + healthcheck: + test: ["CMD-SHELL", "curl -fs http://localhost:8000/_health || exit 1"] + interval: 30s + timeout: 10s + retries: 15 + start_period: 600s + + # Celery worker + scheduler + worker: + image: posthog/posthog:${POSTHOG_VERSION} + restart: unless-stopped + logging: *default-logging + command: ./bin/docker-worker-celery --with-scheduler + depends_on: + db: + condition: service_healthy + redis7: + condition: service_healthy + clickhouse: + condition: service_healthy + kafka: + condition: service_healthy + web: + condition: service_started + environment: + <<: *django-env + POSTHOG_SKIP_MIGRATION_CHECKS: "1" + WEB_CONCURRENCY: 2 + + # Node plugin server: CDP (destinations, webhooks, transformations) + plugins: + image: posthog/posthog-node:${POSTHOG_NODE_VERSION} + restart: unless-stopped + logging: *default-logging + depends_on: + db: + condition: service_healthy + redis7: + condition: service_healthy + kafka: + condition: service_healthy + clickhouse: + condition: service_started + kafka-init: + condition: service_completed_successfully + init-assets: + condition: service_completed_successfully + environment: + SITE_URL: ${POSTHOG_SITE_URL} + SECRET_KEY: ${POSTHOG_SECRET_KEY} + ENCRYPTION_SALT_KEYS: ${POSTHOG_ENCRYPTION_SALT_KEYS} + DATABASE_URL: postgres://posthog:posthog@db:5432/posthog + PERSONS_DATABASE_URL: postgres://posthog:posthog@db:5432/posthog + BEHAVIORAL_COHORTS_DATABASE_URL: postgres://posthog:posthog@db:5432/posthog + CYCLOTRON_DATABASE_URL: postgres://posthog:posthog@db:5432/posthog + KAFKA_HOSTS: kafka:9092 + REDIS_URL: redis://redis7:6379/ + CDP_REDIS_HOST: redis7 + CDP_REDIS_PORT: 6379 + LOGS_REDIS_HOST: redis7 + TRACES_REDIS_HOST: redis7 + COOKIELESS_REDIS_HOST: redis7 + COOKIELESS_REDIS_PORT: 6379 + CLICKHOUSE_HOST: clickhouse + CLICKHOUSE_DATABASE: posthog + CLICKHOUSE_SECURE: "false" + CLICKHOUSE_VERIFY: "false" + OBJECT_STORAGE_ENABLED: "true" + OBJECT_STORAGE_ENDPOINT: http://objectstorage:19000 + OBJECT_STORAGE_PUBLIC_ENDPOINT: ${POSTHOG_SITE_URL} + OBJECT_STORAGE_ACCESS_KEY_ID: object_storage_root_user + OBJECT_STORAGE_SECRET_ACCESS_KEY: object_storage_root_password + OTEL_EXPORTER_OTLP_ENDPOINT: "" + volumes: + - geoip-data:/share:ro + + # Node plugin server: event ingestion (Kafka -> person processing -> ClickHouse) + ingestion-general: + image: posthog/posthog-node:${POSTHOG_NODE_VERSION} + restart: unless-stopped + logging: *default-logging + depends_on: + db: + condition: service_healthy + redis7: + condition: service_healthy + kafka: + condition: service_healthy + clickhouse: + condition: service_started + kafka-init: + condition: service_completed_successfully + init-assets: + condition: service_completed_successfully + environment: + PLUGIN_SERVER_MODE: ingestion-v2-combined + ENCRYPTION_SALT_KEYS: ${POSTHOG_ENCRYPTION_SALT_KEYS} + DATABASE_URL: postgres://posthog:posthog@db:5432/posthog + PERSONS_DATABASE_URL: postgres://posthog:posthog@db:5432/posthog + BEHAVIORAL_COHORTS_DATABASE_URL: postgres://posthog:posthog@db:5432/posthog + KAFKA_HOSTS: kafka:9092 + REDIS_URL: redis://redis7:6379/ + COOKIELESS_REDIS_HOST: redis7 + COOKIELESS_REDIS_PORT: 6379 + CDP_REDIS_HOST: redis7 + LOGS_REDIS_HOST: redis7 + TRACES_REDIS_HOST: redis7 + CLICKHOUSE_HOST: clickhouse + CLICKHOUSE_DATABASE: posthog + CLICKHOUSE_SECURE: "false" + CLICKHOUSE_VERIFY: "false" + volumes: + - geoip-data:/share:ro + + # Node plugin server: session replay blob ingestion (Kafka -> S3) + ingestion-sessionreplay: + image: posthog/posthog-node:${POSTHOG_NODE_VERSION} + restart: unless-stopped + logging: *default-logging + depends_on: + db: + condition: service_healthy + redis7: + condition: service_healthy + kafka: + condition: service_healthy + kafka-init: + condition: service_completed_successfully + objectstorage: + condition: service_started + environment: + PLUGIN_SERVER_MODE: recordings-blob-ingestion-v2 + ENCRYPTION_SALT_KEYS: ${POSTHOG_ENCRYPTION_SALT_KEYS} + DATABASE_URL: postgres://posthog:posthog@db:5432/posthog + KAFKA_HOSTS: kafka:9092 + REDIS_URL: redis://redis7:6379/ + SESSION_RECORDING_V2_S3_ENDPOINT: http://objectstorage:19000 + SESSION_RECORDING_V2_S3_ACCESS_KEY_ID: object_storage_root_user + SESSION_RECORDING_V2_S3_SECRET_ACCESS_KEY: object_storage_root_password + SESSION_RECORDING_V2_S3_TIMEOUT_MS: 120000 + + # Node plugin server: session replay playback API + recording-api: + image: posthog/posthog-node:${POSTHOG_NODE_VERSION} + restart: unless-stopped + logging: *default-logging + depends_on: + db: + condition: service_healthy + redis7: + condition: service_healthy + clickhouse: + condition: service_started + objectstorage: + condition: service_started + environment: + PLUGIN_SERVER_MODE: recording-api + ENCRYPTION_SALT_KEYS: ${POSTHOG_ENCRYPTION_SALT_KEYS} + DATABASE_URL: postgres://posthog:posthog@db:5432/posthog + REDIS_URL: redis://redis7:6379/ + SESSION_RECORDING_API_REDIS_HOST: redis7 + SESSION_RECORDING_API_REDIS_PORT: 6379 + CLICKHOUSE_HOST: clickhouse + CLICKHOUSE_DATABASE: posthog + CLICKHOUSE_SECURE: "false" + CLICKHOUSE_VERIFY: "false" + SESSION_RECORDING_V2_S3_ENDPOINT: http://objectstorage:19000 + SESSION_RECORDING_V2_S3_ACCESS_KEY_ID: object_storage_root_user + SESSION_RECORDING_V2_S3_SECRET_ACCESS_KEY: object_storage_root_password + + # Rust event capture endpoint (/e, /batch, /capture, /i/v0) + capture: + image: ghcr.io/posthog/posthog/capture:master + restart: unless-stopped + logging: *default-logging + depends_on: + kafka: + condition: service_healthy + redis7: + condition: service_healthy + environment: + ADDRESS: 0.0.0.0:3000 + KAFKA_TOPIC: events_plugin_ingestion + KAFKA_HOSTS: kafka:9092 + REDIS_URL: redis://redis7:6379/ + CAPTURE_MODE: events + RUST_LOG: info,rdkafka=warn + CAPTURE_V1_SINKS: msk + CAPTURE_V1_SINK_MSK_KAFKA_HOSTS: kafka:9092 + CAPTURE_V1_SINK_MSK_KAFKA_TOPIC_MAIN: events_plugin_ingestion + CAPTURE_V1_SINK_MSK_KAFKA_TOPIC_HISTORICAL: events_plugin_ingestion_historical + CAPTURE_V1_SINK_MSK_KAFKA_TOPIC_OVERFLOW: events_plugin_ingestion_overflow + CAPTURE_V1_SINK_MSK_KAFKA_TOPIC_DLQ: events_plugin_ingestion_dlq + CAPTURE_V1_SINK_MSK_KAFKA_TOPIC_EXCEPTION: ingestion-errortracking-main + CAPTURE_V1_SINK_MSK_KAFKA_TOPIC_HEATMAP: heatmaps_ingestion + CAPTURE_V1_SINK_MSK_KAFKA_TOPIC_CLIENT_INGESTION_WARNING: ingestion-clientwarnings-main-1 + + # Rust capture endpoint for session recordings (/s) + replay-capture: + image: ghcr.io/posthog/posthog/capture:master + restart: unless-stopped + logging: *default-logging + depends_on: + kafka: + condition: service_healthy + redis7: + condition: service_healthy + environment: + ADDRESS: 0.0.0.0:3000 + KAFKA_TOPIC: session_recording_snapshot_item_events + KAFKA_HOSTS: kafka:9092 + REDIS_URL: redis://redis7:6379/ + CAPTURE_MODE: recordings + + # Rust feature flag evaluation service (/flags) + feature-flags: + image: ghcr.io/posthog/posthog/feature-flags:master + restart: unless-stopped + logging: *default-logging + depends_on: + db: + condition: service_healthy + redis7: + condition: service_healthy + init-assets: + condition: service_completed_successfully + environment: + WRITE_DATABASE_URL: postgres://posthog:posthog@db:5432/posthog + READ_DATABASE_URL: postgres://posthog:posthog@db:5432/posthog + PERSONS_WRITE_DATABASE_URL: postgres://posthog:posthog@db:5432/posthog + PERSONS_READ_DATABASE_URL: postgres://posthog:posthog@db:5432/posthog + MAXMIND_DB_PATH: /share/GeoLite2-City.mmdb + REDIS_URL: redis://redis7:6379/ + ADDRESS: 0.0.0.0:3001 + RUST_LOG: info + COOKIELESS_REDIS_HOST: redis7 + COOKIELESS_REDIS_PORT: 6379 + volumes: + - geoip-data:/share:ro + + # Rust hypercache server (/array remote config + /surveys) + hypercache-server: + image: ghcr.io/posthog/posthog/hypercache-server:master + restart: unless-stopped + logging: *default-logging + depends_on: + redis7: + condition: service_healthy + environment: + REDIS_URL: redis://redis7:6379/ + ADDRESS: 0.0.0.0:3002 + RUST_LOG: info + + # Live events stream (/livestream) + livestream: + image: ghcr.io/posthog/posthog/livestream:master + restart: unless-stopped + logging: *default-logging + depends_on: + kafka: + condition: service_healthy + environment: + LIVESTREAM_JWT_SECRET: ${POSTHOG_SECRET_KEY} + volumes: + - ../files/livestream/configs.yml:/configs/configs.yml:ro + + objectstorage: + image: minio/minio:RELEASE.2025-04-22T22-12-26Z + restart: unless-stopped + logging: *default-logging + environment: + MINIO_ROOT_USER: object_storage_root_user + MINIO_ROOT_PASSWORD: object_storage_root_password + entrypoint: sh + command: -c 'mkdir -p /data/posthog && minio server --address ":19000" --console-address ":19001" /data' + volumes: + - objectstorage-data:/data + + # Caddy routes the single public domain to the right internal service, + # exactly like the upstream hobby deployment (TLS is terminated by Traefik) + proxy: + image: caddy:2.10-alpine + restart: unless-stopped + logging: *default-logging + depends_on: + web: + condition: service_started + entrypoint: sh + command: -c 'printf "%s" "$$CADDYFILE" > /etc/caddy/Caddyfile && exec caddy run -c /etc/caddy/Caddyfile' + environment: + CADDYFILE: | + { + auto_https off + } + :80 { + @capture path /e /e/ /e/* /i/v0 /i/v0/ /i/v0/* /i/v1/analytics/events /i/v1/analytics/events/ /batch /batch/ /batch/* /capture /capture/ /capture/* + @replay-capture path /s /s/ /s/* + @flags path /flags /flags/ /flags/* /api/feature_flag/local_evaluation /api/feature_flag/local_evaluation/ /api/feature_flag/local_evaluation/* + @surveys path /surveys /surveys/ /api/surveys /api/surveys/ + @remote-config path /array/* + @webhooks path /public/webhooks /public/webhooks/ /public/webhooks/* /public/m/ /public/m/* + @livestream path /livestream /livestream/ /livestream/* + @objectstorage path /posthog /posthog/ /posthog/* + + handle @capture { + reverse_proxy capture:3000 + } + handle @replay-capture { + reverse_proxy replay-capture:3000 + } + handle @flags { + reverse_proxy feature-flags:3001 + } + handle @surveys { + reverse_proxy hypercache-server:3002 + } + handle @remote-config { + reverse_proxy hypercache-server:3002 + } + handle @webhooks { + reverse_proxy plugins:6738 + } + handle @livestream { + uri strip_prefix /livestream + reverse_proxy livestream:8080 { + flush_interval -1 + } + } + handle @objectstorage { + reverse_proxy objectstorage:19000 + } + handle { + reverse_proxy web:8000 + } + } + +volumes: + postgres-data: + redis-data: + zookeeper-data: + zookeeper-datalog: + redpanda-data: + clickhouse-data: + posthog-assets: + geoip-data: + objectstorage-data: diff --git a/blueprints/posthog/instructions.md b/blueprints/posthog/instructions.md new file mode 100644 index 000000000..6db1223eb --- /dev/null +++ b/blueprints/posthog/instructions.md @@ -0,0 +1,27 @@ +# PostHog + +Self-hosted PostHog based on the official "hobby" deployment, trimmed to run on a single server. + +## Requirements + +- **RAM: at least 8 GB free** is strongly recommended (the stack runs Django, a Celery worker, two Node plugin-server processes, ClickHouse, Redpanda/Kafka, Postgres, Redis, MinIO and several small Rust services). +- ~15 GB of free disk for the images and data volumes. + +## First boot + +The first deployment takes **5-10 minutes**: an init container fetches the ClickHouse configuration and the GeoIP database, and the `web` service runs all Postgres and ClickHouse migrations before it starts serving. Watch the `web` service logs; once you see the Unit server start, open the domain and create the first account. + +## After enabling HTTPS + +If you serve PostHog over HTTPS (recommended for production), update these environment variables and redeploy: + +- `POSTHOG_SITE_URL` to `https://` (otherwise logins/CSRF and SDK snippets point at the wrong scheme) +- `POSTHOG_SECURE_COOKIES` to `true` (marks session/CSRF cookies as Secure; the default `false` is what allows logging in over the initial plain-HTTP domain) + +## What is not included + +Compared to the full upstream hobby stack, this template omits Temporal + Elasticsearch (data warehouse / batch exports), browserless (dashboard image exports and heatmap screenshots), and the error-tracking/logs/traces ingestion pipelines. Product analytics, feature flags, experiments, surveys, session replay, live events and the CDP destinations pipeline all work. + +## Versioning + +PostHog no longer publishes tagged releases; images are pinned to a tested master commit via the `POSTHOG_VERSION` and `POSTHOG_NODE_VERSION` environment variables. To upgrade, set `POSTHOG_VERSION` to a newer commit sha of `posthog/posthog` (Docker Hub tag) and redeploy. diff --git a/blueprints/posthog/meta.json b/blueprints/posthog/meta.json new file mode 100644 index 000000000..015dec456 --- /dev/null +++ b/blueprints/posthog/meta.json @@ -0,0 +1,19 @@ +{ + "id": "posthog", + "name": "PostHog", + "version": "master-2026-07-14", + "description": "PostHog is an open-source product analytics platform with feature flags, session replay, surveys, A/B testing and a customer data pipeline, all in one tool.", + "logo": "posthog.svg", + "links": { + "github": "https://github.com/PostHog/posthog", + "website": "https://posthog.com/", + "docs": "https://posthog.com/docs/self-host" + }, + "tags": [ + "analytics", + "product-analytics", + "feature-flags", + "session-replay", + "ab-testing" + ] +} diff --git a/blueprints/posthog/posthog.svg b/blueprints/posthog/posthog.svg new file mode 100644 index 000000000..e8cca1e91 --- /dev/null +++ b/blueprints/posthog/posthog.svg @@ -0,0 +1,10 @@ + + + PostHog + + + + + + + diff --git a/blueprints/posthog/template.toml b/blueprints/posthog/template.toml new file mode 100644 index 000000000..9644ee138 --- /dev/null +++ b/blueprints/posthog/template.toml @@ -0,0 +1,73 @@ +[variables] +main_domain = "${domain}" +secret_key = "${password:64}" +salt_keys = "${hash:32}" + +[config] +[[config.domains]] +serviceName = "proxy" +port = 80 +host = "${main_domain}" +path = "/" + +[config.env] +# PostHog stopped publishing tagged releases; images are pinned to a tested +# master commit. POSTHOG_VERSION is both the posthog/posthog image tag and the +# git ref used to fetch the matching ClickHouse configuration. +POSTHOG_VERSION = "1e7b36ebe0da38a738fed250d175809a899ef422" +POSTHOG_NODE_VERSION = "sha-82ea668" +# Set to https:// after enabling HTTPS on the domain +POSTHOG_SITE_URL = "http://${main_domain}" +# Set to "true" once the domain uses HTTPS (marks session/CSRF cookies Secure) +POSTHOG_SECURE_COOKIES = "false" +POSTHOG_SECRET_KEY = "${secret_key}" +POSTHOG_ENCRYPTION_SALT_KEYS = "${salt_keys}" + +[[config.mounts]] +filePath = "/db-init/create-extra-dbs.sh" +content = """ +#!/bin/bash +# Creates the auxiliary databases PostHog services can point at +# (mirrors docker/postgres-init-scripts in the PostHog repo) +set -e +set -u + +for db in cyclotron posthog_persons behavioral_cohorts; do + DB_EXISTS=$(psql -U "$POSTGRES_USER" -tAc "SELECT 1 FROM pg_database WHERE datname='$db'") + if [ -z "$DB_EXISTS" ]; then + echo "Creating database '$db'..." + psql -U "$POSTGRES_USER" -c "CREATE DATABASE $db;" + psql -U "$POSTGRES_USER" -c "GRANT ALL PRIVILEGES ON DATABASE $db TO $POSTGRES_USER;" + fi +done +""" + +[[config.mounts]] +filePath = "/livestream/configs.yml" +content = """ +debug: false +kafka: + brokers: 'kafka:9092' + topic: 'events_plugin_ingestion' + group_id: 'livestream' + security_protocol: 'PLAINTEXT' + session_recording_enabled: true + session_recording_security_protocol: 'PLAINTEXT' +consumers: + event: + enabled: true + brokers: 'kafka:9092' + topic: 'events_plugin_ingestion' + security_protocol: 'PLAINTEXT' + group_id: 'livestream' + session_recording: + enabled: true + brokers: 'kafka:9092' + topic: 'session_recording_snapshot_item_events' + security_protocol: 'PLAINTEXT' + group_id: 'livestream-session-recordings' + notification: + enabled: false +mmdb: + path: 'GeoLite2-City.mmdb' +""" From ee35ed1a42f1197fc3533637ae69b30ff250fd60 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 14 Jul 2026 13:47:42 -0600 Subject: [PATCH 34/80] feat: add Markup template Co-Authored-By: Claude Fable 5 --- blueprints/markup/docker-compose.yml | 39 ++++++++++++++++++++++++++++ blueprints/markup/markup.svg | 5 ++++ blueprints/markup/meta.json | 18 +++++++++++++ blueprints/markup/template.toml | 25 ++++++++++++++++++ 4 files changed, 87 insertions(+) create mode 100644 blueprints/markup/docker-compose.yml create mode 100644 blueprints/markup/markup.svg create mode 100644 blueprints/markup/meta.json create mode 100644 blueprints/markup/template.toml diff --git a/blueprints/markup/docker-compose.yml b/blueprints/markup/docker-compose.yml new file mode 100644 index 000000000..d26ccb6c0 --- /dev/null +++ b/blueprints/markup/docker-compose.yml @@ -0,0 +1,39 @@ +version: "3.8" + +services: + db: + image: postgres:16-alpine + restart: unless-stopped + volumes: + - markup-postgres-data:/var/lib/postgresql/data + environment: + POSTGRES_USER: markup + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: markup + healthcheck: + test: ["CMD-SHELL", "pg_isready -U markup -d markup"] + interval: 10s + timeout: 5s + retries: 5 + + markup: + image: ghcr.io/pphilfre/markup:main + restart: unless-stopped + depends_on: + db: + condition: service_healthy + environment: + DATABASE_URL: ${DATABASE_URL} + NEXT_PUBLIC_DB_PROVIDER: postgres + NEXT_PUBLIC_SITE_URL: ${NEXT_PUBLIC_SITE_URL} + # WorkOS AuthKit (optional): required only for user accounts and cloud sync. + # Placeholders keep the app running; replace them with real credentials + # from https://dashboard.workos.com to enable sign-in. + WORKOS_CLIENT_ID: ${WORKOS_CLIENT_ID} + WORKOS_API_KEY: ${WORKOS_API_KEY} + WORKOS_COOKIE_PASSWORD: ${WORKOS_COOKIE_PASSWORD} + NEXT_PUBLIC_WORKOS_REDIRECT_URI: ${NEXT_PUBLIC_WORKOS_REDIRECT_URI} + NODE_ENV: production + +volumes: + markup-postgres-data: diff --git a/blueprints/markup/markup.svg b/blueprints/markup/markup.svg new file mode 100644 index 000000000..0d9d3be66 --- /dev/null +++ b/blueprints/markup/markup.svg @@ -0,0 +1,5 @@ + + + M + + diff --git a/blueprints/markup/meta.json b/blueprints/markup/meta.json new file mode 100644 index 000000000..3a4f9f086 --- /dev/null +++ b/blueprints/markup/meta.json @@ -0,0 +1,18 @@ +{ + "id": "markup", + "name": "Markup", + "version": "main", + "description": "Markup is a fast, keyboard-first markdown workspace with live preview, graph view, whiteboards, mind maps, and PostgreSQL-powered cloud sync.", + "logo": "markup.svg", + "links": { + "github": "https://github.com/pphilfre/markup", + "website": "https://home.markup.freddiephilpot.dev/", + "docs": "https://docs.markup.freddiephilpot.dev/" + }, + "tags": [ + "markdown", + "notes", + "editor", + "self-hosted" + ] +} diff --git a/blueprints/markup/template.toml b/blueprints/markup/template.toml new file mode 100644 index 000000000..f2e01d73a --- /dev/null +++ b/blueprints/markup/template.toml @@ -0,0 +1,25 @@ +[variables] +main_domain = "${domain}" +postgres_password = "${password:32}" +cookie_password = "${password:32}" + +[config] +env = [ + "POSTGRES_PASSWORD=${postgres_password}", + "DATABASE_URL=postgresql://markup:${postgres_password}@db:5432/markup", + "NEXT_PUBLIC_SITE_URL=https://${main_domain}", + "# WorkOS AuthKit is optional: it powers user accounts and cloud sync.", + "# The app works without it (local, in-browser storage). To enable sign-in,", + "# create an app at https://dashboard.workos.com, set the redirect URI below", + "# in the WorkOS dashboard, and replace the placeholder credentials.", + "WORKOS_CLIENT_ID=client_id_change_me", + "WORKOS_API_KEY=sk_change_me", + "WORKOS_COOKIE_PASSWORD=${cookie_password}", + "NEXT_PUBLIC_WORKOS_REDIRECT_URI=https://${main_domain}/callback", +] +mounts = [] + +[[config.domains]] +serviceName = "markup" +port = 3000 +host = "${main_domain}" From 8c15db8a2cfaadda64db6cc65ecaef01fc6568a9 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 14 Jul 2026 13:53:41 -0600 Subject: [PATCH 35/80] feat: add Frappe LMS template Adds a Frappe LMS (Frappe Learning) template based on the erpnext-v16 blueprint pattern (configurator + create-site + workers + scheduler + websocket + mariadb + 3x redis). Pins ghcr.io/frappe/lms:v2.52.0, the latest upstream image that ships the lms app (v2.52.1+ images are currently published without any apps baked in). The image does not bundle the payments app, so create-site clears lms's required_apps before install; paid-course checkout is unavailable, everything else works. The backend is gated on a create-site completion marker so gunicorn workers never serve (and cache) a half-installed site during the 3-6 minute first boot. Closes #517 Co-Authored-By: Claude Fable 5 --- blueprints/frappe-lms/docker-compose.yml | 313 +++++++++++++++++++++++ blueprints/frappe-lms/instructions.md | 17 ++ blueprints/frappe-lms/lms-logo.png | Bin 0 -> 44490 bytes blueprints/frappe-lms/meta.json | 19 ++ blueprints/frappe-lms/template.toml | 30 +++ 5 files changed, 379 insertions(+) create mode 100644 blueprints/frappe-lms/docker-compose.yml create mode 100644 blueprints/frappe-lms/instructions.md create mode 100644 blueprints/frappe-lms/lms-logo.png create mode 100644 blueprints/frappe-lms/meta.json create mode 100644 blueprints/frappe-lms/template.toml diff --git a/blueprints/frappe-lms/docker-compose.yml b/blueprints/frappe-lms/docker-compose.yml new file mode 100644 index 000000000..da7742fa9 --- /dev/null +++ b/blueprints/frappe-lms/docker-compose.yml @@ -0,0 +1,313 @@ +x-custom-image: &custom_image + image: ${IMAGE_NAME:-ghcr.io/frappe/lms}:${VERSION:-v2.52.0} + pull_policy: ${PULL_POLICY:-always} + deploy: + restart_policy: + condition: always + +services: + backend: + <<: *custom_image + entrypoint: ["bash", "-c"] + command: + - > + until [[ -f "sites/${SITE_NAME}/.create-site-done" ]]; do + echo "Waiting for site ${SITE_NAME} to be created (first boot takes a few minutes)"; + sleep 5; + done; + exec /home/frappe/frappe-bench/env/bin/gunicorn --chdir=/home/frappe/frappe-bench/sites --bind=0.0.0.0:8000 --threads=4 --workers=2 --worker-class=gthread --worker-tmp-dir=/dev/shm --timeout=120 --preload frappe.app:application + volumes: + - sites:/home/frappe/frappe-bench/sites + healthcheck: + test: ["CMD", "wait-for-it", "127.0.0.1:8000"] + interval: 2s + timeout: 10s + retries: 30 + start_period: 600s + + frontend: + <<: *custom_image + command: + - nginx-entrypoint.sh + depends_on: + backend: + condition: service_started + websocket: + condition: service_started + environment: + BACKEND: backend:8000 + FRAPPE_SITE_NAME_HEADER: ${FRAPPE_SITE_NAME_HEADER:-$$host} + SOCKETIO: websocket:9000 + UPSTREAM_REAL_IP_ADDRESS: 127.0.0.1 + UPSTREAM_REAL_IP_HEADER: X-Forwarded-For + UPSTREAM_REAL_IP_RECURSIVE: "off" + volumes: + - sites:/home/frappe/frappe-bench/sites + healthcheck: + test: ["CMD", "wait-for-it", "127.0.0.1:8080"] + interval: 2s + timeout: 30s + retries: 30 + + queue-default: + <<: *custom_image + command: + - bench + - worker + - --queue + - default + volumes: + - sites:/home/frappe/frappe-bench/sites + healthcheck: + test: ["CMD", "wait-for-it", "redis-queue:6379"] + interval: 2s + timeout: 10s + retries: 30 + depends_on: + configurator: + condition: service_completed_successfully + + queue-long: + <<: *custom_image + command: + - bench + - worker + - --queue + - long + volumes: + - sites:/home/frappe/frappe-bench/sites + healthcheck: + test: ["CMD", "wait-for-it", "redis-queue:6379"] + interval: 2s + timeout: 10s + retries: 30 + depends_on: + configurator: + condition: service_completed_successfully + + queue-short: + <<: *custom_image + command: + - bench + - worker + - --queue + - short + volumes: + - sites:/home/frappe/frappe-bench/sites + healthcheck: + test: ["CMD", "wait-for-it", "redis-queue:6379"] + interval: 2s + timeout: 10s + retries: 30 + depends_on: + configurator: + condition: service_completed_successfully + + scheduler: + <<: *custom_image + healthcheck: + test: ["CMD", "wait-for-it", "redis-queue:6379"] + interval: 2s + timeout: 10s + retries: 30 + command: + - bench + - schedule + depends_on: + configurator: + condition: service_completed_successfully + volumes: + - sites:/home/frappe/frappe-bench/sites + + websocket: + <<: *custom_image + healthcheck: + test: ["CMD", "wait-for-it", "websocket:9000"] + interval: 2s + timeout: 10s + retries: 30 + command: + - node + - /home/frappe/frappe-bench/apps/frappe/socketio.js + depends_on: + configurator: + condition: service_completed_successfully + volumes: + - sites:/home/frappe/frappe-bench/sites + + configurator: + <<: *custom_image + deploy: + mode: replicated + replicas: ${CONFIGURE:-0} + restart_policy: + condition: none + entrypoint: ["bash", "-c"] + command: + - > + echo "[configurator] starting"; + if [[ $${REGENERATE_APPS_TXT} == "1" ]]; then + echo "[configurator] regenerating sites/apps.txt"; + ls -1 apps > sites/apps.txt; + fi; + existing_db_host=$$(grep -hs '^' sites/common_site_config.json | jq -r ".db_host // empty" 2>/dev/null || true); + if [[ -n "$${existing_db_host}" ]]; then + echo "[configurator] common_site_config.json already configured, skipping"; + exit 0; + fi; + echo "[configurator] applying bench global config"; + bench set-config -g db_host $$DB_HOST; + bench set-config -gp db_port $$DB_PORT; + bench set-config -g redis_cache "redis://$$REDIS_CACHE"; + bench set-config -g redis_queue "redis://$$REDIS_QUEUE"; + bench set-config -g redis_socketio "redis://$$REDIS_SOCKETIO"; + bench set-config -gp socketio_port $$SOCKETIO_PORT; + echo "[configurator] done"; + environment: + DB_HOST: "${DB_HOST:-db}" + DB_PORT: "3306" + REDIS_CACHE: redis-cache:6379 + REDIS_QUEUE: redis-queue:6379 + REDIS_SOCKETIO: redis-socketio:6379 + SOCKETIO_PORT: "9000" + REGENERATE_APPS_TXT: "${REGENERATE_APPS_TXT:-0}" + volumes: + - sites:/home/frappe/frappe-bench/sites + + create-site: + <<: *custom_image + deploy: + mode: replicated + replicas: ${CREATE_SITE:-0} + restart_policy: + condition: none + entrypoint: ["bash", "-c"] + command: + - > + wait-for-it -t 300 $${DB_HOST}:$${DB_PORT}; + wait-for-it -t 120 redis-cache:6379; + wait-for-it -t 120 redis-queue:6379; + export start=`date +%s`; + until [[ -n `grep -hs ^ sites/common_site_config.json | jq -r ".db_host // empty"` ]] && \ + [[ -n `grep -hs ^ sites/common_site_config.json | jq -r ".redis_cache // empty"` ]] && \ + [[ -n `grep -hs ^ sites/common_site_config.json | jq -r ".redis_queue // empty"` ]]; + do + echo "Waiting for sites/common_site_config.json to be created"; + sleep 5; + if (( `date +%s`-start > 120 )); then + echo "could not find sites/common_site_config.json with required keys"; + exit 1 + fi + done; + echo "sites/common_site_config.json found"; + [[ -d "sites/${SITE_NAME}" ]] && echo "${SITE_NAME} already exists" && touch sites/${SITE_NAME}/.create-site-done && exit 0; + sed -i "s/^required_apps.*/required_apps = []/" apps/lms/lms/hooks.py; + bench new-site --mariadb-user-host-login-scope='%' --admin-password=$${ADMIN_PASSWORD} --db-root-username=root --db-root-password=$${DB_ROOT_PASSWORD} $${INSTALL_APP_ARGS} $${SITE_NAME} || exit 1; + if [[ $${ENABLE_SCHEDULER} == "1" ]]; then + bench --site $${SITE_NAME} enable-scheduler; + else + echo "Skipping scheduler enable"; + fi; + bench --site $${SITE_NAME} clear-cache; + touch sites/$${SITE_NAME}/.create-site-done; + volumes: + - sites:/home/frappe/frappe-bench/sites + environment: + SITE_NAME: ${SITE_NAME} + ADMIN_PASSWORD: ${ADMIN_PASSWORD} + DB_HOST: ${DB_HOST:-db} + DB_PORT: "${DB_PORT:-3306}" + DB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD} + INSTALL_APP_ARGS: ${INSTALL_APP_ARGS} + ENABLE_SCHEDULER: "${ENABLE_SCHEDULER}" + + migration: + <<: *custom_image + deploy: + mode: replicated + replicas: ${MIGRATE:-0} + restart_policy: + condition: none + entrypoint: ["bash", "-c"] + command: + - > + curl -f -H "Host: ${SITE_NAME}" http://frontend:8080/api/method/ping || { echo "Site not reachable yet, skipping migration"; exit 0; }; + bench --site all set-config -p maintenance_mode 1; + bench --site all set-config -p pause_scheduler 1; + bench --site all migrate; + bench --site all set-config -p maintenance_mode 0; + bench --site all set-config -p pause_scheduler 0; + volumes: + - sites:/home/frappe/frappe-bench/sites + + db: + image: mariadb:${DB_VERSION:-11.8.6} + deploy: + mode: replicated + replicas: ${ENABLE_DB:-0} + restart_policy: + condition: always + healthcheck: + test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] + start_period: 120s + interval: 5s + timeout: 5s + retries: 30 + command: + - --character-set-server=utf8mb4 + - --collation-server=utf8mb4_unicode_ci + - --skip-character-set-client-handshake + environment: + - MARIADB_ROOT_PASSWORD=${DB_ROOT_PASSWORD} + volumes: + - db-data:/var/lib/mysql + + redis-cache: + deploy: + restart_policy: + condition: always + image: redis:6.2-alpine + volumes: + - redis-cache-data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 5s + retries: 3 + + redis-queue: + deploy: + restart_policy: + condition: always + image: redis:6.2-alpine + volumes: + - redis-queue-data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 5s + retries: 3 + + redis-socketio: + deploy: + restart_policy: + condition: always + image: redis:6.2-alpine + volumes: + - redis-socketio-data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 5s + retries: 3 + +volumes: + db-data: + redis-cache-data: + redis-queue-data: + redis-socketio-data: + sites: + driver_opts: + type: "${SITE_VOLUME_TYPE}" + o: "${SITE_VOLUME_OPTS}" + device: "${SITE_VOLUME_DEV}" diff --git a/blueprints/frappe-lms/instructions.md b/blueprints/frappe-lms/instructions.md new file mode 100644 index 000000000..48e47a093 --- /dev/null +++ b/blueprints/frappe-lms/instructions.md @@ -0,0 +1,17 @@ +# Frappe LMS (Frappe Learning) + +## Initial setup + +The first deployment creates the Frappe site and installs the LMS app, which can take **3-6 minutes** after all containers are up. The site is ready when the domain returns the Frappe login page. + +## Default credentials + +- **Username:** `Administrator` +- **Password:** the value of the `ADMIN_PASSWORD` environment variable (auto-generated, check the Environment tab of the service) + +After logging in, the learning portal is available at `/lms` and the Frappe admin backend at `/app`. + +## Notes + +- The template pins `ghcr.io/frappe/lms:v2.52.0`, the latest upstream image that ships the LMS app (newer upstream tags are currently published without the app baked in). +- The upstream image does not include the `payments` app, so paid-course checkout features are unavailable. Free courses, batches, quizzes and certifications work normally. diff --git a/blueprints/frappe-lms/lms-logo.png b/blueprints/frappe-lms/lms-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..94ba662b13133788adc17b565a78bb5ad68e6f3e GIT binary patch literal 44490 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

%`iN4)j%<3}5|{=}y6k37|QQv!d)Rw=Wc zU)5PNL6&n{swQ{TNNJ5KWG>qxf1#4isDl=&adxM{LvECLQ7-mZpaI07oM}h{msw5UHCXVY(#>1P2`BG56ix| zr&4At59iNm!1GZneM{O2pEZ!>;@-LJ7$3_3_qG6q;pWcIF42+0mby{SIgU!7(_4@_ zrLV#S0tZ9dqjquM*Gof%SBg~;T`O%S&o!#hMBbPl_J!VH0pqCqX_0=(!?x{v+A-;) zcE`9=Qk}3N)_?j}Z;S3jnvZR=9oARqotwnS{J}wzAABEtNkLJ_`tyh8Lg15GULx zp-#9LQl0SHj#v#ghnSD3u0$v8ni#9gR1pu7#7tP&@l#q!rO$+&#x~Zk=5^7p+@r{U zF0%;|*}3Q3*jQf;3Y4h?-63vLX_+n6DwWv;fy%*zDzjO2&-h#x?<*HSeD+q8FcT>I zGm>+T@JP8uv@P)9ArO!lC9p&0x5mAdjeo>uI|O;i?}yZov~hO!N;1``?gfur%(=EHuiK7_-pIp-s#neJ&0m2t16I-w}mG|jOV02wV*jT*@e zkWI#P6V0Ov!fcu~!99ULll$C}L%MiB3&ouLcR;I+FUYSdGm^Zj+?-EZ&M8->Wgxma zKBZG9pOy-me$R?bDSs|=qGNRqDzzd?vK&-IZYIsjo~rf8t3~2wS{3(*a1Ft?(g_4s zBkftE(k*Ht^(FJUu$;MZ@w!3Q>7Gr#h(p@SgxzbPin~9n87Gt z{wj-7Kt}oO3ZVV!QB-Z#^hm^0;bi*2&b#Ww^YR<2a-#=uukx^)=Q)=-JKwdXXH`81 zCgaAlG4Wi~07#mY&7=ZVX;(!spZKzfQ$5c{+=oPvZs6>oL_v*L?3z$NW)Fx5Z+G*S zhC5=d$Yh7gJgbyfIWKWRtzf4w$&tzGCexd9ZLgPQWpXq#^ETF>lgE7MJ`@4F&%g?ej-{HXX7D#)FuD5wAae`pTOgJ^H21qkXh_xPN-~NH_D5ywQH~ z=Z+Xns1=my64h0LIaoXjsD(!8$LXr_W;#xx5|=C0tBX0E#rd+J)%l!0q>W{$o+Uj; zHsvnL;*>)TYSe&fLDHB$mkHuks#v8#zZKUPg&r{`=ekGSDqfHHJiigb3_=ZD5I*^4 zv;Wf{K6�wIjfTlj?+tSU2;m|IcY<@TS*$ymcg1RCS(@Zq~3nY%jnV2{z-9)52=fioX`W@4Ss;moKmyz=z|HN^j42h0LgQ3Ln zC@nR>Sdbv8?ZoBh`-gndlx^gnIINkR7BI7B8c3Fz(l(0kx%Uu+83efEhU3QjSm9Nk*IR2cqI?Bg(=z6asf^T zf1LkAQiX!o=^1-Bz5&be9CBilET6NwbIq| z-X8(WKZ;&>CfQNHL*#wvc!9u?V46Rj_VN8$J2D()yPD=%-*mlz>18w@nj(;$KX%lM zj-0}7!Nh7cS=|ID;)ZZ@Iu5Cqu-kN~7)sF?m){{}R>2FhR`Z`2o}UH9T@W4Kw-EuF zLsDumvnzWtTbRHliJL9*R?nB`!jpIg#jlii&d&V71>Tz?LmP;zuFc7Rjo(@Xi3ZLH zlezb=+~t>MZiAyR|1x&w;QvPMz*IoQ!UV7&k3_Ghq}p6)Y5AThnrIhurw}KJvQ_ht zndVL%)mNEKF)g~M%EZj(;JkdDL=8w5{t%ZJR$=Nl75k~oQJVmz3jpJsgEG?C5!req3tvDkHVh%hs(h-__8Asnn@Pp^G zBN35n>T{H>RELoE+*vBMqry4<=J}Z|iP)AMD5~Z~K1^Opt?sTdT{f-GVLgQhM1^TA zOxWeVOg~RE|MxJ*guPJMLPakWEIpUxm66J;>W`VPSk<|Fg-OIe6ghe2**DzUJOhDy z#M4c^`|+L#+_HIOBT2tJlLq;E<&tWZ%I6J2xGdhYHj#cc z2oQ=}&gF91%l$zbH@7VooOxyFkgFPuuUlv} zx6CGl99AKV@YV^t4}#bNM}VVU**k)hZa+Jd@K0|3UTk%om7>W0{MohlGW?vb z=?o2Ox{`b6Gq0HHXLNk{c#tQl@5|-<0poY@zhsfkke=AFQrh7$M{}Y2P;Q z;%*jX8Mrk*_U4T`J=WJ`*5GW~!c49?e`sO)BNFjUklZm=Zr2yqG}D$aTL-Z^ctGuz zxQ1!XnbhU579rC8nwdZM$7SaG$MQM(8KtAF1xnc)mj#{|m!ZT(DARazz*C+dmXihm zOuW*hY}Pidi{E=u&Jxy4|8^91Bn@+}jr@A?TUa|VSd;dl@DO~5ND z-Di=+>q_drbHjAq%7gFAOcTFX)|8e+N55AVq^f>b&Y$mZLEKJxjmvLiM2PA2(y@-4 znH|hDylEgLa6S0CCFpxs7y@-V2KCqlpb&G+LQ95cs z!m$bis6{u*U6KTZ^OUfwkUeG0@w{>gbtg^Ec(0~AQ#IeY7*v>PbqI0Xux8d; zELN2*I}6J&eDAlVbQ!V^l(dMzT~!WcVzthM>?ue^bh5mrN!A+i^FX?zcu4hnsC14! z&r`zY**uxZ^bemr`M6yGE~+jR^}0Fz$xSErvPWp#Ny|>umP(*fB_&MKt7a>Ih~uun z#ry|I5y{V6jH{lBrz*3^S*U$he^qcZcR-aL z{{=y+0ft#qZOZawu>xC_*L?Esfy42=?33o2yn?_^0#DP8iT7+(slvt9&les)etC2F z@aFIRn}+)Gf`qbfrducsa|lk6&@Ks2*J5m4`cjH5XiU z^ttmoj=JB*pu|&#u2lgYoZqbqBFc|Mh$M*lFUkUCRKGtytCFu}#~iQfPS%@BsCzLA z?_pn}ZISAlbPgFW@F19o^p`frkDuCx4TnV3SF054hc=zhV^;}L=PuQYQlX6O&$p>g z8i~$K6a1W~nL9_Tlz640%&axX$_txriXaH)Cc&F>QXY$E*C>BQW~@LlAIhubr>T9AnKOa5WTcyx$vH?t#tXBbAHWo48}o!F59`|QU(K?dwi0w ziS)^Jpl922TqnOmvB65ZOJYIm+u z5Zpr5rsNW9&i)EvCRf#gGZ`xaH3+-7&o!#sQLyMa&uIiQU`e?J&41~ zs3Nw8nY5ZKR_SJ8supEu!Zf}QE?cJz6-ngm6KY8Z;SgpLalN@!MY?!T(n8=3K9aN5 zWAZnsFzb_O%iYAeiEA%SFAr Date: Tue, 14 Jul 2026 13:57:21 -0600 Subject: [PATCH 36/80] feat: add TREK template Co-Authored-By: Claude Fable 5 --- blueprints/trek/docker-compose.yml | 32 ++++++++++++++++++++ blueprints/trek/meta.json | 17 +++++++++++ blueprints/trek/template.toml | 46 +++++++++++++++++++++++++++++ blueprints/trek/trek.png | Bin 0 -> 14965 bytes 4 files changed, 95 insertions(+) create mode 100644 blueprints/trek/docker-compose.yml create mode 100644 blueprints/trek/meta.json create mode 100644 blueprints/trek/template.toml create mode 100644 blueprints/trek/trek.png diff --git a/blueprints/trek/docker-compose.yml b/blueprints/trek/docker-compose.yml new file mode 100644 index 000000000..2195ecd8f --- /dev/null +++ b/blueprints/trek/docker-compose.yml @@ -0,0 +1,32 @@ +services: + trek: + image: mauriceboe/trek:3.3.0 + restart: unless-stopped + environment: + - NODE_ENV=production + - PORT=3000 + # Encryption key for secrets at rest. Persisted via the env below so it + # survives container recreation. + - ENCRYPTION_KEY=${ENCRYPTION_KEY} + - TZ=${TZ:-UTC} + - LOG_LEVEL=${LOG_LEVEL:-info} + # Public base URL, used for links in notifications and required for OIDC + - APP_URL=${APP_URL} + # Comma-separated origins for CORS and email notification links + - ALLOWED_ORIGINS=${ALLOWED_ORIGINS} + # Initial admin credentials, only used on first boot when no users exist + - ADMIN_EMAIL=${ADMIN_EMAIL} + - ADMIN_PASSWORD=${ADMIN_PASSWORD} + volumes: + - trek-data:/app/data + - trek-uploads:/app/uploads + healthcheck: + test: ["CMD", "wget", "-qO-", "http://localhost:3000/api/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 15s + +volumes: + trek-data: + trek-uploads: diff --git a/blueprints/trek/meta.json b/blueprints/trek/meta.json new file mode 100644 index 000000000..d815be38b --- /dev/null +++ b/blueprints/trek/meta.json @@ -0,0 +1,17 @@ +{ + "id": "trek", + "name": "TREK", + "version": "3.3.0", + "description": "A minimalist, self-hosted trip planner with Kanban-style task boards, budgets, packing lists, interactive maps, real-time collaboration, and OIDC single sign-on.", + "logo": "trek.png", + "links": { + "github": "https://github.com/mauriceboe/TREK", + "website": "https://github.com/mauriceboe/TREK", + "docs": "https://github.com/mauriceboe/TREK/wiki" + }, + "tags": [ + "travel", + "planning", + "productivity" + ] +} diff --git a/blueprints/trek/template.toml b/blueprints/trek/template.toml new file mode 100644 index 000000000..f45225f95 --- /dev/null +++ b/blueprints/trek/template.toml @@ -0,0 +1,46 @@ +[variables] +main_domain = "${domain}" +encryption_key = "${password:64}" +admin_email = "${email}" +admin_password = "${password:16}" + +[config] +[[config.domains]] +serviceName = "trek" +port = 3000 +host = "${main_domain}" + +[config.env] +# Encryption key for secrets at rest (equivalent to `openssl rand -hex 32`) +"ENCRYPTION_KEY" = "${encryption_key}" +# Timezone for logs, reminders and scheduled tasks (e.g. Europe/Berlin) +"TZ" = "UTC" +# info = concise user actions; debug = verbose admin-level details +"LOG_LEVEL" = "info" +# Public base URL, used for links in notifications and required for OIDC. +# Change to https:// once you enable SSL on the domain. +"APP_URL" = "http://${main_domain}" +# Comma-separated origins for CORS and email notification links +"ALLOWED_ORIGINS" = "http://${main_domain}" +# Initial admin credentials, only used on first boot when no users exist +"ADMIN_EMAIL" = "${admin_email}" +"ADMIN_PASSWORD" = "${admin_password}" + +# --- Optional: OIDC single sign-on (Google, Apple, Authentik, Keycloak, ...) --- +# "OIDC_ISSUER" = "https://auth.example.com" +# "OIDC_CLIENT_ID" = "trek" +# "OIDC_CLIENT_SECRET" = "supersecret" +# "OIDC_DISPLAY_NAME" = "SSO" +# "OIDC_ONLY" = "false" +# "OIDC_SCOPE" = "openid email profile" +# "OIDC_ADMIN_CLAIM" = "groups" +# "OIDC_ADMIN_VALUE" = "app-trek-admins" +# "OIDC_DISCOVERY_URL" = "" + +# --- Optional: extras --- +# Default language on the login page (de, en, es, fr, hu, nl, br, cs, pl, ru, zh, zh-TW, it, ar, ...) +# "DEFAULT_LANGUAGE" = "en" +# How long users stay logged in (1h | 12h | 7d | 30d | 90d). Default: 24h +# "SESSION_DURATION" = "30d" +# Unsplash Access Key for trip-cover / place-image search +# "UNSPLASH_ACCESS_KEY" = "" diff --git a/blueprints/trek/trek.png b/blueprints/trek/trek.png new file mode 100644 index 0000000000000000000000000000000000000000..b6f059ce428c2cbec437bb4fa9610868abeea61a GIT binary patch literal 14965 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

M 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 z Date: Tue, 14 Jul 2026 14:06:04 -0600 Subject: [PATCH 37/80] feat: add Oryx (SRS) media streaming template Co-Authored-By: Claude Fable 5 --- blueprints/oryx/docker-compose.yml | 18 +++++++++++ blueprints/oryx/instructions.md | 29 +++++++++++++++++ blueprints/oryx/logo.svg | 52 ++++++++++++++++++++++++++++++ blueprints/oryx/meta.json | 22 +++++++++++++ blueprints/oryx/template.toml | 8 +++++ 5 files changed, 129 insertions(+) create mode 100644 blueprints/oryx/docker-compose.yml create mode 100644 blueprints/oryx/instructions.md create mode 100644 blueprints/oryx/logo.svg create mode 100644 blueprints/oryx/meta.json create mode 100644 blueprints/oryx/template.toml diff --git a/blueprints/oryx/docker-compose.yml b/blueprints/oryx/docker-compose.yml new file mode 100644 index 000000000..98d460ff3 --- /dev/null +++ b/blueprints/oryx/docker-compose.yml @@ -0,0 +1,18 @@ +version: "3.8" +services: + oryx: + image: ossrs/oryx:5.15.20 + restart: unless-stopped + ports: + # RTMP, WebRTC and SRT are not HTTP protocols, so Traefik cannot route + # them through the domain. They must be published directly on the host + # (same approach as the poste.io template with its mail ports). Use the + # server IP (not the domain) for these protocols. + - "1935:1935" # RTMP ingest/playback (TCP) + - "8000:8000/udp" # WebRTC media transport (UDP) + - "10080:10080/udp" # SRT ingest/playback (UDP) + volumes: + - oryx-data:/data + +volumes: + oryx-data: {} diff --git a/blueprints/oryx/instructions.md b/blueprints/oryx/instructions.md new file mode 100644 index 000000000..d9006e7db --- /dev/null +++ b/blueprints/oryx/instructions.md @@ -0,0 +1,29 @@ +# Oryx (SRS Stack) + +Oryx is an all-in-one video streaming server built on [SRS](https://github.com/ossrs/srs), with a web console for managing streams, recording, forwarding, transcoding and virtual live streaming. + +## First login + +Open the domain assigned to this service. On the first visit Oryx asks you to **create the admin password** — set it right away, since anyone who reaches the page first can claim the instance. The password (and all other state) is stored in the `oryx-data` volume, so it survives restarts and upgrades. + +## Publishing streams + +The web console (Scenarios > Streaming) shows ready-to-copy URLs that include your stream secret. The streaming protocols are **not HTTP**, so Traefik cannot route them through your domain — they are published directly on the server's host ports instead. Use the **server IP address** (not the domain) for these: + +| Protocol | URL format | Host port | +| --- | --- | --- | +| RTMP (OBS, ffmpeg) | `rtmp://SERVER_IP/live/livestream?secret=xxx` | `1935/tcp` | +| SRT | `srt://SERVER_IP:10080?streamid=#!::r=live/livestream,secret=xxx,m=publish` | `10080/udp` | +| WebRTC (WHIP) | `https://your-domain/rtc/v1/whip/?app=live&stream=livestream&secret=xxx` | signaling via domain, media via `8000/udp` | + +Make sure your server firewall / cloud security group allows `1935/tcp`, `8000/udp` and `10080/udp`. Because these are fixed host ports, only one Oryx instance can run per server. + +## Playback + +HTTP-based playback goes through your domain (HTTPS via Traefik): + +- HLS: `https://your-domain/live/livestream.m3u8` +- HTTP-FLV: `https://your-domain/live/livestream.flv` +- WebRTC (WHEP): available from the console's preview page; media flows over `8000/udp`. + +If WebRTC playback or publishing connects but produces no media, verify that UDP port `8000` is reachable from the client — WebRTC media bypasses the reverse proxy entirely. diff --git a/blueprints/oryx/logo.svg b/blueprints/oryx/logo.svg new file mode 100644 index 000000000..d16f644e6 --- /dev/null +++ b/blueprints/oryx/logo.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blueprints/oryx/meta.json b/blueprints/oryx/meta.json new file mode 100644 index 000000000..dcb80fe9f --- /dev/null +++ b/blueprints/oryx/meta.json @@ -0,0 +1,22 @@ +{ + "id": "oryx", + "name": "Oryx", + "version": "5.15.20", + "description": "Oryx (formerly SRS Stack) is an all-in-one, open-source video streaming server with a web console. It ingests RTMP, WebRTC (WHIP) and SRT streams and plays them back over HLS, HTTP-FLV and WebRTC (WHEP), with built-in recording, forwarding, transcoding and virtual live streaming.", + "logo": "logo.svg", + "links": { + "github": "https://github.com/ossrs/oryx", + "website": "https://ossrs.io/", + "docs": "https://ossrs.io/lts/en-us/docs/v6/doc/getting-started-oryx", + "docker": "https://hub.docker.com/r/ossrs/oryx" + }, + "tags": [ + "streaming", + "video", + "rtmp", + "webrtc", + "srt", + "hls", + "media-server" + ] +} diff --git a/blueprints/oryx/template.toml b/blueprints/oryx/template.toml new file mode 100644 index 000000000..4ae2264f6 --- /dev/null +++ b/blueprints/oryx/template.toml @@ -0,0 +1,8 @@ +[variables] +main_domain = "${domain}" + +[config] +[[config.domains]] +serviceName = "oryx" +port = 2022 +host = "${main_domain}" From 3eb1d3b7bd3382b7a8d07e8665fe1d79a291ba6a Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 14 Jul 2026 15:50:16 -0600 Subject: [PATCH 38/80] fix: stop plugins service crash loop (logs-redis TLS default) In production images LOGS_REDIS_TLS/TRACES_REDIS_TLS default to true, so the logs-ingestion consumer (part of the plugin-server default mode) kept attempting a TLS handshake against the plaintext Redis, timed out every ~11s and killed the process after 10 errors (restart loop every ~2.5 min). Constrain the plugins service to the capability groups this template ships (cdp_workflows, realtime_cohorts, feature_flags) and pin the redis TLS flags to false as belt-and-braces. Co-Authored-By: Claude Fable 5 --- blueprints/posthog/docker-compose.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/blueprints/posthog/docker-compose.yml b/blueprints/posthog/docker-compose.yml index b5addc40d..eb8f660c6 100644 --- a/blueprints/posthog/docker-compose.yml +++ b/blueprints/posthog/docker-compose.yml @@ -334,6 +334,11 @@ services: init-assets: condition: service_completed_successfully environment: + # Run only the pipelines this template ships (CDP destinations/workflows, + # realtime cohorts, feature-flag evaluation). The default mode would also + # start the logs/error-tracking ingestion consumers, which this template + # intentionally omits. + NODEJS_CAPABILITY_GROUPS: cdp_workflows,realtime_cohorts,feature_flags SITE_URL: ${POSTHOG_SITE_URL} SECRET_KEY: ${POSTHOG_SECRET_KEY} ENCRYPTION_SALT_KEYS: ${POSTHOG_ENCRYPTION_SALT_KEYS} @@ -345,8 +350,13 @@ services: REDIS_URL: redis://redis7:6379/ CDP_REDIS_HOST: redis7 CDP_REDIS_PORT: 6379 + # TLS for these defaults to "true" in production images; this stack's + # Redis is plaintext (a TLS handshake against it hangs until timeout and + # crash-loops the process if these consumers ever run). LOGS_REDIS_HOST: redis7 + LOGS_REDIS_TLS: "false" TRACES_REDIS_HOST: redis7 + TRACES_REDIS_TLS: "false" COOKIELESS_REDIS_HOST: redis7 COOKIELESS_REDIS_PORT: 6379 CLICKHOUSE_HOST: clickhouse @@ -392,7 +402,9 @@ services: COOKIELESS_REDIS_PORT: 6379 CDP_REDIS_HOST: redis7 LOGS_REDIS_HOST: redis7 + LOGS_REDIS_TLS: "false" TRACES_REDIS_HOST: redis7 + TRACES_REDIS_TLS: "false" CLICKHOUSE_HOST: clickhouse CLICKHOUSE_DATABASE: posthog CLICKHOUSE_SECURE: "false" From fe94f4ce0b30f049a329736fcb4cc971e71c0861 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 14 Jul 2026 16:15:50 -0600 Subject: [PATCH 39/80] feat: add n8n queue mode template n8n running in queue mode (EXECUTIONS_MODE=queue): main instance for the editor/webhooks, a scalable pool of workers (deploy.replicas driven by the N8N_WORKER_REPLICAS env var), Redis (Bull) as the queue broker and PostgreSQL as the database. Pinned to the current stable n8nio/n8n:2.30.4. Closes #455 Co-Authored-By: Claude Fable 5 --- blueprints/n8n-queue/docker-compose.yml | 124 ++++++++++++++++++++++++ blueprints/n8n-queue/instructions.md | 24 +++++ blueprints/n8n-queue/meta.json | 18 ++++ blueprints/n8n-queue/n8n.png | Bin 0 -> 6592 bytes blueprints/n8n-queue/template.toml | 33 +++++++ 5 files changed, 199 insertions(+) create mode 100644 blueprints/n8n-queue/docker-compose.yml create mode 100644 blueprints/n8n-queue/instructions.md create mode 100644 blueprints/n8n-queue/meta.json create mode 100644 blueprints/n8n-queue/n8n.png create mode 100644 blueprints/n8n-queue/template.toml diff --git a/blueprints/n8n-queue/docker-compose.yml b/blueprints/n8n-queue/docker-compose.yml new file mode 100644 index 000000000..f0b601d1e --- /dev/null +++ b/blueprints/n8n-queue/docker-compose.yml @@ -0,0 +1,124 @@ +services: + postgres: + image: postgres:17-alpine + restart: unless-stopped + environment: + - POSTGRES_USER=${POSTGRES_USER} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - POSTGRES_DB=${POSTGRES_DB} + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] + start_period: 30s + interval: 10s + timeout: 5s + retries: 5 + + redis: + image: redis:7-alpine + restart: unless-stopped + command: redis-server --requirepass ${REDIS_PASSWORD} + environment: + - REDIS_PASSWORD=${REDIS_PASSWORD} + volumes: + - redis_data:/data + healthcheck: + test: ["CMD-SHELL", "redis-cli -a \"$$REDIS_PASSWORD\" ping | grep PONG"] + start_period: 20s + interval: 10s + timeout: 5s + retries: 5 + + # Main instance: serves the editor UI and webhooks, publishes executions + # to the Redis (Bull) queue instead of running them itself. + n8n: + image: n8nio/n8n:2.30.4 + restart: unless-stopped + environment: + # PostgreSQL + - DB_TYPE=postgresdb + - DB_POSTGRESDB_HOST=postgres + - DB_POSTGRESDB_PORT=5432 + - DB_POSTGRESDB_DATABASE=${POSTGRES_DB} + - DB_POSTGRESDB_USER=${POSTGRES_USER} + - DB_POSTGRESDB_PASSWORD=${POSTGRES_PASSWORD} + + # Queue mode (Redis / Bull) + - EXECUTIONS_MODE=queue + - QUEUE_BULL_REDIS_HOST=redis + - QUEUE_BULL_REDIS_PORT=6379 + - QUEUE_BULL_REDIS_PASSWORD=${REDIS_PASSWORD} + - QUEUE_HEALTH_CHECK_ACTIVE=true + - OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS=true + + # Encryption key shared between the main instance and the workers + - N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY} + + # Networking + - N8N_HOST=${N8N_HOST} + - N8N_PORT=5678 + - N8N_PROTOCOL=http + - N8N_PROXY_HOPS=1 + - NODE_ENV=production + - N8N_WEBHOOK_URL=https://${N8N_HOST}/ + - N8N_EDITOR_BASE_URL=https://${N8N_HOST}/ + - GENERIC_TIMEZONE=${GENERIC_TIMEZONE} + - N8N_SECURE_COOKIE=false + volumes: + - n8n_data:/home/node/.n8n + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "wget --spider -q http://127.0.0.1:5678/healthz"] + start_period: 60s + interval: 10s + timeout: 5s + retries: 10 + + # Queue workers: pull executions from Redis and run them. Scale them by + # changing the N8N_WORKER_REPLICAS environment variable and redeploying. + n8n-worker: + image: n8nio/n8n:2.30.4 + restart: unless-stopped + command: worker + deploy: + replicas: ${N8N_WORKER_REPLICAS:-2} + environment: + # PostgreSQL + - DB_TYPE=postgresdb + - DB_POSTGRESDB_HOST=postgres + - DB_POSTGRESDB_PORT=5432 + - DB_POSTGRESDB_DATABASE=${POSTGRES_DB} + - DB_POSTGRESDB_USER=${POSTGRES_USER} + - DB_POSTGRESDB_PASSWORD=${POSTGRES_PASSWORD} + + # Queue mode (Redis / Bull) + - EXECUTIONS_MODE=queue + - QUEUE_BULL_REDIS_HOST=redis + - QUEUE_BULL_REDIS_PORT=6379 + - QUEUE_BULL_REDIS_PASSWORD=${REDIS_PASSWORD} + - QUEUE_HEALTH_CHECK_ACTIVE=true + + # Must be the same key as the main instance + - N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY} + + - NODE_ENV=production + - GENERIC_TIMEZONE=${GENERIC_TIMEZONE} + depends_on: + n8n: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "wget --spider -q http://127.0.0.1:5678/healthz"] + start_period: 60s + interval: 10s + timeout: 5s + retries: 10 + +volumes: + postgres_data: + redis_data: + n8n_data: diff --git a/blueprints/n8n-queue/instructions.md b/blueprints/n8n-queue/instructions.md new file mode 100644 index 000000000..782571b8b --- /dev/null +++ b/blueprints/n8n-queue/instructions.md @@ -0,0 +1,24 @@ +# n8n Queue Mode + +This template runs n8n in [queue mode](https://docs.n8n.io/hosting/scaling/queue-mode/): + +- **n8n** (main): serves the editor UI and receives webhooks/triggers, then publishes executions to the queue. +- **n8n-worker** (x2 by default): pulls executions from the Redis (Bull) queue and runs them. +- **redis**: the message broker for the queue (password protected). +- **postgres**: stores workflows, credentials and execution data. + +All n8n containers share the same `N8N_ENCRYPTION_KEY` (generated automatically), which is required for workers to decrypt credentials. + +## Scaling workers + +Worker replicas are controlled by the `N8N_WORKER_REPLICAS` environment variable (default `2`): + +1. Open the compose service in Dokploy and go to the **Environment** tab. +2. Change `N8N_WORKER_REPLICAS` to the number of workers you want. +3. Redeploy. Docker Compose will scale the `n8n-worker` service to the requested number of replicas. + +Each worker runs up to 10 concurrent executions by default. You can tune this by adding `N8N_CONCURRENCY_PRODUCTION_LIMIT` to the worker environment. + +## Optional: dedicated webhook processors + +For very high webhook volumes, n8n supports dedicated webhook processor instances (`command: webhook`). To use them, add another service to the compose file with the same environment as `n8n-worker` but with `command: webhook`, and route the `/webhook/` path of your domain to it. See the [n8n queue mode docs](https://docs.n8n.io/hosting/scaling/queue-mode/#webhook-processors) for details. diff --git a/blueprints/n8n-queue/meta.json b/blueprints/n8n-queue/meta.json new file mode 100644 index 000000000..e0ada259d --- /dev/null +++ b/blueprints/n8n-queue/meta.json @@ -0,0 +1,18 @@ +{ + "id": "n8n-queue", + "name": "n8n Queue Mode", + "version": "2.30.4", + "description": "n8n running in queue mode for high-throughput production workloads: the main instance handles the editor and webhooks while a scalable pool of workers executes workflows from a Redis (Bull) queue, backed by PostgreSQL.", + "logo": "n8n.png", + "links": { + "github": "https://github.com/n8n-io/n8n", + "website": "https://n8n.io/", + "docs": "https://docs.n8n.io/hosting/scaling/queue-mode/" + }, + "tags": [ + "automation", + "workflow", + "low-code", + "queue" + ] +} diff --git a/blueprints/n8n-queue/n8n.png b/blueprints/n8n-queue/n8n.png new file mode 100644 index 0000000000000000000000000000000000000000..0e9a607e2494055b97bfd12878f496a6aa6e749f GIT binary patch literal 6592 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 ZQeyfU3uE)l$Tu?-nyPxQYF@mF{C_OaY>@x} literal 0 HcmV?d00001 diff --git a/blueprints/n8n-queue/template.toml b/blueprints/n8n-queue/template.toml new file mode 100644 index 000000000..46ce3fbed --- /dev/null +++ b/blueprints/n8n-queue/template.toml @@ -0,0 +1,33 @@ +[variables] +main_domain = "${domain}" +postgres_user = "${username}" +postgres_password = "${password:24}" +postgres_db = "n8n" +redis_password = "${password:24}" +n8n_encryption_key = "${base64:32}" + +[config] +mounts = [] + +[[config.domains]] +serviceName = "n8n" +port = 5_678 +host = "${main_domain}" + +[config.env] +N8N_HOST = "${main_domain}" +GENERIC_TIMEZONE = "Europe/Berlin" + +# Number of queue workers. Change it and redeploy to scale. +N8N_WORKER_REPLICAS = "2" + +# PostgreSQL +POSTGRES_USER = "${postgres_user}" +POSTGRES_PASSWORD = "${postgres_password}" +POSTGRES_DB = "${postgres_db}" + +# Redis (Bull queue) +REDIS_PASSWORD = "${redis_password}" + +# Encryption key shared by the main instance and the workers (IMPORTANT) +N8N_ENCRYPTION_KEY = "${n8n_encryption_key}" From 7d252ead5cc99489c82654483c79a20d440ebcca Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Tue, 14 Jul 2026 16:24:32 -0600 Subject: [PATCH 40/80] feat: add bonfire template MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bonfire (social flavour) — federated social networking toolkit. App + PostGIS + Meilisearch, mirroring upstream's reference deployments. Closes #125 Co-Authored-By: Claude Fable 5 --- blueprints/bonfire/bonfire.svg | 1 + blueprints/bonfire/docker-compose.yml | 84 +++++++++++++++++++++++++++ blueprints/bonfire/meta.json | 17 ++++++ blueprints/bonfire/template.toml | 55 ++++++++++++++++++ 4 files changed, 157 insertions(+) create mode 100644 blueprints/bonfire/bonfire.svg create mode 100644 blueprints/bonfire/docker-compose.yml create mode 100644 blueprints/bonfire/meta.json create mode 100644 blueprints/bonfire/template.toml diff --git a/blueprints/bonfire/bonfire.svg b/blueprints/bonfire/bonfire.svg new file mode 100644 index 000000000..ce2cac6f7 --- /dev/null +++ b/blueprints/bonfire/bonfire.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/blueprints/bonfire/docker-compose.yml b/blueprints/bonfire/docker-compose.yml new file mode 100644 index 000000000..b52ca2599 --- /dev/null +++ b/blueprints/bonfire/docker-compose.yml @@ -0,0 +1,84 @@ +version: "3.8" +services: + bonfire: + image: bonfirenetworks/bonfire:1.0.5-social-amd64 + restart: unless-stopped + depends_on: + db: + condition: service_healthy + search: + condition: service_started + environment: + # Instance + - HOSTNAME=${BONFIRE_HOSTNAME} + - PUBLIC_PORT=${PUBLIC_PORT:-80} + - SERVER_PORT=4000 + - APP_NAME=${APP_NAME:-Bonfire} + - LANG=en_US.UTF-8 + # Secrets + - SECRET_KEY_BASE=${SECRET_KEY_BASE} + - SIGNING_SALT=${SIGNING_SALT} + - ENCRYPTION_SALT=${ENCRYPTION_SALT} + - RELEASE_COOKIE=${RELEASE_COOKIE} + # Database (migrations run automatically on startup) + - POSTGRES_HOST=db + - POSTGRES_USER=postgres + - POSTGRES_DB=bonfire_db + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + # Must stay false for the initial migrations; can be set to true + # after the first deployment to speed up future upgrades + - DB_MIGRATE_INDEXES_CONCURRENTLY=${DB_MIGRATE_INDEXES_CONCURRENTLY:-false} + # Search (Meilisearch) + - SEARCH_ADAPTER=meili + - SEARCH_MEILI_INSTANCE=http://search:7700 + - MEILI_MASTER_KEY=${MEILI_MASTER_KEY} + # Email (the first account to sign up becomes admin and needs no + # confirmation email; configure a mail backend for further signups) + - MAIL_BACKEND=${MAIL_BACKEND:-none} + - MAIL_SERVER=${MAIL_SERVER:-} + - MAIL_PORT=${MAIL_PORT:-} + - MAIL_USER=${MAIL_USER:-} + - MAIL_PASSWORD=${MAIL_PASSWORD:-} + - MAIL_DOMAIN=${MAIL_DOMAIN:-} + - MAIL_FROM=${MAIL_FROM:-} + - MAIL_KEY=${MAIL_KEY:-} + # Max upload size in megabytes + - UPLOAD_LIMIT=${UPLOAD_LIMIT:-20} + volumes: + - bonfire-uploads:/opt/app/data/uploads + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:4000"] + interval: 30s + timeout: 10s + retries: 15 + start_period: 120s + + db: + image: postgis/postgis:17-3.5-alpine + restart: unless-stopped + environment: + - POSTGRES_USER=postgres + - POSTGRES_DB=bonfire_db + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + volumes: + - db-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres -d bonfire_db"] + interval: 10s + timeout: 5s + retries: 10 + + search: + image: getmeili/meilisearch:v1.11 + restart: unless-stopped + environment: + - MEILI_MASTER_KEY=${MEILI_MASTER_KEY} + - MEILI_ENV=production + - MEILI_NO_ANALYTICS=true + volumes: + - search-data:/meili_data + +volumes: + bonfire-uploads: + db-data: + search-data: diff --git a/blueprints/bonfire/meta.json b/blueprints/bonfire/meta.json new file mode 100644 index 000000000..bee28afb7 --- /dev/null +++ b/blueprints/bonfire/meta.json @@ -0,0 +1,17 @@ +{ + "id": "bonfire", + "name": "Bonfire", + "version": "1.0.5", + "description": "Federated social networking toolkit for communities: customizable digital spaces with ActivityPub federation, flexible boundaries and moderation tools.", + "logo": "bonfire.svg", + "links": { + "github": "https://github.com/bonfire-networks/bonfire-app", + "website": "https://bonfirenetworks.org", + "docs": "https://docs.bonfirenetworks.org" + }, + "tags": [ + "social", + "fediverse", + "activitypub" + ] +} diff --git a/blueprints/bonfire/template.toml b/blueprints/bonfire/template.toml new file mode 100644 index 000000000..c8db0d005 --- /dev/null +++ b/blueprints/bonfire/template.toml @@ -0,0 +1,55 @@ +[variables] +main_domain = "${domain}" +secret_key_base = "${password:64}" +signing_salt = "${password:32}" +encryption_salt = "${password:32}" +release_cookie = "${password:32}" +postgres_password = "${password:32}" +meili_master_key = "${password:32}" + +[config] +[[config.domains]] +serviceName = "bonfire" +port = 4000 +host = "${main_domain}" + +[config.env] +# Domain of your Bonfire instance (must match the domain above) +"BONFIRE_HOSTNAME" = "${main_domain}" +# Port used to build public URLs: keep 80 while the domain is plain HTTP, +# and change to 443 once you enable HTTPS on the domain +# (HTTPS is required for federation with other fediverse instances) +"PUBLIC_PORT" = "80" +# Display name of your instance +"APP_NAME" = "Bonfire" +# Max upload size in megabytes +"UPLOAD_LIMIT" = "20" + +# Secrets (auto-generated) +"SECRET_KEY_BASE" = "${secret_key_base}" +"SIGNING_SALT" = "${signing_salt}" +"ENCRYPTION_SALT" = "${encryption_salt}" +"RELEASE_COOKIE" = "${release_cookie}" +"POSTGRES_PASSWORD" = "${postgres_password}" +"MEILI_MASTER_KEY" = "${meili_master_key}" + +# Optional: set to true only AFTER the first deployment finished its initial +# database migrations (speeds up index creation during future upgrades) +# "DB_MIGRATE_INDEXES_CONCURRENTLY" = "true" + +# --- Email --- +# The first account to sign up becomes the instance admin and does not need +# a confirmation email. Further signups are invite-only by default (the admin +# can create invite links or open up signups in the instance settings), and +# require a working mail backend to confirm their email address. +# Supported backends include smtp, mailgun, brevo, postmark, ses and more: +# https://docs.bonfirenetworks.org/deploy.html +# "MAIL_BACKEND" = "smtp" +# "MAIL_SERVER" = "smtp.example.com" +# "MAIL_PORT" = "587" +# "MAIL_USER" = "postmaster@example.com" +# "MAIL_PASSWORD" = "" +# "MAIL_DOMAIN" = "example.com" +# "MAIL_FROM" = "bonfire@example.com" +# For API-based backends (e.g. mailgun) set the API key instead: +# "MAIL_KEY" = "" From 0beb68a228750b15959374041a7f92d49b0516f9 Mon Sep 17 00:00:00 2001 From: Stephan Fitzpatrick Date: Tue, 14 Jul 2026 16:41:48 -0700 Subject: [PATCH 41/80] feat: add dagu blueprint Dagu is a self-contained workflow engine with a Web UI for scheduling and orchestrating jobs as DAGs defined in declarative YAML. - Pin ghcr.io/dagucloud/dagu:2.10.7 and expose the Web UI on port 8080 - Set DAGU_HOST=0.0.0.0 so the service is reachable through the proxy - Enable basic auth with a generated per-deploy admin password - Persist /var/lib/dagu (DAGs, logs, run history) in a named volume Claude-Session: https://claude.ai/code/session_01CRNT6sa2q2P9rUTokeoGhQ --- blueprints/dagu/docker-compose.yml | 25 +++++++++++++++++++++++++ blueprints/dagu/logo.png | Bin 0 -> 7754 bytes blueprints/dagu/meta.json | 19 +++++++++++++++++++ blueprints/dagu/template.toml | 15 +++++++++++++++ 4 files changed, 59 insertions(+) create mode 100644 blueprints/dagu/docker-compose.yml create mode 100644 blueprints/dagu/logo.png create mode 100644 blueprints/dagu/meta.json create mode 100644 blueprints/dagu/template.toml diff --git a/blueprints/dagu/docker-compose.yml b/blueprints/dagu/docker-compose.yml new file mode 100644 index 000000000..4fee20eb9 --- /dev/null +++ b/blueprints/dagu/docker-compose.yml @@ -0,0 +1,25 @@ +# Access the Dagu Web UI at the assigned domain. +# Sign in with the basic auth credentials from your .env file (default user: admin). +# Place your DAG definitions (YAML) in the dags directory via the Web UI or the API. + +version: "3.8" + +services: + dagu: + image: ghcr.io/dagucloud/dagu:2.10.7 + command: dagu start-all + restart: unless-stopped + expose: + - 8080 + volumes: + - dagu-data:/var/lib/dagu + environment: + # Dagu binds to 127.0.0.1 by default; 0.0.0.0 is required to reach it + - DAGU_HOST=0.0.0.0 + - DAGU_PORT=8080 + - DAGU_AUTH_MODE=basic + - DAGU_AUTH_BASIC_USERNAME=${ADMIN_USERNAME} + - DAGU_AUTH_BASIC_PASSWORD=${ADMIN_PASSWORD} + +volumes: + dagu-data: {} diff --git a/blueprints/dagu/logo.png b/blueprints/dagu/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..1aa26d2cdffe6c804134f6a35a7be5d76a7055ae GIT binary patch literal 7754 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 n Date: Tue, 14 Jul 2026 16:45:24 -0600 Subject: [PATCH 42/80] feat: add OneUptime template Adds OneUptime (open-source observability platform: uptime monitoring, status pages, incident management, on-call, logs/traces/metrics) as a template, following upstream's current single-app architecture (app + nginx ingress + postgres + redis + clickhouse + probe). Closes #514 Co-Authored-By: Claude Fable 5 --- blueprints/oneuptime/docker-compose.yml | 158 ++++++++++++++++++++++++ blueprints/oneuptime/meta.json | 20 +++ blueprints/oneuptime/oneuptime.svg | 10 ++ blueprints/oneuptime/template.toml | 109 ++++++++++++++++ 4 files changed, 297 insertions(+) create mode 100644 blueprints/oneuptime/docker-compose.yml create mode 100644 blueprints/oneuptime/meta.json create mode 100644 blueprints/oneuptime/oneuptime.svg create mode 100644 blueprints/oneuptime/template.toml diff --git a/blueprints/oneuptime/docker-compose.yml b/blueprints/oneuptime/docker-compose.yml new file mode 100644 index 000000000..7bd823270 --- /dev/null +++ b/blueprints/oneuptime/docker-compose.yml @@ -0,0 +1,158 @@ +services: + oneuptime: + image: oneuptime/app:11.5.6 + restart: unless-stopped + environment: + - PORT=3002 + - NODE_ENV=production + - BILLING_ENABLED=false + - IS_ENTERPRISE_EDITION=false + - LOG_LEVEL=ERROR + - DISABLE_TELEMETRY=true + # Public host (domain) and protocol used to build links. + - HOST=${HOST} + - HTTP_PROTOCOL=${HTTP_PROTOCOL} + - ONEUPTIME_SECRET=${ONEUPTIME_SECRET} + - ENCRYPTION_SECRET=${ENCRYPTION_SECRET} + # Probes that present this key are allowed to self-register. + - REGISTER_PROBE_KEY=${REGISTER_PROBE_KEY} + - DATABASE_HOST=postgres + - DATABASE_PORT=5432 + - DATABASE_USERNAME=postgres + - DATABASE_PASSWORD=${DATABASE_PASSWORD} + - DATABASE_NAME=oneuptimedb + - REDIS_HOST=redis + - REDIS_PORT=6379 + - REDIS_USERNAME=default + - REDIS_PASSWORD=${REDIS_PASSWORD} + - REDIS_DB=0 + - CLICKHOUSE_HOST=clickhouse + - CLICKHOUSE_PORT=8123 + - CLICKHOUSE_USER=default + - CLICKHOUSE_PASSWORD=${CLICKHOUSE_PASSWORD} + - CLICKHOUSE_DATABASE=oneuptime + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + clickhouse: + condition: service_healthy + + ingress: + image: oneuptime/nginx:11.5.6 + restart: unless-stopped + environment: + - NODE_ENV=production + - BILLING_ENABLED=false + - LOG_LEVEL=ERROR + - DISABLE_TELEMETRY=true + - HOST=${HOST} + - NGINX_LISTEN_ADDRESS= + - NGINX_LISTEN_OPTIONS= + - PROVISION_SSL= + # Long domains (e.g. generated *.sslip.io hosts) overflow nginx's + # default server_names_hash_bucket_size of 64. + - SERVER_NAMES_HASH_BUCKET_SIZE=128 + # Where nginx proxies the UI/API to. + - SERVER_APP_HOSTNAME=oneuptime + - APP_PORT=3002 + # The marketing "home" app is not deployed (only used when billing is + # enabled), but nginx needs the vars to render its config. + - SERVER_HOME_HOSTNAME=home + - HOME_PORT=1444 + # The ingress sidecar (custom-domain certificate writer) connects to + # Postgres and Redis. + - ONEUPTIME_SECRET=${ONEUPTIME_SECRET} + - ENCRYPTION_SECRET=${ENCRYPTION_SECRET} + - DATABASE_HOST=postgres + - DATABASE_PORT=5432 + - DATABASE_USERNAME=postgres + - DATABASE_PASSWORD=${DATABASE_PASSWORD} + - DATABASE_NAME=oneuptimedb + - REDIS_HOST=redis + - REDIS_PORT=6379 + - REDIS_USERNAME=default + - REDIS_PASSWORD=${REDIS_PASSWORD} + - REDIS_DB=0 + depends_on: + postgres: + condition: service_healthy + oneuptime: + condition: service_started + + probe: + image: oneuptime/probe:11.5.6 + restart: unless-stopped + environment: + - PORT=3874 + - NODE_ENV=production + - LOG_LEVEL=ERROR + - DISABLE_TELEMETRY=true + # The probe reaches OneUptime through the ingress ("ingress" is part of + # nginx's server_name, so all API routes resolve correctly). + - ONEUPTIME_URL=http://ingress:7849 + - PROBE_NAME=Probe-1 + - PROBE_DESCRIPTION=Default probe for monitors + - PROBE_MONITORING_WORKERS=3 + - PROBE_MONITOR_FETCH_LIMIT=10 + - PROBE_KEY=${PROBE_KEY} + - REGISTER_PROBE_KEY=${REGISTER_PROBE_KEY} + depends_on: + ingress: + condition: service_started + + postgres: + image: postgres:15 + restart: unless-stopped + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=${DATABASE_PASSWORD} + - POSTGRES_DB=oneuptimedb + volumes: + - postgres-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres -d oneuptimedb"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 15s + + redis: + image: redis:7.0.12 + restart: unless-stopped + command: redis-server --requirepass "${REDIS_PASSWORD}" --save "" --appendonly no + healthcheck: + test: ["CMD-SHELL", "redis-cli -a \"$$REDIS_PASSWORD\" ping | grep PONG"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + environment: + - REDIS_PASSWORD=${REDIS_PASSWORD} + + clickhouse: + image: clickhouse/clickhouse-server:25.7 + restart: unless-stopped + environment: + - CLICKHOUSE_USER=default + - CLICKHOUSE_PASSWORD=${CLICKHOUSE_PASSWORD} + - CLICKHOUSE_DB=oneuptime + - CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT=1 + volumes: + - clickhouse-data:/var/lib/clickhouse + # Single-node cluster + embedded Keeper (OneUptime's analytics schema is + # always ReplicatedMergeTree + Distributed) and Distributed-insert + # batching, as shipped by upstream. + - ../files/clickhouse/cluster.xml:/etc/clickhouse-server/config.d/cluster.xml + - ../files/clickhouse/distributed-insert-tuning.xml:/etc/clickhouse-server/users.d/distributed-insert-tuning.xml + healthcheck: + test: ["CMD-SHELL", "clickhouse-client --password \"$$CLICKHOUSE_PASSWORD\" --query 'SELECT 1'"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 15s + +volumes: + postgres-data: + clickhouse-data: diff --git a/blueprints/oneuptime/meta.json b/blueprints/oneuptime/meta.json new file mode 100644 index 000000000..c2108d5c4 --- /dev/null +++ b/blueprints/oneuptime/meta.json @@ -0,0 +1,20 @@ +{ + "id": "oneuptime", + "name": "OneUptime", + "version": "11.5.6", + "description": "OneUptime is an open-source observability platform: uptime monitoring, status pages, incident management, on-call rotations, logs, traces and metrics in one place.", + "logo": "oneuptime.svg", + "links": { + "github": "https://github.com/OneUptime/oneuptime", + "website": "https://oneuptime.com/", + "docs": "https://oneuptime.com/docs" + }, + "tags": [ + "monitoring", + "observability", + "status-page", + "incident-management", + "on-call", + "alerting" + ] +} diff --git a/blueprints/oneuptime/oneuptime.svg b/blueprints/oneuptime/oneuptime.svg new file mode 100644 index 000000000..5f2eec13f --- /dev/null +++ b/blueprints/oneuptime/oneuptime.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/blueprints/oneuptime/template.toml b/blueprints/oneuptime/template.toml new file mode 100644 index 000000000..a674b269b --- /dev/null +++ b/blueprints/oneuptime/template.toml @@ -0,0 +1,109 @@ +[variables] +main_domain = "${domain}" +db_password = "${password:32}" +redis_password = "${password:32}" +clickhouse_password = "${password:32}" +oneuptime_secret = "${password:64}" +encryption_secret = "${password:64}" +register_probe_key = "${password:32}" +probe_key = "${password:32}" + +[config] +[[config.domains]] +serviceName = "ingress" +port = 7849 +host = "${main_domain}" + +[config.env] +# Public host OneUptime is served on. Must match the domain above. +"HOST" = "${main_domain}" +# Change to "https" once you serve the domain over SSL/TLS. +"HTTP_PROTOCOL" = "http" +"DATABASE_PASSWORD" = "${db_password}" +"REDIS_PASSWORD" = "${redis_password}" +"CLICKHOUSE_PASSWORD" = "${clickhouse_password}" +"ONEUPTIME_SECRET" = "${oneuptime_secret}" +"ENCRYPTION_SECRET" = "${encryption_secret}" +# Shared key probes use to self-register with the server. +"REGISTER_PROBE_KEY" = "${register_probe_key}" +# Identity key of the bundled probe. +"PROBE_KEY" = "${probe_key}" + +# Single-node ClickHouse cluster + embedded Keeper drop-in (from upstream: +# OneUptime's analytics schema is always ReplicatedMergeTree + Distributed, +# so a single node runs an embedded Keeper and a 1-node "oneuptime" cluster). +[[config.mounts]] +filePath = "/clickhouse/cluster.xml" +content = """ + + + + + 9181 + 1 + /var/lib/clickhouse/coordination/log + /var/lib/clickhouse/coordination/snapshots + + 10000 + 30000 + warning + + + + 1 + localhost + 9234 + + + + + + + + localhost + 9181 + + + + + + 01 + replica-1 + oneuptime + + + + + + + true + + localhost + 9000 + + + + + +""" + +# Batches the background Distributed-insert sender so telemetry ingestion +# keeps up with volume (from upstream Clickhouse/users.d). +[[config.mounts]] +filePath = "/clickhouse/distributed-insert-tuning.xml" +content = """ + + + + + + 1 + + 1 + + 1 + + + +""" From f6245f84dc2edcce8c757dc4007a58ca7ac0f725 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Thu, 16 Jul 2026 17:50:38 -0600 Subject: [PATCH 43/80] feat: add Mailu template Mailu 2024.06 mail server: front (nginx), admin, imap (dovecot), smtp (postfix), antispam (rspamd), Roundcube webmail and redis. Co-Authored-By: Claude Fable 5 --- blueprints/mailu/docker-compose.yml | 146 ++++++++++++++++++++++++++++ blueprints/mailu/instructions.md | 23 +++++ blueprints/mailu/mailu.svg | 137 ++++++++++++++++++++++++++ blueprints/mailu/meta.json | 21 ++++ blueprints/mailu/template.toml | 17 ++++ 5 files changed, 344 insertions(+) create mode 100644 blueprints/mailu/docker-compose.yml create mode 100644 blueprints/mailu/instructions.md create mode 100644 blueprints/mailu/mailu.svg create mode 100644 blueprints/mailu/meta.json create mode 100644 blueprints/mailu/template.toml diff --git a/blueprints/mailu/docker-compose.yml b/blueprints/mailu/docker-compose.yml new file mode 100644 index 000000000..3b644361e --- /dev/null +++ b/blueprints/mailu/docker-compose.yml @@ -0,0 +1,146 @@ +version: "3.8" + +# Shared Mailu configuration (equivalent to the upstream mailu.env file) +x-mailu-env: &mailu-env + - SECRET_KEY=${SECRET_KEY} + - DOMAIN=${DOMAIN} + - HOSTNAMES=${DOMAIN} + - POSTMASTER=admin + # TLS for the mail protocols is obtained through Let's Encrypt (HTTP-01 + # through Traefik on port 80). The web UI itself is served plain HTTP to + # Traefik, which terminates HTTPS. + - TLS_FLAVOR=mail-letsencrypt + # Mailu grants relay/XCLIENT trust to this network range. It must cover the + # Docker networks of this project (Docker allocates them from 172.16.0.0/12 + # by default). Narrow it down if you know your exact subnet. + - SUBNET=${SUBNET} + - ADMIN=true + - WEBMAIL=roundcube + - API=false + - WEBDAV=none + - ANTIVIRUS=none + - SCAN_MACROS=false + # Ports enabled inside the front container. 4190 (sieve) stays internal for + # the webmail filters UI and is not published on the host. + - PORTS=25,80,443,465,587,993,4190 + - MESSAGE_SIZE_LIMIT=50000000 + - FULL_TEXT_SEARCH=en + - WEBROOT_REDIRECT=/webmail + - WEB_ADMIN=/admin + - WEB_WEBMAIL=/webmail + - SITENAME=Mailu + - WEBSITE=https://mailu.io + # Trust Traefik (dokploy-network) to forward the real client IP for rate limiting + - REAL_IP_FROM=${SUBNET} + - REAL_IP_HEADER=X-Forwarded-For + # Initial admin account: admin@${DOMAIN} (created only if it does not exist) + - INITIAL_ADMIN_ACCOUNT=${INITIAL_ADMIN_ACCOUNT} + - INITIAL_ADMIN_DOMAIN=${DOMAIN} + - INITIAL_ADMIN_PW=${INITIAL_ADMIN_PW} + - INITIAL_ADMIN_MODE=ifmissing + +# Public DNSSEC-validating resolvers. The admin container refuses to start +# without DNSSEC validation, and Postfix needs it for outbound DANE. Service +# discovery still uses Docker's embedded DNS; these are only upstreams. +x-mailu-dns: &mailu-dns + - "1.1.1.1" + - "8.8.8.8" + +services: + front: + image: ghcr.io/mailu/nginx:2024.06 + restart: unless-stopped + environment: *mailu-env + dns: *mailu-dns + # Mail protocol ports are published directly on the host (same approach as + # the poste.io template). They will fail to bind if another mail server + # already uses them on this machine. + ports: + - "25:25" # SMTP (server to server) + - "465:465" # SMTPS submission + - "587:587" # Submission (STARTTLS) + - "993:993" # IMAPS + volumes: + - mailu-certs:/certs + # The image's built-in healthcheck also requires the mail (Dovecot) proxy, + # which only starts once TLS certificates exist. Behind Traefik that + # deadlocks: unhealthy -> Traefik drops the route -> the ACME challenge can + # never be answered. Check only nginx instead (health endpoint returns 204). + healthcheck: + test: ["CMD-SHELL", "curl -m3 -skfLo /dev/null http://127.0.0.1:10204/health"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 60s + depends_on: + - admin + + admin: + image: ghcr.io/mailu/admin:2024.06 + restart: unless-stopped + environment: *mailu-env + dns: *mailu-dns + volumes: + - mailu-data:/data + - mailu-dkim:/dkim + depends_on: + - redis + + imap: + image: ghcr.io/mailu/dovecot:2024.06 + restart: unless-stopped + environment: *mailu-env + dns: *mailu-dns + volumes: + - mailu-mail:/mail + depends_on: + - front + + smtp: + image: ghcr.io/mailu/postfix:2024.06 + restart: unless-stopped + environment: *mailu-env + dns: *mailu-dns + volumes: + - mailu-mailqueue:/queue + depends_on: + - front + + antispam: + image: ghcr.io/mailu/rspamd:2024.06 + restart: unless-stopped + hostname: antispam + environment: *mailu-env + dns: *mailu-dns + volumes: + - mailu-filter:/var/lib/rspamd + depends_on: + - front + - redis + + webmail: + image: ghcr.io/mailu/webmail:2024.06 + restart: unless-stopped + environment: *mailu-env + dns: *mailu-dns + volumes: + - mailu-webmail:/data + depends_on: + - front + - imap + + redis: + image: redis:alpine + restart: unless-stopped + volumes: + - mailu-redis:/data + +volumes: + mailu-certs: {} + mailu-data: {} + mailu-dkim: {} + mailu-mail: {} + mailu-mailqueue: {} + mailu-filter: {} + mailu-webmail: {} + mailu-redis: {} diff --git a/blueprints/mailu/instructions.md b/blueprints/mailu/instructions.md new file mode 100644 index 000000000..014883fe8 --- /dev/null +++ b/blueprints/mailu/instructions.md @@ -0,0 +1,23 @@ +# Mailu + +## Getting started + +1. Point the domain you assign in Dokploy (for example `mail.example.com`) at your server **before** deploying: an `A` record, plus an `MX` record for your mail domain targeting it. +2. Deploy the template and open the domain: `/webmail` is Roundcube, `/admin` is the admin UI. +3. Log in at `/admin` with `admin@` and the auto-generated `INITIAL_ADMIN_PW` (Dokploy → your service → Environment). The account is created only on first boot (`INITIAL_ADMIN_MODE=ifmissing`); change the password from the admin UI afterwards. +4. In the admin UI, open **Mail domains → your domain → Details** and create the DNS records it shows (SPF, DKIM, DMARC). Also set the **PTR/reverse DNS** record of your server IP to your mail hostname — most providers require this to accept your mail. + +## Ports + +The mail protocol ports are published directly on the host: **25** (SMTP), **465** (SMTPS), **587** (submission) and **993** (IMAPS). The deployment fails to start if another mail server (or a previous Mailu deployment) already binds them on the same machine. Many VPS providers block outbound port 25 by default — ask your provider to unblock it, or configure a relay host in Mailu. + +## TLS + +- The web UI is served through Traefik like any other Dokploy app. Enable **HTTPS with Let's Encrypt** on the Dokploy domain: the web login cookie requires HTTPS, and Mailu's internal certbot self-check follows Traefik's HTTP→HTTPS redirect and needs a valid certificate there. +- The mail ports get their own Let's Encrypt certificate: the `front` container runs certbot internally and answers the HTTP-01 challenge through Traefik on port 80. This only succeeds once the DNS record of your domain points at the server. If the certificate was obtained *after* the first boot, restart the `front` service once so the TLS mail listeners (465/587/993) come up. + +## Notes + +- `SUBNET` (default `172.16.0.0/12`) is the network range Mailu trusts for its internal traffic (Postfix relay/XCLIENT, Dovecot proxying, Rspamd). It covers Docker's default address pools; if your Docker daemon uses custom pools, adjust it to match. Note this means other containers on the same Docker host are treated as trusted senders. +- The containers use public DNSSEC-validating resolvers (`1.1.1.1`, `8.8.8.8`) as upstream DNS: the admin container requires DNSSEC validation and Postfix uses it for DANE. Heavy production use benefits from a dedicated local resolver instead, because public resolvers are rate-limited by DNSBLs (see the [Mailu DNS FAQ](https://mailu.io/2024.06/faq.html)). +- Additional mail domains, users, aliases and fetchmail can be managed in the admin UI. ClamAV antivirus is not included in this template to keep memory usage low (it needs >1 GB RAM); see the Mailu docs to add it. diff --git a/blueprints/mailu/mailu.svg b/blueprints/mailu/mailu.svg new file mode 100644 index 000000000..503744908 --- /dev/null +++ b/blueprints/mailu/mailu.svg @@ -0,0 +1,137 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + Mailu + + email since 2016 + + email since 2016 + delivering insular + + + + + diff --git a/blueprints/mailu/meta.json b/blueprints/mailu/meta.json new file mode 100644 index 000000000..3a41a2635 --- /dev/null +++ b/blueprints/mailu/meta.json @@ -0,0 +1,21 @@ +{ + "id": "mailu", + "name": "Mailu", + "version": "2024.06", + "description": "Full-featured open-source mail server as a set of Docker containers: SMTP (Postfix), IMAP (Dovecot), Rspamd antispam, admin UI with DKIM management and Roundcube webmail.", + "logo": "mailu.svg", + "links": { + "github": "https://github.com/Mailu/Mailu", + "website": "https://mailu.io/", + "docs": "https://mailu.io/2024.06/", + "docker": "https://github.com/orgs/Mailu/packages" + }, + "tags": [ + "email", + "mail-server", + "smtp", + "imap", + "antispam", + "webmail" + ] +} diff --git a/blueprints/mailu/template.toml b/blueprints/mailu/template.toml new file mode 100644 index 000000000..acade0412 --- /dev/null +++ b/blueprints/mailu/template.toml @@ -0,0 +1,17 @@ +[variables] +main_domain = "${domain}" +secret_key = "${password:16}" +admin_password = "${password:32}" + +[config] +[[config.domains]] +serviceName = "front" +port = 80 +host = "${main_domain}" + +[config.env] +DOMAIN = "${main_domain}" +SECRET_KEY = "${secret_key}" +INITIAL_ADMIN_ACCOUNT = "admin" +INITIAL_ADMIN_PW = "${admin_password}" +SUBNET = "172.16.0.0/12" From 34f3577b81a80876af1be8846ae989b8ad4e0d27 Mon Sep 17 00:00:00 2001 From: Edward Burton Date: Mon, 20 Jul 2026 03:09:19 +0200 Subject: [PATCH 44/80] feat(templates): add systemprompt (#1032) * feat(templates): add systemprompt * fix header comment: accurate in both repos --- blueprints/systemprompt/docker-compose.yml | 56 ++++++++++++++++++++++ blueprints/systemprompt/instructions.md | 23 +++++++++ blueprints/systemprompt/logo.svg | 6 +++ blueprints/systemprompt/meta.json | 16 +++++++ blueprints/systemprompt/template.toml | 16 +++++++ 5 files changed, 117 insertions(+) create mode 100644 blueprints/systemprompt/docker-compose.yml create mode 100644 blueprints/systemprompt/instructions.md create mode 100644 blueprints/systemprompt/logo.svg create mode 100644 blueprints/systemprompt/meta.json create mode 100644 blueprints/systemprompt/template.toml diff --git a/blueprints/systemprompt/docker-compose.yml b/blueprints/systemprompt/docker-compose.yml new file mode 100644 index 000000000..e0bb23f42 --- /dev/null +++ b/blueprints/systemprompt/docker-compose.yml @@ -0,0 +1,56 @@ +# Dokploy blueprint compose for the systemprompt gateway. +# Canonical copy: systempromptio/systemprompt-template deploy/dokploy/, +# published to Dokploy/templates blueprints/systemprompt/. +# Dokploy conventions: version 3.8, no ports/container_name/networks, +# main service name MUST equal the blueprint folder name (systemprompt). +version: "3.8" +services: + systemprompt: + image: ghcr.io/systempromptio/systemprompt-template:0 + restart: unless-stopped + depends_on: + postgres: + condition: service_healthy + environment: + DATABASE_URL: postgres://systemprompt:${POSTGRES_PASSWORD}@postgres:5432/systemprompt + ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY} + OPENAI_API_KEY: ${OPENAI_API_KEY} + GEMINI_API_KEY: ${GEMINI_API_KEY} + EXTERNAL_URL: ${EXTERNAL_URL} + HOST: 0.0.0.0 + PORT: 8080 + expose: + - 8080 + volumes: + - systemprompt_web:/app/web + - systemprompt_storage:/app/storage/data + - systemprompt_profiles:/app/services/profiles/docker + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/api/v1/health"] + interval: 30s + timeout: 10s + start_period: 300s + retries: 3 + + postgres: + image: postgres:18-alpine + restart: unless-stopped + environment: + POSTGRES_USER: systemprompt + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: systemprompt + volumes: + # postgres:18+ stores data under major-version subdirectories of + # /var/lib/postgresql — mounting the old .../data path aborts startup. + - postgres_data:/var/lib/postgresql + healthcheck: + test: ["CMD-SHELL", "pg_isready -U systemprompt -d systemprompt"] + interval: 5s + timeout: 5s + retries: 10 + +volumes: + systemprompt_web: + systemprompt_storage: + systemprompt_profiles: + postgres_data: diff --git a/blueprints/systemprompt/instructions.md b/blueprints/systemprompt/instructions.md new file mode 100644 index 000000000..64e28c837 --- /dev/null +++ b/blueprints/systemprompt/instructions.md @@ -0,0 +1,23 @@ +# systemprompt + +## Getting started + +1. Before the first deploy, set at least one AI provider key in **Environment**: + `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or `GEMINI_API_KEY`. The container + will not boot without one. +2. Enable **HTTPS** (Let's Encrypt) on the generated domain. The app validates + `EXTERNAL_URL` and refuses plain `http://` origins other than localhost — + with HTTPS off it restart-loops with an "Invalid CORS origin" error. +3. Deploy. The first boot runs database migrations and the publish pipeline; + allow up to 5 minutes before the healthcheck goes green. +4. Verify at `https:///api/v1/health`, then open the domain root + for the admin UI. + +## Notes + +- `POSTGRES_PASSWORD` is auto-generated by the template; `EXTERNAL_URL` is + derived from the domain you assign. +- Web assets, storage, profile state, and Postgres data persist in the + `systemprompt_web`, `systemprompt_storage`, `systemprompt_profiles`, and + `postgres_data` volumes. +- Docs: https://systemprompt.io/documentation/ diff --git a/blueprints/systemprompt/logo.svg b/blueprints/systemprompt/logo.svg new file mode 100644 index 000000000..aab4f145d --- /dev/null +++ b/blueprints/systemprompt/logo.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/blueprints/systemprompt/meta.json b/blueprints/systemprompt/meta.json new file mode 100644 index 000000000..e0fe22248 --- /dev/null +++ b/blueprints/systemprompt/meta.json @@ -0,0 +1,16 @@ +{ + "id": "systemprompt", + "name": "systemprompt", + "version": "0.21.0", + "description": "Self-owned AI control plane: governance gateway for Claude, OpenAI, and Gemini with policy checks, audit trails, rate limits, and MCP orchestration. Requires at least one AI provider API key.", + "logo": "logo.svg", + "links": { + "github": "https://github.com/systempromptio/systemprompt-template", + "website": "https://systemprompt.io", + "docs": "https://systemprompt.io/documentation/" + }, + "tags": [ + "ai", + "self-hosted" + ] +} diff --git a/blueprints/systemprompt/template.toml b/blueprints/systemprompt/template.toml new file mode 100644 index 000000000..4cca39a38 --- /dev/null +++ b/blueprints/systemprompt/template.toml @@ -0,0 +1,16 @@ +[variables] +main_domain = "${domain}" +postgres_password = "${password:32}" + +[config] +[[config.domains]] +serviceName = "systemprompt" +port = 8080 +host = "${main_domain}" + +[config.env] +POSTGRES_PASSWORD = "${postgres_password}" +EXTERNAL_URL = "https://${main_domain}" +ANTHROPIC_API_KEY = "" +OPENAI_API_KEY = "" +GEMINI_API_KEY = "" From 7aee5853685bb5394226a6a8dd4e0e6d9f8db424 Mon Sep 17 00:00:00 2001 From: Marko Kostich Date: Mon, 20 Jul 2026 04:11:40 +0300 Subject: [PATCH 45/80] chore: normalize template tags to kebab-case (#987) --- blueprints/answer/meta.json | 2 +- blueprints/calibre/meta.json | 4 ++-- blueprints/carbone/meta.json | 8 ++++---- blueprints/changedetection/meta.json | 6 +++--- blueprints/chevereto/meta.json | 10 +++++----- blueprints/chibisafe/meta.json | 2 +- blueprints/chiefonboarding/meta.json | 10 +++++----- blueprints/crawl4ai/meta.json | 6 +++--- blueprints/emby/meta.json | 2 +- blueprints/evershop/meta.json | 2 +- blueprints/flowise/meta.json | 4 ++-- blueprints/go2rtc/meta.json | 2 +- blueprints/jellyfin/meta.json | 2 +- blueprints/kaneo/meta.json | 2 +- blueprints/librechat/meta.json | 6 +++--- blueprints/livekit/meta.json | 10 +++++----- blueprints/lobe-chat/meta.json | 2 +- blueprints/scrutiny/meta.json | 2 +- blueprints/verdaccio/meta.json | 2 +- blueprints/zipline/meta.json | 2 +- 20 files changed, 43 insertions(+), 43 deletions(-) diff --git a/blueprints/answer/meta.json b/blueprints/answer/meta.json index d33d2bb5c..5e44315f6 100644 --- a/blueprints/answer/meta.json +++ b/blueprints/answer/meta.json @@ -10,7 +10,7 @@ "docs": "https://answer.apache.org/docs" }, "tags": [ - "q&a", + "q-and-a", "self-hosted" ] } diff --git a/blueprints/calibre/meta.json b/blueprints/calibre/meta.json index 742c89a98..78f3cf7b4 100644 --- a/blueprints/calibre/meta.json +++ b/blueprints/calibre/meta.json @@ -10,7 +10,7 @@ "docs": "https://manual.calibre-ebook.com/" }, "tags": [ - "Documents", - "E-Commerce" + "documents", + "e-commerce" ] } diff --git a/blueprints/carbone/meta.json b/blueprints/carbone/meta.json index 1834d5576..37578445a 100644 --- a/blueprints/carbone/meta.json +++ b/blueprints/carbone/meta.json @@ -10,9 +10,9 @@ "docs": "https://carbone.io/documentation/design/overview/getting-started.html" }, "tags": [ - "Document Generation", - "Automation", - "Reporting", - "Productivity" + "document-generation", + "automation", + "reporting", + "productivity" ] } diff --git a/blueprints/changedetection/meta.json b/blueprints/changedetection/meta.json index 85add899b..172de61bd 100644 --- a/blueprints/changedetection/meta.json +++ b/blueprints/changedetection/meta.json @@ -10,8 +10,8 @@ "docs": "https://github.com/dgtlmoon/changedetection.io/wiki" }, "tags": [ - "Monitoring", - "Data", - "Notifications" + "monitoring", + "data", + "notifications" ] } diff --git a/blueprints/chevereto/meta.json b/blueprints/chevereto/meta.json index 8bdb871b3..cb544a0e3 100644 --- a/blueprints/chevereto/meta.json +++ b/blueprints/chevereto/meta.json @@ -10,10 +10,10 @@ "docs": "https://v4-docs.chevereto.com/" }, "tags": [ - "Image Hosting", - "File Management", - "Open Source", - "Multi-User", - "Private Albums" + "image-hosting", + "file-management", + "open-source", + "multi-user", + "private-albums" ] } diff --git a/blueprints/chibisafe/meta.json b/blueprints/chibisafe/meta.json index 548b4e352..69103ade2 100644 --- a/blueprints/chibisafe/meta.json +++ b/blueprints/chibisafe/meta.json @@ -10,7 +10,7 @@ "docs": "https://chibisafe.app/docs/intro" }, "tags": [ - "media system", + "media-system", "storage", "file-sharing" ] diff --git a/blueprints/chiefonboarding/meta.json b/blueprints/chiefonboarding/meta.json index 12b1ec30e..37fe83cee 100644 --- a/blueprints/chiefonboarding/meta.json +++ b/blueprints/chiefonboarding/meta.json @@ -10,10 +10,10 @@ "docs": "https://docs.chiefonboarding.com/" }, "tags": [ - "Employee Onboarding", - "HR Management", - "Task Tracking", - "Role-Based Access", - "Document Management" + "employee-onboarding", + "hr-management", + "task-tracking", + "role-based-access", + "document-management" ] } diff --git a/blueprints/crawl4ai/meta.json b/blueprints/crawl4ai/meta.json index b0038d038..823958a83 100644 --- a/blueprints/crawl4ai/meta.json +++ b/blueprints/crawl4ai/meta.json @@ -12,8 +12,8 @@ "tags": [ "crawler", "scraping", - "AI", - "LLM", - "API" + "ai", + "llm", + "api" ] } diff --git a/blueprints/emby/meta.json b/blueprints/emby/meta.json index 87eab613d..d30b4abf1 100644 --- a/blueprints/emby/meta.json +++ b/blueprints/emby/meta.json @@ -11,6 +11,6 @@ }, "tags": [ "media", - "media system" + "media-system" ] } diff --git a/blueprints/evershop/meta.json b/blueprints/evershop/meta.json index d038c0910..025980004 100644 --- a/blueprints/evershop/meta.json +++ b/blueprints/evershop/meta.json @@ -10,7 +10,7 @@ "docs": "https://evershop.io/docs/development/getting-started/introduction" }, "tags": [ - "E-Commerce", + "e-commerce", "shopping" ] } diff --git a/blueprints/flowise/meta.json b/blueprints/flowise/meta.json index 9a1357b65..6d68e274d 100644 --- a/blueprints/flowise/meta.json +++ b/blueprints/flowise/meta.json @@ -10,8 +10,8 @@ "docs": "https://docs.flowiseai.com/" }, "tags": [ - "AI", - "LLM", + "ai", + "llm", "workflow", "automation" ] diff --git a/blueprints/go2rtc/meta.json b/blueprints/go2rtc/meta.json index 5c3704a13..7919fe41c 100644 --- a/blueprints/go2rtc/meta.json +++ b/blueprints/go2rtc/meta.json @@ -13,6 +13,6 @@ "streaming", "webrtc", "video", - "home assistant" + "home-assistant" ] } diff --git a/blueprints/jellyfin/meta.json b/blueprints/jellyfin/meta.json index d15c90ac4..3767dca35 100644 --- a/blueprints/jellyfin/meta.json +++ b/blueprints/jellyfin/meta.json @@ -10,6 +10,6 @@ "docs": "https://jellyfin.org/docs/" }, "tags": [ - "media system" + "media-system" ] } diff --git a/blueprints/kaneo/meta.json b/blueprints/kaneo/meta.json index a003754fe..a307061b5 100644 --- a/blueprints/kaneo/meta.json +++ b/blueprints/kaneo/meta.json @@ -10,6 +10,6 @@ "docs": "https://kaneo.app/docs/" }, "tags": [ - "Task Tracking" + "task-tracking" ] } diff --git a/blueprints/librechat/meta.json b/blueprints/librechat/meta.json index 6e0bf518e..6e8fc4633 100644 --- a/blueprints/librechat/meta.json +++ b/blueprints/librechat/meta.json @@ -2,7 +2,7 @@ "id": "librechat", "name": "LibreChat", "version": "latest", - "description": "LibreChat is the ultimate open-source app for all your AI conversations, fully customizable and compatible with any AI provider (Openai, Ollama, Google etc.) — all in one sleek interface.", + "description": "LibreChat is the ultimate open-source app for all your AI conversations, fully customizable and compatible with any AI provider (Openai, Ollama, Google etc.) \u00e2\u20ac\u201d all in one sleek interface.", "logo": "librechat.png", "links": { "github": "https://github.com/danny-avila/librechat", @@ -13,8 +13,8 @@ "ai", "chatbot", "llm", - "MIT-license", - "BYOK", + "mit-license", + "byok", "generative-ai" ] } diff --git a/blueprints/livekit/meta.json b/blueprints/livekit/meta.json index e8873734e..2c4ccb8db 100644 --- a/blueprints/livekit/meta.json +++ b/blueprints/livekit/meta.json @@ -10,10 +10,10 @@ "docs": "https://docs.livekit.io/" }, "tags": [ - "Video", - "Audio", - "Real-time", - "Streaming", - "Webrtc" + "video", + "audio", + "real-time", + "streaming", + "webrtc" ] } diff --git a/blueprints/lobe-chat/meta.json b/blueprints/lobe-chat/meta.json index e64ae59f2..3a8db0469 100644 --- a/blueprints/lobe-chat/meta.json +++ b/blueprints/lobe-chat/meta.json @@ -10,7 +10,7 @@ "docs": "https://lobehub.com/docs/self-hosting/platform/docker-compose" }, "tags": [ - "IA", + "ia", "chat" ] } diff --git a/blueprints/scrutiny/meta.json b/blueprints/scrutiny/meta.json index 1294de34a..515caaa9a 100644 --- a/blueprints/scrutiny/meta.json +++ b/blueprints/scrutiny/meta.json @@ -11,6 +11,6 @@ }, "tags": [ "monitoring", - "NAS" + "nas" ] } diff --git a/blueprints/verdaccio/meta.json b/blueprints/verdaccio/meta.json index e28d57928..44dea8063 100644 --- a/blueprints/verdaccio/meta.json +++ b/blueprints/verdaccio/meta.json @@ -10,7 +10,7 @@ "docs": "https://www.verdaccio.org/docs/what-is-verdaccio" }, "tags": [ - "node.js", + "node-js", "package-repository", "npm" ] diff --git a/blueprints/zipline/meta.json b/blueprints/zipline/meta.json index 9a669e649..64fcb49c0 100644 --- a/blueprints/zipline/meta.json +++ b/blueprints/zipline/meta.json @@ -10,7 +10,7 @@ "docs": "https://zipline.diced.sh/docs/" }, "tags": [ - "media system", + "media-system", "storage" ] } From 8ca2bf939352484ce4aa538ccc009cd707fe1878 Mon Sep 17 00:00:00 2001 From: Zakher Masri <46135573+zaaakher@users.noreply.github.com> Date: Mon, 20 Jul 2026 04:11:43 +0300 Subject: [PATCH 46/80] feat: adds `check-links.js` to find broken links (#983) --- build-scripts/check-links.js | 175 +++++++++++++++++++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 build-scripts/check-links.js diff --git a/build-scripts/check-links.js b/build-scripts/check-links.js new file mode 100644 index 000000000..bbb34b804 --- /dev/null +++ b/build-scripts/check-links.js @@ -0,0 +1,175 @@ +#!/usr/bin/env node + +const fs = require("fs"); +const path = require("path"); + +const repoRoot = path.resolve(__dirname, ".."); +const blueprintsDir = path.join(repoRoot, "blueprints"); + +const MAX_CONCURRENCY = 10; +const TIMEOUT_MS = 15000; +const USER_AGENT = "Mozilla/5.0 (compatible; DokployTemplatesLinkChecker/1.0)"; + +const results = { ok: [], broken: [], skipped: [] }; +let completed = 0; +let total = 0; + +async function checkUrl(url, label, blueprintId) { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), TIMEOUT_MS); + + try { + const res = await fetch(url, { + method: "HEAD", + signal: controller.signal, + redirect: "follow", + headers: { "User-Agent": USER_AGENT }, + }); + + if (res.ok || res.status === 403 || res.status === 429) { + if (res.status === 429) { + results.skipped.push({ blueprintId, label, url, status: 429, reason: "Rate limited" }); + } else if (res.status === 403) { + results.ok.push({ blueprintId, label, url, status: 403, note: "Forbidden (may still be valid)" }); + } else { + results.ok.push({ blueprintId, label, url, status: res.status }); + } + } else { + // If HEAD fails, try GET as fallback + try { + const getRes = await fetch(url, { + method: "GET", + signal: AbortSignal.timeout(TIMEOUT_MS), + redirect: "follow", + headers: { "User-Agent": USER_AGENT }, + }); + if (getRes.ok || getRes.status === 403) { + results.ok.push({ blueprintId, label, url, status: getRes.status, note: `HEAD returned ${res.status}, GET ok` }); + } else { + results.broken.push({ blueprintId, label, url, status: getRes.status }); + } + } catch { + results.broken.push({ blueprintId, label, url, status: res.status, reason: "HEAD & GET both failed" }); + } + } + } catch (err) { + if (err.name === "AbortError") { + results.broken.push({ blueprintId, label, url, status: null, reason: "Timeout" }); + } else if (err.code === "ENOTFOUND" || err.code === "EAI_AGAIN") { + results.broken.push({ blueprintId, label, url, status: null, reason: "DNS resolution failed" }); + } else if (err.code === "ECONNREFUSED") { + results.broken.push({ blueprintId, label, url, status: null, reason: "Connection refused" }); + } else if (err.code === "ECONNRESET") { + results.broken.push({ blueprintId, label, url, status: null, reason: "Connection reset" }); + } else if (err.code === "ENETUNREACH") { + results.broken.push({ blueprintId, label, url, status: null, reason: "Network unreachable" }); + } else if (err.code === "ERR_INVALID_URL") { + results.broken.push({ blueprintId, label, url, status: null, reason: "Invalid URL" }); + } else { + results.broken.push({ blueprintId, label, url, status: null, reason: err.message || String(err) }); + } + } finally { + clearTimeout(timer); + } +} + +async function checkLinks(entries) { + const queue = []; + for (const [blueprintId, links] of entries) { + for (const [label, url] of Object.entries(links)) { + if (!url || typeof url !== "string" || url.trim() === "") { + results.skipped.push({ blueprintId, label, url, reason: "Empty URL" }); + continue; + } + const trimmed = url.trim(); + if (!trimmed.startsWith("http://") && !trimmed.startsWith("https://")) { + results.skipped.push({ blueprintId, label, url: trimmed, reason: "Non-HTTP URL" }); + continue; + } + total++; + queue.push({ url: trimmed, label, blueprintId }); + } + } + + console.log(`\nChecking ${total} links across ${entries.length} templates...\n`); + + const runNext = async () => { + while (queue.length > 0) { + const item = queue.shift(); + await checkUrl(item.url, item.label, item.blueprintId); + completed++; + if (completed % 50 === 0 || completed === total) { + const pct = ((completed / total) * 100).toFixed(1); + console.log(` Progress: ${completed}/${total} (${pct}%) — ${results.broken.length} broken so far`); + } + } + }; + + const workers = Array.from({ length: Math.min(MAX_CONCURRENCY, total || 1) }, () => runNext()); + await Promise.all(workers); +} + +function printReport() { + console.log("\n" + "=".repeat(70)); + console.log("LINK CHECK REPORT"); + console.log("=".repeat(70)); + console.log(` Total checked : ${total}`); + console.log(` OK : ${results.ok.length}`); + console.log(` Broken : ${results.broken.length}`); + console.log(` Skipped : ${results.skipped.length}`); + + if (results.broken.length > 0) { + console.log("\n" + "-".repeat(70)); + console.log("BROKEN LINKS:"); + console.log("-".repeat(70)); + for (const b of results.broken) { + const status = b.status ? `HTTP ${b.status}` : "N/A"; + const reason = b.reason ? ` — ${b.reason}` : ""; + console.log(` [${b.blueprintId}] ${b.label}: ${b.url}`); + console.log(` Status: ${status}${reason}`); + } + } + + if (results.skipped.length > 0) { + console.log("\n" + "-".repeat(70)); + console.log("SKIPPED:"); + console.log("-".repeat(70)); + for (const s of results.skipped) { + console.log(` [${s.blueprintId}] ${s.label}: ${s.url || "(empty)"} — ${s.reason}`); + } + } +} + +async function main() { + const dirs = fs + .readdirSync(blueprintsDir, { withFileTypes: true }) + .filter((d) => d.isDirectory()) + .map((d) => d.name) + .sort((a, b) => a.localeCompare(b)); + + const entries = []; + + for (const dir of dirs) { + const metaFile = path.join(blueprintsDir, dir, "meta.json"); + if (!fs.existsSync(metaFile)) continue; + + try { + const entry = JSON.parse(fs.readFileSync(metaFile, "utf8")); + if (entry.links && typeof entry.links === "object") { + entries.push([entry.id || dir, entry.links]); + } + } catch { + // skip invalid JSON + } + } + + await checkLinks(entries); + printReport(); + + process.exit(results.broken.length > 0 ? 1 : 0); +} + +main().catch((err) => { + console.error("Fatal error:", err); + process.exit(1); +}); From 08d87d8a90d2c551d76e524d98f33f03d7b69d77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Krzy=C5=BCanowski?= Date: Mon, 20 Jul 2026 03:19:42 +0200 Subject: [PATCH 47/80] feat: add struxa template (#1037) * feat: add struxa template Adds a Dokploy blueprint for Struxa (github.com/struxadotcloud/struxa), a self-hosted, open-source server management panel. Deploys the published GHCR images (dashboard, watchkeeper, migrate) plus MySQL and MinIO sidecars. The web service generates its RS256 JWT keypair at startup via Node's built-in crypto module, since no Dokploy template helper covers RSA key generation. * fix: pin struxa images to latest floating tag struxa's release workflow pushes a latest floating tag alongside version tags on every stable release, matching the convention used by ~44% of existing blueprints (version: "latest" in meta.json). * fix: generate a 64-char DATABASE_ENCRYPTION_KEY Dokploy's runtime \${hash:N} helper produces N hex characters, not N bytes (the templates repo's local preview helpers.ts models it as byte count, which is inconsistent with the platform). struxa's env schema requires DATABASE_ENCRYPTION_KEY to be >=64 chars (it's hex decoded to a 32-byte AES-256 key), so \${hash:32} only produced half the required length. Confirmed against other blueprints (sim, chatwoot, outline) which all use \${hash:64} for 64-char secrets. * fix: address Copilot review comments - Add explicit \`version: "3.8"\` to docker-compose.yml, matching the documented template format. - Pin minio/minio to a specific RELEASE tag (used elsewhere in the repo: plane, posthog) instead of the floating latest tag. - Default BETTER_AUTH_URL/APP_URL/CORS_ORIGIN to http://, matching repo convention for Dokploy-fronted apps (TLS terminates at the proxy). * fix: rename web service to struxa AGENTS.md convention: the primary Docker service name should match the blueprint folder name. Renamed the web/dashboard service from "web" to "struxa" in docker-compose.yml and updated template.toml's config.domains.serviceName to match. --- blueprints/struxa/docker-compose.yml | 90 ++++++++++++++++++++++++++++ blueprints/struxa/meta.json | 18 ++++++ blueprints/struxa/struxa.svg | 3 + blueprints/struxa/template.toml | 34 +++++++++++ 4 files changed, 145 insertions(+) create mode 100644 blueprints/struxa/docker-compose.yml create mode 100644 blueprints/struxa/meta.json create mode 100644 blueprints/struxa/struxa.svg create mode 100644 blueprints/struxa/template.toml diff --git a/blueprints/struxa/docker-compose.yml b/blueprints/struxa/docker-compose.yml new file mode 100644 index 000000000..ac63097b5 --- /dev/null +++ b/blueprints/struxa/docker-compose.yml @@ -0,0 +1,90 @@ +version: "3.8" +services: + mysql: + image: mysql:8.4 + restart: unless-stopped + environment: + - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} + - MYSQL_DATABASE=${MYSQL_DATABASE} + - MYSQL_USER=${MYSQL_USER} + - MYSQL_PASSWORD=${MYSQL_PASSWORD} + volumes: + - struxa-mysql:/var/lib/mysql + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD}"] + interval: 5s + timeout: 5s + retries: 30 + start_period: 60s + + minio: + image: minio/minio:RELEASE.2025-04-22T22-12-26Z + restart: unless-stopped + command: server /data --console-address ":9001" + environment: + - MINIO_ROOT_USER=${MINIO_ACCESS_KEY} + - MINIO_ROOT_PASSWORD=${MINIO_SECRET_KEY} + volumes: + - struxa-minio:/data + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] + interval: 10s + timeout: 5s + retries: 10 + + migrate: + image: ghcr.io/struxadotcloud/migrate:${IMAGE_TAG} + restart: on-failure:5 + environment: + - DATABASE_URL=${DATABASE_URL} + depends_on: + mysql: + condition: service_healthy + + struxa: + image: ghcr.io/struxadotcloud/dashboard:${IMAGE_TAG} + restart: unless-stopped + entrypoint: + - /bin/sh + - -c + - | + eval "$$(node -e ' + const {generateKeyPairSync}=require("crypto"); + const {publicKey,privateKey}=generateKeyPairSync("rsa",{modulusLength:2048, + publicKeyEncoding:{type:"spki",format:"pem"}, + privateKeyEncoding:{type:"pkcs8",format:"pem"}}); + console.log("export JWT_PRIVATE_KEY="+JSON.stringify(privateKey.replace(/\n/g,"\\n"))); + console.log("export JWT_PUBLIC_KEY="+JSON.stringify(publicKey.replace(/\n/g,"\\n"))); + ')" + exec node apps/web/server.js + environment: + - DATABASE_URL=${DATABASE_URL} + - BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET} + - BETTER_AUTH_URL=${BETTER_AUTH_URL} + - CORS_ORIGIN=${CORS_ORIGIN} + - APP_URL=${APP_URL} + - DATABASE_ENCRYPTION_KEY=${DATABASE_ENCRYPTION_KEY} + - MINIO_ENDPOINT=${MINIO_ENDPOINT} + - MINIO_ACCESS_KEY=${MINIO_ACCESS_KEY} + - MINIO_SECRET_KEY=${MINIO_SECRET_KEY} + - MINIO_BUCKET=${MINIO_BUCKET} + - NODE_ENV=production + depends_on: + migrate: + condition: service_completed_successfully + minio: + condition: service_healthy + + watchkeeper: + image: ghcr.io/struxadotcloud/watchkeeper:${IMAGE_TAG} + restart: unless-stopped + environment: + - DATABASE_URL=${DATABASE_URL} + - DATABASE_ENCRYPTION_KEY=${DATABASE_ENCRYPTION_KEY} + depends_on: + migrate: + condition: service_completed_successfully + +volumes: + struxa-mysql: + struxa-minio: diff --git a/blueprints/struxa/meta.json b/blueprints/struxa/meta.json new file mode 100644 index 000000000..446e54878 --- /dev/null +++ b/blueprints/struxa/meta.json @@ -0,0 +1,18 @@ +{ + "id": "struxa", + "name": "Struxa", + "version": "latest", + "description": "Struxa is a self-hosted, open-source server management panel — a modern, fully typed replacement for Pterodactyl.", + "links": { + "github": "https://github.com/struxadotcloud/struxa", + "website": "https://struxa.cloud", + "docs": "https://docs.struxa.cloud" + }, + "logo": "struxa.svg", + "tags": [ + "hosting", + "game-server", + "panel", + "self-hosted" + ] +} diff --git a/blueprints/struxa/struxa.svg b/blueprints/struxa/struxa.svg new file mode 100644 index 000000000..e3f8b4000 --- /dev/null +++ b/blueprints/struxa/struxa.svg @@ -0,0 +1,3 @@ + + + diff --git a/blueprints/struxa/template.toml b/blueprints/struxa/template.toml new file mode 100644 index 000000000..9fabf86e7 --- /dev/null +++ b/blueprints/struxa/template.toml @@ -0,0 +1,34 @@ +[variables] +main_domain = "${domain}" +image_tag = "latest" +mysql_root_password = "${password:32}" +mysql_password = "${password:32}" +better_auth_secret = "${base64:32}" +db_encryption_key = "${hash:64}" +minio_access_key = "${username}" +minio_secret_key = "${password:32}" + +[config] +env = [ + "IMAGE_TAG=${image_tag}", + "MYSQL_ROOT_PASSWORD=${mysql_root_password}", + "MYSQL_DATABASE=struxa", + "MYSQL_USER=struxa", + "MYSQL_PASSWORD=${mysql_password}", + "DATABASE_URL=mysql://struxa:${mysql_password}@mysql:3306/struxa", + "BETTER_AUTH_SECRET=${better_auth_secret}", + "BETTER_AUTH_URL=http://${main_domain}", + "APP_URL=http://${main_domain}", + "CORS_ORIGIN=http://${main_domain}", + "DATABASE_ENCRYPTION_KEY=${db_encryption_key}", + "MINIO_ENDPOINT=http://minio:9000", + "MINIO_ACCESS_KEY=${minio_access_key}", + "MINIO_SECRET_KEY=${minio_secret_key}", + "MINIO_BUCKET=struxa", +] +mounts = [] + +[[config.domains]] +serviceName = "struxa" +port = 3001 +host = "${main_domain}" From 8daff07700ca4e05bd7cc2c3be027bc226db68d0 Mon Sep 17 00:00:00 2001 From: SamTV12345 <40429738+SamTV12345@users.noreply.github.com> Date: Mon, 20 Jul 2026 03:20:01 +0200 Subject: [PATCH 48/80] Add PodFetch template (#1035) * Add PodFetch template * Use env array syntax in template.toml * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Address review: match version to image tag, expose admin_username variable, dark-mode friendly logo --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- blueprints/podfetch/docker-compose.yml | 19 +++++++++++++++++++ blueprints/podfetch/logo.png | Bin 0 -> 13826 bytes blueprints/podfetch/meta.json | 15 +++++++++++++++ blueprints/podfetch/template.toml | 15 +++++++++++++++ 4 files changed, 49 insertions(+) create mode 100644 blueprints/podfetch/docker-compose.yml create mode 100644 blueprints/podfetch/logo.png create mode 100644 blueprints/podfetch/meta.json create mode 100644 blueprints/podfetch/template.toml diff --git a/blueprints/podfetch/docker-compose.yml b/blueprints/podfetch/docker-compose.yml new file mode 100644 index 000000000..2e20d9abc --- /dev/null +++ b/blueprints/podfetch/docker-compose.yml @@ -0,0 +1,19 @@ +version: "3.8" +services: + podfetch: + image: samuel19982/podfetch:v5.2.2 + restart: unless-stopped + environment: + - DATABASE_URL=sqlite:///app/db/podcast.db + - POLLING_INTERVAL=300 + - BASIC_AUTH=true + - USERNAME=${USERNAME} + - PASSWORD=${PASSWORD} + - GPODDER_INTEGRATION_ENABLED=true + volumes: + - podfetch-podcasts:/app/podcasts + - podfetch-db:/app/db + +volumes: + podfetch-podcasts: {} + podfetch-db: {} diff --git a/blueprints/podfetch/logo.png b/blueprints/podfetch/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..988448907ec5a8f9ea5db08c89047f8074d4bcbc GIT binary patch literal 13826 zcmZvDbzGE9-|w}slz<=z2+|T#(jd93C|%MW(jcJ-(!0V9iXz=D-3UmxinMe|gMf4` zwZQJV(C2yI`+Uy%XLshBnO{$TCsb2if%F>PH2?rel@uRo0RRq|!~qEL!9SGOC>j7n zyOkb2(D6#$o+U^%(f9wok4;NTE8S@BZ5F40)f>J3J+f^rxT#6Q$4rs1odwy>z7Xg@ zWMQ?0q*Q)LLBYtz!cW%OF4D^o##h`syWvsIQ(807=bz#Jqq(X|?We;@y42c9ze2lF z>1W~EJ}d&ehWN1$)$4YxABs2^1>iC{1DK}HsA>YvJ5THTnK;^T+n_>geyC5GG*h@k31s3Z&2t6r@d^4R?hFk{CF{L3aW$ zD+#R)YkIM=0i99PC>F_M2y?s8+MHTy3Y5T>Bv2L*;fQ`cqzK@AmEOihQYIH%v;V;f zKm!0b?U?0IWR+`r8yVmxal>s#jm$Ntf(#Ha8}W8w$NAATwfKQ4Cz}ayd#JWHyZq?c zvr}=U1p+`PtFWE&b%?(^NCVC%B5KbX8IAvFj{qRo4ln;$nauM23<7w>I%hh<<(i^C zl8T%{fUFo9*jXri=?y@L0f46(XB27eB+jS8dB+goX^af^edvemP`^cxB2ulANEJ#U z4uyjhCvES(rdoew!z;HL^9l(8cDl?WQn{mo2=gf}U?O_! z5ZCS5R41STs-&o*8i!PU)|vhV1^_Ok5RwwB872UE-|FcvBA^{iomyGt{7C^)Ppz## z5a4dQSd4<-Dedcjm$QcUM_oj|wj%;5sp|G_Ze&15{r`x7^slJe837811<;lP-FmdyP`Vp zuN>$CkFZk1KaDe69BG}brbx*F;9D_=X|R77$b~$q=6!Jnm1c?|AnOcLVN)8D1N>W0 zDYHPuZWKM$hG9U;{i~Pg&iu4$B z5kUlI49YHt%T>8*MsNWD8I%_bT&V;XQWdq;h5$g@15SF*``;&F01!IzJkShDIe?RS z1ab=b3MvL%mvr%7m>ihvJp?NdjXj^bjQQ@(4Hn$DAO0(M^tM-p-vI~w-4$QnkSmb} z|LKRwXbfEt0PyTY&$Cn8Hl+}t0E-I@cHII;l(q-U6v*^^%-c=`0MKh zNM`}R(LVxNAg5)3Ffg-Vh64x1Xe(twW=FKXxiTm2m&9@&;1S4r<{>3BsNo-Wvm9`I zkgXd(gc*YiEZqE~2RRW);`%|j^f4IqAhAS}AtnIGKfwVarPJRgh+)^){5QI@`6p`n9>rzk%JE_H4b0f1SEj{KCibllFy)Yj*?*C)vt6Gd=~c zmCH+@n*#qR@GRG5HOWq;$fiXdnoMoNp0u9NfZdHabB(zbmLP@BBH2Zl5MaKvLApI$ zxz36UqpczEXiW$}k&OzS6@NXUqO|GIS{#Giy5Ny7f}p3b#asx-pfKtOm)uczIVc0n zLas{}Vn1MLumOGZTx#5kgurwZ0MzirpXzp-cFdJn)0B;CoTVp3>r5|2?ebm6&EX(J z#b8pG+3_D_o(M&ox>aKaR6CXTF9XZ2lWZIF1X$XmoUWVk15)0z5g1o3k2bxAZ!UqZ zMcy?zKdF_@r^Lg;zVcXRjzOAGTuqERD=ez`%EhGK>$864J5+a3Oysb64GO>@9148$ z3xhTOK0+f>5=?1Cj)T9H> z34Hhx+u^a7@jT>ZDngrFB*+q(gO9N3JJ;M*YgRZiQ{aCDWxlS83tgg!{i^#4(c5Dl zmp8SBmv;2wCcVn|y}jY#fa7zU(>-}wuWj>O0k1?B2(vsJ08d@MRAU%^-d-B*m`-)^ z;0_*kKkhpJ2*=b}f|Bj`_2H_W>-%paqM(4_e#7`MF7WN}7feO+Ufp9YK6lmZi~zLj zw*_AMV3R+FNbHU#6aXWIipntq+siylpKX-hL+D zQ}N-|rgdgrEd;5^R`-=q(SC10J>l3LcDHamxO?ZIyraV2)IR*NXG3Er&Re2ax5-x$ z5qYMP_ZrlOo!=9BwF^G!e8@oQ@k|j1`P=Kw1cou@`RljeFY?y~vGvdft@ap%(lat$C?+S9kfh*Mldyb3c~O}5H8ma@1H6i6N802|+5qVg-NUVde=!E#+ENIhYZ zs98MJx+TfFRD}RE`E>wfgLnfUVPI?WMkn8^i{-eHPGtt+A^`3$NJ?HnESk&5x$eMY z^YctxQf#dJTKmlh9)00ujVcE+;ncAs5G5sbm3{r*r_x9w+TJTal3Q@!L3I68V6{wMwcd_$s_{7sm_T%RdV0J{BWXOiW^;gamQ7UU#3QimRVo zThXeU5BgS-mBdRgIRTQk5+GC0O9bNBlTf3LP58{~U-j0trtNX?XvLwwgW<6r!^OE7 zM(hnm0gEjJsLoBzLSys95JfL zVi(S*OO2p}ETxdK|u+Bim9RM~QSFfT7B!t)~vC2M>hu#efbe^$7*~k`y~~UY0P=F7S>w>Iwi^@DkR4n1_HT>HK{0o%%`?n^D5$->`r4?Au-c64~TL^ zby>}z6GVr5S-TQi^cIV!B+4HNG8a5%>Q;Ti7((COwUshC4p ztIB1{t^!40c!5l<_CS#l{w9fA5qXeLJ1A6ZtnBKnMv-&8#40?u0074JhNFAb*KWzH zGPx%MuZtqVPnzx+!!v%Ljl?Qp=0Ac_w2#=GueWlU<+0y=JQ8Jwu;m9l`o=2wWoUq& zueNabW1Y4WcHzUYOsQr#4moXAZn`KGmn4KQYQ;#;tNGLER#Iyg?tE zP6=o$-~h2aHt{*murt%>qyVi?5b|0r>(PpPzm6*O*WeliI%`n4VIm%&W8ia4{8jq0 zvj{~mKo7*eHw57I@*TI82lmT|z=@Fi@hk-Z5P*X}u)sVH0V3^7Y;bhIe)r#L@BwYr zds-xy_6fPT|}dEx&%DF-QnP2K*^s!yo_O@}p!t+o=9Ojez%@0uYBw3#N- zFMb9E14w0?0&AR=<(3MCbsv)dn+T}qm13SB*>MX9slb->-$0)~iv_kG5&;F!KVSZ= z0fGz&fC3;0I8Gw~b?x~I)-oZHmB{v#pizxqTR z3mSQAzMrm>YYP5Z zB>gqkUweaH)Fzv#jZp+-_LKq0#8HKQ-j)6&hO5a47Ok+Nr-K7YG;jF)uZ{k!O#ZUI zdq=jp3T`+mlZ|*jvtUo;?JVX)u~7_RUr3OcJ*-Zh>JH_W$=`&!8l6Y$zAtnNF4m zD);e^1%+fCgf%%lCidzah~4kq=Y-dTJq`@FKRo{W<&Ou;+@%KaoN3{Pe~L3nz*hGd zCAJ@cKD_xawS-Xpz)JR7U&~>e9qs>GDjtj)63{aX%zw-Zx>2>?_&2%#=q8-} zTBxuwBo26H`2%kadY9(E0jSgd{`9xu!Dzo3;923K7m|H~N5M&XLJW`IZ^v~rXD(C( zG%H>nhP{m@(eP_!4*r-<7{I8RrsJ9HWme?1J#HMZr#Oqir=uhSxNEi&u!5D@37fP0 zjhIat)^yG#ZEs|7pp*Ol?cI3Q^T4ivEvNa*m97A4I$|~CCu8z0T|D3x!h14-gt}ap z*!;F8_t(eoz7fJSmoXN)u=$f9G9TGr%fE7kIu37~dwl%1Pxs7gUXhI(fai*9G_Z$+ zyS_Ci=Z}kT|H`j?lII$_mfOMqOF{siB=jp>@tRYa?h%SVoy4=1d;4qil8q6o)9J%5 zN*L^}x5tHcQKUov*bRwS^Abgm@c>=7!|cwJLp0InwK#12$FhVY%DXm`w|=YpPQ=_3 z-+eHOPXrV#oW3c)1~1I3GY?)iAw!1G2Nhhq(t9^*`n|@tCxaS4#Xb@Vm<6X!auhj< zG_8H{B`<(hfd=%O|M)vRJ%h&Fe_^`vI(*VY{m}5)FkjVHG9;7#eQJx|on`_)?_EVk zZlEBM=JA=a?tOpra>>b-XS#z^FF9#%x`q~;x`^?T&yvam{&LCfU*ek?j$bw5_qVqO2X><#y0qzVq%uW> zzvB!xX{Gl)QfTjfizS*giD_w(AFD1Zo4P9m)a1&GDgu$@0o}=K`-p`%)D5ofz)9<}Pqm`z!A`JN1*W;6(w_bY%?d3yl z`x{+rAR0{XdEX$WrZo2+HCMGfdndF2$b3<>MZ4(LWOUjQ>+76^&Y`0q9&TQdTD3~l zSRzae@>{SwxvBA*LDjveMgGot6qh?04iz}`a*O#{zoDNx-W6KzdzIAjd;?{fQ=_Mg z4cPoLp>!V2lk@5%W-0W{s3Z`-fa5u-!aaqzS9?w_H0w!Jl(IG$k#EOw25rm-Vhq+1QHm6+4!`X*o z$a_MDJdXf5;HL8ql!az2MnltR+n_>TdKTiWu*W2U--2p7j%i4Eokl@YzJ)k&&#Qbo z`&^e8PK6RJe0+p{b_@M*DGFQT+QFIM{OcZDKPMrPy_zcy$)BC1fd{S^}QIKA) z_ci54f7B?IrN*l1LBRU#zSnaqG2;R*OGkRIPltFicFo>D*7H~dzQ|D01u_~BY#dRI zR_Ju6oB_8Kx%N_p9Z`J-qv=!=0)Ojgte5(ALrF zyfO)o?dm>QZ5`7if3mQ%^$kYtljMfACE4p2(chSG!Qmfr80(2zVM@e?TYsb;xma7n zXEpI{R&9^@=tv>ev~Iq|XJgE2vZ}|x${@mdoBLXH@_yc7z3Xpmy2nmA zfi`~1x5taVyFXFR%|j7oVZWUN_|$+vC#S`++MBPCB_~^y{$#nyD6Ot5F>k4;X6u2o znS9?H&w#zRD5e>Tb;^3Q=Ch<%W!68}2u3QNIY%D6@0~6}=1Vq=y=kPw)`OIO*zFG=IN+mUj^SDwkKbgR z=e^`@epo52KaL0w5YUhw5mPAd@m%QkMQPU%an#_$s*Ekxs>~-k6~yuJsc!nlBpB`Y zzvx~}#5TCzyuJBsHCHV#t4z?NG+3B=W6pRv*XDPZy1&M{E;}DgT```dp3PBdS8c}| zJf&frGAP4XpbmXZk@4PZir9~Qo@98jesxS*5KZYMj~|U+VfKS;%)gDuJXN=TUYK6a zeRa31wwMha_g?$$#2fvwG;(*-x4Z9rZG^76gB);F!SkO_XeOJtOuxuB_{jg8Zs3Q( zy?*zBL8c$$+cyLt)pN~CI4$?27`-x3JD4Oxg$JZWlc`OyDy=l`e5f5CmW$}8r|}>w^eq%De)#q_xR$fBZAkIim_F$;t-L^&L(}?>u!AIxmH8P5 z91>0Y#zSffB|}$7G{+aetc0)!u$b|BI~C8>cfVZN>7}PO`TQGed&`*Stv%gF zjqhEL)y=ihbm70bOiq-%9Q?_sUQMa?2+BP*Xb9ns){qhHCzP#YMMt4S=MyAoE^1r~ zHb;4S4Cjai#4LOqB=$yWatp0qrz=Zfhfm(;>mhCnhr8qs7rigC`1R&j+W=Ns!&1Aw zweIE%oEJiTb)KU~Iba`oaeJ}iMB1H9^qD!~(syvIaIk3$1LK!_*0_~Zc)&;hWv({E zokOAR9PysT5A)+p3re@_t_P1rFQ#7#%^XGs_?V$r2a+aZ9K?*7ABhk&&s5*~a#-y7 zKza{lu7bs=3C&QF;Ie0qAU^MfPLE6scL|nbHLmYcdF&SWb{*<0S$k)*QG{LtHrn{{ z12I7Tym?7}X^rypWFP@!e3th(6DxOEHuomO|BMca= zrWdBv9t6#H(_XuEq~q-Ae|Q*#Rw2m1yu(~x^v>~@67$Dyp>o?#@OM=R3Va9(qg+sX zCf6Be*)Px2V{l=OU;M!B=t$&>C+fq|`fS8R zz{Ukz$}n-u=RO&^;rFb zyV?k~N^esYTUy$^!+YMl`}y-9dCZv9TU9#<3IC>^|u0W+C2tuYJUWkG9SE>s#b} zZZTJ$ojsr!ls?xn^qi$xsmA(gJY7xGPCDKoP6O*3l*ZqM$?#WVQqeu&zdR4dC)C?O^PWl=de6#4 zr&X{X;7fb>7x_M(71rB*_cjEM2OQb3#&4mA`d{=kwo+~n>1k*NnDNhL1&}gKMKygy zQlbb8kT>v`(#U4*mA?l!^+_J42|2GMXXenC7|}puRnMinKmVXTa$QABu}}EA=?oHK zT?U-UR3>V$0=fIcFFuPB# zVH^~`zOjEu!BWRzw3>QPWgcay{UFJ{Ag63hD9TETF`+pE^Z3()SWJ+8fNUzb_*3lN z=b6LUjOfILyAjVYYp&ooSPy8pCV-Qo^7o?KI)nTIQQhNn8Cwn$aaLo}YdVI0+iB5H zc$7BE6%5d1JE7b2hzydVI4m8XQg9qa-M3p53Z>rVYvBIWMOS&;W`j`W`(pU((yCnV zV}~2!o*j?M5s?{t`gJTC>*a0^b40DJ?YK|w>pn5Py+KFx#vm(QFnfn|R<%%{{@Iq* zb*V^zS*VC)9%14BoU`d}Z;It`#X{w6aC0&h(b67+Tvik0{Uv|tSnwjCz8wb3DJSx9 z{-^!EWP9_%IeKoeKOX)_5g-qBW3Nu181HH6gxSgdX1y-Cs-$ndpFEvwH5GTKxUpTS zA+$1bWFoME0~LuFRr=_P)*hAlxbtC86!Ju~x?C}UJd?WS36qKJLbDUBRBGnE)96>)MG1XjcIwg=QG*=r4eLRZ^CFy&J4Au7taz4-JIanf zlhZd)F%iA*Hg|7Nt@8@Cqv{`;QI|C+T?@9H_3nS?w4=G!5Xm%dgYY2zM{4(R%cA-x z;+zJWZz?LUpCcZDM@x^?2TXT;)c9F$_xjn*LKDU{UaOQfejf*4EopW@u{zOYsh$1fc z(xUTuqj;nbt7krTJjnKOjShD@*=n@b-VPJOBz)UXXp-7ScY zzSZ4x0+>F3>lV$gU_{fskPNagc|2hMT5@?q4W`lI`#U}cQeT{FlA4Li@NRiYMVG4^ z74FkF(>FUQE)>jS8piuwmwXwJ4{?xQc(a29h<)AlnZ1Dy8{g%RvXT7*<|V#3WweFB zo@E++d%f10dYYWay?#-fAFlLYGUn?SjnPJSwXjK~2VkL~ZP8$OUG|(*skSdRDc_ui zw~sA6k_j*jA?HPRiO2|SU_}r#8XC5yYu$x!|${|Kh4AqR#r~VDfDkC@jCanMVwv; zw9h|v>~&zcu;U$BQ%#c^>7#YsBfD*OB8!n&hoSs5ZL(^i`*$EnsxK7lQ3;5>#k76$k4h6qy}zwzSg{@fo$<=Utq8E?u6MT{8G z6)%VuI8CZ@*;Fu1zJUVq`$rG?0s=HWrB7M9$r$<62i_;pkf_|uvH7w7Vxt_PPi=bi z(IS!A;QreroExe5?u@FmvlD9zqu4VBha}$+uCvacTQr1uw~L(gO}}m!iSd|zPwT?T z22aY18|#hNGI)CDE@+i(i~=jnq*m46I6>+C&lHAVSya7rJs()mxJ6Nr0glFpedh5s zXKP(3?K%4iAJPYhMLq}fXM=n`%?Rq)z*0rPqI55)%I3JRa)|-K5bJN{vcdg?&dcW& zjL*~<`&IRjyTvmdU6k+XqDs}cs+)uuH{6XqW={x{BG|uexbAv>GS83u#+>!^tK-~w z`3RKIcfER;SOV0bf!FuYjy-Ps({Ega^>N_!(=(Q-qwMLI*_l6o<)V(V{c=0iOUXW> z`QuymDLd8!e2VfO-xd#c(tn17`Jxp6BAs%Os&RfvS;xZ78;)@P3espIb(lPJ`^3Kdg1faX7j=wWzgz) zT(w(hu56|)kKg#JYazK#{nAomESc`a2?+1yC)p&s01sTdEhdVGr8n6&1KYZs z_Sr|ojN_ZvwTEj@B-W>E&90Z7uTJjX3*hIt&lq6lIxDrCH6K+2F4(5Ch=X9lUtwZt zuh^TY$Exd>37AAb6r=f`2Uv=`_D+A8kgXd833iSDWC?cTj867k^uY}KBi6vmi;%r5Ts$P10AS1mQa{hkBB|n7ld()bZ zcJaLR~-@bQ#>a$%HsS7bwSpCdA2Zu|Mq10EYNE~1awX(BMI|D=wz z?`&6Iz9xg(AoS33m0rdz-^l?A4g-cyh5lt%V~yfrNnHE=XFbVWft19ZbqX~l+W{B| zc=!=FCo`bDn{M~r4?B4h9Xr&p+`4lN?sLzVCX^a%8v3M{(F?|-II=O9{uM9#u%c6E zK`GXCCz=9}Ruu6FNUyANQ`&hw9a%feH?^+PH{onRjHk2STes)#j&+C6!7N8ljHO#(Ke}*q zzIGn>7SEsKrhR^LtPt&;q67>ARk*P=cx!kXNzNQ_b~Yk`QEh*6YYwm3BK^$sV1oV; zy8%Yp`=~EN;I z`J>MAMYHcjZwqHjL5xTsEE+_AkXAdIOn*7F=`eN8M81}J&bTOW-BlAm`f76%o*$Gh!}0UOtNcgHW+6T&4Y0>DJ|3!Mys?6 zuuWsnvxjS4aX0U0T1Eny`(otDz9Fe79&@5}y=^0bf~SsezGZ%c4W*u%AZke%rk-fs z5RB4~D}Gge`7$ZxX40-{C0Y+WDifgS2tr6nS=Cz`*{SbQsM)mXnDg!|WE3rFM)Q|r z=3(&|xq*~4!o#o6TZJ+?n)=Oi34jm!MD}Tjnv%L5_Xirt7407GXUx1FO~EfllX2rT zo2g=2{t^5NbUXY?p`-ex`-z|Ar2Ck_H5P+wY&gwC3XbC+r075W#!2!&ma;)BIR&yY zTUrgQ+?73w(Cwh^IXV@mWHy=A;M=NN{%Gtmm7$h4_;$<0oU-8Iq~%=gt7eSoVOYM+ zeo=(Tv7n603+=^68S?6h95;7FBcpIN5NhaSSCYV1I-{ow3L=bRxDSK0mTS2#Ez78< zpS)9oXOj}8Myfn$ddNQUZLU+6-*mdgwWTbcm*X`1K|P9#aH8OQeWr>TJ7gNO=p)gz z;=3v*BR=^_WhYBovdx!^RTL@3nI^kzY1hW_ihm@gLP zTfP@&ru2#aKN{&v@hwnvM2=0ZN}M6+pM%o-ZwMqb?Y(H1U^4uhIBDMU7rL4j=&Ql==#q*cT(4!(bHqU05|2yq0&6Udx%5MT<4)+ zTVJC~k^7!UQX;X@?=*II>};_7EgY8wHPC5F4Y5s#J$P#G7VUFzNc>fjR?}%TH%OpfP&*43^v%^Lr?10o# zlZEB&T;k)@kO+pzpiRt!@TP-LbC4+S%eH;*+gMwwDgenfKy7fhze+uN#T9K`Fe@Y5YnjDzIUz3&(J*Vu$>Z+nxz->f74=fU676ONeFF zLRPUXGlL33DOr+QleC~(@GGzXxws0py|nUVpFb}B*7vj5LLkF9t!YwU$MAY1o=j#w zclD@oZ@cd3F{U&&WW{`2DBk8hXlL!{&Kp>9#Nv=X6Vdn6^$mieb}c)&=rk#fm2M2l z%a#(v5z{2^NW3}M;XRD0D7^H9{?-{!{gNAwGFdLy^ri>R%PcmnTRYlE0TQQu^Swjc z8ovcGAIbPMgzUcJGjnBufhD|Rn zM!@nKz5n);?jsMS-8qjJzjB!(uu>&+1f!|7i6qJ?E#UuEzBfI~&kzo4$kxxpjP zI-ig3h<|cOP9T5;CNf&3e{eb5aq{VWsL>|#u!9*wHx*p|vWk6m2N&Gp0ZTeYbQw7n zeBgoas`-UfN3Ls%9dBHCZo*r&@NJ);()|SG`)$JWu_oqgq4VPXe(-0C0I&&eqY6I1 zf@2*dd4$!?vV+#5om^sdYGk~8!iy*@0`x2k^gUq*-VA+X=<`KKzm5q*A=O11ofmSA zH!LVY+)2rD#C5b!`^RXDWG3)1gJ~Gs{JZCNAe7n1QPrHUL+U_Cv&R@X9zknDZKnC9N8vvrgOD}+1 z0Uuza1~J7tuSoz!?*9je0z%sg==lKn(tWeQbfFT!X}KolP^ z^rRx=UmzJFG%pNBP#26!0 zt9mcyFSNgADZ6 znFucE4}paFro}qJ^gn(}ZXy!+b_E?LwNw5Opb-#a2}&2e^v*ck9Hal&*o8(mU_>$p zXn&ac8yJQf#~J~v(ndHC;L|8o?K704IC199KlpIXaq>2*T6LK#;QJ7lxcpy8dyhI$ z02;s|LHVh>w+sL=x)QGDT_-Bz6jlEV%zWGGF&5Xo*~?P|*#83pzbT5BINEryO_f(_ zkIIWyB!U3|BJqo$$g9}eFA&4mH-#{R=x=7D>bK2t;*j`*bwVikR>X0<>*SPkfCcng z=8rP;ox)abxUab@V!_=ayAI>TmQCoZci3a|KFHUcPt>U`KQ#AOKQWZ?=W+`1H0DURxe6m{$=Tct`W(og-+A8>N+Odv8j7 z1V1Y5S}20CbYE}QQh%@3mXMi<;2QX1{VDAmZ`4_o}nE=Tukakq#>JJ(U202P`=%76(0HXFMMBrOW z7q{;obvf&Rsvo)cb4D|r%8?$*E0?n00R1<(fq(6SR2aI@6N&?Tv+@Y~9vkAPom&7t zKH#oscz}y2_&9IEMF9cYbV^+I+L&G}sl^-lPCKaHh5}(#Xi9!bD#|pUs@(B_)JLF9 zmFRo5IEb!panpBTcINZXA+QO$KY Date: Mon, 20 Jul 2026 08:24:15 +0700 Subject: [PATCH 49/80] fix(docker): share plugins and themes volumes across containers (#990) --- blueprints/billmora/docker-compose.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/blueprints/billmora/docker-compose.yml b/blueprints/billmora/docker-compose.yml index 870629e18..eb5828d7e 100644 --- a/blueprints/billmora/docker-compose.yml +++ b/blueprints/billmora/docker-compose.yml @@ -28,6 +28,9 @@ services: - AUTORUN_LARAVEL_STORAGE_LINK=true volumes: - billmora-storage:/var/www/html/storage + - billmora-plugins:/var/www/html/plugin + - billmora-themes-resources:/var/www/html/resources/themes + - billmora-themes-public:/var/www/html/public/themes depends_on: billmora-db: condition: service_healthy @@ -58,6 +61,9 @@ services: - AUTORUN_ENABLED=false volumes: - billmora-storage:/var/www/html/storage + - billmora-plugins:/var/www/html/plugin + - billmora-themes-resources:/var/www/html/resources/themes + - billmora-themes-public:/var/www/html/public/themes depends_on: billmora-db: condition: service_healthy @@ -88,6 +94,9 @@ services: - AUTORUN_ENABLED=false volumes: - billmora-storage:/var/www/html/storage + - billmora-plugins:/var/www/html/plugin + - billmora-themes-resources:/var/www/html/resources/themes + - billmora-themes-public:/var/www/html/public/themes depends_on: billmora-db: condition: service_healthy @@ -126,5 +135,8 @@ services: volumes: billmora-storage: + billmora-plugins: + billmora-themes-resources: + billmora-themes-public: billmora-db-data: billmora-redis-data: From b4542d82f828eb9492e8ea321cb0b42c41f9c898 Mon Sep 17 00:00:00 2001 From: Marko Kostich Date: Mon, 20 Jul 2026 04:25:33 +0300 Subject: [PATCH 50/80] feat(directus): upgrade template to Directus 12.1.1 (#986) * feat(directus): upgrade template to Directus 12.1.1 * feat(directus): add commented S3 env variables to template.toml --- blueprints/directus/docker-compose.yml | 98 ++++++++++++++++---------- blueprints/directus/meta.json | 4 +- blueprints/directus/template.toml | 33 ++++++++- 3 files changed, 95 insertions(+), 40 deletions(-) diff --git a/blueprints/directus/docker-compose.yml b/blueprints/directus/docker-compose.yml index 1f4572dd3..ef27d2ba2 100644 --- a/blueprints/directus/docker-compose.yml +++ b/blueprints/directus/docker-compose.yml @@ -1,34 +1,6 @@ services: - database: - image: postgis/postgis:13-master - volumes: - - directus_database:/var/lib/postgresql/data - - environment: - POSTGRES_USER: "directus" - POSTGRES_PASSWORD: ${DATABASE_PASSWORD} - POSTGRES_DB: "directus" - healthcheck: - test: ["CMD", "pg_isready", "--host=localhost", "--username=directus"] - interval: 10s - timeout: 5s - retries: 5 - start_interval: 5s - start_period: 30s - - cache: - image: redis:6 - healthcheck: - test: ["CMD-SHELL", "[ $$(redis-cli ping) = 'PONG' ]"] - interval: 10s - timeout: 5s - retries: 5 - start_interval: 5s - start_period: 30s - - directus: - image: directus/directus:11.12.0 + image: directus/directus:12.1.1 ports: - 8055 volumes: @@ -39,25 +11,79 @@ services: condition: service_healthy cache: condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "wget --spider -q http://127.0.0.1:8055/server/ping || exit 1"] + interval: 10s + timeout: 5s + retries: 5 + start_interval: 5s + start_period: 30s environment: SECRET: ${DIRECTUS_SECRET} DB_CLIENT: "pg" DB_HOST: "database" DB_PORT: "5432" - DB_DATABASE: "directus" - DB_USER: "directus" - DB_PASSWORD: ${DATABASE_PASSWORD} + DB_DATABASE: ${DB_NAME} + DB_USER: ${DB_USER} + DB_PASSWORD: ${DB_PASSWORD} CACHE_ENABLED: "true" CACHE_AUTO_PURGE: "true" CACHE_STORE: "redis" REDIS: "redis://cache:6379" - # After first successful login, remove the admin email/password env. variables below - # as these will now be stored in the database. - ADMIN_EMAIL: "admin@example.com" - ADMIN_PASSWORD: "d1r3ctu5" + WEBSOCKETS_ENABLED: "true" + PUBLIC_URL: ${PUBLIC_URL} + LICENSE_KEY: ${LICENSE_KEY} + + # After first successful login, remove ADMIN_EMAIL and ADMIN_PASSWORD below. + ADMIN_EMAIL: ${ADMIN_EMAIL} + ADMIN_PASSWORD: ${ADMIN_PASSWORD} + + STORAGE_LOCATIONS: "local" + STORAGE_LOCAL_DRIVER: "local" + STORAGE_LOCAL_ROOT: "/directus/uploads" + + # S3-compatible storage — uncomment and set S3_* env vars in template.toml. + # STORAGE_LOCATIONS: "s3" + # STORAGE_S3_DRIVER: "s3" + # STORAGE_S3_KEY: ${S3_ACCESS_KEY} + # STORAGE_S3_SECRET: ${S3_SECRET_KEY} + # STORAGE_S3_BUCKET: ${S3_BUCKET} + # STORAGE_S3_REGION: ${S3_REGION} + # STORAGE_S3_ENDPOINT: ${S3_ENDPOINT} + # STORAGE_S3_ROOT: ${S3_ROOT} + # STORAGE_S3_FORCE_PATH_STYLE: ${S3_FORCE_PATH_STYLE} + # STORAGE_S3_ACL: "private" + + database: + image: postgis/postgis:13-master + volumes: + - directus_database:/var/lib/postgresql/data + + environment: + POSTGRES_USER: ${DB_USER} + POSTGRES_PASSWORD: ${DB_PASSWORD} + POSTGRES_DB: ${DB_NAME} + healthcheck: + test: ["CMD-SHELL", "pg_isready --host=localhost --username=$$POSTGRES_USER"] + interval: 10s + timeout: 5s + retries: 5 + start_interval: 5s + start_period: 30s + + cache: + image: redis:6 + healthcheck: + test: ["CMD-SHELL", "[ $$(redis-cli ping) = 'PONG' ]"] + interval: 10s + timeout: 5s + retries: 5 + start_interval: 5s + start_period: 30s + volumes: directus_uploads: directus_extensions: diff --git a/blueprints/directus/meta.json b/blueprints/directus/meta.json index c9b20d20a..f7757a478 100644 --- a/blueprints/directus/meta.json +++ b/blueprints/directus/meta.json @@ -1,13 +1,13 @@ { "id": "directus", "name": "Directus", - "version": "11.0.2", + "version": "12.1.1", "description": "Directus is an open source headless CMS that provides an API-first solution for building custom backends.", "logo": "directus.jpg", "links": { "github": "https://github.com/directus/directus", "website": "https://directus.io/", - "docs": "https://docs.directus.io/" + "docs": "https://directus.com/docs/" }, "tags": [ "cms" diff --git a/blueprints/directus/template.toml b/blueprints/directus/template.toml index 10738332b..497f96b98 100644 --- a/blueprints/directus/template.toml +++ b/blueprints/directus/template.toml @@ -1,12 +1,41 @@ [variables] main_domain = "${domain}" +public_url = "https://${main_domain}" directus_secret = "${base64:64}" -database_password = "${password}" +db_password = "${password:32}" +db_user = "directus" +db_name = "directus" +admin_email = "${email}" +admin_password = "${password:24}" +license_key = "" + +# Object storage (S3-compatible) — uncomment when switching from local to S3. +# s3_access_key = "" +# s3_secret_key = "" +# s3_bucket = "" +# s3_region = "us-east-1" +# s3_endpoint = "https://s3.amazonaws.com" +# s3_root = "" +# s3_force_path_style = "false" [config] env = [ - "DATABASE_PASSWORD=${database_password}", + "PUBLIC_URL=${public_url}", "DIRECTUS_SECRET=${directus_secret}", + "DB_PASSWORD=${db_password}", + "DB_USER=${db_user}", + "DB_NAME=${db_name}", + "ADMIN_EMAIL=${admin_email}", + "ADMIN_PASSWORD=${admin_password}", + "LICENSE_KEY=${license_key}", + # Uncomment when enabling S3 storage in docker-compose.yml: + # "S3_ACCESS_KEY=${s3_access_key}", + # "S3_SECRET_KEY=${s3_secret_key}", + # "S3_BUCKET=${s3_bucket}", + # "S3_REGION=${s3_region}", + # "S3_ENDPOINT=${s3_endpoint}", + # "S3_ROOT=${s3_root}", + # "S3_FORCE_PATH_STYLE=${s3_force_path_style}", ] mounts = [] From 285e667c68668c7774cb0dc9d11216563210f254 Mon Sep 17 00:00:00 2001 From: Stefan Ernst Date: Wed, 22 Jul 2026 05:56:19 +0200 Subject: [PATCH 51/80] chore: update Windshift to v0.8.2 (#1041) --- blueprints/windshift/docker-compose.yml | 2 +- blueprints/windshift/meta.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/blueprints/windshift/docker-compose.yml b/blueprints/windshift/docker-compose.yml index 81008518d..c3f52eb14 100644 --- a/blueprints/windshift/docker-compose.yml +++ b/blueprints/windshift/docker-compose.yml @@ -2,7 +2,7 @@ version: "3.8" services: windshift: - image: ghcr.io/windshiftapp/windshift:v0.5.3 + image: ghcr.io/windshiftapp/windshift:v0.8.2 restart: unless-stopped environment: - BASE_URL=https://${DOMAIN} diff --git a/blueprints/windshift/meta.json b/blueprints/windshift/meta.json index 422edbc53..32b025d27 100644 --- a/blueprints/windshift/meta.json +++ b/blueprints/windshift/meta.json @@ -1,7 +1,7 @@ { "id": "windshift", "name": "Windshift", - "version": "v0.5.0", + "version": "v0.8.2", "description": "Self-hosted work management platform with projects, tasks, sprints, and team collaboration.", "logo": "windshift.png", "links": { From 0c336682672d932cffea37e5b2493e9018b426fa Mon Sep 17 00:00:00 2001 From: Jiho Lee Date: Mon, 3 Aug 2026 19:02:30 +0900 Subject: [PATCH 52/80] fix: bring scrutiny, stack-auth and vault in line with the compose conventions (#1048) * fix: drop no-op container_name from three blueprints (#1047) * fix: drop the empty [[config.mounts]] block from vault * fix: pin vault and scrutiny images, add the missing version header --- blueprints/scrutiny/docker-compose.yml | 5 +++-- blueprints/scrutiny/meta.json | 2 +- blueprints/stack-auth/docker-compose.yml | 1 - blueprints/vault/docker-compose.yml | 3 +-- blueprints/vault/meta.json | 2 +- blueprints/vault/template.toml | 3 --- 6 files changed, 6 insertions(+), 10 deletions(-) diff --git a/blueprints/scrutiny/docker-compose.yml b/blueprints/scrutiny/docker-compose.yml index 56b27bd26..d951b15e1 100644 --- a/blueprints/scrutiny/docker-compose.yml +++ b/blueprints/scrutiny/docker-compose.yml @@ -1,8 +1,9 @@ +version: "3.8" + services: scrutiny: restart: unless-stopped - container_name: scrutiny - image: ghcr.io/analogj/scrutiny:master-omnibus + image: ghcr.io/analogj/scrutiny:v0.8.3-omnibus cap_add: - SYS_RAWIO ports: diff --git a/blueprints/scrutiny/meta.json b/blueprints/scrutiny/meta.json index 515caaa9a..9b862a642 100644 --- a/blueprints/scrutiny/meta.json +++ b/blueprints/scrutiny/meta.json @@ -1,7 +1,7 @@ { "id": "scrutiny", "name": "Scrutiny", - "version": "latest", + "version": "v0.8.3-omnibus", "description": "Hard Drive S.M.A.R.T Monitoring, Historical Trends & Real World Failure Thresholds", "logo": "scrutiny.png", "links": { diff --git a/blueprints/stack-auth/docker-compose.yml b/blueprints/stack-auth/docker-compose.yml index 461b23d39..40f5298f6 100644 --- a/blueprints/stack-auth/docker-compose.yml +++ b/blueprints/stack-auth/docker-compose.yml @@ -18,7 +18,6 @@ services: stack-auth: image: stackauth/server:latest - container_name: stack-auth environment: - POSTGRES_USER=${POSTGRES_USER} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} diff --git a/blueprints/vault/docker-compose.yml b/blueprints/vault/docker-compose.yml index d9ef30164..c8abc02f4 100644 --- a/blueprints/vault/docker-compose.yml +++ b/blueprints/vault/docker-compose.yml @@ -2,8 +2,7 @@ version: "3.8" services: vault: - image: hashicorp/vault:latest - container_name: vault + image: hashicorp/vault:2.0.3 cap_add: - IPC_LOCK environment: diff --git a/blueprints/vault/meta.json b/blueprints/vault/meta.json index f6de2da3f..8f2f35a7a 100644 --- a/blueprints/vault/meta.json +++ b/blueprints/vault/meta.json @@ -1,7 +1,7 @@ { "id": "vault", "name": "Vault", - "version": "latest", + "version": "2.0.3", "description": "Vault is a tool for securely accessing secrets. A secret is anything that you want to tightly control access to, such as API keys, passwords, certificates, and more. Vault provides a unified interface to any secret, while providing tight access control and recording a detailed audit log. To sign in: In the Vault UI, select 'Token' as the authentication method (not GitHub), then enter the root token from the VAULT_DEV_ROOT_TOKEN_ID environment variable (auto-generated).", "logo": "vault.svg", "links": { diff --git a/blueprints/vault/template.toml b/blueprints/vault/template.toml index e20571f78..ba6aad716 100644 --- a/blueprints/vault/template.toml +++ b/blueprints/vault/template.toml @@ -12,6 +12,3 @@ host = "${main_domain}" [config.env] VAULT_DEV_ROOT_TOKEN_ID = "${root_token}" VAULT_DEV_LISTEN_ADDRESS = "0.0.0.0:8200" - -[[config.mounts]] - From 4de48c2a734034d636b31500ae657b69f90fb441 Mon Sep 17 00:00:00 2001 From: Javier <100307829+javierlnmn@users.noreply.github.com> Date: Mon, 3 Aug 2026 12:09:10 +0200 Subject: [PATCH 53/80] Add WupNext blueprint (#1051) --- blueprints/wupnext/docker-compose.yml | 43 +++++++++++++++++++++++++++ blueprints/wupnext/logo.svg | 1 + blueprints/wupnext/meta.json | 20 +++++++++++++ blueprints/wupnext/template.toml | 26 ++++++++++++++++ 4 files changed, 90 insertions(+) create mode 100644 blueprints/wupnext/docker-compose.yml create mode 100644 blueprints/wupnext/logo.svg create mode 100644 blueprints/wupnext/meta.json create mode 100644 blueprints/wupnext/template.toml diff --git a/blueprints/wupnext/docker-compose.yml b/blueprints/wupnext/docker-compose.yml new file mode 100644 index 000000000..95bd27a2b --- /dev/null +++ b/blueprints/wupnext/docker-compose.yml @@ -0,0 +1,43 @@ +version: "3.8" + +services: + wupnext: + image: docker.io/javierlnmn/wupnext:latest + restart: unless-stopped + environment: + SECRET_KEY: ${SECRET_KEY} + DEBUG: "False" + ALLOWED_HOSTS: ${ALLOWED_HOSTS} + CSRF_TRUSTED_ORIGINS: ${CSRF_TRUSTED_ORIGINS} + EMAIL_BACKEND: anymail.backends.resend.EmailBackend + DEFAULT_FROM_EMAIL: ${DEFAULT_FROM_EMAIL} + RESEND_API_KEY: ${RESEND_API_KEY} + # First-run bootstrap (admin user; app has no signup) + DJANGO_SUPERUSER_USERNAME: ${DJANGO_SUPERUSER_USERNAME} + DJANGO_SUPERUSER_EMAIL: ${DJANGO_SUPERUSER_EMAIL} + DJANGO_SUPERUSER_PASSWORD: ${DJANGO_SUPERUSER_PASSWORD} + # Expose only to internal network; domain routing handled by Dokploy/Traefik + expose: + - "8000" + volumes: + - wupnext-db:/app/db + + wupnext-worker: + image: docker.io/javierlnmn/wupnext:latest + restart: unless-stopped + command: python manage.py qcluster + environment: + SECRET_KEY: ${SECRET_KEY} + DEBUG: "False" + ALLOWED_HOSTS: ${ALLOWED_HOSTS} + CSRF_TRUSTED_ORIGINS: ${CSRF_TRUSTED_ORIGINS} + EMAIL_BACKEND: anymail.backends.resend.EmailBackend + DEFAULT_FROM_EMAIL: ${DEFAULT_FROM_EMAIL} + RESEND_API_KEY: ${RESEND_API_KEY} + volumes: + - wupnext-db:/app/db + depends_on: + - wupnext + +volumes: + wupnext-db: diff --git a/blueprints/wupnext/logo.svg b/blueprints/wupnext/logo.svg new file mode 100644 index 000000000..16465949b --- /dev/null +++ b/blueprints/wupnext/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/blueprints/wupnext/meta.json b/blueprints/wupnext/meta.json new file mode 100644 index 000000000..7cd10c489 --- /dev/null +++ b/blueprints/wupnext/meta.json @@ -0,0 +1,20 @@ +{ + "id": "wupnext", + "name": "WupNext", + "version": "latest", + "description": "WupNext is a self-hosted to-do and task queue app: groups, subtasks, deadlines, a built-in pomodoro timer, and daily email reminders. Server-rendered Django with HTMX, backed by SQLite.", + "logo": "logo.svg", + "links": { + "github": "https://github.com/javierlnmn/wupnext", + "website": "", + "docs": "" + }, + "tags": [ + "todo", + "tasks", + "productivity", + "pomodoro", + "django", + "self-hosted" + ] +} diff --git a/blueprints/wupnext/template.toml b/blueprints/wupnext/template.toml new file mode 100644 index 000000000..eb4f48034 --- /dev/null +++ b/blueprints/wupnext/template.toml @@ -0,0 +1,26 @@ +[variables] +main_domain = "${domain}" +secret_key = "${password:50}" +admin_username = "admin" +admin_email = "${email}" +admin_password = "${password:24}" +from_email = "wupnext " +resend_api_key = "" + +[config] +env = [ + "SECRET_KEY=${secret_key}", + "ALLOWED_HOSTS=${main_domain}", + "CSRF_TRUSTED_ORIGINS=https://${main_domain}", + "DEFAULT_FROM_EMAIL=${from_email}", + "RESEND_API_KEY=${resend_api_key}", + "DJANGO_SUPERUSER_USERNAME=${admin_username}", + "DJANGO_SUPERUSER_EMAIL=${admin_email}", + "DJANGO_SUPERUSER_PASSWORD=${admin_password}" +] +mounts = [] + +[[config.domains]] +serviceName = "wupnext" +port = 8000 +host = "${main_domain}" From c1ccb2735954c7f53561f55cbbd7880879f9379d Mon Sep 17 00:00:00 2001 From: Andre Carbajal Vargas Date: Mon, 3 Aug 2026 05:12:23 -0500 Subject: [PATCH 54/80] feat: add NaviServer template (#1062) * feat: add NaviServer template * fix: address NaviServer template review comments * fix: align NaviServer template env format * fix: publish NaviServer Minecraft ports --- blueprints/naviserver/docker-compose.yml | 21 +++++++++++++++++++++ blueprints/naviserver/meta.json | 18 ++++++++++++++++++ blueprints/naviserver/naviserver.png | Bin 0 -> 1422 bytes blueprints/naviserver/template.toml | 19 +++++++++++++++++++ 4 files changed, 58 insertions(+) create mode 100644 blueprints/naviserver/docker-compose.yml create mode 100644 blueprints/naviserver/meta.json create mode 100644 blueprints/naviserver/naviserver.png create mode 100644 blueprints/naviserver/template.toml diff --git a/blueprints/naviserver/docker-compose.yml b/blueprints/naviserver/docker-compose.yml new file mode 100644 index 000000000..8b9807d0b --- /dev/null +++ b/blueprints/naviserver/docker-compose.yml @@ -0,0 +1,21 @@ +version: "3.8" + +services: + naviserver: + image: ghcr.io/andre-carbajal/naviserver:2.4.5 + restart: unless-stopped + expose: + - "23008" + ports: + - "25565-25600" + environment: + NAVISERVER_HOST: ${NAVISERVER_HOST} + NAVISERVER_PORT: ${NAVISERVER_PORT} + NAVISERVER_ALLOWED_ORIGINS: ${NAVISERVER_ALLOWED_ORIGINS} + NAVISERVER_SECRET_KEY: ${NAVISERVER_SECRET_KEY} + NAVISERVER_CLI_TOKEN: ${NAVISERVER_CLI_TOKEN} + CURSEFORGE_API_KEY: ${CURSEFORGE_API_KEY} + volumes: + - naviserver-data:/data +volumes: + naviserver-data: {} diff --git a/blueprints/naviserver/meta.json b/blueprints/naviserver/meta.json new file mode 100644 index 000000000..fadca7cd1 --- /dev/null +++ b/blueprints/naviserver/meta.json @@ -0,0 +1,18 @@ +{ + "id": "naviserver", + "name": "NaviServer", + "version": "2.4.5", + "description": "A lightweight Minecraft server manager with a web interface, CLI, automatic JVM management, monitoring, and support for Vanilla, Paper, Fabric, Forge, and NeoForge.", + "logo": "naviserver.png", + "links": { + "github": "https://github.com/andre-carbajal/NaviServer", + "website": "https://github.com/andre-carbajal/NaviServer", + "docs": "https://github.com/andre-carbajal/NaviServer/wiki" + }, + "tags": [ + "minecraft", + "game-server", + "self-hosted", + "management" + ] +} diff --git a/blueprints/naviserver/naviserver.png b/blueprints/naviserver/naviserver.png new file mode 100644 index 0000000000000000000000000000000000000000..6fd5ee9e2a64e2ce21163d67f960f382667c61b5 GIT binary patch literal 1422 zcmYk6dpML?9LL{bCSw?8VkFryCKX9hSv;oEOz*hg7UNPT#*~#38kb2(ufZ(VrBLnK zX@@*iOzcLI@J`CMtQqUFiPcn_OS!z&x|OuUU;D>7-_PfH&NR`eM(q?4}dpl5@tz3ipw9-^$9{AaB?OH9~BRyulnQWwJHo4Q9}SnwQEc(2&eiL*BOwYJx&B@;-B@ zj2WtOGbjP&p>hf|(CMKeFV^QS2(hauO=6^%P^oprLN=*MNL4qJ&4q<=_#Z#b`Pi=x z^ObglRE@C|gTj~$>=1z zC}6#tHv$gDsDR;yfPKycP~qls}PoS^pL=sb1v<*|Jt1}b-B}Ha%ksLetbnT%Ec?FzKnvVTdf#8 zf^%XyI=wb>hNb%WO4O8(yD@>%H1eiKPxC3=h?^te22mmoxdZA2A^$)5Qy|j_u>R+ilZt z$Di4)#%h78>=L3gZy!%WwuzE39(hGFJ|fJOhR0BA2{g$~%itfeRlzv1?=b0lL4G;Wr2yHWXk==>?mWX#>6leU6Y+ZJf+ zMD?uNnXWujQtD%S`Db7#?795FwOESEbV6gcTQrQhVEF{LwQU58}bZCS& ziJRT;)B@@0oh6|-@V!cVJym3>A&3byVW8W|-g^VyY@P|F67BT(41eqPY5}4qp#B;bapL*IhXJE+SYv z{b`1FKQ(F!N50x$%OU%(p}1H+9YY^n$E)2dX=9sif{H&$yNYv403nX(hkSI$C=6+) z&vYpdBqb3E{2IvVU_1)TUhgvtV3w};S+O19ItzQ=ViRG#!x&1Bh`@+Jgh?nYi8|2w z<)~vIngW7|A&u(=MC4+{nLBz6dTTaH4yoT*?vk&l8M3JVE(!5(n?tZ$J%nZQp?A=I zj18F;{KA2rHIij>npNM-|7**l*0EGk(Farav5-hf{?kYB&BCr}eTL#?-86jX S9Kgyg?~59QqH6|9uhw literal 0 HcmV?d00001 diff --git a/blueprints/naviserver/template.toml b/blueprints/naviserver/template.toml new file mode 100644 index 000000000..1f401e66b --- /dev/null +++ b/blueprints/naviserver/template.toml @@ -0,0 +1,19 @@ +[variables] +main_domain = "${domain}" +naviserver_secret_key = "${password:64}" +naviserver_cli_token = "${password:64}" + +[config] +env = [ + "NAVISERVER_HOST=0.0.0.0", + "NAVISERVER_PORT=23008", + "NAVISERVER_ALLOWED_ORIGINS=", + "NAVISERVER_SECRET_KEY=${naviserver_secret_key}", + "NAVISERVER_CLI_TOKEN=${naviserver_cli_token}", + "CURSEFORGE_API_KEY=", +] + +[[config.domains]] +serviceName = "naviserver" +port = 23008 +host = "${main_domain}" From 69e731eda1098e2fee3fa8e9a358a8ddcb36d227 Mon Sep 17 00:00:00 2001 From: Khiet Tam Nguyen <86177399+nktnet1@users.noreply.github.com> Date: Mon, 3 Aug 2026 20:12:26 +1000 Subject: [PATCH 55/80] fix: change "ports" -> "expose" for multiple services (#1053) * fix: change "ports" -> "expose" for multiple services * chore: remove helper script --- README.md | 2 +- blueprints/ackee/docker-compose.yml | 2 +- blueprints/activepieces/docker-compose.yml | 2 +- blueprints/adguardhome/docker-compose.yml | 2 +- blueprints/adminer/docker-compose.yml | 4 +- blueprints/affinepro/docker-compose.yml | 2 +- blueprints/agent-zero/docker-compose.yml | 2 +- blueprints/agentdvr/docker-compose.yml | 5 +- blueprints/alist/docker-compose.yml | 2 +- blueprints/alltube/docker-compose.yml | 4 +- blueprints/ampache/docker-compose.yml | 4 +- blueprints/anonupload/docker-compose.yml | 4 +- blueprints/answer/docker-compose.yml | 2 +- blueprints/anubis/docker-compose.yml | 2 +- blueprints/anythingllm/docker-compose.yml | 4 +- blueprints/apprise-api/docker-compose.yml | 4 +- blueprints/appsmith/docker-compose.yml | 2 +- blueprints/arangodb/docker-compose.yml | 4 +- blueprints/archivebox/docker-compose.yml | 3 +- blueprints/argilla/docker-compose.yml | 4 +- blueprints/audiobookshelf/docker-compose.yml | 4 +- blueprints/authelia/docker-compose.yml | 5 +- blueprints/authorizer/docker-compose.yml | 4 +- blueprints/autobase/docker-compose.yml | 2 +- blueprints/azuracast/docker-compose.yml | 4 +- blueprints/babybuddy/docker-compose.yml | 4 +- blueprints/backrest/docker-compose.yml | 2 +- blueprints/baikal/docker-compose.yml | 4 +- blueprints/barrage/docker-compose.yml | 4 +- blueprints/bazarr/docker-compose.yml | 4 +- blueprints/beszel/docker-compose.yml | 4 +- blueprints/bigcapital/docker-compose.yml | 18 +-- blueprints/blender/docker-compose.yml | 2 +- blueprints/blinko/docker-compose.yml | 3 +- blueprints/bluesky-pds/docker-compose.yml | 2 +- blueprints/booklore/docker-compose.yml | 2 +- blueprints/bookstack/docker-compose.yml | 4 +- blueprints/botpress/docker-compose.yml | 4 +- blueprints/budget-board/docker-compose.yml | 2 +- blueprints/bytebase/docker-compose.yml | 4 +- blueprints/bytestash/docker-compose.yml | 4 +- blueprints/calibre/docker-compose.yml | 4 +- blueprints/carbone/docker-compose.yml | 4 +- blueprints/casdoor/docker-compose.yml | 2 +- blueprints/certmate/docker-compose.yml | 2 +- blueprints/changedetection/docker-compose.yml | 4 +- blueprints/chatwoot/docker-compose.yml | 2 +- blueprints/checkmate/docker-compose.yml | 2 +- blueprints/chevereto/docker-compose.yml | 4 +- blueprints/chiefonboarding/docker-compose.yml | 4 +- blueprints/chirpstack/docker-compose.yml | 11 +- blueprints/chromium/docker-compose.yml | 4 +- blueprints/classicpress/docker-compose.yml | 4 +- blueprints/cloud9/docker-compose.yml | 4 +- blueprints/cloudcommander/docker-compose.yml | 4 +- blueprints/cockpit/docker-compose.yml | 4 +- blueprints/codex-docs/docker-compose.yml | 4 +- .../collabora-office/docker-compose.yml | 4 +- blueprints/commafeed/docker-compose.yml | 2 +- blueprints/commento/docker-compose.yml | 4 +- .../commentoplusplus/docker-compose.yml | 4 +- blueprints/confluence/docker-compose.yml | 4 +- blueprints/convertx/docker-compose.yml | 2 +- blueprints/coralproject/docker-compose.yml | 4 +- blueprints/couchdb/docker-compose.yml | 2 +- blueprints/crowdsec/docker-compose.yml | 2 +- blueprints/cup/docker-compose.yml | 2 +- blueprints/cut/docker-compose.yml | 2 +- blueprints/dashy/docker-compose.yml | 4 +- blueprints/databasus/docker-compose.yml | 2 +- .../directory-lister/docker-compose.yml | 3 +- blueprints/directus/docker-compose.yml | 2 +- blueprints/discord-tickets/docker-compose.yml | 2 +- blueprints/discourse/docker-compose.yml | 2 +- blueprints/dockge/docker-compose.yml | 3 +- blueprints/docling-serve/docker-compose.yml | 2 +- blueprints/docmost/docker-compose.yml | 2 +- blueprints/documenso/docker-compose.yml | 2 +- blueprints/docuseal/docker-compose.yml | 2 +- blueprints/dragonfly-db/docker-compose.yml | 2 +- blueprints/drawio/docker-compose.yml | 10 +- blueprints/dumbassets/docker-compose.yml | 2 +- blueprints/dumbbudget/docker-compose.yml | 2 +- blueprints/dumbdrop/docker-compose.yml | 2 +- blueprints/dumbpad/docker-compose.yml | 2 +- blueprints/elastic-search/docker-compose.yml | 6 +- blueprints/emby/docker-compose.yml | 4 +- blueprints/enshrouded/docker-compose.yml | 2 +- blueprints/erpnext-v16/docker-compose.yml | 2 +- blueprints/erpnext/docker-compose.yml | 2 +- blueprints/evershop/docker-compose.yml | 6 +- blueprints/evolutionapi/docker-compose.yml | 2 +- blueprints/filebrowser/docker-compose.yml | 2 +- blueprints/filegator/docker-compose.yml | 3 +- blueprints/filestash/docker-compose.yml | 7 +- blueprints/firecrawl/docker-compose.yml | 4 +- blueprints/fivem/docker-compose.yml | 2 +- blueprints/flagsmith/docker-compose.yml | 4 +- blueprints/flaresolverr/docker-compose.yml | 2 +- blueprints/flarum/docker-compose.yml | 2 +- blueprints/fmd-server/docker-compose.yml | 2 +- blueprints/focalboard/docker-compose.yml | 2 +- blueprints/fonoster/docker-compose.yml | 3 +- blueprints/forgejo/docker-compose.yml | 2 +- blueprints/formbricks/docker-compose.yml | 2 +- blueprints/frappe-hr/docker-compose.yml | 2 +- blueprints/freshrss/docker-compose.yml | 2 +- blueprints/garage-with-ui/docker-compose.yml | 7 +- blueprints/garage/docker-compose.yml | 5 +- blueprints/gel/docker-compose.yml | 2 +- blueprints/glance/docker-compose.yml | 4 +- .../docker-compose.yml | 2 +- blueprints/gotify/docker-compose.yml | 3 +- blueprints/grimoire/docker-compose.yml | 2 +- blueprints/grist/docker-compose.yml | 2 +- blueprints/habitica/docker-compose.yml | 3 +- blueprints/hermes/docker-compose.yml | 2 +- blueprints/heyform/docker-compose.yml | 2 +- blueprints/hi-events/docker-compose.yml | 2 +- blueprints/hoarder/docker-compose.yml | 4 +- blueprints/homarr/docker-compose.yml | 3 +- blueprints/homeassistant/docker-compose.yml | 2 +- blueprints/homebridge/docker-compose.yml | 2 +- blueprints/huly/docker-compose.yml | 2 +- blueprints/influxdb/docker-compose.yml | 2 +- blueprints/inngest/docker-compose.yml | 6 +- blueprints/instantdb/docker-compose.yml | 2 +- blueprints/ipfs/docker-compose.yml | 2 +- blueprints/java/docker-compose.yml | 4 +- blueprints/jellyseerr/docker-compose.yml | 3 +- blueprints/jenkins/docker-compose.yml | 5 +- blueprints/jitsi/docker-compose.yml | 2 +- blueprints/joplin-server/docker-compose.yml | 3 +- blueprints/kaneo/docker-compose.yml | 4 +- blueprints/kestra/docker-compose.yml | 2 +- blueprints/kimai/docker-compose.yml | 2 +- blueprints/kokoro-tts/docker-compose.yml | 2 +- blueprints/kutt/docker-compose.yml | 2 +- blueprints/langflow/docker-compose.yml | 2 +- blueprints/lavalink/docker-compose.yml | 2 +- blueprints/leantime/docker-compose.yml | 2 +- blueprints/librechat/docker-compose.yml | 2 +- blueprints/libredesk/docker-compose.yml | 10 +- blueprints/libretranslate/docker-compose.yml | 2 +- blueprints/linkding/docker-compose.yml | 2 +- blueprints/linkwarden/docker-compose.yml | 2 +- blueprints/listmonk/docker-compose.yml | 2 +- blueprints/lobe-chat/docker-compose.yml | 4 +- blueprints/lodestone/docker-compose.yml | 2 +- blueprints/macos/docker-compose.yml | 2 +- blueprints/mailpit/docker-compose.yml | 2 +- .../marketing-dashboard/docker-compose.yml | 2 +- blueprints/mautic/docker-compose.yml | 5 +- blueprints/mcsmanager/docker-compose.yml | 4 +- blueprints/mediacms/docker-compose.yml | 2 +- blueprints/mediafetch/docker-compose.yml | 2 +- blueprints/memos/docker-compose.yml | 2 +- blueprints/metube/docker-compose.yml | 2 +- blueprints/mixpost/docker-compose.yml | 2 +- blueprints/moltbot/docker-compose.yml | 4 +- blueprints/movary/docker-compose.yml | 2 +- blueprints/mulesoft-esb/docker-compose.yml | 2 +- blueprints/mumble/docker-compose.yml | 2 +- blueprints/navidrome/docker-compose.yml | 5 +- blueprints/netdata/docker-compose.yml | 3 +- blueprints/nginx/docker-compose.yml | 4 +- blueprints/ntfy/docker-compose.yml | 2 +- .../obsidian-livesync/docker-compose.yml | 3 +- blueprints/ojs/docker-compose.yml | 2 +- .../ollama-chat-tone/docker-compose.yml | 2 +- blueprints/omni-tools/docker-compose.yml | 2 +- blueprints/onedev/docker-compose.yml | 2 +- blueprints/ontime/docker-compose.yml | 2 +- blueprints/open-webui/docker-compose.yml | 2 +- blueprints/open_notebook/docker-compose.yml | 4 +- blueprints/opengist/docker-compose.yml | 2 +- .../openhabittracker/docker-compose.yml | 2 +- blueprints/openhands/docker-compose.yml | 4 +- blueprints/openinary/docker-compose.yml | 2 +- blueprints/openspeedtest/docker-compose.yml | 4 +- blueprints/outline/docker-compose.yml | 5 +- blueprints/owncast/docker-compose.yml | 5 +- blueprints/palmr/docker-compose.yml | 2 +- blueprints/paperclip/docker-compose.yml | 2 +- blueprints/parseable/docker-compose.yml | 2 +- blueprints/paymenter/docker-compose.yml | 4 +- blueprints/penpot/docker-compose.yml | 27 +--- blueprints/peppermint/docker-compose.yml | 2 +- blueprints/photoprism/docker-compose.yml | 152 +++++++++--------- blueprints/plark/docker-compose.yml | 2 +- blueprints/plunk/docker-compose.yml | 7 +- blueprints/portainer/docker-compose.yml | 5 +- blueprints/postgresus/docker-compose.yml | 2 +- blueprints/postiz/docker-compose.yml | 2 +- blueprints/privatebin/docker-compose.yml | 3 +- blueprints/pulse/docker-compose.yml | 2 +- blueprints/quant-ux/docker-compose.yml | 4 +- blueprints/rabbitmq/docker-compose.yml | 5 +- blueprints/rallly/docker-compose.yml | 2 +- blueprints/rsshub/docker-compose.yml | 4 +- blueprints/rustrak-full/docker-compose.yml | 4 +- blueprints/rustrak/docker-compose.yml | 3 +- blueprints/ryot/docker-compose.yml | 2 +- blueprints/scrutiny/docker-compose.yml | 2 +- blueprints/seanime/docker-compose.yml | 2 +- blueprints/seq/docker-compose.yml | 2 +- blueprints/sftpgo/docker-compose.yml | 3 +- blueprints/signoz/docker-compose.yml | 4 +- blueprints/silex/docker-compose.yml | 2 +- blueprints/slash/docker-compose.yml | 2 +- blueprints/snapp/docker-compose.yml | 2 +- blueprints/spacedrive/docker-compose.yml | 2 +- blueprints/spacetimedb/docker-compose.yml | 2 +- .../speedtest-tracker/docker-compose.yml | 3 +- blueprints/spliit/docker-compose.yml | 2 +- blueprints/stalwart/docker-compose.yml | 4 +- blueprints/statping-ng/docker-compose.yml | 2 +- blueprints/stirling/docker-compose.yml | 4 +- blueprints/streamflow/docker-compose.yml | 3 +- .../tailscale-exitnode/docker-compose.yml | 2 +- blueprints/tandoor-recipes/docker-compose.yml | 3 +- blueprints/tianji/docker-compose.yml | 2 +- blueprints/tolgee/docker-compose.yml | 2 +- blueprints/tooljet/docker-compose.yml | 2 +- blueprints/triggerdotdev/docker-compose.yml | 13 +- blueprints/trilium-next/docker-compose.yml | 7 +- blueprints/trilium/docker-compose.yml | 2 +- blueprints/twenty/docker-compose.yml | 2 +- blueprints/typecho/docker-compose.yml | 2 +- blueprints/unifi/docker-compose.yml | 10 +- blueprints/uptimekit/docker-compose.yml | 2 +- blueprints/usesend/docker-compose.yml | 4 +- blueprints/vault/docker-compose.yml | 2 +- blueprints/verdaccio/docker-compose.yml | 4 +- blueprints/victoriametrics/docker-compose.yml | 2 +- blueprints/vikunja/docker-compose.yml | 2 +- blueprints/wallos/docker-compose.yml | 4 +- blueprints/web-check/docker-compose.yml | 2 +- blueprints/windmill/docker-compose.yml | 2 +- blueprints/windows/docker-compose.yml | 2 +- blueprints/yourls/docker-compose.yml | 2 +- blueprints/yt-dlp-webui/docker-compose.yml | 2 +- 242 files changed, 418 insertions(+), 504 deletions(-) diff --git a/README.md b/README.md index bba2bda3e..1ae549179 100644 --- a/README.md +++ b/README.md @@ -183,7 +183,7 @@ services: - 3000:3000 # Instead use this way: - ports: + expose: - 3000 ``` diff --git a/blueprints/ackee/docker-compose.yml b/blueprints/ackee/docker-compose.yml index 43e4220f1..90255c69a 100644 --- a/blueprints/ackee/docker-compose.yml +++ b/blueprints/ackee/docker-compose.yml @@ -3,7 +3,7 @@ version: "3" services: ackee: image: electerious/ackee:3.4.2 - ports: + expose: - "3000" environment: - ACKEE_USERNAME=${ACKEE_USERNAME} diff --git a/blueprints/activepieces/docker-compose.yml b/blueprints/activepieces/docker-compose.yml index a5511e7fa..da06d316f 100644 --- a/blueprints/activepieces/docker-compose.yml +++ b/blueprints/activepieces/docker-compose.yml @@ -61,4 +61,4 @@ services: volumes: postgres_data: - redis_data: \ No newline at end of file + redis_data: diff --git a/blueprints/adguardhome/docker-compose.yml b/blueprints/adguardhome/docker-compose.yml index 72b5be339..c0b01d3dc 100644 --- a/blueprints/adguardhome/docker-compose.yml +++ b/blueprints/adguardhome/docker-compose.yml @@ -17,4 +17,4 @@ services: volumes: adguardhome-work: {} - adguardhome-conf: {} \ No newline at end of file + adguardhome-conf: {} diff --git a/blueprints/adminer/docker-compose.yml b/blueprints/adminer/docker-compose.yml index 29d7c270c..3674d2946 100644 --- a/blueprints/adminer/docker-compose.yml +++ b/blueprints/adminer/docker-compose.yml @@ -3,5 +3,5 @@ services: adminer: image: adminer:4.8.1 restart: unless-stopped - ports: - - 8080 \ No newline at end of file + expose: + - 8080 diff --git a/blueprints/affinepro/docker-compose.yml b/blueprints/affinepro/docker-compose.yml index e8a905d5d..0af8e2cfa 100644 --- a/blueprints/affinepro/docker-compose.yml +++ b/blueprints/affinepro/docker-compose.yml @@ -3,7 +3,7 @@ services: affinepro: image: ghcr.io/toeverything/affine:stable restart: unless-stopped - ports: + expose: - 3010 volumes: - affine-storage:/root/.affine/storage diff --git a/blueprints/agent-zero/docker-compose.yml b/blueprints/agent-zero/docker-compose.yml index fceed3cf5..187d506e6 100644 --- a/blueprints/agent-zero/docker-compose.yml +++ b/blueprints/agent-zero/docker-compose.yml @@ -11,4 +11,4 @@ services: volumes: - agent-zero-data:/a0/usr volumes: - agent-zero-data: \ No newline at end of file + agent-zero-data: diff --git a/blueprints/agentdvr/docker-compose.yml b/blueprints/agentdvr/docker-compose.yml index 8dc4e7c37..a56e702cf 100644 --- a/blueprints/agentdvr/docker-compose.yml +++ b/blueprints/agentdvr/docker-compose.yml @@ -11,12 +11,11 @@ services: - agentdvr-config:/AgentDVR/Media/XML/ - agentdvr-media:/AgentDVR/Media/WebServerRoot/Media/ - agentdvr-commands:/AgentDVR/Commands/ - ports: + expose: - 8090 - 3478/udp - 50000-50100/udp - volumes: agentdvr-config: {} agentdvr-media: {} - agentdvr-commands: {} \ No newline at end of file + agentdvr-commands: {} diff --git a/blueprints/alist/docker-compose.yml b/blueprints/alist/docker-compose.yml index 1577387f3..6311c7403 100644 --- a/blueprints/alist/docker-compose.yml +++ b/blueprints/alist/docker-compose.yml @@ -11,4 +11,4 @@ services: restart: unless-stopped volumes: - alist-data: \ No newline at end of file + alist-data: diff --git a/blueprints/alltube/docker-compose.yml b/blueprints/alltube/docker-compose.yml index c04e5238f..90bbc5045 100644 --- a/blueprints/alltube/docker-compose.yml +++ b/blueprints/alltube/docker-compose.yml @@ -3,10 +3,10 @@ services: alltube: image: dnomd343/alltube:latest restart: unless-stopped - ports: + expose: - 80 environment: - TITLE=${TITLE} - CONVERT=${CONVERT} - STREAM=${STREAM} - - REMUX=${REMUX} \ No newline at end of file + - REMUX=${REMUX} diff --git a/blueprints/ampache/docker-compose.yml b/blueprints/ampache/docker-compose.yml index 0af8f418a..f4a1da455 100644 --- a/blueprints/ampache/docker-compose.yml +++ b/blueprints/ampache/docker-compose.yml @@ -3,7 +3,7 @@ services: ampache: image: ampache/ampache:latest restart: unless-stopped - ports: + expose: - 80 volumes: - config:/var/www/config @@ -14,4 +14,4 @@ services: volumes: config: {} log: {} - mysql: {} \ No newline at end of file + mysql: {} diff --git a/blueprints/anonupload/docker-compose.yml b/blueprints/anonupload/docker-compose.yml index 9d88e7987..7ff3b2ea0 100644 --- a/blueprints/anonupload/docker-compose.yml +++ b/blueprints/anonupload/docker-compose.yml @@ -3,7 +3,7 @@ services: anonupload: image: ghcr.io/supernova3339/anonfiles:1 restart: unless-stopped - ports: + expose: - 80 environment: - ADMIN_EMAIL=${ADMIN_EMAIL} @@ -13,4 +13,4 @@ services: - uploads:/var/www/html/uploads volumes: - uploads: {} \ No newline at end of file + uploads: {} diff --git a/blueprints/answer/docker-compose.yml b/blueprints/answer/docker-compose.yml index 2b9fc3440..b23f87022 100644 --- a/blueprints/answer/docker-compose.yml +++ b/blueprints/answer/docker-compose.yml @@ -1,7 +1,7 @@ services: answer: image: apache/answer:1.4.1 - ports: + expose: - '80' restart: on-failure volumes: diff --git a/blueprints/anubis/docker-compose.yml b/blueprints/anubis/docker-compose.yml index 51b30b38f..70b8b7d5d 100644 --- a/blueprints/anubis/docker-compose.yml +++ b/blueprints/anubis/docker-compose.yml @@ -3,7 +3,7 @@ services: anubis: image: ghcr.io/techarohq/anubis:latest restart: unless-stopped - ports: + expose: - "8923" # Anubis default port environment: # Required: Point to your frontend service locally diff --git a/blueprints/anythingllm/docker-compose.yml b/blueprints/anythingllm/docker-compose.yml index 248753a74..e7ac94273 100644 --- a/blueprints/anythingllm/docker-compose.yml +++ b/blueprints/anythingllm/docker-compose.yml @@ -3,7 +3,7 @@ services: anythingllm: image: mintplexlabs/anythingllm:latest restart: unless-stopped - ports: + expose: - 3001 environment: - STORAGE_DIR=/app/server/storage @@ -13,4 +13,4 @@ services: - SYS_ADMIN volumes: - storage: {} \ No newline at end of file + storage: {} diff --git a/blueprints/apprise-api/docker-compose.yml b/blueprints/apprise-api/docker-compose.yml index cc46a5e35..af502e520 100644 --- a/blueprints/apprise-api/docker-compose.yml +++ b/blueprints/apprise-api/docker-compose.yml @@ -3,7 +3,7 @@ services: apprise-api: image: linuxserver/apprise-api:latest restart: unless-stopped - ports: + expose: - 8000 environment: - PUID=1000 @@ -13,4 +13,4 @@ services: - config:/config volumes: - config: {} \ No newline at end of file + config: {} diff --git a/blueprints/appsmith/docker-compose.yml b/blueprints/appsmith/docker-compose.yml index e22be5579..2f8914e99 100644 --- a/blueprints/appsmith/docker-compose.yml +++ b/blueprints/appsmith/docker-compose.yml @@ -7,4 +7,4 @@ services: restart: unless-stopped volumes: - appsmith-data: \ No newline at end of file + appsmith-data: diff --git a/blueprints/arangodb/docker-compose.yml b/blueprints/arangodb/docker-compose.yml index 65301d164..9137789fb 100644 --- a/blueprints/arangodb/docker-compose.yml +++ b/blueprints/arangodb/docker-compose.yml @@ -3,7 +3,7 @@ services: arangodb: image: arangodb:3.12.4 restart: unless-stopped - ports: + expose: - 8529 environment: - ARANGO_ROOT_PASSWORD=${ARANGO_PASSWORD} @@ -11,4 +11,4 @@ services: - data:/var/lib/arangodb3 volumes: - data: {} \ No newline at end of file + data: {} diff --git a/blueprints/archivebox/docker-compose.yml b/blueprints/archivebox/docker-compose.yml index 2f1493451..17868f237 100644 --- a/blueprints/archivebox/docker-compose.yml +++ b/blueprints/archivebox/docker-compose.yml @@ -9,8 +9,7 @@ services: PUBLIC_SNAPSHOTS: ${PUBLIC_SNAPSHOTS} volumes: - archivebox_data:/data - ports: + expose: - "8000" - volumes: archivebox_data: diff --git a/blueprints/argilla/docker-compose.yml b/blueprints/argilla/docker-compose.yml index 735350dc3..1e2976eae 100644 --- a/blueprints/argilla/docker-compose.yml +++ b/blueprints/argilla/docker-compose.yml @@ -3,7 +3,7 @@ services: argilla-web: image: argilla/argilla-server:latest restart: unless-stopped - ports: + expose: - 6900 environment: - ARGILLA_HOME_PATH=/var/lib/argilla @@ -74,4 +74,4 @@ volumes: argilladata: {} elasticdata: {} dbdata: {} - redisdata: {} \ No newline at end of file + redisdata: {} diff --git a/blueprints/audiobookshelf/docker-compose.yml b/blueprints/audiobookshelf/docker-compose.yml index 821da7619..262cfa7e5 100644 --- a/blueprints/audiobookshelf/docker-compose.yml +++ b/blueprints/audiobookshelf/docker-compose.yml @@ -3,7 +3,7 @@ services: audiobookshelf: image: ghcr.io/advplyr/audiobookshelf:2.19.4 restart: unless-stopped - ports: + expose: - 80 environment: - TZ=UTC @@ -14,4 +14,4 @@ services: volumes: config: {} - metadata: {} \ No newline at end of file + metadata: {} diff --git a/blueprints/authelia/docker-compose.yml b/blueprints/authelia/docker-compose.yml index fa65571bf..6cd5c77d1 100644 --- a/blueprints/authelia/docker-compose.yml +++ b/blueprints/authelia/docker-compose.yml @@ -16,9 +16,8 @@ services: condition: service_healthy postgres: condition: service_healthy - ports: + expose: - 9091 - redis: image: redis:7-alpine restart: unless-stopped @@ -52,4 +51,4 @@ services: volumes: authelia_config: redis_data: - postgres_data: \ No newline at end of file + postgres_data: diff --git a/blueprints/authorizer/docker-compose.yml b/blueprints/authorizer/docker-compose.yml index c99e92e84..05f034d4b 100644 --- a/blueprints/authorizer/docker-compose.yml +++ b/blueprints/authorizer/docker-compose.yml @@ -3,7 +3,7 @@ services: authorizer: image: lakhansamani/authorizer:1.4.4 restart: unless-stopped - ports: + expose: - 8080 environment: - DATABASE_TYPE=postgres @@ -37,4 +37,4 @@ services: volumes: db_data: {} - redis_data: {} \ No newline at end of file + redis_data: {} diff --git a/blueprints/autobase/docker-compose.yml b/blueprints/autobase/docker-compose.yml index 7582c9c63..7b931d9e7 100644 --- a/blueprints/autobase/docker-compose.yml +++ b/blueprints/autobase/docker-compose.yml @@ -2,7 +2,7 @@ services: autobase-console: image: autobase/console:2.7.2 restart: unless-stopped - ports: + expose: - "80" - "8080" environment: diff --git a/blueprints/azuracast/docker-compose.yml b/blueprints/azuracast/docker-compose.yml index 536802000..c5b61391f 100644 --- a/blueprints/azuracast/docker-compose.yml +++ b/blueprints/azuracast/docker-compose.yml @@ -2,7 +2,7 @@ services: azuracast: image: ghcr.io/azuracast/azuracast:latest restart: unless-stopped - ports: + expose: - 80 volumes: - azuracast-station-data:/var/azuracast/stations @@ -36,4 +36,4 @@ volumes: azuracast-data: {} azuracast-uploads: {} azuracast-backups: {} - mariadb-data: {} \ No newline at end of file + mariadb-data: {} diff --git a/blueprints/babybuddy/docker-compose.yml b/blueprints/babybuddy/docker-compose.yml index c07d90621..8642ef789 100644 --- a/blueprints/babybuddy/docker-compose.yml +++ b/blueprints/babybuddy/docker-compose.yml @@ -3,7 +3,7 @@ services: babybuddy: image: linuxserver/babybuddy:2.7.0 restart: unless-stopped - ports: + expose: - 8000 environment: - PUID=1000 @@ -16,4 +16,4 @@ services: - config:/config volumes: - config: {} \ No newline at end of file + config: {} diff --git a/blueprints/backrest/docker-compose.yml b/blueprints/backrest/docker-compose.yml index cdff2dfba..9fc714721 100644 --- a/blueprints/backrest/docker-compose.yml +++ b/blueprints/backrest/docker-compose.yml @@ -2,7 +2,7 @@ services: backrest: image: garethgeorge/backrest:v1.7.3 restart: unless-stopped - ports: + expose: - 9898 environment: - BACKREST_PORT=9898 diff --git a/blueprints/baikal/docker-compose.yml b/blueprints/baikal/docker-compose.yml index d0ba2a6a8..80169c6a6 100644 --- a/blueprints/baikal/docker-compose.yml +++ b/blueprints/baikal/docker-compose.yml @@ -3,7 +3,7 @@ services: baikal: image: ckulka/baikal:nginx-php8.2 restart: unless-stopped - ports: + expose: - 80 environment: - TZ=UTC @@ -13,4 +13,4 @@ services: volumes: config: {} - data: {} \ No newline at end of file + data: {} diff --git a/blueprints/barrage/docker-compose.yml b/blueprints/barrage/docker-compose.yml index 554f967fb..79c048f22 100644 --- a/blueprints/barrage/docker-compose.yml +++ b/blueprints/barrage/docker-compose.yml @@ -3,11 +3,11 @@ services: barrage: image: maulik9898/barrage:0.3.0 restart: unless-stopped - ports: + expose: - 3000 environment: - NEXTAUTH_SECRET=${NEXTAUTH_SECRET} - NEXTAUTH_URL=http://${DOMAIN} - DELUGE_URL=${DELUGE_URL} - DELUGE_PASSWORD=${DELUGE_PASSWORD} - - BARRAGE_PASSWORD=${BARRAGE_PASSWORD} \ No newline at end of file + - BARRAGE_PASSWORD=${BARRAGE_PASSWORD} diff --git a/blueprints/bazarr/docker-compose.yml b/blueprints/bazarr/docker-compose.yml index a0df5be7e..557ba5eb5 100644 --- a/blueprints/bazarr/docker-compose.yml +++ b/blueprints/bazarr/docker-compose.yml @@ -3,7 +3,7 @@ services: bazarr: image: lscr.io/linuxserver/bazarr:1.5.1 restart: unless-stopped - ports: + expose: - 6767 environment: - PUID=1000 @@ -15,4 +15,4 @@ services: - ${TV_PATH}:/tv volumes: - config: {} \ No newline at end of file + config: {} diff --git a/blueprints/beszel/docker-compose.yml b/blueprints/beszel/docker-compose.yml index a14372ae0..c5d9957f1 100644 --- a/blueprints/beszel/docker-compose.yml +++ b/blueprints/beszel/docker-compose.yml @@ -3,11 +3,11 @@ services: beszel: image: henrygd/beszel:0.10.2 restart: unless-stopped - ports: + expose: - 8090 volumes: - beszel_data:/beszel_data - /var/run/docker.sock:/var/run/docker.sock:ro volumes: - beszel_data: {} \ No newline at end of file + beszel_data: {} diff --git a/blueprints/bigcapital/docker-compose.yml b/blueprints/bigcapital/docker-compose.yml index aa8ff4876..605e87122 100644 --- a/blueprints/bigcapital/docker-compose.yml +++ b/blueprints/bigcapital/docker-compose.yml @@ -4,10 +4,8 @@ services: restart: unless-stopped depends_on: - server - ports: + expose: - '80' - - server: image: bigcapitalhq/server:latest restart: unless-stopped @@ -18,9 +16,8 @@ services: condition: service_started redis: condition: service_started - ports: + expose: - '3000' - environment: # Mail - MAIL_HOST=${MAIL_HOST} @@ -97,9 +94,8 @@ services: - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} volumes: - mysql:/var/lib/mysql - ports: + expose: - '3306' - healthcheck: test: ["CMD-SHELL", "mysqladmin ping -h localhost -u root -p$$MYSQL_ROOT_PASSWORD || exit 1"] interval: 10s @@ -110,7 +106,7 @@ services: mongo: image: mongo:7 restart: unless-stopped - ports: + expose: - '27017' volumes: - mongo:/data/db @@ -119,7 +115,7 @@ services: redis: image: redis:7-alpine restart: unless-stopped - ports: + expose: - '6379' volumes: - redis:/data @@ -133,10 +129,8 @@ services: gotenberg: image: gotenberg/gotenberg:7 restart: unless-stopped - ports: + expose: - '9000' - - volumes: mysql: name: bigcapital_mysql diff --git a/blueprints/blender/docker-compose.yml b/blueprints/blender/docker-compose.yml index 893f3deea..8d7c6484a 100644 --- a/blueprints/blender/docker-compose.yml +++ b/blueprints/blender/docker-compose.yml @@ -19,7 +19,7 @@ services: - PGID=1000 - TZ=Etc/UTC - SUBFOLDER=/ #optional - ports: + expose: - 3000 - 3001 restart: unless-stopped diff --git a/blueprints/blinko/docker-compose.yml b/blueprints/blinko/docker-compose.yml index 0d110956e..43736a5ee 100644 --- a/blueprints/blinko/docker-compose.yml +++ b/blueprints/blinko/docker-compose.yml @@ -16,9 +16,8 @@ services: options: max-size: "10m" max-file: "3" - ports: + expose: - 1111 - blinko-postgres: image: postgres:14 restart: always diff --git a/blueprints/bluesky-pds/docker-compose.yml b/blueprints/bluesky-pds/docker-compose.yml index 4f9ae9a2b..7048c3703 100644 --- a/blueprints/bluesky-pds/docker-compose.yml +++ b/blueprints/bluesky-pds/docker-compose.yml @@ -45,4 +45,4 @@ services: timeout: 10s retries: 10 volumes: - pds-data: \ No newline at end of file + pds-data: diff --git a/blueprints/booklore/docker-compose.yml b/blueprints/booklore/docker-compose.yml index b092368c6..cf79e01e2 100644 --- a/blueprints/booklore/docker-compose.yml +++ b/blueprints/booklore/docker-compose.yml @@ -8,7 +8,7 @@ services: depends_on: mariadb: condition: service_healthy - ports: + expose: - 6060 volumes: - booklore-data:/app/data diff --git a/blueprints/bookstack/docker-compose.yml b/blueprints/bookstack/docker-compose.yml index e8fd4001e..acfff69e8 100644 --- a/blueprints/bookstack/docker-compose.yml +++ b/blueprints/bookstack/docker-compose.yml @@ -3,7 +3,7 @@ services: bookstack: image: lscr.io/linuxserver/bookstack:24.12.1 restart: unless-stopped - ports: + expose: - 80 environment: - PUID=1000 @@ -32,4 +32,4 @@ services: volumes: config: {} - db_data: {} \ No newline at end of file + db_data: {} diff --git a/blueprints/botpress/docker-compose.yml b/blueprints/botpress/docker-compose.yml index 7afb52b2e..64c0209a9 100644 --- a/blueprints/botpress/docker-compose.yml +++ b/blueprints/botpress/docker-compose.yml @@ -3,7 +3,7 @@ services: botpress: image: botpress/server:12.31.9 restart: unless-stopped - ports: + expose: - 81 environment: - BP_HOST=0.0.0.0 @@ -31,4 +31,4 @@ services: volumes: data: {} - db_data: {} \ No newline at end of file + db_data: {} diff --git a/blueprints/budget-board/docker-compose.yml b/blueprints/budget-board/docker-compose.yml index 80ec95118..86cc73de9 100644 --- a/blueprints/budget-board/docker-compose.yml +++ b/blueprints/budget-board/docker-compose.yml @@ -22,7 +22,7 @@ services: environment: VITE_API_URL: http://budget-board-server PORT: 6253 - ports: + expose: - 6253 depends_on: - budget-board-server diff --git a/blueprints/bytebase/docker-compose.yml b/blueprints/bytebase/docker-compose.yml index 0b4bba92d..8cef0a385 100644 --- a/blueprints/bytebase/docker-compose.yml +++ b/blueprints/bytebase/docker-compose.yml @@ -3,7 +3,7 @@ services: bytebase: image: bytebase/bytebase:3.3.0 restart: unless-stopped - ports: + expose: - 8080 environment: - PG_URL=postgres://postgres:${DB_PASSWORD}@bytebase-db:5432/bytebase @@ -24,4 +24,4 @@ services: volumes: data: {} - db_data: {} \ No newline at end of file + db_data: {} diff --git a/blueprints/bytestash/docker-compose.yml b/blueprints/bytestash/docker-compose.yml index 3fa7e037f..677190a65 100644 --- a/blueprints/bytestash/docker-compose.yml +++ b/blueprints/bytestash/docker-compose.yml @@ -4,7 +4,7 @@ services: bytestash: image: ghcr.io/jordan-dalby/bytestash:1.5.6 restart: unless-stopped - ports: + expose: - "5000" environment: - BASE_PATH= @@ -24,4 +24,4 @@ services: - snippets:/data/snippets volumes: - snippets: \ No newline at end of file + snippets: diff --git a/blueprints/calibre/docker-compose.yml b/blueprints/calibre/docker-compose.yml index 3594e7bc0..07a43cb5f 100644 --- a/blueprints/calibre/docker-compose.yml +++ b/blueprints/calibre/docker-compose.yml @@ -9,7 +9,7 @@ services: - TZ=Etc/UTC - PUID=1000 - PGID=1000 - ports: + expose: - 8080 volumes: - books:/books @@ -17,4 +17,4 @@ services: volumes: books: - data: \ No newline at end of file + data: diff --git a/blueprints/carbone/docker-compose.yml b/blueprints/carbone/docker-compose.yml index e908c103d..8f771ca5d 100644 --- a/blueprints/carbone/docker-compose.yml +++ b/blueprints/carbone/docker-compose.yml @@ -7,10 +7,10 @@ services: environment: - CARBONE_EE_LICENSE=${CARBONE_KEY} - CARBONE_EE_STUDIO=true - ports: + expose: - 4000 volumes: - template:/app/template volumes: - template: \ No newline at end of file + template: diff --git a/blueprints/casdoor/docker-compose.yml b/blueprints/casdoor/docker-compose.yml index 6cc1c07e3..65f663302 100644 --- a/blueprints/casdoor/docker-compose.yml +++ b/blueprints/casdoor/docker-compose.yml @@ -30,4 +30,4 @@ services: volumes: casdoor-postgres-data: - casdoor-data: \ No newline at end of file + casdoor-data: diff --git a/blueprints/certmate/docker-compose.yml b/blueprints/certmate/docker-compose.yml index d7dde9dcb..6e9031f82 100644 --- a/blueprints/certmate/docker-compose.yml +++ b/blueprints/certmate/docker-compose.yml @@ -11,7 +11,7 @@ services: - CLOUDFLARE_TOKEN=${CLOUDFLARE_TOKEN} - LETSENCRYPT_EMAIL=${LETSENCRYPT_EMAIL} - LOG_LEVEL=${LOG_LEVEL} - ports: + expose: - 8000 volumes: - certmate_certificates:/app/certificates diff --git a/blueprints/changedetection/docker-compose.yml b/blueprints/changedetection/docker-compose.yml index 11dc9d07e..9258e29e5 100644 --- a/blueprints/changedetection/docker-compose.yml +++ b/blueprints/changedetection/docker-compose.yml @@ -4,10 +4,10 @@ services: changedetection: image: ghcr.io/dgtlmoon/changedetection.io:0.49 restart: unless-stopped - ports: + expose: - 5000 volumes: - datastore:/datastore volumes: - datastore: \ No newline at end of file + datastore: diff --git a/blueprints/chatwoot/docker-compose.yml b/blueprints/chatwoot/docker-compose.yml index a5604573b..17f25a77f 100644 --- a/blueprints/chatwoot/docker-compose.yml +++ b/blueprints/chatwoot/docker-compose.yml @@ -76,4 +76,4 @@ services: volumes: chatwoot-storage: chatwoot-postgres-data: - chatwoot-redis-data: \ No newline at end of file + chatwoot-redis-data: diff --git a/blueprints/checkmate/docker-compose.yml b/blueprints/checkmate/docker-compose.yml index bfb69bbc3..8855e24da 100644 --- a/blueprints/checkmate/docker-compose.yml +++ b/blueprints/checkmate/docker-compose.yml @@ -3,7 +3,7 @@ services: server: image: ghcr.io/bluewave-labs/checkmate-backend-mono:latest restart: always - ports: + expose: - 52345 environment: - UPTIME_APP_API_BASE_URL=${UPTIME_APP_API_BASE_URL} diff --git a/blueprints/chevereto/docker-compose.yml b/blueprints/chevereto/docker-compose.yml index 89c05e058..5e08351b1 100644 --- a/blueprints/chevereto/docker-compose.yml +++ b/blueprints/chevereto/docker-compose.yml @@ -15,7 +15,7 @@ services: - CHEVERETO_HTTPS=0 - CHEVERETO_MAX_POST_SIZE=2G - CHEVERETO_MAX_UPLOAD_SIZE=2G - ports: + expose: - 80 volumes: - storage:/var/www/html/images/ @@ -35,4 +35,4 @@ services: volumes: storage: - mysql: \ No newline at end of file + mysql: diff --git a/blueprints/chiefonboarding/docker-compose.yml b/blueprints/chiefonboarding/docker-compose.yml index cf57cb1ad..a2621e430 100644 --- a/blueprints/chiefonboarding/docker-compose.yml +++ b/blueprints/chiefonboarding/docker-compose.yml @@ -8,7 +8,7 @@ services: - SECRET_KEY=${SECRET_KEY} - DATABASE_URL=postgres://postgres:${DB_PASSWORD}@db:5432/chiefonboarding - ALLOWED_HOSTS=${DOMAIN} - ports: + expose: - 8000 depends_on: - db @@ -23,4 +23,4 @@ services: - postgres_data:/var/lib/postgresql/data volumes: - postgres_data: \ No newline at end of file + postgres_data: diff --git a/blueprints/chirpstack/docker-compose.yml b/blueprints/chirpstack/docker-compose.yml index 85b32a12a..62894dfd1 100644 --- a/blueprints/chirpstack/docker-compose.yml +++ b/blueprints/chirpstack/docker-compose.yml @@ -5,7 +5,7 @@ services: restart: unless-stopped volumes: - ../files/chirpstack:/etc/chirpstack - ports: + expose: - 8080 environment: - MQTT_BROKER_HOST=mosquitto @@ -21,7 +21,7 @@ services: restart: unless-stopped volumes: - ../files/chirpstack-gateway-bridge:/etc/chirpstack-gateway-bridge - ports: + expose: - 1700/udp environment: - INTEGRATION__MQTT__EVENT_TOPIC_TEMPLATE=eu868/gateway/{{ .GatewayID }}/event/{{ .EventType }} @@ -36,7 +36,7 @@ services: restart: unless-stopped volumes: - ../files/chirpstack-gateway-bridge:/etc/chirpstack-gateway-bridge - ports: + expose: - 3001 depends_on: - mosquitto @@ -45,7 +45,7 @@ services: image: chirpstack/chirpstack-rest-api:4 restart: unless-stopped command: --server chirpstack:8080 --bind 0.0.0.0:8090 --insecure - ports: + expose: - 8090 depends_on: - chirpstack @@ -73,9 +73,8 @@ services: restart: unless-stopped volumes: - ../files/mosquitto/config/:/mosquitto/config/ - ports: + expose: - 1883 - volumes: postgresqldata: redisdata: diff --git a/blueprints/chromium/docker-compose.yml b/blueprints/chromium/docker-compose.yml index f6f766e2a..f555f7f2f 100644 --- a/blueprints/chromium/docker-compose.yml +++ b/blueprints/chromium/docker-compose.yml @@ -3,7 +3,7 @@ version: "3" services: chromium: image: lscr.io/linuxserver/chromium:5f5dd27e-ls102 - ports: + expose: - "3000" environment: - PUID=1000 @@ -13,4 +13,4 @@ services: - config:/config volumes: - config: \ No newline at end of file + config: diff --git a/blueprints/classicpress/docker-compose.yml b/blueprints/classicpress/docker-compose.yml index 1f15f43bc..3842985d2 100644 --- a/blueprints/classicpress/docker-compose.yml +++ b/blueprints/classicpress/docker-compose.yml @@ -3,7 +3,7 @@ version: "3" services: classicpress: image: classicpress/classicpress:php8.3-apache - ports: + expose: - "80" environment: - WORDPRESS_DB_HOST=db @@ -27,4 +27,4 @@ services: volumes: wordpress-data: - db-data: \ No newline at end of file + db-data: diff --git a/blueprints/cloud9/docker-compose.yml b/blueprints/cloud9/docker-compose.yml index cf836bbff..6a27f7489 100644 --- a/blueprints/cloud9/docker-compose.yml +++ b/blueprints/cloud9/docker-compose.yml @@ -3,7 +3,7 @@ version: "3" services: cloud9: image: lscr.io/linuxserver/cloud9:1.29.2 - ports: + expose: - "8000" environment: - PUID=1000 @@ -17,4 +17,4 @@ services: - code:/code volumes: - code: \ No newline at end of file + code: diff --git a/blueprints/cloudcommander/docker-compose.yml b/blueprints/cloudcommander/docker-compose.yml index 33690f09f..5a29f3a54 100644 --- a/blueprints/cloudcommander/docker-compose.yml +++ b/blueprints/cloudcommander/docker-compose.yml @@ -3,7 +3,7 @@ version: "3" services: cloudcmd: image: coderaiser/cloudcmd:18.5.1 - ports: + expose: - "80" environment: - CLOUDCMD_ROOT=/mnt/fs @@ -12,4 +12,4 @@ services: - CLOUDCMD_PASSWORD=${PASSWORD} volumes: - /root:/root - - /:/mnt/fs \ No newline at end of file + - /:/mnt/fs diff --git a/blueprints/cockpit/docker-compose.yml b/blueprints/cockpit/docker-compose.yml index fbc99a8e8..7c6a652ae 100644 --- a/blueprints/cockpit/docker-compose.yml +++ b/blueprints/cockpit/docker-compose.yml @@ -3,7 +3,7 @@ version: "3" services: cockpit: image: cockpithq/cockpit:core-2.11.0 - ports: + expose: - "80" environment: - COCKPIT_SESSION_NAME=cockpit @@ -27,4 +27,4 @@ services: volumes: config: storage: - mongo-data: \ No newline at end of file + mongo-data: diff --git a/blueprints/codex-docs/docker-compose.yml b/blueprints/codex-docs/docker-compose.yml index 99fb55975..78549d072 100644 --- a/blueprints/codex-docs/docker-compose.yml +++ b/blueprints/codex-docs/docker-compose.yml @@ -3,7 +3,7 @@ version: "3" services: codex: image: ghcr.io/codex-team/codex.docs:v2.2 - ports: + expose: - "3000" environment: - APP_CONFIG_database_driver=mongodb @@ -28,4 +28,4 @@ services: volumes: uploads: db: - mongo-data: \ No newline at end of file + mongo-data: diff --git a/blueprints/collabora-office/docker-compose.yml b/blueprints/collabora-office/docker-compose.yml index 2d6faeb62..8d8bd91b3 100644 --- a/blueprints/collabora-office/docker-compose.yml +++ b/blueprints/collabora-office/docker-compose.yml @@ -3,10 +3,10 @@ version: "3" services: collabora: image: collabora/code:latest - ports: + expose: - "9980" environment: - domain=${DOMAIN} - username=${USERNAME} - password=${PASSWORD} - - extra_params=--o:ssl.enable=false \ No newline at end of file + - extra_params=--o:ssl.enable=false diff --git a/blueprints/commafeed/docker-compose.yml b/blueprints/commafeed/docker-compose.yml index 3996e54c6..6037c626a 100644 --- a/blueprints/commafeed/docker-compose.yml +++ b/blueprints/commafeed/docker-compose.yml @@ -5,5 +5,5 @@ services: restart: unless-stopped volumes: - ../files/commafeed-data:/commafeed/data - ports: + expose: - 8082 diff --git a/blueprints/commento/docker-compose.yml b/blueprints/commento/docker-compose.yml index ebac8389e..971e1d496 100644 --- a/blueprints/commento/docker-compose.yml +++ b/blueprints/commento/docker-compose.yml @@ -3,7 +3,7 @@ version: "3" services: commento: image: registry.gitlab.com/commento/commento:v1.8.0 - ports: + expose: - "8080" environment: - COMMENTO_ORIGIN=${COMMENTO_ORIGIN} @@ -19,4 +19,4 @@ services: - postgres-data:/var/lib/postgresql/data volumes: - postgres-data: \ No newline at end of file + postgres-data: diff --git a/blueprints/commentoplusplus/docker-compose.yml b/blueprints/commentoplusplus/docker-compose.yml index fc555f3ae..98af00e6d 100644 --- a/blueprints/commentoplusplus/docker-compose.yml +++ b/blueprints/commentoplusplus/docker-compose.yml @@ -3,7 +3,7 @@ version: "3" services: commentoplusplus: image: caroga/commentoplusplus:v1.8.7 - ports: + expose: - "8080" environment: - COMMENTO_ORIGIN=${COMMENTO_ORIGIN} @@ -22,4 +22,4 @@ services: - postgres-data:/var/lib/postgresql/data volumes: - postgres-data: \ No newline at end of file + postgres-data: diff --git a/blueprints/confluence/docker-compose.yml b/blueprints/confluence/docker-compose.yml index 3142cfe79..53a069377 100644 --- a/blueprints/confluence/docker-compose.yml +++ b/blueprints/confluence/docker-compose.yml @@ -3,7 +3,7 @@ version: "3" services: confluence: image: atlassian/confluence-server:8.6-ubuntu-jdk17 - ports: + expose: - "8090" volumes: - confluence-data:/var/atlassian/application-data/confluence @@ -15,4 +15,4 @@ services: - CATALINA_CONNECTOR_SECURE=true volumes: - confluence-data: \ No newline at end of file + confluence-data: diff --git a/blueprints/convertx/docker-compose.yml b/blueprints/convertx/docker-compose.yml index c372ce505..d5de4b0c9 100644 --- a/blueprints/convertx/docker-compose.yml +++ b/blueprints/convertx/docker-compose.yml @@ -3,7 +3,7 @@ services: convertx: image: ghcr.io/c4illin/convertx restart: unless-stopped - ports: + expose: - 3000 environment: - JWT_SECRET=${JWT_SECRET} diff --git a/blueprints/coralproject/docker-compose.yml b/blueprints/coralproject/docker-compose.yml index f308098d5..55483c8fa 100644 --- a/blueprints/coralproject/docker-compose.yml +++ b/blueprints/coralproject/docker-compose.yml @@ -3,7 +3,7 @@ version: "3" services: coral: image: coralproject/talk:9.7.0 - ports: + expose: - "3000" environment: - MONGODB_URI=${MONGODB_URI} @@ -30,4 +30,4 @@ services: volumes: mongo-data: - redis-data: \ No newline at end of file + redis-data: diff --git a/blueprints/couchdb/docker-compose.yml b/blueprints/couchdb/docker-compose.yml index cb00bf69d..2263c7421 100644 --- a/blueprints/couchdb/docker-compose.yml +++ b/blueprints/couchdb/docker-compose.yml @@ -3,7 +3,7 @@ version: '3.8' services: couchdb: image: couchdb:latest - ports: + expose: - '5984' volumes: - couchdb-data:/opt/couchdb/data diff --git a/blueprints/crowdsec/docker-compose.yml b/blueprints/crowdsec/docker-compose.yml index ae9ab573a..f8c05e8ae 100644 --- a/blueprints/crowdsec/docker-compose.yml +++ b/blueprints/crowdsec/docker-compose.yml @@ -28,4 +28,4 @@ services: restart: unless-stopped volumes: crowdsec-db: - crowdsec-config: \ No newline at end of file + crowdsec-config: diff --git a/blueprints/cup/docker-compose.yml b/blueprints/cup/docker-compose.yml index 6775151a6..8677cf0d7 100644 --- a/blueprints/cup/docker-compose.yml +++ b/blueprints/cup/docker-compose.yml @@ -5,7 +5,7 @@ services: image: ghcr.io/sergi0g/cup:latest restart: unless-stopped command: serve - ports: + expose: - "8000" volumes: - /var/run/docker.sock:/var/run/docker.sock diff --git a/blueprints/cut/docker-compose.yml b/blueprints/cut/docker-compose.yml index b31578d10..389295a49 100644 --- a/blueprints/cut/docker-compose.yml +++ b/blueprints/cut/docker-compose.yml @@ -11,7 +11,7 @@ services: # Bundled Redis below, private to the project network. - REDIS_URL=redis://redis:6379 # Expose the container port only; Dokploy maps the domain to it. - ports: + expose: - 3000 healthcheck: test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/"] diff --git a/blueprints/dashy/docker-compose.yml b/blueprints/dashy/docker-compose.yml index df27c431f..8614ef3af 100644 --- a/blueprints/dashy/docker-compose.yml +++ b/blueprints/dashy/docker-compose.yml @@ -3,9 +3,9 @@ services: dashy: image: lissy93/dashy:latest restart: unless-stopped - ports: + expose: - 8080 volumes: - dashy-config:/app/user-data volumes: - dashy-config: {} \ No newline at end of file + dashy-config: {} diff --git a/blueprints/databasus/docker-compose.yml b/blueprints/databasus/docker-compose.yml index aafa7744a..b46a337a1 100644 --- a/blueprints/databasus/docker-compose.yml +++ b/blueprints/databasus/docker-compose.yml @@ -1,7 +1,7 @@ services: databasus: image: databasus/databasus:latest - ports: + expose: - "4005" volumes: # Persistent data storage diff --git a/blueprints/directory-lister/docker-compose.yml b/blueprints/directory-lister/docker-compose.yml index 0e073c6e7..3c50cbe04 100644 --- a/blueprints/directory-lister/docker-compose.yml +++ b/blueprints/directory-lister/docker-compose.yml @@ -2,8 +2,7 @@ services: directory-lister: image: directorylister/directorylister:latest restart: unless-stopped - ports: - # The internal port of the application. + expose: - 80 volumes: # Mounts a persistent named volume to store directory data. diff --git a/blueprints/directus/docker-compose.yml b/blueprints/directus/docker-compose.yml index ef27d2ba2..a4af6581a 100644 --- a/blueprints/directus/docker-compose.yml +++ b/blueprints/directus/docker-compose.yml @@ -1,7 +1,7 @@ services: directus: image: directus/directus:12.1.1 - ports: + expose: - 8055 volumes: - directus_uploads:/directus/uploads diff --git a/blueprints/discord-tickets/docker-compose.yml b/blueprints/discord-tickets/docker-compose.yml index 62105b3a1..93c62d8de 100644 --- a/blueprints/discord-tickets/docker-compose.yml +++ b/blueprints/discord-tickets/docker-compose.yml @@ -47,4 +47,4 @@ services: volumes: tickets-mysql-data: - tickets-app-data: \ No newline at end of file + tickets-app-data: diff --git a/blueprints/discourse/docker-compose.yml b/blueprints/discourse/docker-compose.yml index d0353e8af..4920374e3 100644 --- a/blueprints/discourse/docker-compose.yml +++ b/blueprints/discourse/docker-compose.yml @@ -54,7 +54,7 @@ services: # DISCOURSE_SMTP_ENABLE_START_TLS: "true" # DISCOURSE_NOTIFICATION_EMAIL: [email protected] # DISCOURSE_DEVELOPER_EMAILS: [email protected] - ports: + expose: - 80 restart: unless-stopped diff --git a/blueprints/dockge/docker-compose.yml b/blueprints/dockge/docker-compose.yml index 8736f2fd5..6b9a17331 100644 --- a/blueprints/dockge/docker-compose.yml +++ b/blueprints/dockge/docker-compose.yml @@ -8,9 +8,8 @@ services: - dockge_stacks:/opt/stacks environment: DOCKGE_STACKS_DIR: /opt/stacks - ports: + expose: - "5001" - volumes: dockge_data: dockge_stacks: diff --git a/blueprints/docling-serve/docker-compose.yml b/blueprints/docling-serve/docker-compose.yml index 28b6b3652..42bcba796 100644 --- a/blueprints/docling-serve/docker-compose.yml +++ b/blueprints/docling-serve/docker-compose.yml @@ -2,7 +2,7 @@ services: docling-serve: image: quay.io/docling-project/docling-serve:latest restart: unless-stopped - ports: + expose: - 5001 environment: - DOCLING_SERVE_ENABLE_UI=1 diff --git a/blueprints/docmost/docker-compose.yml b/blueprints/docmost/docker-compose.yml index d7efba929..b74bba2e6 100644 --- a/blueprints/docmost/docker-compose.yml +++ b/blueprints/docmost/docker-compose.yml @@ -37,4 +37,4 @@ services: volumes: docmost: db_docmost_data: - redis_docmost_data: \ No newline at end of file + redis_docmost_data: diff --git a/blueprints/documenso/docker-compose.yml b/blueprints/documenso/docker-compose.yml index 2eb563167..273791fdb 100644 --- a/blueprints/documenso/docker-compose.yml +++ b/blueprints/documenso/docker-compose.yml @@ -43,7 +43,7 @@ services: - CERT_INFO_ORGANIZATIONAL_UNIT=${CERT_INFO_ORGANIZATIONAL_UNIT:-IT Department} - CERT_INFO_EMAIL=${CERT_INFO_EMAIL:-admin@example.com} - DOCUMENSO_HOST=${DOCUMENSO_HOST} - ports: + expose: - ${DOCUMENSO_PORT} entrypoint: - /bin/sh diff --git a/blueprints/docuseal/docker-compose.yml b/blueprints/docuseal/docker-compose.yml index f5a180b1c..f976fb251 100644 --- a/blueprints/docuseal/docker-compose.yml +++ b/blueprints/docuseal/docker-compose.yml @@ -35,4 +35,4 @@ services: volumes: docuseal: docuseal-db: - docuseal-redis-data: \ No newline at end of file + docuseal-redis-data: diff --git a/blueprints/dragonfly-db/docker-compose.yml b/blueprints/dragonfly-db/docker-compose.yml index 7feaeb82b..44920f546 100644 --- a/blueprints/dragonfly-db/docker-compose.yml +++ b/blueprints/dragonfly-db/docker-compose.yml @@ -11,4 +11,4 @@ services: environment: - DFLY_requirepass volumes: - dragonflydata: \ No newline at end of file + dragonflydata: diff --git a/blueprints/drawio/docker-compose.yml b/blueprints/drawio/docker-compose.yml index cdd036d92..8a8921020 100644 --- a/blueprints/drawio/docker-compose.yml +++ b/blueprints/drawio/docker-compose.yml @@ -2,23 +2,21 @@ version: '3' services: plantuml-server: image: plantuml/plantuml-server - ports: + expose: - "8080" - volumes: - fonts_volume:/usr/share/fonts/drawio image-export: image: jgraph/export-server - ports: + expose: - "8000" - volumes: - fonts_volume:/usr/share/fonts/drawio environment: - DRAWIO_BASE_URL=${DRAWIO_BASE_URL} drawio: image: jgraph/drawio:24.7.17 - ports: + expose: - "8080" links: - plantuml-server:plantuml-server @@ -52,4 +50,4 @@ services: DRAWIO_GITLAB_URL: ${DRAWIO_GITLAB_URL} DRAWIO_CLOUD_CONVERT_APIKEY: ${DRAWIO_CLOUD_CONVERT_APIKEY} volumes: - fonts_volume: \ No newline at end of file + fonts_volume: diff --git a/blueprints/dumbassets/docker-compose.yml b/blueprints/dumbassets/docker-compose.yml index eda4a3f77..5f96f1dd2 100644 --- a/blueprints/dumbassets/docker-compose.yml +++ b/blueprints/dumbassets/docker-compose.yml @@ -3,7 +3,7 @@ services: dumbassets: image: dumbwareio/dumbassets:latest restart: unless-stopped - ports: + expose: - 3000 volumes: - dumbassets-data:/app/data diff --git a/blueprints/dumbbudget/docker-compose.yml b/blueprints/dumbbudget/docker-compose.yml index 1d7607788..41fdbe855 100644 --- a/blueprints/dumbbudget/docker-compose.yml +++ b/blueprints/dumbbudget/docker-compose.yml @@ -3,7 +3,7 @@ services: dumbbudget: image: dumbwareio/dumbbudget:latest restart: unless-stopped - ports: + expose: - 3000 volumes: - dumbbudget-data:/app/data diff --git a/blueprints/dumbdrop/docker-compose.yml b/blueprints/dumbdrop/docker-compose.yml index 993e0b304..f4e2650b6 100644 --- a/blueprints/dumbdrop/docker-compose.yml +++ b/blueprints/dumbdrop/docker-compose.yml @@ -2,7 +2,7 @@ services: dumbdrop: image: dumbwareio/dumbdrop:latest restart: unless-stopped - ports: + expose: - 3000 volumes: - dumbdrop-uploads:/app/uploads diff --git a/blueprints/dumbpad/docker-compose.yml b/blueprints/dumbpad/docker-compose.yml index ec9487c59..ea536cb38 100644 --- a/blueprints/dumbpad/docker-compose.yml +++ b/blueprints/dumbpad/docker-compose.yml @@ -3,7 +3,7 @@ services: dumbpad: image: dumbwareio/dumbpad:latest restart: unless-stopped - ports: + expose: - 3000 volumes: - dumbpad-data:/app/data diff --git a/blueprints/elastic-search/docker-compose.yml b/blueprints/elastic-search/docker-compose.yml index 929006ff1..35110aabc 100644 --- a/blueprints/elastic-search/docker-compose.yml +++ b/blueprints/elastic-search/docker-compose.yml @@ -13,7 +13,7 @@ services: memlock: soft: -1 hard: -1 - ports: + expose: - "9200" volumes: - es_data:/usr/share/elasticsearch/data @@ -23,7 +23,7 @@ services: container_name: kibana environment: - ELASTICSEARCH_HOSTS=http://elasticsearch:9200 - ports: + expose: - "5601" depends_on: - elasticsearch @@ -31,4 +31,4 @@ services: volumes: es_data: driver: local - \ No newline at end of file + diff --git a/blueprints/emby/docker-compose.yml b/blueprints/emby/docker-compose.yml index 1b7cb15aa..32b54f189 100644 --- a/blueprints/emby/docker-compose.yml +++ b/blueprints/emby/docker-compose.yml @@ -12,9 +12,9 @@ services: - /emby/programdata:/config # Configuration directory - /emby/series:/mnt/share1 # Media directory - /emby/movies:/mnt/share2 # Media directory - ports: + expose: - 8096 # HTTP port - 8920 # HTTPS port devices: - /dev/dri:/dev/dri # VAAPI/NVDEC/NVENC render nodes - restart: on-failure \ No newline at end of file + restart: on-failure diff --git a/blueprints/enshrouded/docker-compose.yml b/blueprints/enshrouded/docker-compose.yml index 614dd36e3..9a023c499 100644 --- a/blueprints/enshrouded/docker-compose.yml +++ b/blueprints/enshrouded/docker-compose.yml @@ -22,4 +22,4 @@ services: - PGID=4711 volumes: - enshrouded-persistent-data: \ No newline at end of file + enshrouded-persistent-data: diff --git a/blueprints/erpnext-v16/docker-compose.yml b/blueprints/erpnext-v16/docker-compose.yml index 3201b4d77..0f5c7c735 100644 --- a/blueprints/erpnext-v16/docker-compose.yml +++ b/blueprints/erpnext-v16/docker-compose.yml @@ -298,4 +298,4 @@ volumes: driver_opts: type: "${SITE_VOLUME_TYPE}" o: "${SITE_VOLUME_OPTS}" - device: "${SITE_VOLUME_DEV}" \ No newline at end of file + device: "${SITE_VOLUME_DEV}" diff --git a/blueprints/erpnext/docker-compose.yml b/blueprints/erpnext/docker-compose.yml index 28cd8f6ab..44561de62 100644 --- a/blueprints/erpnext/docker-compose.yml +++ b/blueprints/erpnext/docker-compose.yml @@ -351,4 +351,4 @@ volumes: device: "${SITE_VOLUME_DEV}" networks: - bench-network: \ No newline at end of file + bench-network: diff --git a/blueprints/evershop/docker-compose.yml b/blueprints/evershop/docker-compose.yml index d0a7fcd01..71832e555 100644 --- a/blueprints/evershop/docker-compose.yml +++ b/blueprints/evershop/docker-compose.yml @@ -12,9 +12,8 @@ services: DB_NAME: ${DB_NAME} depends_on: - database - ports: + expose: - 3000 - database: image: postgres:16 restart: unless-stopped @@ -24,8 +23,7 @@ services: POSTGRES_PASSWORD: ${DB_PASSWORD} POSTGRES_USER: ${DB_USER} POSTGRES_DB: ${DB_NAME} - ports: + expose: - ${DB_PORT} - volumes: postgres-data: diff --git a/blueprints/evolutionapi/docker-compose.yml b/blueprints/evolutionapi/docker-compose.yml index 5bdd30751..adb37b6ac 100644 --- a/blueprints/evolutionapi/docker-compose.yml +++ b/blueprints/evolutionapi/docker-compose.yml @@ -57,4 +57,4 @@ services: volumes: evolution-instances: evolution-postgres-data: - evolution-redis-data: \ No newline at end of file + evolution-redis-data: diff --git a/blueprints/filebrowser/docker-compose.yml b/blueprints/filebrowser/docker-compose.yml index 10c119091..0a5d4c08c 100644 --- a/blueprints/filebrowser/docker-compose.yml +++ b/blueprints/filebrowser/docker-compose.yml @@ -11,4 +11,4 @@ services: volumes: filebrowser-data: filebrowser-config: - \ No newline at end of file + diff --git a/blueprints/filegator/docker-compose.yml b/blueprints/filegator/docker-compose.yml index f2f733516..bce6fdcb9 100644 --- a/blueprints/filegator/docker-compose.yml +++ b/blueprints/filegator/docker-compose.yml @@ -5,9 +5,8 @@ services: volumes: - filegator_repository:/var/www/filegator/repository - filegator_private:/var/www/filegator/private - ports: + expose: - "8080" - volumes: filegator_repository: filegator_private: diff --git a/blueprints/filestash/docker-compose.yml b/blueprints/filestash/docker-compose.yml index 6659fc4a6..c0c98a270 100644 --- a/blueprints/filestash/docker-compose.yml +++ b/blueprints/filestash/docker-compose.yml @@ -10,7 +10,7 @@ services: - OFFICE_URL=${OFFICE_URL} - OFFICE_FILESTASH_URL=${OFFICE_FILESTASH_URL} - OFFICE_REWRITE_URL=${OFFICE_REWRITE_URL} - ports: + expose: - 8334 volumes: - filestash:/app/data/state/ @@ -29,8 +29,7 @@ services: curl -o /usr/share/coolwsd/browser/dist/branding-desktop.css https://gist.githubusercontent.com/mickael-kerjean/bc1f57cd312cf04731d30185cc4e7ba2/raw/d706dcdf23c21441e5af289d871b33defc2770ea/destop.css /bin/su -s /bin/bash -c '/start-collabora-online.sh' cool user: root - ports: + expose: - 9980 - volumes: - filestash: \ No newline at end of file + filestash: diff --git a/blueprints/firecrawl/docker-compose.yml b/blueprints/firecrawl/docker-compose.yml index d7622f7a5..1a36c8f71 100644 --- a/blueprints/firecrawl/docker-compose.yml +++ b/blueprints/firecrawl/docker-compose.yml @@ -56,7 +56,7 @@ services: api: <<: *common-service restart: unless-stopped - ports: + expose: - "3002" environment: <<: *common-env @@ -126,4 +126,4 @@ services: retries: 10 volumes: - nuq_pg_data: \ No newline at end of file + nuq_pg_data: diff --git a/blueprints/fivem/docker-compose.yml b/blueprints/fivem/docker-compose.yml index 162239bff..7ea36bd54 100644 --- a/blueprints/fivem/docker-compose.yml +++ b/blueprints/fivem/docker-compose.yml @@ -38,4 +38,4 @@ services: volumes: fivem_config: - fivem_txdata: \ No newline at end of file + fivem_txdata: diff --git a/blueprints/flagsmith/docker-compose.yml b/blueprints/flagsmith/docker-compose.yml index 126051f79..99f66db0d 100644 --- a/blueprints/flagsmith/docker-compose.yml +++ b/blueprints/flagsmith/docker-compose.yml @@ -54,7 +54,7 @@ services: # EMAIL_HOST_PASSWORD: smtp_account_password # EMAIL_PORT: 587 # optional # EMAIL_USE_TLS: 'true' # optional - ports: + expose: - 8000 depends_on: postgres: @@ -69,7 +69,7 @@ services: USE_POSTGRES_FOR_ANALYTICS: "true" DJANGO_ALLOWED_HOSTS: "*" PROMETHEUS_ENABLED: "true" - ports: + expose: - 8000 depends_on: - flagsmith diff --git a/blueprints/flaresolverr/docker-compose.yml b/blueprints/flaresolverr/docker-compose.yml index 318141336..d8e61526c 100644 --- a/blueprints/flaresolverr/docker-compose.yml +++ b/blueprints/flaresolverr/docker-compose.yml @@ -2,6 +2,6 @@ version: "3.8" services: flaresolverr: image: ghcr.io/flaresolverr/flaresolverr:latest - ports: + expose: - 8191 restart: unless-stopped diff --git a/blueprints/flarum/docker-compose.yml b/blueprints/flarum/docker-compose.yml index 62595ec9b..80a126647 100644 --- a/blueprints/flarum/docker-compose.yml +++ b/blueprints/flarum/docker-compose.yml @@ -3,7 +3,7 @@ services: flarum: image: crazymax/flarum:latest restart: unless-stopped - ports: + expose: - 8000 environment: - FLARUM_BASE_URL=https://${FLARUM_DOMAIN} diff --git a/blueprints/fmd-server/docker-compose.yml b/blueprints/fmd-server/docker-compose.yml index 85bf58767..530131f35 100644 --- a/blueprints/fmd-server/docker-compose.yml +++ b/blueprints/fmd-server/docker-compose.yml @@ -1,7 +1,7 @@ services: fmd-server: image: registry.gitlab.com/fmd-foss/fmd-server:v0.11.0 - ports: + expose: - 8080 environment: # View all configurations at: diff --git a/blueprints/focalboard/docker-compose.yml b/blueprints/focalboard/docker-compose.yml index b2283c7ae..b33b687cb 100644 --- a/blueprints/focalboard/docker-compose.yml +++ b/blueprints/focalboard/docker-compose.yml @@ -25,4 +25,4 @@ volumes: focalboardData: driver: local focalboardPostgre: - driver: local \ No newline at end of file + driver: local diff --git a/blueprints/fonoster/docker-compose.yml b/blueprints/fonoster/docker-compose.yml index 38ddb5718..d1c23bba9 100644 --- a/blueprints/fonoster/docker-compose.yml +++ b/blueprints/fonoster/docker-compose.yml @@ -2,7 +2,7 @@ services: dashboard: image: fonoster/dashboard:0.15.15 restart: unless-stopped - ports: + expose: - 3030 environment: - SERVER_DASHBOARD_SESSION_SECRET=${SERVER_DASHBOARD_SESSION_SECRET} @@ -199,7 +199,6 @@ services: - ../files/config:/etc/envoy:ro ports: - 8449:8449 - volumes: db: influxdb: diff --git a/blueprints/forgejo/docker-compose.yml b/blueprints/forgejo/docker-compose.yml index 36733c6ea..4b13b97a6 100644 --- a/blueprints/forgejo/docker-compose.yml +++ b/blueprints/forgejo/docker-compose.yml @@ -34,4 +34,4 @@ volumes: forgejo_db: driver: local forgejo_server: - driver: local \ No newline at end of file + driver: local diff --git a/blueprints/formbricks/docker-compose.yml b/blueprints/formbricks/docker-compose.yml index 55300d4e8..9d1107793 100644 --- a/blueprints/formbricks/docker-compose.yml +++ b/blueprints/formbricks/docker-compose.yml @@ -25,7 +25,7 @@ services: image: ghcr.io/formbricks/formbricks:v3.1.5 depends_on: - postgres - ports: + expose: - 3000 volumes: - ../files/uploads:/home/nextjs/apps/web/uploads/ diff --git a/blueprints/frappe-hr/docker-compose.yml b/blueprints/frappe-hr/docker-compose.yml index a7ce9b262..7eec52c4b 100644 --- a/blueprints/frappe-hr/docker-compose.yml +++ b/blueprints/frappe-hr/docker-compose.yml @@ -351,4 +351,4 @@ volumes: device: "${SITE_VOLUME_DEV}" networks: - bench-network: \ No newline at end of file + bench-network: diff --git a/blueprints/freshrss/docker-compose.yml b/blueprints/freshrss/docker-compose.yml index 8965edc42..248b82f60 100644 --- a/blueprints/freshrss/docker-compose.yml +++ b/blueprints/freshrss/docker-compose.yml @@ -7,7 +7,7 @@ services: - freshrss_data:/var/www/FreshRSS/data # Optional volume for storing third-party extensions - freshrss_extensions:/var/www/FreshRSS/extensions - ports: + expose: - "80" environment: # Server timezone diff --git a/blueprints/garage-with-ui/docker-compose.yml b/blueprints/garage-with-ui/docker-compose.yml index 7495e1680..8b2800fc0 100644 --- a/blueprints/garage-with-ui/docker-compose.yml +++ b/blueprints/garage-with-ui/docker-compose.yml @@ -6,18 +6,17 @@ services: - garage-storage:/var/lib/garage - garage-storage:/var/lib/garage restart: unless-stopped - ports: + expose: - 3900 - 3901 - 3902 - 3903 - garage-webui: image: khairul169/garage-webui:1.1.0 restart: unless-stopped volumes: - ../files/garage.toml:/etc/garage.toml:ro - ports: + expose: - 3909 environment: - AUTH_USER_PASS @@ -25,4 +24,4 @@ services: - S3_ENDPOINT_URL volumes: - garage-storage: {} \ No newline at end of file + garage-storage: {} diff --git a/blueprints/garage/docker-compose.yml b/blueprints/garage/docker-compose.yml index 105049344..b6b9253f5 100644 --- a/blueprints/garage/docker-compose.yml +++ b/blueprints/garage/docker-compose.yml @@ -6,11 +6,10 @@ services: - garage-storage:/var/lib/garage - garage-storage:/var/lib/garage restart: unless-stopped - ports: + expose: - 3900 - 3901 - 3902 - 3903 - volumes: - garage-storage: {} \ No newline at end of file + garage-storage: {} diff --git a/blueprints/gel/docker-compose.yml b/blueprints/gel/docker-compose.yml index 248909117..65215fe4d 100644 --- a/blueprints/gel/docker-compose.yml +++ b/blueprints/gel/docker-compose.yml @@ -3,7 +3,7 @@ services: gel: image: geldata/gel:6 restart: unless-stopped - ports: + expose: - 5656 environment: - GEL_SERVER_SECURITY=${GEL_SERVER_SECURITY} diff --git a/blueprints/glance/docker-compose.yml b/blueprints/glance/docker-compose.yml index ace8bc940..43510fab1 100644 --- a/blueprints/glance/docker-compose.yml +++ b/blueprints/glance/docker-compose.yml @@ -6,6 +6,6 @@ services: - ../files/app/assets:/app/assets # Optionally, also mount docker socket if you want to use the docker containers widget # - /var/run/docker.sock:/var/run/docker.sock:ro - ports: + expose: - 8080 - env_file: .env \ No newline at end of file + env_file: .env diff --git a/blueprints/go-whatsapp-web-multidevice/docker-compose.yml b/blueprints/go-whatsapp-web-multidevice/docker-compose.yml index 3f998bb66..3d57f662c 100644 --- a/blueprints/go-whatsapp-web-multidevice/docker-compose.yml +++ b/blueprints/go-whatsapp-web-multidevice/docker-compose.yml @@ -2,7 +2,7 @@ services: whatsapp: image: aldinokemal2104/go-whatsapp-web-multidevice restart: always - ports: + expose: - "3080" volumes: - whatsapp:/app/storages diff --git a/blueprints/gotify/docker-compose.yml b/blueprints/gotify/docker-compose.yml index 1d3d82656..c23a65f95 100644 --- a/blueprints/gotify/docker-compose.yml +++ b/blueprints/gotify/docker-compose.yml @@ -4,8 +4,7 @@ services: restart: unless-stopped volumes: - gotify_data:/app/data - ports: + expose: - "80" - volumes: gotify_data: diff --git a/blueprints/grimoire/docker-compose.yml b/blueprints/grimoire/docker-compose.yml index d497f9826..d0bdd66f4 100644 --- a/blueprints/grimoire/docker-compose.yml +++ b/blueprints/grimoire/docker-compose.yml @@ -4,7 +4,7 @@ services: restart: unless-stopped volumes: - grimoire_data:/app/data - ports: + expose: - "5173" volumes: grimoire_data: {} diff --git a/blueprints/grist/docker-compose.yml b/blueprints/grist/docker-compose.yml index 4301c7173..8a6cd93d4 100644 --- a/blueprints/grist/docker-compose.yml +++ b/blueprints/grist/docker-compose.yml @@ -3,7 +3,7 @@ services: grist: image: gristlabs/grist:latest restart: unless-stopped - ports: + expose: - 8484 volumes: - grist_data:/persist diff --git a/blueprints/habitica/docker-compose.yml b/blueprints/habitica/docker-compose.yml index 3990e571e..c72618e38 100644 --- a/blueprints/habitica/docker-compose.yml +++ b/blueprints/habitica/docker-compose.yml @@ -19,9 +19,8 @@ services: restart: unless-stopped depends_on: - server - ports: + expose: - "80" - mongo: image: docker.io/mongo:latest restart: unless-stopped diff --git a/blueprints/hermes/docker-compose.yml b/blueprints/hermes/docker-compose.yml index 6e915e33c..623a4d359 100644 --- a/blueprints/hermes/docker-compose.yml +++ b/blueprints/hermes/docker-compose.yml @@ -4,7 +4,7 @@ services: image: nousresearch/hermes-agent:v2026.6.19 restart: unless-stopped command: gateway run - ports: + expose: - 9119 - 8642 environment: diff --git a/blueprints/heyform/docker-compose.yml b/blueprints/heyform/docker-compose.yml index 3374befc2..1dec7d438 100644 --- a/blueprints/heyform/docker-compose.yml +++ b/blueprints/heyform/docker-compose.yml @@ -8,7 +8,7 @@ services: depends_on: - mongo - keydb - ports: + expose: - 8000 env_file: - .env diff --git a/blueprints/hi-events/docker-compose.yml b/blueprints/hi-events/docker-compose.yml index dd3d7269a..e83a87efc 100644 --- a/blueprints/hi-events/docker-compose.yml +++ b/blueprints/hi-events/docker-compose.yml @@ -39,4 +39,4 @@ services: - pg_hi-events_data:/var/lib/postgresql/data volumes: - pg_hi-events_data: \ No newline at end of file + pg_hi-events_data: diff --git a/blueprints/hoarder/docker-compose.yml b/blueprints/hoarder/docker-compose.yml index 74a3f1d99..87526b29b 100644 --- a/blueprints/hoarder/docker-compose.yml +++ b/blueprints/hoarder/docker-compose.yml @@ -4,7 +4,7 @@ services: restart: unless-stopped volumes: - hoarder-data:/data - ports: + expose: - 3000 environment: - DISABLE_SIGNUPS @@ -43,4 +43,4 @@ services: retries: 15 volumes: meilisearch-data: - hoarder-data: \ No newline at end of file + hoarder-data: diff --git a/blueprints/homarr/docker-compose.yml b/blueprints/homarr/docker-compose.yml index c43e62f3c..8e3ff3e5c 100644 --- a/blueprints/homarr/docker-compose.yml +++ b/blueprints/homarr/docker-compose.yml @@ -7,8 +7,7 @@ services: - homarr_appdata:/appdata environment: - SECRET_ENCRYPTION_KEY=${SECRET_ENCRYPTION_KEY} - ports: + expose: - 7575 - volumes: homarr_appdata: diff --git a/blueprints/homeassistant/docker-compose.yml b/blueprints/homeassistant/docker-compose.yml index 61d2dee6d..4a5ece2e8 100644 --- a/blueprints/homeassistant/docker-compose.yml +++ b/blueprints/homeassistant/docker-compose.yml @@ -2,7 +2,7 @@ services: homeassistant: image: ghcr.io/home-assistant/home-assistant:stable restart: unless-stopped - ports: + expose: - 8123 volumes: - ../files/configuration.yaml:/config/configuration.yaml diff --git a/blueprints/homebridge/docker-compose.yml b/blueprints/homebridge/docker-compose.yml index d8aba2a9b..1584821e1 100644 --- a/blueprints/homebridge/docker-compose.yml +++ b/blueprints/homebridge/docker-compose.yml @@ -2,7 +2,7 @@ services: homebridge: image: homebridge/homebridge:latest restart: always - ports: + expose: - 8581 volumes: - ./volumes/homebridge:/homebridge diff --git a/blueprints/huly/docker-compose.yml b/blueprints/huly/docker-compose.yml index 639b10d2f..f0aea9a6e 100644 --- a/blueprints/huly/docker-compose.yml +++ b/blueprints/huly/docker-compose.yml @@ -4,7 +4,7 @@ services: nginx: image: "nginx:1.21.3" - ports: + expose: - 80 volumes: - ../files/volumes/nginx/.huly.nginx:/etc/nginx/conf.d/default.conf diff --git a/blueprints/influxdb/docker-compose.yml b/blueprints/influxdb/docker-compose.yml index 1327c6028..0e724be76 100644 --- a/blueprints/influxdb/docker-compose.yml +++ b/blueprints/influxdb/docker-compose.yml @@ -8,4 +8,4 @@ services: volumes: influxdb2-data: - influxdb2-config: \ No newline at end of file + influxdb2-config: diff --git a/blueprints/inngest/docker-compose.yml b/blueprints/inngest/docker-compose.yml index 601184f16..a915ff56c 100644 --- a/blueprints/inngest/docker-compose.yml +++ b/blueprints/inngest/docker-compose.yml @@ -35,7 +35,7 @@ services: - INNGEST_LOG_LEVEL=${INNGEST_LOG_LEVEL:-info} - INNGEST_JSON=${INNGEST_JSON:-false} - INNGEST_VERBOSE=${INNGEST_VERBOSE:-false} - ports: + expose: - 8288 depends_on: postgres: @@ -62,7 +62,7 @@ services: - PGUSER=${POSTGRES_USER} volumes: - postgres_data:/var/lib/postgresql/data - ports: + expose: - 5432 healthcheck: test: ['CMD-SHELL', 'pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}'] @@ -78,7 +78,7 @@ services: - net.core.somaxconn=1024 volumes: - redis_data:/data - ports: + expose: - 6379 healthcheck: test: ['CMD', 'redis-cli', 'ping'] diff --git a/blueprints/instantdb/docker-compose.yml b/blueprints/instantdb/docker-compose.yml index b40e043dd..e5d3b8d18 100644 --- a/blueprints/instantdb/docker-compose.yml +++ b/blueprints/instantdb/docker-compose.yml @@ -45,7 +45,7 @@ services: PYTHONUNBUFFERED: "1" NODE_ENV: "production" command: ["java", "-Djava.awt.headless=true", "-server", "-jar", "target/instant-standalone.jar"] - ports: + expose: - "8888" - "6005" logging: diff --git a/blueprints/ipfs/docker-compose.yml b/blueprints/ipfs/docker-compose.yml index 4275f5535..e04e3531c 100644 --- a/blueprints/ipfs/docker-compose.yml +++ b/blueprints/ipfs/docker-compose.yml @@ -8,7 +8,7 @@ services: volumes: - ipfs_data:/data/ipfs - ipfs_staging:/export - ports: + expose: - 4001 - 8080 - 5001 diff --git a/blueprints/java/docker-compose.yml b/blueprints/java/docker-compose.yml index 7d3d02cdf..fa87f637a 100644 --- a/blueprints/java/docker-compose.yml +++ b/blueprints/java/docker-compose.yml @@ -11,9 +11,9 @@ services: - STARTUP=${STARTUP_COMMAND} - SERVER_JARFILE=${SERVER_JARFILE} - JAVA_VERSION=${JAVA_VERSION} - ports: + expose: - ${SERVER_PORT} user: container volumes: - app-data: {} \ No newline at end of file + app-data: {} diff --git a/blueprints/jellyseerr/docker-compose.yml b/blueprints/jellyseerr/docker-compose.yml index e17f3ddb1..13bbd473a 100644 --- a/blueprints/jellyseerr/docker-compose.yml +++ b/blueprints/jellyseerr/docker-compose.yml @@ -6,8 +6,7 @@ services: - jellyseerr_config:/app/config environment: LOG_LEVEL: info - ports: + expose: - "5055" - volumes: jellyseerr_config: diff --git a/blueprints/jenkins/docker-compose.yml b/blueprints/jenkins/docker-compose.yml index f2b8ad3cb..0c9971fcf 100644 --- a/blueprints/jenkins/docker-compose.yml +++ b/blueprints/jenkins/docker-compose.yml @@ -6,8 +6,7 @@ services: volumes: - jenkins-home:/var/jenkins_home - /var/run/docker.sock:/var/run/docker.sock - ports: + expose: - 8080 - volumes: - jenkins-home: {} \ No newline at end of file + jenkins-home: {} diff --git a/blueprints/jitsi/docker-compose.yml b/blueprints/jitsi/docker-compose.yml index 3e235416e..011c5f857 100644 --- a/blueprints/jitsi/docker-compose.yml +++ b/blueprints/jitsi/docker-compose.yml @@ -74,7 +74,7 @@ services: restart: unless-stopped depends_on: - prosody - ports: + expose: - 10000 volumes: - jitsiJvbConfig:/config diff --git a/blueprints/joplin-server/docker-compose.yml b/blueprints/joplin-server/docker-compose.yml index fefac15b1..ea281de79 100644 --- a/blueprints/joplin-server/docker-compose.yml +++ b/blueprints/joplin-server/docker-compose.yml @@ -23,8 +23,7 @@ services: POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PORT: 5432 POSTGRES_HOST: joplin-db - ports: + expose: - "22300" - volumes: joplin_db_data: diff --git a/blueprints/kaneo/docker-compose.yml b/blueprints/kaneo/docker-compose.yml index 71e60b167..64061b293 100644 --- a/blueprints/kaneo/docker-compose.yml +++ b/blueprints/kaneo/docker-compose.yml @@ -14,7 +14,7 @@ services: environment: JWT_ACCESS: ${KANEO_JWT_ACCESS} DATABASE_URL: "postgresql://${KANEO_DB_USER}:${KANEO_DB_PASSWORD}@postgres:5432/${KANEO_DB}" - ports: + expose: - 1337 depends_on: postgres: @@ -25,7 +25,7 @@ services: image: ghcr.io/usekaneo/web:latest environment: KANEO_API_URL: "http://${BACKEND_HOST}" - ports: + expose: - 5173 depends_on: backend: diff --git a/blueprints/kestra/docker-compose.yml b/blueprints/kestra/docker-compose.yml index 8f9672cdf..dcceebd1c 100644 --- a/blueprints/kestra/docker-compose.yml +++ b/blueprints/kestra/docker-compose.yml @@ -57,7 +57,7 @@ services: tmpDir: path: /tmp/kestra-wd/tmp url: http://localhost:8080/ - ports: + expose: - "8080" - "8081" depends_on: diff --git a/blueprints/kimai/docker-compose.yml b/blueprints/kimai/docker-compose.yml index c97cadfbc..e73d6147a 100644 --- a/blueprints/kimai/docker-compose.yml +++ b/blueprints/kimai/docker-compose.yml @@ -42,4 +42,4 @@ services: volumes: kimai-data: - mysql-data: \ No newline at end of file + mysql-data: diff --git a/blueprints/kokoro-tts/docker-compose.yml b/blueprints/kokoro-tts/docker-compose.yml index a0298ce1b..b8fe6b272 100644 --- a/blueprints/kokoro-tts/docker-compose.yml +++ b/blueprints/kokoro-tts/docker-compose.yml @@ -4,7 +4,7 @@ services: context: https://github.com/remsky/Kokoro-FastAPI.git#master dockerfile: docker/cpu/Dockerfile restart: unless-stopped - ports: + expose: - 8880 environment: - MODEL_PATH=/app/models diff --git a/blueprints/kutt/docker-compose.yml b/blueprints/kutt/docker-compose.yml index 947f4d262..226ce8b42 100644 --- a/blueprints/kutt/docker-compose.yml +++ b/blueprints/kutt/docker-compose.yml @@ -21,4 +21,4 @@ services: CONTACT_EMAIL: ${CONTACT_EMAIL} volumes: - kutt_db_data: {} \ No newline at end of file + kutt_db_data: {} diff --git a/blueprints/langflow/docker-compose.yml b/blueprints/langflow/docker-compose.yml index 70495a7a0..078394721 100644 --- a/blueprints/langflow/docker-compose.yml +++ b/blueprints/langflow/docker-compose.yml @@ -4,7 +4,7 @@ services: user: root restart: always pull_policy: always - ports: + expose: - 7860 depends_on: - postgres-langflow diff --git a/blueprints/lavalink/docker-compose.yml b/blueprints/lavalink/docker-compose.yml index 1f4e91cf7..196d1237c 100644 --- a/blueprints/lavalink/docker-compose.yml +++ b/blueprints/lavalink/docker-compose.yml @@ -22,7 +22,7 @@ services: volumes: - "../files/application.yml:/opt/Lavalink/application.yml:rw" - "../files/plugins/:/opt/Lavalink/plugins/:rw" - ports: + expose: - ${SERVER_PORT} healthcheck: diff --git a/blueprints/leantime/docker-compose.yml b/blueprints/leantime/docker-compose.yml index 813748b53..1d9fe9a06 100644 --- a/blueprints/leantime/docker-compose.yml +++ b/blueprints/leantime/docker-compose.yml @@ -3,7 +3,7 @@ services: leantime: image: leantime/leantime:latest restart: unless-stopped - ports: + expose: - 8080 environment: - LEAN_APP_URL=${LEAN_APP_URL} diff --git a/blueprints/librechat/docker-compose.yml b/blueprints/librechat/docker-compose.yml index a5a4c5a9e..f27c97ae1 100644 --- a/blueprints/librechat/docker-compose.yml +++ b/blueprints/librechat/docker-compose.yml @@ -107,4 +107,4 @@ volumes: mongo_data: meili_data: postgres_data: - librechat_data: \ No newline at end of file + librechat_data: diff --git a/blueprints/libredesk/docker-compose.yml b/blueprints/libredesk/docker-compose.yml index 212b528f6..4b1533dd2 100644 --- a/blueprints/libredesk/docker-compose.yml +++ b/blueprints/libredesk/docker-compose.yml @@ -2,7 +2,7 @@ services: libredesk: image: libredesk/libredesk:latest restart: unless-stopped - ports: + expose: - 9000 environment: # If the password is set during first docker-compose up, the system user password will be set to this value. @@ -20,8 +20,7 @@ services: db: image: postgres:17-alpine restart: unless-stopped - ports: - # Only bind on the local interface. To connect to Postgres externally, change this to 0.0.0.0 + expose: - 5432 environment: # Set these environment variables to configure the database, defaults to libredesk. @@ -40,12 +39,11 @@ services: redis: image: redis:7-alpine restart: unless-stopped - ports: - # Only bind on the local interface. + expose: - 6379 volumes: - redis-data:/data volumes: postgres-data: - redis-data: \ No newline at end of file + redis-data: diff --git a/blueprints/libretranslate/docker-compose.yml b/blueprints/libretranslate/docker-compose.yml index f746f550b..bbaf4b14f 100644 --- a/blueprints/libretranslate/docker-compose.yml +++ b/blueprints/libretranslate/docker-compose.yml @@ -4,7 +4,7 @@ services: libretranslate: image: libretranslate/libretranslate:latest restart: unless-stopped - ports: + expose: - "5000" environment: # Enables the API key system diff --git a/blueprints/linkding/docker-compose.yml b/blueprints/linkding/docker-compose.yml index 0d283c368..76f4fe0ae 100644 --- a/blueprints/linkding/docker-compose.yml +++ b/blueprints/linkding/docker-compose.yml @@ -3,7 +3,7 @@ services: linkding: image: sissbruecker/linkding:latest restart: unless-stopped - ports: + expose: - 9090 volumes: - ../files/linkding-data:/etc/linkding/data diff --git a/blueprints/linkwarden/docker-compose.yml b/blueprints/linkwarden/docker-compose.yml index 05ffb8a0a..ba2fd88e3 100644 --- a/blueprints/linkwarden/docker-compose.yml +++ b/blueprints/linkwarden/docker-compose.yml @@ -6,7 +6,7 @@ services: - DATABASE_URL=postgresql://linkwarden:${POSTGRES_PASSWORD}@postgres:5432/linkwarden restart: unless-stopped image: ghcr.io/linkwarden/linkwarden:v2.9.3 - ports: + expose: - 3000 volumes: - linkwarden-data:/data/data diff --git a/blueprints/listmonk/docker-compose.yml b/blueprints/listmonk/docker-compose.yml index a77ebeb96..028223c45 100644 --- a/blueprints/listmonk/docker-compose.yml +++ b/blueprints/listmonk/docker-compose.yml @@ -1,7 +1,7 @@ services: db: image: postgres:17-alpine - ports: + expose: - 5432 environment: - POSTGRES_USER=listmonk diff --git a/blueprints/lobe-chat/docker-compose.yml b/blueprints/lobe-chat/docker-compose.yml index 676140903..6b7ae4965 100644 --- a/blueprints/lobe-chat/docker-compose.yml +++ b/blueprints/lobe-chat/docker-compose.yml @@ -4,9 +4,9 @@ services: lobe-chat: image: lobehub/lobe-chat:v1.26.1 restart: always - ports: + expose: - 3210 environment: OPENAI_API_KEY: sk-xxxx OPENAI_PROXY_URL: https://api-proxy.com/v1 - ACCESS_CODE: lobe66 \ No newline at end of file + ACCESS_CODE: lobe66 diff --git a/blueprints/lodestone/docker-compose.yml b/blueprints/lodestone/docker-compose.yml index 41b444f25..42f5d8dc9 100644 --- a/blueprints/lodestone/docker-compose.yml +++ b/blueprints/lodestone/docker-compose.yml @@ -16,4 +16,4 @@ services: volumes: lodestone: - driver: local \ No newline at end of file + driver: local diff --git a/blueprints/macos/docker-compose.yml b/blueprints/macos/docker-compose.yml index 585c1bf97..43cc88a80 100644 --- a/blueprints/macos/docker-compose.yml +++ b/blueprints/macos/docker-compose.yml @@ -13,4 +13,4 @@ services: stop_grace_period: 2m volumes: - macos-storage: \ No newline at end of file + macos-storage: diff --git a/blueprints/mailpit/docker-compose.yml b/blueprints/mailpit/docker-compose.yml index d0dbdb8ec..d83b62d81 100644 --- a/blueprints/mailpit/docker-compose.yml +++ b/blueprints/mailpit/docker-compose.yml @@ -22,4 +22,4 @@ services: retries: 10 volumes: - mailpit-data: \ No newline at end of file + mailpit-data: diff --git a/blueprints/marketing-dashboard/docker-compose.yml b/blueprints/marketing-dashboard/docker-compose.yml index 637849e7f..efb6fcbcd 100644 --- a/blueprints/marketing-dashboard/docker-compose.yml +++ b/blueprints/marketing-dashboard/docker-compose.yml @@ -19,7 +19,7 @@ services: MARKETING_ENABLE_PILOT_API: ${MARKETING_ENABLE_PILOT_API} MARKETING_DASHBOARD_ADMIN_USER: ${MARKETING_DASHBOARD_ADMIN_USER} MARKETING_DASHBOARD_ADMIN_PASSWORD: ${MARKETING_DASHBOARD_ADMIN_PASSWORD} - ports: + expose: - 3000 healthcheck: test: ["CMD-SHELL", "curl -fsS http://localhost:3000/api/health > /dev/null || exit 1"] diff --git a/blueprints/mautic/docker-compose.yml b/blueprints/mautic/docker-compose.yml index 223ea7ee9..ce2cff8ff 100644 --- a/blueprints/mautic/docker-compose.yml +++ b/blueprints/mautic/docker-compose.yml @@ -30,7 +30,7 @@ services: depends_on: mysql: condition: service_healthy - ports: + expose: - 80 environment: - DOCKER_MAUTIC_ROLE=mautic_web @@ -123,9 +123,8 @@ services: PMA_HOST: mysql PMA_PORT: 3306 UPLOAD_LIMIT: 64M - ports: + expose: - 80 - volumes: mysql_data: mautic_data: diff --git a/blueprints/mcsmanager/docker-compose.yml b/blueprints/mcsmanager/docker-compose.yml index 7994a3827..939cac725 100644 --- a/blueprints/mcsmanager/docker-compose.yml +++ b/blueprints/mcsmanager/docker-compose.yml @@ -2,7 +2,7 @@ services: web: image: githubyumao/mcsmanager-web:latest restart: unless-stopped - ports: + expose: - 23333 volumes: - /etc/localtime:/etc/localtime:ro @@ -11,7 +11,7 @@ services: daemon: image: githubyumao/mcsmanager-daemon:latest restart: unless-stopped - ports: + expose: - 24444 environment: - MCSM_DOCKER_WORKSPACE_PATH=/opt/mcsmanager/daemon/data/InstanceData diff --git a/blueprints/mediacms/docker-compose.yml b/blueprints/mediacms/docker-compose.yml index cf58ba436..172917e1b 100644 --- a/blueprints/mediacms/docker-compose.yml +++ b/blueprints/mediacms/docker-compose.yml @@ -36,7 +36,7 @@ services: image: mediacms/mediacms:latest deploy: replicas: 1 - ports: + expose: - 80 volumes: - mediacms_data:/home/mediacms.io/mediacms/media diff --git a/blueprints/mediafetch/docker-compose.yml b/blueprints/mediafetch/docker-compose.yml index 17ba61761..03dc98e45 100644 --- a/blueprints/mediafetch/docker-compose.yml +++ b/blueprints/mediafetch/docker-compose.yml @@ -18,4 +18,4 @@ services: - VERSION_CHECK_TTL_MS=${VERSION_CHECK_TTL_MS} volumes: - mediafetch_data: \ No newline at end of file + mediafetch_data: diff --git a/blueprints/memos/docker-compose.yml b/blueprints/memos/docker-compose.yml index e66007629..9ead8402c 100644 --- a/blueprints/memos/docker-compose.yml +++ b/blueprints/memos/docker-compose.yml @@ -9,7 +9,7 @@ services: environment: - MEMOS_MODE=${MEMOS_MODE} - MEMOS_PORT=${MEMOS_PORT} - ports: + expose: - "5230" volumes: memos_data: diff --git a/blueprints/metube/docker-compose.yml b/blueprints/metube/docker-compose.yml index 538351b3c..7db6738c1 100644 --- a/blueprints/metube/docker-compose.yml +++ b/blueprints/metube/docker-compose.yml @@ -3,7 +3,7 @@ services: metube: image: ghcr.io/alexta69/metube restart: unless-stopped - ports: + expose: - 8081 volumes: - ../files/downloads:/downloads diff --git a/blueprints/mixpost/docker-compose.yml b/blueprints/mixpost/docker-compose.yml index bf98c7283..cde08e1b8 100644 --- a/blueprints/mixpost/docker-compose.yml +++ b/blueprints/mixpost/docker-compose.yml @@ -45,4 +45,4 @@ volumes: mysql: {} redis: {} storage: {} - logs: {} \ No newline at end of file + logs: {} diff --git a/blueprints/moltbot/docker-compose.yml b/blueprints/moltbot/docker-compose.yml index e0014a7a9..ac4c3c301 100644 --- a/blueprints/moltbot/docker-compose.yml +++ b/blueprints/moltbot/docker-compose.yml @@ -13,7 +13,7 @@ services: volumes: - moltbot-config:/home/node/.clawdbot - moltbot-workspace:/home/node/clawd - ports: + expose: - "18789" - "18790" init: true @@ -32,4 +32,4 @@ services: volumes: moltbot-config: - moltbot-workspace: \ No newline at end of file + moltbot-workspace: diff --git a/blueprints/movary/docker-compose.yml b/blueprints/movary/docker-compose.yml index f84ff9e5d..692d1f0f6 100644 --- a/blueprints/movary/docker-compose.yml +++ b/blueprints/movary/docker-compose.yml @@ -2,7 +2,7 @@ services: movary: image: leepeuker/movary:${MOVARY_VERSION:-latest} restart: unless-stopped - ports: + expose: - "8080" environment: # TMDB API configuration diff --git a/blueprints/mulesoft-esb/docker-compose.yml b/blueprints/mulesoft-esb/docker-compose.yml index 6691e4554..c9af933dc 100644 --- a/blueprints/mulesoft-esb/docker-compose.yml +++ b/blueprints/mulesoft-esb/docker-compose.yml @@ -4,7 +4,7 @@ services: mule: image: yogeshmulecraft/mulece-esb:latest restart: unless-stopped - ports: + expose: - "8081" environment: - MULE_VERSION=${MULE_VERSION:-4.9.0} diff --git a/blueprints/mumble/docker-compose.yml b/blueprints/mumble/docker-compose.yml index 92580f1aa..8f7a591e7 100644 --- a/blueprints/mumble/docker-compose.yml +++ b/blueprints/mumble/docker-compose.yml @@ -2,7 +2,7 @@ services: mumble-server: image: mumblevoip/mumble-server:latest restart: unless-stopped - ports: + expose: - 64738 - 64738/udp volumes: diff --git a/blueprints/navidrome/docker-compose.yml b/blueprints/navidrome/docker-compose.yml index 923b3d576..75d744407 100644 --- a/blueprints/navidrome/docker-compose.yml +++ b/blueprints/navidrome/docker-compose.yml @@ -9,9 +9,8 @@ services: volumes: - navidrome-data:/data - navidrome-music:/music:ro - ports: + expose: - 4533 - volumes: navidrome-data: {} - navidrome-music: {} \ No newline at end of file + navidrome-music: {} diff --git a/blueprints/netdata/docker-compose.yml b/blueprints/netdata/docker-compose.yml index 36f6a736b..a9cd5bb0c 100644 --- a/blueprints/netdata/docker-compose.yml +++ b/blueprints/netdata/docker-compose.yml @@ -22,9 +22,8 @@ services: - NETDATA_CLAIM_TOKEN=${NETDATA_CLAIM_TOKEN:-} - NETDATA_CLAIM_URL=${NETDATA_CLAIM_URL:-} - NETDATA_CLAIM_ROOMS=${NETDATA_CLAIM_ROOMS:-} - ports: + expose: - "19999" - volumes: netdata-config: netdata-lib: diff --git a/blueprints/nginx/docker-compose.yml b/blueprints/nginx/docker-compose.yml index 85e6c4c66..ca4c56c5f 100644 --- a/blueprints/nginx/docker-compose.yml +++ b/blueprints/nginx/docker-compose.yml @@ -3,7 +3,7 @@ services: nginx: image: nginx:latest restart: unless-stopped - ports: + expose: - 80 - 443 volumes: @@ -11,4 +11,4 @@ services: - nginx-html:/usr/share/nginx/html volumes: nginx-config: {} - nginx-html: {} \ No newline at end of file + nginx-html: {} diff --git a/blueprints/ntfy/docker-compose.yml b/blueprints/ntfy/docker-compose.yml index ac68ff31b..c43df5d5c 100644 --- a/blueprints/ntfy/docker-compose.yml +++ b/blueprints/ntfy/docker-compose.yml @@ -4,7 +4,7 @@ services: restart: unless-stopped command: - serve - ports: + expose: - "${HTTP_PORT}" volumes: - ntfy-data:/var/lib/ntfy diff --git a/blueprints/obsidian-livesync/docker-compose.yml b/blueprints/obsidian-livesync/docker-compose.yml index fbe57ae56..88a57d2d4 100644 --- a/blueprints/obsidian-livesync/docker-compose.yml +++ b/blueprints/obsidian-livesync/docker-compose.yml @@ -9,8 +9,7 @@ services: volumes: - couchdb-data:/opt/couchdb/data - ../files/local.ini:/opt/couchdb/etc/local.ini - ports: + expose: - 5984 - volumes: couchdb-data: {} diff --git a/blueprints/ojs/docker-compose.yml b/blueprints/ojs/docker-compose.yml index 72aba02e1..19b922869 100644 --- a/blueprints/ojs/docker-compose.yml +++ b/blueprints/ojs/docker-compose.yml @@ -15,7 +15,7 @@ services: ojs: image: "pkpofficial/ojs:3_3_0-21" hostname: "${COMPOSE_PROJECT_NAME}" - ports: + expose: - 80 - 443 volumes: diff --git a/blueprints/ollama-chat-tone/docker-compose.yml b/blueprints/ollama-chat-tone/docker-compose.yml index 8605f824c..28da0149c 100644 --- a/blueprints/ollama-chat-tone/docker-compose.yml +++ b/blueprints/ollama-chat-tone/docker-compose.yml @@ -1,7 +1,7 @@ services: ollama-chat-tone: image: billnice250/chattone:latest - ports: + expose: - 8080 environment: APP_NAME: "Ollama Chat Tone" diff --git a/blueprints/omni-tools/docker-compose.yml b/blueprints/omni-tools/docker-compose.yml index 166cc6364..722ec84ab 100644 --- a/blueprints/omni-tools/docker-compose.yml +++ b/blueprints/omni-tools/docker-compose.yml @@ -2,5 +2,5 @@ services: omni-tools: image: iib0011/omni-tools:latest restart: unless-stopped - ports: + expose: - 80 diff --git a/blueprints/onedev/docker-compose.yml b/blueprints/onedev/docker-compose.yml index af4122cf8..8ef6a2aff 100644 --- a/blueprints/onedev/docker-compose.yml +++ b/blueprints/onedev/docker-compose.yml @@ -9,4 +9,4 @@ services: - "onedev-data:/opt/onedev" volumes: - onedev-data: \ No newline at end of file + onedev-data: diff --git a/blueprints/ontime/docker-compose.yml b/blueprints/ontime/docker-compose.yml index 2c04bcb3f..4ab1d1831 100644 --- a/blueprints/ontime/docker-compose.yml +++ b/blueprints/ontime/docker-compose.yml @@ -1,7 +1,7 @@ services: ontime: image: getontime/ontime:v3.8.0 - ports: + expose: - 4001 - 8888 - 9999 diff --git a/blueprints/open-webui/docker-compose.yml b/blueprints/open-webui/docker-compose.yml index bde3a1a0a..a18caa3c0 100644 --- a/blueprints/open-webui/docker-compose.yml +++ b/blueprints/open-webui/docker-compose.yml @@ -3,7 +3,7 @@ services: open-webui: image: ghcr.io/open-webui/open-webui:main restart: unless-stopped - ports: + expose: - 8080 environment: # This should point to your Ollama instance. diff --git a/blueprints/open_notebook/docker-compose.yml b/blueprints/open_notebook/docker-compose.yml index 3cd09fdb9..b0a1b3453 100644 --- a/blueprints/open_notebook/docker-compose.yml +++ b/blueprints/open_notebook/docker-compose.yml @@ -2,7 +2,7 @@ version: "3.8" services: surrealdb: image: surrealdb/surrealdb:v2 - ports: + expose: - 8000 volumes: - ../files/surreal_data:/mydata @@ -12,7 +12,7 @@ services: open_notebook: image: lfnovo/open_notebook:latest - ports: + expose: - 8502 environment: - SURREAL_URL=ws://surrealdb:8000/rpc diff --git a/blueprints/opengist/docker-compose.yml b/blueprints/opengist/docker-compose.yml index a321052ab..8ccc17954 100644 --- a/blueprints/opengist/docker-compose.yml +++ b/blueprints/opengist/docker-compose.yml @@ -4,7 +4,7 @@ services: opengist: image: ghcr.io/thomiceli/opengist:1 restart: unless-stopped - ports: + expose: - 6157 # HTTP port - 2222 # SSH port (optional) volumes: diff --git a/blueprints/openhabittracker/docker-compose.yml b/blueprints/openhabittracker/docker-compose.yml index 31cebec51..e99470bf5 100644 --- a/blueprints/openhabittracker/docker-compose.yml +++ b/blueprints/openhabittracker/docker-compose.yml @@ -3,7 +3,7 @@ version: "3" services: openhabittracker: image: jinjinov/openhabittracker:latest - ports: + expose: - '8080' environment: AppSettings__UserName: ${OHT_USERNAME} diff --git a/blueprints/openhands/docker-compose.yml b/blueprints/openhands/docker-compose.yml index 03d5048a4..be5e7d40b 100644 --- a/blueprints/openhands/docker-compose.yml +++ b/blueprints/openhands/docker-compose.yml @@ -7,10 +7,8 @@ services: restart: unless-stopped # The port is exposed without mapping. Dokploy handles the routing via the domain. - ports: + expose: - "3000" - - # Environment variables are sourced from the template.toml file. environment: - SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE} - WORKSPACE_MOUNT_PATH=/opt/workspace_base diff --git a/blueprints/openinary/docker-compose.yml b/blueprints/openinary/docker-compose.yml index 0469c72b9..2452f7539 100644 --- a/blueprints/openinary/docker-compose.yml +++ b/blueprints/openinary/docker-compose.yml @@ -23,4 +23,4 @@ services: volumes: cache-data: public-files: - db-data: \ No newline at end of file + db-data: diff --git a/blueprints/openspeedtest/docker-compose.yml b/blueprints/openspeedtest/docker-compose.yml index 0f35540bc..e85da62dd 100644 --- a/blueprints/openspeedtest/docker-compose.yml +++ b/blueprints/openspeedtest/docker-compose.yml @@ -2,5 +2,5 @@ services: openspeedtest: image: openspeedtest/latest:latest restart: unless-stopped - ports: - - 3000 \ No newline at end of file + expose: + - 3000 diff --git a/blueprints/outline/docker-compose.yml b/blueprints/outline/docker-compose.yml index 9f7260404..387836f09 100644 --- a/blueprints/outline/docker-compose.yml +++ b/blueprints/outline/docker-compose.yml @@ -6,7 +6,7 @@ services: - postgres - redis - dex - ports: + expose: - 3000 environment: NODE_ENV: production @@ -38,9 +38,8 @@ services: - dex-server volumes: - ../files/etc/nginx/conf.d/default.conf:/etc/nginx/conf.d/default.conf - ports: + expose: - 5556 - dex-server: image: ghcr.io/dexidp/dex:v2.37.0 restart: always diff --git a/blueprints/owncast/docker-compose.yml b/blueprints/owncast/docker-compose.yml index 08119dafb..d6867883a 100644 --- a/blueprints/owncast/docker-compose.yml +++ b/blueprints/owncast/docker-compose.yml @@ -7,9 +7,8 @@ services: restart: unless-stopped volumes: - owncast-data:/app/data - ports: + expose: - 8080 - 1935 - volumes: - owncast-data: {} \ No newline at end of file + owncast-data: {} diff --git a/blueprints/palmr/docker-compose.yml b/blueprints/palmr/docker-compose.yml index 5000310e9..a92948bbf 100644 --- a/blueprints/palmr/docker-compose.yml +++ b/blueprints/palmr/docker-compose.yml @@ -4,7 +4,7 @@ services: environment: - ENABLE_S3=false - ENCRYPTION_KEY=${ENCRYPTION_KEY} - ports: + expose: - "5487" - "3333" volumes: diff --git a/blueprints/paperclip/docker-compose.yml b/blueprints/paperclip/docker-compose.yml index 5c1ce7bfe..7be91e977 100644 --- a/blueprints/paperclip/docker-compose.yml +++ b/blueprints/paperclip/docker-compose.yml @@ -41,4 +41,4 @@ services: volumes: postgres-data: {} - paperclip-data: {} \ No newline at end of file + paperclip-data: {} diff --git a/blueprints/parseable/docker-compose.yml b/blueprints/parseable/docker-compose.yml index 344ac6248..928cde2d8 100644 --- a/blueprints/parseable/docker-compose.yml +++ b/blueprints/parseable/docker-compose.yml @@ -13,7 +13,7 @@ services: - P_PASSWORD=${PARSEABLE_PASSWORD} - P_STAGING_DIR=/parseable/staging - P_FS_DIR=/parseable/data - ports: + expose: - 8000 - 8001 - 8002 diff --git a/blueprints/paymenter/docker-compose.yml b/blueprints/paymenter/docker-compose.yml index 35a2ea5e1..21b63b98d 100644 --- a/blueprints/paymenter/docker-compose.yml +++ b/blueprints/paymenter/docker-compose.yml @@ -20,7 +20,7 @@ services: paymenter: image: ghcr.io/paymenter/paymenter:latest restart: unless-stopped - ports: + expose: - 80 depends_on: - database @@ -50,4 +50,4 @@ volumes: paymenter-storage: {} paymenter-logs: {} paymenter-public: {} - paymenter-redis: {} \ No newline at end of file + paymenter-redis: {} diff --git a/blueprints/penpot/docker-compose.yml b/blueprints/penpot/docker-compose.yml index ba238bdfc..e7534fc54 100644 --- a/blueprints/penpot/docker-compose.yml +++ b/blueprints/penpot/docker-compose.yml @@ -35,10 +35,9 @@ services: penpot-frontend: image: "penpotapp/frontend:2.6.1" restart: always - ports: + expose: - 8080 - 9001 - volumes: - penpot_assets:/opt/data/assets @@ -181,27 +180,5 @@ services: restart: always expose: - '1025' - ports: + expose: - 1080 - - - ## Example configuration of MiniIO (S3 compatible object storage service); If you don't - ## have preference, then just use filesystem, this is here just for the completeness. - - # minio: - # image: "minio/minio:latest" - # command: minio server /mnt/data --console-address ":9001" - # restart: always - # - # volumes: - # - "penpot_minio:/mnt/data" - # - # environment: - # - MINIO_ROOT_USER=minioadmin - # - MINIO_ROOT_PASSWORD=minioadmin - # - # ports: - # - 9000:9000 - # - 9001:9001 - - diff --git a/blueprints/peppermint/docker-compose.yml b/blueprints/peppermint/docker-compose.yml index a5af058f9..72b868287 100644 --- a/blueprints/peppermint/docker-compose.yml +++ b/blueprints/peppermint/docker-compose.yml @@ -31,4 +31,4 @@ services: SECRET: ${SECRET} volumes: - peppermint-postgres-data: \ No newline at end of file + peppermint-postgres-data: diff --git a/blueprints/photoprism/docker-compose.yml b/blueprints/photoprism/docker-compose.yml index 56793dbd3..08daa9b9d 100644 --- a/blueprints/photoprism/docker-compose.yml +++ b/blueprints/photoprism/docker-compose.yml @@ -1,76 +1,76 @@ -services: - photoprism: - image: photoprism/photoprism:latest - stop_grace_period: 10s - depends_on: - - mariadb - security_opt: - - seccomp:unconfined - - apparmor:unconfined - - environment: - PHOTOPRISM_ADMIN_USER: "admin" - PHOTOPRISM_ADMIN_PASSWORD: ${ADMIN_PASSWORD} - PHOTOPRISM_AUTH_MODE: "password" - PHOTOPRISM_SITE_URL: "http://localhost:2342/" - PHOTOPRISM_DISABLE_TLS: "false" - PHOTOPRISM_DEFAULT_TLS: "false" - PHOTOPRISM_ORIGINALS_LIMIT: 5000 # file size limit for originals in MB (increase for high-res video) - PHOTOPRISM_HTTP_COMPRESSION: "gzip" - PHOTOPRISM_LOG_LEVEL: "info" # log level: trace, debug, info, warning, error, fatal, or panic - PHOTOPRISM_READONLY: "false" - PHOTOPRISM_EXPERIMENTAL: "false" - PHOTOPRISM_DISABLE_CHOWN: "false" - PHOTOPRISM_DISABLE_WEBDAV: "false" - PHOTOPRISM_DISABLE_SETTINGS: "false" - PHOTOPRISM_DISABLE_TENSORFLOW: "false" - PHOTOPRISM_DISABLE_FACES: "false" - PHOTOPRISM_DISABLE_CLASSIFICATION: "false" - PHOTOPRISM_DISABLE_VECTORS: "false" - PHOTOPRISM_DISABLE_RAW: "false" - PHOTOPRISM_RAW_PRESETS: "false" - PHOTOPRISM_SIDECAR_YAML: "true" - PHOTOPRISM_BACKUP_ALBUMS: "true" - PHOTOPRISM_BACKUP_DATABASE: "true" - PHOTOPRISM_BACKUP_SCHEDULE: "daily" - PHOTOPRISM_INDEX_SCHEDULE: "" - PHOTOPRISM_AUTO_INDEX: 300 - PHOTOPRISM_AUTO_IMPORT: -1 - PHOTOPRISM_DETECT_NSFW: "false" - PHOTOPRISM_UPLOAD_NSFW: "true" - PHOTOPRISM_DATABASE_DRIVER: "mysql" - PHOTOPRISM_DATABASE_SERVER: "mariadb:3306" - PHOTOPRISM_DATABASE_NAME: "photoprism" - PHOTOPRISM_DATABASE_USER: "photoprism" - PHOTOPRISM_DATABASE_PASSWORD: "insecure" - PHOTOPRISM_SITE_CAPTION: "AI-Powered Photos App" - PHOTOPRISM_SITE_DESCRIPTION: "" - PHOTOPRISM_SITE_AUTHOR: "" - working_dir: - "/photoprism" - volumes: - - pictures:/photoprism/originals - - storage-data:/photoprism/storage - - mariadb: - image: mariadb:11 - restart: unless-stopped - stop_grace_period: 5s - - security_opt: - - seccomp:unconfined - - apparmor:unconfined - volumes: - - db-data:/var/lib/mysql - environment: - MARIADB_AUTO_UPGRADE: "1" - MARIADB_INITDB_SKIP_TZINFO: "1" - MARIADB_DATABASE: "photoprism" - MARIADB_USER: "photoprism" - MARIADB_PASSWORD: "insecure" - MARIADB_ROOT_PASSWORD: "insecure" - -volumes: - db-data: - storage-data: - pictures: \ No newline at end of file +services: + photoprism: + image: photoprism/photoprism:latest + stop_grace_period: 10s + depends_on: + - mariadb + security_opt: + - seccomp:unconfined + - apparmor:unconfined + + environment: + PHOTOPRISM_ADMIN_USER: "admin" + PHOTOPRISM_ADMIN_PASSWORD: ${ADMIN_PASSWORD} + PHOTOPRISM_AUTH_MODE: "password" + PHOTOPRISM_SITE_URL: "http://localhost:2342/" + PHOTOPRISM_DISABLE_TLS: "false" + PHOTOPRISM_DEFAULT_TLS: "false" + PHOTOPRISM_ORIGINALS_LIMIT: 5000 # file size limit for originals in MB (increase for high-res video) + PHOTOPRISM_HTTP_COMPRESSION: "gzip" + PHOTOPRISM_LOG_LEVEL: "info" # log level: trace, debug, info, warning, error, fatal, or panic + PHOTOPRISM_READONLY: "false" + PHOTOPRISM_EXPERIMENTAL: "false" + PHOTOPRISM_DISABLE_CHOWN: "false" + PHOTOPRISM_DISABLE_WEBDAV: "false" + PHOTOPRISM_DISABLE_SETTINGS: "false" + PHOTOPRISM_DISABLE_TENSORFLOW: "false" + PHOTOPRISM_DISABLE_FACES: "false" + PHOTOPRISM_DISABLE_CLASSIFICATION: "false" + PHOTOPRISM_DISABLE_VECTORS: "false" + PHOTOPRISM_DISABLE_RAW: "false" + PHOTOPRISM_RAW_PRESETS: "false" + PHOTOPRISM_SIDECAR_YAML: "true" + PHOTOPRISM_BACKUP_ALBUMS: "true" + PHOTOPRISM_BACKUP_DATABASE: "true" + PHOTOPRISM_BACKUP_SCHEDULE: "daily" + PHOTOPRISM_INDEX_SCHEDULE: "" + PHOTOPRISM_AUTO_INDEX: 300 + PHOTOPRISM_AUTO_IMPORT: -1 + PHOTOPRISM_DETECT_NSFW: "false" + PHOTOPRISM_UPLOAD_NSFW: "true" + PHOTOPRISM_DATABASE_DRIVER: "mysql" + PHOTOPRISM_DATABASE_SERVER: "mariadb:3306" + PHOTOPRISM_DATABASE_NAME: "photoprism" + PHOTOPRISM_DATABASE_USER: "photoprism" + PHOTOPRISM_DATABASE_PASSWORD: "insecure" + PHOTOPRISM_SITE_CAPTION: "AI-Powered Photos App" + PHOTOPRISM_SITE_DESCRIPTION: "" + PHOTOPRISM_SITE_AUTHOR: "" + working_dir: + "/photoprism" + volumes: + - pictures:/photoprism/originals + - storage-data:/photoprism/storage + + mariadb: + image: mariadb:11 + restart: unless-stopped + stop_grace_period: 5s + + security_opt: + - seccomp:unconfined + - apparmor:unconfined + volumes: + - db-data:/var/lib/mysql + environment: + MARIADB_AUTO_UPGRADE: "1" + MARIADB_INITDB_SKIP_TZINFO: "1" + MARIADB_DATABASE: "photoprism" + MARIADB_USER: "photoprism" + MARIADB_PASSWORD: "insecure" + MARIADB_ROOT_PASSWORD: "insecure" + +volumes: + db-data: + storage-data: + pictures: diff --git a/blueprints/plark/docker-compose.yml b/blueprints/plark/docker-compose.yml index 133b0bb88..5b8cbf5d3 100644 --- a/blueprints/plark/docker-compose.yml +++ b/blueprints/plark/docker-compose.yml @@ -3,7 +3,7 @@ services: image: plarkinc/plark:latest pull_policy: always restart: unless-stopped - ports: + expose: - "80" volumes: - plark-data:/var/data diff --git a/blueprints/plunk/docker-compose.yml b/blueprints/plunk/docker-compose.yml index 9991d8554..bf258d094 100644 --- a/blueprints/plunk/docker-compose.yml +++ b/blueprints/plunk/docker-compose.yml @@ -51,7 +51,7 @@ services: MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD} volumes: - minio_data:/data - ports: + expose: - 9000 - 9001 healthcheck: @@ -133,13 +133,10 @@ services: # Mount Traefik certificates for SSL - /etc/dokploy/traefik/dynamic/acme.json:/certs/acme.json:ro - ports: - # Main nginx port (handles all subdomain routing) + expose: - 80 - # SMTP ports (for email relay) - 465 - 587 - depends_on: postgres: condition: service_healthy diff --git a/blueprints/portainer/docker-compose.yml b/blueprints/portainer/docker-compose.yml index 778c3a265..facbf88db 100644 --- a/blueprints/portainer/docker-compose.yml +++ b/blueprints/portainer/docker-compose.yml @@ -5,8 +5,7 @@ services: volumes: - /var/run/docker.sock:/var/run/docker.sock - portainer-data:/data - ports: + expose: - 9000 - volumes: - portainer-data: {} \ No newline at end of file + portainer-data: {} diff --git a/blueprints/postgresus/docker-compose.yml b/blueprints/postgresus/docker-compose.yml index ebcdaabbd..0b2299b2c 100644 --- a/blueprints/postgresus/docker-compose.yml +++ b/blueprints/postgresus/docker-compose.yml @@ -3,7 +3,7 @@ services: # Postgresus was renamed upstream to Databasus (https://github.com/databasus/databasus). # This image is frozen at its final release; use the "databasus" template for new deployments. image: rostislavdugin/postgresus:v2.15.3 - ports: + expose: - "4005" volumes: # Persistent data storage diff --git a/blueprints/postiz/docker-compose.yml b/blueprints/postiz/docker-compose.yml index a00832dd2..876bbfb49 100644 --- a/blueprints/postiz/docker-compose.yml +++ b/blueprints/postiz/docker-compose.yml @@ -128,4 +128,4 @@ volumes: postiz-config: postiz-uploads: temporal-es-data: - temporal-pg-data: \ No newline at end of file + temporal-pg-data: diff --git a/blueprints/privatebin/docker-compose.yml b/blueprints/privatebin/docker-compose.yml index dc621ca0b..ba1757e1a 100644 --- a/blueprints/privatebin/docker-compose.yml +++ b/blueprints/privatebin/docker-compose.yml @@ -4,8 +4,7 @@ services: restart: unless-stopped volumes: - privatebin_data:/srv/data - ports: + expose: - "8080" - volumes: privatebin_data: diff --git a/blueprints/pulse/docker-compose.yml b/blueprints/pulse/docker-compose.yml index 074aec078..c217209ef 100644 --- a/blueprints/pulse/docker-compose.yml +++ b/blueprints/pulse/docker-compose.yml @@ -19,4 +19,4 @@ services: start_period: 40s volumes: - pulse_data: \ No newline at end of file + pulse_data: diff --git a/blueprints/quant-ux/docker-compose.yml b/blueprints/quant-ux/docker-compose.yml index e96a9d6ac..6a1346249 100644 --- a/blueprints/quant-ux/docker-compose.yml +++ b/blueprints/quant-ux/docker-compose.yml @@ -19,7 +19,7 @@ services: links: - mongo - qux-be - ports: + expose: - 8082 depends_on: - qux-be @@ -56,7 +56,7 @@ services: environment: - QUX_SERVER=http://quant-ux-backend:8080/ - QUX_SERVER_PORT=8086 - ports: + expose: - 8086 links: - qux-be diff --git a/blueprints/rabbitmq/docker-compose.yml b/blueprints/rabbitmq/docker-compose.yml index 3919a98f7..c8d54f970 100644 --- a/blueprints/rabbitmq/docker-compose.yml +++ b/blueprints/rabbitmq/docker-compose.yml @@ -9,9 +9,8 @@ services: - RABBITMQ_SERVER_ADDITIONAL_ERL_ARGS=-rabbit log_levels [{connection,error},{default,error}] disk_free_limit ${RABBITMQ_DISK_FREE_LIMIT} volumes: - rabbitmq-data:/var/lib/rabbitmq - ports: + expose: - 15672 - 5672 - volumes: - rabbitmq-data: {} \ No newline at end of file + rabbitmq-data: {} diff --git a/blueprints/rallly/docker-compose.yml b/blueprints/rallly/docker-compose.yml index ac6badae1..aeed3b1d3 100644 --- a/blueprints/rallly/docker-compose.yml +++ b/blueprints/rallly/docker-compose.yml @@ -63,4 +63,4 @@ services: volumes: rallly-db: - rallly-minio: \ No newline at end of file + rallly-minio: diff --git a/blueprints/rsshub/docker-compose.yml b/blueprints/rsshub/docker-compose.yml index cd36d6c8a..057c32722 100644 --- a/blueprints/rsshub/docker-compose.yml +++ b/blueprints/rsshub/docker-compose.yml @@ -5,7 +5,7 @@ services: # * (consumes more disk space and memory) leave everything unchanged image: diygod/rsshub restart: always - ports: + expose: - 1200 environment: NODE_ENV: production @@ -47,4 +47,4 @@ services: start_period: 5s volumes: - redis-data: \ No newline at end of file + redis-data: diff --git a/blueprints/rustrak-full/docker-compose.yml b/blueprints/rustrak-full/docker-compose.yml index 16f734c7c..fa35356fe 100644 --- a/blueprints/rustrak-full/docker-compose.yml +++ b/blueprints/rustrak-full/docker-compose.yml @@ -12,7 +12,7 @@ services: - DATABASE_URL=postgres://rustrak:${POSTGRES_PASSWORD}@postgres:5432/rustrak - SESSION_SECRET_KEY=${SESSION_SECRET_KEY} - CREATE_SUPERUSER=${CREATE_SUPERUSER} - ports: + expose: - 8080 depends_on: postgres: @@ -23,7 +23,7 @@ services: restart: unless-stopped environment: - RUSTRAK_API_URL=${RUSTRAK_API_URL} - ports: + expose: - 3000 depends_on: - rustrak-server diff --git a/blueprints/rustrak/docker-compose.yml b/blueprints/rustrak/docker-compose.yml index e4c52b0f0..1203e9c12 100644 --- a/blueprints/rustrak/docker-compose.yml +++ b/blueprints/rustrak/docker-compose.yml @@ -13,8 +13,7 @@ services: - CREATE_SUPERUSER=${CREATE_SUPERUSER} volumes: - rustrak_data:/data - ports: + expose: - 8080 - volumes: rustrak_data: diff --git a/blueprints/ryot/docker-compose.yml b/blueprints/ryot/docker-compose.yml index 09a727071..44c284599 100644 --- a/blueprints/ryot/docker-compose.yml +++ b/blueprints/ryot/docker-compose.yml @@ -34,4 +34,4 @@ services: restart: unless-stopped volumes: - ryot-postgres-data: \ No newline at end of file + ryot-postgres-data: diff --git a/blueprints/scrutiny/docker-compose.yml b/blueprints/scrutiny/docker-compose.yml index d951b15e1..9ade2efc3 100644 --- a/blueprints/scrutiny/docker-compose.yml +++ b/blueprints/scrutiny/docker-compose.yml @@ -6,7 +6,7 @@ services: image: ghcr.io/analogj/scrutiny:v0.8.3-omnibus cap_add: - SYS_RAWIO - ports: + expose: - 8080 # webapp - 8086 # influxDB admin volumes: diff --git a/blueprints/seanime/docker-compose.yml b/blueprints/seanime/docker-compose.yml index 9f6992a0a..d68e5f60a 100644 --- a/blueprints/seanime/docker-compose.yml +++ b/blueprints/seanime/docker-compose.yml @@ -23,4 +23,4 @@ services: - ./seanime-config:/home/seanime/.config/Seanime - ./anime:/anime - ./downloads:/downloads - restart: unless-stopped \ No newline at end of file + restart: unless-stopped diff --git a/blueprints/seq/docker-compose.yml b/blueprints/seq/docker-compose.yml index 2f9aa3def..4ce715229 100644 --- a/blueprints/seq/docker-compose.yml +++ b/blueprints/seq/docker-compose.yml @@ -14,4 +14,4 @@ services: - seq-data:/data volumes: - seq-data: {} \ No newline at end of file + seq-data: {} diff --git a/blueprints/sftpgo/docker-compose.yml b/blueprints/sftpgo/docker-compose.yml index 2cd48773a..429cef8d6 100644 --- a/blueprints/sftpgo/docker-compose.yml +++ b/blueprints/sftpgo/docker-compose.yml @@ -5,10 +5,9 @@ services: volumes: - sftpgo_data:/srv/sftpgo - sftpgo_home:/var/lib/sftpgo - ports: + expose: - "8080" - "2022" - volumes: sftpgo_data: sftpgo_home: diff --git a/blueprints/signoz/docker-compose.yml b/blueprints/signoz/docker-compose.yml index 67ef17710..e73908bc6 100644 --- a/blueprints/signoz/docker-compose.yml +++ b/blueprints/signoz/docker-compose.yml @@ -99,7 +99,7 @@ services: image: signoz/signoz:v0.97.1 command: - --config=/root/config/prometheus.yml - ports: + expose: - "8080" volumes: - ../files/signoz/prometheus.yml:/root/config/prometheus.yml @@ -130,7 +130,7 @@ services: - --config=/etc/otel-collector-config.yaml volumes: - ../files/collector/otel-collector-config.yaml:/etc/otel-collector-config.yaml - ports: + expose: - "4317" # OTLP gRPC receiver - "4318" # OTLP HTTP receiver depends_on: diff --git a/blueprints/silex/docker-compose.yml b/blueprints/silex/docker-compose.yml index bc821dca8..ba14def24 100644 --- a/blueprints/silex/docker-compose.yml +++ b/blueprints/silex/docker-compose.yml @@ -2,7 +2,7 @@ services: silex: image: silexlabs/silex:3.7.0 restart: unless-stopped - ports: + expose: - 6805 environment: - SILEX_URL=${SILEX_URL} diff --git a/blueprints/slash/docker-compose.yml b/blueprints/slash/docker-compose.yml index c3aba95ac..34995246b 100644 --- a/blueprints/slash/docker-compose.yml +++ b/blueprints/slash/docker-compose.yml @@ -32,4 +32,4 @@ services: volumes: slash-app-data: - slash-postgres-data: \ No newline at end of file + slash-postgres-data: diff --git a/blueprints/snapp/docker-compose.yml b/blueprints/snapp/docker-compose.yml index fec99b75f..bc7aaa0cc 100644 --- a/blueprints/snapp/docker-compose.yml +++ b/blueprints/snapp/docker-compose.yml @@ -2,7 +2,7 @@ version: "3.8" services: snapp: image: uraniadev/snapp:0.9-rc-020 - ports: + expose: - 3000 environment: - DATABASE_URL=${DATABASE_URL} diff --git a/blueprints/spacedrive/docker-compose.yml b/blueprints/spacedrive/docker-compose.yml index b98d55abf..c12ddad9e 100644 --- a/blueprints/spacedrive/docker-compose.yml +++ b/blueprints/spacedrive/docker-compose.yml @@ -1,7 +1,7 @@ services: server: image: ghcr.io/spacedriveapp/spacedrive/server:latest - ports: + expose: - 8080 environment: - SD_AUTH=${SD_USERNAME}:${SD_PASSWORD} diff --git a/blueprints/spacetimedb/docker-compose.yml b/blueprints/spacetimedb/docker-compose.yml index 05d85869f..2c207b788 100644 --- a/blueprints/spacetimedb/docker-compose.yml +++ b/blueprints/spacetimedb/docker-compose.yml @@ -4,7 +4,7 @@ services: image: clockworklabs/spacetime:v2.6.1 restart: unless-stopped command: start - ports: + expose: - 3000 volumes: - spacetimedb-data:/home/spacetime diff --git a/blueprints/speedtest-tracker/docker-compose.yml b/blueprints/speedtest-tracker/docker-compose.yml index 234eddb4c..12fb68abf 100644 --- a/blueprints/speedtest-tracker/docker-compose.yml +++ b/blueprints/speedtest-tracker/docker-compose.yml @@ -9,8 +9,7 @@ services: DB_CONNECTION: sqlite volumes: - speedtest_config:/config - ports: + expose: - "80" - volumes: speedtest_config: diff --git a/blueprints/spliit/docker-compose.yml b/blueprints/spliit/docker-compose.yml index 69b4acd9d..1d69bcc28 100644 --- a/blueprints/spliit/docker-compose.yml +++ b/blueprints/spliit/docker-compose.yml @@ -25,4 +25,4 @@ services: retries: 5 restart: unless-stopped env_file: - - .env \ No newline at end of file + - .env diff --git a/blueprints/stalwart/docker-compose.yml b/blueprints/stalwart/docker-compose.yml index 67432f183..b31ca56bb 100644 --- a/blueprints/stalwart/docker-compose.yml +++ b/blueprints/stalwart/docker-compose.yml @@ -1,7 +1,7 @@ services: stalwart-mail: image: stalwartlabs/stalwart:latest-alpine # for production choose specific version from https://hub.docker.com/r/stalwartlabs/stalwart/tags - ports: + expose: - "443" # HTTPS - "8080" # HTTP API - "25" # SMTP @@ -17,4 +17,4 @@ services: restart: unless-stopped volumes: - stalwart_data: \ No newline at end of file + stalwart_data: diff --git a/blueprints/statping-ng/docker-compose.yml b/blueprints/statping-ng/docker-compose.yml index a900a6963..e1b63e993 100644 --- a/blueprints/statping-ng/docker-compose.yml +++ b/blueprints/statping-ng/docker-compose.yml @@ -8,5 +8,5 @@ services: - DB_CONN=sqlite volumes: - ../files/statping-ng:/app - ports: + expose: - 8080 diff --git a/blueprints/stirling/docker-compose.yml b/blueprints/stirling/docker-compose.yml index 756b07482..6947fd6b5 100644 --- a/blueprints/stirling/docker-compose.yml +++ b/blueprints/stirling/docker-compose.yml @@ -1,7 +1,7 @@ services: stirling-pdf: image: docker.stirlingpdf.com/stirlingtools/stirling-pdf:latest - ports: + expose: - 8080 volumes: - stirling_pdf_trainingdata:/usr/share/tessdata # Required for extra OCR languages @@ -17,4 +17,4 @@ volumes: stirling_pdf_extraconfigs: stirling_pdf_customfiles: stirling_pdf_logs: - stirling_pdf_pipeline: \ No newline at end of file + stirling_pdf_pipeline: diff --git a/blueprints/streamflow/docker-compose.yml b/blueprints/streamflow/docker-compose.yml index cbfa8b519..a0a25a8d5 100644 --- a/blueprints/streamflow/docker-compose.yml +++ b/blueprints/streamflow/docker-compose.yml @@ -13,9 +13,8 @@ services: - streamflow-db:/app/db - streamflow-logs:/app/logs - streamflow-uploads:/app/public/uploads - ports: + expose: - 7575 - volumes: streamflow-db: {} streamflow-logs: {} diff --git a/blueprints/tailscale-exitnode/docker-compose.yml b/blueprints/tailscale-exitnode/docker-compose.yml index 102efb91d..a439928c5 100644 --- a/blueprints/tailscale-exitnode/docker-compose.yml +++ b/blueprints/tailscale-exitnode/docker-compose.yml @@ -12,4 +12,4 @@ services: restart: always volumes: - tailscale_exitnode_data: \ No newline at end of file + tailscale_exitnode_data: diff --git a/blueprints/tandoor-recipes/docker-compose.yml b/blueprints/tandoor-recipes/docker-compose.yml index d945fd92c..de582b06d 100644 --- a/blueprints/tandoor-recipes/docker-compose.yml +++ b/blueprints/tandoor-recipes/docker-compose.yml @@ -28,9 +28,8 @@ services: volumes: - tandoor_staticfiles:/opt/recipes/staticfiles - tandoor_mediafiles:/opt/recipes/mediafiles - ports: + expose: - "8080" - volumes: tandoor_db_data: tandoor_staticfiles: diff --git a/blueprints/tianji/docker-compose.yml b/blueprints/tianji/docker-compose.yml index a6d6c2e69..0c6d55f31 100644 --- a/blueprints/tianji/docker-compose.yml +++ b/blueprints/tianji/docker-compose.yml @@ -2,7 +2,7 @@ version: '3' services: tianji: image: moonrailgun/tianji - ports: + expose: - "12345" environment: DATABASE_URL: postgresql://tianji:tianji@postgres:5432/tianji diff --git a/blueprints/tolgee/docker-compose.yml b/blueprints/tolgee/docker-compose.yml index 40141f642..bdada6aa9 100644 --- a/blueprints/tolgee/docker-compose.yml +++ b/blueprints/tolgee/docker-compose.yml @@ -7,7 +7,7 @@ services: volumes: - ./data:/data - ./config.yaml:/config.yaml - ports: + expose: - '8080' environment: TOLGEE_FRONT_END_URL: ${TOLGEE_HOST} diff --git a/blueprints/tooljet/docker-compose.yml b/blueprints/tooljet/docker-compose.yml index c09289f74..7d25330f9 100644 --- a/blueprints/tooljet/docker-compose.yml +++ b/blueprints/tooljet/docker-compose.yml @@ -41,4 +41,4 @@ services: volumes: certs: logs: - fallbackcerts: \ No newline at end of file + fallbackcerts: diff --git a/blueprints/triggerdotdev/docker-compose.yml b/blueprints/triggerdotdev/docker-compose.yml index f53980277..c8cee8504 100644 --- a/blueprints/triggerdotdev/docker-compose.yml +++ b/blueprints/triggerdotdev/docker-compose.yml @@ -25,7 +25,7 @@ services: - .env environment: <<: *webapp-env - ports: + expose: - 3000 depends_on: - postgres @@ -42,7 +42,7 @@ services: - .env networks: - webapp - ports: + expose: - 5432 command: - -c @@ -55,9 +55,8 @@ services: - redis-data:/data networks: - webapp - ports: + expose: - 6379 - docker-provider: image: ghcr.io/triggerdotdev/provider/docker:${TRIGGER_IMAGE_TAG:-v3} restart: ${RESTART_POLICY:-unless-stopped} @@ -68,7 +67,7 @@ services: - webapp depends_on: - webapp - ports: + expose: - 9020 env_file: - .env @@ -86,7 +85,7 @@ services: - webapp depends_on: - webapp - ports: + expose: - 9020 env_file: - .env @@ -103,5 +102,5 @@ services: - webapp depends_on: - postgres - ports: + expose: - 3000 diff --git a/blueprints/trilium-next/docker-compose.yml b/blueprints/trilium-next/docker-compose.yml index 6ffc82a51..699d37fac 100755 --- a/blueprints/trilium-next/docker-compose.yml +++ b/blueprints/trilium-next/docker-compose.yml @@ -8,12 +8,7 @@ services: restart: unless-stopped environment: - TRILIUM_DATA_DIR=/home/node/trilium-data - ports: - # By default, Trilium will be available at http://localhost:8080 - # It will also be accessible at http://:8080 - # You might want to limit this with something like Docker Networks, reverse proxies, or firewall rules, - # however be aware that using UFW is known to not work with default Docker installations, see: - # https://docs.docker.com/engine/network/packet-filtering-firewalls/#docker-and-ufw + expose: - "8080" volumes: # Unless TRILIUM_DATA_DIR is set, the data will be stored in the "trilium-data" directory in the home directory. diff --git a/blueprints/trilium/docker-compose.yml b/blueprints/trilium/docker-compose.yml index 20f7dcd1f..e4ed1ea4e 100644 --- a/blueprints/trilium/docker-compose.yml +++ b/blueprints/trilium/docker-compose.yml @@ -1,7 +1,7 @@ services: trilium: image: zadam/trilium:latest - ports: + expose: - 8080 restart: always volumes: diff --git a/blueprints/twenty/docker-compose.yml b/blueprints/twenty/docker-compose.yml index ee69ee8f2..cc8585193 100644 --- a/blueprints/twenty/docker-compose.yml +++ b/blueprints/twenty/docker-compose.yml @@ -97,4 +97,4 @@ volumes: twenty-docker-data: twenty-postgres-data: twenty-server-local-data: - twenty-redis-data: \ No newline at end of file + twenty-redis-data: diff --git a/blueprints/typecho/docker-compose.yml b/blueprints/typecho/docker-compose.yml index b94b5743a..7a8f0c954 100644 --- a/blueprints/typecho/docker-compose.yml +++ b/blueprints/typecho/docker-compose.yml @@ -24,4 +24,4 @@ services: volumes: typecho_data: - db_data: \ No newline at end of file + db_data: diff --git a/blueprints/unifi/docker-compose.yml b/blueprints/unifi/docker-compose.yml index 0aeb31ea0..7a44dff57 100644 --- a/blueprints/unifi/docker-compose.yml +++ b/blueprints/unifi/docker-compose.yml @@ -18,14 +18,6 @@ services: - ../files/config:/config # Map host directory to container directory ports: - 8443:8443 # HTTPS portal - # - 3478:3478/udp # STUN service - # - 10001:10001/udp # UniFi AP discovery - # - 8080:8080 # HTTP portal - # - 1900:1900/udp #optional # For DLNA - # - 8843:8843 #optional # HTTPS guest portal - # - 8880:8880 #optional # HTTP guest portal - # - 6789:6789 #optional # Mobile speed test port - # - 5514:5514/udp #optional # Remote syslog port restart: unless-stopped depends_on: - unifi-db @@ -36,7 +28,7 @@ services: volumes: - ../files/db/data:/data/db - ../files/init-mongo.sh:/docker-entrypoint-initdb.d/init-mongo.sh:ro - ports: + expose: - 27017 restart: unless-stopped diff --git a/blueprints/uptimekit/docker-compose.yml b/blueprints/uptimekit/docker-compose.yml index 35d46f907..3a7b9fa6c 100644 --- a/blueprints/uptimekit/docker-compose.yml +++ b/blueprints/uptimekit/docker-compose.yml @@ -88,4 +88,4 @@ services: volumes: db_data: clickhouse_data: - redis_data: \ No newline at end of file + redis_data: diff --git a/blueprints/usesend/docker-compose.yml b/blueprints/usesend/docker-compose.yml index 9a3266b3a..3839f6c34 100644 --- a/blueprints/usesend/docker-compose.yml +++ b/blueprints/usesend/docker-compose.yml @@ -32,7 +32,7 @@ services: usesend-storage-prod: image: minio/minio:latest - ports: + expose: - 9002 - 9001 volumes: @@ -46,7 +46,7 @@ services: usesend: image: usesend/usesend:latest restart: always - ports: + expose: - ${PORT:-3000} environment: - PORT=${PORT:-3000} diff --git a/blueprints/vault/docker-compose.yml b/blueprints/vault/docker-compose.yml index c8abc02f4..75534d58d 100644 --- a/blueprints/vault/docker-compose.yml +++ b/blueprints/vault/docker-compose.yml @@ -8,7 +8,7 @@ services: environment: VAULT_DEV_ROOT_TOKEN_ID: "${VAULT_DEV_ROOT_TOKEN_ID}" VAULT_DEV_LISTEN_ADDRESS: "${VAULT_DEV_LISTEN_ADDRESS}" - ports: + expose: - "8200" volumes: - vault-data:/vault/file diff --git a/blueprints/verdaccio/docker-compose.yml b/blueprints/verdaccio/docker-compose.yml index 091f3d156..85438319d 100644 --- a/blueprints/verdaccio/docker-compose.yml +++ b/blueprints/verdaccio/docker-compose.yml @@ -5,7 +5,7 @@ services: image: verdaccio/verdaccio:6 environment: - VERDACCIO_PORT=4873 - ports: + expose: - 4873 volumes: - verdaccio_storage:/verdaccio/storage @@ -14,4 +14,4 @@ services: volumes: verdaccio_storage: - verdaccio_plugins: \ No newline at end of file + verdaccio_plugins: diff --git a/blueprints/victoriametrics/docker-compose.yml b/blueprints/victoriametrics/docker-compose.yml index 22c90e902..f56918dbd 100644 --- a/blueprints/victoriametrics/docker-compose.yml +++ b/blueprints/victoriametrics/docker-compose.yml @@ -3,7 +3,7 @@ services: victoriametrics: image: victoriametrics/victoria-metrics:v1.108.1 restart: unless-stopped - ports: + expose: - 8428 command: - "--storageDataPath=/storage" diff --git a/blueprints/vikunja/docker-compose.yml b/blueprints/vikunja/docker-compose.yml index 0d1c7fbef..a68636233 100644 --- a/blueprints/vikunja/docker-compose.yml +++ b/blueprints/vikunja/docker-compose.yml @@ -11,7 +11,7 @@ services: VIKUNJA_DATABASE_USER: ${VIKUNJA_DATABASE_USER} VIKUNJA_DATABASE_DATABASE: ${VIKUNJA_DATABASE_DATABASE} VIKUNJA_SERVICE_JWTSECRET: ${VIKUNJA_SERVICE_JWTSECRET} - ports: + expose: - 3456 volumes: - vikunja-files:/app/vikunja/files diff --git a/blueprints/wallos/docker-compose.yml b/blueprints/wallos/docker-compose.yml index 0399ef954..13e25ad41 100644 --- a/blueprints/wallos/docker-compose.yml +++ b/blueprints/wallos/docker-compose.yml @@ -2,7 +2,7 @@ version: '3.8' services: wallos: image: bellamy/wallos:latest - ports: + expose: - '80' environment: TZ: 'Europe/Paris' @@ -13,4 +13,4 @@ services: volumes: wallos_db: - wallos_logos: \ No newline at end of file + wallos_logos: diff --git a/blueprints/web-check/docker-compose.yml b/blueprints/web-check/docker-compose.yml index d8ed1e064..4c92f5ad4 100644 --- a/blueprints/web-check/docker-compose.yml +++ b/blueprints/web-check/docker-compose.yml @@ -2,6 +2,6 @@ version: '3.9' services: web-check: image: lissy93/web-check - ports: + expose: - 3000 restart: unless-stopped diff --git a/blueprints/windmill/docker-compose.yml b/blueprints/windmill/docker-compose.yml index 8b6eafdc1..1e65b16f5 100644 --- a/blueprints/windmill/docker-compose.yml +++ b/blueprints/windmill/docker-compose.yml @@ -99,4 +99,4 @@ volumes: windmill-postgres-data: windmill-worker-cache: windmill-worker-logs: - windmill-lsp-cache: \ No newline at end of file + windmill-lsp-cache: diff --git a/blueprints/windows/docker-compose.yml b/blueprints/windows/docker-compose.yml index 8a0833702..a3364b974 100644 --- a/blueprints/windows/docker-compose.yml +++ b/blueprints/windows/docker-compose.yml @@ -14,4 +14,4 @@ services: stop_grace_period: 2m volumes: - win-storage: \ No newline at end of file + win-storage: diff --git a/blueprints/yourls/docker-compose.yml b/blueprints/yourls/docker-compose.yml index f4aa16e35..f5059c205 100644 --- a/blueprints/yourls/docker-compose.yml +++ b/blueprints/yourls/docker-compose.yml @@ -38,4 +38,4 @@ services: volumes: yourls-data: - yourls-mysql-data: \ No newline at end of file + yourls-mysql-data: diff --git a/blueprints/yt-dlp-webui/docker-compose.yml b/blueprints/yt-dlp-webui/docker-compose.yml index 0bde5e08e..38873c7fe 100644 --- a/blueprints/yt-dlp-webui/docker-compose.yml +++ b/blueprints/yt-dlp-webui/docker-compose.yml @@ -2,7 +2,7 @@ version: "3.8" services: yt-dlp-webui: image: marcobaobao/yt-dlp-webui - ports: + expose: - 3033 volumes: - downloads:/downloads From a176ba3e167bbe89b0d0e3abd00786d3b6e19922 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20LAGACHE?= <43389096+Asuniia@users.noreply.github.com> Date: Mon, 3 Aug 2026 12:18:15 +0200 Subject: [PATCH 56/80] Blueprints/portabase (#1055) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(blueprints): add portabase * feat(blueprints): add portabase --------- Co-authored-by: Théo LAGACHE --- blueprints/portabase/docker-compose.yml | 55 +++++++++++++++++++++++++ blueprints/portabase/meta.json | 41 ++++++++++++++++++ blueprints/portabase/portabase.svg | 4 ++ blueprints/portabase/template.toml | 21 ++++++++++ 4 files changed, 121 insertions(+) create mode 100644 blueprints/portabase/docker-compose.yml create mode 100644 blueprints/portabase/meta.json create mode 100644 blueprints/portabase/portabase.svg create mode 100644 blueprints/portabase/template.toml diff --git a/blueprints/portabase/docker-compose.yml b/blueprints/portabase/docker-compose.yml new file mode 100644 index 000000000..59fb0a963 --- /dev/null +++ b/blueprints/portabase/docker-compose.yml @@ -0,0 +1,55 @@ +services: + portabase: + image: portabase/portabase:latest + restart: unless-stopped + environment: + - PROJECT_URL=${PROJECT_URL} + - PROJECT_SECRET=${PROJECT_SECRET} + - DATABASE_URL=${DATABASE_URL} + - PROJECT_NAME=${PROJECT_NAME:-Portabase} + - TZ=${TZ:-Europe/Paris} + - LOG_LEVEL=${LOG_LEVEL:-info} + - RETENTION_CRON=${RETENTION_CRON:-0 7 * * *} + - STALE_BACKUP_THRESHOLD_HOURS=${STALE_BACKUP_THRESHOLD_HOURS:-6} + - BACKUP_FOLDER_NAME=${BACKUP_FOLDER_NAME:-backups} + - TELEMETRY=${TELEMETRY:-true} + - SKIP_ONBOARDING=${SKIP_ONBOARDING:-false} + - API_ENABLED=${API_ENABLED:-false} + - OPENAPI_ENABLED=${OPENAPI_ENABLED:-false} + - MCP_ENABLED=${MCP_ENABLED:-false} + - SMTP_HOST=${SMTP_HOST:-} + - SMTP_PORT=${SMTP_PORT:-} + - SMTP_USER=${SMTP_USER:-} + - SMTP_PASSWORD=${SMTP_PASSWORD:-} + - SMTP_FROM=${SMTP_FROM:-} + - SMTP_SECURE=${SMTP_SECURE:-false} + expose: + - "80" + volumes: + - portabase-data:/data + depends_on: + db: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost/api/health"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 60s + db: + image: postgres:17-alpine + restart: always + volumes: + - postgres-data:/var/lib/postgresql/data + environment: + - POSTGRES_DB=${POSTGRES_DB:-portabase} + - POSTGRES_USER=${POSTGRES_USER:-portabase} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-portabase} -d ${POSTGRES_DB:-portabase}"] + interval: 10s + timeout: 5s + retries: 5 +volumes: + postgres-data: + portabase-data: diff --git a/blueprints/portabase/meta.json b/blueprints/portabase/meta.json new file mode 100644 index 000000000..360ab0b96 --- /dev/null +++ b/blueprints/portabase/meta.json @@ -0,0 +1,41 @@ +{ + "id": "portabase", + "name": "Portabase", + "version": "latest", + "description": "A free, open-source and self-hosted tool from a non-profit organization that helps you automate backups and restore your databases on demand.", + "logo": "portabase.svg", + "links": { + "github": "https://github.com/portabase/portabase", + "website": "https://portabase.io", + "docs": "https://portabase.io/docs" + }, + "tags": [ + "postgres", + "mysql", + "mariadb", + "mongodb", + "sqlite", + "firebird", + "valkey", + "redis", + "mssql", + "docker-volumes", + "backup", + "s3", + "gcs", + "azure", + "google-drive", + "discord", + "telegram", + "slack", + "email", + "apprise", + "ntfy", + "gotify", + "pushover", + "teams", + "webhook", + "self-hosted", + "open-source" + ] +} diff --git a/blueprints/portabase/portabase.svg b/blueprints/portabase/portabase.svg new file mode 100644 index 000000000..ae94a6686 --- /dev/null +++ b/blueprints/portabase/portabase.svg @@ -0,0 +1,4 @@ + + + + diff --git a/blueprints/portabase/template.toml b/blueprints/portabase/template.toml new file mode 100644 index 000000000..07ecd630a --- /dev/null +++ b/blueprints/portabase/template.toml @@ -0,0 +1,21 @@ +[variables] +main_domain = "${domain}" +postgres_password = "${password:32}" +project_secret = "${password:64}" + +[config] +env = [ + "PROJECT_URL=https://${main_domain}", + "PROJECT_SECRET=${project_secret}", + "POSTGRES_USER=portabase", + "POSTGRES_PASSWORD=${postgres_password}", + "POSTGRES_DB=portabase", + "DATABASE_URL=postgresql://portabase:${postgres_password}@db:5432/portabase?schema=public", + "TZ=Europe/Paris", + "LOG_LEVEL=info", +] + +[[config.domains]] +serviceName = "portabase" +port = 80 +host = "${main_domain}" From 292877b3703255f9409df0145f023d1fb810d31b Mon Sep 17 00:00:00 2001 From: Finley Crawford-McQuillan Date: Mon, 3 Aug 2026 11:36:10 +0100 Subject: [PATCH 57/80] feat: Add Fluxer template (#1045) * feat: Add Fluxer template * fix: populate config.env in template.toml * fix: syntax in template.toml for config.env * fix: use multiline string for config.env in template.toml * fix: format config.env as valid string array in template.toml * fix: update template.toml format to match standard env array structure * add: template.toml with optional env vars and http guidance * fix: change wording to avoid confusion * fix: add more missing instructions * fix: more wording changes * fix: final wording improvements * fix: comma missing from previous commit --- blueprints/fluxer/docker-compose.yml | 459 +++++++++++++++++++++++++++ blueprints/fluxer/logo.svg | 1 + blueprints/fluxer/meta.json | 17 + blueprints/fluxer/template.toml | 107 +++++++ 4 files changed, 584 insertions(+) create mode 100644 blueprints/fluxer/docker-compose.yml create mode 100644 blueprints/fluxer/logo.svg create mode 100644 blueprints/fluxer/meta.json create mode 100644 blueprints/fluxer/template.toml diff --git a/blueprints/fluxer/docker-compose.yml b/blueprints/fluxer/docker-compose.yml new file mode 100644 index 000000000..1ea9d0199 --- /dev/null +++ b/blueprints/fluxer/docker-compose.yml @@ -0,0 +1,459 @@ +x-fluxer-env: &fluxer-env + FLUXER_ENV: production + NODE_ENV: production + FLUXER_SELF_HOSTED: "true" + FLUXER_BASE_DOMAIN: ${FLUXER_DOMAIN} + FLUXER_PUBLIC_SCHEME: ${FLUXER_PUBLIC_SCHEME:-https} + FLUXER_PUBLIC_PORT: ${FLUXER_PUBLIC_PORT:-443} + FLUXER_TRUST_CLIENT_IP_HEADER: "true" + FLUXER_CLIENT_IP_HEADER_NAME: x-forwarded-for + + FLUXER_DATABASE_BACKEND: postgres + FLUXER_POSTGRES_HOST: postgres + FLUXER_POSTGRES_PORT: "5432" + FLUXER_POSTGRES_DATABASE: fluxer + FLUXER_POSTGRES_USERNAME: fluxer + FLUXER_POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + FLUXER_POSTGRES_SSL: "false" + + FLUXER_KV_URL: redis://valkey:6379/0 + FLUXER_NATS_URL: nats://nats:4222 + FLUXER_NATS_JETSTREAM_URL: nats://nats:4222 + FLUXER_SVC_NATS_URL: nats://nats:4222 + FLUXER_SVC_SHARD_COUNT: "1" + + FLUXER_SEARCH_ENGINE: meilisearch + FLUXER_SEARCH_URL: http://meilisearch:7700 + FLUXER_SEARCH_API_KEY: ${MEILI_MASTER_KEY} + + FLUXER_S3_ENDPOINT: http://seaweedfs:8333 + FLUXER_S3_PUBLIC_ENDPOINT: http://seaweedfs:8333 + FLUXER_S3_REGION: us-east-1 + FLUXER_S3_ACCESS_KEY_ID: ${FLUXER_S3_ACCESS_KEY} + FLUXER_S3_SECRET_ACCESS_KEY: ${FLUXER_S3_SECRET_KEY} + FLUXER_S3_FORCE_PATH_STYLE: "true" + FLUXER_S3_BUCKET_CDN: fluxer + FLUXER_S3_BUCKET_UPLOADS: fluxer-uploads + FLUXER_S3_BUCKET_DOWNLOADS: fluxer-downloads + FLUXER_S3_BUCKET_REPORTS: fluxer-reports + FLUXER_S3_BUCKET_HARVESTS: fluxer-harvests + AWS_ACCESS_KEY_ID: ${FLUXER_S3_ACCESS_KEY} + AWS_SECRET_ACCESS_KEY: ${FLUXER_S3_SECRET_KEY} + AWS_DEFAULT_REGION: us-east-1 + AWS_EC2_METADATA_DISABLED: "true" + + FLUXER_LIVEKIT_ENABLED: "true" + FLUXER_LIVEKIT_API_KEY: ${LIVEKIT_API_KEY} + FLUXER_LIVEKIT_API_SECRET: ${LIVEKIT_API_SECRET} + FLUXER_LIVEKIT_WEBHOOK_URL: http://api:8080/webhooks/livekit + + FLUXER_EMAIL_ENABLED: ${FLUXER_EMAIL_ENABLED:-false} + FLUXER_EMAIL_PROVIDER: ${FLUXER_EMAIL_PROVIDER:-none} + FLUXER_EMAIL_FROM_EMAIL: ${FLUXER_EMAIL_FROM_EMAIL:-noreply@localhost} + FLUXER_EMAIL_FROM_NAME: ${FLUXER_EMAIL_FROM_NAME:-Fluxer} + FLUXER_EMAIL_SMTP_HOST: ${FLUXER_EMAIL_SMTP_HOST:-} + FLUXER_EMAIL_SMTP_PORT: ${FLUXER_EMAIL_SMTP_PORT:-587} + FLUXER_EMAIL_SMTP_USERNAME: ${FLUXER_EMAIL_SMTP_USERNAME:-} + FLUXER_EMAIL_SMTP_PASSWORD: ${FLUXER_EMAIL_SMTP_PASSWORD:-} + FLUXER_EMAIL_SMTP_SECURE: ${FLUXER_EMAIL_SMTP_SECURE:-true} + + FLUXER_SMS_ENABLED: "false" + FLUXER_CAPTCHA_ENABLED: ${FLUXER_CAPTCHA_ENABLED:-false} + FLUXER_CAPTCHA_PROVIDER: ${FLUXER_CAPTCHA_PROVIDER:-none} + FLUXER_STRIPE_ENABLED: "false" + FLUXER_NCMEC_ENABLED: "false" + FLUXER_CLAMAV_ENABLED: "false" + FLUXER_DISCOVERY_ENABLED: ${FLUXER_DISCOVERY_ENABLED:-true} + + FLUXER_SUDO_MODE_SECRET: ${FLUXER_SUDO_MODE_SECRET} + FLUXER_CONNECTION_INITIATION_SECRET: ${FLUXER_CONNECTION_INITIATION_SECRET} + FLUXER_VAPID_PUBLIC_KEY: ${FLUXER_VAPID_PUBLIC_KEY} + FLUXER_VAPID_PRIVATE_KEY: ${FLUXER_VAPID_PRIVATE_KEY} + FLUXER_VAPID_EMAIL: ${FLUXER_VAPID_EMAIL} + FLUXER_GATEWAY_RPC_AUTH_TOKEN: ${FLUXER_GATEWAY_RPC_AUTH_TOKEN} + FLUXER_MEDIA_PROXY_SECRET_KEY: ${FLUXER_MEDIA_PROXY_SECRET_KEY} + FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64: ${FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64} + FLUXER_ADMIN_SECRET_KEY_BASE: ${FLUXER_ADMIN_SECRET_KEY_BASE} + FLUXER_ADMIN_OAUTH_CLIENT_SECRET: ${FLUXER_ADMIN_OAUTH_CLIENT_SECRET} + + FLUXER_INTERNAL_API_ENDPOINT: http://api:8080 + FLUXER_INTERNAL_GATEWAY_ENDPOINT: http://gateway:8080 + FLUXER_INTERNAL_MEDIA_PROXY_ENDPOINT: http://media-proxy:8080 + FLUXER_MARKETING_ENDPOINT: ${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN} + FLUXER_MEDIA_PROXY_ENDPOINT: http://media-proxy:8080 + FLUXER_MEDIA_ENDPOINT: ${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}/media + FLUXER_MEDIA_PROXY_UPLOAD_RELAY_ENDPOINT: ${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}/media + + FLUXER_PASSKEY_RP_NAME: ${FLUXER_PASSKEY_RP_NAME:-Fluxer} + FLUXER_PASSKEY_RP_ID: ${FLUXER_PASSKEY_RP_ID:-${FLUXER_DOMAIN}} + FLUXER_PASSKEY_ADDITIONAL_ALLOWED_ORIGINS: ${FLUXER_PASSKEY_ADDITIONAL_ALLOWED_ORIGINS:-https://${FLUXER_DOMAIN}} + +x-fluxer-service: &fluxer-service + restart: unless-stopped + +services: + caddy: + image: caddy:2.10-alpine + restart: unless-stopped + ports: + - "80" + configs: + - source: caddyfile + target: /etc/caddy/Caddyfile + volumes: + - caddy-data:/data + - caddy-config:/config + depends_on: [api, gateway, media-proxy, static-proxy, admin] + + postgres: + image: postgres:16-alpine + restart: unless-stopped + environment: + POSTGRES_DB: fluxer + POSTGRES_USER: fluxer + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + volumes: + - postgres-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U fluxer -d fluxer"] + interval: 10s + timeout: 5s + retries: 10 + + valkey: + image: valkey/valkey:8.1-alpine + restart: unless-stopped + command: ["valkey-server", "--save", "", "--appendonly", "no"] + healthcheck: + test: ["CMD", "valkey-cli", "ping"] + interval: 10s + timeout: 5s + retries: 10 + + nats: + image: nats:2.14-alpine + restart: unless-stopped + command: ["-js", "-sd", "/data", "-m", "8222"] + volumes: + - nats-data:/data + + meilisearch: + image: getmeili/meilisearch:v1.12 + restart: unless-stopped + environment: + MEILI_ENV: production + MEILI_NO_ANALYTICS: "true" + MEILI_MASTER_KEY: ${MEILI_MASTER_KEY} + volumes: + - meilisearch-data:/meili_data + + seaweedfs: + image: chrislusf/seaweedfs:4.34 + restart: unless-stopped + command: ["server", "-s3", "-dir=/data"] + volumes: + - seaweedfs-data:/data + + gifs: + <<: *fluxer-service + image: ${FLUXER_REGISTRY:-ghcr.io/fluxerapp}/fluxer-gifs:${FLUXER_IMAGE_TAG:-v1} + environment: + <<: *fluxer-env + FLUXER_SVC_NAME: gifs + FLUXER_SVC_MODE: router + FLUXER_MEDIA_PROXY_PUBLIC_ENDPOINT: ${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}/media + depends_on: + nats: {condition: service_started} + + gifs-shard: + <<: *fluxer-service + image: ${FLUXER_REGISTRY:-ghcr.io/fluxerapp}/fluxer-gifs:${FLUXER_IMAGE_TAG:-v1} + environment: + <<: *fluxer-env + FLUXER_SVC_NAME: gifs + FLUXER_SVC_MODE: shard + FLUXER_SVC_SHARD_ID: "0" + FLUXER_MEDIA_PROXY_PUBLIC_ENDPOINT: ${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}/media + depends_on: + nats: {condition: service_started} + + seaweedfs-init: + image: chrislusf/seaweedfs:4.34 + depends_on: [seaweedfs] + restart: "no" + entrypoint: + - /bin/sh + - -c + - | + echo "Waiting for SeaweedFS to initialise..." + sleep 12 + echo "Creating S3 buckets..." + printf 's3.bucket.create -name fluxer\ns3.bucket.create -name fluxer-uploads\ns3.bucket.create -name fluxer-downloads\ns3.bucket.create -name fluxer-reports\ns3.bucket.create -name fluxer-harvests\nexit\n' | weed shell -master=seaweedfs:9333 2>&1 & + WEED_PID=$$! + sleep 15 + kill $$WEED_PID 2>/devnull || true + echo "buckets ready" + + livekit: + image: livekit/livekit-server:v1.12.0 + restart: unless-stopped + command: ["--config", "/etc/livekit.yaml"] + environment: + LIVEKIT_KEYS: "${LIVEKIT_API_KEY}: ${LIVEKIT_API_SECRET}" + configs: + - source: livekit_config + target: /etc/livekit.yaml + ports: + - "7881" + - "7882/udp" + + api: + <<: *fluxer-service + image: ${FLUXER_REGISTRY:-ghcr.io/fluxerapp}/fluxer-api:${FLUXER_IMAGE_TAG:-v1} + environment: + <<: *fluxer-env + FLUXER_API_PORT: "8080" + FLUXER_API_PRESIGNED_ATTACHMENT_UPLOADS_ENABLED: "true" + healthcheck: + test: ["CMD-SHELL", "node -e \"fetch('http://127.0.0.1:8080/_health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))\""] + interval: 10s + timeout: 5s + retries: 30 + start_period: 90s + depends_on: + postgres: {condition: service_healthy} + valkey: {condition: service_healthy} + nats: {condition: service_started} + meilisearch: {condition: service_started} + seaweedfs-init: {condition: service_completed_successfully} + snowflakes: {condition: service_started} + snowflakes-shard: {condition: service_started} + messages: {condition: service_started} + messages-shard: {condition: service_started} + users: {condition: service_started} + users-shard: {condition: service_started} + + worker: + <<: *fluxer-service + image: ${FLUXER_REGISTRY:-ghcr.io/fluxerapp}/fluxer-api:${FLUXER_IMAGE_TAG:-v1} + working_dir: /usr/src/app/fluxer_api + command: ["./node_modules/.bin/tsx", "src/WorkerEntrypoint.ts"] + environment: + <<: *fluxer-env + FLUXER_API_WORKER_MODE: all_lanes + FLUXER_API_WORKER_ENABLE_CRON_SCHEDULER: "true" + FLUXER_API_WORKER_ENABLE_VOICE_RECONCILIATION: "true" + depends_on: + postgres: {condition: service_healthy} + valkey: {condition: service_healthy} + nats: {condition: service_started} + seaweedfs-init: {condition: service_completed_successfully} + snowflakes-shard: {condition: service_started} + messages-shard: {condition: service_started} + users-shard: {condition: service_started} + + gateway: + <<: *fluxer-service + image: ${FLUXER_REGISTRY:-ghcr.io/fluxerapp}/fluxer-gateway:${FLUXER_IMAGE_TAG:-v1} + environment: + <<: *fluxer-env + FLUXER_GATEWAY_PORT: "8080" + FLUXER_GATEWAY_MEDIA_PROXY_ENDPOINT: ${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}/media + FLUXER_GATEWAY_STATIC_CDN_ENDPOINT: ${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN} + FLUXER_GATEWAY_LOGGER_LEVEL: info + depends_on: + nats: {condition: service_started} + valkey: {condition: service_healthy} + + media-proxy: + <<: *fluxer-service + image: ${FLUXER_REGISTRY:-ghcr.io/fluxerapp}/fluxer-media-proxy:${FLUXER_IMAGE_TAG:-v1} + environment: + <<: *fluxer-env + FLUXER_MEDIA_PROXY_HOST: 0.0.0.0 + FLUXER_MEDIA_PROXY_PORT: "8080" + FLUXER_MEDIA_PROXY_MODE: upload + FLUXER_MEDIA_PROXY_STORAGE_BACKEND: s3 + depends_on: + seaweedfs-init: {condition: service_completed_successfully} + nats: {condition: service_started} + + static-proxy: + <<: *fluxer-service + image: ${FLUXER_REGISTRY:-ghcr.io/fluxerapp}/fluxer-static:${FLUXER_IMAGE_TAG:-v1} + + app-proxy: + <<: *fluxer-service + image: ${FLUXER_REGISTRY:-ghcr.io/fluxerapp}/fluxer-app-proxy-self-hosted:${FLUXER_IMAGE_TAG:-v1} + environment: + FLUXER_APP_PROXY_HOST: 0.0.0.0 + FLUXER_APP_PROXY_PORT: "8080" + DISCOVERY_UPSTREAM_URL: http://caddy:8088/api/.well-known/fluxer + PUBLIC_BOOTSTRAP_API_ENDPOINT: /api + PUBLIC_BOOTSTRAP_API_PUBLIC_ENDPOINT: ${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}/api + depends_on: + api: {condition: service_healthy} + caddy: {condition: service_started} + + snowflakes: + <<: *fluxer-service + image: ${FLUXER_REGISTRY:-ghcr.io/fluxerapp}/fluxer-snowflakes:${FLUXER_IMAGE_TAG:-v1} + environment: + <<: *fluxer-env + FLUXER_SVC_MODE: router + depends_on: + nats: {condition: service_started} + + snowflakes-shard: + <<: *fluxer-service + image: ${FLUXER_REGISTRY:-ghcr.io/fluxerapp}/fluxer-snowflakes:${FLUXER_IMAGE_TAG:-v1} + environment: + <<: *fluxer-env + FLUXER_SVC_MODE: shard + FLUXER_SVC_SHARD_ID: "0" + depends_on: + nats: {condition: service_started} + + users: + <<: *fluxer-service + image: ${FLUXER_REGISTRY:-ghcr.io/fluxerapp}/fluxer-users:${FLUXER_IMAGE_TAG:-v1} + environment: + <<: *fluxer-env + FLUXER_SVC_MODE: router + depends_on: + nats: {condition: service_started} + + users-shard: + <<: *fluxer-service + image: ${FLUXER_REGISTRY:-ghcr.io/fluxerapp}/fluxer-users:${FLUXER_IMAGE_TAG:-v1} + environment: + <<: *fluxer-env + FLUXER_SVC_MODE: shard + FLUXER_SVC_SHARD_ID: "0" + depends_on: + nats: {condition: service_started} + + messages: + <<: *fluxer-service + image: ${FLUXER_REGISTRY:-ghcr.io/fluxerapp}/fluxer-messages:${FLUXER_IMAGE_TAG:-v1} + environment: + <<: *fluxer-env + FLUXER_SVC_MODE: router + depends_on: + nats: {condition: service_started} + + messages-shard: + <<: *fluxer-service + image: ${FLUXER_REGISTRY:-ghcr.io/fluxerapp}/fluxer-messages:${FLUXER_IMAGE_TAG:-v1} + environment: + <<: *fluxer-env + FLUXER_SVC_MODE: shard + FLUXER_SVC_SHARD_ID: "0" + depends_on: + nats: {condition: service_started} + postgres: {condition: service_healthy} + + unfurl: + <<: *fluxer-service + image: ${FLUXER_REGISTRY:-ghcr.io/fluxerapp}/fluxer-unfurl:${FLUXER_IMAGE_TAG:-v1} + environment: + <<: *fluxer-env + FLUXER_SVC_MODE: router + depends_on: + nats: {condition: service_started} + + unfurl-shard: + <<: *fluxer-service + image: ${FLUXER_REGISTRY:-ghcr.io/fluxerapp}/fluxer-unfurl:${FLUXER_IMAGE_TAG:-v1} + environment: + <<: *fluxer-env + FLUXER_SVC_MODE: shard + FLUXER_SVC_SHARD_ID: "0" + depends_on: + nats: {condition: service_started} + + admin: + <<: *fluxer-service + image: ${FLUXER_REGISTRY:-ghcr.io/fluxerapp}/fluxer-admin:${FLUXER_IMAGE_TAG:-v1} + environment: + <<: *fluxer-env + FLUXER_ADMIN_HOST: 0.0.0.0 + FLUXER_ADMIN_PORT: "8080" + FLUXER_ADMIN_BASE_PATH: /admin + FLUXER_API_ENDPOINT: http://api:8080 + FLUXER_ADMIN_ENDPOINT: ${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}/admin + FLUXER_APP_ENDPOINT: ${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN} + FLUXER_MEDIA_ENDPOINT: ${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}/media + FLUXER_STATIC_CDN_ENDPOINT: ${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN} + FLUXER_ADMIN_OAUTH_REDIRECT_URI: ${FLUXER_PUBLIC_SCHEME:-https}://${FLUXER_DOMAIN}/admin/oauth2_callback + depends_on: + api: {condition: service_healthy} + +volumes: + caddy-data: + caddy-config: + postgres-data: + nats-data: + meilisearch-data: + seaweedfs-data: + +configs: + caddyfile: + content: | + :80 { + encode zstd gzip + handle /.well-known/* { + reverse_proxy api:8080 + } + handle_path /api/* { + reverse_proxy api:8080 + } + handle /gateway { + rewrite * / + reverse_proxy gateway:8080 + } + handle_path /gateway/* { + reverse_proxy gateway:8080 + } + handle_path /media/* { + reverse_proxy media-proxy:8080 + } + handle_path /livekit/* { + reverse_proxy livekit:7880 + } + handle /admin { + rewrite * / + reverse_proxy admin:8080 + } + handle_path /admin/* { + reverse_proxy admin:8080 + } + @staticAssets path /fonts/* /web/* /emoji/* /libs/* /avatars/* /badges/* /desktop/* /embeds/* + handle @staticAssets { + reverse_proxy static-proxy:8080 + } + handle { + reverse_proxy app-proxy:8080 + } + } + :8088 { + handle_path /api/* { + reverse_proxy api:8080 + } + } + + livekit_config: + content: | + port: 7880 + log_level: info + rtc: + tcp_port: 7881 + udp_port: 7882 + use_external_ip: true + stun_servers: + - stun.l.google.com:19302 + - stun1.l.google.com:19302 + webhook: + api_key: ${LIVEKIT_API_KEY} + urls: + - http://api:8080/webhooks/livekit \ No newline at end of file diff --git a/blueprints/fluxer/logo.svg b/blueprints/fluxer/logo.svg new file mode 100644 index 000000000..0c18745ba --- /dev/null +++ b/blueprints/fluxer/logo.svg @@ -0,0 +1 @@ +Fluxer diff --git a/blueprints/fluxer/meta.json b/blueprints/fluxer/meta.json new file mode 100644 index 000000000..498f7ccbb --- /dev/null +++ b/blueprints/fluxer/meta.json @@ -0,0 +1,17 @@ +{ + "id": "fluxer", + "name": "Fluxer", + "version": "1.0.0", + "description": "Fluxer is a self-hosted, scalable chat and community platform.", + "logo": "logo.svg", + "links": { + "github": "https://github.com/fluxerapp", + "website": "https://fluxer.app", + "docs": "https://github.com/fluxerapp" + }, + "tags": [ + "chat", + "community", + "communication" + ] +} \ No newline at end of file diff --git a/blueprints/fluxer/template.toml b/blueprints/fluxer/template.toml new file mode 100644 index 000000000..8018ef122 --- /dev/null +++ b/blueprints/fluxer/template.toml @@ -0,0 +1,107 @@ +[variables] +FLUXER_DOMAIN = "${domain}" +FLUXER_PUBLIC_SCHEME = "http" +FLUXER_PUBLIC_PORT = "80" + +POSTGRES_PASSWORD = "${hash:64}" +MEILI_MASTER_KEY = "${hash:64}" +FLUXER_S3_ACCESS_KEY = "fluxer" +FLUXER_S3_SECRET_KEY = "${hash:64}" + +FLUXER_SUDO_MODE_SECRET = "${hash:64}" +FLUXER_CONNECTION_INITIATION_SECRET = "${hash:64}" +FLUXER_GATEWAY_RPC_AUTH_TOKEN = "${hash:64}" +FLUXER_MEDIA_PROXY_SECRET_KEY = "${hash:64}" +FLUXER_ADMIN_SECRET_KEY_BASE = "${hash:64}" +FLUXER_ADMIN_OAUTH_CLIENT_SECRET = "${hash:64}" + +FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64 = "${base64:32}" + +FLUXER_VAPID_PUBLIC_KEY = "${password:64}" +FLUXER_VAPID_PRIVATE_KEY = "${password:32}" +FLUXER_VAPID_EMAIL = "admin@example.com" + +LIVEKIT_API_KEY = "fluxer" +LIVEKIT_API_SECRET = "${hash:64}" + +FLUXER_PASSKEY_RP_NAME = "Fluxer" +FLUXER_PASSKEY_RP_ID = "${FLUXER_DOMAIN}" +FLUXER_PASSKEY_ADDITIONAL_ALLOWED_ORIGINS = "http://${FLUXER_DOMAIN}" + +FLUXER_EMAIL_ENABLED = "false" +FLUXER_EMAIL_PROVIDER = "none" +FLUXER_EMAIL_FROM_EMAIL = "noreply@example.com" +FLUXER_EMAIL_FROM_NAME = "Fluxer" +FLUXER_EMAIL_SMTP_HOST = "" +FLUXER_EMAIL_SMTP_PORT = "587" +FLUXER_EMAIL_SMTP_USERNAME = "" +FLUXER_EMAIL_SMTP_PASSWORD = "" +FLUXER_EMAIL_SMTP_SECURE = "true" + +FLUXER_CAPTCHA_ENABLED = "false" +FLUXER_CAPTCHA_PROVIDER = "none" +FLUXER_DISCOVERY_ENABLED = "true" +FLUXER_KLIPY_API_KEY = "" + +[config] +env = [ + "# HTTPS SETUP INSTRUCTIONS:", + "# By default, this template starts over HTTP for initial testing/setup.", + "# To enable HTTPS (Required for most services to work!)", + "# 1. Change FLUXER_PUBLIC_SCHEME from 'http' to 'https'", + "# 2. Change FLUXER_PUBLIC_PORT from '80' to '443'", + "# 3. Enable HTTPS on your domain in the Dokploy Domains tab and change it if needed.", + "# 4. Change FLUXER_DOMAIN from ${FLUXER_DOMAIN} to the new domain if needed.", + "# 5. Change FLUXER_PASSKEY_RP_ID from ${FLUXER_DOMAIN} to the new domain if needed.", + "# 6. Update FLUXER_PASSKEY_ADDITIONAL_ALLOWED_ORIGINS from ${FLUXER_DOMAIN} to the new domain if needed.", + "", + "# The FLUXER_DOMAIN and FLUXER_PASSKEY_RP_ID should not have http/https at the start.", + "", + "FLUXER_DOMAIN=${FLUXER_DOMAIN}", + "FLUXER_PUBLIC_SCHEME=${FLUXER_PUBLIC_SCHEME}", + "FLUXER_PUBLIC_PORT=${FLUXER_PUBLIC_PORT}", + "POSTGRES_PASSWORD=${POSTGRES_PASSWORD}", + "MEILI_MASTER_KEY=${MEILI_MASTER_KEY}", + "FLUXER_S3_ACCESS_KEY=${FLUXER_S3_ACCESS_KEY}", + "FLUXER_S3_SECRET_KEY=${FLUXER_S3_SECRET_KEY}", + "FLUXER_SUDO_MODE_SECRET=${FLUXER_SUDO_MODE_SECRET}", + "FLUXER_CONNECTION_INITIATION_SECRET=${FLUXER_CONNECTION_INITIATION_SECRET}", + "FLUXER_GATEWAY_RPC_AUTH_TOKEN=${FLUXER_GATEWAY_RPC_AUTH_TOKEN}", + "FLUXER_MEDIA_PROXY_SECRET_KEY=${FLUXER_MEDIA_PROXY_SECRET_KEY}", + "FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64=${FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64}", + "FLUXER_ADMIN_SECRET_KEY_BASE=${FLUXER_ADMIN_SECRET_KEY_BASE}", + "FLUXER_ADMIN_OAUTH_CLIENT_SECRET=${FLUXER_ADMIN_OAUTH_CLIENT_SECRET}", + "", + "FLUXER_VAPID_PUBLIC_KEY=${FLUXER_VAPID_PUBLIC_KEY}", + "FLUXER_VAPID_PRIVATE_KEY=${FLUXER_VAPID_PRIVATE_KEY}", + "FLUXER_VAPID_EMAIL=${FLUXER_VAPID_EMAIL}", + "", + "LIVEKIT_API_KEY=${LIVEKIT_API_KEY}", + "LIVEKIT_API_SECRET=${LIVEKIT_API_SECRET}", + "", + "FLUXER_PASSKEY_RP_NAME=${FLUXER_PASSKEY_RP_NAME}", + "FLUXER_PASSKEY_RP_ID=${FLUXER_PASSKEY_RP_ID}", + "FLUXER_PASSKEY_ADDITIONAL_ALLOWED_ORIGINS=${FLUXER_PASSKEY_ADDITIONAL_ALLOWED_ORIGINS}", + "", + "FLUXER_EMAIL_ENABLED=${FLUXER_EMAIL_ENABLED}", + "FLUXER_EMAIL_PROVIDER=${FLUXER_EMAIL_PROVIDER}", + "FLUXER_EMAIL_FROM_EMAIL=${FLUXER_EMAIL_FROM_EMAIL}", + "FLUXER_EMAIL_FROM_NAME=${FLUXER_EMAIL_FROM_NAME}", + "FLUXER_EMAIL_SMTP_HOST=${FLUXER_EMAIL_SMTP_HOST}", + "FLUXER_EMAIL_SMTP_PORT=${FLUXER_EMAIL_SMTP_PORT}", + "FLUXER_EMAIL_SMTP_USERNAME=${FLUXER_EMAIL_SMTP_USERNAME}", + "FLUXER_EMAIL_SMTP_PASSWORD=${FLUXER_EMAIL_SMTP_PASSWORD}", + "FLUXER_EMAIL_SMTP_SECURE=${FLUXER_EMAIL_SMTP_SECURE}", + "", + "FLUXER_CAPTCHA_ENABLED=${FLUXER_CAPTCHA_ENABLED}", + "FLUXER_CAPTCHA_PROVIDER=${FLUXER_CAPTCHA_PROVIDER}", + "FLUXER_DISCOVERY_ENABLED=${FLUXER_DISCOVERY_ENABLED}", + "", + "FLUXER_KLIPY_API_KEY=${FLUXER_KLIPY_API_KEY}", +] +mounts = [] + +[[config.domains]] +serviceName = "caddy" +port = 80 +host = "${FLUXER_DOMAIN}" \ No newline at end of file From a49f00fd7b0eb26dd1b2002493e9b7927e5d6a72 Mon Sep 17 00:00:00 2001 From: Jiho Lee Date: Mon, 3 Aug 2026 19:36:31 +0900 Subject: [PATCH 58/80] ci: audit every blueprint on a schedule (#1049) * ci: audit every blueprint on a schedule (#1047) * ci: use RUNNER_TEMP and gate the fail step on the dispatch event * ci: bound the step summary and publish the full report as an artifact * ci: treat a missing file as a finding, and never truncate mid-fence --- .github/workflows/audit-all-blueprints.yml | 163 +++++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 .github/workflows/audit-all-blueprints.yml diff --git a/.github/workflows/audit-all-blueprints.yml b/.github/workflows/audit-all-blueprints.yml new file mode 100644 index 000000000..f6a37c50d --- /dev/null +++ b/.github/workflows/audit-all-blueprints.yml @@ -0,0 +1,163 @@ +name: Audit All Blueprints + +# The per-PR validators only look at blueprints a PR touches, which is the right +# call for CI cost but means a template is checked when it lands and then never +# again. When a rule is added or tightened, every existing blueprint that breaks +# it stays broken with nothing surfacing it (see #1047). +# +# This job runs the same validators over ALL blueprints on a schedule, so drift +# is reported instead of accumulating. It never runs on pull_request, so it +# cannot block a contributor for a pre-existing problem they did not introduce. + +on: + schedule: + # Mondays, 04:00 UTC + - cron: "0 4 * * 1" + workflow_dispatch: + inputs: + fail_on_findings: + description: "Exit non-zero when findings exist (default: report only)" + type: boolean + default: false + +permissions: + contents: read + +jobs: + audit: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 20.16.0 + + - name: Set up pnpm + uses: pnpm/action-setup@v4 + with: + version: 8 + + - name: Install dependencies + run: cd build-scripts && pnpm install + + - name: Validate per-template metadata + run: node build-scripts/generate-meta.js --check + + - name: Audit every blueprint + id: audit + run: | + # $RUNNER_TEMP is per-job; also cleared explicitly so a re-run or a + # partially-executed earlier step can never leak into the summary. + WORK="${RUNNER_TEMP:-/tmp}" + OUT="$WORK/validator-out.txt" + REPORT="$WORK/audit-report.md" + SUMMARY="$WORK/audit-summary.md" + rm -f "$OUT" "$REPORT" "$SUMMARY" + + COMPOSE_FAILED="" + TOML_FAILED="" + + for dir in blueprints/*/; do + id="$(basename "$dir")" + + # Run unconditionally: a blueprint with no docker-compose.yml or no + # template.toml is itself a finding, and the validators already + # report a missing file clearly. Skipping them would hide exactly + # the blueprints most likely to be broken. + if ! (cd build-scripts && pnpm exec tsx validate-docker-compose.ts \ + --file "../$dir/docker-compose.yml") > "$OUT" 2>&1; then + COMPOSE_FAILED="$COMPOSE_FAILED $id" + { + echo "### \`$id\` — docker-compose.yml" + echo '```' + grep '❌' "$OUT" || cat "$OUT" + echo '```' + } >> "$REPORT" + fi + + if ! (cd build-scripts && pnpm exec tsx validate-template.ts \ + --dir "../$dir") > "$OUT" 2>&1; then + TOML_FAILED="$TOML_FAILED $id" + { + echo "### \`$id\` — template.toml" + echo '```' + grep '❌' "$OUT" || cat "$OUT" + echo '```' + } >> "$REPORT" + fi + done + + COMPOSE_COUNT=$(echo $COMPOSE_FAILED | wc -w | tr -d ' ') + TOML_COUNT=$(echo $TOML_FAILED | wc -w | tr -d ' ') + TOTAL=$(ls -d blueprints/*/ | wc -l | tr -d ' ') + + { + echo "## Blueprint audit" + echo "" + echo "| check | failing | of |" + echo "| --- | --- | --- |" + echo "| \`validate-docker-compose.ts\` | $COMPOSE_COUNT | $TOTAL |" + echo "| \`validate-template.ts\` | $TOML_COUNT | $TOTAL |" + echo "" + } > "$SUMMARY" + + if [ -f "$REPORT" ]; then + cat "$REPORT" >> "$SUMMARY" + else + echo "No findings." >> "$SUMMARY" + fi + + # The step summary caps at ~1MB and truncates silently past it, which + # would hide findings exactly as the backlog grows. Bound what we + # append and point at the artifact for the rest. + SUMMARY_LIMIT=$((512 * 1024)) + SUMMARY_BYTES=$(wc -c < "$SUMMARY" | tr -d ' ') + + if [ "$SUMMARY_BYTES" -gt "$SUMMARY_LIMIT" ]; then + # Emit the header table only, never a byte-sliced prefix: cutting + # mid-``` leaves an unclosed code fence, which swallows the notice + # below it and makes the truncation invisible in the rendered summary. + { + head -n 7 "$SUMMARY" + echo "" + echo "_Report omitted: $SUMMARY_BYTES bytes exceeds the $SUMMARY_LIMIT byte budget for a job summary._" + echo "_Download the \`blueprint-audit\` artifact for the complete output._" + } >> "$GITHUB_STEP_SUMMARY" + else + cat "$SUMMARY" >> "$GITHUB_STEP_SUMMARY" + fi + + # Always published, so the full report survives regardless of size. + cp "$SUMMARY" "$WORK/blueprint-audit.md" + echo "report_path=$WORK/blueprint-audit.md" >> "$GITHUB_OUTPUT" + + echo "compose_failed=$COMPOSE_COUNT" >> "$GITHUB_OUTPUT" + echo "toml_failed=$TOML_COUNT" >> "$GITHUB_OUTPUT" + + if [ "$COMPOSE_COUNT" != "0" ] || [ "$TOML_COUNT" != "0" ]; then + echo "::warning::$COMPOSE_COUNT compose and $TOML_COUNT template.toml findings across $TOTAL blueprints" + fi + + - name: Upload the full report + if: always() && steps.audit.outputs.report_path != '' + uses: actions/upload-artifact@v4 + with: + name: blueprint-audit + path: ${{ steps.audit.outputs.report_path }} + retention-days: 30 + + - name: Fail if requested + # `inputs` is only populated for workflow_dispatch; on a schedule run it + # is not available, so gate on the event first and read the value through + # github.event.inputs, which is an empty string when absent. + if: >- + github.event_name == 'workflow_dispatch' && + github.event.inputs.fail_on_findings == 'true' && + (steps.audit.outputs.compose_failed != '0' || steps.audit.outputs.toml_failed != '0') + run: | + echo "❌ Findings present and fail_on_findings was requested." + exit 1 From 5aba7908a231af219a30c3ff0d3efe0fef8048a8 Mon Sep 17 00:00:00 2001 From: Khiet Tam Nguyen <86177399+nktnet1@users.noreply.github.com> Date: Mon, 3 Aug 2026 20:36:59 +1000 Subject: [PATCH 59/80] chore: remove conduwuit in favour of tuwunel (#1054) --- blueprints/conduwuit/conduwuit.svg | 37 ------------------- blueprints/conduwuit/docker-compose.yml | 48 ------------------------- blueprints/conduwuit/meta.json | 19 ---------- blueprints/conduwuit/template.toml | 15 -------- 4 files changed, 119 deletions(-) delete mode 100644 blueprints/conduwuit/conduwuit.svg delete mode 100644 blueprints/conduwuit/docker-compose.yml delete mode 100644 blueprints/conduwuit/meta.json delete mode 100644 blueprints/conduwuit/template.toml diff --git a/blueprints/conduwuit/conduwuit.svg b/blueprints/conduwuit/conduwuit.svg deleted file mode 100644 index 162a3d9e3..000000000 --- a/blueprints/conduwuit/conduwuit.svg +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - diff --git a/blueprints/conduwuit/docker-compose.yml b/blueprints/conduwuit/docker-compose.yml deleted file mode 100644 index 7945d6c97..000000000 --- a/blueprints/conduwuit/docker-compose.yml +++ /dev/null @@ -1,48 +0,0 @@ -# conduwuit -# https://conduwuit.puppyirl.gay/deploying/docker-compose.yml - -services: - homeserver: - image: girlbossceo/conduwuit:latest - restart: unless-stopped - ports: - - 8448:6167 - volumes: - - db:/var/lib/conduwuit - #- ./conduwuit.toml:/etc/conduwuit.toml - environment: - # Edit this in your Dokploy Environment - CONDUWUIT_SERVER_NAME: ${CONDUWUIT_SERVER_NAME} - - CONDUWUIT_DATABASE_PATH: /var/lib/conduwuit - CONDUWUIT_PORT: 6167 - CONDUWUIT_MAX_REQUEST_SIZE: 20000000 # in bytes, ~20 MB - - CONDUWUIT_ALLOW_REGISTRATION: 'true' - CONDUWUIT_REGISTRATION_TOKEN: ${CONDUWUIT_REGISTRATION_TOKEN} - - CONDUWUIT_ALLOW_FEDERATION: 'true' - CONDUWUIT_ALLOW_CHECK_FOR_UPDATES: 'true' - CONDUWUIT_TRUSTED_SERVERS: '["matrix.org"]' - #CONDUWUIT_LOG: warn,state_res=warn - CONDUWUIT_ADDRESS: 0.0.0.0 - - # Uncomment if you mapped config toml in volumes - #CONDUWUIT_CONFIG: '/etc/conduwuit.toml' - - ### Uncomment if you want to use your own Element-Web App. - ### Note: You need to provide a config.json for Element and you also need a second - ### Domain or Subdomain for the communication between Element and conduwuit - ### Config-Docs: https://github.com/vector-im/element-web/blob/develop/docs/config.md - # element-web: - # image: vectorim/element-web:latest - # restart: unless-stopped - # ports: - # - 8009:80 - # volumes: - # - ./element_config.json:/app/config.json - # depends_on: - # - homeserver - -volumes: - db: diff --git a/blueprints/conduwuit/meta.json b/blueprints/conduwuit/meta.json deleted file mode 100644 index 0e346276d..000000000 --- a/blueprints/conduwuit/meta.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "id": "conduwuit", - "name": "Conduwuit", - "version": "latest", - "description": "Well-maintained, featureful Matrix chat homeserver (fork of Conduit)", - "logo": "conduwuit.svg", - "links": { - "github": "https://github.com/girlbossceo/conduwuit", - "website": "https://conduwuit.puppyirl.gay", - "docs": "https://conduwuit.puppyirl.gay/configuration.html" - }, - "tags": [ - "backend", - "chat", - "communication", - "matrix", - "server" - ] -} diff --git a/blueprints/conduwuit/template.toml b/blueprints/conduwuit/template.toml deleted file mode 100644 index f7bc73016..000000000 --- a/blueprints/conduwuit/template.toml +++ /dev/null @@ -1,15 +0,0 @@ -[variables] -main_domain = "${domain}" -registration_token = "${password:20}" - -[config] -env = [ - "CONDUWUIT_SERVER_NAME=${main_domain}", - "CONDUWUIT_REGISTRATION_TOKEN=${registration_token}", -] -mounts = [] - -[[config.domains]] -serviceName = "homeserver" -port = 6_167 -host = "${main_domain}" From 1dc9c362162c29433aa323bad09c3c9212a9b175 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20G=C3=BCndo=C4=9Fdu?= Date: Mon, 3 Aug 2026 13:37:04 +0300 Subject: [PATCH 60/80] libredb-studio: bump version to 0.9.59 (#1039) --- blueprints/libredb-studio/docker-compose.yml | 2 +- blueprints/libredb-studio/meta.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/blueprints/libredb-studio/docker-compose.yml b/blueprints/libredb-studio/docker-compose.yml index 00b887669..ca3f9b9c6 100644 --- a/blueprints/libredb-studio/docker-compose.yml +++ b/blueprints/libredb-studio/docker-compose.yml @@ -1,7 +1,7 @@ version: "3.8" services: libredb-studio: - image: ghcr.io/libredb/libredb-studio:0.9.27 + image: ghcr.io/libredb/libredb-studio:0.9.59 restart: unless-stopped environment: - ADMIN_EMAIL=admin@libredb.org diff --git a/blueprints/libredb-studio/meta.json b/blueprints/libredb-studio/meta.json index 32868b858..1bb029c99 100644 --- a/blueprints/libredb-studio/meta.json +++ b/blueprints/libredb-studio/meta.json @@ -1,7 +1,7 @@ { "id": "libredb-studio", "name": "LibreDB Studio", - "version": "0.9.27", + "version": "0.9.59", "description": "The modern, AI-powered open-source SQL IDE. Query PostgreSQL, MySQL, Oracle, SQL Server, SQLite, MongoDB and Redis from your browser.", "logo": "libredb-studio.svg", "links": { From a73e4928f2a011d514d1fce8f37d835002b16fb5 Mon Sep 17 00:00:00 2001 From: Khiet Tam Nguyen <86177399+nktnet1@users.noreply.github.com> Date: Mon, 3 Aug 2026 20:39:40 +1000 Subject: [PATCH 61/80] feat: bump fmd-server to 0.16.0 for compatibility with Open Street Map (#1052) * feat(fmd-server): bump to version 0.16.0 * fix: use expose instead of ports * fix: fmd-server image tag is 0.16.0 (upstream dropped the v prefix) Co-Authored-By: Claude Fable 5 --------- Co-authored-by: Mauricio Siu Co-authored-by: Claude Fable 5 --- blueprints/fmd-server/docker-compose.yml | 2 +- blueprints/fmd-server/meta.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/blueprints/fmd-server/docker-compose.yml b/blueprints/fmd-server/docker-compose.yml index 530131f35..22a3a199e 100644 --- a/blueprints/fmd-server/docker-compose.yml +++ b/blueprints/fmd-server/docker-compose.yml @@ -1,6 +1,6 @@ services: fmd-server: - image: registry.gitlab.com/fmd-foss/fmd-server:v0.11.0 + image: registry.gitlab.com/fmd-foss/fmd-server:0.16.0 expose: - 8080 environment: diff --git a/blueprints/fmd-server/meta.json b/blueprints/fmd-server/meta.json index f222c94ae..e2ac9931f 100644 --- a/blueprints/fmd-server/meta.json +++ b/blueprints/fmd-server/meta.json @@ -1,7 +1,7 @@ { "id": "fmd-server", "name": "FMD Server", - "version": "0.11.0", + "version": "0.16.0", "description": "A server to communicate with the FMD Android app, to locate and control your devices.", "logo": "fmd-server.svg", "links": { From de726de736331c3a512a93d120dc67bbe74c92ed Mon Sep 17 00:00:00 2001 From: Jiho Lee Date: Mon, 3 Aug 2026 19:39:57 +0900 Subject: [PATCH 62/80] feat: add 9Router template (#1046) * feat: add 9Router template (#1040) * fix: pin the headroom sidecar instead of :latest * fix: use http for BASE_URL per the repo URL convention * fix: set NEXT_PUBLIC_BASE_URL so the browser hits the deployed domain --- blueprints/9router/9router.png | Bin 0 -> 107769 bytes blueprints/9router/docker-compose.yml | 32 ++++++++++++++++++++++++++ blueprints/9router/meta.json | 17 ++++++++++++++ blueprints/9router/template.toml | 21 +++++++++++++++++ 4 files changed, 70 insertions(+) create mode 100644 blueprints/9router/9router.png create mode 100644 blueprints/9router/docker-compose.yml create mode 100644 blueprints/9router/meta.json create mode 100644 blueprints/9router/template.toml diff --git a/blueprints/9router/9router.png b/blueprints/9router/9router.png new file mode 100644 index 0000000000000000000000000000000000000000..af467dea5760631c822c934fd8f4ab986c482b08 GIT binary patch literal 107769 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 z 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 Date: Mon, 3 Aug 2026 05:46:16 -0500 Subject: [PATCH 63/80] Update SendDock template: track latest image, drop deprecated env vars (#1057) - Point the image at ghcr.io/arkhe-systems/senddock:latest so fresh deploys always get the current release without a per-release PR. - Remove DEPLOYMENT_MODE (self-host is the default now; the cloud flag became the boolean CLOUD). - Remove SENDDOCK_LICENSE_KEY (Pro license is activated from the dashboard in 0.8; the env var is removed in 0.9). --- blueprints/senddock/docker-compose.yml | 4 +--- blueprints/senddock/meta.json | 2 +- blueprints/senddock/template.toml | 3 +-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/blueprints/senddock/docker-compose.yml b/blueprints/senddock/docker-compose.yml index c4ff03d0f..6f7bd2a04 100644 --- a/blueprints/senddock/docker-compose.yml +++ b/blueprints/senddock/docker-compose.yml @@ -27,7 +27,7 @@ services: retries: 5 senddock: - image: ghcr.io/arkhe-systems/senddock:0.6.5.1 + image: ghcr.io/arkhe-systems/senddock:latest restart: unless-stopped depends_on: postgres: @@ -40,8 +40,6 @@ services: - JWT_SECRET=${JWT_SECRET} - PUBLIC_URL=${PUBLIC_URL} - FRONTEND_URL=${PUBLIC_URL} - - DEPLOYMENT_MODE=self-hosted - - SENDDOCK_LICENSE_KEY=${SENDDOCK_LICENSE_KEY} - PORT=8080 expose: - 8080 diff --git a/blueprints/senddock/meta.json b/blueprints/senddock/meta.json index f7ca8736e..a91cbcd66 100644 --- a/blueprints/senddock/meta.json +++ b/blueprints/senddock/meta.json @@ -1,7 +1,7 @@ { "id": "senddock", "name": "SendDock", - "version": "0.6.5.1", + "version": "0.8.0", "description": "Open-source, self-hosted email marketing platform with BYO SMTP. Manage subscribers, templates, broadcasts, and scheduled campaigns from one place.", "logo": "senddock.svg", "links": { diff --git a/blueprints/senddock/template.toml b/blueprints/senddock/template.toml index 4a8c456e8..3ce8d1343 100644 --- a/blueprints/senddock/template.toml +++ b/blueprints/senddock/template.toml @@ -7,8 +7,7 @@ jwt_secret = "${password:64}" env = [ "POSTGRES_PASSWORD=${postgres_password}", "JWT_SECRET=${jwt_secret}", - "PUBLIC_URL=https://${main_domain}", - "SENDDOCK_LICENSE_KEY=" + "PUBLIC_URL=https://${main_domain}" ] [[config.domains]] From 0b731157ce0434265bcdc306eb3749312b7d3454 Mon Sep 17 00:00:00 2001 From: Marios Antonoudiou Date: Mon, 3 Aug 2026 13:46:41 +0300 Subject: [PATCH 64/80] Chatto latest (#1060) * Switch to latest channel for chatto * Update meta.json too * Switch to 0.4 series --- blueprints/chatto/docker-compose.yml | 2 +- blueprints/chatto/meta.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/blueprints/chatto/docker-compose.yml b/blueprints/chatto/docker-compose.yml index 3edb99d3b..b9013af99 100644 --- a/blueprints/chatto/docker-compose.yml +++ b/blueprints/chatto/docker-compose.yml @@ -2,7 +2,7 @@ version: "3.8" services: chatto: - image: ghcr.io/chattocorp/chatto:0.3.8 + image: ghcr.io/chattocorp/chatto:0.4 restart: unless-stopped expose: - "4000" diff --git a/blueprints/chatto/meta.json b/blueprints/chatto/meta.json index d420b2e39..b48f120a8 100644 --- a/blueprints/chatto/meta.json +++ b/blueprints/chatto/meta.json @@ -1,7 +1,7 @@ { "id": "chatto", "name": "Chatto (single binary)", - "version": "0.3.8", + "version": "0.4.x", "description": "A fully-featured real-time chat application for teams and communities. This template deploys the single Chatto binary with embedded NATS. Note: email/password registration requires SMTP to be configured after deployment; voice/video calls are not included in the single-binary setup.", "logo": "logo.png", "links": { From 025ae61c2ce0fc1821480a2794c6b64108693ff2 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Mon, 3 Aug 2026 05:01:17 -0600 Subject: [PATCH 65/80] feat: add Whatomate template (#1064) Co-authored-by: Claude Fable 5 --- blueprints/whatomate/docker-compose.yml | 46 +++++++++++++++++ blueprints/whatomate/instructions.md | 14 ++++++ blueprints/whatomate/meta.json | 17 +++++++ blueprints/whatomate/template.toml | 67 +++++++++++++++++++++++++ blueprints/whatomate/whatomate.svg | 5 ++ 5 files changed, 149 insertions(+) create mode 100644 blueprints/whatomate/docker-compose.yml create mode 100644 blueprints/whatomate/instructions.md create mode 100644 blueprints/whatomate/meta.json create mode 100644 blueprints/whatomate/template.toml create mode 100644 blueprints/whatomate/whatomate.svg diff --git a/blueprints/whatomate/docker-compose.yml b/blueprints/whatomate/docker-compose.yml new file mode 100644 index 000000000..ac5d26785 --- /dev/null +++ b/blueprints/whatomate/docker-compose.yml @@ -0,0 +1,46 @@ +services: + whatomate: + image: shridh0r/whatomate:0.1.1-beta-0.16.1 + restart: unless-stopped + depends_on: + db: + condition: service_healthy + redis: + condition: service_healthy + environment: + - TZ=Etc/UTC + volumes: + - ../files/config.toml:/app/config.toml:ro + - whatomate-uploads:/app/uploads + command: ["server", "-migrate", "-config", "/app/config.toml"] + + db: + image: postgres:17-alpine + restart: unless-stopped + environment: + - POSTGRES_USER=whatomate + - POSTGRES_PASSWORD=whatomate + - POSTGRES_DB=whatomate + healthcheck: + test: ["CMD-SHELL", "pg_isready -U whatomate -d whatomate"] + interval: 10s + timeout: 5s + retries: 6 + volumes: + - whatomate-postgres-data:/var/lib/postgresql/data + + redis: + image: redis:7-alpine + restart: unless-stopped + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + volumes: + - whatomate-redis-data:/data + +volumes: + whatomate-uploads: + whatomate-postgres-data: + whatomate-redis-data: diff --git a/blueprints/whatomate/instructions.md b/blueprints/whatomate/instructions.md new file mode 100644 index 000000000..69d5f947c --- /dev/null +++ b/blueprints/whatomate/instructions.md @@ -0,0 +1,14 @@ +# Whatomate + +## Getting started + +1. Deploy the template and open the app domain. +2. Log in with the default admin credentials: **admin@admin.com** / **admin**. Change the password right after the first login. +3. Connect your WhatsApp Business account: go to **Settings → WhatsApp Accounts** and add your Meta Cloud API credentials (phone number ID, access token). See the [configuration docs](https://shridarpatil.github.io/whatomate/getting-started/configuration/). +4. Point your Meta webhook to `https:///webhook` and use the `webhook_verify_token` from the generated `config.toml` mount (Dokploy → your service → Advanced → Mounts). + +## Notes + +- The full application configuration lives in the `config.toml` file mount (JWT secret, encryption key and webhook verify token are auto-generated per deployment). Edit it and redeploy to change settings. +- Uploads are persisted in the `whatomate-uploads` volume; PostgreSQL and Redis data in `whatomate-postgres-data` and `whatomate-redis-data`. +- Voice calling / IVR requires extra WebRTC configuration (UDP ports, public IP or TURN server). See the [calling docs](https://shridarpatil.github.io/whatomate/features/calling/). diff --git a/blueprints/whatomate/meta.json b/blueprints/whatomate/meta.json new file mode 100644 index 000000000..42729bc8c --- /dev/null +++ b/blueprints/whatomate/meta.json @@ -0,0 +1,17 @@ +{ + "id": "whatomate", + "name": "Whatomate", + "version": "0.1.1-beta-0.16.1", + "description": "Modern, open-source WhatsApp Business Platform with real-time chat, chatbot automation, bulk campaigns, template management, and voice calling via Meta's Cloud API.", + "logo": "whatomate.svg", + "links": { + "github": "https://github.com/shridarpatil/whatomate", + "website": "https://whatomate.io/", + "docs": "https://shridarpatil.github.io/whatomate/" + }, + "tags": [ + "communication", + "automation", + "crm" + ] +} diff --git a/blueprints/whatomate/template.toml b/blueprints/whatomate/template.toml new file mode 100644 index 000000000..6a8378b38 --- /dev/null +++ b/blueprints/whatomate/template.toml @@ -0,0 +1,67 @@ +[variables] +main_domain = "${domain}" +jwt_secret = "${password:64}" +encryption_key = "${password:32}" +webhook_verify_token = "${password:32}" + +[config] +[[config.domains]] +serviceName = "whatomate" +port = 8_080 +host = "${main_domain}" + +[[config.mounts]] +filePath = "config.toml" +content = """ +[app] +name = "Whatomate" +environment = "staging" +debug = false +encryption_key = "${encryption_key}" + +[server] +host = "0.0.0.0" +port = 8080 +read_timeout = 30 +write_timeout = 30 +base_path = "" +allowed_origins = "" + +[database] +host = "db" +port = 5432 +user = "whatomate" +password = "whatomate" +name = "whatomate" +ssl_mode = "disable" +max_open_conns = 25 +max_idle_conns = 5 +conn_max_lifetime = 300 + +[redis] +host = "redis" +port = 6379 +username = "" +password = "" +db = 0 +tls = false + +[jwt] +secret = "${jwt_secret}" +access_expiry_mins = 15 +refresh_expiry_days = 1 + +[storage] +type = "local" +local_path = "./uploads" + +[whatsapp] +webhook_verify_token = "${webhook_verify_token}" +api_version = "v24.0" +base_url = "https://graph.facebook.com" + +[default_admin] +email = "admin@admin.com" +password = "admin" +full_name = "Admin" +""" diff --git a/blueprints/whatomate/whatomate.svg b/blueprints/whatomate/whatomate.svg new file mode 100644 index 000000000..e65ab80cd --- /dev/null +++ b/blueprints/whatomate/whatomate.svg @@ -0,0 +1,5 @@ + + + + + From 755c7c5c5045fb67267688267aa75f429a870754 Mon Sep 17 00:00:00 2001 From: abhishukla610 <47338553+abhishukla610@users.noreply.github.com> Date: Mon, 3 Aug 2026 16:32:23 +0530 Subject: [PATCH 66/80] Add Open Design template (#966) * Add Open Design template Open Design is a local-first, open-source design tool that ships as a single self-contained container (no external DB or cache). Adds the blueprint (docker-compose.yml + template.toml + logo) and a meta.json entry. * Use node-based healthcheck for Open Design template (matches upstream; verified locally) * Pin Open Design image to 0.16.1 for reproducible deploys Replace :latest with the immutable released tag ghcr.io/nexu-io/od:0.16.1 in docker-compose.yml and set the matching version in meta.json, per the repo pinning convention (AGENTS.md). Tag 0.16.1 verified: /api/health returns 200 and the node-based healthcheck passes. * Allow both http and https origins for Open Design template Dokploy's auto-generated domain is served over plain HTTP by default (no cert), so OD_ALLOWED_ORIGINS=https://${main_domain} alone made the daemon reject the browser's http origin with 403, breaking the app after the onboarding shell loaded. List both schemes so the template works on the default HTTP domain and after a user adds an HTTPS certificate. Verified against a live deploy: listed origin -> 200, unlisted -> 403; the daemon supports comma-separated multi-origin per deploy/README.md. * Disable daemon API auth so the Open Design web UI loads The daemon token-gates its API when bound to 0.0.0.0 (every container), and a browser has no way to present that bearer token, so the web UI's bootstrap calls all returned 401 and the app failed to load. Set OD_DISABLE_API_AUTH=1 (the daemon's documented escape hatch) so the UI works on deploy. Add a README noting the instance is then open (front it with auth/HTTPS if internet-facing) and how to enable AI features. Verified end to end against a live deploy on a real domain. --------- Co-authored-by: Abhiigg Co-authored-by: Mauricio Siu --- blueprints/open-design/README.md | 19 +++++++++++++++ blueprints/open-design/docker-compose.yml | 29 +++++++++++++++++++++++ blueprints/open-design/meta.json | 16 +++++++++++++ blueprints/open-design/open-design.svg | 3 +++ blueprints/open-design/template.toml | 15 ++++++++++++ 5 files changed, 82 insertions(+) create mode 100644 blueprints/open-design/README.md create mode 100644 blueprints/open-design/docker-compose.yml create mode 100644 blueprints/open-design/meta.json create mode 100644 blueprints/open-design/open-design.svg create mode 100644 blueprints/open-design/template.toml diff --git a/blueprints/open-design/README.md b/blueprints/open-design/README.md new file mode 100644 index 000000000..0afd0397d --- /dev/null +++ b/blueprints/open-design/README.md @@ -0,0 +1,19 @@ +# Open Design + +Local-first, open-source design tool with native desktop apps, a large library of +design systems, and an extensible plugin ecosystem. + +## Notes + +- **Access / security.** This template sets `OD_DISABLE_API_AUTH=1`. The Open Design + daemon token-gates its API when bound to a non-loopback address, and a browser + cannot supply that bearer token — so disabling it is required for the web UI to + load. As a result the instance is **reachable by anyone who has the URL**. If it is + internet-facing, put an authenticating layer in front (for example Dokploy/Traefik + basic-auth) and enable an HTTPS certificate on the domain. + +- **AI features.** The image intentionally does not bundle an AI agent CLI. To + generate designs, either **Sign in to Open Design** (cloud) or choose **Bring your + own key** and provide a provider API key (e.g. `ANTHROPIC_API_KEY`, + `OPENAI_API_KEY`). A "vela binary not found" notice on the sign-in screen is + expected and only affects the cloud-agent path. diff --git a/blueprints/open-design/docker-compose.yml b/blueprints/open-design/docker-compose.yml new file mode 100644 index 000000000..709b4da0c --- /dev/null +++ b/blueprints/open-design/docker-compose.yml @@ -0,0 +1,29 @@ +version: "3.8" + +services: + open-design: + image: ghcr.io/nexu-io/od:0.16.1 + restart: unless-stopped + environment: + NODE_ENV: production + NODE_OPTIONS: --max-old-space-size=192 + OD_BIND_HOST: 0.0.0.0 + OD_PORT: 7456 + OD_API_TOKEN: ${OD_API_TOKEN} + OD_ALLOWED_ORIGINS: ${OD_ALLOWED_ORIGINS} + # The daemon token-gates its API when bound to 0.0.0.0, and a browser + # cannot present that token, so the web UI needs API auth disabled to + # load. The instance is then reachable by anyone with the URL — front it + # with auth/HTTPS if it is internet-facing (see README.md). + OD_DISABLE_API_AUTH: "1" + volumes: + - open_design_data:/app/.od + healthcheck: + test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:7456/api/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 20s + +volumes: + open_design_data: {} diff --git a/blueprints/open-design/meta.json b/blueprints/open-design/meta.json new file mode 100644 index 000000000..b3c437398 --- /dev/null +++ b/blueprints/open-design/meta.json @@ -0,0 +1,16 @@ +{ + "id": "open-design", + "name": "Open Design", + "version": "0.16.1", + "description": "Open Design is a local-first, open-source design tool with native desktop apps, a large library of design systems, and an extensible plugin ecosystem that runs across many coding agents.", + "logo": "open-design.svg", + "links": { + "github": "https://github.com/nexu-io/open-design", + "website": "https://open-design.ai", + "docs": "https://open-design.ai" + }, + "tags": [ + "design", + "self-hosted" + ] +} diff --git a/blueprints/open-design/open-design.svg b/blueprints/open-design/open-design.svg new file mode 100644 index 000000000..19980f0fa --- /dev/null +++ b/blueprints/open-design/open-design.svg @@ -0,0 +1,3 @@ + + + diff --git a/blueprints/open-design/template.toml b/blueprints/open-design/template.toml new file mode 100644 index 000000000..1bfd9b3a1 --- /dev/null +++ b/blueprints/open-design/template.toml @@ -0,0 +1,15 @@ +[variables] +main_domain = "${domain}" +api_token = "${password:32}" + +[config] +env = [ + "OD_API_TOKEN=${api_token}", + "OD_ALLOWED_ORIGINS=https://${main_domain},http://${main_domain}", +] +mounts = [] + +[[config.domains]] +serviceName = "open-design" +port = 7456 +host = "${main_domain}" From b258e9f77498b3de5d4e5a3a7d7d33906cb54f38 Mon Sep 17 00:00:00 2001 From: rowupstream <259214402+rowupstream@users.noreply.github.com> Date: Tue, 4 Aug 2026 10:07:09 -0500 Subject: [PATCH 67/80] chore: update stalwart named volumes for v0.16 --- blueprints/stalwart/docker-compose.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/blueprints/stalwart/docker-compose.yml b/blueprints/stalwart/docker-compose.yml index b31ca56bb..41cac0d91 100644 --- a/blueprints/stalwart/docker-compose.yml +++ b/blueprints/stalwart/docker-compose.yml @@ -13,8 +13,10 @@ services: - "110" # POP3 - "995" # POP3S volumes: - - stalwart_data:/opt/stalwart-mail + - stalwart_etc:/etc/stalwart + - stalwart_data:/var/lib/stalwart restart: unless-stopped volumes: + stalwart_etc: stalwart_data: From ebeeb58dee152b8c6c8ed77557c70bbc01135d0a Mon Sep 17 00:00:00 2001 From: Szymon Bludnik <120786782+Simciutoja@users.noreply.github.com> Date: Thu, 6 Aug 2026 09:06:00 +0200 Subject: [PATCH 68/80] Add Creed self-host template (#1043) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(templates): add Creed self-host template Adds a Dokploy blueprint for Creed (creed.md), built from source since no official Docker image exists yet. Requires an external Supabase project (schema pushed via the Supabase CLI), so only what can be generated locally (CREED_ENCRYPTION_SECRET) is auto-filled; Supabase, Stripe, OpenRouter, Resend, and GitHub OAuth vars are left blank with setup steps documented in the mounted README. NEXT_PUBLIC_* vars are threaded through as Docker build args (not just runtime env), since Next.js inlines them at build time and the app hard-fails the build if NEXT_PUBLIC_SITE_URL is unset — this was caught by an actual `docker compose build` run against the upstream repo, not just template validation. Verified with docker compose build/up: image builds cleanly, container boots, / returns 200, and /api/health correctly reports "degraded" with Supabase unconfigured as expected. * Pin creed build context and refactor env - Pin creed docker-compose build context to a specific commit hash to ensure reproducible builds - Expose port 3000 in docker-compose (use expose instead of ports) - Update meta.json version from latest to the pinned hash e9b096d - Convert template.toml env block from [config.env] to a single env array - Keep the README.md mount in place * Modify docker-compose build context URL Updated the build context URL to the main branch. * fix(creed): pin build context and move env under [config] Restore the commit pin so the inline Dockerfile matches the pre-monorepo layout, and place env under [config] before domains so Dokploy imports the environment correctly. --- blueprints/creed/creed.svg | 8 +++ blueprints/creed/docker-compose.yml | 65 +++++++++++++++++ blueprints/creed/meta.json | 19 +++++ blueprints/creed/template.toml | 105 ++++++++++++++++++++++++++++ 4 files changed, 197 insertions(+) create mode 100644 blueprints/creed/creed.svg create mode 100644 blueprints/creed/docker-compose.yml create mode 100644 blueprints/creed/meta.json create mode 100644 blueprints/creed/template.toml diff --git a/blueprints/creed/creed.svg b/blueprints/creed/creed.svg new file mode 100644 index 000000000..a07c3b26e --- /dev/null +++ b/blueprints/creed/creed.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/blueprints/creed/docker-compose.yml b/blueprints/creed/docker-compose.yml new file mode 100644 index 000000000..fb74bfcc5 --- /dev/null +++ b/blueprints/creed/docker-compose.yml @@ -0,0 +1,65 @@ +version: "3.8" +services: + creed: + build: + context: https://github.com/connorhpbrn/creed.git#e9b096d6d68fef6df336338bc9965a94ab62f75e + dockerfile_inline: | + FROM node:20-alpine AS builder + WORKDIR /app + COPY package.json package-lock.json ./ + RUN npm ci + COPY . . + ARG NEXT_PUBLIC_SITE_URL + ARG NEXT_PUBLIC_SUPABASE_URL + ARG NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY + ARG NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY + ARG NEXT_PUBLIC_CONTACT_EMAIL + ARG NEXT_PUBLIC_GITHUB_URL + ENV NEXT_PUBLIC_SITE_URL=$NEXT_PUBLIC_SITE_URL \ + NEXT_PUBLIC_SUPABASE_URL=$NEXT_PUBLIC_SUPABASE_URL \ + NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=$NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY \ + NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=$NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY \ + NEXT_PUBLIC_CONTACT_EMAIL=$NEXT_PUBLIC_CONTACT_EMAIL \ + NEXT_PUBLIC_GITHUB_URL=$NEXT_PUBLIC_GITHUB_URL + RUN npm run build + + FROM node:20-alpine AS runner + WORKDIR /app + ENV NODE_ENV=production + COPY --from=builder /app/public ./public + COPY --from=builder /app/.next ./.next + COPY --from=builder /app/node_modules ./node_modules + COPY --from=builder /app/package.json ./package.json + COPY --from=builder /app/next.config.ts ./next.config.ts + EXPOSE 3000 + CMD ["npm", "start"] + args: + NEXT_PUBLIC_SITE_URL: ${NEXT_PUBLIC_SITE_URL} + NEXT_PUBLIC_SUPABASE_URL: ${NEXT_PUBLIC_SUPABASE_URL} + NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY: ${NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY} + NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: ${NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY} + NEXT_PUBLIC_CONTACT_EMAIL: ${NEXT_PUBLIC_CONTACT_EMAIL} + NEXT_PUBLIC_GITHUB_URL: ${NEXT_PUBLIC_GITHUB_URL} + restart: unless-stopped + environment: + - NEXT_PUBLIC_SITE_URL=${NEXT_PUBLIC_SITE_URL} + - NEXT_PUBLIC_SUPABASE_URL=${NEXT_PUBLIC_SUPABASE_URL} + - NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=${NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY} + - SUPABASE_SECRET_KEY=${SUPABASE_SECRET_KEY} + - CREED_ENCRYPTION_SECRET=${CREED_ENCRYPTION_SECRET} + - STRIPE_SECRET_KEY=${STRIPE_SECRET_KEY} + - NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=${NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY} + - STRIPE_WEBHOOK_SECRET=${STRIPE_WEBHOOK_SECRET} + - OPENROUTER_PLATFORM_KEY=${OPENROUTER_PLATFORM_KEY} + - ANALYSIS_MODEL=${ANALYSIS_MODEL} + - TAB_MODEL=${TAB_MODEL} + - PANEL_MODEL=${PANEL_MODEL} + - RESEND_API_KEY=${RESEND_API_KEY} + - RESEND_FROM_EMAIL=${RESEND_FROM_EMAIL} + - GITHUB_OAUTH_CLIENT_ID=${GITHUB_OAUTH_CLIENT_ID} + - GITHUB_OAUTH_CLIENT_SECRET=${GITHUB_OAUTH_CLIENT_SECRET} + - NEXT_PUBLIC_CONTACT_EMAIL=${NEXT_PUBLIC_CONTACT_EMAIL} + - NEXT_PUBLIC_GITHUB_URL=${NEXT_PUBLIC_GITHUB_URL} + - CREED_CSP_ENFORCE=${CREED_CSP_ENFORCE} + expose: + - 3000 diff --git a/blueprints/creed/meta.json b/blueprints/creed/meta.json new file mode 100644 index 000000000..d73220b09 --- /dev/null +++ b/blueprints/creed/meta.json @@ -0,0 +1,19 @@ +{ + "id": "creed", + "name": "Creed", + "version": "e9b096d", + "description": "One personal context file every AI agent reads before answering and proposes updates to as it learns about you. Requires an external Supabase project (see instructions).", + "logo": "creed.svg", + "links": { + "github": "https://github.com/connorhpbrn/creed", + "website": "https://creed.md", + "docs": "https://creed.md/docs" + }, + "tags": [ + "ai", + "productivity", + "mcp", + "nextjs", + "supabase" + ] +} diff --git a/blueprints/creed/template.toml b/blueprints/creed/template.toml new file mode 100644 index 000000000..5b3b3b220 --- /dev/null +++ b/blueprints/creed/template.toml @@ -0,0 +1,105 @@ +[variables] +main_domain = "${domain}" +creed_encryption_secret = "${base64:32}" + +[config] +env = [ + "NEXT_PUBLIC_SITE_URL=https://${main_domain}", + "NEXT_PUBLIC_SUPABASE_URL=", + "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=", + "SUPABASE_SECRET_KEY=", + "CREED_ENCRYPTION_SECRET=${creed_encryption_secret}", + "STRIPE_SECRET_KEY=", + "NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=", + "STRIPE_WEBHOOK_SECRET=", + "OPENROUTER_PLATFORM_KEY=", + "ANALYSIS_MODEL=", + "TAB_MODEL=", + "PANEL_MODEL=", + "RESEND_API_KEY=", + "RESEND_FROM_EMAIL=Creed ", + "GITHUB_OAUTH_CLIENT_ID=", + "GITHUB_OAUTH_CLIENT_SECRET=", + "NEXT_PUBLIC_CONTACT_EMAIL=", + "NEXT_PUBLIC_GITHUB_URL=https://github.com/connorhpbrn/creed", + "CREED_CSP_ENFORCE=", +] + +[[config.domains]] +serviceName = "creed" +port = 3000 +host = "${main_domain}" + +[[config.mounts]] +filePath = "README.md" +content = """# Creed + +Creed (creed.md) is a personal context file: one Markdown file every AI agent +reads before answering and proposes updates to as it learns about you. This +template builds the app from source (https://github.com/connorhpbrn/creed) +and runs it as a stateless Next.js container — Postgres, auth, and storage +live in Supabase, which is **not** part of this template. + +## Before you deploy + +Creed needs a Supabase project. It is not enough to point it at a plain +Postgres database: the app relies on Supabase Auth, Row Level Security, and +realtime subscriptions, and its schema is applied through the Supabase CLI. + +1. Create a Supabase project — either a hosted one at supabase.com, or your + own self-hosted instance (Dokploy has a separate **Supabase** template you + can deploy for this). +2. On a machine with Node 20+, clone the Creed repo and push its schema to + your project: + ```bash + git clone https://github.com/connorhpbrn/creed.git + cd creed && npm install + supabase link --project-ref + supabase db push + ``` +3. From Project Settings -> API in your Supabase dashboard, copy the + project URL, the publishable (anon) key, and the secret (service_role) + key. + +## Required environment variables + +Fill these in on the **Environment** tab before the first deploy, then +redeploy: + +- `NEXT_PUBLIC_SUPABASE_URL`: your Supabase project URL. +- `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY`: the Supabase anon/publishable key. +- `SUPABASE_SECRET_KEY`: the Supabase service_role/secret key. Server-only, + never exposed to the browser. +- `CREED_ENCRYPTION_SECRET` is generated for you (used to encrypt provider + tokens at rest). +- `NEXT_PUBLIC_SITE_URL` is set to this service's domain automatically. + +The app boots with only these set, but sign-in, AI features, and billing +stay disabled until their own variables are configured. + +## Optional integrations + +- **Stripe** (paid plans): `STRIPE_SECRET_KEY`, `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY`, + `STRIPE_WEBHOOK_SECRET`. Create a webhook endpoint at + `https:///api/stripe/webhook` in the Stripe dashboard for the + signing secret. +- **OpenRouter** (managed AI credits): `OPENROUTER_PLATFORM_KEY` from + https://openrouter.ai/keys. `ANALYSIS_MODEL`, `TAB_MODEL`, `PANEL_MODEL` + override the default model per AI feature and can be left blank. +- **Resend** (Company invite emails): `RESEND_API_KEY` and + `RESEND_FROM_EMAIL` (must be on a domain verified in Resend). +- **GitHub OAuth** (repo sync, not sign-in): create an OAuth App at + https://github.com/settings/developers with callback URL + `https:///auth/github/callback`, then set + `GITHUB_OAUTH_CLIENT_ID` and `GITHUB_OAUTH_CLIENT_SECRET`. +- `CREED_CSP_ENFORCE=1` switches the Content-Security-Policy header from + report-only to enforcing. Leave it unset for at least one deploy cycle so + you can check the browser console for violations first. + +## Notes + +- This template builds the image from the Creed GitHub repository during + deployment; the first deploy can take a few minutes. +- The app itself is stateless — all data lives in Supabase, so there are no + volumes to back up here. +""" From 01930d7a714b228e66233f9487a887e2ff871932 Mon Sep 17 00:00:00 2001 From: Maarten Coppens Date: Thu, 6 Aug 2026 09:06:22 +0200 Subject: [PATCH 69/80] Add Yuvomi template (#1065) * Add Yuvomi template Privacy-first, self-hosted family planner. - Image pinned to 0.71.10 (latest stable GHCR tag) - 4 named volumes (data/backups/modules/documents) - SESSION_SECRET via ${password:64} helper - Single domain on port 3000 - Healthcheck wired * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * chore(yuvomi): bump image to 1.85.0 The PR pinned ghcr.io/ulsklyc/yuvomi:0.71.10, but upstream's latest stable release is v1.85.0. Bump the compose image tag and meta.json version to match. Co-Authored-By: Claude Fable 5 --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Mauricio Siu Co-authored-by: Claude Fable 5 --- blueprints/yuvomi/docker-compose.yml | 32 ++++++++++++++++++++++++++++ blueprints/yuvomi/meta.json | 19 +++++++++++++++++ blueprints/yuvomi/template.toml | 17 +++++++++++++++ blueprints/yuvomi/yuvomi.svg | 20 +++++++++++++++++ 4 files changed, 88 insertions(+) create mode 100644 blueprints/yuvomi/docker-compose.yml create mode 100644 blueprints/yuvomi/meta.json create mode 100644 blueprints/yuvomi/template.toml create mode 100644 blueprints/yuvomi/yuvomi.svg diff --git a/blueprints/yuvomi/docker-compose.yml b/blueprints/yuvomi/docker-compose.yml new file mode 100644 index 000000000..86dab9f9f --- /dev/null +++ b/blueprints/yuvomi/docker-compose.yml @@ -0,0 +1,32 @@ +services: + yuvomi: + image: ghcr.io/ulsklyc/yuvomi:1.85.0 + restart: unless-stopped + environment: + NODE_ENV: production + DB_PATH: /data/yuvomi.db + BACKUP_DIR: /backups + DOCUMENT_STORAGE_LOCAL_ENABLED: ${DOCUMENT_STORAGE_LOCAL_ENABLED} + DOCUMENT_STORAGE_LOCAL_PATH: /documents + SESSION_SECURE: "true" + TRUST_PROXY: "1" + SESSION_SECRET: ${SESSION_SECRET} + volumes: + - yuvomi-data:/data + - yuvomi-backups:/backups + - yuvomi-modules:/app/modules + - yuvomi-documents:/documents + expose: + - 3000 + healthcheck: + test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/health', r => process.exit(r.statusCode === 200 ? 0 : 1))"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s + +volumes: + yuvomi-data: + yuvomi-backups: + yuvomi-modules: + yuvomi-documents: diff --git a/blueprints/yuvomi/meta.json b/blueprints/yuvomi/meta.json new file mode 100644 index 000000000..18f511af0 --- /dev/null +++ b/blueprints/yuvomi/meta.json @@ -0,0 +1,19 @@ +{ + "id": "yuvomi", + "name": "Yuvomi", + "version": "1.85.0", + "description": "Privacy-first, self-hosted family planner. Tasks, shopping, meals, calendar sync, budget — one private home for everything your household runs on. No cloud, no subscriptions.", + "logo": "yuvomi.svg", + "links": { + "github": "https://github.com/ulsklyc/yuvomi", + "website": "https://yuvomi.cloud/", + "docs": "https://github.com/ulsklyc/yuvomi#readme" + }, + "tags": [ + "open-source", + "family", + "productivity", + "calendar", + "tasks" + ] +} diff --git a/blueprints/yuvomi/template.toml b/blueprints/yuvomi/template.toml new file mode 100644 index 000000000..ea7a4fb88 --- /dev/null +++ b/blueprints/yuvomi/template.toml @@ -0,0 +1,17 @@ +[variables] +main_domain = "${domain}" +session_secret = "${password:64}" + +[config] +mounts = [] + +[[config.domains]] +serviceName = "yuvomi" +port = 3000 +host = "${main_domain}" + +[config.env] +SESSION_SECRET = "${session_secret}" +SESSION_SECURE = "true" +TRUST_PROXY = "1" +DOCUMENT_STORAGE_LOCAL_ENABLED = "false" diff --git a/blueprints/yuvomi/yuvomi.svg b/blueprints/yuvomi/yuvomi.svg new file mode 100644 index 000000000..06f7d7301 --- /dev/null +++ b/blueprints/yuvomi/yuvomi.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + From 8ad0f9134e545926f4a4db88cd83f1f7c8abf68f Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Thu, 6 Aug 2026 01:11:35 -0600 Subject: [PATCH 70/80] fix(supabase): upgrade database to Postgres 17 (#1068) Align the template with the current upstream self-hosted stack (supabase/postgres 17.6.1.136, studio 2026.08.03, kong 3.9.3), remove the deprecated analytics/vector services and switch Studio/pg-meta to the postgres role per upstream guidance. Closes #1067 Co-authored-by: Claude Fable 5 --- blueprints/supabase/docker-compose.yml | 102 ++------- blueprints/supabase/meta.json | 2 +- blueprints/supabase/template.toml | 290 +------------------------ 3 files changed, 23 insertions(+), 371 deletions(-) diff --git a/blueprints/supabase/docker-compose.yml b/blueprints/supabase/docker-compose.yml index f6fed3b50..8a193e6ca 100644 --- a/blueprints/supabase/docker-compose.yml +++ b/blueprints/supabase/docker-compose.yml @@ -11,7 +11,7 @@ name: supabase services: studio: - image: supabase/studio:2026.06.03-sha-0bca601 + image: supabase/studio:2026.08.03-sha-022b374 restart: unless-stopped healthcheck: test: @@ -23,9 +23,6 @@ services: interval: 5s retries: 5 start_period: 30s - depends_on: - analytics: - condition: service_healthy environment: HOSTNAME: "0.0.0.0" @@ -35,6 +32,9 @@ services: POSTGRES_DB: ${POSTGRES_DB} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + # See: https://supabase.com/docs/guides/self-hosting/remove-superuser-access + POSTGRES_USER_READ_WRITE: postgres + PG_META_CRYPTO_KEY: ${PG_META_CRYPTO_KEY} PGRST_DB_SCHEMAS: ${PGRST_DB_SCHEMAS} PGRST_DB_MAX_ROWS: ${PGRST_DB_MAX_ROWS:-1000} @@ -49,14 +49,12 @@ services: SUPABASE_ANON_KEY: ${ANON_KEY} SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY} AUTH_JWT_SECRET: ${JWT_SECRET} + SUPABASE_PUBLISHABLE_KEY: ${SUPABASE_PUBLISHABLE_KEY:-} + SUPABASE_SECRET_KEY: ${SUPABASE_SECRET_KEY:-} - LOGFLARE_API_KEY: ${LOGFLARE_PUBLIC_ACCESS_TOKEN} - LOGFLARE_PUBLIC_ACCESS_TOKEN: ${LOGFLARE_PUBLIC_ACCESS_TOKEN} - LOGFLARE_PRIVATE_ACCESS_TOKEN: ${LOGFLARE_PRIVATE_ACCESS_TOKEN} + # Analytics (Logflare) was removed from the upstream self-hosted stack + ENABLED_FEATURES_LOGS_ALL: "false" - LOGFLARE_URL: http://analytics:4000 - NEXT_PUBLIC_ENABLE_LOGS: "true" - NEXT_ANALYTICS_BACKEND_PROVIDER: postgres SNIPPETS_MANAGEMENT_FOLDER: /app/snippets EDGE_FUNCTIONS_MANAGEMENT_FOLDER: /app/edge-functions volumes: @@ -64,7 +62,7 @@ services: - ../files/volumes/functions:/app/edge-functions:Z kong: - image: kong/kong:3.9.1 + image: kong/kong:3.9.3 restart: unless-stopped healthcheck: test: ["CMD", "kong", "health"] @@ -138,6 +136,7 @@ services: GOTRUE_JWT_EXP: ${JWT_EXPIRY} GOTRUE_JWT_SECRET: ${JWT_SECRET} GOTRUE_JWT_KEYS: ${JWT_KEYS:-[]} + GOTRUE_JWT_ISSUER: ${API_EXTERNAL_URL} GOTRUE_EXTERNAL_EMAIL_ENABLED: ${ENABLE_EMAIL_SIGNUP} GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED: ${ENABLE_ANONYMOUS_USERS} @@ -163,12 +162,19 @@ services: depends_on: db: condition: service_healthy + healthcheck: + test: [ "CMD", "postgrest", "--ready" ] + interval: 5s + timeout: 5s + retries: 3 environment: PGRST_DB_URI: postgres://authenticator:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} PGRST_DB_SCHEMAS: ${PGRST_DB_SCHEMAS} PGRST_DB_MAX_ROWS: ${PGRST_DB_MAX_ROWS:-1000} PGRST_DB_EXTRA_SEARCH_PATH: ${PGRST_DB_EXTRA_SEARCH_PATH:-public} PGRST_DB_ANON_ROLE: anon + PGRST_ADMIN_SERVER_PORT: 3001 + PGRST_ADMIN_SERVER_HOST: localhost PGRST_JWT_SECRET: ${JWT_SECRET} PGRST_DB_USE_LEGACY_GUCS: "false" PGRST_APP_SETTINGS_JWT_SECRET: ${JWT_SECRET} @@ -292,7 +298,8 @@ services: PG_META_DB_HOST: ${POSTGRES_HOST} PG_META_DB_PORT: ${POSTGRES_PORT} PG_META_DB_NAME: ${POSTGRES_DB} - PG_META_DB_USER: supabase_admin + # See: https://supabase.com/docs/guides/self-hosting/remove-superuser-access + PG_META_DB_USER: postgres PG_META_DB_PASSWORD: ${POSTGRES_PASSWORD} CRYPTO_KEY: ${PG_META_CRYPTO_KEY} @@ -322,43 +329,9 @@ services: "/home/deno/functions/main" ] - analytics: - image: supabase/logflare:1.36.1 - restart: unless-stopped - expose: - - 4000 - # First boot: Logflare seeds _analytics in Postgres; allow extra time before marking unhealthy. - healthcheck: - test: - [ - "CMD-SHELL", - "curl -sSfL -o /dev/null http://localhost:4000/health" - ] - timeout: 10s - interval: 5s - retries: 15 - start_period: 90s - depends_on: - db: - condition: service_healthy - environment: - LOGFLARE_NODE_HOST: 127.0.0.1 - DB_USERNAME: supabase_admin - DB_DATABASE: _supabase - DB_HOSTNAME: ${POSTGRES_HOST} - DB_PORT: ${POSTGRES_PORT} - DB_PASSWORD: ${POSTGRES_PASSWORD} - DB_SCHEMA: _analytics - LOGFLARE_PUBLIC_ACCESS_TOKEN: ${LOGFLARE_PUBLIC_ACCESS_TOKEN} - LOGFLARE_PRIVATE_ACCESS_TOKEN: ${LOGFLARE_PRIVATE_ACCESS_TOKEN} - LOGFLARE_SINGLE_TENANT: "true" - LOGFLARE_SUPABASE_MODE: "true" - LOGFLARE_MIN_CLUSTER_SIZE: 1 - POSTGRES_BACKEND_URL: postgresql://supabase_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/_supabase - POSTGRES_BACKEND_SCHEMA: _analytics - LOGFLARE_FEATURE_FLAG_OVERRIDE: multibackend=true - db: + # Major upgrade from Postgres 15 requires a dump/restore for existing installs. + # See: https://github.com/orgs/supabase/discussions/46080 image: supabase/postgres:17.6.1.136 restart: unless-stopped volumes: @@ -385,9 +358,6 @@ services: timeout: 5s retries: 15 start_period: 60s - depends_on: - vector: - condition: service_healthy environment: POSTGRES_HOST: /var/run/postgresql PGPORT: ${POSTGRES_PORT} @@ -407,36 +377,6 @@ services: "log_min_messages=fatal" ] - vector: - image: timberio/vector:0.53.0-alpine - restart: unless-stopped - volumes: - - ../files/volumes/logs/vector.yml:/etc/vector/vector.yml:ro,z - - ${DOCKER_SOCKET_LOCATION}:/var/run/docker.sock:ro,z - healthcheck: - test: - [ - "CMD", - "wget", - "--no-verbose", - "--tries=1", - "--spider", - "http://vector:9001/health" - ] - timeout: 5s - interval: 5s - retries: 5 - start_period: 10s - environment: - LOGFLARE_PUBLIC_ACCESS_TOKEN: ${LOGFLARE_PUBLIC_ACCESS_TOKEN} - command: - [ - "--config", - "/etc/vector/vector.yml" - ] - security_opt: - - "label=disable" - supavisor: image: supabase/supavisor:2.9.5 restart: unless-stopped diff --git a/blueprints/supabase/meta.json b/blueprints/supabase/meta.json index d5d2cdadf..d604c052e 100644 --- a/blueprints/supabase/meta.json +++ b/blueprints/supabase/meta.json @@ -1,7 +1,7 @@ { "id": "supabase", "name": "SupaBase", - "version": "2026.06.03 / dokploy >= 0.22.5", + "version": "2026.08.03 / dokploy >= 0.22.5", "description": "The open source Firebase alternative. Supabase gives you a dedicated Postgres database to build your web, mobile, and AI applications. This require at least version 0.22.5 of dokploy.", "links": { "github": "https://github.com/supabase/supabase", diff --git a/blueprints/supabase/template.toml b/blueprints/supabase/template.toml index 0e76b7dd4..bf64558a6 100644 --- a/blueprints/supabase/template.toml +++ b/blueprints/supabase/template.toml @@ -2,8 +2,6 @@ main_domain = "${domain}" postgres_password = "${password:32}" dashboard_password = "${password:32}" -logflare_public_access_token = "${password:32}" -logflare_private_access_token = "${password:32}" pg_meta_crypto_key = "${password:32}" s3_protocol_access_key_id = "${password:24}" s3_protocol_access_key_secret = "${password:48}" @@ -59,8 +57,6 @@ env = [ 'SECRET_KEY_BASE=${secret_key_base}', 'VAULT_ENC_KEY=${vault_enc_key}', 'PG_META_CRYPTO_KEY=${pg_meta_crypto_key}', -'LOGFLARE_PUBLIC_ACCESS_TOKEN=${logflare_public_access_token}', -'LOGFLARE_PRIVATE_ACCESS_TOKEN=${logflare_private_access_token}', '', '', '############', @@ -164,19 +160,7 @@ env = [ '# Functions - Configuration for Functions', '############', '# NOTE: VERIFY_JWT applies to all functions. Per-function VERIFY_JWT is not supported yet.', -'FUNCTIONS_VERIFY_JWT=false', -'', -'', -'############', -'# Logs - Configuration for Logflare', -'############', -'', -'# Docker socket location - this value will differ depending on your OS', -'DOCKER_SOCKET_LOCATION=/var/run/docker.sock', -'', -'# Google Cloud Project details', -'GOOGLE_PROJECT_ID=GOOGLE_PROJECT_ID', -'GOOGLE_PROJECT_NUMBER=GOOGLE_PROJECT_NUMBER'] +'FUNCTIONS_VERIFY_JWT=false'] [[config.mounts]] filePath = "/volumes/api/kong-entrypoint.sh" @@ -1007,278 +991,6 @@ Deno.serve(async (req: Request) => { }) """ -[[config.mounts]] -filePath = "/volumes/logs/vector.yml" -content = """api: - enabled: true - address: 0.0.0.0:9001 - -sources: - docker_host: - type: docker_logs - -transforms: - project_logs: - type: remap - inputs: - - docker_host - source: |- - .project = "default" - .event_message = del(.message) - compose_service, label_err = get(.label, ["com.docker.compose.service"]) - if label_err != null || compose_service == null { - abort - } - compose_service = to_string!(compose_service) - if compose_service == "vector" { - abort - } - .appname = "supabase-" + compose_service - del(.container_created_at) - del(.container_id) - del(.source_type) - del(.stream) - del(.label) - del(.image) - del(.host) - del(.stream) - router: - type: route - inputs: - - project_logs - route: - kong: '.appname == "supabase-kong" || .appname == "supabase-envoy"' - auth: '.appname == "supabase-auth"' - rest: '.appname == "supabase-rest"' - realtime: '.appname == "supabase-realtime"' - storage: '.appname == "supabase-storage"' - functions: '.appname == "supabase-functions"' - db: '.appname == "supabase-db"' - # Ignores non nginx errors since they are related with kong booting up - kong_logs: - type: remap - inputs: - - router.kong - source: |- - req, err = parse_nginx_log(.event_message, "combined") - if err == null { - .timestamp = req.timestamp - .metadata.request.headers.referer = req.referer - .metadata.request.headers.user_agent = req.agent - .metadata.request.headers.cf_connecting_ip = req.client - .metadata.response.status_code = req.status - url, split_err = split(req.request, " ") - if split_err == null { - .metadata.request.method = url[0] - .metadata.request.path = url[1] - .metadata.request.protocol = url[2] - } - } - if err != null { - abort - } - kong_err: - type: remap - inputs: - - router.kong - source: |- - .metadata.request.method = "GET" - .metadata.response.status_code = 200 - parsed, err = parse_nginx_log(.event_message, "error") - if err == null { - .timestamp = parsed.timestamp - .severity = parsed.severity - .metadata.request.host = parsed.host - .metadata.request.headers.cf_connecting_ip = parsed.client - url, err = split(parsed.request, " ") - if err == null { - .metadata.request.method = url[0] - .metadata.request.path = url[1] - .metadata.request.protocol = url[2] - } - } - if err != null { - abort - } - # Gotrue logs are structured json strings which frontend parses directly. But we keep metadata for consistency. - auth_logs: - type: remap - inputs: - - router.auth - source: |- - parsed, err = parse_json(.event_message) - if err == null { - .metadata.timestamp = parsed.time - .metadata = merge!(.metadata, parsed) - } - # PostgREST logs are structured so we separate timestamp from message using regex - rest_logs: - type: remap - inputs: - - router.rest - source: |- - parsed, err = parse_regex(.event_message, r'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%0ACl3SQQDxQ#p#DBu?dG%k z3Y9W@WV@0rcbQ}g`JLm+%R2AYt_S3KcU!(=#&VoyaMIYo?odx~DLK?TLy{bUEMx^i zz1A4QVI<@e&K+pFM#Nc<6zM?tE)?FEeKvnPuVIuPa9p_9C_^MN=b73UCWkVyB8 zad1TKBNh{L7ZDObtx>6LQsa=P%-7=8ZMW+>=zp(NTXQojnUu1MsJ6hzcEHeGKs%@` zN�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

^hAn%c3$d%xZ)@ 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 literal 0 HcmV?d00001 From bbd0817b9db31c16377113248c02916441b1d1c2 Mon Sep 17 00:00:00 2001 From: Huseyin Cenik <127469334+huseyincenik@users.noreply.github.com> Date: Sun, 9 Aug 2026 09:01:50 +0300 Subject: [PATCH 78/80] Add Langfuse template (#1072) * Add Langfuse template Open-source LLM engineering platform: tracing, prompt management, evaluation and cost/usage analytics for LLM apps. Stack: langfuse-web + langfuse-worker (image tag 4), Postgres 17, ClickHouse 25.12, Redis 7, MinIO. Adapted from the official docker-compose.yml at https://github.com/langfuse/langfuse/blob/main/docker-compose.yml, with all secrets generated via Dokploy's template.toml helpers instead of the upstream hardcoded defaults. MinIO gets its own domain because Langfuse's web UI issues presigned S3 URLs that the browser fetches directly (media attached to traces), so that endpoint has to be reachable from outside the internal network. Validated locally with build-scripts/validate-template.ts, validate-docker-compose.ts and generate-meta.js --check (all pass), plus `docker compose config` with sample secrets to confirm the langfuse-web override of LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT resolves correctly. Not yet deployed on a live Dokploy instance. * fix: bind langfuse-web to 0.0.0.0 so Traefik can reach it Docker sets HOSTNAME to the container id, and Next.js standalone binds to it, which resolves to a single network's IP. With Dokploy's proxy network attached as a second network, Traefik got connection refused (502). Setting HOSTNAME=0.0.0.0 fixes routing; verified with a live deploy. Co-Authored-By: Claude Fable 5 --------- Co-authored-by: Mauricio Siu Co-authored-by: Claude Fable 5 --- blueprints/langfuse/docker-compose.yml | 139 +++++++++++++++++++++++++ blueprints/langfuse/instructions.md | 26 +++++ blueprints/langfuse/langfuse.svg | 8 ++ blueprints/langfuse/meta.json | 13 +++ blueprints/langfuse/template.toml | 36 +++++++ 5 files changed, 222 insertions(+) create mode 100644 blueprints/langfuse/docker-compose.yml create mode 100644 blueprints/langfuse/instructions.md create mode 100644 blueprints/langfuse/langfuse.svg create mode 100644 blueprints/langfuse/meta.json create mode 100644 blueprints/langfuse/template.toml diff --git a/blueprints/langfuse/docker-compose.yml b/blueprints/langfuse/docker-compose.yml new file mode 100644 index 000000000..81dcdf1fa --- /dev/null +++ b/blueprints/langfuse/docker-compose.yml @@ -0,0 +1,139 @@ +# Langfuse self-hosted, adapted from the official docker-compose.yml: +# https://github.com/langfuse/langfuse/blob/main/docker-compose.yml +# +# MinIO is exposed on its own domain because Langfuse's web UI generates +# presigned S3 URLs that the browser fetches directly (e.g. media in +# traces) - it cannot resolve the internal "minio" hostname. + +services: + postgres: + image: docker.io/postgres:17 + restart: unless-stopped + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: postgres + TZ: UTC + PGTZ: UTC + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 3s + timeout: 3s + retries: 10 + volumes: + - langfuse-postgres-data:/var/lib/postgresql/data + + clickhouse: + image: docker.io/clickhouse/clickhouse-server:25.12 + restart: unless-stopped + user: "101:101" + environment: + CLICKHOUSE_DB: default + CLICKHOUSE_USER: clickhouse + CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD} + healthcheck: + test: wget --no-verbose --tries=1 --spider http://localhost:8123/ping || exit 1 + interval: 5s + timeout: 5s + retries: 10 + start_period: 1s + volumes: + - langfuse-clickhouse-data:/var/lib/clickhouse + - langfuse-clickhouse-logs:/var/log/clickhouse-server + + minio: + image: cgr.dev/chainguard/minio + restart: unless-stopped + entrypoint: sh + command: -c 'mkdir -p /data/langfuse && minio server --address ":9000" --console-address ":9001" /data' + environment: + MINIO_ROOT_USER: minio + MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD} + healthcheck: + test: ["CMD", "mc", "ready", "local"] + interval: 1s + timeout: 5s + retries: 5 + start_period: 1s + volumes: + - langfuse-minio-data:/data + + redis: + image: docker.io/redis:7 + restart: unless-stopped + command: > + --requirepass ${REDIS_AUTH} + --maxmemory-policy noeviction + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 3s + timeout: 10s + retries: 10 + volumes: + - langfuse-redis-data:/data + + langfuse-worker: + image: docker.io/langfuse/langfuse-worker:4 + restart: unless-stopped + depends_on: &langfuse-depends-on + postgres: + condition: service_healthy + minio: + condition: service_healthy + redis: + condition: service_healthy + clickhouse: + condition: service_healthy + environment: &langfuse-worker-env + NEXTAUTH_URL: ${NEXTAUTH_URL} + DATABASE_URL: postgresql://postgres:${POSTGRES_PASSWORD}@postgres:5432/postgres + SALT: ${SALT} + ENCRYPTION_KEY: ${ENCRYPTION_KEY} + TELEMETRY_ENABLED: "true" + LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES: "false" + CLICKHOUSE_MIGRATION_URL: clickhouse://clickhouse:9000 + CLICKHOUSE_URL: http://clickhouse:8123 + CLICKHOUSE_USER: clickhouse + CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD} + CLICKHOUSE_CLUSTER_ENABLED: "false" + LANGFUSE_S3_EVENT_UPLOAD_BUCKET: langfuse + LANGFUSE_S3_EVENT_UPLOAD_REGION: auto + LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID: minio + LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY: ${MINIO_ROOT_PASSWORD} + LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT: http://minio:9000 + LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE: "true" + LANGFUSE_S3_EVENT_UPLOAD_PREFIX: events/ + LANGFUSE_S3_MEDIA_UPLOAD_BUCKET: langfuse + LANGFUSE_S3_MEDIA_UPLOAD_REGION: auto + LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID: minio + LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY: ${MINIO_ROOT_PASSWORD} + LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT: http://minio:9000 + LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE: "true" + LANGFUSE_S3_MEDIA_UPLOAD_PREFIX: media/ + LANGFUSE_S3_BATCH_EXPORT_ENABLED: "false" + REDIS_HOST: redis + REDIS_PORT: "6379" + REDIS_AUTH: ${REDIS_AUTH} + REDIS_TLS_ENABLED: "false" + + langfuse-web: + image: docker.io/langfuse/langfuse:4 + restart: unless-stopped + depends_on: *langfuse-depends-on + environment: + <<: *langfuse-worker-env + # Bind Next.js to all interfaces: Docker sets HOSTNAME to the container id, + # which resolves to a single network's IP - with Dokploy's extra proxy + # network attached, Traefik would get connection refused (502) otherwise. + HOSTNAME: "0.0.0.0" + NEXTAUTH_SECRET: ${NEXTAUTH_SECRET} + # Overrides the worker's internal endpoint above: the browser resolves + # presigned media URLs itself, so this one must be publicly reachable. + LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT: ${LANGFUSE_S3_MEDIA_PUBLIC_ENDPOINT} + +volumes: + langfuse-postgres-data: + langfuse-clickhouse-data: + langfuse-clickhouse-logs: + langfuse-minio-data: + langfuse-redis-data: diff --git a/blueprints/langfuse/instructions.md b/blueprints/langfuse/instructions.md new file mode 100644 index 000000000..5abefd188 --- /dev/null +++ b/blueprints/langfuse/instructions.md @@ -0,0 +1,26 @@ +# Langfuse + +Self-hosted Langfuse: open-source LLM tracing, prompt management, evaluation and cost/usage analytics, based on the official `docker-compose.yml`. + +## Two domains + +This template exposes two domains: + +- The **main domain** serves the Langfuse web app/API (`langfuse-web`, port 3000). +- The **second domain** exposes MinIO's S3 API (port 9000) directly. Langfuse's web UI generates presigned URLs for media (e.g. images attached to traces) that the *browser* fetches straight from MinIO, so this endpoint has to be publicly reachable — it isn't just an internal implementation detail. + +## First boot + +`langfuse-web` and `langfuse-worker` wait on Postgres, ClickHouse, Redis and MinIO to report healthy, then run their own migrations on startup. Once `langfuse-web` is up, open its domain and create the first user — they become the owner of the initial organization. + +## After enabling HTTPS + +By default `NEXTAUTH_URL` and `LANGFUSE_S3_MEDIA_PUBLIC_ENDPOINT` are set to `http://`. If you enable HTTPS on either domain (recommended for production), update the matching environment variable to `https://` and redeploy, otherwise auth callbacks and media URLs will point at the wrong scheme. + +## Licensing + +The core of Langfuse (tracing, prompt management, evaluation, playground) is open-source (MIT) and fully usable self-hosted without a license key. Some enterprise-only features (e.g. SSO/SAML, fine-grained RBAC) live under a separate commercial license — see [self-hosting license docs](https://langfuse.com/self-hosting/license-key). This template does not configure a license key. + +## Versioning + +Images are pinned to the `4` major tag (`langfuse/langfuse:4`, `langfuse/langfuse-worker:4`), matching the upstream compose file. To pin to a specific release instead, replace `4` with a version tag from [Docker Hub](https://hub.docker.com/r/langfuse/langfuse/tags). diff --git a/blueprints/langfuse/langfuse.svg b/blueprints/langfuse/langfuse.svg new file mode 100644 index 000000000..d56d582be --- /dev/null +++ b/blueprints/langfuse/langfuse.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/blueprints/langfuse/meta.json b/blueprints/langfuse/meta.json new file mode 100644 index 000000000..7b39595eb --- /dev/null +++ b/blueprints/langfuse/meta.json @@ -0,0 +1,13 @@ +{ + "id": "langfuse", + "name": "Langfuse", + "version": "4", + "description": "Langfuse is an open-source LLM engineering platform for tracing, prompt management, evaluation, and cost/usage analytics of LLM applications.", + "logo": "langfuse.svg", + "links": { + "github": "https://github.com/langfuse/langfuse", + "website": "https://langfuse.com/", + "docs": "https://langfuse.com/self-hosting" + }, + "tags": ["ai", "llm", "observability", "analytics", "monitoring"] +} diff --git a/blueprints/langfuse/template.toml b/blueprints/langfuse/template.toml new file mode 100644 index 000000000..9d388a88b --- /dev/null +++ b/blueprints/langfuse/template.toml @@ -0,0 +1,36 @@ +[variables] +main_domain = "${domain}" +minio_domain = "${domain}" +postgres_password = "${password:32}" +clickhouse_password = "${password:32}" +minio_root_password = "${password:32}" +redis_auth = "${password:32}" +nextauth_secret = "${password:32}" +salt = "${password:32}" +encryption_key = "${hash:64}" +nextauth_url = "http://${main_domain}" +minio_public_endpoint = "http://${minio_domain}" + +[config] +[[config.domains]] +serviceName = "langfuse-web" +port = 3000 +host = "${main_domain}" + +[[config.domains]] +serviceName = "minio" +port = 9000 +host = "${minio_domain}" + +[config.env] +POSTGRES_PASSWORD = "${postgres_password}" +CLICKHOUSE_PASSWORD = "${clickhouse_password}" +MINIO_ROOT_PASSWORD = "${minio_root_password}" +REDIS_AUTH = "${redis_auth}" +NEXTAUTH_SECRET = "${nextauth_secret}" +SALT = "${salt}" +ENCRYPTION_KEY = "${encryption_key}" +# Set to https:// once HTTPS is enabled on the domain +NEXTAUTH_URL = "${nextauth_url}" +# Set to https:// once HTTPS is enabled on the domain +LANGFUSE_S3_MEDIA_PUBLIC_ENDPOINT = "${minio_public_endpoint}" From a7339677c929e4dbb1bb21673a991312bd961c1c Mon Sep 17 00:00:00 2001 From: Anukul Pandey <62092256+anukulpandey@users.noreply.github.com> Date: Sun, 9 Aug 2026 11:36:41 +0530 Subject: [PATCH 79/80] Reef chain (#776) * feat: reef chain bootstrap * fix: missing doc field * feat: reef rpc * fix: meta * feat: toml changes * fix: var in rpc * feat: minimal rpc * feat: default envs * bootnode toml * feat: icon * feat: icon * feat: old icons * Update template.toml * Update template.toml * fix: service name * feat: reef-faucet * feat: reef-keygen * feat: meta added * fix: node_name unused var * fix: service name * feat: add platform * feat: dokploy compatible * force: platform * feat: linux * feat: linux-2 * feat: eth-rpc * fix: image * fix: variables * feat: bootnode update * feat: spec generator running * feat: validator * fix: meta.json for validator * fix: exec binary for reef-validator * feat: validator start * feat: remove container name * fix: env var for eth-rpc * feat: custom spec generator * fix: image for reef-spec * feat: dev cluster * fix: name * fix: meta.json * fix: wget * feat: reef-* removed * feat: reef-dev cluster * Update docker-compose.yml * feat: close rpc ports for reef-dev-cluster * feat: removed rpc ports from validator * feat: rpc node seperate * feat: rpc node * feat: reef-rpc * fix: meta * feat: removed version from docker-compose bootstrap * feat: expose 9944 for dev cluster * fix: start-rpc script * feat: eth-rpc rpc host * fix: flag for eth-rpc external rpc connections * feat: cors for all * fix: meta.json missing ] * feat:process meta * feat: removed container_name from docker-compose * feat: port and p2p port must be string for bootnode * feat: ports for reef dev cluster * fix: ports for reef-dev-cluster * feat: all ports to strings * Wire Reef templates to external scripts repo * fix: align reef templates with dokploy rules * chore: process meta.json * fix: route reef spec generator via subdomain * feat: make reef dev cluster self-contained * fix: make reef templates amd64 compatible * chore: update reef images to latest tags * fix: make reef dev cluster bootstrap portable * fix: bootstrap reef dev cluster from pinned assets * fix: make reef dev cluster zero-config * fix: pin reef dev cluster bootstrap * feat: add eth rpc to reef dev cluster * fix: pin reef dev cluster eth rpc assets * fix: align reef traefik routing * fix: pin reef dev cluster traefik updates * feat: add reef faucet frontend * fix: pin reef faucet frontend assets * fix: make reef dev cluster eth rpc dokploy-safe * fix: gate reef dev cluster eth rpc on health * fix: restore reef dev cluster ws rpc * fix: pin reef dev cluster ws assets * fix: proxy reef dev cluster ws host headers * fix: pin reef dev cluster host proxy assets * fix: raise reef faucet drip amount * fix: pin reef faucet amount assets * Update docker-compose.yml * chore: scope Reef PR to one-click viable templates (dev-cluster + keygen) Remove reef-bootnode, reef-rpc, reef-validator, reef-spec-gen, reef-eth-rpc and reef-faucet: their defaults depend on external infrastructure (*.reef.host / 72.60.35.83) that is offline, or on manually wiring values between deployments, so they cannot work as one-click templates. reef-dev-cluster and reef-keygen are fully self-contained and verified working. Co-Authored-By: Claude Fable 5 --------- Co-authored-by: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Co-authored-by: Mauricio Siu Co-authored-by: Mauricio Siu Co-authored-by: Claude Fable 5 --- .../reef-dev-cluster/docker-compose.yml | 129 ++++ .../reef-dev-cluster/faucet/package.json | 13 + blueprints/reef-dev-cluster/faucet/server.js | 696 ++++++++++++++++++ blueprints/reef-dev-cluster/meta.json | 19 + blueprints/reef-dev-cluster/reef.svg | 68 ++ blueprints/reef-dev-cluster/run-cluster.sh | 549 ++++++++++++++ blueprints/reef-dev-cluster/template.toml | 51 ++ blueprints/reef-keygen/docker-compose.yml | 41 ++ blueprints/reef-keygen/meta.json | 18 + blueprints/reef-keygen/reef.svg | 68 ++ blueprints/reef-keygen/template.toml | 11 + 11 files changed, 1663 insertions(+) create mode 100644 blueprints/reef-dev-cluster/docker-compose.yml create mode 100644 blueprints/reef-dev-cluster/faucet/package.json create mode 100644 blueprints/reef-dev-cluster/faucet/server.js create mode 100644 blueprints/reef-dev-cluster/meta.json create mode 100644 blueprints/reef-dev-cluster/reef.svg create mode 100755 blueprints/reef-dev-cluster/run-cluster.sh create mode 100644 blueprints/reef-dev-cluster/template.toml create mode 100644 blueprints/reef-keygen/docker-compose.yml create mode 100644 blueprints/reef-keygen/meta.json create mode 100644 blueprints/reef-keygen/reef.svg create mode 100644 blueprints/reef-keygen/template.toml diff --git a/blueprints/reef-dev-cluster/docker-compose.yml b/blueprints/reef-dev-cluster/docker-compose.yml new file mode 100644 index 000000000..49b726af8 --- /dev/null +++ b/blueprints/reef-dev-cluster/docker-compose.yml @@ -0,0 +1,129 @@ +services: + reef-dev-cluster: + image: anukulpandey/reef-chain-node:latest + restart: unless-stopped + working_dir: /workspace + + environment: + v1sec: "${v1sec:-}" + v2sec: "${v2sec:-}" + v3sec: "${v3sec:-}" + faucetsec: "${faucetsec:-}" + DEFAULT_AMOUNT: "${DEFAULT_AMOUNT:-2000}" + MAX_AMOUNT: "${MAX_AMOUNT:-2000}" + + volumes: + - reef-dev-cluster-state:/workspace/state + - reef-dev-cluster-download:/workspace/download + + expose: + - "30335" + - "30333" + - "30334" + - "30336" + - "8001" + - "9944" + - "9945" + - "8080" + + healthcheck: + test: + - CMD-SHELL + - bash -lc 'exec 3<>/dev/tcp/127.0.0.1/9944' + interval: 10s + timeout: 5s + retries: 30 + start_period: 300s + + entrypoint: /bin/sh + + command: + - -c + - | + set -e + + export DEBIAN_FRONTEND=noninteractive + + echo '📦 Installing required tools...' + + apt-get update + + apt-get install -y \ + wget \ + curl \ + python3 \ + ca-certificates \ + gnupg \ + nginx + + mkdir -p \ + /etc/apt/keyrings \ + /workspace \ + /workspace/state \ + /workspace/download + + echo '🔑 Configuring NodeSource repository...' + + curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key \ + | gpg --batch --yes --dearmor -o /etc/apt/keyrings/nodesource.gpg + + echo 'deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main' \ + > /etc/apt/sources.list.d/nodesource.list + + apt-get update + + apt-get install -y nodejs + + echo '📥 Downloading cluster bootstrap...' + + wget -O /workspace/run-cluster.sh \ + https://raw.githubusercontent.com/anukulpandey/dokploy-templates/d8a82420dad013a3eb7e5d8afc0b9f2700a0045a/blueprints/reef-dev-cluster/run-cluster.sh + + echo '🔧 Making script executable...' + + chmod +x /workspace/run-cluster.sh + + echo '🚀 Starting self-contained network...' + + exec env TEMPLATE_ASSET_REF=d8a82420dad013a3eb7e5d8afc0b9f2700a0045a \ + /workspace/run-cluster.sh + + reef-dev-cluster-eth-rpc: + image: anukulpandey/reef-chain-eth-rpc:latest + restart: unless-stopped + working_dir: /workspace + + depends_on: + reef-dev-cluster: + condition: service_healthy + + expose: + - "8545" + + environment: + NODE_RPC_URL: ws://reef-dev-cluster:9945 + PORT: "8545" + + healthcheck: + test: + - CMD-SHELL + - bash -lc 'exec 3<>/dev/tcp/127.0.0.1/8545' + interval: 10s + timeout: 5s + retries: 12 + start_period: 20s + + entrypoint: /bin/sh + + command: + - -c + - | + exec eth-rpc \ + --node-rpc-url "$$NODE_RPC_URL" \ + --rpc-port "$$PORT" \ + --rpc-external \ + --rpc-cors all + +volumes: + reef-dev-cluster-state: + reef-dev-cluster-download: diff --git a/blueprints/reef-dev-cluster/faucet/package.json b/blueprints/reef-dev-cluster/faucet/package.json new file mode 100644 index 000000000..fd45ff3d7 --- /dev/null +++ b/blueprints/reef-dev-cluster/faucet/package.json @@ -0,0 +1,13 @@ +{ + "name": "reef-dev-cluster-faucet", + "version": "1.0.0", + "private": true, + "description": "HTTP faucet for a self-contained Reef dev cluster", + "main": "server.js", + "scripts": { + "start": "node server.js" + }, + "dependencies": { + "@polkadot/api": "^16.4.8" + } +} diff --git a/blueprints/reef-dev-cluster/faucet/server.js b/blueprints/reef-dev-cluster/faucet/server.js new file mode 100644 index 000000000..15d1ce32b --- /dev/null +++ b/blueprints/reef-dev-cluster/faucet/server.js @@ -0,0 +1,696 @@ +#!/usr/bin/env node + +const http = require("node:http"); +const { ApiPromise, Keyring, WsProvider } = require("@polkadot/api"); + +const config = { + wsEndpoint: process.env.WS_ENDPOINT || "ws://127.0.0.1:9944", + evmRpcUrl: process.env.EVM_RPC_URL || "", + faucetSeed: process.env.FAUCET_SEED || "", + port: Number.parseInt(process.env.PORT || "8080", 10), + defaultAmount: String(process.env.DEFAULT_AMOUNT || "2000"), + maxAmount: String(process.env.MAX_AMOUNT || "2000"), + serviceName: process.env.SERVICE_NAME || "reef-faucet", +}; + +if (!config.faucetSeed) { + throw new Error("FAUCET_SEED is required"); +} + +let contextPromise = null; +let requestQueue = Promise.resolve(); + +function normalizeEvmAddress(value) { + const normalized = String(value || "").trim(); + if (!/^0x[0-9a-fA-F]{40}$/.test(normalized)) { + throw new Error(`Invalid EVM address: ${value}`); + } + return normalized; +} + +function parseUnits(value, decimals) { + const input = String(value).trim(); + if (!/^\d+(\.\d+)?$/.test(input)) { + throw new Error(`Invalid decimal amount: ${value}`); + } + + const [whole, fraction = ""] = input.split("."); + if (fraction.length > decimals) { + throw new Error(`Too many decimal places in ${value}`); + } + + const wholeUnits = BigInt(whole) * 10n ** BigInt(decimals); + const fractionUnits = fraction ? BigInt(fraction.padEnd(decimals, "0")) : 0n; + return wholeUnits + fractionUnits; +} + +function formatUnits(value, decimals) { + const negative = value < 0n; + const abs = negative ? -value : value; + const base = 10n ** BigInt(decimals); + const whole = abs / base; + const fraction = abs % base; + + if (fraction === 0n) { + return `${negative ? "-" : ""}${whole.toString()}`; + } + + return `${negative ? "-" : ""}${whole.toString()}.${fraction + .toString() + .padStart(decimals, "0") + .replace(/0+$/, "")}`; +} + +function fallbackAccountFromEvm(evmAddress) { + return `0x${evmAddress.slice(2).toLowerCase()}${"ee".repeat(12)}`; +} + +function decodeDispatchError(api, dispatchError) { + if (dispatchError && dispatchError.isModule) { + const decoded = api.registry.findMetaError(dispatchError.asModule); + return `${decoded.section}.${decoded.name}: ${decoded.docs.join(" ")}`; + } + + return dispatchError && dispatchError.toString ? dispatchError.toString() : String(dispatchError); +} + +async function signAndWait(api, tx, signer) { + return new Promise(async (resolve, reject) => { + let unsubscribe = null; + + try { + unsubscribe = await tx.signAndSend(signer, ({ dispatchError, events, status, txHash }) => { + if (dispatchError) { + if (unsubscribe) { + unsubscribe(); + } + reject(new Error(decodeDispatchError(api, dispatchError))); + return; + } + + if (status.isFinalized) { + if (unsubscribe) { + unsubscribe(); + } + resolve({ + txHash: txHash.toHex(), + finalizedBlock: status.asFinalized.toHex(), + eventCount: events.length, + }); + } + }); + } catch (error) { + if (unsubscribe) { + unsubscribe(); + } + reject(error); + } + }); +} + +async function maybeClaimDefaultAccount(api, signer) { + const storage = api.query && api.query.evmAccounts && api.query.evmAccounts.evmAddresses; + const extrinsic = api.tx && api.tx.evmAccounts && api.tx.evmAccounts.claimDefaultAccount; + + if (!storage || !extrinsic) { + return null; + } + + const current = await storage(signer.address); + if (!current.isEmpty) { + return current.toString(); + } + + await signAndWait(api, extrinsic(), signer); + return (await storage(signer.address)).toString(); +} + +function buildReviveTransfer(api, signerAddress, targetEvm, amountUnits) { + const reviveTransfer = api.tx && api.tx.revive && api.tx.revive.transfer; + if (!reviveTransfer) { + return null; + } + + const metaArgs = reviveTransfer.meta.toJSON().args || []; + const names = metaArgs.map((arg) => String((arg && arg.name) || "").toLowerCase()); + const types = metaArgs.map((arg) => JSON.stringify((arg && arg.type) || "").toLowerCase()); + const haystack = `${names.join(",")}|${types.join(",")}`; + + if (metaArgs.length === 2 && haystack.includes("h160")) { + return { + strategy: "revive.transfer(H160, Balance)", + tx: reviveTransfer(targetEvm, amountUnits), + }; + } + + if (metaArgs.length === 3 && haystack.includes("accountid") && haystack.includes("h160")) { + return { + strategy: "revive.transfer(AccountId, H160, Balance)", + tx: reviveTransfer(signerAddress, targetEvm, amountUnits), + }; + } + + return { + strategy: "revive.transfer(fallback call signature)", + tx: reviveTransfer(targetEvm, amountUnits), + }; +} + +async function getContext() { + if (!contextPromise) { + contextPromise = (async () => { + const provider = new WsProvider(config.wsEndpoint); + const api = await ApiPromise.create({ provider }); + const keyring = new Keyring({ type: "sr25519" }); + const sender = keyring.addFromUri(config.faucetSeed); + const nativeDecimals = api.registry.chainDecimals[0] || 12; + const tokenSymbol = api.registry.chainTokens[0] || "REEF"; + + return { api, nativeDecimals, provider, sender, tokenSymbol }; + })(); + } + + return contextPromise; +} + +function jsonResponse(response, statusCode, body) { + response.writeHead(statusCode, { "Content-Type": "application/json" }); + response.end(JSON.stringify(body)); +} + +function htmlResponse(response, statusCode, body) { + response.writeHead(statusCode, { "Content-Type": "text/html; charset=utf-8" }); + response.end(body); +} + +function escapeHtml(value) { + return String(value) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +function wantsHtml(request) { + const accept = String(request.headers.accept || ""); + return accept.includes("text/html"); +} + +function buildInfoPayload() { + return { + service: config.serviceName, + status: "ok", + defaults: { + defaultAmount: config.defaultAmount, + maxAmount: config.maxAmount, + }, + upstream: { + wsEndpoint: config.wsEndpoint, + evmRpcUrl: config.evmRpcUrl || null, + }, + endpoints: { + health: { + method: "GET", + path: "/health", + }, + drip: { + method: "POST", + path: "/drip", + body: { + to: "0x0000000000000000000000000000000000000000", + amount: config.defaultAmount, + }, + }, + }, + }; +} + +function buildFrontendHtml() { + const bootPayload = JSON.stringify(buildInfoPayload()); + const title = escapeHtml(config.serviceName); + const defaultAmount = escapeHtml(config.defaultAmount); + const maxAmount = escapeHtml(config.maxAmount); + const wsEndpoint = escapeHtml(config.wsEndpoint); + const evmRpcUrl = escapeHtml(config.evmRpcUrl || "Not configured"); + + return ` + + + + + ${title} + + + +
+
+
+

Reef Faucet

+

${title}

+

Send test REEF to any EVM address without reaching for curl first.

+
+ +
+
+ Default amount + ${defaultAmount} REEF +
+
+ Max amount + ${maxAmount} REEF +
+
+ WS upstream + ${wsEndpoint} +
+
+ EVM upstream + ${evmRpcUrl} +
+
+ +
+ + + + +
+ + +
+
+ +
+
Ready.
+
+ + +
+
+ + + +`; +} + +async function parseRequestBody(request) { + const chunks = []; + for await (const chunk of request) { + chunks.push(chunk); + } + + if (chunks.length === 0) { + return {}; + } + + return JSON.parse(Buffer.concat(chunks).toString("utf8")); +} + +async function executeDrip(targetEvm, amountText) { + const { api, nativeDecimals, sender, tokenSymbol } = await getContext(); + const amountUnits = parseUnits(amountText, nativeDecimals); + const maxUnits = parseUnits(config.maxAmount, nativeDecimals); + + if (amountUnits <= 0n) { + throw new Error("Amount must be greater than zero"); + } + + if (amountUnits > maxUnits) { + throw new Error(`Amount exceeds MAX_AMOUNT (${config.maxAmount})`); + } + + await maybeClaimDefaultAccount(api, sender); + + const senderBefore = (await api.query.system.account(sender.address)).data.free.toBigInt(); + + let strategy = ""; + let result = null; + + try { + const revive = buildReviveTransfer(api, sender.address, targetEvm, amountUnits); + if (!revive) { + throw new Error("revive.transfer unavailable"); + } + strategy = revive.strategy; + result = await signAndWait(api, revive.tx, sender); + } catch (error) { + strategy = "balances.transferAllowDeath(AccountId32 fallback)"; + result = await signAndWait( + api, + api.tx.balances.transferAllowDeath(fallbackAccountFromEvm(targetEvm), amountUnits), + sender + ); + } + + const senderAfter = (await api.query.system.account(sender.address)).data.free.toBigInt(); + + return { + amount: amountText, + strategy, + tokenSymbol, + txHash: result.txHash, + finalizedBlock: result.finalizedBlock, + eventCount: result.eventCount, + sender: sender.address, + senderNativeBefore: formatUnits(senderBefore, nativeDecimals), + senderNativeAfter: formatUnits(senderAfter, nativeDecimals), + }; +} + +function enqueue(task) { + const current = requestQueue.then(task, task); + requestQueue = current.catch(() => undefined); + return current; +} + +const server = http.createServer(async (request, response) => { + try { + if (request.method === "GET" && request.url === "/favicon.ico") { + response.writeHead(204); + response.end(); + return; + } + + if (request.method === "GET" && request.url === "/") { + if (wantsHtml(request)) { + htmlResponse(response, 200, buildFrontendHtml()); + } else { + jsonResponse(response, 200, buildInfoPayload()); + } + return; + } + + if (request.method === "GET" && request.url === "/health") { + jsonResponse(response, 200, { status: "ok" }); + return; + } + + if (request.method === "POST" && request.url === "/drip") { + const payload = await parseRequestBody(request); + const targetEvm = normalizeEvmAddress(payload.to); + const amount = payload.amount ? String(payload.amount) : config.defaultAmount; + const result = await enqueue(() => executeDrip(targetEvm, amount)); + jsonResponse(response, 200, result); + return; + } + + jsonResponse(response, 404, { error: "Not found" }); + } catch (error) { + jsonResponse(response, 400, { error: error.message }); + } +}); + +server.listen(config.port, "0.0.0.0", () => { + console.log(`reef dev-cluster faucet listening on ${config.port}`); +}); diff --git a/blueprints/reef-dev-cluster/meta.json b/blueprints/reef-dev-cluster/meta.json new file mode 100644 index 000000000..05687870b --- /dev/null +++ b/blueprints/reef-dev-cluster/meta.json @@ -0,0 +1,19 @@ +{ + "id": "reef-dev-cluster", + "name": "Reef Chain - Dev Cluster", + "version": "1", + "description": "Self-contained Reef dev cluster with bootnode, validators, spec server, substrate RPC, EVM RPC, and faucet", + "links": { + "github": "https://github.com/reef-chain/chain-upgrade", + "website": "https://hub.docker.com/repository/docker/anukulpandey/reef-chain-node", + "docs": "https://docs.reef.io/" + }, + "logo": "reef.svg", + "tags": [ + "reef-chain", + "dev-cluster", + "self-hosted", + "evm", + "faucet" + ] +} diff --git a/blueprints/reef-dev-cluster/reef.svg b/blueprints/reef-dev-cluster/reef.svg new file mode 100644 index 000000000..598d7930a --- /dev/null +++ b/blueprints/reef-dev-cluster/reef.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + diff --git a/blueprints/reef-dev-cluster/run-cluster.sh b/blueprints/reef-dev-cluster/run-cluster.sh new file mode 100755 index 000000000..4dca6dc68 --- /dev/null +++ b/blueprints/reef-dev-cluster/run-cluster.sh @@ -0,0 +1,549 @@ +#!/usr/bin/env bash + +set -euo pipefail + +NODE_BIN="${NODE_BIN:-reef-node}" +CHAIN_TEMPLATE="${CHAIN_TEMPLATE:-testnet-new}" +WORK_DIR="${WORK_DIR:-/tmp/reef-dev-cluster}" +STATE_DIR="${STATE_DIR:-/workspace/state}" +SEED_DIR="${SEED_DIR:-$STATE_DIR/seeds}" +NODE_KEY_DIR="${NODE_KEY_DIR:-$STATE_DIR/node-keys}" +OUTPUT_DIR="${OUTPUT_DIR:-/workspace/download}" +PLAIN_SPEC="${PLAIN_SPEC:-$WORK_DIR/local-chain-spec.json}" +UPDATED_SPEC="${UPDATED_SPEC:-$WORK_DIR/local-chain-spec-updated.json}" +SPEC_FILE="${SPEC_FILE:-$OUTPUT_DIR/local-chain-spec-raw.json}" +SPEC_HTTP_PORT="${SPEC_HTTP_PORT:-8001}" +BOOTNODE_P2P_PORT="${BOOTNODE_P2P_PORT:-30335}" +VALIDATOR1_P2P_PORT="${VALIDATOR1_P2P_PORT:-30333}" +VALIDATOR2_P2P_PORT="${VALIDATOR2_P2P_PORT:-30334}" +VALIDATOR3_P2P_PORT="${VALIDATOR3_P2P_PORT:-30336}" +RPC_NODE_P2P_PORT="${RPC_NODE_P2P_PORT:-30337}" +RPC_NODE_WS_PORT="${RPC_NODE_WS_PORT:-9944}" +RPC_NODE_PUBLIC_WS_PORT="${RPC_NODE_PUBLIC_WS_PORT:-9945}" +ETH_RPC_PORT="${ETH_RPC_PORT:-8545}" +FAUCET_PORT="${FAUCET_PORT:-8080}" +BOOTNODE_PROMETHEUS_PORT="${BOOTNODE_PROMETHEUS_PORT:-9615}" +VALIDATOR1_PROMETHEUS_PORT="${VALIDATOR1_PROMETHEUS_PORT:-9616}" +VALIDATOR2_PROMETHEUS_PORT="${VALIDATOR2_PROMETHEUS_PORT:-9617}" +VALIDATOR3_PROMETHEUS_PORT="${VALIDATOR3_PROMETHEUS_PORT:-9618}" +RPC_NODE_PROMETHEUS_PORT="${RPC_NODE_PROMETHEUS_PORT:-9619}" +DEFAULT_AMOUNT="${DEFAULT_AMOUNT:-2000}" +MAX_AMOUNT="${MAX_AMOUNT:-2000}" +TEMPLATE_ASSET_REF="${TEMPLATE_ASSET_REF:-reef-chain}" +TEMPLATE_ASSET_BASE_URL="${TEMPLATE_ASSET_BASE_URL:-https://raw.githubusercontent.com/anukulpandey/dokploy-templates/${TEMPLATE_ASSET_REF}/blueprints/reef-dev-cluster}" +FAUCET_DIR="${FAUCET_DIR:-/workspace/faucet}" +BOOTNODE_PEER_ID_FILE="$OUTPUT_DIR/bootnode_peer_id.txt" +BOOTNODE_COMPAT_FILE="$OUTPUT_DIR/bootnode_node_key.txt" +CLUSTER_INFO_FILE="$OUTPUT_DIR/cluster-info.json" + +PIDS=() +TAIL_PID="" + +cleanup() { + kill "$TAIL_PID" 2>/dev/null || true + if [ "${#PIDS[@]}" -gt 0 ]; then + kill "${PIDS[@]}" 2>/dev/null || true + fi +} + +trap cleanup EXIT + +trim_whitespace() { + printf '%s' "$1" | tr -d '[:space:]' +} + +canonicalize_seed() { + local name="$1" + local value + value=$(trim_whitespace "${2:-}") + + if [ -z "$value" ]; then + printf '\n' + return 0 + fi + + if [[ "$value" =~ ^0x[0-9A-Fa-f]{64}$ ]]; then + printf '0x%s\n' "$(printf '%s' "${value#0x}" | tr '[:upper:]' '[:lower:]')" + return 0 + fi + + if [[ "$value" =~ ^[0-9A-Fa-f]{64}$ ]]; then + printf '0x%s\n' "$(printf '%s' "$value" | tr '[:upper:]' '[:lower:]')" + return 0 + fi + + echo "Invalid seed for $name. Expected 64 hex bytes with optional 0x prefix." >&2 + exit 1 +} + +write_secret_file() { + local file="$1" + local value="$2" + + mkdir -p "$(dirname "$file")" + printf '%s\n' "$value" > "$file" + chmod 600 "$file" +} + +generate_seed() { + printf '0x%s\n' "$(od -An -N32 -tx1 /dev/urandom | tr -d ' \n' | tr '[:upper:]' '[:lower:]')" +} + +resolve_seed() { + local env_name="$1" + local file_name="$2" + local file="$SEED_DIR/$file_name" + local raw_value normalized source + + raw_value=$(trim_whitespace "${!env_name:-}") + if [ -n "$raw_value" ]; then + normalized=$(canonicalize_seed "$env_name" "$raw_value") + source="env" + write_secret_file "$file" "$normalized" + elif [ -f "$file" ]; then + normalized=$(canonicalize_seed "$env_name" "$(cat "$file")") + source="persisted" + write_secret_file "$file" "$normalized" + else + normalized=$(generate_seed) + source="generated" + write_secret_file "$file" "$normalized" + fi + + printf '%s|%s\n' "$normalized" "$source" +} + +resolve_optional_seed() { + local env_name="$1" + local file_name="$2" + local file="$SEED_DIR/$file_name" + local raw_value normalized source + + raw_value=$(trim_whitespace "${!env_name:-}") + if [ -n "$raw_value" ]; then + normalized=$(canonicalize_seed "$env_name" "$raw_value") + source="env" + write_secret_file "$file" "$normalized" + elif [ -f "$file" ]; then + normalized=$(canonicalize_seed "$env_name" "$(cat "$file")") + source="persisted" + write_secret_file "$file" "$normalized" + else + normalized="" + source="shared-v1" + fi + + printf '%s|%s\n' "$normalized" "$source" +} + +read_seed_result() { + local var_prefix="$1" + local result="$2" + local seed="${result%%|*}" + local source="${result##*|}" + + printf -v "${var_prefix}_SEED" '%s' "$seed" + printf -v "${var_prefix}_SOURCE" '%s' "$source" +} + +ensure_node_key_file() { + local name="$1" + local file="$NODE_KEY_DIR/${name}.key" + + if [ ! -s "$file" ]; then + mkdir -p "$NODE_KEY_DIR" + "$NODE_BIN" key generate-node-key --chain local > "$file" + chmod 600 "$file" + fi + + printf '%s\n' "$file" +} + +derive_address() { + local suri="$1" + local scheme="${2:-Sr25519}" + + "$NODE_BIN" key inspect --scheme "$scheme" "$suri" --output-type json \ + | grep -o '"ss58Address": "[^"]*"' \ + | cut -d'"' -f4 +} + +insert_keys() { + local base_path="$1" + local seed="$2" + + "$NODE_BIN" key insert --base-path "$base_path" --chain "$SPEC_FILE" --scheme Sr25519 --suri "$seed//babe" --key-type babe + "$NODE_BIN" key insert --base-path "$base_path" --chain "$SPEC_FILE" --scheme Ed25519 --suri "$seed//grandpa" --key-type gran + "$NODE_BIN" key insert --base-path "$base_path" --chain "$SPEC_FILE" --scheme Sr25519 --suri "$seed//im_online" --key-type imon + "$NODE_BIN" key insert --base-path "$base_path" --chain "$SPEC_FILE" --scheme Sr25519 --suri "$seed//authority_discovery" --key-type audi +} + +start_logged() { + local log_file="$1" + shift + + "$@" >"$log_file" 2>&1 & + PIDS+=("$!") +} + +write_ws_proxy_config() { + cat > /tmp/rpc-nginx.conf </dev/null || true + +echo "Resolved validator addresses:" +echo " - Validator1: $V1_ADDR ($V1_SOURCE)" +echo " - Validator2: $V2_ADDR ($V2_SOURCE)" +echo " - Validator3: $V3_ADDR ($V3_SOURCE)" +echo " - Faucet: $FAUCET_ADDR ($FAUCET_SOURCE)" +echo "Private state directory: $STATE_DIR" + +echo "Downloading local faucet sources..." +wget -q -O "$FAUCET_DIR/package.json" \ + "${TEMPLATE_ASSET_BASE_URL}/faucet/package.json" +wget -q -O "$FAUCET_DIR/server.js" \ + "${TEMPLATE_ASSET_BASE_URL}/faucet/server.js" + +echo "Installing faucet dependencies..." +( + cd "$FAUCET_DIR" + npm install --omit=dev --no-fund --no-audit +) + +echo "Generating chain spec..." +"$NODE_BIN" build-spec --chain "$CHAIN_TEMPLATE" --disable-default-bootnode > "$PLAIN_SPEC" + +python3 - "$PLAIN_SPEC" "$UPDATED_SPEC" \ + "$V1_ADDR" "$V1_BABE" "$V1_GRAN" "$V1_IMON" "$V1_AUDI" \ + "$V2_ADDR" "$V2_BABE" "$V2_GRAN" "$V2_IMON" "$V2_AUDI" \ + "$V3_ADDR" "$V3_BABE" "$V3_GRAN" "$V3_IMON" "$V3_AUDI" \ + "$FAUCET_ADDR" "$V1_ADDR" <<'PY' +import json +import sys + +input_file = sys.argv[1] +output_file = sys.argv[2] + +v1_addr, v1_babe, v1_gran, v1_imon, v1_audi = sys.argv[3:8] +v2_addr, v2_babe, v2_gran, v2_imon, v2_audi = sys.argv[8:13] +v3_addr, v3_babe, v3_gran, v3_imon, v3_audi = sys.argv[13:18] +faucet_addr = sys.argv[18] +default_faucet_addr = sys.argv[19] + +AMOUNT = 100000000000000000000000000 +STAKE = 1000000000000000000000000 + +with open(input_file, "r", encoding="utf-8") as handle: + spec = json.load(handle) + +balances = spec["genesis"]["runtimeGenesis"]["patch"]["balances"]["balances"] + +def upsert_balance(address, amount): + for entry in balances: + if entry[0] == address: + entry[1] = max(int(entry[1]), amount) + return + balances.append([address, amount]) + +for address in (v1_addr, v2_addr, v3_addr): + upsert_balance(address, AMOUNT) + +if faucet_addr != default_faucet_addr: + upsert_balance(faucet_addr, AMOUNT) + +spec["genesis"]["runtimeGenesis"]["patch"]["session"]["keys"] = [ + [ + v1_addr, + v1_addr, + { + "authority_discovery": v1_audi, + "babe": v1_babe, + "grandpa": v1_gran, + "im_online": v1_imon, + }, + ], + [ + v2_addr, + v2_addr, + { + "authority_discovery": v2_audi, + "babe": v2_babe, + "grandpa": v2_gran, + "im_online": v2_imon, + }, + ], + [ + v3_addr, + v3_addr, + { + "authority_discovery": v3_audi, + "babe": v3_babe, + "grandpa": v3_gran, + "im_online": v3_imon, + }, + ], +] + +spec["genesis"]["runtimeGenesis"]["patch"]["staking"]["invulnerables"] = [ + v1_addr, + v2_addr, + v3_addr, +] + +spec["genesis"]["runtimeGenesis"]["patch"]["staking"]["stakers"] = [ + [v1_addr, v1_addr, STAKE, "Validator"], + [v2_addr, v2_addr, STAKE, "Validator"], + [v3_addr, v3_addr, STAKE, "Validator"], +] + +with open(output_file, "w", encoding="utf-8") as handle: + json.dump(spec, handle, indent=2) +PY + +"$NODE_BIN" build-spec \ + --chain "$UPDATED_SPEC" \ + --disable-default-bootnode \ + --raw > "$SPEC_FILE" + +BOOTNODE_NODE_KEY_FILE=$(ensure_node_key_file bootnode) +V1_NODE_KEY_FILE=$(ensure_node_key_file validator1) +V2_NODE_KEY_FILE=$(ensure_node_key_file validator2) +V3_NODE_KEY_FILE=$(ensure_node_key_file validator3) +RPC_NODE_KEY_FILE=$(ensure_node_key_file rpc-node) + +BOOTNODE_PEER_ID=$("$NODE_BIN" key inspect-node-key --file "$BOOTNODE_NODE_KEY_FILE" 2>/dev/null | tail -n1 | tr -d '\r') +if [ -z "$BOOTNODE_PEER_ID" ]; then + echo "Failed to derive bootnode peer id" >&2 + exit 1 +fi + +printf '%s\n' "$BOOTNODE_PEER_ID" > "$BOOTNODE_PEER_ID_FILE" +printf '%s\n' "$BOOTNODE_PEER_ID" > "$BOOTNODE_COMPAT_FILE" +write_cluster_info + +echo "Bootnode peer ID: $BOOTNODE_PEER_ID" + +insert_keys /tmp/validator1 "$V1_SEED" +insert_keys /tmp/validator2 "$V2_SEED" +insert_keys /tmp/validator3 "$V3_SEED" + +python3 -m http.server "$SPEC_HTTP_PORT" --bind 0.0.0.0 --directory "$OUTPUT_DIR" >/tmp/spec-server.log 2>&1 & +PIDS+=("$!") + +BOOTNODE_MULTIADDR="/ip4/127.0.0.1/tcp/${BOOTNODE_P2P_PORT}/p2p/${BOOTNODE_PEER_ID}" + +write_ws_proxy_config + +start_logged /tmp/bootnode.log \ + "$NODE_BIN" \ + --base-path /tmp/bootnode \ + --chain "$SPEC_FILE" \ + --port "$BOOTNODE_P2P_PORT" \ + --prometheus-port "$BOOTNODE_PROMETHEUS_PORT" \ + --rpc-port 0 \ + --node-key-file "$BOOTNODE_NODE_KEY_FILE" \ + --name Bootnode \ + --no-telemetry + +start_logged /tmp/validator1.log \ + "$NODE_BIN" \ + --base-path /tmp/validator1 \ + --chain "$SPEC_FILE" \ + --port "$VALIDATOR1_P2P_PORT" \ + --prometheus-port "$VALIDATOR1_PROMETHEUS_PORT" \ + --rpc-port 0 \ + --node-key-file "$V1_NODE_KEY_FILE" \ + --bootnodes "$BOOTNODE_MULTIADDR" \ + --validator \ + --name Validator1 \ + --no-telemetry + +start_logged /tmp/validator2.log \ + "$NODE_BIN" \ + --base-path /tmp/validator2 \ + --chain "$SPEC_FILE" \ + --port "$VALIDATOR2_P2P_PORT" \ + --prometheus-port "$VALIDATOR2_PROMETHEUS_PORT" \ + --rpc-port 0 \ + --node-key-file "$V2_NODE_KEY_FILE" \ + --bootnodes "$BOOTNODE_MULTIADDR" \ + --validator \ + --name Validator2 \ + --no-telemetry + +start_logged /tmp/validator3.log \ + "$NODE_BIN" \ + --base-path /tmp/validator3 \ + --chain "$SPEC_FILE" \ + --port "$VALIDATOR3_P2P_PORT" \ + --prometheus-port "$VALIDATOR3_PROMETHEUS_PORT" \ + --rpc-port 0 \ + --node-key-file "$V3_NODE_KEY_FILE" \ + --bootnodes "$BOOTNODE_MULTIADDR" \ + --validator \ + --name Validator3 \ + --no-telemetry + +start_logged /tmp/rpc-node.log \ + "$NODE_BIN" \ + --base-path /tmp/rpc-node \ + --chain "$SPEC_FILE" \ + --port "$RPC_NODE_P2P_PORT" \ + --prometheus-port "$RPC_NODE_PROMETHEUS_PORT" \ + --node-key-file "$RPC_NODE_KEY_FILE" \ + --bootnodes "$BOOTNODE_MULTIADDR" \ + --rpc-external \ + --rpc-port "$RPC_NODE_WS_PORT" \ + --rpc-cors all \ + --rpc-methods Unsafe \ + --rpc-max-connections 10000 \ + --pruning archive \ + --name rpc-node \ + --no-telemetry + +start_logged /tmp/ws-proxy.log \ + nginx \ + -c /tmp/rpc-nginx.conf \ + -g 'daemon off;' + +start_logged /tmp/faucet.log \ + env \ + SERVICE_NAME="reef-dev-cluster-faucet" \ + PORT="$FAUCET_PORT" \ + WS_ENDPOINT="ws://127.0.0.1:${RPC_NODE_WS_PORT}" \ + EVM_RPC_URL="http://127.0.0.1:${ETH_RPC_PORT}" \ + FAUCET_SEED="$FAUCET_SEED" \ + DEFAULT_AMOUNT="$DEFAULT_AMOUNT" \ + MAX_AMOUNT="$MAX_AMOUNT" \ + npm --prefix "$FAUCET_DIR" start + +tail -F /tmp/spec-server.log /tmp/bootnode.log /tmp/validator1.log /tmp/validator2.log /tmp/validator3.log /tmp/rpc-node.log /tmp/ws-proxy.log /tmp/faucet.log & +TAIL_PID=$! + +wait -n "${PIDS[@]}" +STATUS=$? +exit "$STATUS" diff --git a/blueprints/reef-dev-cluster/template.toml b/blueprints/reef-dev-cluster/template.toml new file mode 100644 index 000000000..05345c2bd --- /dev/null +++ b/blueprints/reef-dev-cluster/template.toml @@ -0,0 +1,51 @@ +[variables] +# Optional advanced overrides. Leave blank to auto-generate and persist validator identities. +v1sec = "" +v2sec = "" +v3sec = "" + +# Optional dedicated faucet seed. Leave blank to reuse validator 1. +faucetsec = "" +DEFAULT_AMOUNT = "2000" +MAX_AMOUNT = "2000" + +[[config.domains]] +serviceName = "reef-dev-cluster" +port = 8001 +host = "${domain}" +path = "/" + +[[config.domains]] +serviceName = "reef-dev-cluster" +port = 8001 +host = "spec.${domain}" +path = "/" + +[[config.domains]] +serviceName = "reef-dev-cluster" +port = 9945 +host = "ws.${domain}" +path = "/" + +[[config.domains]] +serviceName = "reef-dev-cluster-eth-rpc" +port = 8545 +host = "eth.${domain}" +path = "/" + +[[config.domains]] +serviceName = "reef-dev-cluster" +port = 8080 +host = "faucet.${domain}" +path = "/" + +[config.env] +# Optional advanced overrides. Leave blank to auto-generate and persist validator identities. +v1sec = "" +v2sec = "" +v3sec = "" + +# Optional dedicated faucet seed. Leave blank to reuse validator 1. +faucetsec = "" +DEFAULT_AMOUNT = "2000" +MAX_AMOUNT = "2000" diff --git a/blueprints/reef-keygen/docker-compose.yml b/blueprints/reef-keygen/docker-compose.yml new file mode 100644 index 000000000..b6d332ce2 --- /dev/null +++ b/blueprints/reef-keygen/docker-compose.yml @@ -0,0 +1,41 @@ +version: "3.8" + +services: + reef-keygen: + image: anukulpandey/reef-chain-node:latest + restart: unless-stopped + working_dir: /workspace + volumes: + - ./output:/output + expose: + - "${PORT}" + entrypoint: /bin/sh + command: -c " + set -e && + + echo '📦 Installing python3...' && + apt-get update && + apt-get install -y python3 && + + mkdir -p /output && + chmod -R 777 /output && + + echo '=====================================' && + echo '🔑 Generating Validator Keys' && + echo '=====================================' && + + reef-node key generate --scheme Sr25519 --output-type json > /output/validator1.json && + reef-node key generate-node-key --chain local > /output/v1_node_key.txt && + + reef-node key generate --scheme Sr25519 --output-type json > /output/validator2.json && + reef-node key generate-node-key --chain local > /output/v2_node_key.txt && + + reef-node key generate --scheme Sr25519 --output-type json > /output/validator3.json && + reef-node key generate-node-key --chain local > /output/v3_node_key.txt && + + echo '✅ Keys generated:' && + ls -lah /output && + + echo '🌐 Starting HTTP server on port ${PORT}' && + cd /output && + python3 -m http.server ${PORT} --bind 0.0.0.0" diff --git a/blueprints/reef-keygen/meta.json b/blueprints/reef-keygen/meta.json new file mode 100644 index 000000000..9f1d7a4bd --- /dev/null +++ b/blueprints/reef-keygen/meta.json @@ -0,0 +1,18 @@ +{ + "id": "reef-keygen", + "name": "Reef Chain - Keys Generator", + "version": "1", + "description": "Generates 3 Validators keys for a custom Reef Chain cluster", + "links": { + "github": "https://github.com/reef-chain/chain-upgrade", + "website": "https://hub.docker.com/repository/docker/anukulpandey/reef-chain-node", + "docs": "https://docs.reef.io/" + }, + "logo": "reef.svg", + "tags": [ + "reef-chain", + "keys generator", + "self-hosted", + "keys" + ] +} diff --git a/blueprints/reef-keygen/reef.svg b/blueprints/reef-keygen/reef.svg new file mode 100644 index 000000000..598d7930a --- /dev/null +++ b/blueprints/reef-keygen/reef.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + diff --git a/blueprints/reef-keygen/template.toml b/blueprints/reef-keygen/template.toml new file mode 100644 index 000000000..6986372ff --- /dev/null +++ b/blueprints/reef-keygen/template.toml @@ -0,0 +1,11 @@ +[variables] +PORT = "48765" + +[[config.domains]] +serviceName = "reef-keygen" +port = 48765 +host = "${domain}" +path = "/" + +[config.env] +PORT = "48765" From 979eadc06a9b952645180e14f08e54c2361ac5b5 Mon Sep 17 00:00:00 2001 From: taqin Date: Sun, 9 Aug 2026 20:48:59 +0700 Subject: [PATCH 80/80] Change website and docs URLs in meta.json Updated website and documentation links for Drizzle Gateway. --- blueprints/drizzle-gateway/meta.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/blueprints/drizzle-gateway/meta.json b/blueprints/drizzle-gateway/meta.json index 7bfd97b41..25d099ce5 100644 --- a/blueprints/drizzle-gateway/meta.json +++ b/blueprints/drizzle-gateway/meta.json @@ -6,8 +6,8 @@ "logo": "drizzle-gateway.svg", "links": { "github": "https://github.com/drizzle-team/drizzle-gateway", - "website": "https://drizzle-team.github.io/", - "docs": "https://drizzle-team.github.io/docs" + "website": "https://gateway.drizzle.team/", + "docs": "https://gateway.drizzle.team/docs/railway" }, "tags": [ "database",