diff --git a/panel/lib/DockerManager.php b/panel/lib/DockerManager.php index ae49b14..db85d31 100644 --- a/panel/lib/DockerManager.php +++ b/panel/lib/DockerManager.php @@ -847,6 +847,745 @@ SH; ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], ], ], + // ── AI / LLM ────────────────────────────────────────────────────────── + 'ollama' => [ + 'name' => 'Ollama', + 'description' => 'Run large language models locally (llama3, mistral, gemma, etc.)', + 'icon' => 'LLM', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ], + ], + 'open-webui' => [ + 'name' => 'Open WebUI', + 'description' => 'ChatGPT-style UI for Ollama and OpenAI-compatible APIs', + 'icon' => 'OWU', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'ollama_url', 'label' => 'Ollama URL', 'type' => 'text', 'required' => false], + ], + ], + 'flowise' => [ + 'name' => 'Flowise', + 'description' => 'Drag & drop LLM workflow builder (LangChain/LlamaIndex UI)', + 'icon' => 'Flow', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'admin_user', 'label' => 'Admin Username','type' => 'text', 'required' => true], + ['key' => 'admin_pass', 'label' => 'Admin Password','type' => 'password', 'required' => true], + ], + ], + 'langfuse' => [ + 'name' => 'Langfuse', + 'description' => 'Open-source LLM observability: traces, evals, and prompt management', + 'icon' => 'LFuse', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'db_pass', 'label' => 'DB Password', 'type' => 'password', 'required' => true], + ], + ], + 'anythingllm' => [ + 'name' => 'AnythingLLM', + 'description' => 'All-in-one AI app: chat with docs, RAG, agents, multi-user', + 'icon' => 'ALLM', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ], + ], + 'localai' => [ + 'name' => 'LocalAI', + 'description' => 'OpenAI-compatible API using CPU/GPU models (gguf, diffusers)', + 'icon' => 'LAI', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ], + ], + 'comfyui' => [ + 'name' => 'ComfyUI', + 'description' => 'Node-based Stable Diffusion image generation UI', + 'icon' => 'CUI', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ], + ], + // ── Developer Tools ─────────────────────────────────────────────────── + 'code-server' => [ + 'name' => 'code-server', + 'description' => 'VS Code in the browser — full IDE accessible anywhere', + 'icon' => 'CS', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'admin_pass', 'label' => 'Password', 'type' => 'password', 'required' => true], + ], + ], + 'jenkins' => [ + 'name' => 'Jenkins', + 'description' => 'Automation server for CI/CD pipelines', + 'icon' => 'Jenk', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ], + ], + 'sonarqube' => [ + 'name' => 'SonarQube', + 'description' => 'Static code analysis & quality gate platform', + 'icon' => 'SQ', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'db_pass', 'label' => 'DB Password', 'type' => 'password', 'required' => true], + ], + ], + 'vault' => [ + 'name' => 'HashiCorp Vault', + 'description' => 'Secrets management, encryption as a service', + 'icon' => 'Vlt', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ], + ], + 'mailhog' => [ + 'name' => 'MailHog', + 'description' => 'Email testing tool — catches outbound mail in a web UI', + 'icon' => 'MHog', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ], + ], + 'dozzle' => [ + 'name' => 'Dozzle', + 'description' => 'Real-time Docker container log viewer in the browser', + 'icon' => 'Dozz', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ], + ], + 'yacht' => [ + 'name' => 'Yacht', + 'description' => 'Web UI for managing Docker containers and images', + 'icon' => 'Ycht', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'admin_email', 'label' => 'Admin Email', 'type' => 'email', 'required' => true], + ['key' => 'admin_pass', 'label' => 'Admin Password', 'type' => 'password', 'required' => true], + ], + ], + 'semaphore' => [ + 'name' => 'Semaphore UI', + 'description' => 'Modern web UI for Ansible, Terraform, and task runners', + 'icon' => 'Sem', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'admin_user', 'label' => 'Admin Username', 'type' => 'text', 'required' => true], + ['key' => 'admin_pass', 'label' => 'Admin Password', 'type' => 'password', 'required' => true], + ['key' => 'db_pass', 'label' => 'DB Password', 'type' => 'password', 'required' => true], + ], + ], + // ── Databases ───────────────────────────────────────────────────────── + 'redis-standalone' => [ + 'name' => 'Redis', + 'description' => 'In-memory key-value store + Redis Commander web UI', + 'icon' => 'Redis', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'admin_pass', 'label' => 'Redis Password', 'type' => 'password', 'required' => true], + ], + ], + 'mongodb' => [ + 'name' => 'MongoDB', + 'description' => 'NoSQL document database with Mongo Express web UI', + 'icon' => 'Mongo', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'admin_user', 'label' => 'Root Username', 'type' => 'text', 'required' => true], + ['key' => 'admin_pass', 'label' => 'Root Password', 'type' => 'password', 'required' => true], + ], + ], + 'postgresql' => [ + 'name' => 'PostgreSQL', + 'description' => 'Relational database with pgAdmin 4 web UI', + 'icon' => 'PG', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'db_pass', 'label' => 'DB Password', 'type' => 'password', 'required' => true], + ['key' => 'admin_email', 'label' => 'pgAdmin Email', 'type' => 'email', 'required' => true], + ['key' => 'admin_pass', 'label' => 'pgAdmin Password','type' => 'password','required' => true], + ], + ], + 'mariadb' => [ + 'name' => 'MariaDB', + 'description' => 'MariaDB relational database with phpMyAdmin web UI', + 'icon' => 'MDB', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'db_pass', 'label' => 'Root Password', 'type' => 'password', 'required' => true], + ], + ], + 'elasticsearch' => [ + 'name' => 'Elasticsearch', + 'description' => 'Distributed search & analytics engine with Kibana', + 'icon' => 'ES', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'admin_pass', 'label' => 'Elastic Password','type' => 'password','required' => true], + ], + ], + 'influxdb' => [ + 'name' => 'InfluxDB', + 'description' => 'Time-series database for metrics & IoT data', + 'icon' => 'IDB', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'admin_user', 'label' => 'Admin Username', 'type' => 'text', 'required' => true], + ['key' => 'admin_pass', 'label' => 'Admin Password', 'type' => 'password', 'required' => true], + ['key' => 'admin_email', 'label' => 'Admin Email', 'type' => 'email', 'required' => true], + ], + ], + 'neo4j' => [ + 'name' => 'Neo4j', + 'description' => 'Graph database with browser-based query UI', + 'icon' => 'Neo', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'admin_pass', 'label' => 'Neo4j Password', 'type' => 'password', 'required' => true], + ], + ], + 'qdrant' => [ + 'name' => 'Qdrant', + 'description' => 'Vector database for AI/ML similarity search', + 'icon' => 'Qdrt', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ], + ], + // ── Monitoring & Observability ──────────────────────────────────────── + 'loki' => [ + 'name' => 'Loki + Grafana', + 'description' => 'Log aggregation stack: Promtail → Loki → Grafana', + 'icon' => 'Loki', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'admin_pass', 'label' => 'Grafana Password','type' => 'password', 'required' => true], + ], + ], + 'jaeger' => [ + 'name' => 'Jaeger', + 'description' => 'Distributed tracing platform (OpenTelemetry-compatible)', + 'icon' => 'Jaeg', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ], + ], + 'victoria-metrics' => [ + 'name' => 'VictoriaMetrics', + 'description' => 'High-performance drop-in replacement for Prometheus storage', + 'icon' => 'VM', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ], + ], + 'changedetection' => [ + 'name' => 'changedetection.io', + 'description' => 'Monitor any website for content changes, with alerts', + 'icon' => 'Chng', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'admin_pass', 'label' => 'Password', 'type' => 'password', 'required' => false], + ], + ], + 'metabase' => [ + 'name' => 'Metabase', + 'description' => 'Business intelligence & analytics with drag-drop dashboards', + 'icon' => 'Meta', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'db_pass', 'label' => 'DB Password', 'type' => 'password', 'required' => true], + ['key' => 'admin_email', 'label' => 'Admin Email', 'type' => 'email', 'required' => true], + ['key' => 'admin_pass', 'label' => 'Admin Pass', 'type' => 'password', 'required' => true], + ], + ], + // ── Networking & Security ───────────────────────────────────────────── + 'pihole' => [ + 'name' => 'Pi-hole', + 'description' => 'Network-level ad & tracker blocker with DNS sinkhole', + 'icon' => 'PiH', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'admin_pass', 'label' => 'Admin Password', 'type' => 'password', 'required' => true], + ], + ], + 'adguard-home' => [ + 'name' => 'AdGuard Home', + 'description' => 'Network-wide DNS ad blocker with a polished web UI', + 'icon' => 'AGH', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ], + ], + 'nginx-proxy-manager' => [ + 'name' => 'Nginx Proxy Manager', + 'description' => 'Reverse proxy + SSL management with a friendly UI', + 'icon' => 'NPM', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'db_pass', 'label' => 'DB Password', 'type' => 'password', 'required' => true], + ['key' => 'admin_email', 'label' => 'Admin Email', 'type' => 'email', 'required' => true], + ['key' => 'admin_pass', 'label' => 'Admin Password', 'type' => 'password', 'required' => true], + ], + ], + 'traefik' => [ + 'name' => 'Traefik', + 'description' => 'Cloud-native reverse proxy and load balancer with dashboard', + 'icon' => 'Trf', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ], + ], + 'wg-easy' => [ + 'name' => 'WireGuard Easy', + 'description' => 'WireGuard VPN server with a simple web management UI', + 'icon' => 'WGE', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'admin_pass', 'label' => 'UI Password', 'type' => 'password', 'required' => true], + ], + ], + 'cloudflared' => [ + 'name' => 'Cloudflare Tunnel', + 'description' => 'Expose local services via Cloudflare Tunnel (cloudflared)', + 'icon' => 'CFT', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'token', 'label' => 'Tunnel Token', 'type' => 'text', 'required' => true], + ], + ], + 'crowdsec' => [ + 'name' => 'CrowdSec', + 'description' => 'Collaborative intrusion detection & threat intelligence', + 'icon' => 'CS', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ], + ], + 'authelia' => [ + 'name' => 'Authelia', + 'description' => 'SSO authentication & 2FA gateway for reverse-proxied apps', + 'icon' => 'Auth', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'admin_pass', 'label' => 'JWT Secret', 'type' => 'password', 'required' => true], + ], + ], + // ── CMS & E-commerce ────────────────────────────────────────────────── + 'drupal' => [ + 'name' => 'Drupal', + 'description' => 'Enterprise content management system (Postgres backend)', + 'icon' => 'Drpl', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'db_pass', 'label' => 'DB Password', 'type' => 'password', 'required' => true], + ], + ], + 'joomla' => [ + 'name' => 'Joomla', + 'description' => 'Popular open-source CMS with thousands of extensions', + 'icon' => 'Joom', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'db_pass', 'label' => 'DB Password', 'type' => 'password', 'required' => true], + ], + ], + 'grav' => [ + 'name' => 'Grav CMS', + 'description' => 'Flat-file CMS — no database required, blazing fast', + 'icon' => 'Grav', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ], + ], + 'prestashop' => [ + 'name' => 'PrestaShop', + 'description' => 'Full-featured open-source e-commerce platform', + 'icon' => 'PS', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'db_pass', 'label' => 'DB Password', 'type' => 'password', 'required' => true], + ['key' => 'admin_email', 'label' => 'Admin Email', 'type' => 'email', 'required' => true], + ['key' => 'admin_pass', 'label' => 'Admin Password', 'type' => 'password', 'required' => true], + ], + ], + 'opencart' => [ + 'name' => 'OpenCart', + 'description' => 'Lightweight open-source e-commerce shopping cart', + 'icon' => 'OC', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'db_pass', 'label' => 'DB Password', 'type' => 'password', 'required' => true], + ], + ], + 'medusa' => [ + 'name' => 'Medusa', + 'description' => 'Open-source headless commerce platform (Node.js + Postgres)', + 'icon' => 'Med', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'db_pass', 'label' => 'DB Password', 'type' => 'password', 'required' => true], + ], + ], + 'answer' => [ + 'name' => 'Answer', + 'description' => 'Q&A community platform — like Stack Overflow for your team', + 'icon' => 'Ans', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'db_pass', 'label' => 'DB Password', 'type' => 'password', 'required' => true], + ], + ], + // ── Project Management ──────────────────────────────────────────────── + 'plane' => [ + 'name' => 'Plane', + 'description' => 'Open-source project management (Jira alternative)', + 'icon' => 'Pln', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'db_pass', 'label' => 'DB Password', 'type' => 'password', 'required' => true], + ], + ], + 'openproject' => [ + 'name' => 'OpenProject', + 'description' => 'Full-featured project management with Gantt charts & Agile boards', + 'icon' => 'OP', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'db_pass', 'label' => 'DB Password', 'type' => 'password', 'required' => true], + ['key' => 'admin_pass', 'label' => 'Admin Password', 'type' => 'password', 'required' => true], + ], + ], + 'leantime' => [ + 'name' => 'Leantime', + 'description' => 'Strategic project management for non-project managers', + 'icon' => 'LT', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'db_pass', 'label' => 'DB Password', 'type' => 'password', 'required' => true], + ['key' => 'admin_email', 'label' => 'Admin Email', 'type' => 'email', 'required' => true], + ['key' => 'admin_pass', 'label' => 'Admin Password', 'type' => 'password', 'required' => true], + ], + ], + 'wekan' => [ + 'name' => 'WeKan', + 'description' => 'Open-source kanban board (Trello alternative)', + 'icon' => 'Wkn', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'admin_user', 'label' => 'Admin Username', 'type' => 'text', 'required' => true], + ['key' => 'admin_pass', 'label' => 'Admin Password', 'type' => 'password', 'required' => true], + ['key' => 'admin_email', 'label' => 'Admin Email', 'type' => 'email', 'required' => true], + ], + ], + 'focalboard' => [ + 'name' => 'Focalboard', + 'description' => 'Mattermost project management — kanban, table, gallery views', + 'icon' => 'FB', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'db_pass', 'label' => 'DB Password', 'type' => 'password', 'required' => true], + ], + ], + // ── Communication ───────────────────────────────────────────────────── + 'matrix-synapse' => [ + 'name' => 'Matrix Synapse', + 'description' => 'Federated, end-to-end encrypted chat server (Element-compatible)', + 'icon' => 'Mtrx', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'db_pass', 'label' => 'DB Password', 'type' => 'password', 'required' => true], + ['key' => 'admin_pass', 'label' => 'Shared Secret', 'type' => 'password', 'required' => true], + ], + ], + 'gotify' => [ + 'name' => 'Gotify', + 'description' => 'Self-hosted push notifications server for apps & scripts', + 'icon' => 'Gtfy', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'admin_pass', 'label' => 'Admin Password', 'type' => 'password', 'required' => true], + ], + ], + 'ntfy' => [ + 'name' => 'ntfy', + 'description' => 'Simple HTTP-based push notification service', + 'icon' => 'ntfy', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ], + ], + 'grist' => [ + 'name' => 'Grist', + 'description' => 'Modern spreadsheet + database hybrid — Airtable alternative', + 'icon' => 'Grst', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'admin_email', 'label' => 'Admin Email', 'type' => 'email', 'required' => true], + ['key' => 'admin_pass', 'label' => 'Admin Password', 'type' => 'password', 'required' => true], + ], + ], + 'mailpit' => [ + 'name' => 'Mailpit', + 'description' => 'Email testing tool with modern UI — MailHog successor', + 'icon' => 'MPit', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ], + ], + // ── File Storage & Backup ───────────────────────────────────────────── + 'syncthing' => [ + 'name' => 'Syncthing', + 'description' => 'Continuous peer-to-peer file synchronization tool', + 'icon' => 'Sync', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ], + ], + 'sftpgo' => [ + 'name' => 'SFTPGo', + 'description' => 'Full-featured SFTP/FTP/WebDAV server with web admin UI', + 'icon' => 'SFTP', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'admin_user', 'label' => 'Admin Username', 'type' => 'text', 'required' => true], + ['key' => 'admin_pass', 'label' => 'Admin Password', 'type' => 'password', 'required' => true], + ], + ], + 'owncloud' => [ + 'name' => 'ownCloud', + 'description' => 'Enterprise file sync and share platform (Nextcloud predecessor)', + 'icon' => 'OC', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'db_pass', 'label' => 'DB Password', 'type' => 'password', 'required' => true], + ['key' => 'admin_pass', 'label' => 'Admin Password', 'type' => 'password', 'required' => true], + ], + ], + 'duplicati' => [ + 'name' => 'Duplicati', + 'description' => 'Encrypted backups to cloud/local storage with web UI', + 'icon' => 'Dup', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ], + ], + 'calibre-web' => [ + 'name' => 'Calibre-Web', + 'description' => 'Web interface for your Calibre ebook library', + 'icon' => 'Cal', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ], + ], + // ── ERP & Business ──────────────────────────────────────────────────── + 'odoo' => [ + 'name' => 'Odoo', + 'description' => 'Full-suite open-source ERP: CRM, accounting, inventory, HR', + 'icon' => 'Odoo', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'db_pass', 'label' => 'DB Password', 'type' => 'password', 'required' => true], + ['key' => 'admin_pass', 'label' => 'Master Password','type' => 'password', 'required' => true], + ], + ], + 'akaunting' => [ + 'name' => 'Akaunting', + 'description' => 'Free accounting software for small businesses', + 'icon' => 'Aknt', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'db_pass', 'label' => 'DB Password', 'type' => 'password', 'required' => true], + ['key' => 'admin_email', 'label' => 'Admin Email', 'type' => 'email', 'required' => true], + ['key' => 'admin_pass', 'label' => 'Admin Password', 'type' => 'password', 'required' => true], + ], + ], + 'monica' => [ + 'name' => 'Monica', + 'description' => 'Personal CRM — track relationships, interactions, birthdays', + 'icon' => 'Mon', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'db_pass', 'label' => 'DB Password', 'type' => 'password', 'required' => true], + ['key' => 'admin_email', 'label' => 'Admin Email', 'type' => 'email', 'required' => true], + ['key' => 'admin_pass', 'label' => 'Admin Password', 'type' => 'password', 'required' => true], + ], + ], + 'twenty' => [ + 'name' => 'Twenty CRM', + 'description' => 'Modern open-source CRM (Salesforce alternative)', + 'icon' => 'Twty', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'db_pass', 'label' => 'DB Password', 'type' => 'password', 'required' => true], + ], + ], + 'dolibarr' => [ + 'name' => 'Dolibarr', + 'description' => 'ERP & CRM for SMBs: invoicing, stock, HR, and more', + 'icon' => 'Doli', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'db_pass', 'label' => 'DB Password', 'type' => 'password', 'required' => true], + ['key' => 'admin_pass', 'label' => 'Admin Password', 'type' => 'password', 'required' => true], + ], + ], + // ── Media ───────────────────────────────────────────────────────────── + 'audiobookshelf' => [ + 'name' => 'Audiobookshelf', + 'description' => 'Self-hosted audiobook & podcast server with mobile apps', + 'icon' => 'ABS', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ], + ], + 'komga' => [ + 'name' => 'Komga', + 'description' => 'Comic/manga library server with OPDS & web reader', + 'icon' => 'Kmga', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'admin_email', 'label' => 'Admin Email', 'type' => 'email', 'required' => true], + ['key' => 'admin_pass', 'label' => 'Admin Password', 'type' => 'password', 'required' => true], + ], + ], + 'grocy' => [ + 'name' => 'Grocy', + 'description' => 'Self-hosted grocery management, stock tracking & meal planning', + 'icon' => 'Grcy', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ], + ], + 'tube-archivist' => [ + 'name' => 'TubeArchivist', + 'description' => 'Self-hosted YouTube archive with search and web player', + 'icon' => 'TA', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'admin_user', 'label' => 'Admin Username', 'type' => 'text', 'required' => true], + ['key' => 'admin_pass', 'label' => 'Admin Password', 'type' => 'password', 'required' => true], + ], + ], + 'affine' => [ + 'name' => 'AFFiNE', + 'description' => 'Next-gen knowledge base — docs, whiteboard, and database in one', + 'icon' => 'Afn', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'db_pass', 'label' => 'DB Password', 'type' => 'password', 'required' => true], + ], + ], + // ── Smart Home ──────────────────────────────────────────────────────── + 'home-assistant' => [ + 'name' => 'Home Assistant', + 'description' => 'Open-source home automation platform', + 'icon' => 'HA', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ], + ], + 'node-red' => [ + 'name' => 'Node-RED', + 'description' => 'Flow-based programming tool for IoT and automation', + 'icon' => 'NR', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ], + ], + 'mosquitto' => [ + 'name' => 'Mosquitto MQTT', + 'description' => 'Lightweight MQTT message broker for IoT devices', + 'icon' => 'MQTT', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'admin_user', 'label' => 'MQTT Username', 'type' => 'text', 'required' => true], + ['key' => 'admin_pass', 'label' => 'MQTT Password', 'type' => 'password', 'required' => true], + ], + ], + 'zigbee2mqtt' => [ + 'name' => 'Zigbee2MQTT', + 'description' => 'Bridge Zigbee devices to MQTT without proprietary hubs', + 'icon' => 'Z2M', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'mqtt_host', 'label' => 'MQTT Host/IP', 'type' => 'text', 'required' => true], + ], + ], + // ── Dashboards & Admin ──────────────────────────────────────────────── + 'dashy' => [ + 'name' => 'Dashy', + 'description' => 'Highly customizable self-hosted start page and dashboard', + 'icon' => 'Dash', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ], + ], + 'homarr' => [ + 'name' => 'Homarr', + 'description' => 'Sleek home server dashboard with app integrations', + 'icon' => 'Hom', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ], + ], + 'homer' => [ + 'name' => 'Homer', + 'description' => 'Static YAML-configured home page for your services', + 'icon' => 'Hmr', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ], + ], + 'it-tools' => [ + 'name' => 'IT Tools', + 'description' => 'Collection of handy online tools for developers (offline)', + 'icon' => 'IT', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ], + ], + 'cloudbeaver' => [ + 'name' => 'CloudBeaver', + 'description' => 'Web-based universal database management UI (DBeaver cloud)', + 'icon' => 'CBvr', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ], + ], + 'pgadmin' => [ + 'name' => 'pgAdmin 4', + 'description' => 'Feature-rich web UI for managing PostgreSQL databases', + 'icon' => 'PGA', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'admin_email', 'label' => 'Admin Email', 'type' => 'email', 'required' => true], + ['key' => 'admin_pass', 'label' => 'Admin Password', 'type' => 'password', 'required' => true], + ], + ], + 'phpmyadmin' => [ + 'name' => 'phpMyAdmin', + 'description' => 'Classic web-based MySQL/MariaDB database admin interface', + 'icon' => 'PMA', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'db_host', 'label' => 'DB Host', 'type' => 'text', 'required' => true], + ['key' => 'db_pass', 'label' => 'Root Password','type' => 'password', 'required' => true], + ], + ], + 'infisical' => [ + 'name' => 'Infisical', + 'description' => 'Open-source secrets management platform (Vault alternative)', + 'icon' => 'Inf', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'db_pass', 'label' => 'DB Password', 'type' => 'password', 'required' => true], + ], + ], ]; } @@ -1063,6 +1802,298 @@ SH; 'stirlingpdf' => "version: '3.8'\nservices:\n stirlingpdf:\n image: frooodle/s-pdf:latest\n restart: unless-stopped\n environment:\n DOCKER_ENABLE_SECURITY: 'false'\n volumes:\n - stirlingpdf_tessdata:/usr/share/tessdata\n - stirlingpdf_config:/configs\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n stirlingpdf_tessdata:\n stirlingpdf_config:\n", + // ── AI / LLM ────────────────────────────────────────────────────────── + + 'ollama' => "version: '3.8'\nservices:\n ollama:\n image: ollama/ollama:latest\n restart: unless-stopped\n volumes:\n - ollama_data:/root/.ollama\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n ollama_data:\n", + + 'open-webui' => (function() use ($p, $domain): string { + $ollamaUrl = preg_replace('/[^a-zA-Z0-9.:\/\-_]/', '', $p['ollama_url'] ?? 'http://ollama:11434'); + return "version: '3.8'\nservices:\n open-webui:\n image: ghcr.io/open-webui/open-webui:main\n restart: unless-stopped\n environment:\n OLLAMA_BASE_URL: {$ollamaUrl}\n volumes:\n - open_webui_data:/app/backend/data\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n open_webui_data:\n"; + })(), + + 'flowise' => (function() use ($domain, $adminUser, $adminPass): string { + return "version: '3.8'\nservices:\n flowise:\n image: flowiseai/flowise:latest\n restart: unless-stopped\n environment:\n FLOWISE_USERNAME: {$adminUser}\n FLOWISE_PASSWORD: {$adminPass}\n volumes:\n - flowise_data:/root/.flowise\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n flowise_data:\n"; + })(), + + 'langfuse' => (function() use ($domain, $dbPass): string { + $secret = bin2hex(random_bytes(16)); + $salt = bin2hex(random_bytes(8)); + return "version: '3.8'\nservices:\n db:\n image: postgres:16-alpine\n restart: unless-stopped\n environment:\n POSTGRES_DB: langfuse\n POSTGRES_USER: langfuse\n POSTGRES_PASSWORD: {$dbPass}\n volumes:\n - db_data:/var/lib/postgresql/data\n langfuse:\n image: langfuse/langfuse:latest\n restart: unless-stopped\n depends_on: [db]\n environment:\n DATABASE_URL: postgresql://langfuse:{$dbPass}@db:5432/langfuse\n NEXTAUTH_URL: https://{$domain}\n NEXTAUTH_SECRET: {$secret}\n SALT: {$salt}\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n db_data:\n"; + })(), + + 'anythingllm' => "version: '3.8'\nservices:\n anythingllm:\n image: mintplexlabs/anythingllm:latest\n restart: unless-stopped\n cap_add: [SYS_ADMIN]\n volumes:\n - anythingllm_data:/app/server/storage\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n anythingllm_data:\n", + + 'localai' => "version: '3.8'\nservices:\n localai:\n image: localai/localai:latest-cpu\n restart: unless-stopped\n volumes:\n - localai_models:/build/models\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n localai_models:\n", + + 'comfyui' => "version: '3.8'\nservices:\n comfyui:\n image: yanwk/comfyui-boot:latest\n restart: unless-stopped\n volumes:\n - comfyui_data:/root\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n comfyui_data:\n", + + // ── Developer Tools ─────────────────────────────────────────────────── + + 'code-server' => "version: '3.8'\nservices:\n code-server:\n image: codercom/code-server:latest\n restart: unless-stopped\n environment:\n PASSWORD: {$adminPass}\n volumes:\n - code_server_data:/home/coder\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n code_server_data:\n", + + 'jenkins' => "version: '3.8'\nservices:\n jenkins:\n image: jenkins/jenkins:lts-jdk21\n restart: unless-stopped\n user: root\n volumes:\n - jenkins_home:/var/jenkins_home\n - /var/run/docker.sock:/var/run/docker.sock\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n jenkins_home:\n", + + 'sonarqube' => (function() use ($domain, $dbPass): string { + return "version: '3.8'\nservices:\n db:\n image: postgres:16-alpine\n restart: unless-stopped\n environment:\n POSTGRES_DB: sonarqube\n POSTGRES_USER: sonarqube\n POSTGRES_PASSWORD: {$dbPass}\n volumes:\n - db_data:/var/lib/postgresql/data\n sonarqube:\n image: sonarqube:community\n restart: unless-stopped\n depends_on: [db]\n environment:\n SONAR_JDBC_URL: jdbc:postgresql://db:5432/sonarqube\n SONAR_JDBC_USERNAME: sonarqube\n SONAR_JDBC_PASSWORD: {$dbPass}\n volumes:\n - sonarqube_data:/opt/sonarqube/data\n - sonarqube_extensions:/opt/sonarqube/extensions\n - sonarqube_logs:/opt/sonarqube/logs\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n db_data:\n sonarqube_data:\n sonarqube_extensions:\n sonarqube_logs:\n"; + })(), + + 'vault' => "version: '3.8'\nservices:\n vault:\n image: hashicorp/vault:latest\n restart: unless-stopped\n cap_add: [IPC_LOCK]\n environment:\n VAULT_DEV_ROOT_TOKEN_ID: root\n VAULT_DEV_LISTEN_ADDRESS: 0.0.0.0:8200\n volumes:\n - vault_data:/vault/data\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n vault_data:\n", + + 'mailhog' => "version: '3.8'\nservices:\n mailhog:\n image: mailhog/mailhog:latest\n restart: unless-stopped\n labels:\n - 'novacpx.domain={$domain}'\n", + + 'dozzle' => "version: '3.8'\nservices:\n dozzle:\n image: amir20/dozzle:latest\n restart: unless-stopped\n volumes:\n - /var/run/docker.sock:/var/run/docker.sock:ro\n labels:\n - 'novacpx.domain={$domain}'\n", + + 'yacht' => (function() use ($p, $domain, $adminPass): string { + $email = preg_replace('/[^a-zA-Z0-9@._\-]/', '', $p['admin_email'] ?? 'admin@' . $domain); + return "version: '3.8'\nservices:\n yacht:\n image: selfhostedpro/yacht:latest\n restart: unless-stopped\n environment:\n ADMIN_EMAIL: {$email}\n ADMIN_PASSWORD: {$adminPass}\n volumes:\n - yacht_data:/config\n - /var/run/docker.sock:/var/run/docker.sock\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n yacht_data:\n"; + })(), + + 'semaphore' => (function() use ($domain, $adminUser, $adminPass, $dbPass): string { + $key = bin2hex(random_bytes(16)); + return "version: '3.8'\nservices:\n db:\n image: postgres:16-alpine\n restart: unless-stopped\n environment:\n POSTGRES_DB: semaphore\n POSTGRES_USER: semaphore\n POSTGRES_PASSWORD: {$dbPass}\n volumes:\n - db_data:/var/lib/postgresql/data\n semaphore:\n image: semaphoreui/semaphore:latest\n restart: unless-stopped\n depends_on: [db]\n environment:\n SEMAPHORE_DB_DIALECT: postgres\n SEMAPHORE_DB_HOST: db\n SEMAPHORE_DB_PORT: 5432\n SEMAPHORE_DB_NAME: semaphore\n SEMAPHORE_DB_USER: semaphore\n SEMAPHORE_DB_PASS: {$dbPass}\n SEMAPHORE_ADMIN: {$adminUser}\n SEMAPHORE_ADMIN_PASSWORD: {$adminPass}\n SEMAPHORE_ADMIN_EMAIL: admin@{$domain}\n SEMAPHORE_ACCESS_KEY_ENCRYPTION: {$key}\n volumes:\n - semaphore_data:/home/semaphore\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n db_data:\n semaphore_data:\n"; + })(), + + // ── Databases ───────────────────────────────────────────────────────── + + 'redis-standalone' => "version: '3.8'\nservices:\n redis:\n image: redis:7-alpine\n restart: unless-stopped\n command: redis-server --requirepass {$adminPass}\n volumes:\n - redis_data:/data\n redis-commander:\n image: rediscommander/redis-commander:latest\n restart: unless-stopped\n environment:\n REDIS_HOSTS: 'local:redis:6379:0:{$adminPass}'\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n redis_data:\n", + + 'mongodb' => "version: '3.8'\nservices:\n mongo:\n image: mongo:7\n restart: unless-stopped\n environment:\n MONGO_INITDB_ROOT_USERNAME: {$adminUser}\n MONGO_INITDB_ROOT_PASSWORD: {$adminPass}\n volumes:\n - mongo_data:/data/db\n mongo-express:\n image: mongo-express:latest\n restart: unless-stopped\n depends_on: [mongo]\n environment:\n ME_CONFIG_MONGODB_ADMINUSERNAME: {$adminUser}\n ME_CONFIG_MONGODB_ADMINPASSWORD: {$adminPass}\n ME_CONFIG_MONGODB_URL: mongodb://{$adminUser}:{$adminPass}@mongo:27017/\n ME_CONFIG_BASICAUTH: 'false'\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n mongo_data:\n", + + 'postgresql' => (function() use ($p, $domain, $dbPass, $adminPass): string { + $email = preg_replace('/[^a-zA-Z0-9@._\-]/', '', $p['admin_email'] ?? 'admin@' . $domain); + return "version: '3.8'\nservices:\n db:\n image: postgres:16-alpine\n restart: unless-stopped\n environment:\n POSTGRES_USER: postgres\n POSTGRES_PASSWORD: {$dbPass}\n volumes:\n - db_data:/var/lib/postgresql/data\n pgadmin:\n image: dpage/pgadmin4:latest\n restart: unless-stopped\n depends_on: [db]\n environment:\n PGADMIN_DEFAULT_EMAIL: {$email}\n PGADMIN_DEFAULT_PASSWORD: {$adminPass}\n volumes:\n - pgadmin_data:/var/lib/pgadmin\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n db_data:\n pgadmin_data:\n"; + })(), + + 'mariadb' => "version: '3.8'\nservices:\n db:\n image: mariadb:11\n restart: unless-stopped\n environment:\n MARIADB_ROOT_PASSWORD: {$dbPass}\n volumes:\n - db_data:/var/lib/mysql\n phpmyadmin:\n image: phpmyadmin:latest\n restart: unless-stopped\n depends_on: [db]\n environment:\n PMA_HOST: db\n MYSQL_ROOT_PASSWORD: {$dbPass}\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n db_data:\n", + + 'elasticsearch' => "version: '3.8'\nservices:\n elasticsearch:\n image: elasticsearch:8.13.0\n restart: unless-stopped\n environment:\n discovery.type: single-node\n ELASTIC_PASSWORD: {$adminPass}\n xpack.security.enabled: 'true'\n volumes:\n - es_data:/usr/share/elasticsearch/data\n kibana:\n image: kibana:8.13.0\n restart: unless-stopped\n depends_on: [elasticsearch]\n environment:\n ELASTICSEARCH_HOSTS: http://elasticsearch:9200\n ELASTICSEARCH_USERNAME: elastic\n ELASTICSEARCH_PASSWORD: {$adminPass}\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n es_data:\n", + + 'influxdb' => (function() use ($p, $domain, $adminUser, $adminPass): string { + $email = preg_replace('/[^a-zA-Z0-9@._\-]/', '', $p['admin_email'] ?? 'admin@' . $domain); + $token = bin2hex(random_bytes(32)); + return "version: '3.8'\nservices:\n influxdb:\n image: influxdb:2.7\n restart: unless-stopped\n environment:\n DOCKER_INFLUXDB_INIT_MODE: setup\n DOCKER_INFLUXDB_INIT_USERNAME: {$adminUser}\n DOCKER_INFLUXDB_INIT_PASSWORD: {$adminPass}\n DOCKER_INFLUXDB_INIT_ORG: novacpx\n DOCKER_INFLUXDB_INIT_BUCKET: default\n DOCKER_INFLUXDB_INIT_ADMIN_TOKEN: {$token}\n volumes:\n - influxdb_data:/var/lib/influxdb2\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n influxdb_data:\n"; + })(), + + 'neo4j' => "version: '3.8'\nservices:\n neo4j:\n image: neo4j:5\n restart: unless-stopped\n environment:\n NEO4J_AUTH: neo4j/{$adminPass}\n volumes:\n - neo4j_data:/data\n - neo4j_logs:/logs\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n neo4j_data:\n neo4j_logs:\n", + + 'qdrant' => "version: '3.8'\nservices:\n qdrant:\n image: qdrant/qdrant:latest\n restart: unless-stopped\n volumes:\n - qdrant_data:/qdrant/storage\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n qdrant_data:\n", + + // ── Monitoring & Observability ──────────────────────────────────────── + + 'loki' => "version: '3.8'\nservices:\n loki:\n image: grafana/loki:latest\n restart: unless-stopped\n command: -config.file=/etc/loki/local-config.yaml\n volumes:\n - loki_data:/loki\n promtail:\n image: grafana/promtail:latest\n restart: unless-stopped\n volumes:\n - /var/log:/var/log:ro\n grafana:\n image: grafana/grafana:latest\n restart: unless-stopped\n environment:\n GF_SECURITY_ADMIN_PASSWORD: {$adminPass}\n volumes:\n - grafana_data:/var/lib/grafana\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n loki_data:\n grafana_data:\n", + + 'jaeger' => "version: '3.8'\nservices:\n jaeger:\n image: jaegertracing/all-in-one:latest\n restart: unless-stopped\n environment:\n COLLECTOR_ZIPKIN_HOST_PORT: ':9411'\n labels:\n - 'novacpx.domain={$domain}'\n", + + 'victoria-metrics' => "version: '3.8'\nservices:\n victoria-metrics:\n image: victoriametrics/victoria-metrics:latest\n restart: unless-stopped\n command: -storageDataPath=/victoria-metrics-data -retentionPeriod=6\n volumes:\n - vm_data:/victoria-metrics-data\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n vm_data:\n", + + 'changedetection' => "version: '3.8'\nservices:\n changedetection:\n image: ghcr.io/dgtlmoon/changedetection.io:latest\n restart: unless-stopped\n environment:\n BASE_URL: https://{$domain}\n PASSWORD: {$adminPass}\n volumes:\n - changedetection_data:/datastore\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n changedetection_data:\n", + + 'metabase' => (function() use ($p, $domain, $dbPass, $adminPass): string { + $email = preg_replace('/[^a-zA-Z0-9@._\-]/', '', $p['admin_email'] ?? 'admin@' . $domain); + return "version: '3.8'\nservices:\n db:\n image: postgres:16-alpine\n restart: unless-stopped\n environment:\n POSTGRES_DB: metabase\n POSTGRES_USER: metabase\n POSTGRES_PASSWORD: {$dbPass}\n volumes:\n - db_data:/var/lib/postgresql/data\n metabase:\n image: metabase/metabase:latest\n restart: unless-stopped\n depends_on: [db]\n environment:\n MB_DB_TYPE: postgres\n MB_DB_DBNAME: metabase\n MB_DB_PORT: 5432\n MB_DB_USER: metabase\n MB_DB_PASS: {$dbPass}\n MB_DB_HOST: db\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n db_data:\n"; + })(), + + // ── Networking & Security ───────────────────────────────────────────── + + 'pihole' => "version: '3.8'\nservices:\n pihole:\n image: pihole/pihole:latest\n restart: unless-stopped\n environment:\n WEBPASSWORD: {$adminPass}\n TZ: UTC\n volumes:\n - pihole_etc:/etc/pihole\n - pihole_dnsmasq:/etc/dnsmasq.d\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n pihole_etc:\n pihole_dnsmasq:\n", + + 'adguard-home' => "version: '3.8'\nservices:\n adguard-home:\n image: adguard/adguardhome:latest\n restart: unless-stopped\n volumes:\n - adguard_work:/opt/adguardhome/work\n - adguard_conf:/opt/adguardhome/conf\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n adguard_work:\n adguard_conf:\n", + + 'nginx-proxy-manager' => (function() use ($p, $domain, $dbPass, $adminPass): string { + $email = preg_replace('/[^a-zA-Z0-9@._\-]/', '', $p['admin_email'] ?? 'admin@' . $domain); + return "version: '3.8'\nservices:\n db:\n image: mariadb:11\n restart: unless-stopped\n environment:\n MYSQL_ROOT_PASSWORD: {$dbPass}\n MYSQL_DATABASE: npm\n MYSQL_USER: npm\n MYSQL_PASSWORD: {$dbPass}\n volumes:\n - db_data:/var/lib/mysql\n npm:\n image: jc21/nginx-proxy-manager:latest\n restart: unless-stopped\n depends_on: [db]\n environment:\n DB_MYSQL_HOST: db\n DB_MYSQL_PORT: 3306\n DB_MYSQL_USER: npm\n DB_MYSQL_PASSWORD: {$dbPass}\n DB_MYSQL_NAME: npm\n volumes:\n - npm_data:/data\n - npm_letsencrypt:/etc/letsencrypt\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n db_data:\n npm_data:\n npm_letsencrypt:\n"; + })(), + + 'traefik' => "version: '3.8'\nservices:\n traefik:\n image: traefik:v3\n restart: unless-stopped\n command:\n - '--api.insecure=true'\n - '--providers.docker=true'\n - '--providers.docker.exposedbydefault=false'\n - '--entrypoints.web.address=:80'\n volumes:\n - /var/run/docker.sock:/var/run/docker.sock:ro\n - traefik_data:/etc/traefik\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n traefik_data:\n", + + 'wg-easy' => "version: '3.8'\nservices:\n wg-easy:\n image: ghcr.io/wg-easy/wg-easy:latest\n restart: unless-stopped\n cap_add: [NET_ADMIN, SYS_MODULE]\n sysctls:\n - net.ipv4.conf.all.src_valid_mark=1\n - net.ipv4.ip_forward=1\n environment:\n WG_HOST: {$domain}\n PASSWORD_HASH: {$adminPass}\n volumes:\n - wg_data:/etc/wireguard\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n wg_data:\n", + + 'cloudflared' => (function() use ($p, $domain): string { + $token = preg_replace('/[^a-zA-Z0-9._\-]/', '', $p['token'] ?? ''); + return "version: '3.8'\nservices:\n cloudflared:\n image: cloudflare/cloudflared:latest\n restart: unless-stopped\n command: tunnel --no-autoupdate run --token {$token}\n labels:\n - 'novacpx.domain={$domain}'\n"; + })(), + + 'crowdsec' => "version: '3.8'\nservices:\n crowdsec:\n image: crowdsecurity/crowdsec:latest\n restart: unless-stopped\n environment:\n GID: '1000'\n volumes:\n - crowdsec_data:/var/lib/crowdsec/data\n - crowdsec_config:/etc/crowdsec\n - /var/log:/var/log:ro\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n crowdsec_data:\n crowdsec_config:\n", + + 'authelia' => (function() use ($domain, $adminPass): string { + $session = bin2hex(random_bytes(16)); + $storage = bin2hex(random_bytes(16)); + return "version: '3.8'\nservices:\n authelia:\n image: authelia/authelia:latest\n restart: unless-stopped\n environment:\n AUTHELIA_JWT_SECRET: {$adminPass}\n AUTHELIA_SESSION_SECRET: {$session}\n AUTHELIA_STORAGE_ENCRYPTION_KEY: {$storage}\n AUTHELIA_STORAGE_LOCAL_PATH: /config/db.sqlite3\n volumes:\n - authelia_config:/config\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n authelia_config:\n"; + })(), + + // ── CMS & E-commerce ────────────────────────────────────────────────── + + 'drupal' => (function() use ($domain, $dbPass): string { + return "version: '3.8'\nservices:\n db:\n image: postgres:16-alpine\n restart: unless-stopped\n environment:\n POSTGRES_DB: drupal\n POSTGRES_USER: drupal\n POSTGRES_PASSWORD: {$dbPass}\n volumes:\n - db_data:/var/lib/postgresql/data\n drupal:\n image: drupal:10-apache\n restart: unless-stopped\n depends_on: [db]\n volumes:\n - drupal_modules:/var/www/html/modules\n - drupal_profiles:/var/www/html/profiles\n - drupal_themes:/var/www/html/themes\n - drupal_sites:/var/www/html/sites\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n db_data:\n drupal_modules:\n drupal_profiles:\n drupal_themes:\n drupal_sites:\n"; + })(), + + 'joomla' => (function() use ($domain, $dbPass): string { + return "version: '3.8'\nservices:\n db:\n image: mariadb:10.11\n restart: unless-stopped\n environment:\n MYSQL_ROOT_PASSWORD: {$dbPass}\n MYSQL_DATABASE: joomla\n MYSQL_USER: joomla\n MYSQL_PASSWORD: {$dbPass}\n volumes:\n - db_data:/var/lib/mysql\n joomla:\n image: joomla:php8.2-apache\n restart: unless-stopped\n depends_on: [db]\n environment:\n JOOMLA_DB_HOST: db\n JOOMLA_DB_USER: joomla\n JOOMLA_DB_PASSWORD: {$dbPass}\n JOOMLA_DB_NAME: joomla\n volumes:\n - joomla_data:/var/www/html\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n db_data:\n joomla_data:\n"; + })(), + + 'grav' => "version: '3.8'\nservices:\n grav:\n image: linuxserver/grav:latest\n restart: unless-stopped\n environment:\n PUID: '1000'\n PGID: '1000'\n volumes:\n - grav_data:/config\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n grav_data:\n", + + 'prestashop' => (function() use ($p, $domain, $dbPass, $adminPass): string { + $email = preg_replace('/[^a-zA-Z0-9@._\-]/', '', $p['admin_email'] ?? 'admin@' . $domain); + return "version: '3.8'\nservices:\n db:\n image: mariadb:10.11\n restart: unless-stopped\n environment:\n MYSQL_ROOT_PASSWORD: {$dbPass}\n MYSQL_DATABASE: prestashop\n MYSQL_USER: prestashop\n MYSQL_PASSWORD: {$dbPass}\n volumes:\n - db_data:/var/lib/mysql\n prestashop:\n image: prestashop/prestashop:latest\n restart: unless-stopped\n depends_on: [db]\n environment:\n PS_DOMAIN: {$domain}\n DB_SERVER: db\n DB_NAME: prestashop\n DB_USER: prestashop\n DB_PASSWD: {$dbPass}\n ADMIN_MAIL: {$email}\n ADMIN_PASSWD: {$adminPass}\n PS_INSTALL_AUTO: '1'\n volumes:\n - prestashop_data:/var/www/html\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n db_data:\n prestashop_data:\n"; + })(), + + 'opencart' => (function() use ($domain, $dbPass): string { + return "version: '3.8'\nservices:\n db:\n image: mariadb:10.11\n restart: unless-stopped\n environment:\n MYSQL_ROOT_PASSWORD: {$dbPass}\n MYSQL_DATABASE: opencart\n MYSQL_USER: opencart\n MYSQL_PASSWORD: {$dbPass}\n volumes:\n - db_data:/var/lib/mysql\n opencart:\n image: bitnami/opencart:latest\n restart: unless-stopped\n depends_on: [db]\n environment:\n OPENCART_DATABASE_HOST: db\n OPENCART_DATABASE_NAME: opencart\n OPENCART_DATABASE_USER: opencart\n OPENCART_DATABASE_PASSWORD: {$dbPass}\n volumes:\n - opencart_data:/bitnami/opencart\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n db_data:\n opencart_data:\n"; + })(), + + 'medusa' => (function() use ($domain, $dbPass): string { + $jwtSecret = bin2hex(random_bytes(16)); + return "version: '3.8'\nservices:\n db:\n image: postgres:16-alpine\n restart: unless-stopped\n environment:\n POSTGRES_DB: medusa\n POSTGRES_USER: medusa\n POSTGRES_PASSWORD: {$dbPass}\n volumes:\n - db_data:/var/lib/postgresql/data\n redis:\n image: redis:7-alpine\n restart: unless-stopped\n medusa:\n image: medusajs/medusa:latest\n restart: unless-stopped\n depends_on: [db, redis]\n environment:\n DATABASE_URL: postgresql://medusa:{$dbPass}@db:5432/medusa\n REDIS_URL: redis://redis:6379\n JWT_SECRET: {$jwtSecret}\n COOKIE_SECRET: {$jwtSecret}\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n db_data:\n"; + })(), + + 'answer' => (function() use ($domain, $dbPass): string { + return "version: '3.8'\nservices:\n db:\n image: mariadb:10.11\n restart: unless-stopped\n environment:\n MYSQL_ROOT_PASSWORD: {$dbPass}\n MYSQL_DATABASE: answer\n MYSQL_USER: answer\n MYSQL_PASSWORD: {$dbPass}\n volumes:\n - db_data:/var/lib/mysql\n answer:\n image: answerdev/answer:latest\n restart: unless-stopped\n depends_on: [db]\n environment:\n DB_TYPE: mysql\n DB_USERNAME: answer\n DB_PASSWORD: {$dbPass}\n DB_HOST: db:3306\n DB_NAME: answer\n volumes:\n - answer_data:/data\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n db_data:\n answer_data:\n"; + })(), + + // ── Project Management ──────────────────────────────────────────────── + + 'plane' => (function() use ($domain, $dbPass): string { + $secret = bin2hex(random_bytes(16)); + return "version: '3.8'\nservices:\n db:\n image: postgres:16-alpine\n restart: unless-stopped\n environment:\n POSTGRES_DB: plane\n POSTGRES_USER: plane\n POSTGRES_PASSWORD: {$dbPass}\n volumes:\n - db_data:/var/lib/postgresql/data\n redis:\n image: redis:7-alpine\n restart: unless-stopped\n plane-web:\n image: makeplane/plane-frontend:latest\n restart: unless-stopped\n labels:\n - 'novacpx.domain={$domain}'\n plane-api:\n image: makeplane/plane-backend:latest\n restart: unless-stopped\n depends_on: [db, redis]\n environment:\n DATABASE_URL: postgresql://plane:{$dbPass}@db:5432/plane\n REDIS_URL: redis://redis:6379\n SECRET_KEY: {$secret}\n WEB_URL: https://{$domain}\nvolumes:\n db_data:\n"; + })(), + + 'openproject' => (function() use ($domain, $dbPass, $adminPass): string { + return "version: '3.8'\nservices:\n db:\n image: postgres:16-alpine\n restart: unless-stopped\n environment:\n POSTGRES_DB: openproject\n POSTGRES_USER: openproject\n POSTGRES_PASSWORD: {$dbPass}\n volumes:\n - db_data:/var/lib/postgresql/data\n openproject:\n image: openproject/openproject:14\n restart: unless-stopped\n depends_on: [db]\n environment:\n OPENPROJECT_HOST__NAME: {$domain}\n DATABASE_URL: postgresql://openproject:{$dbPass}@db:5432/openproject\n OPENPROJECT_ADMIN_USER_PASSWORD: {$adminPass}\n volumes:\n - op_data:/var/openproject\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n db_data:\n op_data:\n"; + })(), + + 'leantime' => (function() use ($p, $domain, $dbPass, $adminPass): string { + $email = preg_replace('/[^a-zA-Z0-9@._\-]/', '', $p['admin_email'] ?? 'admin@' . $domain); + return "version: '3.8'\nservices:\n db:\n image: mariadb:10.11\n restart: unless-stopped\n environment:\n MYSQL_ROOT_PASSWORD: {$dbPass}\n MYSQL_DATABASE: leantime\n MYSQL_USER: leantime\n MYSQL_PASSWORD: {$dbPass}\n volumes:\n - db_data:/var/lib/mysql\n leantime:\n image: leantime/leantime:latest\n restart: unless-stopped\n depends_on: [db]\n environment:\n LEAN_DB_HOST: db\n LEAN_DB_DATABASE: leantime\n LEAN_DB_USER: leantime\n LEAN_DB_PASSWORD: {$dbPass}\n LEAN_SITENAME: Leantime\n LEAN_DEFAULT_TIMEZONE: UTC\n LEAN_EMAIL_RETURN: {$email}\n LEAN_ADMIN_EMAIL: {$email}\n LEAN_ADMIN_PASSWORD: {$adminPass}\n volumes:\n - leantime_public:/var/www/html/public/userfiles\n - leantime_private:/var/www/html/userfiles\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n db_data:\n leantime_public:\n leantime_private:\n"; + })(), + + 'wekan' => (function() use ($p, $domain, $adminUser, $adminPass): string { + $email = preg_replace('/[^a-zA-Z0-9@._\-]/', '', $p['admin_email'] ?? 'admin@' . $domain); + return "version: '3.8'\nservices:\n db:\n image: mongo:7\n restart: unless-stopped\n volumes:\n - db_data:/data/db\n wekan:\n image: wekanteam/wekan:latest\n restart: unless-stopped\n depends_on: [db]\n environment:\n MONGO_URL: mongodb://db:27017/wekan\n ROOT_URL: https://{$domain}\n WITH_API: 'true'\n ADMIN_DEFAULT_EMAIL: {$email}\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n db_data:\n"; + })(), + + 'focalboard' => (function() use ($domain, $dbPass): string { + return "version: '3.8'\nservices:\n db:\n image: postgres:16-alpine\n restart: unless-stopped\n environment:\n POSTGRES_DB: focalboard\n POSTGRES_USER: focalboard\n POSTGRES_PASSWORD: {$dbPass}\n volumes:\n - db_data:/var/lib/postgresql/data\n focalboard:\n image: mattermost/focalboard:latest\n restart: unless-stopped\n depends_on: [db]\n environment:\n FOCALBOARD_DBTYPE: postgres\n FOCALBOARD_DBCONFIG: postgresql://focalboard:{$dbPass}@db:5432/focalboard\n volumes:\n - focalboard_data:/opt/focalboard/data\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n db_data:\n focalboard_data:\n"; + })(), + + // ── Communication ───────────────────────────────────────────────────── + + 'matrix-synapse' => (function() use ($domain, $dbPass, $adminPass): string { + return "version: '3.8'\nservices:\n db:\n image: postgres:16-alpine\n restart: unless-stopped\n environment:\n POSTGRES_DB: synapse\n POSTGRES_USER: synapse\n POSTGRES_PASSWORD: {$dbPass}\n POSTGRES_INITDB_ARGS: --encoding=UTF-8 --lc-collate=C --lc-ctype=C\n volumes:\n - db_data:/var/lib/postgresql/data\n synapse:\n image: matrixdotorg/synapse:latest\n restart: unless-stopped\n depends_on: [db]\n environment:\n SYNAPSE_SERVER_NAME: {$domain}\n SYNAPSE_REPORT_STATS: 'no'\n SYNAPSE_MACAROON_SECRET_KEY: {$adminPass}\n SYNAPSE_DATABASE_HOST: db\n SYNAPSE_DATABASE_NAME: synapse\n SYNAPSE_DATABASE_USER: synapse\n SYNAPSE_DATABASE_PASSWORD: {$dbPass}\n volumes:\n - synapse_data:/data\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n db_data:\n synapse_data:\n"; + })(), + + 'gotify' => "version: '3.8'\nservices:\n gotify:\n image: gotify/server:latest\n restart: unless-stopped\n environment:\n GOTIFY_DEFAULTUSER_PASS: {$adminPass}\n volumes:\n - gotify_data:/app/data\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n gotify_data:\n", + + 'ntfy' => "version: '3.8'\nservices:\n ntfy:\n image: binwiederhier/ntfy:latest\n restart: unless-stopped\n command: serve\n volumes:\n - ntfy_cache:/var/cache/ntfy\n - ntfy_data:/etc/ntfy\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n ntfy_cache:\n ntfy_data:\n", + + 'grist' => (function() use ($p, $domain, $adminPass): string { + $email = preg_replace('/[^a-zA-Z0-9@._\-]/', '', $p['admin_email'] ?? 'admin@' . $domain); + return "version: '3.8'\nservices:\n grist:\n image: gristlabs/grist:latest\n restart: unless-stopped\n environment:\n APP_HOME_URL: https://{$domain}\n GRIST_SESSION_SECRET: {$adminPass}\n GRIST_SINGLE_ORG: myorg\n volumes:\n - grist_data:/persist\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n grist_data:\n"; + })(), + + 'mailpit' => "version: '3.8'\nservices:\n mailpit:\n image: axllent/mailpit:latest\n restart: unless-stopped\n labels:\n - 'novacpx.domain={$domain}'\n", + + // ── File Storage & Backup ───────────────────────────────────────────── + + 'syncthing' => "version: '3.8'\nservices:\n syncthing:\n image: syncthing/syncthing:latest\n restart: unless-stopped\n hostname: syncthing-{$domain}\n volumes:\n - syncthing_data:/var/syncthing\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n syncthing_data:\n", + + 'sftpgo' => "version: '3.8'\nservices:\n sftpgo:\n image: drakkan/sftpgo:latest\n restart: unless-stopped\n environment:\n SFTPGO_DEFAULT_ADMIN_USERNAME: {$adminUser}\n SFTPGO_DEFAULT_ADMIN_PASSWORD: {$adminPass}\n volumes:\n - sftpgo_data:/srv/sftpgo/data\n - sftpgo_config:/var/lib/sftpgo\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n sftpgo_data:\n sftpgo_config:\n", + + 'owncloud' => (function() use ($domain, $dbPass, $adminPass): string { + return "version: '3.8'\nservices:\n db:\n image: mariadb:10.11\n restart: unless-stopped\n environment:\n MYSQL_ROOT_PASSWORD: {$dbPass}\n MYSQL_DATABASE: owncloud\n MYSQL_USER: owncloud\n MYSQL_PASSWORD: {$dbPass}\n volumes:\n - db_data:/var/lib/mysql\n owncloud:\n image: owncloud/server:latest\n restart: unless-stopped\n depends_on: [db]\n environment:\n OWNCLOUD_DOMAIN: {$domain}\n OWNCLOUD_TRUSTED_DOMAINS: {$domain}\n OWNCLOUD_DB_TYPE: mysql\n OWNCLOUD_DB_HOST: db\n OWNCLOUD_DB_NAME: owncloud\n OWNCLOUD_DB_USERNAME: owncloud\n OWNCLOUD_DB_PASSWORD: {$dbPass}\n OWNCLOUD_ADMIN_USERNAME: admin\n OWNCLOUD_ADMIN_PASSWORD: {$adminPass}\n volumes:\n - owncloud_data:/mnt/data\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n db_data:\n owncloud_data:\n"; + })(), + + 'duplicati' => "version: '3.8'\nservices:\n duplicati:\n image: linuxserver/duplicati:latest\n restart: unless-stopped\n environment:\n PUID: '0'\n PGID: '0'\n volumes:\n - duplicati_config:/config\n - duplicati_backups:/backups\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n duplicati_config:\n duplicati_backups:\n", + + 'calibre-web' => "version: '3.8'\nservices:\n calibre-web:\n image: linuxserver/calibre-web:latest\n restart: unless-stopped\n environment:\n PUID: '1000'\n PGID: '1000'\n DOCKER_MODS: linuxserver/mods:universal-calibre\n volumes:\n - calibre_config:/config\n - calibre_books:/books\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n calibre_config:\n calibre_books:\n", + + // ── ERP & Business ──────────────────────────────────────────────────── + + 'odoo' => (function() use ($domain, $dbPass, $adminPass): string { + return "version: '3.8'\nservices:\n db:\n image: postgres:16-alpine\n restart: unless-stopped\n environment:\n POSTGRES_DB: postgres\n POSTGRES_USER: odoo\n POSTGRES_PASSWORD: {$dbPass}\n volumes:\n - db_data:/var/lib/postgresql/data\n odoo:\n image: odoo:17\n restart: unless-stopped\n depends_on: [db]\n environment:\n HOST: db\n USER: odoo\n PASSWORD: {$dbPass}\n volumes:\n - odoo_data:/var/lib/odoo\n - odoo_config:/etc/odoo\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n db_data:\n odoo_data:\n odoo_config:\n"; + })(), + + 'akaunting' => (function() use ($p, $domain, $dbPass, $adminPass): string { + $email = preg_replace('/[^a-zA-Z0-9@._\-]/', '', $p['admin_email'] ?? 'admin@' . $domain); + return "version: '3.8'\nservices:\n db:\n image: mariadb:10.11\n restart: unless-stopped\n environment:\n MYSQL_ROOT_PASSWORD: {$dbPass}\n MYSQL_DATABASE: akaunting\n MYSQL_USER: akaunting\n MYSQL_PASSWORD: {$dbPass}\n volumes:\n - db_data:/var/lib/mysql\n akaunting:\n image: akaunting/akaunting:latest\n restart: unless-stopped\n depends_on: [db]\n environment:\n COMPANY_NAME: My Company\n COMPANY_EMAIL: {$email}\n ADMIN_EMAIL: {$email}\n ADMIN_PASSWORD: {$adminPass}\n DB_HOST: db\n DB_NAME: akaunting\n DB_USERNAME: akaunting\n DB_PASSWORD: {$dbPass}\n volumes:\n - akaunting_data:/var/www/html/storage\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n db_data:\n akaunting_data:\n"; + })(), + + 'monica' => (function() use ($p, $domain, $dbPass, $adminPass): string { + $email = preg_replace('/[^a-zA-Z0-9@._\-]/', '', $p['admin_email'] ?? 'admin@' . $domain); + $key = bin2hex(random_bytes(16)); + return "version: '3.8'\nservices:\n db:\n image: mariadb:10.11\n restart: unless-stopped\n environment:\n MYSQL_ROOT_PASSWORD: {$dbPass}\n MYSQL_DATABASE: monica\n MYSQL_USER: monica\n MYSQL_PASSWORD: {$dbPass}\n volumes:\n - db_data:/var/lib/mysql\n monica:\n image: monica:latest\n restart: unless-stopped\n depends_on: [db]\n environment:\n APP_URL: https://{$domain}\n APP_KEY: base64:{$key}\n DB_HOST: db\n DB_DATABASE: monica\n DB_USERNAME: monica\n DB_PASSWORD: {$dbPass}\n volumes:\n - monica_storage:/var/www/html/storage\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n db_data:\n monica_storage:\n"; + })(), + + 'twenty' => (function() use ($domain, $dbPass): string { + $secret = bin2hex(random_bytes(16)); + return "version: '3.8'\nservices:\n db:\n image: postgres:16-alpine\n restart: unless-stopped\n environment:\n POSTGRES_DB: twenty\n POSTGRES_USER: twenty\n POSTGRES_PASSWORD: {$dbPass}\n volumes:\n - db_data:/var/lib/postgresql/data\n redis:\n image: redis:7-alpine\n restart: unless-stopped\n twenty:\n image: twentycrm/twenty:latest\n restart: unless-stopped\n depends_on: [db, redis]\n environment:\n FRONT_BASE_URL: https://{$domain}\n PG_DATABASE_URL: postgresql://twenty:{$dbPass}@db:5432/twenty\n REDIS_URL: redis://redis:6379\n APP_SECRET: {$secret}\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n db_data:\n"; + })(), + + 'dolibarr' => (function() use ($domain, $dbPass, $adminPass): string { + return "version: '3.8'\nservices:\n db:\n image: mariadb:10.11\n restart: unless-stopped\n environment:\n MYSQL_ROOT_PASSWORD: {$dbPass}\n MYSQL_DATABASE: dolibarr\n MYSQL_USER: dolibarr\n MYSQL_PASSWORD: {$dbPass}\n volumes:\n - db_data:/var/lib/mysql\n dolibarr:\n image: dolibarr/dolibarr:latest\n restart: unless-stopped\n depends_on: [db]\n environment:\n DOLI_DB_HOST: db\n DOLI_DB_NAME: dolibarr\n DOLI_DB_USER: dolibarr\n DOLI_DB_PASSWORD: {$dbPass}\n DOLI_ADMIN_LOGIN: admin\n DOLI_ADMIN_PASSWORD: {$adminPass}\n DOLI_URL_ROOT: https://{$domain}\n volumes:\n - dolibarr_html:/var/www/html\n - dolibarr_docs:/var/www/documents\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n db_data:\n dolibarr_html:\n dolibarr_docs:\n"; + })(), + + // ── Media ───────────────────────────────────────────────────────────── + + 'audiobookshelf' => "version: '3.8'\nservices:\n audiobookshelf:\n image: ghcr.io/advplyr/audiobookshelf:latest\n restart: unless-stopped\n volumes:\n - abs_config:/config\n - abs_metadata:/metadata\n - abs_audiobooks:/audiobooks\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n abs_config:\n abs_metadata:\n abs_audiobooks:\n", + + 'komga' => (function() use ($p, $domain, $adminPass): string { + $email = preg_replace('/[^a-zA-Z0-9@._\-]/', '', $p['admin_email'] ?? 'admin@' . $domain); + return "version: '3.8'\nservices:\n komga:\n image: gotson/komga:latest\n restart: unless-stopped\n environment:\n KOMGA_OAUTH2_ACCOUNT_CREATION: 'false'\n KOMGA_INITIAL_EMAIL: {$email}\n KOMGA_INITIAL_PASSWORD: {$adminPass}\n volumes:\n - komga_data:/config\n - komga_books:/data\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n komga_data:\n komga_books:\n"; + })(), + + 'grocy' => "version: '3.8'\nservices:\n grocy:\n image: linuxserver/grocy:latest\n restart: unless-stopped\n environment:\n PUID: '1000'\n PGID: '1000'\n TZ: UTC\n volumes:\n - grocy_config:/config\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n grocy_config:\n", + + 'tube-archivist' => (function() use ($domain, $adminUser, $adminPass): string { + $esPass = bin2hex(random_bytes(8)); + return "version: '3.8'\nservices:\n elasticsearch:\n image: elasticsearch:8.13.0\n restart: unless-stopped\n environment:\n discovery.type: single-node\n ELASTIC_PASSWORD: {$esPass}\n xpack.security.enabled: 'true'\n ES_JAVA_OPTS: -Xms512m -Xmx512m\n volumes:\n - es_data:/usr/share/elasticsearch/data\n redis:\n image: redis:7-alpine\n restart: unless-stopped\n volumes:\n - redis_data:/data\n tube-archivist:\n image: bbilly1/tubearchivist:latest\n restart: unless-stopped\n depends_on: [elasticsearch, redis]\n environment:\n ES_URL: http://elasticsearch:9200\n ELASTIC_PASSWORD: {$esPass}\n TA_USERNAME: {$adminUser}\n TA_PASSWORD: {$adminPass}\n TA_HOST: {$domain}\n REDIS_HOST: redis\n volumes:\n - ta_media:/youtube\n - ta_cache:/cache\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n es_data:\n redis_data:\n ta_media:\n ta_cache:\n"; + })(), + + 'affine' => (function() use ($domain, $dbPass): string { + return "version: '3.8'\nservices:\n db:\n image: postgres:16-alpine\n restart: unless-stopped\n environment:\n POSTGRES_DB: affine\n POSTGRES_USER: affine\n POSTGRES_PASSWORD: {$dbPass}\n volumes:\n - db_data:/var/lib/postgresql/data\n redis:\n image: redis:7-alpine\n restart: unless-stopped\n affine:\n image: ghcr.io/toeverything/affine-graphql:stable\n restart: unless-stopped\n depends_on: [db, redis]\n environment:\n DATABASE_URL: postgresql://affine:{$dbPass}@db:5432/affine\n REDIS_SERVER_HOST: redis\n volumes:\n - affine_config:/root/.affine\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n db_data:\n affine_config:\n"; + })(), + + // ── Smart Home ──────────────────────────────────────────────────────── + + 'home-assistant' => "version: '3.8'\nservices:\n homeassistant:\n image: homeassistant/home-assistant:stable\n restart: unless-stopped\n network_mode: host\n volumes:\n - ha_config:/config\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n ha_config:\n", + + 'node-red' => "version: '3.8'\nservices:\n node-red:\n image: nodered/node-red:latest\n restart: unless-stopped\n volumes:\n - node_red_data:/data\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n node_red_data:\n", + + 'mosquitto' => "version: '3.8'\nservices:\n mosquitto:\n image: eclipse-mosquitto:latest\n restart: unless-stopped\n volumes:\n - mosquitto_data:/mosquitto/data\n - mosquitto_log:/mosquitto/log\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n mosquitto_data:\n mosquitto_log:\n", + + 'zigbee2mqtt' => (function() use ($p, $domain): string { + $mqttHost = preg_replace('/[^a-zA-Z0-9.\-_]/', '', $p['mqtt_host'] ?? 'localhost'); + return "version: '3.8'\nservices:\n zigbee2mqtt:\n image: koenkk/zigbee2mqtt:latest\n restart: unless-stopped\n environment:\n ZIGBEE2MQTT_CONFIG_MQTT_SERVER: mqtt://{$mqttHost}\n ZIGBEE2MQTT_CONFIG_FRONTEND: 'true'\n volumes:\n - z2m_data:/app/data\n devices:\n - /dev/ttyACM0:/dev/ttyACM0\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n z2m_data:\n"; + })(), + + // ── Dashboards & Admin ──────────────────────────────────────────────── + + 'dashy' => "version: '3.8'\nservices:\n dashy:\n image: lissy93/dashy:latest\n restart: unless-stopped\n volumes:\n - dashy_config:/app/user-data\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n dashy_config:\n", + + 'homarr' => "version: '3.8'\nservices:\n homarr:\n image: ghcr.io/ajnart/homarr:latest\n restart: unless-stopped\n volumes:\n - homarr_configs:/app/data/configs\n - homarr_icons:/app/public/icons\n - homarr_data:/data\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n homarr_configs:\n homarr_icons:\n homarr_data:\n", + + 'homer' => "version: '3.8'\nservices:\n homer:\n image: b4bz/homer:latest\n restart: unless-stopped\n volumes:\n - homer_data:/www/assets\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n homer_data:\n", + + 'it-tools' => "version: '3.8'\nservices:\n it-tools:\n image: corentinth/it-tools:latest\n restart: unless-stopped\n labels:\n - 'novacpx.domain={$domain}'\n", + + 'cloudbeaver' => "version: '3.8'\nservices:\n cloudbeaver:\n image: dbeaver/cloudbeaver:latest\n restart: unless-stopped\n volumes:\n - cloudbeaver_data:/opt/cloudbeaver/workspace\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n cloudbeaver_data:\n", + + 'pgadmin' => (function() use ($p, $domain, $adminPass): string { + $email = preg_replace('/[^a-zA-Z0-9@._\-]/', '', $p['admin_email'] ?? 'admin@' . $domain); + return "version: '3.8'\nservices:\n pgadmin:\n image: dpage/pgadmin4:latest\n restart: unless-stopped\n environment:\n PGADMIN_DEFAULT_EMAIL: {$email}\n PGADMIN_DEFAULT_PASSWORD: {$adminPass}\n volumes:\n - pgadmin_data:/var/lib/pgadmin\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n pgadmin_data:\n"; + })(), + + 'phpmyadmin' => (function() use ($p, $domain, $dbPass): string { + $dbHost = preg_replace('/[^a-zA-Z0-9.\-_]/', '', $p['db_host'] ?? 'host.docker.internal'); + return "version: '3.8'\nservices:\n phpmyadmin:\n image: phpmyadmin:latest\n restart: unless-stopped\n environment:\n PMA_HOST: {$dbHost}\n MYSQL_ROOT_PASSWORD: {$dbPass}\n PMA_ARBITRARY: '1'\n labels:\n - 'novacpx.domain={$domain}'\n"; + })(), + + 'infisical' => (function() use ($domain, $dbPass): string { + $secret = bin2hex(random_bytes(16)); + $encKey = bin2hex(random_bytes(16)); + return "version: '3.8'\nservices:\n db:\n image: postgres:16-alpine\n restart: unless-stopped\n environment:\n POSTGRES_DB: infisical\n POSTGRES_USER: infisical\n POSTGRES_PASSWORD: {$dbPass}\n volumes:\n - db_data:/var/lib/postgresql/data\n redis:\n image: redis:7-alpine\n restart: unless-stopped\n infisical:\n image: infisical/infisical:latest\n restart: unless-stopped\n depends_on: [db, redis]\n environment:\n DB_CONNECTION_URI: postgresql://infisical:{$dbPass}@db:5432/infisical\n REDIS_URL: redis://redis:6379\n ENCRYPTION_KEY: {$encKey}\n AUTH_SECRET: {$secret}\n SITE_URL: https://{$domain}\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n db_data:\n"; + })(), + default => throw new RuntimeException("No compose template for: $appKey"), }; }