From 08d87d8a90d2c551d76e524d98f33f03d7b69d77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Krzy=C5=BCanowski?= Date: Mon, 20 Jul 2026 03:19:42 +0200 Subject: [PATCH] feat: add struxa template (#1037) * feat: add struxa template Adds a Dokploy blueprint for Struxa (github.com/struxadotcloud/struxa), a self-hosted, open-source server management panel. Deploys the published GHCR images (dashboard, watchkeeper, migrate) plus MySQL and MinIO sidecars. The web service generates its RS256 JWT keypair at startup via Node's built-in crypto module, since no Dokploy template helper covers RSA key generation. * fix: pin struxa images to latest floating tag struxa's release workflow pushes a latest floating tag alongside version tags on every stable release, matching the convention used by ~44% of existing blueprints (version: "latest" in meta.json). * fix: generate a 64-char DATABASE_ENCRYPTION_KEY Dokploy's runtime \${hash:N} helper produces N hex characters, not N bytes (the templates repo's local preview helpers.ts models it as byte count, which is inconsistent with the platform). struxa's env schema requires DATABASE_ENCRYPTION_KEY to be >=64 chars (it's hex decoded to a 32-byte AES-256 key), so \${hash:32} only produced half the required length. Confirmed against other blueprints (sim, chatwoot, outline) which all use \${hash:64} for 64-char secrets. * fix: address Copilot review comments - Add explicit \`version: "3.8"\` to docker-compose.yml, matching the documented template format. - Pin minio/minio to a specific RELEASE tag (used elsewhere in the repo: plane, posthog) instead of the floating latest tag. - Default BETTER_AUTH_URL/APP_URL/CORS_ORIGIN to http://, matching repo convention for Dokploy-fronted apps (TLS terminates at the proxy). * fix: rename web service to struxa AGENTS.md convention: the primary Docker service name should match the blueprint folder name. Renamed the web/dashboard service from "web" to "struxa" in docker-compose.yml and updated template.toml's config.domains.serviceName to match. --- blueprints/struxa/docker-compose.yml | 90 ++++++++++++++++++++++++++++ blueprints/struxa/meta.json | 18 ++++++ blueprints/struxa/struxa.svg | 3 + blueprints/struxa/template.toml | 34 +++++++++++ 4 files changed, 145 insertions(+) create mode 100644 blueprints/struxa/docker-compose.yml create mode 100644 blueprints/struxa/meta.json create mode 100644 blueprints/struxa/struxa.svg create mode 100644 blueprints/struxa/template.toml diff --git a/blueprints/struxa/docker-compose.yml b/blueprints/struxa/docker-compose.yml new file mode 100644 index 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}"