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 00000000..1a7b3418 Binary files /dev/null and b/blueprints/certmate/logo.png differ 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 00000000..1aa26d2c Binary files /dev/null and b/blueprints/dagu/logo.png differ diff --git a/blueprints/dagu/meta.json b/blueprints/dagu/meta.json new file mode 100644 index 00000000..5b40f222 --- /dev/null +++ b/blueprints/dagu/meta.json @@ -0,0 +1,19 @@ +{ + "id": "dagu", + "name": "Dagu", + "version": "2.10.7", + "description": "Dagu is a self-contained workflow engine with a Web UI. Schedule and orchestrate jobs as DAGs defined in declarative YAML, with no DBMS required.", + "logo": "logo.png", + "links": { + "github": "https://github.com/dagucloud/dagu", + "website": "https://dagu.sh", + "docs": "https://docs.dagu.sh" + }, + "tags": [ + "workflow", + "automation", + "scheduler", + "cron", + "devops" + ] +} diff --git a/blueprints/dagu/template.toml b/blueprints/dagu/template.toml new file mode 100644 index 00000000..5b618529 --- /dev/null +++ b/blueprints/dagu/template.toml @@ -0,0 +1,15 @@ +[variables] +main_domain = "${domain}" +admin_password = "${password:32}" + +[config] +mounts = [] + +[[config.domains]] +serviceName = "dagu" +port = 8080 +host = "${main_domain}" + +[config.env] +ADMIN_USERNAME = "admin" +ADMIN_PASSWORD = "${admin_password}" diff --git a/blueprints/databasus/databasus.svg b/blueprints/databasus/databasus.svg new file mode 100644 index 00000000..b41cdd40 --- /dev/null +++ b/blueprints/databasus/databasus.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/blueprints/databasus/docker-compose.yml b/blueprints/databasus/docker-compose.yml new file mode 100644 index 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 00000000..94ba662b Binary files /dev/null and b/blueprints/frappe-lms/lms-logo.png differ diff --git a/blueprints/frappe-lms/meta.json b/blueprints/frappe-lms/meta.json new file mode 100644 index 00000000..2f22b6b1 --- /dev/null +++ b/blueprints/frappe-lms/meta.json @@ -0,0 +1,19 @@ +{ + "id": "frappe-lms", + "name": "Frappe LMS", + "version": "v2.52.0", + "description": "Easy to use, open source learning management system to structure courses and reach out to learners, built on the Frappe Framework.", + "logo": "lms-logo.png", + "links": { + "github": "https://github.com/frappe/lms", + "docs": "https://docs.frappe.io/learning", + "website": "https://frappe.io/learning" + }, + "tags": [ + "lms", + "learning", + "education", + "courses", + "frappe" + ] +} diff --git a/blueprints/frappe-lms/template.toml b/blueprints/frappe-lms/template.toml new file mode 100644 index 00000000..4f9064df --- /dev/null +++ b/blueprints/frappe-lms/template.toml @@ -0,0 +1,30 @@ +[variables] +main_domain = "${domain}" +db_root_password = "${password:32}" +admin_password = "${password:32}" + +[config] +[[config.domains]] +serviceName = "frontend" +port = 8_080 +host = "${main_domain}" + +[config.env] +SITE_NAME = "${main_domain}" +ADMIN_PASSWORD = "${admin_password}" +DB_ROOT_PASSWORD = "${db_root_password}" +MIGRATE = "1" +ENABLE_DB = "1" +DB_HOST = "db" +CREATE_SITE = "1" +CONFIGURE = "1" +REGENERATE_APPS_TXT = "1" +INSTALL_APP_ARGS = "--install-app lms" +ENABLE_SCHEDULER = "1" +IMAGE_NAME = "ghcr.io/frappe/lms" +VERSION = "v2.52.0" +FRAPPE_SITE_NAME_HEADER = "" +DB_VERSION = "11.8.6" +SITE_VOLUME_TYPE = "" +SITE_VOLUME_OPTS = "" +SITE_VOLUME_DEV = "" diff --git a/blueprints/gitlab-ce/docker-compose.yml b/blueprints/gitlab-ce/docker-compose.yml index 3100a148..8e6c0734 100644 --- a/blueprints/gitlab-ce/docker-compose.yml +++ b/blueprints/gitlab-ce/docker-compose.yml @@ -1,65 +1,24 @@ services: gitlab: - image: gitlab/gitlab-ce:latest + image: gitlab/gitlab-ce:19.1.2-ce.0 restart: unless-stopped - hostname: gitlab.example.com + shm_size: "256m" environment: GITLAB_OMNIBUS_CONFIG: | external_url 'http://${GITLAB_HOST}' - gitlab_rails['gitlab_ssh_host'] = '${GITLAB_HOST}' - gitlab_rails['gitlab_shell_ssh_port'] = 2224 - gitlab_rails['db_adapter'] = 'postgresql' - gitlab_rails['db_host'] = 'postgresql' - gitlab_rails['db_port'] = '5432' - gitlab_rails['db_database'] = '${POSTGRES_DB}' - gitlab_rails['db_username'] = '${POSTGRES_USER}' - gitlab_rails['db_password'] = '${POSTGRES_PASSWORD}' - # Redis config for external TCP connection - gitlab_rails['redis_url'] = 'redis://redis:6379/0' - gitlab_rails['redis_host'] = 'redis' - gitlab_rails['redis_port'] = 6379 - gitlab_rails['redis_socket'] = nil - gitlab_rails['gitlab_email_enabled'] = false - gitlab_rails['gitlab_default_can_create_group'] = true - gitlab_rails['gitlab_username_changing_enabled'] = false - unicorn['worker_processes'] = 2 - unicorn['worker_timeout'] = 60 - postgresql['enable'] = false - redis['enable'] = false - nginx['enable'] = true nginx['listen_port'] = 80 nginx['listen_https'] = false + gitlab_rails['initial_root_password'] = '${GITLAB_ROOT_PASSWORD}' + gitlab_rails['gitlab_email_enabled'] = false + puma['worker_processes'] = 0 + sidekiq['concurrency'] = 10 prometheus_monitoring['enable'] = false - ports: - - "80" - - "2224" volumes: - gitlab_config:/etc/gitlab - gitlab_logs:/var/log/gitlab - gitlab_data:/var/opt/gitlab - depends_on: - - postgresql - - redis - - postgresql: - image: postgres:16-alpine - restart: unless-stopped - environment: - POSTGRES_DB: ${POSTGRES_DB} - POSTGRES_USER: ${POSTGRES_USER} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} - volumes: - - postgresql_data:/var/lib/postgresql/data - - redis: - image: redis:7-alpine - restart: unless-stopped - volumes: - - redis_data:/data volumes: gitlab_config: gitlab_logs: gitlab_data: - postgresql_data: - redis_data: diff --git a/blueprints/gitlab-ce/meta.json b/blueprints/gitlab-ce/meta.json index 8092fbd9..4ca3e959 100644 --- a/blueprints/gitlab-ce/meta.json +++ b/blueprints/gitlab-ce/meta.json @@ -1,7 +1,7 @@ { "id": "gitlab-ce", "name": "GitLab CE", - "version": "latest", + "version": "19.1.2", "description": "GitLab Community Edition is a free and open source platform for managing Git repositories, CI/CD pipelines, and project management.", "logo": "gitlab-ce.svg", "links": { diff --git a/blueprints/gitlab-ce/template.toml b/blueprints/gitlab-ce/template.toml index 1bc24e87..d531d80d 100644 --- a/blueprints/gitlab-ce/template.toml +++ b/blueprints/gitlab-ce/template.toml @@ -1,15 +1,11 @@ [variables] main_domain = "${domain}" -postgres_db = "gitlab" -postgres_user = "gitlab" -postgres_password = "${password:32}" +gitlab_root_password = "${password:32}" [config] env = [ "GITLAB_HOST=${main_domain}", - "POSTGRES_DB=${postgres_db}", - "POSTGRES_USER=${postgres_user}", - "POSTGRES_PASSWORD=${postgres_password}", + "GITLAB_ROOT_PASSWORD=${gitlab_root_password}", ] [[config.domains]] diff --git a/blueprints/go2rtc/meta.json b/blueprints/go2rtc/meta.json index 5c3704a1..7919fe41 100644 --- a/blueprints/go2rtc/meta.json +++ b/blueprints/go2rtc/meta.json @@ -13,6 +13,6 @@ "streaming", "webrtc", "video", - "home assistant" + "home-assistant" ] } diff --git a/blueprints/homarr/docker-compose.yml b/blueprints/homarr/docker-compose.yml index 876ea3f6..c43e62f3 100644 --- a/blueprints/homarr/docker-compose.yml +++ b/blueprints/homarr/docker-compose.yml @@ -4,8 +4,11 @@ services: restart: unless-stopped volumes: # - /var/run/docker.sock:/var/run/docker.sock # Optional, only if you want docker integration - - ../homarr/appdata:/appdata + - homarr_appdata:/appdata environment: - SECRET_ENCRYPTION_KEY=${SECRET_ENCRYPTION_KEY} ports: - 7575 + +volumes: + homarr_appdata: diff --git a/blueprints/homarr/template.toml b/blueprints/homarr/template.toml index d73be947..6cdf00cd 100644 --- a/blueprints/homarr/template.toml +++ b/blueprints/homarr/template.toml @@ -1,6 +1,6 @@ [variables] main_domain = "${domain}" -secret_key = "${password:64}" +secret_key = "${hash:64}" [config] env = ["SECRET_ENCRYPTION_KEY=${secret_key}"] diff --git a/blueprints/immich/docker-compose.yml b/blueprints/immich/docker-compose.yml index f385b329..822117ca 100644 --- a/blueprints/immich/docker-compose.yml +++ b/blueprints/immich/docker-compose.yml @@ -2,10 +2,10 @@ version: "3.9" services: immich-server: - image: ghcr.io/immich-app/immich-server:v2.1.0 + image: ghcr.io/immich-app/immich-server:v3.0.2 volumes: - - immich-library:/usr/src/app/upload + - immich-library:/data - /etc/localtime:/etc/localtime:ro depends_on: immich-redis: @@ -13,9 +13,6 @@ services: immich-database: condition: service_healthy environment: - PORT: 2283 - SERVER_URL: ${SERVER_URL} - FRONT_BASE_URL: ${FRONT_BASE_URL} # Database Configuration DB_HOSTNAME: ${DB_HOSTNAME} DB_PORT: ${DB_PORT} @@ -30,70 +27,45 @@ services: TZ: ${TZ} restart: unless-stopped healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:2283/server-info/ping"] - interval: 30s - timeout: 10s - retries: 3 + disable: false immich-machine-learning: - image: ghcr.io/immich-app/immich-machine-learning:v2.1.0 + image: ghcr.io/immich-app/immich-machine-learning:v3.0.2 volumes: - immich-model-cache:/cache - environment: - REDIS_HOSTNAME: ${REDIS_HOSTNAME} - REDIS_PORT: ${REDIS_PORT} - REDIS_DBINDEX: ${REDIS_DBINDEX} restart: unless-stopped healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:3003/ping"] - interval: 30s - timeout: 10s - retries: 3 + disable: false immich-redis: - image: redis:6.2-alpine + image: valkey/valkey:9 volumes: - immich-redis-data:/data healthcheck: - test: ["CMD", "redis-cli", "ping"] + test: redis-cli ping || exit 1 interval: 10s timeout: 5s retries: 5 restart: unless-stopped immich-database: - image: tensorchord/pgvecto-rs:pg14-v0.3.0 + image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0 volumes: - immich-postgres:/var/lib/postgresql/data environment: POSTGRES_PASSWORD: ${DB_PASSWORD} POSTGRES_USER: ${DB_USERNAME} - POSTGRES_DB: immich + POSTGRES_DB: ${DB_DATABASE_NAME} POSTGRES_INITDB_ARGS: '--data-checksums' + shm_size: 128mb healthcheck: - test: pg_isready -U ${DB_USERNAME} -d immich || exit 1 + test: pg_isready -U ${DB_USERNAME} -d ${DB_DATABASE_NAME} || exit 1 interval: 10s timeout: 5s retries: 5 - command: - [ - 'postgres', - '-c', - 'shared_preload_libraries=vectors.so', - '-c', - 'search_path="$$user", public, vectors', - '-c', - 'logging_collector=on', - '-c', - 'max_wal_size=2GB', - '-c', - 'shared_buffers=512MB', - '-c', - 'wal_compression=on', - ] restart: unless-stopped volumes: diff --git a/blueprints/immich/meta.json b/blueprints/immich/meta.json index bc3ecb6b..cf1aa634 100644 --- a/blueprints/immich/meta.json +++ b/blueprints/immich/meta.json @@ -1,7 +1,7 @@ { "id": "immich", "name": "Immich", - "version": "v1.121.0", + "version": "v3.0.2", "description": "High performance self-hosted photo and video backup solution directly from your mobile phone.", "logo": "immich.svg", "links": { diff --git a/blueprints/immich/template.toml b/blueprints/immich/template.toml index f2b910e7..a18ac2bb 100644 --- a/blueprints/immich/template.toml +++ b/blueprints/immich/template.toml @@ -5,9 +5,6 @@ db_user = "immich" [config] env = [ - "IMMICH_HOST=${main_domain}", - "SERVER_URL=https://${main_domain}", - "FRONT_BASE_URL=https://${main_domain}", "DB_HOSTNAME=immich-database", "DB_PORT=5432", "DB_USERNAME=${db_user}", diff --git a/blueprints/infisical/docker-compose.yml b/blueprints/infisical/docker-compose.yml index ac5718bc..d28dd686 100644 --- a/blueprints/infisical/docker-compose.yml +++ b/blueprints/infisical/docker-compose.yml @@ -1,58 +1,29 @@ services: - db-migration: - depends_on: - db: - condition: service_healthy - image: infisical/infisical:v0.135.0-postgres - environment: - - NODE_ENV=production - - ENCRYPTION_KEY - - AUTH_SECRET - - SITE_URL - - DB_CONNECTION_URI=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB} - - REDIS_URL=redis://redis:6379 - - SMTP_HOST - - SMTP_PORT - - SMTP_FROM_NAME - - SMTP_USERNAME - - SMTP_PASSWORD - - SMTP_SECURE=true - command: npm run migration:latest - pull_policy: always - backend: + image: infisical/infisical:v0.162.6 restart: unless-stopped depends_on: db: condition: service_healthy redis: condition: service_started - db-migration: - condition: service_completed_successfully - image: infisical/infisical:v0.135.0-postgres - pull_policy: always environment: - NODE_ENV=production - - ENCRYPTION_KEY - - AUTH_SECRET - - SITE_URL + - ENCRYPTION_KEY=${ENCRYPTION_KEY} + - AUTH_SECRET=${AUTH_SECRET} + - SITE_URL=${SITE_URL} - DB_CONNECTION_URI=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB} - REDIS_URL=redis://redis:6379 - - SMTP_HOST - - SMTP_PORT - - SMTP_FROM_NAME - - SMTP_USERNAME - - SMTP_PASSWORD - - SMTP_SECURE=true - + - SMTP_HOST=${SMTP_HOST} + - SMTP_PORT=${SMTP_PORT} + - SMTP_FROM_ADDRESS=${SMTP_FROM_ADDRESS} + - SMTP_FROM_NAME=${SMTP_FROM_NAME} + - SMTP_USERNAME=${SMTP_USERNAME} + - SMTP_PASSWORD=${SMTP_PASSWORD} redis: image: redis:7.4.1 - env_file: .env restart: always - environment: - - ALLOW_EMPTY_PASSWORD=yes - volumes: - redis_infisical_data:/data @@ -60,12 +31,11 @@ services: image: postgres:14-alpine restart: always environment: - - POSTGRES_PASSWORD - - POSTGRES_USER - - POSTGRES_DB + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - POSTGRES_USER=${POSTGRES_USER} + - POSTGRES_DB=${POSTGRES_DB} volumes: - pg_infisical_data:/var/lib/postgresql/data - healthcheck: test: "pg_isready --username=${POSTGRES_USER} && psql --username=${POSTGRES_USER} --list" interval: 5s @@ -75,6 +45,3 @@ services: volumes: pg_infisical_data: redis_infisical_data: - - - diff --git a/blueprints/infisical/meta.json b/blueprints/infisical/meta.json index b086bace..5f41cb13 100644 --- a/blueprints/infisical/meta.json +++ b/blueprints/infisical/meta.json @@ -1,7 +1,7 @@ { "id": "infisical", "name": "Infisical", - "version": "0.135.0", + "version": "0.162.6", "description": "All-in-one platform to securely manage application configuration and secrets across your team and infrastructure.", "logo": "infisical.jpg", "links": { diff --git a/blueprints/infisical/template.toml b/blueprints/infisical/template.toml index a96bfb6c..4ea6af67 100644 --- a/blueprints/infisical/template.toml +++ b/blueprints/infisical/template.toml @@ -3,56 +3,27 @@ main_domain = "${domain}" postgres_password = "${password}" postgres_user = "infisical" postgres_db = "infisical" +encryption_key = "${hash:32}" +auth_secret = "${base64:32}" [config] env = [ - "ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218", - "AUTH_SECRET=5lrMXKKWCVocS/uerPsl7V+TX/aaUaI7iDkgl3tSmLE=", + "ENCRYPTION_KEY=${encryption_key}", + "AUTH_SECRET=${auth_secret}", "POSTGRES_PASSWORD=${postgres_password}", "POSTGRES_USER=${postgres_user}", "POSTGRES_DB=${postgres_db}", - "SITE_URL=http://${main_domain}:8080", + "SITE_URL=http://${main_domain}", "SMTP_HOST=", - "SMTP_PORT=", - "SMTP_NAME=", + "SMTP_PORT=587", + "SMTP_FROM_ADDRESS=", + "SMTP_FROM_NAME=Infisical", "SMTP_USERNAME=", "SMTP_PASSWORD=", - "CLIENT_ID_HEROKU=", - "CLIENT_ID_VERCEL=", - "CLIENT_ID_NETLIFY=", - "CLIENT_ID_GITHUB=", - "CLIENT_ID_GITHUB_APP=", - "CLIENT_SLUG_GITHUB_APP=", - "CLIENT_ID_GITLAB=", - "CLIENT_ID_BITBUCKET=", - "CLIENT_SECRET_HEROKU=", - "CLIENT_SECRET_VERCEL=", - "CLIENT_SECRET_NETLIFY=", - "CLIENT_SECRET_GITHUB=", - "CLIENT_SECRET_GITHUB_APP=", - "CLIENT_SECRET_GITLAB=", - "CLIENT_SECRET_BITBUCKET=", - "CLIENT_SLUG_VERCEL=", - "CLIENT_PRIVATE_KEY_GITHUB_APP=", - "CLIENT_APP_ID_GITHUB_APP=", - "SENTRY_DSN=", - "POSTHOG_HOST=", - "POSTHOG_PROJECT_API_KEY=", - "CLIENT_ID_GOOGLE_LOGIN=", - "CLIENT_SECRET_GOOGLE_LOGIN=", - "CLIENT_ID_GITHUB_LOGIN=", - "CLIENT_SECRET_GITHUB_LOGIN=", - "CLIENT_ID_GITLAB_LOGIN=", - "CLIENT_SECRET_GITLAB_LOGIN=", - "CAPTCHA_SECRET=", - "NEXT_PUBLIC_CAPTCHA_SITE_KEY=", - "PLAIN_API_KEY=", - "PLAIN_WISH_LABEL_IDS=", - "SSL_CLIENT_CERTIFICATE_HEADER_KEY=", ] mounts = [] [[config.domains]] serviceName = "backend" -port = 8_080 +port = 8080 host = "${main_domain}" diff --git a/blueprints/jellyfin/meta.json b/blueprints/jellyfin/meta.json index d15c90ac..3767dca3 100644 --- a/blueprints/jellyfin/meta.json +++ b/blueprints/jellyfin/meta.json @@ -10,6 +10,6 @@ "docs": "https://jellyfin.org/docs/" }, "tags": [ - "media system" + "media-system" ] } diff --git a/blueprints/kaneo/meta.json b/blueprints/kaneo/meta.json index a003754f..a307061b 100644 --- a/blueprints/kaneo/meta.json +++ b/blueprints/kaneo/meta.json @@ -10,6 +10,6 @@ "docs": "https://kaneo.app/docs/" }, "tags": [ - "Task Tracking" + "task-tracking" ] } diff --git a/blueprints/kener/docker-compose.yml b/blueprints/kener/docker-compose.yml index eccb8f8f..f1978c1c 100644 --- a/blueprints/kener/docker-compose.yml +++ b/blueprints/kener/docker-compose.yml @@ -1,5 +1,3 @@ -version: "3.8" - services: redis: image: redis:7-alpine diff --git a/blueprints/librechat/meta.json b/blueprints/librechat/meta.json index 6e0bf518..6e8fc463 100644 --- a/blueprints/librechat/meta.json +++ b/blueprints/librechat/meta.json @@ -2,7 +2,7 @@ "id": "librechat", "name": "LibreChat", "version": "latest", - "description": "LibreChat is the ultimate open-source app for all your AI conversations, fully customizable and compatible with any AI provider (Openai, Ollama, Google etc.) — all in one sleek interface.", + "description": "LibreChat is the ultimate open-source app for all your AI conversations, fully customizable and compatible with any AI provider (Openai, Ollama, Google etc.) \u00e2\u20ac\u201d all in one sleek interface.", "logo": "librechat.png", "links": { "github": "https://github.com/danny-avila/librechat", @@ -13,8 +13,8 @@ "ai", "chatbot", "llm", - "MIT-license", - "BYOK", + "mit-license", + "byok", "generative-ai" ] } diff --git a/blueprints/livekit/meta.json b/blueprints/livekit/meta.json index e8873734..2c4ccb8d 100644 --- a/blueprints/livekit/meta.json +++ b/blueprints/livekit/meta.json @@ -10,10 +10,10 @@ "docs": "https://docs.livekit.io/" }, "tags": [ - "Video", - "Audio", - "Real-time", - "Streaming", - "Webrtc" + "video", + "audio", + "real-time", + "streaming", + "webrtc" ] } diff --git a/blueprints/lobe-chat/meta.json b/blueprints/lobe-chat/meta.json index e64ae59f..3a8db046 100644 --- a/blueprints/lobe-chat/meta.json +++ b/blueprints/lobe-chat/meta.json @@ -10,7 +10,7 @@ "docs": "https://lobehub.com/docs/self-hosting/platform/docker-compose" }, "tags": [ - "IA", + "ia", "chat" ] } diff --git a/blueprints/logto/docker-compose.yml b/blueprints/logto/docker-compose.yml index 04d7464f..2e05a7b8 100644 --- a/blueprints/logto/docker-compose.yml +++ b/blueprints/logto/docker-compose.yml @@ -1,14 +1,11 @@ services: app: + image: ghcr.io/logto-io/logto:1.41.0 + restart: unless-stopped depends_on: postgres: condition: service_healthy - image: ghcr.io/logto-io/logto:1.27.0 entrypoint: ["sh", "-c", "npm run cli db seed -- --swe && npm start"] - ports: - - 3001 - - 3002 - environment: TRUST_PROXY_HEADER: 1 DB_URL: postgres://logto:${LOGTO_POSTGRES_PASSWORD}@postgres:5432/logto @@ -18,20 +15,19 @@ services: - logto-connectors:/etc/logto/packages/core/connectors postgres: image: postgres:17-alpine - user: postgres - + restart: unless-stopped environment: POSTGRES_USER: logto POSTGRES_PASSWORD: ${LOGTO_POSTGRES_PASSWORD} + POSTGRES_DB: logto volumes: - postgres-data:/var/lib/postgresql/data healthcheck: - test: ["CMD-SHELL", "pg_isready"] + test: ["CMD-SHELL", "pg_isready -U logto -d logto"] interval: 10s timeout: 5s retries: 5 - volumes: logto-connectors: postgres-data: diff --git a/blueprints/logto/meta.json b/blueprints/logto/meta.json index 03bd1ded..60e7aedc 100644 --- a/blueprints/logto/meta.json +++ b/blueprints/logto/meta.json @@ -1,7 +1,7 @@ { "id": "logto", "name": "Logto", - "version": "1.27.0", + "version": "1.41.0", "description": "Logto is an open-source Identity and Access Management (IAM) platform designed to streamline Customer Identity and Access Management (CIAM) and Workforce Identity Management.", "logo": "logto.png", "links": { diff --git a/blueprints/logto/template.toml b/blueprints/logto/template.toml index 896a5d33..e8e420e6 100644 --- a/blueprints/logto/template.toml +++ b/blueprints/logto/template.toml @@ -1,7 +1,7 @@ [variables] main_domain = "${domain}" admin_domain = "${domain}" -postgres_password = "${password}" +postgres_password = "${password:32}" [config] mounts = [] @@ -17,6 +17,6 @@ port = 3_002 host = "${admin_domain}" [config.env] -LOGTO_ENDPOINT = "http://${admin_domain}" +LOGTO_ENDPOINT = "http://${main_domain}" LOGTO_ADMIN_ENDPOINT = "http://${admin_domain}" LOGTO_POSTGRES_PASSWORD = "${postgres_password}" diff --git a/blueprints/mailcatcher-ng/docker-compose.yml b/blueprints/mailcatcher-ng/docker-compose.yml new file mode 100644 index 00000000..3a31b68a --- /dev/null +++ b/blueprints/mailcatcher-ng/docker-compose.yml @@ -0,0 +1,27 @@ +services: + mailcatcher-ng: + # Only the "latest" tag is published, so the image is pinned by digest + # (v2.3.9 of the image, MailCatcher NG 1.6.8). + image: stpaquet/alpinemailcatcher:latest@sha256:499c0eabca5e82b23d030706ba9d5b8b27bba5a5f34a43154ee5e68b07cba52c + restart: unless-stopped + # Same as the image default CMD, plus --persistence so caught mail + # survives container restarts (stored in the named volume below). + command: sh -c "mailcatcher --foreground --smtp-port=1025 --http-port=1080 --ip=0.0.0.0 --messages-limit=$$MAIL_LIMIT --persistence --no-quit" + environment: + - MAIL_LIMIT=${MAIL_LIMIT} + expose: + # SMTP: reachable from other services in the same Dokploy network + # at mailcatcher-ng:1025 (intentionally not published on the host). + - 1025 + # Web UI (routed through the domain below) + - 1080 + volumes: + - mailcatcher-data:/home/mailcatcher/.mailcatcher + healthcheck: + test: ['CMD', 'wget', '-q', '--spider', 'http://127.0.0.1:1080'] + interval: 10s + timeout: 5s + retries: 10 + +volumes: + mailcatcher-data: diff --git a/blueprints/mailcatcher-ng/mailcatcher-ng.svg b/blueprints/mailcatcher-ng/mailcatcher-ng.svg new file mode 100644 index 00000000..9ffcc2cf --- /dev/null +++ b/blueprints/mailcatcher-ng/mailcatcher-ng.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/blueprints/mailcatcher-ng/meta.json b/blueprints/mailcatcher-ng/meta.json new file mode 100644 index 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 00000000..0e9a607e Binary files /dev/null and b/blueprints/n8n-queue/n8n.png differ 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 00000000..92741bc1 Binary files /dev/null and b/blueprints/openhabittracker/openhabittracker.png differ 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 00000000..98844890 Binary files /dev/null and b/blueprints/podfetch/logo.png differ diff --git a/blueprints/podfetch/meta.json b/blueprints/podfetch/meta.json new file mode 100644 index 00000000..c4163f46 --- /dev/null +++ b/blueprints/podfetch/meta.json @@ -0,0 +1,15 @@ +{ + "id": "podfetch", + "name": "PodFetch", + "version": "v5.2.2", + "description": "PodFetch is a sleek, self-hosted podcast manager that automatically downloads new episodes, offers a web UI for listening and managing podcasts, and provides a GPodder-compatible sync API for mobile apps like AntennaPod.", + "logo": "logo.png", + "links": { + "github": "https://github.com/SamTV12345/PodFetch", + "website": "https://samtv12345.github.io/PodFetch/", + "docs": "https://samtv12345.github.io/PodFetch/" + }, + "tags": [ + "media" + ] +} diff --git a/blueprints/podfetch/template.toml b/blueprints/podfetch/template.toml new file mode 100644 index 00000000..7167b593 --- /dev/null +++ b/blueprints/podfetch/template.toml @@ -0,0 +1,15 @@ +[variables] +main_domain = "${domain}" +admin_username = "admin" +admin_password = "${password:24}" + +[config] +env = [ + "USERNAME=${admin_username}", + "PASSWORD=${admin_password}" +] + +[[config.domains]] +serviceName = "podfetch" +port = 8000 +host = "${main_domain}" diff --git a/blueprints/poke/meta.json b/blueprints/poke/meta.json index bbb9bcba..6766ff27 100644 --- a/blueprints/poke/meta.json +++ b/blueprints/poke/meta.json @@ -5,9 +5,9 @@ "description": "Poke is an open-source, self-hosted alternative to YouTube. A privacy-focused video platform that allows you to watch and share videos without tracking.", "logo": "image.png", "links": { - "github": "https://codeberg.org/ashley/poke", + "github": "https://codeberg.org/ashleyirispuppy/poke", "website": "https://poketube.fun/", - "docs": "https://codeberg.org/ashley/poke" + "docs": "https://codeberg.org/ashleyirispuppy/poke" }, "tags": [ "video", diff --git a/blueprints/postgresus/docker-compose.yml b/blueprints/postgresus/docker-compose.yml index e0c573ee..ebcdaabb 100644 --- a/blueprints/postgresus/docker-compose.yml +++ b/blueprints/postgresus/docker-compose.yml @@ -1,6 +1,8 @@ services: postgresus: - image: rostislavdugin/postgresus:latest + # Postgresus was renamed upstream to Databasus (https://github.com/databasus/databasus). + # This image is frozen at its final release; use the "databasus" template for new deployments. + image: rostislavdugin/postgresus:v2.15.3 ports: - "4005" volumes: diff --git a/blueprints/postgresus/meta.json b/blueprints/postgresus/meta.json index 05d79890..d5fef960 100644 --- a/blueprints/postgresus/meta.json +++ b/blueprints/postgresus/meta.json @@ -1,13 +1,13 @@ { "id": "postgresus", "name": "Postgresus", - "version": "latest", - "description": "Free, open source and self-hosted solution for automated PostgreSQL backups. With multiple storage options and notifications", + "version": "v2.15.3", + "description": "Deprecated: the project was renamed to Databasus, use the Databasus template instead. Free, open source and self-hosted solution for automated PostgreSQL backups, frozen at its final release", "logo": "postgresus.svg", "links": { "github": "https://github.com/RostislavDugin/postgresus", - "website": "https://postgresus.com", - "docs": "https://postgresus.com" + "website": "https://databasus.com", + "docs": "https://databasus.com" }, "tags": [ "postgres", diff --git a/blueprints/posthog/docker-compose.yml b/blueprints/posthog/docker-compose.yml new file mode 100644 index 00000000..eb8f660c --- /dev/null +++ b/blueprints/posthog/docker-compose.yml @@ -0,0 +1,646 @@ +# PostHog self-hosted ("hobby" deployment, trimmed for a single VPS). +# Based on https://github.com/PostHog/posthog/blob/master/docker-compose.hobby.yml +# Omitted vs upstream hobby (heavy/optional): Temporal + Elasticsearch (data +# warehouse / batch exports), browserless (image exports / heatmap screenshots), +# personhog, cymbal + error-tracking/logs/traces ingestion, capture-ai and +# capture-logs. Everything needed for product analytics, feature flags, +# surveys, session replay and the CDP pipeline is included. + +x-logging: &default-logging + driver: json-file + options: + max-size: "50m" + max-file: "3" + +x-django-env: &django-env + # Postgres + DATABASE_URL: postgres://posthog:posthog@db:5432/posthog + PGHOST: db + PGUSER: posthog + PGPASSWORD: posthog + PERSONS_DB_WRITER_URL: postgres://posthog:posthog@db:5432/posthog + PERSONS_DB_READER_URL: postgres://posthog:posthog@db:5432/posthog + # ClickHouse (users match docker/clickhouse/users.xml from the PostHog repo) + CLICKHOUSE_HOST: clickhouse + CLICKHOUSE_DATABASE: posthog + CLICKHOUSE_SECURE: "false" + CLICKHOUSE_VERIFY: "false" + CLICKHOUSE_API_USER: api + CLICKHOUSE_API_PASSWORD: apipass + CLICKHOUSE_APP_USER: app + CLICKHOUSE_APP_PASSWORD: apppass + CLICKHOUSE_LOGS_CLUSTER_HOST: clickhouse + CLICKHOUSE_LOGS_CLUSTER_SECURE: "false" + # Redis / Kafka + REDIS_URL: redis://redis7:6379/ + KAFKA_HOSTS: kafka:9092 + FLAGS_REDIS_ENABLED: "false" + # Object storage (MinIO) + OBJECT_STORAGE_ENABLED: "true" + OBJECT_STORAGE_ENDPOINT: http://objectstorage:19000 + OBJECT_STORAGE_PUBLIC_ENDPOINT: ${POSTHOG_SITE_URL} + OBJECT_STORAGE_ACCESS_KEY_ID: object_storage_root_user + OBJECT_STORAGE_SECRET_ACCESS_KEY: object_storage_root_password + OBJECT_STORAGE_FORCE_PATH_STYLE: "true" + # Session replay storage + SESSION_RECORDING_V2_S3_ENDPOINT: http://objectstorage:19000 + SESSION_RECORDING_V2_S3_ACCESS_KEY_ID: object_storage_root_user + SESSION_RECORDING_V2_S3_SECRET_ACCESS_KEY: object_storage_root_password + RECORDING_API_URL: http://recording-api:6738 + # App + SITE_URL: ${POSTHOG_SITE_URL} + SECRET_KEY: ${POSTHOG_SECRET_KEY} + ENCRYPTION_SALT_KEYS: ${POSTHOG_ENCRYPTION_SALT_KEYS} + DEPLOYMENT: hobby + IS_BEHIND_PROXY: "true" + DISABLE_SECURE_SSL_REDIRECT: "true" + # "false" so login works over plain HTTP; set to "true" once the domain uses HTTPS + SECURE_COOKIES: ${POSTHOG_SECURE_COOKIES} + CDP_API_URL: http://plugins:6738 + FEATURE_FLAGS_SERVICE_URL: http://feature-flags:3001 + LIVESTREAM_HOST: ${POSTHOG_SITE_URL}/livestream + API_QUERIES_PER_TEAM: '{"1": 100}' + OTEL_SDK_DISABLED: "true" + OTEL_EXPORTER_OTLP_ENDPOINT: "" + +services: + # One-shot: fetches the ClickHouse configuration, Kafka table schemas and + # funnel UDFs from the PostHog repo (pinned to the same commit as the app + # image) plus the GeoLite2 GeoIP database. Idempotent across redeploys. + init-assets: + image: alpine:3.21 + restart: on-failure + logging: *default-logging + entrypoint: /bin/sh + command: + - -c + - | + set -e + apk add --no-cache git curl brotli >/dev/null + if [ ! -f /assets/.ok-${POSTHOG_VERSION} ]; then + echo "Fetching PostHog repo assets @ ${POSTHOG_VERSION}" + rm -rf /tmp/ph && mkdir -p /tmp/ph && cd /tmp/ph + git init -q . + git remote add origin https://github.com/PostHog/posthog.git + git sparse-checkout set --no-cone /docker/clickhouse/ /posthog/idl/ /posthog/user_scripts/ + git -c protocol.version=2 fetch -q --depth 1 --filter=blob:none origin ${POSTHOG_VERSION} + git checkout -q FETCH_HEAD + rm -rf /assets/clickhouse /assets/idl /assets/user_scripts /assets/.ok-* + cp -r docker/clickhouse /assets/clickhouse + cp -r posthog/idl /assets/idl + cp -r posthog/user_scripts /assets/user_scripts + touch /assets/.ok-${POSTHOG_VERSION} + echo "Repo assets ready" + fi + if [ ! -s /share/GeoLite2-City.mmdb ]; then + echo "Downloading GeoLite2 database" + curl -sL --http1.1 https://mmdbcdn.posthog.net/ | brotli --decompress --output=/share/GeoLite2-City.mmdb + chmod 644 /share/GeoLite2-City.mmdb + fi + echo "init-assets done" + volumes: + - posthog-assets:/assets + - geoip-data:/share + + db: + image: postgres:15.12-alpine + restart: unless-stopped + logging: *default-logging + environment: + POSTGRES_USER: posthog + POSTGRES_DB: posthog + POSTGRES_PASSWORD: posthog + healthcheck: + test: ["CMD-SHELL", "pg_isready -U posthog"] + interval: 5s + timeout: 10s + retries: 30 + start_period: 10s + volumes: + - postgres-data:/var/lib/postgresql/data + - ../files/db-init:/docker-entrypoint-initdb.d:ro + + redis7: + image: redis:7.2-alpine + restart: unless-stopped + logging: *default-logging + command: redis-server --maxmemory-policy allkeys-lru --maxmemory 200mb + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 10s + retries: 10 + volumes: + - redis-data:/data + + # Required by ClickHouse for distributed DDL (docker/clickhouse/config.xml + # points at zookeeper:2181) + zookeeper: + image: zookeeper:3.7.0 + restart: unless-stopped + logging: *default-logging + environment: + ZOO_AUTOPURGE_PURGEINTERVAL: 1 + ZOO_AUTOPURGE_SNAPRETAINCOUNT: 3 + volumes: + - zookeeper-data:/data + - zookeeper-datalog:/datalog + + # Kafka API provided by Redpanda (as upstream), tuned down for small servers + kafka: + image: docker.io/redpandadata/redpanda:v25.1.9 + restart: unless-stopped + logging: *default-logging + command: + - redpanda + - start + - --kafka-addr internal://0.0.0.0:9092 + - --advertise-kafka-addr internal://kafka:9092 + - --rpc-addr kafka:33145 + - --advertise-rpc-addr kafka:33145 + - --mode dev-container + - --smp 1 + - --memory 1G + - --reserve-memory 0M + - --overprovisioned + - --set redpanda.empty_seed_starts_cluster=false + - --seeds kafka:33145 + - --set redpanda.auto_create_topics_enabled=true + volumes: + - redpanda-data:/var/lib/redpanda/data + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:9644/v1/status/ready || exit 1"] + interval: 5s + timeout: 10s + retries: 30 + start_period: 20s + + # One-shot: pre-creates the Kafka topics that consumers check at startup + kafka-init: + image: docker.io/redpandadata/redpanda:v25.1.9 + restart: on-failure + logging: *default-logging + entrypoint: /bin/sh + depends_on: + kafka: + condition: service_healthy + command: + - -c + - | + set -e + TOPICS="clickhouse_events_json \ + clickhouse_ai_events_json \ + clickhouse_heatmap_events \ + clickhouse_ingestion_warnings \ + events_plugin_ingestion \ + events_plugin_ingestion_dlq \ + events_plugin_ingestion_overflow \ + events_plugin_ingestion_async \ + events_plugin_ingestion_historical \ + ingestion-clientwarnings-main-1 \ + heatmaps_ingestion \ + session_recording_snapshot_item_events \ + session_recording_snapshot_item_overflow \ + clickhouse_groups \ + clickhouse_person \ + clickhouse_person_distinct_id \ + clickhouse_app_metrics2 \ + log_entries \ + clickhouse_tophog" + for topic in $$TOPICS; do + rpk topic create "$$topic" --brokers kafka:9092 -p 1 -r 1 2>/dev/null \ + || rpk topic list --brokers kafka:9092 | grep -q "$$topic" + done + echo "Kafka topics ready" + + clickhouse: + image: clickhouse/clickhouse-server:26.6.1.1193 + restart: unless-stopped + logging: *default-logging + depends_on: + init-assets: + condition: service_completed_successfully + zookeeper: + condition: service_started + kafka: + condition: service_started + entrypoint: /bin/bash + command: + - -c + - | + set -e + cp /assets/clickhouse/config.xml /etc/clickhouse-server/config.xml + cp /assets/clickhouse/config.d/default.xml /etc/clickhouse-server/config.d/default.xml + cp /assets/clickhouse/users.xml /etc/clickhouse-server/users.xml + cp /assets/clickhouse/user_defined_function.xml /etc/clickhouse-server/user_defined_function.xml + mkdir -p /var/lib/clickhouse/format_schemas /var/lib/clickhouse/user_scripts + cp -r /assets/idl/. /var/lib/clickhouse/format_schemas/ + cp -r /assets/user_scripts/. /var/lib/clickhouse/user_scripts/ + chmod -R 755 /var/lib/clickhouse/user_scripts + chown -R clickhouse:clickhouse /var/lib/clickhouse/format_schemas /var/lib/clickhouse/user_scripts || true + # Materialize system log tables (system.crash_log is required by the + # custom_metrics ClickHouse migration), mirroring upstream init-db.sh + ( + i=0 + while [ $$i -lt 90 ]; do + if clickhouse-client --query "SELECT 1" >/dev/null 2>&1; then + clickhouse-client --query "SYSTEM FLUSH LOGS" && echo "system logs flushed" && break + fi + sleep 2 + i=$$((i+1)) + done + ) & + exec /entrypoint.sh + environment: + CLICKHOUSE_SKIP_USER_SETUP: 1 + KAFKA_HOSTS: kafka:9092 + volumes: + - posthog-assets:/assets:ro + - clickhouse-data:/var/lib/clickhouse + healthcheck: + test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:8123/ping || exit 1"] + interval: 5s + timeout: 10s + retries: 30 + start_period: 30s + + # Django app: runs migrations, then serves the API + UI on :8000 + web: + image: posthog/posthog:${POSTHOG_VERSION} + restart: unless-stopped + logging: *default-logging + command: ["sh", "-c", "./bin/migrate && exec ./bin/docker-server"] + depends_on: + db: + condition: service_healthy + redis7: + condition: service_healthy + clickhouse: + condition: service_healthy + kafka: + condition: service_healthy + kafka-init: + condition: service_completed_successfully + objectstorage: + condition: service_started + environment: + <<: *django-env + NGINX_UNIT_APP_PROCESSES: 2 + healthcheck: + test: ["CMD-SHELL", "curl -fs http://localhost:8000/_health || exit 1"] + interval: 30s + timeout: 10s + retries: 15 + start_period: 600s + + # Celery worker + scheduler + worker: + image: posthog/posthog:${POSTHOG_VERSION} + restart: unless-stopped + logging: *default-logging + command: ./bin/docker-worker-celery --with-scheduler + depends_on: + db: + condition: service_healthy + redis7: + condition: service_healthy + clickhouse: + condition: service_healthy + kafka: + condition: service_healthy + web: + condition: service_started + environment: + <<: *django-env + POSTHOG_SKIP_MIGRATION_CHECKS: "1" + WEB_CONCURRENCY: 2 + + # Node plugin server: CDP (destinations, webhooks, transformations) + plugins: + image: posthog/posthog-node:${POSTHOG_NODE_VERSION} + restart: unless-stopped + logging: *default-logging + depends_on: + db: + condition: service_healthy + redis7: + condition: service_healthy + kafka: + condition: service_healthy + clickhouse: + condition: service_started + kafka-init: + condition: service_completed_successfully + init-assets: + condition: service_completed_successfully + environment: + # 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 00000000..1a6544f9 Binary files /dev/null and b/blueprints/silex/logo.png differ diff --git a/blueprints/silex/meta.json b/blueprints/silex/meta.json new file mode 100644 index 00000000..d1d046f8 --- /dev/null +++ b/blueprints/silex/meta.json @@ -0,0 +1,17 @@ +{ + "id": "silex", + "name": "Silex", + "version": "3.7.0", + "description": "Silex is a free and open-source website builder for professional designers. It lets you create static websites visually with a drag-and-drop editor, custom CSS and JavaScript, and publish them anywhere.", + "logo": "logo.png", + "links": { + "github": "https://github.com/silexlabs/Silex", + "website": "https://www.silex.me", + "docs": "https://docs.silex.me" + }, + "tags": [ + "website-builder", + "no-code", + "static-site" + ] +} diff --git a/blueprints/silex/template.toml b/blueprints/silex/template.toml new file mode 100644 index 00000000..606a1eff --- /dev/null +++ b/blueprints/silex/template.toml @@ -0,0 +1,13 @@ +[variables] +main_domain = "${domain}" +session_secret = "${password:32}" + +[config] +[[config.domains]] +serviceName = "silex" +port = 6805 +host = "${main_domain}" + +[config.env] +SILEX_URL = "http://${main_domain}" +SILEX_SESSION_SECRET = "${session_secret}" diff --git a/blueprints/sim/docker-compose.yml b/blueprints/sim/docker-compose.yml new file mode 100644 index 00000000..cc920b18 --- /dev/null +++ b/blueprints/sim/docker-compose.yml @@ -0,0 +1,76 @@ +services: + simstudio: + image: ghcr.io/simstudioai/simstudio:latest + restart: unless-stopped + environment: + - NODE_ENV=production + - DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@db:5432/simstudio + - BETTER_AUTH_URL=${SIM_APP_URL} + - NEXT_PUBLIC_APP_URL=${SIM_APP_URL} + - BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET} + - ENCRYPTION_KEY=${ENCRYPTION_KEY} + - API_ENCRYPTION_KEY=${API_ENCRYPTION_KEY} + - INTERNAL_API_SECRET=${INTERNAL_API_SECRET} + - SOCKET_SERVER_URL=http://realtime:3002 + depends_on: + db: + condition: service_healthy + migrations: + condition: service_completed_successfully + realtime: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-fsS", "http://127.0.0.1:3000"] + interval: 30s + timeout: 5s + retries: 5 + start_period: 30s + + realtime: + image: ghcr.io/simstudioai/realtime:latest + restart: unless-stopped + environment: + - NODE_ENV=production + - DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@db:5432/simstudio + - NEXT_PUBLIC_APP_URL=${SIM_APP_URL} + - BETTER_AUTH_URL=${SIM_APP_URL} + - BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET} + - INTERNAL_API_SECRET=${INTERNAL_API_SECRET} + depends_on: + db: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-fsS", "http://127.0.0.1:3002/health"] + interval: 30s + timeout: 5s + retries: 5 + start_period: 15s + + migrations: + image: ghcr.io/simstudioai/migrations:latest + working_dir: /app/packages/db + environment: + - DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@db:5432/simstudio + depends_on: + db: + condition: service_healthy + command: ["bun", "run", "db:migrate"] + restart: "no" + + db: + image: pgvector/pgvector:pg17 + restart: unless-stopped + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - POSTGRES_DB=simstudio + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 5 + +volumes: + postgres_data: diff --git a/blueprints/sim/meta.json b/blueprints/sim/meta.json new file mode 100644 index 00000000..1a774468 --- /dev/null +++ b/blueprints/sim/meta.json @@ -0,0 +1,17 @@ +{ + "id": "sim", + "name": "Sim", + "version": "latest", + "description": "Open-source visual workflow builder for building and deploying AI agent workflows.", + "logo": "sim.svg", + "links": { + "github": "https://github.com/simstudioai/sim", + "website": "https://sim.ai", + "docs": "https://docs.sim.ai" + }, + "tags": [ + "ai", + "automation", + "workflow" + ] +} diff --git a/blueprints/sim/sim.svg b/blueprints/sim/sim.svg new file mode 100644 index 00000000..d1b94a8d --- /dev/null +++ b/blueprints/sim/sim.svg @@ -0,0 +1 @@ + diff --git a/blueprints/sim/template.toml b/blueprints/sim/template.toml new file mode 100644 index 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 00000000..77099a12 Binary files /dev/null and b/blueprints/spacetimedb/logo.png differ 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 00000000..64a306fd Binary files /dev/null and b/blueprints/steedos/steedos.png differ 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 00000000..b6f059ce Binary files /dev/null and b/blueprints/trek/trek.png differ diff --git a/blueprints/triggerdotdev/instructions.md b/blueprints/triggerdotdev/instructions.md new file mode 100644 index 00000000..2f0415d1 --- /dev/null +++ b/blueprints/triggerdotdev/instructions.md @@ -0,0 +1,54 @@ +# Trigger.dev Setup Instructions + +## Deploy + +1. In Dokploy, create the service from the **Trigger.dev** template. +2. Dokploy generates all required secrets (`MAGIC_LINK_SECRET`, `SESSION_SECRET`, `ENCRYPTION_KEY`, database credentials, etc.) automatically. +3. Deploy and wait until the containers are running. The main domain points to the `webapp` service (port `3000`). + +If you serve the app over HTTPS, set `TRIGGER_PROTOCOL=https` in the **Environment** tab (the template defaults to `http`) so that login links use the correct scheme, then redeploy. + +## First login (no email server configured) + +Trigger.dev logs you in with **magic links** sent by email. The template does not configure an email transport by default, so the email is never actually sent. Instead, **the magic link is printed to the logs of the `webapp` container**: + +1. Open `https://` and enter your email address to request a magic link. +2. In Dokploy, go to your Trigger.dev service, open the **Logs** tab, and select the `webapp` container/service. +3. Look for a recent log entry containing a URL like `.../magic?token=...` (search for `magic`). +4. Copy that URL into your browser to complete the login. + +## Configure email sending (optional) + +To have magic links delivered by email, add these variables in the **Environment** tab and redeploy. + +Using SMTP: + +``` +EMAIL_TRANSPORT=smtp +FROM_EMAIL=trigger@example.com +REPLY_TO_EMAIL=trigger@example.com +SMTP_HOST=smtp.example.com +SMTP_PORT=587 +SMTP_SECURE=false +SMTP_USER=your-smtp-user +SMTP_PASSWORD=your-smtp-password +``` + +Using [Resend](https://resend.com): + +``` +EMAIL_TRANSPORT=resend +FROM_EMAIL=trigger@example.com +REPLY_TO_EMAIL=trigger@example.com +RESEND_API_KEY=your-resend-api-key +``` + +## Restrict who can sign up (recommended) + +By default anyone who can reach the webapp can request a magic link. Restrict access with a regex of allowed email addresses: + +``` +WHITELISTED_EMAILS=you@example\.com|teammate@example\.com +``` + +You can also grant admin rights by email with `ADMIN_EMAILS` (same regex format). 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); +});