mirror of
https://github.com/Dokploy/templates.git
synced 2026-07-24 23:35:25 +02:00
New release (#1038)
* fix: meta.json of Poke blueprint The current github/docs links was broken before: https://codeberg.org/ashley/poke after: https://codeberg.org/ashleyirispuppy/poke * fix: broken links in some blueprints * Add openhabittracker blueprint * fix(affinepro): update to current AFFiNE images The template pinned ghcr.io/toeverything/affine-graphql:stable-780dd83, an ephemeral per-commit tag on the old image name that has since been removed from GHCR, so the template could no longer pull and deploy. - Switch both app and migration services to ghcr.io/toeverything/affine:stable, the rolling stable tag recommended by the official self-host compose, so the template no longer rots when per-commit tags are garbage-collected - Move postgres to pgvector/pgvector:pg16 and set AFFINE_INDEXER_ENABLED=false, matching the current upstream self-host compose requirements - Add postgres/redis healthchecks and proper depends_on conditions so the self-host-predeploy migration job runs only once the database is ready, and the server starts only after migrations complete successfully Closes #777 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(cockpit): persist config and storage volumes per upstream docs The template mounted `html:/var/www/html` (freezing the app code from the first deploy) and `data:/var/www/html/storage/data`, which misses uploaded assets (stored under `storage/uploads`) and cache. Per the official Cockpit persistent-storage docs, mount `config:/var/www/html/config` and `storage:/var/www/html/storage` instead. Closes #549 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(kener): define REDIS_URL and drop unused databases Kener v4 requires Redis for its BullMQ queues, caching and scheduler, so the app crashed on startup with "REDIS_URL is not defined in environment variables" right after printing that it was running (issue #976). The template also spun up both a postgres and a mariadb container while DATABASE_URL pointed at SQLite, so neither database was ever used (issue #282). Changes, aligned with the upstream docker-compose.yml (rajnandan1/kener): - Add a redis:7-alpine service with a healthcheck and persistent volume, wire REDIS_URL=redis://redis:6379 and make kener depend on redis being healthy - Remove the unused postgres and mysql services plus their env vars and generated passwords; keep SQLite (upstream default) persisted in the kener_db volume - Set ORIGIN to https://<domain> instead of http://localhost:3000 so CSRF protection works behind the generated domain - Drop the stale /app/uploads bind mount (kener v4 only persists /app/database) and the obsolete compose version key Deploy-tested on a Dokploy instance: migrations and seed complete, schedulers start, "Kener version 4.1.2 is running!", HTTP 200 on the generated domain, both containers stable. Closes #976 Closes #282 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(homarr): use named volume for /appdata and hex encryption key - Replace the relative bind mount `../homarr/appdata:/appdata` with a named volume `homarr_appdata:/appdata` so the data survives cleanups and can be backed up through Dokploy volume backups. - Generate SECRET_ENCRYPTION_KEY with `${hash:64}` instead of `${password:64}`: homarr requires a 64-character hex string (`openssl rand -hex 32`) and the password helper emits non-hex characters (g-z). Closes #738 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(logto): fix postgres role error and update to 1.41.0 The postgres healthcheck ran pg_isready without -U while POSTGRES_USER was set to logto, so every probe attempted to connect as role "postgres" and spammed FATAL: role "postgres" does not exist. Pass -U logto -d logto explicitly and set POSTGRES_DB. Also fix LOGTO_ENDPOINT pointing at the admin domain instead of the main domain, update Logto 1.27.0 -> 1.41.0, use ${password:32} for the generated password, drop the ephemeral host port publishing and add restart policies. Closes #130 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(gitlab-ce): serve on the generated domain (remove removed unicorn config, pin omnibus) The template set unicorn['worker_processes'] / unicorn['worker_timeout'] in GITLAB_OMNIBUS_CONFIG. Unicorn was removed in GitLab 14.0, so on current gitlab-ce images `gitlab-ctl reconfigure` aborts with "Removed configurations found in gitlab.rb" and GitLab never starts serving — the generated domain returned 502 Bad Gateway (issue #380). Rewrite the template as the official all-in-one Omnibus setup: - pin gitlab/gitlab-ce:19.1.2-ce.0 instead of :latest - external_url http://<domain> with nginx listen_port 80 / listen_https false (TLS terminates at Traefik) - set gitlab_rails['initial_root_password'] from a generated template password - puma single mode + prometheus monitoring off to keep memory reasonable - drop the external postgres/redis containers (Omnibus bundles and manages its own) and the broken ports entries ("2224" published a random host port while sshd listens on 22) Verified on a live Dokploy instance: domain serves the GitLab sign-in page and root login with the generated password works. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(zitadel): wire EXTERNALDOMAIN/PORT/SECURE to the generated domain Zitadel resolves its instance from the external domain/port/scheme, so the template's ZITADEL_EXTERNALPORT=8080 made every generated URL (OIDC issuer, login redirects) point at http://<domain>:8080 instead of the domain served by Traefik, ending in {"code":5,"message":"Not Found"} (#516). - Expose ZITADEL_EXTERNALDOMAIN / ZITADEL_EXTERNALPORT / ZITADEL_EXTERNALSECURE as template env vars, defaulting to the generated domain over HTTP (port 80, secure=false) so the routed domain and Zitadel's external config always match; HTTPS users flip them to 443/true in the Env tab. - Pin image to v4.16.0 (was latest) and keep the built-in login v1 via ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_REQUIRED=false so no separate login-v2 container is needed. - Add the official readiness healthcheck, drop the unused /app/data volume, bogus SMTP env keys, published port and obsolete compose version key. Closes #516 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(immich): update to v3.0.2 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(automatisch): switch to official image and wire up DOMAIN to fix Bad Gateway - Replace unofficial dockeriddonuts/automatisch:2.0 mirror (stale since Nov 2024) with the official automatischio/automatisch:0.15.0 image - Define DOMAIN in template.toml [config.env]: the compose referenced ${DOMAIN} but it was never provided, so HOST resolved empty and the app generated localhost URLs for the web app and webhooks - Set API_URL/WEB_APP_URL so generated URLs don't carry the internal :3000 port behind Traefik - Add postgres healthcheck and gate the app on service_healthy to avoid the boot-time migration race; worker now starts after main (official compose order) - Drop unnecessary published port and no-op REDIS_* env on the redis service - Bump meta.json version to 0.15.0 Closes #97 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(signoz): fix ClickHouse hostname resolution and IPv6 binding - Override the ClickHouse image's config.d/docker_related_config.xml, which listens on the IPv6 wildcard address ([::]) first and fails to bind on hosts without IPv6 support; listen on 0.0.0.0 (IPv4) only and disable the internal DNS cache so transient startup DNS failures for zookeeper-1/clickhouse are not cached (issue #534). - Set hostname: clickhouse (instead of container_name) so the server identifies itself as the cluster host declared in cluster.xml. - Remove the explicit signoz-net network: Dokploy attaches its own networks automatically and the repo validator rejects explicit networks; a fixed-name shared network also breaks service DNS across deployments. - Fix mount mismatch: template.toml created files/signoz/prometheus.xml while the compose mounts ../files/signoz/prometheus.yml, so Docker silently mounted a directory as the signoz prometheus config. - Give the otel-collector domain its own generated host: both domains previously shared the same host and path "/", creating two conflicting Traefik routers where one service randomly won. Closes #534 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(infisical): update to v0.162.6 - Bump image from v0.135.0-postgres to v0.162.6 (upstream dropped the -postgres tag suffix after v0.146.0; plain tags are the postgres build) - Remove the db-migration service: the standalone image runs database migrations automatically on boot (auto-start-migrations), matching the official docker-compose.prod.yml which no longer ships that service - Generate ENCRYPTION_KEY (${hash:32}) and AUTH_SECRET (${base64:32}) per deployment instead of shipping hardcoded example secrets - Fix SITE_URL to http://${main_domain} (no :8080; the domain is proxied) - Refresh SMTP env names (SMTP_FROM_ADDRESS/SMTP_FROM_NAME; SMTP_SECURE was removed upstream) and drop unused CLIENT_* env placeholders that were never passed to the container Closes #215 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: add databasus (postgresus renamed upstream) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(evolutionapi): enable integration flags and update version Adds the integration enable env flags introduced in Evolution API v2.3.x (N8N_ENABLED, OPENAI_ENABLED, EVOAI_ENABLED, DIFY_ENABLED, TYPEBOT_ENABLED, CHATWOOT_ENABLED) so integrations no longer fail with 'N8n is disabled', and pins the image to v2.3.7 instead of latest. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(rybbit): update to v2.7.0 The template shipped rybbit v1.5.1, which is no longer supported and is vulnerable to known React2Shell exploits (see #659). Update to the latest stable release v2.7.0 and align the stack with the upstream docker-compose: - Pin ghcr.io/rybbit-io/rybbit-backend and rybbit-client to v2.7.0 - Add the redis service now required by the backend (session tracking and BullMQ queues), with password auth, AOF persistence and noeviction policy per upstream - Drop the DOMAIN_NAME env var (only used by the upstream Caddy webserver, which Dokploy replaces with Traefik) - Add DISABLE_TELEMETRY env and generate BETTER_AUTH_SECRET as base64 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(phpmyadmin): raise PHP upload limit so SQL imports over 2MB work Without UPLOAD_LIMIT the phpMyAdmin image keeps PHP's default 2M post_max_size/upload_max_filesize, so importing any SQL file larger than 2MB fails with 'POST Content-Length exceeds the limit of 2097152 bytes' followed by a session_start error page. - Add UPLOAD_LIMIT (512M) and MAX_EXECUTION_TIME (600) env vars, configurable from the template env - Wire the previously unused MYSQL_DATABASE env var into the compose file, replacing the hardcoded 'tu_base_de_datos' placeholder, and default it to 'phpmyadmin' instead of the system 'mysql' schema Closes #236 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(openpanel): pin verified 2.2.1 images and fix ClickHouse interserver host - Pin lindesvard/openpanel-api|dashboard|worker to 2.2.1 (verified working end-to-end on a Dokploy instance) instead of the mutable :2 tag, so a future broken 2.x release cannot silently break the template again — which is how #615 originated when 2.0.0 shipped. - Fix interserver_http_host to match the actual service name (op-ch), aligning with upstream self-hosting config. - Set meta.json version to the pinned 2.2.1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(wordpress): route loopback requests through Traefik to fix cURL error 28 WordPress resolves its own public domain to the server's public IP from inside the container, so REST API / WP-Cron / Site Health loopback requests depend on hairpin NAT, which times out (cURL error 28) on many hosts. Pin the site domain to the dokploy-traefik container IP in /etc/hosts at startup (with retries, since Traefik joins the compose network shortly after the container starts) so loopback traffic stays inside the Docker network for both HTTP and HTTPS. No-op fallback when dokploy-traefik is not resolvable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(minio): migrate to pgsty/minio community fork (upstream is source-only and archived) MinIO Inc. stopped publishing Docker images and pre-built binaries in October 2025 and archived the minio/minio repository in February 2026. The official minio/minio image is frozen at RELEASE.2025-09-07T16-13-09Z and no longer receives security fixes (the fix for CVE-2025-62506 was never published as an image). - Switch image to pgsty/minio:RELEASE.2026-06-18T00-00-00Z, the actively maintained community fork (drop-in replacement, AGPLv3, same env vars, same /data on-disk format, restores the full web console) - Route the S3 API (port 9000) through its own generated domain instead of publishing host port 9000, per repo conventions - Update meta.json links/description to point at the maintained fork Closes #469 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(nextcloud-aio): drop AIO branding, keep template id, pin nextcloud 33.0.6 The template ships plain nextcloud + mariadb + redis + cron, not the nextcloud/all-in-one mastercontainer. Display name and description no longer claim AIO; folder id is kept as nextcloud-aio to avoid breaking existing installs. Image pinned to the current stable tag (33.0.6). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: add MailCatcher NG template Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: add per-template setup instructions (supabase, trigger.dev) Adds instructions.md files for the Supabase and Trigger.dev templates and renders them in the template browser as a new Instructions tab in the template dialog (fetched from blueprints/<id>/instructions.md, rendered with a small dependency-free markdown component). Also removes the placeholder blueprints/ackee/instructions.md so it does not surface as an empty Instructions tab. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: add Weblate template Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: add Synapse (Matrix homeserver) template Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: add Dify template Dify 1.15.0 (open-source LLM app development platform): api + worker + worker_beat + web + pgvector PostgreSQL (also used as the vector store) + redis + sandbox + ssrf_proxy (squid) + plugin_daemon, fronted by an internal nginx gateway that mirrors the upstream path routing. Closes #88 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: add Sim Studio template Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: add Steedos template Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: add Silex template Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: add SpacetimeDB template Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: add CertMate template Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: add PostHog template Based on the official hobby deployment, trimmed to run on a single server: Django web + Celery worker, Node plugin-server (CDP, ingestion, session replay, recording API), Rust capture/replay-capture/ feature-flags/hypercache, livestream, Caddy path router, Postgres, Redis, Redpanda (Kafka), ZooKeeper, ClickHouse and MinIO. Images are pinned to a tested master commit (PostHog no longer publishes tagged releases); an init container fetches the matching ClickHouse configuration, Kafka table schemas, funnel UDF binaries and the GeoIP database at the same pinned ref. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: add Markup template Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: add Frappe LMS template Adds a Frappe LMS (Frappe Learning) template based on the erpnext-v16 blueprint pattern (configurator + create-site + workers + scheduler + websocket + mariadb + 3x redis). Pins ghcr.io/frappe/lms:v2.52.0, the latest upstream image that ships the lms app (v2.52.1+ images are currently published without any apps baked in). The image does not bundle the payments app, so create-site clears lms's required_apps before install; paid-course checkout is unavailable, everything else works. The backend is gated on a create-site completion marker so gunicorn workers never serve (and cache) a half-installed site during the 3-6 minute first boot. Closes #517 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: add TREK template Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: add Oryx (SRS) media streaming template Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: stop plugins service crash loop (logs-redis TLS default) In production images LOGS_REDIS_TLS/TRACES_REDIS_TLS default to true, so the logs-ingestion consumer (part of the plugin-server default mode) kept attempting a TLS handshake against the plaintext Redis, timed out every ~11s and killed the process after 10 errors (restart loop every ~2.5 min). Constrain the plugins service to the capability groups this template ships (cdp_workflows, realtime_cohorts, feature_flags) and pin the redis TLS flags to false as belt-and-braces. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: add n8n queue mode template n8n running in queue mode (EXECUTIONS_MODE=queue): main instance for the editor/webhooks, a scalable pool of workers (deploy.replicas driven by the N8N_WORKER_REPLICAS env var), Redis (Bull) as the queue broker and PostgreSQL as the database. Pinned to the current stable n8nio/n8n:2.30.4. Closes #455 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: add bonfire template Bonfire (social flavour) — federated social networking toolkit. App + PostGIS + Meilisearch, mirroring upstream's reference deployments. Closes #125 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: add dagu blueprint Dagu is a self-contained workflow engine with a Web UI for scheduling and orchestrating jobs as DAGs defined in declarative YAML. - Pin ghcr.io/dagucloud/dagu:2.10.7 and expose the Web UI on port 8080 - Set DAGU_HOST=0.0.0.0 so the service is reachable through the proxy - Enable basic auth with a generated per-deploy admin password - Persist /var/lib/dagu (DAGs, logs, run history) in a named volume Claude-Session: https://claude.ai/code/session_01CRNT6sa2q2P9rUTokeoGhQ * feat: add OneUptime template Adds OneUptime (open-source observability platform: uptime monitoring, status pages, incident management, on-call, logs/traces/metrics) as a template, following upstream's current single-app architecture (app + nginx ingress + postgres + redis + clickhouse + probe). Closes #514 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: add Mailu template Mailu 2024.06 mail server: front (nginx), admin, imap (dovecot), smtp (postfix), antispam (rspamd), Roundcube webmail and redis. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(templates): add systemprompt (#1032) * feat(templates): add systemprompt * fix header comment: accurate in both repos * chore: normalize template tags to kebab-case (#987) * feat: adds `check-links.js` to find broken links (#983) * feat: add struxa template (#1037) * feat: add struxa template Adds a Dokploy blueprint for Struxa (github.com/struxadotcloud/struxa), a self-hosted, open-source server management panel. Deploys the published GHCR images (dashboard, watchkeeper, migrate) plus MySQL and MinIO sidecars. The web service generates its RS256 JWT keypair at startup via Node's built-in crypto module, since no Dokploy template helper covers RSA key generation. * fix: pin struxa images to latest floating tag struxa's release workflow pushes a latest floating tag alongside version tags on every stable release, matching the convention used by ~44% of existing blueprints (version: "latest" in meta.json). * fix: generate a 64-char DATABASE_ENCRYPTION_KEY Dokploy's runtime \${hash:N} helper produces N hex characters, not N bytes (the templates repo's local preview helpers.ts models it as byte count, which is inconsistent with the platform). struxa's env schema requires DATABASE_ENCRYPTION_KEY to be >=64 chars (it's hex decoded to a 32-byte AES-256 key), so \${hash:32} only produced half the required length. Confirmed against other blueprints (sim, chatwoot, outline) which all use \${hash:64} for 64-char secrets. * fix: address Copilot review comments - Add explicit \`version: "3.8"\` to docker-compose.yml, matching the documented template format. - Pin minio/minio to a specific RELEASE tag (used elsewhere in the repo: plane, posthog) instead of the floating latest tag. - Default BETTER_AUTH_URL/APP_URL/CORS_ORIGIN to http://, matching repo convention for Dokploy-fronted apps (TLS terminates at the proxy). * fix: rename web service to struxa AGENTS.md convention: the primary Docker service name should match the blueprint folder name. Renamed the web/dashboard service from "web" to "struxa" in docker-compose.yml and updated template.toml's config.domains.serviceName to match. * Add PodFetch template (#1035) * Add PodFetch template * Use env array syntax in template.toml * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Address review: match version to image tag, expose admin_username variable, dark-mode friendly logo --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * fix(docker): share plugins and themes volumes across containers (#990) * feat(directus): upgrade template to Directus 12.1.1 (#986) * feat(directus): upgrade template to Directus 12.1.1 * feat(directus): add commented S3 env variables to template.toml --------- Co-authored-by: Zakher Masri <46135573+zaaakher@users.noreply.github.com> Co-authored-by: zakher <zakhermasri@gmail.com> Co-authored-by: Jinjinov <dzindzinovic@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Stephan Fitzpatrick <stephan@knowsuchagency.com> Co-authored-by: Mauricio Siu <mauricio@dokploy.com> Co-authored-by: Edward Burton <ejb503@gmail.com> Co-authored-by: Marko Kostich <markokosticdev@gmail.com> Co-authored-by: Jakub Krzyżanowski <jakubkrzyz.kontakt@gmail.com> Co-authored-by: SamTV12345 <40429738+SamTV12345@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Mafly <110502739+Nekoplii@users.noreply.github.com>
This commit is contained in:
@@ -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<TemplateDialogProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Tabs defaultValue="docker-compose" className="w-full">
|
||||
<Tabs
|
||||
defaultValue={
|
||||
templateFiles?.instructions
|
||||
? "instructions"
|
||||
: "docker-compose"
|
||||
}
|
||||
className="w-full"
|
||||
>
|
||||
<TabsList className="w-full justify-start">
|
||||
{templateFiles?.instructions && (
|
||||
<TabsTrigger
|
||||
value="instructions"
|
||||
className="data-[state=active]:font-bold"
|
||||
>
|
||||
Instructions
|
||||
</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger
|
||||
value="docker-compose"
|
||||
className="data-[state=active]:font-bold"
|
||||
@@ -184,6 +201,24 @@ const TemplateDialog: React.FC<TemplateDialogProps> = ({
|
||||
Configuration
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{templateFiles?.instructions && (
|
||||
<TabsContent value="instructions" className="mt-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="flex flex-col items-start w-fit justify-start gap-1">
|
||||
<span className="leading-tight text-xl font-semibold">
|
||||
Setup Instructions
|
||||
</span>
|
||||
<span className="leading-tight text-sm text-gray-500">
|
||||
instructions.md
|
||||
</span>
|
||||
</Label>
|
||||
<div className="rounded-md border p-4">
|
||||
<Markdown content={templateFiles.instructions} />
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
<TabsContent value="docker-compose" className="mt-4">
|
||||
{templateFiles?.dockerCompose && (
|
||||
<div className="space-y-2">
|
||||
|
||||
@@ -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<TemplateGridProps> = ({ 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);
|
||||
}
|
||||
|
||||
177
app/src/components/ui/markdown.tsx
Normal file
177
app/src/components/ui/markdown.tsx
Normal file
@@ -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(
|
||||
<code
|
||||
key={key++}
|
||||
className="px-1.5 py-0.5 rounded bg-muted font-mono text-[0.85em]"
|
||||
>
|
||||
{token.slice(1, -1)}
|
||||
</code>
|
||||
);
|
||||
} else if (token.startsWith("**")) {
|
||||
nodes.push(<strong key={key++}>{token.slice(2, -2)}</strong>);
|
||||
} else {
|
||||
const link = /^\[([^\]]+)\]\(([^)\s]+)\)$/.exec(token);
|
||||
if (link) {
|
||||
nodes.push(
|
||||
<a
|
||||
key={key++}
|
||||
href={link[2]}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 dark:text-blue-400 underline underline-offset-2"
|
||||
>
|
||||
{link[1]}
|
||||
</a>
|
||||
);
|
||||
} else {
|
||||
nodes.push(token);
|
||||
}
|
||||
}
|
||||
lastIndex = match.index + token.length;
|
||||
}
|
||||
if (lastIndex < text.length) {
|
||||
nodes.push(text.slice(lastIndex));
|
||||
}
|
||||
return nodes;
|
||||
};
|
||||
|
||||
const HEADING_CLASSES: Record<number, string> = {
|
||||
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<MarkdownProps> = ({ 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(
|
||||
<pre
|
||||
key={key++}
|
||||
className="bg-muted rounded-md p-4 overflow-x-auto text-sm font-mono whitespace-pre"
|
||||
>
|
||||
<code>{code.join("\n")}</code>
|
||||
</pre>
|
||||
);
|
||||
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(
|
||||
<Tag key={key++} className={HEADING_CLASSES[level]}>
|
||||
{renderInline(heading[2])}
|
||||
</Tag>
|
||||
);
|
||||
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(
|
||||
<ul key={key++} className="list-disc pl-6 space-y-1">
|
||||
{items.map((item, idx) => (
|
||||
<li key={idx}>{renderInline(item)}</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
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(
|
||||
<ol key={key++} className="list-decimal pl-6 space-y-1">
|
||||
{items.map((item, idx) => (
|
||||
<li key={idx}>{renderInline(item)}</li>
|
||||
))}
|
||||
</ol>
|
||||
);
|
||||
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(
|
||||
<p key={key++} className="leading-relaxed">
|
||||
{renderInline(paragraph.join(" "))}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return <div className="space-y-3 text-sm">{blocks}</div>;
|
||||
};
|
||||
|
||||
export default Markdown;
|
||||
Reference in New Issue
Block a user