From a14bab01ad31f122a90cdc1bb367422e17e69248 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Mon, 20 Jul 2026 01:20:53 -0600 Subject: [PATCH] New release (#1038) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 * fix: broken links in some blueprints * Add openhabittracker blueprint * 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 * 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 * 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 * 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 * 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 * fix(gitlab-ce): serve on the generated domain (remove removed unicorn config, pin omnibus) 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 * 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 * fix(immich): update to v3.0.2 Co-Authored-By: Claude Fable 5 * 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 * 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 * 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 * fix: add databasus (postgresus renamed upstream) Co-Authored-By: Claude Fable 5 * 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 * 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 * 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 * fix(openpanel): pin verified 2.2.1 images and fix ClickHouse interserver host - 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 * 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 * 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 * 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 * feat: add MailCatcher NG template Co-Authored-By: Claude Fable 5 * 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 * feat: add Weblate template Co-Authored-By: Claude Fable 5 * feat: add Synapse (Matrix homeserver) template Co-Authored-By: Claude Fable 5 * 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 * feat: add Sim Studio template Co-Authored-By: Claude Fable 5 * feat: add Steedos template Co-Authored-By: Claude Fable 5 * feat: add Silex template Co-Authored-By: Claude Fable 5 * feat: add SpacetimeDB template Co-Authored-By: Claude Fable 5 * feat: add CertMate template Co-Authored-By: Claude Fable 5 * 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 * feat: add Markup template Co-Authored-By: Claude Fable 5 * 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 * feat: add TREK template Co-Authored-By: Claude Fable 5 * feat: add Oryx (SRS) media streaming template Co-Authored-By: Claude Fable 5 * 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 * 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 * feat: add bonfire template Bonfire (social flavour) — federated social networking toolkit. App + PostGIS + Meilisearch, mirroring upstream's reference deployments. Closes #125 Co-Authored-By: Claude Fable 5 * 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 * 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 * 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 * feat(templates): add systemprompt (#1032) * feat(templates): add systemprompt * fix header comment: accurate in both repos * chore: normalize template tags to kebab-case (#987) * feat: adds `check-links.js` to find broken links (#983) * 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. * 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> * fix(docker): share plugins and themes volumes across containers (#990) * 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 --------- Co-authored-by: Zakher Masri <46135573+zaaakher@users.noreply.github.com> Co-authored-by: zakher Co-authored-by: Jinjinov Co-authored-by: Claude Fable 5 Co-authored-by: Stephan Fitzpatrick Co-authored-by: Mauricio Siu Co-authored-by: Edward Burton Co-authored-by: Marko Kostich Co-authored-by: Jakub Krzyżanowski Co-authored-by: SamTV12345 <40429738+SamTV12345@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Mafly <110502739+Nekoplii@users.noreply.github.com> --- 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/adventurelog/meta.json | 2 +- blueprints/affinepro/docker-compose.yml | 37 +- blueprints/affinepro/meta.json | 4 +- blueprints/answer/meta.json | 2 +- blueprints/anythingllm/meta.json | 4 +- blueprints/automatisch/docker-compose.yml | 29 +- blueprints/automatisch/meta.json | 2 +- blueprints/automatisch/template.toml | 1 + blueprints/babybuddy/meta.json | 2 +- blueprints/bazarr/meta.json | 2 +- blueprints/bentopdf/meta.json | 4 +- blueprints/billmora/docker-compose.yml | 12 + blueprints/bonfire/bonfire.svg | 1 + blueprints/bonfire/docker-compose.yml | 84 +++ blueprints/bonfire/meta.json | 17 + blueprints/bonfire/template.toml | 55 ++ blueprints/calibre/meta.json | 4 +- blueprints/carbone/meta.json | 8 +- 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 + blueprints/changedetection/meta.json | 6 +- blueprints/checkmate/meta.json | 2 +- blueprints/chevereto/meta.json | 10 +- blueprints/chibisafe/meta.json | 2 +- blueprints/chiefonboarding/meta.json | 10 +- blueprints/cockpit/docker-compose.yml | 8 +- blueprints/crawl4ai/meta.json | 6 +- blueprints/dagu/docker-compose.yml | 25 + blueprints/dagu/logo.png | Bin 0 -> 7754 bytes blueprints/dagu/meta.json | 19 + blueprints/dagu/template.toml | 15 + blueprints/databasus/databasus.svg | 12 + blueprints/databasus/docker-compose.yml | 22 + blueprints/databasus/meta.json | 17 + blueprints/databasus/template.toml | 11 + 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 +++++ blueprints/directus/docker-compose.yml | 98 ++- blueprints/directus/meta.json | 4 +- blueprints/directus/template.toml | 33 +- blueprints/documenso/meta.json | 2 +- blueprints/emby/meta.json | 2 +- blueprints/evershop/meta.json | 2 +- blueprints/evolutionapi/docker-compose.yml | 9 +- blueprints/evolutionapi/meta.json | 2 +- blueprints/evolutionapi/template.toml | 7 + blueprints/flowise/meta.json | 4 +- 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 + blueprints/gitlab-ce/docker-compose.yml | 53 +- blueprints/gitlab-ce/meta.json | 2 +- blueprints/gitlab-ce/template.toml | 8 +- blueprints/go2rtc/meta.json | 2 +- blueprints/homarr/docker-compose.yml | 5 +- blueprints/homarr/template.toml | 2 +- blueprints/immich/docker-compose.yml | 50 +- blueprints/immich/meta.json | 2 +- blueprints/immich/template.toml | 3 - blueprints/infisical/docker-compose.yml | 59 +- blueprints/infisical/meta.json | 2 +- blueprints/infisical/template.toml | 47 +- blueprints/jellyfin/meta.json | 2 +- blueprints/kaneo/meta.json | 2 +- blueprints/kener/docker-compose.yml | 2 - blueprints/librechat/meta.json | 6 +- blueprints/livekit/meta.json | 10 +- blueprints/lobe-chat/meta.json | 2 +- blueprints/logto/docker-compose.yml | 14 +- blueprints/logto/meta.json | 2 +- blueprints/logto/template.toml | 4 +- 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 + 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 + blueprints/markup/docker-compose.yml | 39 ++ blueprints/markup/markup.svg | 5 + blueprints/markup/meta.json | 18 + blueprints/markup/template.toml | 25 + blueprints/maybe/meta.json | 2 +- blueprints/minio/docker-compose.yml | 21 +- blueprints/minio/meta.json | 10 +- blueprints/minio/template.toml | 7 + blueprints/mumble/meta.json | 2 +- 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 + blueprints/nextcloud-aio/docker-compose.yml | 4 +- blueprints/nextcloud-aio/meta.json | 2 +- blueprints/oneuptime/docker-compose.yml | 158 +++++ blueprints/oneuptime/meta.json | 20 + blueprints/oneuptime/oneuptime.svg | 10 + blueprints/oneuptime/template.toml | 109 +++ .../openhabittracker/docker-compose.yml | 18 + blueprints/openhabittracker/meta.json | 19 + .../openhabittracker/openhabittracker.png | Bin 0 -> 36538 bytes blueprints/openhabittracker/template.toml | 16 + blueprints/openpanel/docker-compose.yml | 6 +- blueprints/openpanel/meta.json | 2 +- blueprints/openpanel/template.toml | 2 +- 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 + blueprints/paymenter/meta.json | 2 +- blueprints/phpmyadmin/docker-compose.yml | 4 +- blueprints/phpmyadmin/template.toml | 4 +- blueprints/podfetch/docker-compose.yml | 19 + blueprints/podfetch/logo.png | Bin 0 -> 13826 bytes blueprints/podfetch/meta.json | 15 + blueprints/podfetch/template.toml | 15 + blueprints/poke/meta.json | 4 +- blueprints/postgresus/docker-compose.yml | 4 +- blueprints/postgresus/meta.json | 8 +- blueprints/posthog/docker-compose.yml | 646 ++++++++++++++++++ blueprints/posthog/instructions.md | 27 + blueprints/posthog/meta.json | 19 + blueprints/posthog/posthog.svg | 10 + blueprints/posthog/template.toml | 73 ++ blueprints/rustrak/meta.json | 4 +- blueprints/rybbit/docker-compose.yml | 52 +- blueprints/rybbit/meta.json | 2 +- blueprints/rybbit/template.toml | 6 +- blueprints/scrutiny/meta.json | 2 +- blueprints/signoz/docker-compose.yml | 8 +- blueprints/signoz/template.toml | 21 +- blueprints/silex/docker-compose.yml | 20 + blueprints/silex/logo.png | Bin 0 -> 13715 bytes blueprints/silex/meta.json | 17 + blueprints/silex/template.toml | 13 + blueprints/sim/docker-compose.yml | 76 +++ blueprints/sim/meta.json | 17 + blueprints/sim/sim.svg | 1 + blueprints/sim/template.toml | 28 + 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 + blueprints/steedos/docker-compose.yml | 13 + blueprints/steedos/meta.json | 16 + blueprints/steedos/steedos.png | Bin 0 -> 4448 bytes blueprints/steedos/template.toml | 13 + blueprints/struxa/docker-compose.yml | 90 +++ blueprints/struxa/meta.json | 18 + blueprints/struxa/struxa.svg | 3 + blueprints/struxa/template.toml | 34 + blueprints/supabase/instructions.md | 61 ++ 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 +++ 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 + blueprints/trek/docker-compose.yml | 32 + blueprints/trek/meta.json | 17 + blueprints/trek/template.toml | 46 ++ blueprints/trek/trek.png | Bin 0 -> 14965 bytes blueprints/triggerdotdev/instructions.md | 54 ++ blueprints/verdaccio/meta.json | 2 +- blueprints/weblate/docker-compose.yml | 66 ++ blueprints/weblate/meta.json | 18 + blueprints/weblate/template.toml | 26 + blueprints/weblate/weblate.svg | 1 + blueprints/wordpress/docker-compose.yml | 22 + blueprints/wordpress/template.toml | 3 +- blueprints/zipline/meta.json | 2 +- blueprints/zitadel/docker-compose.yml | 37 +- blueprints/zitadel/meta.json | 2 +- blueprints/zitadel/template.toml | 14 +- build-scripts/check-links.js | 175 +++++ 195 files changed, 5163 insertions(+), 410 deletions(-) create mode 100644 app/src/components/ui/markdown.tsx delete mode 100644 blueprints/ackee/instructions.md 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 create mode 100644 blueprints/supabase/instructions.md 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 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 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 create mode 100644 blueprints/triggerdotdev/instructions.md 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 create mode 100644 build-scripts/check-links.js diff --git a/app/src/components/TemplateDialog.tsx b/app/src/components/TemplateDialog.tsx index b1e9b2f0..de10ce38 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 8f51dd69..42e1a655 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 00000000..03c0ca87 --- /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 b107d360..00000000 --- a/blueprints/ackee/instructions.md +++ /dev/null @@ -1,4 +0,0 @@ - -## Instructions - -We don't have nothing to show here.... diff --git a/blueprints/adventurelog/meta.json b/blueprints/adventurelog/meta.json index 83f1bc33..8c6109e6 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/docker-compose.yml b/blueprints/affinepro/docker-compose.yml index 93fa9da4..e8a905d5 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 1a3da4a6..66e67054 100644 --- a/blueprints/affinepro/meta.json +++ b/blueprints/affinepro/meta.json @@ -1,13 +1,13 @@ { "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": { "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/answer/meta.json b/blueprints/answer/meta.json index d33d2bb5..5e44315f 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/anythingllm/meta.json b/blueprints/anythingllm/meta.json index eff3e8c2..2a525808 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/automatisch/docker-compose.yml b/blueprints/automatisch/docker-compose.yml index 42f585c8..550c771d 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 9acf2c66..7750995f 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 2e78ef00..a718ea0e 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}" diff --git a/blueprints/babybuddy/meta.json b/blueprints/babybuddy/meta.json index f564e489..bbc59fc2 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 473ed7d0..e3c538b3 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 2acc729b..99ed44a2 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/billmora/docker-compose.yml b/blueprints/billmora/docker-compose.yml index 870629e1..eb5828d7 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: diff --git a/blueprints/bonfire/bonfire.svg b/blueprints/bonfire/bonfire.svg new file mode 100644 index 00000000..ce2cac6f --- /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 00000000..b52ca259 --- /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 00000000..bee28afb --- /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 00000000..c8db0d00 --- /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" = "" diff --git a/blueprints/calibre/meta.json b/blueprints/calibre/meta.json index 742c89a9..78f3cf7b 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 1834d557..37578445 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/certmate/docker-compose.yml b/blueprints/certmate/docker-compose.yml new file mode 100644 index 00000000..d7dde9dc --- /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 00000000..6a048e3c --- /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 00000000..f8be746c --- /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 00000000..48d6228d --- /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" diff --git a/blueprints/changedetection/meta.json b/blueprints/changedetection/meta.json index 85add899..172de61b 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/checkmate/meta.json b/blueprints/checkmate/meta.json index b9f9d155..5f4abc1b 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/chevereto/meta.json b/blueprints/chevereto/meta.json index 8bdb871b..cb544a0e 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 548b4e35..69103ade 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 12b1ec30..37fe83ce 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/cockpit/docker-compose.yml b/blueprints/cockpit/docker-compose.yml index 3c823897..fbc99a8e 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 diff --git a/blueprints/crawl4ai/meta.json b/blueprints/crawl4ai/meta.json index b0038d03..823958a8 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/dagu/docker-compose.yml b/blueprints/dagu/docker-compose.yml new file mode 100644 index 00000000..4fee20eb --- /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 + + + + + + + + + + + diff --git a/blueprints/databasus/docker-compose.yml b/blueprints/databasus/docker-compose.yml new file mode 100644 index 00000000..aafa7744 --- /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 00000000..9eb68a6f --- /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 00000000..0a6dd3b9 --- /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/dify/docker-compose.yml b/blueprints/dify/docker-compose.yml new file mode 100644 index 00000000..6c89415b --- /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 00000000..f554f671 --- /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 00000000..52ed710f --- /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 00000000..170013dd --- /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 00000000..ded696fb --- /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 % + 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 00000000..48e47a09 --- /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 zcmV*CKyAN?P)@~0drDELIAGL9O(c600d`2O+f$vv5yPR(#UFGi9!IUsvt_aKcr5_4QyqW3;>kspIgcuynJ$pGld-iDK z^es8(7dOX8DW&N+{n^It(Qu+ES-Oq`PRR{kLM6ENLyw0!X-v;+dVcua;Case<=g|O z5!egc`fP_mX$#ywa`~-tKc{W@ahpQF`cRo4b& z-nV&ZFK;UIBioVYKETGibGSZIgT8jkZUZlFb21iKz$01Z9ylp5VXJnN>9_VsY(Z!u zyot1`yiB*j?Bzw|tVq&}+sfnCNa>TC|4%pN`_z@$6MO4hKl#eXKmLjZx`^IHU{auO zm_4>P-gy6}!arW6H1|Z+_$PFTlrf@M&t9PxC?)DC8AY^s&0zvZgT=tKA3>5~mEN`E z92SXkk&{ED*cO$a*DgoNQ(@os-zuxX(}`?T&cC#=(0%fizyJ3?X@Lczx3HZO{o2zdS7D&s%{*+F4?^Qr8v5GwY;fh02=#~IkFMMs$yEqT@<%ek$BO%Ujy8Cc~zLfW^% zK6ATbfbg-6iTLL>>h+0T0M5GgPh|SW8*hBX9!Qn0!NTmsjWWTjHZ&qujo-}f#r%5~ zU=rlP47#_%2ks3m5@o;D3!2Oprrg%6JSr@jh`Bu$uz7cpmM|SFbt+V1clu}FKH-*q z$X6|Z42&4RZBi)ODBF&a5H%RoT(NeD?VOzwysvOShU^O4B+Hf^+ zwq={I!kFL2%^Y>yAn0DYDxZ-)#_^xXby_|A)8|C4C*L!ix-uOFH}2lb!L9os;kF3) z;^7hPsA)KG6j-R#&h_k0MA#(E>kEu*a={Y~k`41Fv+v@1=f>rbX5p+~$y>C4k;frx zanEFJXQo_Fx(E2?4}9Rmn;ZDZ<_CFXCjvP_w*Clv@nX7&kBvkvWGXncGCf2ZM+eU= ztbo5si+DL~E1kfDqeZ+!N>^BzDk6h;=;m!gQNpy8D~2%}X#brx7z2d3x6&11o~ARH zb3Mrxd`b(DdT6Zx&SJ5WI}(OYTs3}mG_zw`#BiMGJ@bN=X*kI8EKs#8BEm)3&e{kw zO>Rt93sbE#$V=ucPBk!41YG57Z6al0Sw~F5Tqg2=`fNk4e|VzgdQvTYGWEXk>Z?Dq zIsLF*7<%(DIRA>KBW{qb#SRMl5an5rtEW6Uj7O$Qu?D$b6nYDxM1<>OqfIZ!v(x^O zs8#zn^9R-YTDiQ1S4#bMRPV=5alZ9Ub|QGk2R`tV&t7}&`hE{OSXR^zSgQ? z&SMTI7BFR}&MM;EC9)_Fmt!9^`ktqo>mNJ?)p}(|j7goo@%rok!Y+_dCzQs)b1`TF zt`+LOs5u&SWT-h3tgv1qXjP8fhxl-ka*NB#qRp#Ts z(PGh-QhTaqXewtQ?nwJb6{=PgW4dOfR@DNjBtDs!QrE(z`VwUsV>H%)JlPz7{8&}% z(Y4$cKJbB;Hxs?TIE_p!;6gS|o)alNXCmi&i=QddCL%8Gc%Dl98%!MW;cxm=f69(+ z5!2DM-go@R|MAak9^&6vbTAX5lj(y-pFA(4OgUS3(<4T&at8*0gvsuXbzu(AaYNk( zR={ijZk@b|SSd-Rbi0e9m^8Eh-l{iMg=yopnMpJA);;Hsr?Q*;EjkzhF;Bv#7!xB<#=i3*Bk?X4Or~ri_U2qi>Uy+?vjUQZx*kp^ z`R~X!SN?mJ6KA0Az9)-)kLY(A^QCgGxTS!1Zco#ZtPXzA*0>|oPt5bDEwfl)fi;UB z*yPfx^d3CDqs$~C3- zVBA>^w>+^5LDpe`gUR#KEa`tGpe_M|-zslxZHYa1) z;y;%M{H;OSc$|qA`zJx_iO!YU-r&Xj?AibME$?~H=fClp&wR=r2oFN4-~7Wr{Gp8< z{J-5inVSVk{6iFA=fZA$exdN@CaMbWm}iRH97m<&_~k|%5BtP9EAVeMN{&(A0}gt% zA+4w{t+0#5N=%kCKu=&cPQ7|x!tMO3I5gN~v+*{_E{zS|dX>@#A94ZHy_4!U|IiP8 zXmj}3=I^bGPD4G!a*a@(O`s2Wt~~myDl1fQHuIWUcO`Joe0!o_2z6(3Jq(*e9bE<*=>SagAi>Ya3pA%Z4Z@pYFj=hQ}CBJ z#QIO|j&Y}?I=wctxt;&krbS5z9({X$eDp}OwWuku#bV{&%|+#oN)-uo_TL@ndDPm} za+B8Xhy=Tq1bg8x;+MujB>!%#SXG!H*1#ZcTT7C2zP)Z(%x+~mCU@@@!QJ(byyZRb zd3x8c+uT`en>TjI`O$DBMY8Bu;B^$yfw<_63}^pDA0`$rWvgrNsh@?Kd`OJ6iB@~S zaLPa0OW;5#{f2jaCwZpv4-4$9ZLg#^z3B(<~aCXiE4UCd1hi$3UXdeMPJn4Dq?VAtIwc@KbYts_M`^r^HKX6xD z8s5oA%6EJDKC*p*4`4bk*UHT0y#p2K0!fpf7_R!AKW4o9EqFAFag<*cVc9Qm0Pv{P23+u51n-1`&%b&H4dS#&bf^q)9>1DEuP(E{H@~XdObN55y+j+kC_MD9 zyf-12BMO2VkMh^Q{)=`8IOsy9>3gV~qJPxx0%~y$FJe+*3og*)HFCG!oCK}_Mz;yG z=@v}J#U1l4;YOc2&gG6^P1uWps^g`7c=f;6E@*%?6ekck5Z3e`E5kgxi&t$9)-tAf z)Hhy#{d0C-SiAV*0!h`wK64zcxz@$2HwFuxAWwAn5Jb5LSZ%VUp|@gQowwRcw57la z!t3`o^Q+S*AJPIjUUq-)-}{U^HLMp5{`ONzlsW4Y{IHO?0jnIkB6WUf|S%7v}8AmWffpnXsH-F#*AKo;mkJ>|mUnK|}6s?^~QX>p*e~2no znS~Siqu*M1Cgh8&8BCNdqE!q3T5V}$9YY{+I(T%b9hYv^5^nuI>TSSgDm5om!UZId z-1bf*r8*6mZ{<1I@>`6~uX@-wUSNmb94w;O#-e=%&Ing;deaZwc4LTJnSbxZ>^q8< z35p4o=U+Meg<`L}hfZ`R%-vsQxEbN&*J8_zUpj+%j6exYa`cyp!7o#)PZ zF;r_+vAPZfjvMd#ruV!j{kzY6=85fzy=wb|ntgll=7&5#578OSkfvPWk8`Z@+6l{0 zWEcdwCwgj<2R}X2uxI*hnS`S)1x^dM+Qx7%^Y4S2eS1)*tO+~^+e35$Ga9>+)b3?f zu$g%amooT`n0{q`2MTi$7^&-uP(2muf!h@mb0Ba!xYcUl)uj632R`ufronvJ9ugys zw53XwioIa0?ee@&FfGpQ=CvC6c_tGIbT>E4gsWBzGxDH&{NezQ2-O+{k7(6eg%Lvb z=4yNK0)acmrq(@v@rQr-gSH1&lj<90cd}pNDvX%3RM;NDUMNPoE&OIGD^}hFg7*Y2 zdCZ$a;Hx;`hxwE^RT->ZR&P~tYJ>}Ppoou;1xUKekHK@b3ei3T4}@na{med_?yn-% z-QuK2cFb5a;%$wP0~KZya3>rtP~uNEL1HtJmes@jHJq|im{{pJcFj=K4e=omYd0OG4?FQ}=IsfEZwK`0yC!tamMn=ss z$GSHbW?r6`qfR-qIdP{_i&;2hTx*cF*<#)`lRuJcUTPd(>6MRv{FRZwLrV48>#u)klu(aS74$~U zXrjWf%5=qP1{Rp-O37^tdZt^Fqm{w9SwAXLwGfH@Jfy*JEyOS0fj4Nyqh}RT?jvNW zh&T{9EM9!}^{;<)H1H6q=9E5cgE-1Dqaw#%9*?iny`Tmyus@lRf>PbkN?(|ClKgkNUgS&YR(^Wz3hy@%9U3HqhF}iE{5xd zGRx!njz|++7n3gkF2$W!5SQ{~BIVU(4ICCDRwL!77CSM?s`OgwI1m`cMy>XOxTHG0 zMzc9QuF`|^)*RGXKs7?O*aDAUIv0vweL<9WRkK*gg1au+E)4F}R9?N|jV5qw#ra)W z)5AX&3#FT#LVoBc>4+`Kaq?*MR}#=?gZE1xu1t%K{~3E^86& zS-~UEXm|(3f*=#@uml}q^(7p;F##{;NkOGnq`M>J`P#-6>TT+HcFqw|qjqSiRs~e8 zymGiT$GSG(wdD$&Jz5KvB&p_S&;D|C4d=>y=HVo&q4#Qs8sTynUL+BXZ#CSVmz+f< z8OU*-YlZW&xT%CM;aVR?Z(-Ct+={Qx1AD@=8gaehdE1t`7{jXkdULX^IvY4kq@3UH z`mw{bKzJfO=8)#zG>aq3TDb-ABC41__YQmHoQS973McN{LMY8)KYRdpxN^>Nhw6DKc{;s7}hEqdBlyM*T11K#4I78$mPj|XHP!Fr3SZnM}a+W$#nV*>um(j6~e zeEj&u*PnatOX%4$i<#qB{LiYhXqlbXn2Ex4v>Ii>QuYHZO~GahvMmdns;I@^Oq!7y zhcp9oyt8XF_ldRU;0Zh@yJk9vP0JZg^daLrHu*rY2}6&Z@lSEkLRB|*PEDU zGx>g>N495IZPY@Da`Z;Pv3mF*;4KZ4(>W?a#ZS($nb%XTJwo+}*&6W_Qs+9b0xd#S zmL(*)mZD{{F8-}WP@FHGtGcVs6%L_jLlqws&vO1CyVd63tE;V?-F7^?&ILQ5YJQ6T z48olSikYWHj4d+}qGfgp3?Z-Va59;G(Vt(>;qx8|rcU_WNJAlN#T}zHN2<^xz}vQe zHZ`mBR=IA27}OsWo2R)j0oSDZ?Ahb#klTsONBldxj)6z51vt7@jnx~~mbfaPz$$oO zzVV26-mVbU?k*j!&t~HTLod9#9%)m7J49)REmA>6i}CPyoA!_S+{ZvW`j$J#<_W*d z8$ryZI=xDhQ+hO=mkuWn{wSVV%nLi3=Fz}wh-?~Cjkaa(X{8<53uTK#o=+{uO1&2U z;s@#Qd9OZV7!@`({PuAV>;aGD&|jUm7De%(xkaFu?ua>fA9yO*3lq34UbH{@qnFWP zCe=5-@r}pRaemw>xvL_*PDXVcYv-TVhPxMdjg2cOj$qY%OH}4N!{W1X@T?PbVAWp2 zyM;^DI)XAMYbM`m%}%~(u|d^ag(Y{MHp!jvfdgT*9V*iG4mQe8SvvFjRG})2ML)6YdX;pN^Xev#4CPNQ(XxdgRWg;87^z zR_B2`Mr*Llq>0CJ-c!6iE172^=6U3v%03kCaz6L?`pXwz{7?RqnOL`^`ucOvy{r~? z;76Xz5$BE&Y&uqtl*61XS`|i_Z}3m~g>pR@DpF}RzLbm?EfwPAdD0Sod+n?`Z&z(-2+;4EnY)VCZ5*lHgG3MqGL*; zbID@nOChzb@?qvig-+oR7o?`5^ zf6uR+^B3m99xO=SSdKjmE2& zQ@GOsPPg+2_bcU*`g4vRwwc363q#;E7LHNuBRkh3X`G2DpKb-N>r4%Yw+-W=Zj@$rK|1A z-;KlES@1(hH{Y|fMTS83B&H1xvbo}ZW6J)Xc%tx!0DYhAJKq7>21BMS6yrHlTpfqyG33IinTR^R85px5uUh>r&wicmRiF8>Iq4C&dkN7M0Y z+DYl!A52}D_D_Y#vnjYF1pXPq%znkq`kjC*x2=zJ)Mz!5UPTGq84AHokl$n_qUEr= zi>l4={=-=)3*Oit&++@One(NvUN^wCCWdAw3{Pz_S(UIYYnKX#M2P++E)q7T$%LfB zdLjY|w?%5+d~XlRWM0lySapAR-s*=_`jgY)$|j{c#}ONmBVh^!JBKUhyKAp8bLYTm znG=uCPZm5nD!|H#KVHiW!wBNt8$}V;jx~w$1}TdVp;H*uu_cO&Dz-|y#E0ul>8Nwg zi{s6CSm5N)QkpKgQ{*^GU@C1^t*lr*MXBy#wEt6z*i*kP>!26bC;xAq{!OcY z|Kv>^`eYVogd=#mVSE)~QARlLnXx?FrV}`-4#ca#3?|Xuh-u%-A8=8r!QB27A`)!1 z(8+8OkIl{y)plq59|PnS?M~jBuBlO z;xN(P1$mw%yb)hHs>-J-@oh$VfhL+y76zh=MSR7NEjl|??MV2WXj>jZVmUpM#q(ZU zAH_(U4%`Dg0$kHCD3Tk^3*AwrvU`ijJmNx<_ol-X0djgwzACL2K`YexUXyBqIn}K@ zo>StBhM$E=taLDRJzttt@ACx&@P}%>Kub!$E!nj$)Hm)}UM>V>0ugKZz zHiXw%7iM^#mHo35W-X#UpEx{{LLAB>woiYh$WIX`r=mo=zV&^~O0~W( zt4Gaf+RDy|yL2SDfy7Ln)Iqo%lyiQOKog8YZ!|eF+6)Y1q&_BRJ|_=!6#VHqX-eL? zyX^#~W_ahbCK+3w4D*104BsvQqe^xH3jyUnLgJX$i0PNrr$@Tz#&OwQ#bJMVHtFp@ z^h5i%2Ut*1NVIx2hfooANkaIkc$kmbg^h~2g7g$BqF1>`!(1@1-Yxvn@>QGc`0X64 z#Y;%2?h)gI+nQgD!_V#2*0-fzTaB7@fnam&Xq*X1)ZDl2bM4L5j+P127%k)Ocy)F4 z(v{r?#nfe1brd(=pk~Rfs8_yKb?a=_I_pN;GK+`~+B}m-xgM$PL8wi%p5CI)liy;1 znRBg5FCqE^@B;hBEdlx;)} zx8j*F|Biy+O=WX1n~u{>#OpAS7LJ}o@scW_#@y|zXRx!Di_6D0c>JEm3L^G%L&I3vu~?`V|o`4 zG#`4(75&-WOEa(<&HCys=GztHpeY_o7IEZ5o?fVL5f-v zxO3b`-_+nZ54T&Wq>z@c;P6MC3zP~~1lltPUmU!-ht~W7o90JUZOc8d!040m=2@9+ zdD^77@gBq_Z?#iXz7BMAzB{7HxLAyk6lJ-^u|>SOk;YmCNe7M|x0zENOaI1{RW(NC zW_9B)*Oe*D$tOFA^CF88s`bwC@s8!vnX%=$z^(5_S05>73Jb;OjU>jfGYe zw-jpX9IX-8!_jLpkMIik-754~3Khe0IE0$74cr=vVC3iG*d5b=mBP3R*h~*JD|NaX zEU~#*k*i9#b*|7N+m`#GaPsXxa1uBesVWjxyTnvm98%mKA(loz+ISfwaZegzdH3R6 zkuZk|+zYz(SeS`<-nE!=d2t}mug+;udPvHxuuQPoImhE@Tf46|dG_r;a1^*TsiKKV zEx^DzA-MvW2o)8oJ36-|a>u`fIa}&L#fj!xBu+fCs@>Pb8)o5q0k)M2I}&AOS?$UM zVHfdomJ5tHztkyPRV3j)Iv`f*91<~f4BRztO{%CibEkt?V0F#|Aj~Ilr|{IB z8oacZad(;RJY&^zZN+&r^#InRz?An=g5Q&Pn}fXQQSz(}6n;);TW zIha-pqu(N~<9?Qnan!`i^Qj}|E7$6`&-Kn`*e-cZ#9~WC$Y+smX}g`d z*M$|zwCXMX9$IR5-95^)Y#r(^=wN&o-2kuO;Ugc+ZHNx$MKRb9P+g7-8?J3%k z^aA$=Y!^Kf?HostCbmsft_ri+7zWCP)GG^;EzQ5Ee&@lo>GsHR7Ui%$8LsIs?gBy9 zmy7$EQ!Q{BysJP{YR<{zma4^U$$f2wKj{Y2BE*peO{4R(qh$yj1Za-6-Fc57_#IgC zyg7;iZiYOPs1Mj4RU6|$K+Cl2s$;WZv+_f?{}_5+F79?tRk@WBE-Y4>O~m=;1I*rt zJJpFTM##N5+18ta&!&Ok{As!N=0??mrRzcA-hU3YV7KiF#y| zDcnZeK*O7_TB>XwMbd za(Q+~rBYjiV~+2&cfP)D`#+XG;%xh>BgdVS>Y{@4=u|D1F+GaK#*HIlG%exoGN@vi zP?NaEyp;P{!&9$Jrs$5O>yfgTN8n%>t)r|!TXB>4y%P?o%4*^3z;jv^S@J-c%+AyAVEZ>GZtt0SYIhBo$W^+5^TWNHe?3(5J=D18`I=n6Z^!dN zIBG2;UNw@HW@I>LLf#jxPT=UF=2bm&FPn%xH%_;eXW-t^B+?zq0z=-6AFUQRl`~J@ zz}ze=R92ZS>z236S{2^fNS!#U{kQTP$>-$Se_(GM4XIYxHOj@D3pQN3zKJS2c59o0 z(I1uVA-njKqYE0`m{=jYfM)`BO#E)Xfd|8)U67T+BRE!*MaAMEmKR82Tf}ykIZbTG zqD><_b;{Vc{qL(_PpHBzhRXmDTC zEYevs3f;E?on^&SMClRaC~=b2VYPqYsGvv^l`FQ*Yp)y261s2wc5!zBOsyiHtyDz1 zIA6=P=Qi!%BR?isB=;=R%#HY-NxD_e!13W|&A$~$xvDvu*>sdrWv;@l(-vWEIcmj< zf9xa22f>KR*E9DrQ7Q$DexYnn22Cpw5GZ+6i<-7h;EmQOB&fwAMVfO>l=q?AzgH_$ z=V+AQZ4fQvcySt}I-pp11iS?`RBkL@s@xcEt6t9<-XP#O1U{ z`waDL578nXx>b#s-Fx8N)-L<%jfe@lsu95z>7w*uwi@Z*PRC$iX)Eo_U%D8C+j$ku}H(jZ@F;?-! zwfDoU>3Gft_?sY^syWvZUd**E(h3{}dMZoCVMVAEC=8@BO(&@Dz;4}d5}dQ0E2% zb2M|hVr~UTx?V*Y8ll^TflX@Vnw97T>NsUQMk`L>WWevTs*h~&>R_xBiwz*Mem^$x zz*)6_qk%f6o0i+6V&C9FDbjN7)mV>k4}pEsSrg}>l=HE+{^#rj;VpQWi8U>k=`YMl zR-2pT2pm+E8jU7#M89|c@Jsf-zx8MBlJNSguh}=g`i<-J&GEIbzP3I8`WL>oIseAS z?KOLC6Xt7Q`0Doj>tA@)zV`W7ZJ|n)sn|QR`VAZ%xQ!|0sUR0MHT<4`*9-RC3vaQv zz4Li{^V{FD`Fpdy`Gq%cu3y-Od2Z|W+_pq-e&M;z^*2G;CepnzGk<+kN3rF9eN#`T ztFLY9tZk29+t%yru&-|2J++y|Ih#DQ?ME~!Juh5Z=|AE&5@(J7XxL+I@h3^DYNH25 zq>He$+6>kR=h-bMV^cD+2!K7O$RppZL#afMlf|qQ^)k$AHHUP_}N$N%fJ1l zE$Mz`b3T#q79M%F2sH@zC@`ujyiHuTq_gpR{yUzxcYODYTXKBIcYn)<7dY}Gn#NR7-E3JJ7 z9s3ScZj;AJD^=yj@31c3WkvOgrX`)xwjs}P%^#t86+1A7=QjlW{Emd*_2>UY<@c4( zerc^sR|t0+JYEy+m#^#g#$6GgG9O#RjS2KN%JZ)Ku916d#o33H6-Lvs z!E#MFNmBJNvQ^<6)@t+bBAi;%FBUhc5o2`Yhs8U-`&+liZ-3u+%x>2m)R*ilpLxZ; z_^IFD67H;M11E)vK&Rh$QlQ_qA*AW}{0b|_so;iiH-8_vai0iwG9`a$J81mUwl25S zs7IIv;myWLW*;)z2WtpSDQ(1UiLjT>tD3M%|YD{l%hlPZKo%s(oVtF<3I zx?SR&>O*20^-rV!FaFC7$!>^t>Zro}dwiHk^xHNnbRw8Pdp<*r z!oD*|^+8cc_VfSvcQ+&(O7@{pl;}JE!+$SGbWcp`b+X7zWIO$-zNnz5{%pX1tyy$m zGipni97;LfCni!oEAua+{3zNpxhsfK!7g@ZGF^$$z4$fr58Ml`O^V;Tu^2qsUQY`4 zcYo#SjfxH2873l~EH%@-;U0iM6}a@@%2|7&2$-XPKkL{YI5wkmc*vS0Z1|7B~LdA&?|&=i(gdy%DD7=`D_ zYC(HfTrCb(&NTNu)nvN@|ceE<`mH1wFtK7zt0@3GUtw_snS}jU_Jjs zq&4BJ=Tr--5s{-_D?i-On@q)XY$8-;V!4=+%9&w$ZKu4t^Si(Dx$PY6)FB0~pUt~J z@M!xUamY6}ruG@l!jI#)X&F|Cpw?5yq{gPdsWbZRjd}R@FVH+(%)1kj zPMa_UP8ZWwjW7M^d$;qh6RAqo=)6LhK}ZzQf&QG1XA)N7NzgM_dxM0Q8dhat zVl|4}Qq*Tpsy%pni87%sc4{i_lwN9F1x^!FXY_qP@$&X>Qm&JEcrp{8GbU9!y#n){ zfAOVIrDurBs)nS7grYuDU_O!ZUh1P}WOa>sdHyXEbF1fVkv|V8)}}Y%;z`xRVuV6! zmPcyU_Jl1~rtkT&?~S?AGe@;WtLV#c$}H{HofNg| zsk-W$w9N>dSQseodh zHFWCvVp=`?zMuND_TC@+KKt}9{N^>ep78EX*QV}28Dx4PKw?{n0NQb`^l$TC=%w!? zlAF`!`PXa5B`MpIXiL9}1UKhTVfbDaW@6e}wq+`2qUN;OPb%cy^Lp--JAy+Y^)`dp zotGae54@+EE!65IU@{FCa{V_OV*R~e`P|9OyO#OV@7m0>g^Bm_P%WyYg_^8FOSm$> zIuqqCjzKtS0v4ubr7)FhE@jP>)-J)>4+)PIu$eYNTfXqr2-NUtTxe7hvC#(6J9agv(zPyQ_=#{^Fr08J`B5xQ<4NU3(}lHC4MMHJvVKOc z-|fOr$1Uax;+X~jgZW4a}T`$;s{OWL3Uzg7(y!j6Q2v1q z%Jzrf^`q~xfBI9O@L$2{3~Z-YWWM*W|3H{}1AC$LZCMccsXF;GJ^k=HB?yO2JTZ5KTK;rAP`_osDXX4P^aXt1cIkb%?Z0$dxCkBiUiNa8kHmn75(QliyOpCpvYx@qTE$_ zc0;B`{i=RGUa47$vQ)b@YPc9AB-OtTU3z7cJ#?%|DdINU>?7fs& zkSZ|OpUUL$p4dPWEt`9@V+uA^HsQ`?<2Y4ZQmdI%t4eoKPMee}MGCZmri#j4jK1P+ z)xcUzs`bzPmp^FVyDN?HJGNt)f_O0^eau>?ZVMB<|=bZ&}!f|*v_^7 z{9oE`8u168`h9!f-}fIDS-4woobk*{L} zx$e+?>_Y8_`8RL~OmnT~En*4;?ir)vV(;|5^(rg#@D`4GWZNa!PPq!M77;xAa+rsk z(95^unFRuY%YoP9-9}#YRwwuJ>zR~QxRLVmJZ`}asp8UP&M?$!AP@)y0Bcww-!lGM z^vJ!XNJpmns=O$zTB)*HEhbGg1BYTALJUI!fxtQA)@EM28>Bt*Oao!iKQ-cCsTXXk z^H8mOA%-4-Kp=4c*lY2WtpkQofoPT|DKH=-1e{~N3+E>4^^=EKh-X7oi!uAt<*LC@Yjr;T0 zb@{uZE(fmBC)Jkew>O6xX5d?4=#xZ1qUB5V^Zu6QgT1}@@cq{kwnebGg9wNJ+81s* zD?Imb-7&pj@A&R--E>&bZ;mh8w`|UzdwYB@G|F&iQ1T1}4vBT7 zIzhc!!zSU|5NZ(eexMKMB3VwyLaYU87j8?k^t_RP@fwMhO~J|}+%2-1OVnTa?3cEG zpa1pWo82a2{q_yXe*63WxP4oY>{+9b=;#0O|FSjXPJ^yM;1sZ)R2hd}kQ9Wv3f{bH zR=Z?D*t8?lLYBsA)oL@ZIZ9QTi%S~nm&}bgJxm?zwsZZ3O~u{JY5lR6zr)`7AH91+ zw10e_^9r0UCiCqyi2A}m{=F@cs(H>p;Mj3{QpL~1v3uaofoils5p5zut^DkI{x#PT zX1_Vib%xhz*%B2ATmKVDYk}##zUgqM4!9`UNyWbVhhGYUJuyr~`n$jK^m@SbFaNCt z0;i63q?(+4f?FFxFh+M^3Jk;hvVC?;!c4B2=E9L2QMIZCN*Kn3U$fj0cEo(F@+@w% zinSovW5;CL{oF5pdP|~V)(xB+)-O~-D3$7cHg079;K0Ga#9Ga#;%eWbI$7C58YMma z-O!H}>YO&6vst;d@Tq4$Zq*0zA`P51ZfE}W zc3;9*Z@h&~>WJ|Jw}w)x?5+kqC2SJELxROib9if$oSH-R!h57SLZ%A8`zxQ@{!Q-} zzW1+vf2h}cMp2~`nNIJ6?%n>w!WB3X+e^y( zi1xoGTp;j(I4r61{3@!}Lbi%zkubU8#0d4)@D?$pYloR2XEUvMC8wHa#rfHkh&=4tM*L6tuP8jy-wu%og33|yqSQ`mXlm56yhHz3VoHZGT#mzle`si$^1&Kh%a3x!;# z`PNCf2D$E!>u342o-bt+Z*kkV{i}F^z>(o_r0Nl9@nd)w7lj8jG67u{HDl!f#%aX>Qmg zlmhkIJS*7N9IQ$!5O_cwj#RxxOYEo2zbZ~Nl7nGEbeXmxyz!V%x8#@VIoE1MF%5>0 zloObeXFpmkjAdg2O>}Nf_*Mm_KaWM=Inn~WR$*F$Hl8Z#^$qN|wy*HQMxPUC;L7eAj*^+rTV~@D28p-)sq_Lv zSOEAy2fzKq_?Z`_TjpbT=W~dZbRM+8X{3Ca+*@;~cY*RM&j0YA{rA;3I4=p4srUc= z*ME7OP!V~>JZo`lnT@$FdVVV3p6PiM(dPsjxF7Q`0!N@cI#vF?IoQBzaJQ0djFc81 zK^O5C^RB2);didg%TO+lP?bvMFfDlMcN7!3{trL)-`h(+`reI6`1>#7d#|pk_cmy? zf3MA>dkJ5_gLFP;RmpuwPjxEg9Q{zBf%_*_1yxzShE=Wu2g4ddoe2}?S-tdxqL}AQ zg=$s5sxlR;I2n#!Iv(;sn1X3%plK8T_x-Iud)BLjr~CY^jWRvxmts7nsODHl&Y5IK z=43sR^~jP<&aFAyK)~+9{HxHLW91R6H@6p+`QDJh)T_2Clq9(`+fOlPGHs9c{TplLX`cHrA6Wi6V=Y~n0PWSe&{_B5n_)o8S?RKB(9A&}c{y~vAlWsXG zcNNYnOAPb_J3I`jBGjl+jUJVXRc55X!Qjqc0>QG|B3!i4+&Pd) zGj%xCVx*m4vf9kM*ihm+QMnV-yy(eH&Z{PI^a%Ji;I#hLfAufyE1!AAzV9b~;G`BY zO)C4JfArVwE1&(69VI-H#C~8xi`e!xH;<_37{~_*53f`ws1c{~Cuq%y zD&*?`Gr=0o$0lbW#6-EvXz@O43F8qer?)5@o#)nlbTGZwI=y;wD(2v6uinYrdsKv~ z%)E>dv#ugyBu2Jyp_Poun)?_<<}r{D>~I{UiWo7cv_^})${nP8D~vw9M1ve@=H0om zskfMTGsI`o%egiU8_n+`DJ$|^l-EqIGDiPa{DR3G{A)k)%SUexp3J?|`?vRQ?(JdU z_Su^s<>@2NQ?2r4!c_5FR<^)J;yBE|EvQPX=2pFmEzF96yMwn4;^O)`aAo$*kVc79 zEnLFzWLB<7Td3X)=SJtsOq{5!JySEfy*ABJZp^`xVm-EZT94k`yDxh4pFQ&Ph*p`3 z)u29zHL$~RDOE*y9-)@-p7}S-G7kbKPc-u`Uco732uc;z>P(`$>*@{0x)aq|81<`*taeNDvM&ZqGv>E}&4Lk_g6kObji8r%J-)d7X@6?0?u9;|Y;S%J- z%Cx0368(AMdR=t!pUlDcxmm#SkjvHj1=m)pGlfeQX^kg5F>yFyk~xwbeM# z48tnb8&QoAZ{W_r3z@17Bd#4vz?ooi-ZaD9R^oK=`o?47hYOPy;ccvY%#fBcK`Tt= z;ECw&+8q2_KmVKCe5U*Yw?u0Yeu{lZRapo7LeJ`V3b@bT=#T#B1kAsqXerX3xx!n} z5V#jC=2y+?jUIr6ab>VUI7n<>{acY8xm9FrTBVwqTs5r1^b{|md`@mUzURlj&kl)c zccg#*(SLNg>hvzqBc4-JZdGM<`o7VAP{rZ-=DX@sN7KE}{`{?PJz?`NL(Qe~kE_C# za4Kvl*ayMLp2ECGuz{jASS7smPpSDTO_jDSp_*iN(kn-G&X~SW`fGpvR}N+lF5f*3 zLcM34T0gN^JRthM)<8j7mGHc|nCpP(P5s?BcKM$VskT=6vZsHVfqO@y5pJTn z)%lo~`wVBb@*B*^ilmi#HE=_M#5hKN3H;Q#iS(z%OOu)Q3%~w*b{kCdwPEgkK%~|$ zePpQTOnh=`xx*Ap<=0A=;q}M8upZXrw-0*pcln?)}I=+P*RqctGTq zP2xz=5+VwoDrLWY%(84<#I!i93a6mV^lRgI@x5@HIn@QJo;-E%2uk^R;rItsKYHL^ zpz0wnZbDOUaZ|rxwJ-`6ILU9f0_hgN)_f|*Z_THACg_6cQ*v9iIcN_4%4fc`HTO=R zYzqWdgZnqt?-gKw>4}Y=bx?&nOiD*B(GN9l>SVrXGNwA zHBxDr85$345jJq=XnqAJ=jEI$xuGAD;e?qeA&+8y-lo6{qRg7F<0v9*Z1B#$0Z6H&K3% zI91-gGQRb#XHKdG{4fjD5ATh_IJ|oBLALjTJUaNn@F`XAzgo`HM4OmUZxX2*VbU3G z-;qTni)pCy9^p=_f~Sw4f9GF($v*er{cHQitFJXz6bAyUpa{~N22&*36C;X575OTn zQ)cCsAE)E__XZMNes%5j^=GyDLkm4JRim(x(Hpy$ctNfYicF2R%$;WXZMVcmL^-b( z-bJ6o7!Yzv8uBdR6iFr#7LTp-36nCN-f2~Sh-^W&ft6^fO-8HVY(e#@!fGV_JhE&N zEa%(mj}_sovUmdx4tG|YKeW)3_pNZs-ao+`Z3Uqo9kMeB;ys2@sZs(r$6UYegQ|3P zwpxK^;abH(t!Kf?vJ~G^^a%+{-GJJA<7Y*8(P8EXPpMcWI=KKcA1J z83+t3&Q?56Wc1Kefl4WQ=U)1FRB5*2dwqLDNL5O8kH>mmP!&UsVmO={AqDOgYY1Zg zh4RhD5V%1^f`;UM?WV+^mFO-mzWbVYVN@BjD1VRWawC2(kK737g$Y~;_U*Gq|*66u~v!D!5!g~)L%?22b(+V6F`1KleUu*_JziCmNOroex=X7)ZiMdL( zQoZ0>(yBGZ&m+v1OKXtYZOpzyEs|8F%vhV$C-%s)EC?8mPW#f{eHjOCA zVq`k2)$0J3$?}m)nj)WVuyT!gJA*Mc87rbKX%&A|u$&*K!)c7-E~@ic_bGufuo?Ct z%doO6?&@D|fNSP^&Kmrn)g(NOeio#9q_|{K#ktj;Lrs!sg zEwFxPHpeQf1RN7h1$c4h0qJf<@w_jw*<#^RrZ@~H!z2heo1V7uRBax}r-wY4)`)u` za1L0I>d5F-h38JsH_ru_aZngHcEa3UjjPtpxK{ngvb8Qx^{p~#Q(B8MinuLA`ws-h;O_m)jfh9WeC~~?TR4jmEiP0E1a?@w z^D6vt1#y{|E$yx{+wRihFX$i^qzCWFqKOvITi`Z+%=Rpww}?lp;3!%UWih%tOc&gL&>;`3zI%+DHb z_l+3N2961{`L{qdCJ+0}(**SdP60!vPBD;DA51s(?kmluRwO!L^-%M;YN3!iHxbU9 zIL$IRM{}+{w?N>WaD`i`%}{OvX~2K&+)W^GPLMk|U7y^LC%sCO!5_ChG(6`;O%NYf z5;I|)wC(^GR|bPwc+L-1 zuvkT`2zA8T#N@kg`{!~50;h`E{L3R&bTi?{CLm73Al$wPy%#t)EEe;Kg-y7r1pDE~ z)D1V1fZe<@ZH34x7zx+tyx0Cw0ju`Uq|XXewP`j>n~2+TQ~3k}4}~jL0i}5c=6A|b znRxO*TBC>&Yv7!rJ{6}{6ekKhn}f^iIE7q6kuJ7EG*GETjFLPFyfvfO{yjf*SNAts z4iEcU#R7pt<7#f4fNf)A5{38Vypbh0niw$#0zTZ8!L}q(JSLuZ#T}d|Oop_SI!&q^ zc9`~G+<369RP1QKAqjsV-XZ&<=+^=*Tw$Y|phk>p&b4K#@kYS=A}Vp<(%=#9yuG2= zWI~-+ijjy|eHcKte@d$makKq{zo$Sses;oV=bHN&rH(mI4{HcHlyIPh>m8MFBuq>1 z7w52)zqs)prZ{5ljZjAjGb(W)P(y9t-wTHem{gc<`!D`wF_oDuc8Y?$Fpa9ez`RR% z^9SiIQcfbBH5iE({ZC*GuBwe5$|8u}xzT77=XSDiL#WEiO4qDCx(wVS;x(X?2AXLT zw&MvznkE!&v;FU0-@$a}#ZV)<=B(+HIsZuM0T$b?Na>>ne+BS36eankTs412&8!P{#aX#e0oD^!}tHQK&>wyao!4V1L4jEF<`h`%%sZmD?Dm6-erj!PfCmVRvuCIOf{@t zqni(0B1Tl#s50|b6!Yigl-af%w|Noy%J*KuEZlAXJ3{6@fzQ=^X^&vn_8ZA5V=Eoi zp9KOFuHRFoks?Bl>}U>^$Dn^DqR)8}|)KLteJdbhawN{ukVlm>%lAv774xT@mL zBRO0sVKKTp3B>&B$)f7)*O>Ix!Xjw@MyB2pC&AWw<_!A5~wpILEAH=|Vu zGppQ2PVO`>PQE5?Qwdx;sa>6;-%w}UsE0Z7Z=v30K$W*-YSI#RFcFD_ZvHqh~ z!_$C!reh!=Nww8zj4JkAX~haa10tT&w!Y%zk)}CeMi7d25Hwf5g;FO$v?qbvv=3IS zYLDU)P7*G@X2WG7Q?Y!fI;25AIY+)BI*M;>Js;=MklMvD2HXcV?IF`M}+cQINe|9}AhrucW8v;rrE z9yv7UR)u9sO)2m@vIeAQDJ}eq`c05mS7W`KXJ z$Q}35LBr^?IMg6y&45O6!(CuOs@~2lsxdXmsw_mfPHY~bmWG3A^yV&jblIu^t@OJE_{cO!StRLm%($lDxNZ5;;c$^5>lgwM1{N`+CIQ>6Mv zs_^RAySGyTiy4J|?(jbQ*foUOF&AYSeczKkmMw`?yyb)q?%XEzhMS4q=&TC}x zQY($XSz}9nVzuxZpxHO8baMT=-)wqSi=HU#oF4dPtHpsVkLiK`>_*dD=Q0>y^UK|) zkIi;iGjZ-*tAW%Zf%mgFsqzXy<;U-`qAAB%4?rz>Rk8yZd(StpIa-{W$G|*Doirv8qLQG#YvUBS&qhe zuYv>`K=MSAoq%Sz(GE4{Spx#SmBp3Wmdwi7dC&a`4hB)FvNFYKEL$q>%JiGy+p%VH zjmN3h?op+U&!a__z4#-3hj^Yn<=QGkZ-Bw!_7T3bGTVC7Yk)B$(FjNjR zS?L2Sp%j2ppLktCEX;jb6S#g={E{-&gNyfwk+O!Fd%e0 zCDh`kc3lULjIxmXUqXUtJ- zIhx?>mU&^#2xJY;hB8{FUL;pG4fASYwVF5(7(k{)O!KuvuHF<>v3wO`EcGx6^KT(e ze1)gA!WXS=rwA4y2=A_PCp|oGU8{5z$+w6tJL!0Sr5t!{g;&Jf%YVYHN!8si6dJ@r z<&;GOX4RsD^5Ag<>HQGi4SP2|I#N<;kK(K;Lz#Th&6|T&2*nx@z;<7bEh`xB^oC(v zGcqVqCf?SdkJo!}zNggo@uPc4E}rtrpc&TQ+m82f33+?|w#p z=YyDkhXEtN8>gENnn#UDgrlPp_0W^`M@j*K7FqI~E5_*&E2kL<*m8iLg_|4R3n#27 zd3T+8%|Oo8dXRcq$VQzIQ);g0F!aim;c9QJPwFbWPv zsv|E#Cz{u&V(|P(A>W+chtYcwReHj$Mc4r|Y5w)*R53i0am$Px2n^uX8!b5V{fj;% z*PHauBwLO}nUYFAwE??V-j>S4FzTAs+=!yJM%W&~^`Oet5^6oaURg?*gOTdU+(Kn$ zfstaFcV&`g6AdqU=OaF}Y}O@Xv;L?m(7_1l;;Rho9tiB9I^%+>uUM0&Fdw^{Q_!Fv z7b(qg4f-PRKdre{+8D!h|BZd9r)u%qb2lfA)Ch}ZP-Vh%PdKS`M$0sMk1GFrG5;oI zLM)Y={P0G3iM#sXIr`7xoG^=WSrF=K@ccNtd&<;HXQVRt zJ}g)?Am=#jrm}lC!w&`1E#XvI4%tm#WS6fNpCM44dS+yv6Z1^Op=bkl0QKgAB1nA2 zPrh3}GKf4<6qSnR@8Ujzr|hFlo?h84Pys9puKxt}S`I7O-Z2okXK??tMM`aw zJ#ti2Ye{@UkB-(9jqW$S2kA}2LcDgJ~gWh z^wSB~%R+1ulH*>Wws6F2Hj8^|u6h?4&v$+P;`5+8(rtZ05c_Xdrix(kn$u9^%lV;u zHe0CN`&p>uxHsQHy9s1!d$9RNQmTI_82R9J701iV>XDU))bYPq(%pBoj_OsZ)^gr+ z4+QqWXzfv}hEHqN?+(Rq1eZ)eYQ9;$n>)uzsahk$WJg)w5O=!y&v&3D*+vW9O=3fPOhZL~qV~3k~ z#jw%HB_*jst2|AVqv6*g=RgC>JdC8Nju@tu4yTF!VKxo~Y9yk4q`b<*6s5*_(#`f!}%S?Hr>aDh-6!|F?J-d;Wv+@%tc^wt0PC#2bVqtE;IG{!o zn1c<>5S7BE{M3W1MoL%?>m#(0gPw|11d66%l^&mus)}m%B&J=G$TsgtRI@jAJz^^7LGlcQNyVf@PC1^2JQ{)>gl>_=0i)U4Fjbjp zajHI0T#wrDwhXlD*ayB*0=`(M~Ve$+<06cz$tkHU9%hWY}QkAb6(K)DkX_w zb6%|aWulknTuPf)As6f9qMxuEGUeAY6Ac!sGFkO-Zt^*HrP>3I5Ec*k`egA#7 zURrY*60MCXX$z7?rONp+X;v0aOfS`6Ai{TQW%;0=ItfzEgB{3|QqQEtN!^ zUODG}9|;=DyO=Mg!GM|*#&9QV{++=$1%J5PMplZIAC_75f-?UstYpi1HgjLk2^;_( zfr*i-GyxZPl`f~{<<&AHI}I=pIA^e#bqy9oYT7(p^6}D0OUE11ZCSIZ@AuNK=5STo zdAXSB^hvexV`j34qnQ`ySSM8Px#sM?KBIC|65B0M?E|V&hK1#CWlrf znw75IsL>I^4cswQ!&2(Cg!RI+y5$jgkY(VEp=x*6G`y$ZB5!3OQejj+j>>FdWI8b= z+6H$;+_NB6b>qCOx^$RmPtRfwZl0Ll#Y1y0s@K%H#$(`~p+=CYv)D-~+2Y0{)Ii|O z@SwFi=i#~CvE*)dH@JXlbdhX{H{m%uQ)JZ<(!;J6C(#d!>_xqi9132QCQrN}_bV==1G%nAhO|&cpoM@>7dS zb{$}~K{Ao-;ytKN=UN^v&bbC22;T4Hg+X`=oxC|$jJGd%m2wbkAh0)j3#>5AU9_~b zAXNodwwF#*EL2k0^QyF>d`kdY{8YJF;6X7$N^2H4wZeO6v(2uj;6DikmVidyAqJ!3WU>0(XYBt*aOQ z>`B$NsjD43GJUln`8#u9oOuP9)kGX!;fVsxx9sj+v#A(~l;=#@&C6eU-fm{bby8yPqwU>)#GwcZ>ps#IseoNGevh4JzU1OoSpizL<2u4dklUo2+A zZY^Is&YfMh%P{?3+>oJ1Rvw9tkRoqF(K0OuSq1_}i?iKPsfAH8Xe!NI@4&=FdupBE zKIZC+>)KhRs`pq`c(u|P-PLt#S0}vo!g_H6fxw;OB1v^`sE*Ujs*m1I4q1OF_m~mg z?n?C3C)SnLJJYQ=c)gZPG0hf&bOV8dBIZ;Hs(ayY2J(vF`MuSh+0OeCPT)eo=3Dk- zMcOkbdyAtmjEXl>&Zwh-z(EnDN>JUA_uW1<--_X5q0)sx5mak+ZaMenPmyqyYTc)P zsfAX7K;U2qQYGxu%~UIZaYv>z?X+U3;9wussLZ;2jVp!Oq`X$Krp{ukKp=1{1gR3% z5Jvgj55Bv~@2j2))|525<68n1sJHlt!y`%@Z$~FyN$g=Czp%ap0!;*|9sq?*8YB3GaV7Ba~;?Nx2Y#g zxUD%__FtQ?T7VVGH4vz96^-KVaB2&cN@)`vTO@m^JTDrGtVZE&458HRZSz(ul|#7( z0u`Q%MiD4T4flOEhqz6ifgRS)dn&Uqx+_v#`yMT?MDCfd1Az-dn12H^vYGq+*yu7( z6i9w*K@^gtn&UHFS?JKfy0#hKPha20{#<`bMBo_pbx8Cj*l zZlPoYCx|Pz0!M|uD5RDCy=%{{hp2V8M9b9MdL^c%Xh-sEy}F~q7xV@Jqlo@0a3r{L z7aNScj2<{^v{Y;E`15hJ#)Nu(0JKcHJrYKDwE+c(^-R*e^0W|BSAiqKwfT2sM2UzB ze0ER^m0GXZ;1Nl)n29rmyG@OWA>18(RB*mW>lg3P&6qw0_6O-JTc*C=fDIfxdyF$pU5+1fIZ`x5o~4yWM} zHlO#1zmZYnSW^^MtvuZ`J?K)jtUnOu`+GVr+Q?I*Y{OZqRSk_=qop> zPiQGtd`+iEnrIg0v@lL9ot7BDdl*6VXMsJT-%~A(Va>d1jQUO*GwJPq$B``-S+SxL zZ7p0Heck3Zh%WI!=*@q63zdrB$m=vrx}G_jm1)n+tn!O~DsacRTFim8My(!t4KHvS zfX-^GT6*n(BjnqPKN>f1(YTVT zR8@9HYsFyTpRrksCuu~ zn@d$h&UvVOduS1FB|s(tKZn3fBo~X z&d1lj`i;%sYxcD-ygIvo`Llnpb(`Q3tXdG&LJ;Z+!6QXhsLE|~eEvJ0x97h3E%y97 zUzm?mU7hOe+vfUweSFKt?YR~8c*0ayCzSd+)z#NG^>}iBeOs5mx=l9_xF*#Wk*fI& zMr#p5;3V+!-~MxUXiObf>Aa>+Zj1M|<9zwEU)pw3U;fOO?CW3nntf$+oJe?bA5pz) zi8pXWP-+#(bgDz|_*37y8T7r~-uBMtwpZfi+x}C0DxDBBOA>R_JpV%Cy>4tax`S08k=(oN69iaw~0U?a`#!{&kv`jHA z5>{j!2;31SMKk?PbHWoQ<@&#V>I?S8PyJi_;wQf_WHuf})aF}a*Jr%ryI!_4H zCV~6IWMz2gMv4Bhjiq7wU4ldd=ZqUtZJBP>t|qyX+Tc;6A>6qd>oXW6GUO_B=lx2pk;_$XU--iB7J*3Wc;2IN=zU$BbiH#}#yF!g#I$G}rA7lT1z%nh(zaD|M zNLDRoQtz92FL8(2IPd_NUded(yI!()Z{}kQ!T$Dt_piH)odP!~iuAjF^m~Fx&k1)$ ze)rkGD_*(>teaD9g`MCLWJ}Eki3W}UNU&eq5bV>x_i6k5um79vd7#ELNBYuV{nKIE zJvZ>0nM0(r#_!O0AhG6?YH!;x_HU_B?|oM9IuLk3Os{x+?|=RS+rLS@{_e&+{Qvvq z-@bUW@1#oK`=Rd-B0X31_8n-gqiLnX>6P=5wASJXxa2w82kNz&5gR&^?4d?QfxsQ2 zsIm9{wePq8bwjYf_4B`RaP@l33!jQA4fF09Vl7FI^wX_rzn&@f5QKOHT(Tdj_Mk>Q zc&^lIAaKN(%){TgF%M7g!%oVzx45YQ3DYCbV{8eNDxK~%UV%ANj1b$XHPPEUUJ0ny zlwyw)){^ca=2Z7k)ccdF_i%a(o!FcZ<5?X~*ibB|z0MnbI- z2Zs#=9tis?*N3RJ@7|E<_XL?#5il(c<|CSq7m7js&C1l@;oJDApj;z!q$VU8~lx4_9P|;V>VJtHc-QnK~w6 zCA?Y|&7S&J9Hz?6&jD~ZtAAT^8Gne@58AqsHRTAh4LlHva((YkxiYzO%^azzE#gH# z4i^iM0unvb?F6O%*xy5ogqX-$X?q0RQm3lyJw6yA;!c`(L#iG|evlEu<*=Sx5NhBE z@I07>|34r3we1Ji5iP~ae#va;)tm%*zwdAVxggVk2KKW(axCzO*gJAwjFI$;yDGb< zJk`1KQz{tq^NtQj-7l#k)Ci;{y;Xcqjt2rqh)KErAOD*l*}miY>7W0N`7?Aa#o8lJ z57WD)-}9k=-(LFB@3BCji58g_c!Y)5-cg0cYmY3wbd;ZGxVu+ROyPur(|?l-n>p89O5SP#>yG4K6r|NeG2q(EQ+ z#XT)j+Q!N2q&2N9p zcCPi^Km3w?>i_mXZg*!IA<$M0e#f8sR{OsH{67d&Z(uz}$gvftXTlvxqr_A09ER8C z^sJp8Nh(#0CS~s)8vOHcq{7K1FcQ8sIPl^$(l{PcWgtni?Yn0?_egP+EQ3%3 z_l(ITJSoibe(PuroL2Qti=g5anZQ}Vn@c?eNss2+avQ+$lBy@;`Mjr6(LKmAaKtF9 zg#XxPUVz*%As5_7SJ6qzg7GP)wK<0aLCQTgtzAZvvQ1db5z-l^8I zaRkq;hB^%d2Cy$dd-JdC@36fg?cKIAS8#-~6xsCwu*? zuWiZo`~NThL8#L};6AV?DIY(nBD|Yq;+^14a<@nm!*P0nzyqMnwN5L9V@ISw;P`P` zq>50g6bZEzzDKO;JP>#wloi5(K;ZasWv7akYULHc%3Rz_ClCk(0)dq{T~bwOya7I! z{CltO1OkCT;39Ee=3fuI8wc;wq{5b!!YxJHdOar)2m}Hbhsz*U1glx~-s{NrSfNe> zfj}TYxI9uV@ILY5so9Gw z8?ZVZd2Ock`62dGc?AN2K;UAqHUBcToGl%Auhq0(%VE;wIal0|I1mT~jvUcb-7U7H zda;^HDOROaIS#H0ZjosaZ6FXhGK3B7ZgF{Ys%tR1DTMcW&c^L)pMTW?fk5C`@Y<{2 zu)y6Rz3}*P$f+JuD4Kc~fu@OJ5^oqLIw z=)Cx;bG%k@de^GVs>b)$ag407Gulm@J%*$rJN!ZL* znQ2x05!JeHo@@KEefzs;>F0~9mN_P{gSR@^+i^+l$}~ZdZ4haofkaaTJQthMjb9Pl zxv^rc6lYPXNXAyHS5YY6j4S4E7tx*bmPt1crr}{EOunUGPl>NB&oLl+r}emMRsH~A zWKI>$#2(S^lSkmJks80dQmBdQ6%yur6m$F;lx-nf1GDiWFFuyM#C&UpyfWp7B;IQU z9IjyncTd%ErM^g%+v_eYthF}TG1n|hg_%>qZ5JN>ou)7DlVFeN5b{o0w;^s zq9sMR8BDr62N||pCpH5M6`EiZlZ6sCtCX;umlf`1-gi-cTM`@&?vOhPeywYB(%V=TyH@1+r9jJqrfN}BAwL-C;hb%8<8+fqdv~JA02VYc!Kpb|<(C~lPYpBS_WCrX zit#*k!gJWv3D?6u>Q|Lz-?$~tUG_MK~vDIp;iMGNOuTp)cWOQ}+GZmLm?&!lPASBvF1 zLJUm2o~q?I3?)o*C(b??Nhj?Y@4j$I!oExLAH~&3g`BXauovIW^8%&fHh~qYGX1JK zF|V@>)q3&p=0u&q_4$sZ6Ac8BICl;=(TWrKApsZn3~nXOg{n;Al^(huCO4%D?9&%^CaVui{;?dE_>U73?!laF+1q zLzOB(@T)lG*ozmq7_=5QrP2DqCS5S=R>7<`!Ls_~^Id{BX{iNF;P3T8ypp(_^PDfH z)4FpeU-lb?_Y92OviW@--)}GfL$!Dwka&ZG(LQ=q{U0KowP~)wAjxMEn5Cx%mTJ-@)&qoqoRcWa_~RW#dXQNO%7+7d2u3w66fA?SW}jf{0@b?NV7ReT!E`H zlJp2MIp-zg9?=Fm;Q3be<2hDm{wfe?p<0>ittjTfog&#f(sj_Agsb$_Zcp52xE0qA1%BR|B$I|?YXE*_ag%aO^{Tl6%7P`4Kcr7o*ZgzKLg(4jx4 zt%w@arh1zx?b4qZsaE4M0(s&tQHd5+aXQ#np{ckcW^1d#(TobR27C7JSoK* zi<<%sRPB`O3alt@0m(SsBsL#$!w5`w&fhUJGr?vom!o>Q?2<&tD#xZ|gc1&|r{Sr+ zmftARd1&?7nT~9h_QLTw;SMZ&Xp47WNiieQVfq-xz^v@4+LB)tr=;JA z=Mn)ej8X*1ZmfJU9^NmjXO-q{)gh23L!~ zP)WTXoJV=g@al&$>r3{FM6W|C#E0rssn&9i;dtKFOTTs9LJ;spAn*A*XE9()WHmuG zt4zzrre{iTLGJ11^&R}YU4ry7r;labl>tr1+k@@;L@I;oloP4zLipuc#Z_r$GdU%P zX#FxH?<=?Zc`pxCxY-F~*5(Dp9jTE{>$(R|y=Ldwii`0*|JnS@^BWi;QO0VtmS-#Ln+D9`S3!f2}ru=FH}Kw-tza{)(~*uhDu3@LRVP&S{Xv$cN!Rfyh5E#Lr5WLvZd z)&tM8qIubi8zg$s$hIDC$Ipr{(Klg>JA~y;BZ?d5g>$pT%{U$mA`_*>q&xS0XcBhX z*kUCy=3UIcq78As>k$zWV9t$sZA#b7CksAeKc1a5dL-5A`&)HL5xJQj45+j@&M3*6 z*=n`gO8?5lh%V#Y1&$6y;qZa-50F+C#>2=%y-4t=9u+K;CLS>kUeoA35V$i4CF;5t zvn?uHG8-o{<7S@M+@({I5iDSmiW$;MVoifmuLf>thAwV}5c#~oBP<4+m^lvj#U5FY z_?I+CNecV^+z88IQpa88>#1*520X6e5j7a__-(=!uD~l50Xx@ul$Fx3f^4zA4fruy zBd&{tBFqWc;jrl!{ne3`Y9R2yNS#68$eiIKJy67~QqkngQ!Ir5*=<&!*NNm3yNDAMFm|N*PY9bKmVV}Wp?hxl& z8TDT|FXC4l_=7?=5Eg9Ns}wWkZ&|NIp7Yp+sqPRIXph7^k}%VRhcP_J&PPTSmUy(PZmz^>uv&sy<@Ut=S^HAw=IaK)z zZeFS3Ig0Cx>U>T(Emy=XMolp4!%;~K>Yk zc`-hpn;=@R{VO73-oOC`4nA5upavSf^H%xPy)Fhp=9l3LxBR;HU8+%B?Z$~4jHcF* zrdgZ)M@JbPc1^sq2K;7~TPqx!ykKs9)Vkx&RiKFVb^HRN6`JqZ@VTNfc(Pm2y;A5qjVUU!G&$#?5FT6MWV{Y=Ed%@NG zFv&zwI!>NXSd0u5K>k;&;;7(N6gpxahw5U7%6?ApItN8)OspOmavEaPF2iz~YluA% zxI^IlWLDzB36Cgd-Xu)DUYte-^&h04jmmrNd83}aL2XF`+O1l2Rn8ZrMT6)Jc~^7D zm^GxaFoVN6t8{M>8FIm#XR5!ci}ziTU|O!Kp34R4CnysNKfXUqn`_EdWv0N6&#W4? zFyr$oHy*k0NCneOW=+#@7U4breG%O@fr(KOpQs~MIQA>>2s9t!mkb0BgO-9RNK8`K zCsn`bsVssVWO47IWI_M?t`H{&*X*f*sqhqMbogC2bQOCucbhlR>n~# zmi2p0t|`IB$TnG^uMl zk2JE?MB9sRois9>@G-6qvK;WL?)xLaydWju8s@|Pv zQ}N=0DJJ7Z98#;5nx+A9R_By9ntQ9V+mhh-EmLsidccbrJ|Y4My#NM;Lrbd@M{s^v!JfCT`>r15w3k&*~8F1-YdXe|foYa)N<6tLVkO6mzoyf#JMH&%I> zxOW^(7t5t+|BE~lnX@U+1u+7VVL8-vq@0^gH&dF(P1SuQnB%2>EJGY5=LuIOKxN)d z^wKgE3?+2t!W+owyqr%hB%JAisGnc?d8#&xG!EUxUUZku1nBuOsIgZKj{WiIxq102 zVh;rF8HcKYqa-bfGiOOci-n~l%2edTX6iK1{ww8~b8Yb~{-O+3USN7=#f#`k-QOEs=V0W35yGg*pC;4Rq;!*CBI!7>fS38GZ5?M;+j-97r(;{Xiy7xr7E3Kc15_t z>>}Z%7YLjw+}bWkU{v`UlNjeI+8C1|FP_2$NmZCe`_H|BbJp8AX6{=Q@#2&e;)y7tft$oXb*3wja z+#GXWDCXI{&a6I?d&Nlu&uP2*w+hT19LigKsGLF0mDzZabzXi#?#7GMajw6Jqtq<^ zB&cXT^i*xnKhOXZq^B%fCSguzpM2SG-*f|kGs4I)G!H7%MR-A&ES!4jjV`KEyiw(! z>Q_S-A0O!Ed+*Az7S+!X9!mw z&*ow}$3JTv&CE3+D-{3i#1E{1^E=OInUTFCr@`^K4vv&95C|-Q3UD9vNXlKgE67mA zWg_-o4Pw7Tp>XNd>$v*Yta|T9z0)&8s7JL-8H=7kwN}d3mJc+Ml%Gw+^-V11ReN1y zF%^5)92WDzd|TmdPydUDYILgJv0`nQaGGhM`GBJb9X?rNFt~Sf5D`6RV1p>y4h1F!QR;iEZyi5zni~jLg1J zCMHo-qe_wXl&BXLRq6;~FKqA~|A3jdIAEb*S#hdkAy_ZHGPR%-vs%yqQxU@m5)K4z zjeUr3O`O{jtBU{J>#u)uI+o2JUQvZlUH-d5U?~c}X_MgYf)rkqj%`@SIhy z3X?5}@?OCvT_jIdl*->DXim?|BM=Cj1ommIhqNhe^8aKxFG}^3H#yaN_?B~}AURIX z_19?7Y-_lJEBV_?&1w`DKdDYKnblz^y*Ymfk8VkoAN&-7D%0{B9P&rJ7$|a6%9Pcq zsH4p^*_^E6wcJ|nfk5C+@!%~lrB9X9EvY^SU#hX|lj@z*Ii6W%>MaDSh|v_e;hbv{ zUul5&l{%iI)1Jd6F6N!HoMT0jOh$xD%9_s=Fx}A~^tPmYk!TTBi}3KM&M|yTv8u41 ziVbxe2wWucjzcGGN%c);Pbw9vM(r6^-vS|4>JGvrh_@hWG|^Rie}wD$PJ3aPdf2SF^Qu?jfFk z|NB3;IXuEul(Smp1Pb91*{PMmi_VD_D!KDv5KqnLW|8I`AL22x@WXov&O5R_{7}!Z}$F8=~pXK$zvPqr(M`4p@PD#(kKLLR+)n5 z6}~%_iHeoyZu#9;EEm6iGndzvW?NL=3qNaPLC}rs9@d5r5R{2`f>Ny<5B-#xSmmXZ zZL4g7K;W#9CQC11Ce=JGR$?MGTPy^+PKDH_Efp=3q}=TS%(8_zd1L*BLZF3C8Onnt zDCI`Ay;wyoNO$*;s&`ad@JJie;&i-ojGr5M1st@9jptQcCfyb(D^v04yl*JkK;ZlU zi0_ zZ5sJ2#k|TLOEKS;UE20N5 zaTYRNLvmjCIYPF9Tf#HtvIPdMAznI4*&P! z<>Hw&&9h#d`$BZj0%}gQM`S#hRBBcX24}FvhusljE)E2a8q<7g*-5G3iUTO(Xl|^( z1HKv_8h0CI6OkZROwU zGIQs8EuwoSh^dFw@lmz*h_yfwsUm7bDAqvWD3Lcpl-(*)ee*Q0YM|s&dt-eCG!qws zbGrmE4|CYW=45bRkh_@Uss;vN*5kc{3sF~#r*i#;0}RJR+5?g>x{bsu`HaR1tiZlR z#YE_tZIzluWvtR{U3>Wi0)dCavu97(sUp=$#%_q^IuH<=x3PXE^BF2hOas#?;f&Pp zYXRp*WXCIZcxLuXv-y_%+>P~>lDxjSb~CHhWxmD6 z`Vg+9&-)3xofoQ5Bz{b1Kv@^EMze1@H`64juCs`TDq0-_cYxcf*0pJ~g{7nggSp|f zIK4IT0)fEZNW%0xVL__jWcD$`RkEN&QDtTWwMdJBa$astSi&>u=4Cp8# z&(;m%BrFdM3QBnl(}T2>;x$mti)|O#e4ND`v@OBUBZ+XWIoaIs>$wH)2r#WsxlN#U zQ=#RD{+J%;#pf+0yf66&0s$ZNDoufGZ7;n4{h!!0vB%p0FsF+1du0x^gejO+9CiXk zc9?Oc)Uw{ba9+%WVZg7%#q#bh7ITw)^PJwC29y`eSJK5a4EJ;*F)hu3@aFw|k-Gy?USMf5@(c&X46TZGPlM^ZDDr+b&5cLa zL{^|?nsl=*l`4iU?#b2>+@ZS2=xQCbNXRqGdTNvX3o(1wRR0!fa#_mJ8@#GA<~6k~ z5IBE4_w3onJU>aQyH&soCgv>!`L&bTeRd?1RbH;9MgN59AiLY(8#BV$IW(|bgVbdUfMJr&7HmFl&O z!})on$k*PWz$4*6;DV8^H+*>N`Cm06ems$RNo~wd9wtBoW$BFa7mJ{F*KFdRp8@7)wphiB@6u){L#A?}YG*%n&S-@TRe9tf>&wFY zs+6fBXpfBXT(w&!XHJ{*S86&CxIk?B^`C3`H~Zcf9zXuXCV|J>fGIUQc+ad^)SlW{ z|2iOjw^fj^bT`u^OX`&u52^QV@gA~@Px-5S@Lutr)S{{qRy{LLgTe9Cxi#EI{BDE0 zO7|k+3}UxXB&xzPVPjafBzcOp<#$NjS3ZHj{%{v46D!Q`=h9VqxTT41EwL6-Y(>C3(Yup*3VgnD1)&ix1 zCaG?B-%si9@ZPw*_wGXNVIrx&u|6uxGQXJw@mBf5Ij_deSRShh0q0LyT^f~p5zauZ zQk+G)Db?jdz_T)VF6DU?B2?^f>_*j9rc(ozOKNad$e2#PSy@}9nR7*aoClj?(Hx8S zl)O#~Nu1I~HO-5Xs{6s@L>V)rkK~y#FVlpll&D$TJk0JMF|WlMqKvLmphrXnow`~P zQ?Jc>aaqL{qE_jua^8jvgGd7pjX{+Y8IHRA_ zU>Af5*BR0;WmogC(#>FCK=-6pFSnGdQo6~^U8tnoaj??DZM(I6pfCXek8~8-riKX_ zk2sjbv=l)qqE+XZKIXyc@pY)!z+Gd7N}Vu7suPM@ojc}CD_g}VxDW&`a^lfxBBDtU z*C=f?tMbZRRBgDRYN75ZzA2>$VUg^S;IU)CMM~xQWT!Jj#mt*)%<38Ld43+Cf-98? z`J%h`PAqpp8RBLqjM?3iS~^geKnH5Di&&%jxGjtJKj(>UhTEAsR|*)9oJSyVhghRh zCk&J7q*kwXYITBKutbE#Ce|4x4YP_sDxy>kCM`dXj(H)mFhMR9ADU_@yfWRog9p3^ zrEoF6P>(ZNXINpqbI18vEaPm#Hj@jnl)1d6M%6jHDYGz!@w&G|b$ii$9so3;b)krk z$HsSV3CJ=~UvgUBxmo4)t&tWl{GhDmk8SD!rj=q8t^tX2)o ztVKQXT{U(Vm0J(AJ`b#mKeq~C|d0{5aSPmm4TKxDrrCP=D{b2biUU6R# zIr^hgHCvdJtN7XZKs!7~OnbHzlyh4mJLf74D%sw1>$$JRq0+f{jF5s-oJ`0{J#u?h zZ5Z7=b1$c*%Ej;o2^=R4)Fd?!SQ9^RE_nLfvu8iMGR`_u-9`BEiVRV8BFUOn^P1&) zR-`+3o-3>tJwdoa)-DO8RGW{H{OragTq#n{CudWOtg3L7|D2x*Wcb)Qm6^%0loJ(n zNdr5xISiDu8jMi+cn2p&HDG9j%Vu3wds_^2_klxUAH}7Jl>M2I)it#1MVdx}u)J@NfjuNT?P0AgV(&9wJ%&a!OTxfj=uiP!lZm=LjS$#KJO!z132JX+W%PT&Ng z>S=N3daUAlZM^5lc`_-tRAv*-+P5fUoI3b|f%v}n9(`8A-7 z?nt&qq{M>?$g}z_z#rX>&bdobN;NrQ<-OaItnUKZIS)J#xE-N8`gtaGJV#LHJo(v_^OJ2Mu1iu?I;nt&bTJIRa+7$Z2@Qi^LqSqABD9ir zCUuyxJffV1_>96OBjViUnN4)=S!`TECS819ys&8wU-D!13eSp3x{z93oLrH!d++5D zfL}$nV!-6Z?Gajb&OIawukJH&q)=qTq@~Uo-2Tx`wEsCD42$lnJY1F*k+Gj5Toq;% zw~4zZ-Mfx=I+zwGebjD&y-9V#WCq?m;#Xz`Wwq*K@>-}bt2^eD>dp5nvbhd06Xe2- zToha>OS;+VX;yVuco@l|qFVsSuTp+M(sghOpHihH6E$k3(s_W9oQ93hDv42f!hDdx zp&S^dy0#nAS}0#SSISU@Wks8uG&sI;_Xs_3gy?m|rG8WWo&7j1CQ*+ZJ(4YM+-5w& z?U8&B`w*cb_(LK^>rvPU#UI~o&AT_=WVgWUPoKW>mPe2N-KGNk2)d_Kuf(<5^X91h zyvD`xIj@xD>oC1>)uN0%5*B2=B^e$lK)G>{P!XC5Vz-*$5`?KpIP;#c{r&LGjY zjVUb_7vb|mGG#2tn9`}z6i0SrczL(V6u4kuyI`W|^)af=czH}veN&HcnXEaTUY^H> zyDrzrv$-Q|`nI3`>L)(&F}o!WQu-GjKmNp~^q!sEqa_{`Hypz8zPms$19ihqV@Hs@Q)Y9>v$ zRX8)(Jj@trwt2wa3llg|C}M=RC!xrO+mcGF{FQ4YlQpEnRwT|>ovR}gs3KIaOsXsj zJyLExv-86%w^{AWI0&gutARHO{gvsw>d?7z;t$CRF4Jo$*eE#NImnVU=CHdw7i0!; zc>xnP1SA@79zY(6oz;$XSdc@JmXHC!S_$U~6>lauPJTJ0a*6)~&wsdGhM9EJr1=iS1- z;g$O+QSXb9_=h0&WqtVR+pP9w+-go0@r|cXpT7Cgqp8<_ymd(R?nbCWi+iOcfoM|_ zJ0nr$Q>JBp+$qtboV1*GT_`?59#z`hFx1Exj}95l#Do4kx^0vV2Q4^vo1naEu9<&} zxs04Y3}7;q!87w$<)wQyvxQkbb;9vEhxfwu{MHa~;KZTS<49P%ZV?}+-Dv+alp4h} z6p4!VzqlWDZ+s7;tuDet`(LEfQ}XE+t!pH1EAKt>nO>jSn0-HL2gJeqm**cp{>9DV zgI*;>lc+a;S^NyHo0aW4;Ox%xu_aw511yg+t1j*#E$36wuf&mMH(SnQVmc~*NgCs* zXJGoda^sa@%$E0*k|aV5>mBj=V;W|~ z!pgZ=YZEH}Y$*xVhUZRYt>!AJ(az5Ff5qKvjOYi1rp)7PF_`_aY&mOFY+mjo zyf7Ez{aSsmi@0VB1=%8C<-SihxHis#y4t|8fmPf{Sns+Oj|q+aJwn7I$EWt+RF`

%`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 + + + + + + + + + diff --git a/blueprints/mailcatcher-ng/meta.json b/blueprints/mailcatcher-ng/meta.json new file mode 100644 index 00000000..814ea781 --- /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 00000000..2a72b979 --- /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}" diff --git a/blueprints/mailu/docker-compose.yml b/blueprints/mailu/docker-compose.yml new file mode 100644 index 00000000..3b644361 --- /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 00000000..014883fe --- /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 00000000..50374490 --- /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 00000000..3a41a263 --- /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 00000000..acade041 --- /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" diff --git a/blueprints/markup/docker-compose.yml b/blueprints/markup/docker-compose.yml new file mode 100644 index 00000000..d26ccb6c --- /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 00000000..0d9d3be6 --- /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 00000000..3a4f9f08 --- /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 00000000..f2e01d73 --- /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}" diff --git a/blueprints/maybe/meta.json b/blueprints/maybe/meta.json index a27e7f22..1d7c54ae 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/minio/docker-compose.yml b/blueprints/minio/docker-compose.yml index a25e3aee..575ddf95 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 95427234..423c0c82 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 ae6f1779..5a358b41 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}" diff --git a/blueprints/mumble/meta.json b/blueprints/mumble/meta.json index 56783bb9..05707dbf 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/n8n-queue/docker-compose.yml b/blueprints/n8n-queue/docker-compose.yml new file mode 100644 index 00000000..f0b601d1 --- /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 00000000..782571b8 --- /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 00000000..e0ada259 --- /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 00000000..46ce3fbe --- /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}" diff --git a/blueprints/nextcloud-aio/docker-compose.yml b/blueprints/nextcloud-aio/docker-compose.yml index 7550c252..954684a4 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 4cb120ad..6d4001de 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": { diff --git a/blueprints/oneuptime/docker-compose.yml b/blueprints/oneuptime/docker-compose.yml new file mode 100644 index 00000000..7bd82327 --- /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 00000000..c2108d5c --- /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 00000000..5f2eec13 --- /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 00000000..a674b269 --- /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 + + + +""" diff --git a/blueprints/openhabittracker/docker-compose.yml b/blueprints/openhabittracker/docker-compose.yml new file mode 100644 index 00000000..31cebec5 --- /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 00000000..de635f7f --- /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 00000000..8aca131f --- /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}" diff --git a/blueprints/openpanel/docker-compose.yml b/blueprints/openpanel/docker-compose.yml index 53c95bd2..34d030e4 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 e395573e..ba3276ee 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 2a93c33c..208df223 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 diff --git a/blueprints/oryx/docker-compose.yml b/blueprints/oryx/docker-compose.yml new file mode 100644 index 00000000..98d460ff --- /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 00000000..d9006e7d --- /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 00000000..d16f644e --- /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 00000000..dcb80fe9 --- /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 00000000..4ae2264f --- /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}" diff --git a/blueprints/paymenter/meta.json b/blueprints/paymenter/meta.json index 2b1a3f19..6df437f1 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/phpmyadmin/docker-compose.yml b/blueprints/phpmyadmin/docker-compose.yml index 91674e87..c4554184 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 39c53845..cfbf8d00 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" diff --git a/blueprints/podfetch/docker-compose.yml b/blueprints/podfetch/docker-compose.yml new file mode 100644 index 00000000..2e20d9ab --- /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 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/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: + # 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} + 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 + # 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 + 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 + LOGS_REDIS_TLS: "false" + TRACES_REDIS_HOST: redis7 + TRACES_REDIS_TLS: "false" + 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 00000000..6db1223e --- /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 00000000..015dec45 --- /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 00000000..e8cca1e9 --- /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 00000000..9644ee13 --- /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' +""" diff --git a/blueprints/rustrak/meta.json b/blueprints/rustrak/meta.json index 9789fb2e..b841c197 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", diff --git a/blueprints/rybbit/docker-compose.yml b/blueprints/rybbit/docker-compose.yml index 2bd2d6b4..4f13c5c6 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 bfac0f2c..47765f56 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 c67e6523..c31bddaf 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" diff --git a/blueprints/scrutiny/meta.json b/blueprints/scrutiny/meta.json index 1294de34..515caaa9 100644 --- a/blueprints/scrutiny/meta.json +++ b/blueprints/scrutiny/meta.json @@ -11,6 +11,6 @@ }, "tags": [ "monitoring", - "NAS" + "nas" ] } diff --git a/blueprints/signoz/docker-compose.yml b/blueprints/signoz/docker-compose.yml index aaa6921a..67ef1771 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 492a613a..98839a94 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: diff --git a/blueprints/silex/docker-compose.yml b/blueprints/silex/docker-compose.yml new file mode 100644 index 00000000..bc821dca --- /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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 diff --git a/blueprints/sim/template.toml b/blueprints/sim/template.toml new file mode 100644 index 00000000..b61ef6a3 --- /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" diff --git a/blueprints/spacetimedb/docker-compose.yml b/blueprints/spacetimedb/docker-compose.yml new file mode 100644 index 00000000..05d85869 --- /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 00000000..676ced23 --- /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 00000000..824840ef --- /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 00000000..35d00d6f --- /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}" diff --git a/blueprints/steedos/docker-compose.yml b/blueprints/steedos/docker-compose.yml new file mode 100644 index 00000000..0f2637fa --- /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 00000000..baabd013 --- /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 00000000..342571dc --- /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}" diff --git a/blueprints/struxa/docker-compose.yml b/blueprints/struxa/docker-compose.yml new file mode 100644 index 00000000..ac63097b --- /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 00000000..446e5487 --- /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 00000000..e3f8b400 --- /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 00000000..9fabf86e --- /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}" diff --git a/blueprints/supabase/instructions.md b/blueprints/supabase/instructions.md new file mode 100644 index 00000000..af95f213 --- /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/synapse/docker-compose.yml b/blueprints/synapse/docker-compose.yml new file mode 100644 index 00000000..ee9c0bd8 --- /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 00000000..91b308a2 --- /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 00000000..bd4dbd1f --- /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 00000000..d2ab11db --- /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 00000000..f87bcdbb --- /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 +""" diff --git a/blueprints/systemprompt/docker-compose.yml b/blueprints/systemprompt/docker-compose.yml new file mode 100644 index 00000000..e0bb23f4 --- /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 00000000..64e28c83 --- /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 00000000..aab4f145 --- /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 00000000..e0fe2224 --- /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 00000000..4cca39a3 --- /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 = "" diff --git a/blueprints/trek/docker-compose.yml b/blueprints/trek/docker-compose.yml new file mode 100644 index 00000000..2195ecd8 --- /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 00000000..d815be38 --- /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 00000000..f45225f9 --- /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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 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). diff --git a/blueprints/verdaccio/meta.json b/blueprints/verdaccio/meta.json index e28d5792..44dea806 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/weblate/docker-compose.yml b/blueprints/weblate/docker-compose.yml new file mode 100644 index 00000000..f48a7cb3 --- /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 00000000..1d2fe31f --- /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 00000000..f960b581 --- /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 00000000..b8ed62fe --- /dev/null +++ b/blueprints/weblate/weblate.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/blueprints/wordpress/docker-compose.yml b/blueprints/wordpress/docker-compose.yml index 70b8aba3..75ab474a 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 91d95e9c..c11c63f2 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]] diff --git a/blueprints/zipline/meta.json b/blueprints/zipline/meta.json index 9a669e64..64fcb49c 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" ] } diff --git a/blueprints/zitadel/docker-compose.yml b/blueprints/zitadel/docker-compose.yml index aaa457c3..ba7322e2 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 05917241..ac66c4b5 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 f04da0be..44c177e5 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]] diff --git a/build-scripts/check-links.js b/build-scripts/check-links.js new file mode 100644 index 00000000..bbb34b80 --- /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); +});