diff --git a/panel/lib/DockerManager.php b/panel/lib/DockerManager.php index e1feb03..ae49b14 100644 --- a/panel/lib/DockerManager.php +++ b/panel/lib/DockerManager.php @@ -493,6 +493,360 @@ SH; ['key' => 'db_pass', 'label' => 'DB Password', 'type' => 'password', 'required' => true], ], ], + // ── Media & Files ────────────────────────────────────────────────────── + 'jellyfin' => [ + 'name' => 'Jellyfin', + 'description' => 'Free media server — movies, TV & music', + 'icon' => 'JF', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ], + ], + 'navidrome' => [ + 'name' => 'Navidrome', + 'description' => 'Music streaming server (Subsonic-compatible API)', + 'icon' => 'Nav', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ], + ], + 'kavita' => [ + 'name' => 'Kavita', + 'description' => 'eBook, manga & comic server', + 'icon' => 'Kav', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ], + ], + 'paperless-ngx' => [ + 'name' => 'Paperless-ngx', + 'description' => 'Document management & OCR system (Postgres + Redis)', + 'icon' => 'Doc', + '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], + ], + ], + 'filebrowser' => [ + 'name' => 'File Browser', + 'description' => 'Web-based file manager for your server files', + 'icon' => 'FB', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ], + ], + 'seafile' => [ + 'name' => 'Seafile', + 'description' => 'Dropbox-like file sync & share (MariaDB + Memcached)', + 'icon' => 'Sea', + '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], + ['key' => 'db_pass', 'label' => 'DB Password', 'type' => 'password', 'required' => true], + ], + ], + 'immich' => [ + 'name' => 'Immich', + 'description' => 'Google Photos alternative — photo & video backup (Postgres)', + 'icon' => 'Imm', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'db_pass', 'label' => 'DB Password', 'type' => 'password', 'required' => true], + ], + ], + // ── Monitoring & DevOps ──────────────────────────────────────────────── + 'adminer' => [ + 'name' => 'Adminer', + 'description' => 'Lightweight multi-database admin UI (MySQL/PG/SQLite)', + 'icon' => 'Adm', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ], + ], + 'grafana' => [ + 'name' => 'Grafana', + 'description' => 'Monitoring & observability dashboards', + 'icon' => 'Grf', + '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], + ], + ], + 'prometheus' => [ + 'name' => 'Prometheus', + 'description' => 'Metrics collection & alerting engine', + 'icon' => 'Prom', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ], + ], + 'netdata' => [ + 'name' => 'Netdata', + 'description' => 'Real-time per-second system performance monitoring', + 'icon' => 'ND', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ], + ], + 'glances' => [ + 'name' => 'Glances', + 'description' => 'Web-based system monitoring dashboard', + 'icon' => 'Gl', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ], + ], + 'healthchecks' => [ + 'name' => 'Healthchecks', + 'description' => 'Cron job & scheduled task uptime monitoring (Postgres)', + 'icon' => 'HC', + '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], + ['key' => 'db_pass', 'label' => 'DB Password', 'type' => 'password', 'required' => true], + ], + ], + 'registry' => [ + 'name' => 'Docker Registry', + 'description' => 'Private Docker image registry', + 'icon' => 'Reg', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ], + ], + 'verdaccio' => [ + 'name' => 'Verdaccio', + 'description' => 'Private npm / yarn / pnpm package registry', + 'icon' => 'NPM', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ], + ], + 'watchtower' => [ + 'name' => 'Watchtower', + 'description' => 'Automatically update running Docker containers', + 'icon' => 'WT', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ], + ], + // ── Git & CI/CD ──────────────────────────────────────────────────────── + 'forgejo' => [ + 'name' => 'Forgejo', + 'description' => 'Self-hosted Git forge — Gitea community fork (MariaDB)', + 'icon' => 'FGJ', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'db_pass', 'label' => 'DB Password', 'type' => 'password', 'required' => true], + ], + ], + 'woodpecker-ci' => [ + 'name' => 'Woodpecker CI', + 'description' => 'Lightweight CI/CD pipeline server + agent', + 'icon' => 'WP', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'admin_user', 'label' => 'Admin User', 'type' => 'text', 'required' => true], + ], + ], + // ── Knowledge & Publishing ───────────────────────────────────────────── + 'bookstack' => [ + 'name' => 'BookStack', + 'description' => 'Simple wiki & documentation platform (MariaDB)', + 'icon' => 'BS', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'db_pass', 'label' => 'DB Password', 'type' => 'password', 'required' => true], + ], + ], + 'hedgedoc' => [ + 'name' => 'HedgeDoc', + 'description' => 'Collaborative real-time Markdown editor (Postgres)', + 'icon' => 'HD', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'db_pass', 'label' => 'DB Password', 'type' => 'password', 'required' => true], + ], + ], + 'freshrss' => [ + 'name' => 'FreshRSS', + 'description' => 'Self-hosted RSS & Atom news aggregator', + 'icon' => 'RSS', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ], + ], + 'wallabag' => [ + 'name' => 'Wallabag', + 'description' => 'Read-it-later & web article archiver (MariaDB)', + 'icon' => 'Wbg', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'db_pass', 'label' => 'DB Password', 'type' => 'password', 'required' => true], + ], + ], + 'homepage' => [ + 'name' => 'Homepage', + 'description' => 'Customizable application start page & dashboard', + 'icon' => 'HP', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ], + ], + // ── Auth & Security ──────────────────────────────────────────────────── + 'keycloak' => [ + 'name' => 'Keycloak', + 'description' => 'Enterprise IAM & single sign-on (Postgres)', + 'icon' => 'KC', + '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], + ], + ], + 'authentik' => [ + 'name' => 'Authentik', + 'description' => 'Identity provider & SSO gateway (Postgres + Redis)', + 'icon' => 'Auth', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'db_pass', 'label' => 'DB Password', 'type' => 'password', 'required' => true], + ], + ], + 'passbolt' => [ + 'name' => 'Passbolt CE', + 'description' => 'Open-source team password manager (MariaDB)', + 'icon' => 'PBlt', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'db_pass', 'label' => 'DB Password', 'type' => 'password', 'required' => true], + ], + ], + // ── Analytics ───────────────────────────────────────────────────────── + 'plausible' => [ + 'name' => 'Plausible Analytics', + 'description' => 'Privacy-first analytics — Postgres + ClickHouse (resource-heavy)', + 'icon' => 'Plau', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'db_pass', 'label' => 'DB Password', 'type' => 'password', 'required' => true], + ], + ], + // ── Low-code & No-code ───────────────────────────────────────────────── + 'baserow' => [ + 'name' => 'Baserow', + 'description' => 'No-code database platform, Airtable alternative (Postgres)', + 'icon' => 'BRow', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'db_pass', 'label' => 'DB Password', 'type' => 'password', 'required' => true], + ], + ], + 'appsmith' => [ + 'name' => 'Appsmith', + 'description' => 'Low-code internal app builder (bundled CE image)', + 'icon' => 'Apps', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ], + ], + 'nocodb' => [ + 'name' => 'NocoDB', + 'description' => 'Airtable alternative — spreadsheet meets database', + 'icon' => 'Noco', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ], + ], + // ── Communication & Support ──────────────────────────────────────────── + 'rocketchat' => [ + 'name' => 'Rocket.Chat', + 'description' => 'Open-source team messaging platform (MongoDB)', + 'icon' => 'RC', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ], + ], + 'chatwoot' => [ + 'name' => 'Chatwoot', + 'description' => 'Customer support & live chat platform (Postgres + Redis)', + 'icon' => 'CW', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'db_pass', 'label' => 'DB Password', 'type' => 'password', 'required' => true], + ], + ], + 'zammad' => [ + 'name' => 'Zammad', + 'description' => 'Help desk & ticketing system (Postgres)', + 'icon' => 'Zam', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'db_pass', 'label' => 'DB Password', 'type' => 'password', 'required' => true], + ], + ], + // ── Business & Productivity ──────────────────────────────────────────── + 'invoiceninja' => [ + 'name' => 'Invoice Ninja', + 'description' => 'Invoicing, billing & time-tracking (MariaDB)', + 'icon' => 'IN', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'db_pass', 'label' => 'DB Password', 'type' => 'password', 'required' => true], + ], + ], + 'linkding' => [ + 'name' => 'Linkding', + 'description' => 'Minimal self-hosted bookmark manager', + 'icon' => 'LD', + '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], + ], + ], + 'mealie' => [ + 'name' => 'Mealie', + 'description' => 'Self-hosted recipe manager & meal planner', + 'icon' => 'Meal', + '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], + ], + ], + // ── Design & Collaboration ───────────────────────────────────────────── + 'penpot' => [ + 'name' => 'Penpot', + 'description' => 'Open-source design & prototyping tool (Postgres + Redis)', + 'icon' => 'PenP', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ['key' => 'db_pass', 'label' => 'DB Password', 'type' => 'password', 'required' => true], + ], + ], + 'excalidraw' => [ + 'name' => 'Excalidraw', + 'description' => 'Virtual whiteboard & collaborative sketching tool', + 'icon' => 'Exc', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ], + ], + 'stirlingpdf' => [ + 'name' => 'Stirling PDF', + 'description' => 'Self-hosted PDF manipulation & conversion toolkit', + 'icon' => 'PDF', + 'params' => [ + ['key' => 'domain', 'label' => 'Domain', 'type' => 'text', 'required' => true], + ], + ], ]; } @@ -571,6 +925,144 @@ SH; 'mattermost' => "version: '3.8'\nservices:\n db:\n image: postgres:16-alpine\n restart: unless-stopped\n environment:\n POSTGRES_USER: mattermost\n POSTGRES_PASSWORD: {$dbPass}\n POSTGRES_DB: mattermost\n volumes:\n - db_data:/var/lib/postgresql/data\n mattermost:\n image: mattermost/mattermost-team-edition:latest\n restart: unless-stopped\n depends_on: [db]\n environment:\n MM_SQLSETTINGS_DRIVERNAME: postgres\n MM_SQLSETTINGS_DATASOURCE: postgres://mattermost:{$dbPass}@db:5432/mattermost?sslmode=disable\n MM_SERVICESETTINGS_SITEURL: https://{$domain}\n volumes:\n - mm_data:/mattermost/data\n - mm_logs:/mattermost/logs\n - mm_config:/mattermost/config\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n db_data:\n mm_data:\n mm_logs:\n mm_config:\n", + // ── Media & Files ────────────────────────────────────────────────────── + + 'jellyfin' => "version: '3.8'\nservices:\n jellyfin:\n image: jellyfin/jellyfin:latest\n restart: unless-stopped\n volumes:\n - jellyfin_config:/config\n - jellyfin_cache:/cache\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n jellyfin_config:\n jellyfin_cache:\n", + + 'navidrome' => "version: '3.8'\nservices:\n navidrome:\n image: deluan/navidrome:latest\n restart: unless-stopped\n environment:\n ND_SCANSCHEDULE: 1h\n ND_LOGLEVEL: info\n ND_BASEURL: ''\n volumes:\n - navidrome_data:/data\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n navidrome_data:\n", + + 'kavita' => "version: '3.8'\nservices:\n kavita:\n image: jvmilazz0/kavita:latest\n restart: unless-stopped\n volumes:\n - kavita_data:/kavita/config\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n kavita_data:\n", + + 'paperless-ngx' => (function() use ($p, $domain, $dbPass, $adminPass, $adminUser): string { + $secret = bin2hex(random_bytes(24)); + return "version: '3.8'\nservices:\n db:\n image: postgres:16-alpine\n restart: unless-stopped\n environment:\n POSTGRES_DB: paperless\n POSTGRES_USER: paperless\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 paperless:\n image: ghcr.io/paperless-ngx/paperless-ngx:latest\n restart: unless-stopped\n depends_on: [db, redis]\n environment:\n PAPERLESS_REDIS: redis://redis:6379\n PAPERLESS_DBHOST: db\n PAPERLESS_DBNAME: paperless\n PAPERLESS_DBUSER: paperless\n PAPERLESS_DBPASS: {$dbPass}\n PAPERLESS_URL: https://{$domain}\n PAPERLESS_SECRET_KEY: {$secret}\n PAPERLESS_ADMIN_USER: {$adminUser}\n PAPERLESS_ADMIN_PASSWORD: {$adminPass}\n volumes:\n - paperless_data:/usr/src/paperless/data\n - paperless_media:/usr/src/paperless/media\n - paperless_consume:/usr/src/paperless/consume\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n db_data:\n paperless_data:\n paperless_media:\n paperless_consume:\n"; + })(), + + 'filebrowser' => "version: '3.8'\nservices:\n filebrowser:\n image: filebrowser/filebrowser:latest\n restart: unless-stopped\n volumes:\n - /home:/srv\n - filebrowser_db:/database\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n filebrowser_db:\n", + + 'seafile' => (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_LOG_CONSOLE: 'true'\n volumes:\n - db_data:/var/lib/mysql\n memcached:\n image: memcached:1.6\n restart: unless-stopped\n seafile:\n image: seafileltd/seafile-mc:latest\n restart: unless-stopped\n depends_on: [db, memcached]\n environment:\n DB_HOST: db\n DB_ROOT_PASSWD: {$dbPass}\n SEAFILE_ADMIN_EMAIL: {$email}\n SEAFILE_ADMIN_PASSWORD: {$adminPass}\n SEAFILE_SERVER_HOSTNAME: {$domain}\n SEAFILE_SERVER_LETSENCRYPT: 'false'\n volumes:\n - seafile_data:/shared\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n db_data:\n seafile_data:\n"; + })(), + + 'immich' => "version: '3.8'\nservices:\n db:\n image: tensorchord/pgvecto-rs:pg16-v0.2.0\n restart: unless-stopped\n environment:\n POSTGRES_DB: immich\n POSTGRES_USER: postgres\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 immich-server:\n image: ghcr.io/immich-app/immich-server:release\n restart: unless-stopped\n depends_on: [db, redis]\n environment:\n DB_HOSTNAME: db\n DB_DATABASE_NAME: immich\n DB_USERNAME: postgres\n DB_PASSWORD: {$dbPass}\n REDIS_HOSTNAME: redis\n volumes:\n - immich_upload:/usr/src/app/upload\n labels:\n - 'novacpx.domain={$domain}'\n immich-machine-learning:\n image: ghcr.io/immich-app/immich-machine-learning:release\n restart: unless-stopped\n volumes:\n - immich_model_cache:/cache\nvolumes:\n db_data:\n immich_upload:\n immich_model_cache:\n", + + // ── Monitoring & DevOps ──────────────────────────────────────────────── + + 'adminer' => "version: '3.8'\nservices:\n adminer:\n image: adminer:latest\n restart: unless-stopped\n labels:\n - 'novacpx.domain={$domain}'\n", + + 'grafana' => "version: '3.8'\nservices:\n grafana:\n image: grafana/grafana:latest\n restart: unless-stopped\n environment:\n GF_SECURITY_ADMIN_USER: {$adminUser}\n GF_SECURITY_ADMIN_PASSWORD: {$adminPass}\n GF_SERVER_ROOT_URL: https://{$domain}\n volumes:\n - grafana_data:/var/lib/grafana\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n grafana_data:\n", + + 'prometheus' => "version: '3.8'\nservices:\n prometheus:\n image: prom/prometheus:latest\n restart: unless-stopped\n command:\n - '--config.file=/etc/prometheus/prometheus.yml'\n - '--storage.tsdb.path=/prometheus'\n - '--storage.tsdb.retention.time=30d'\n volumes:\n - prometheus_data:/prometheus\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n prometheus_data:\n", + + 'netdata' => "version: '3.8'\nservices:\n netdata:\n image: netdata/netdata:latest\n restart: unless-stopped\n cap_add:\n - SYS_PTRACE\n - SYS_ADMIN\n security_opt:\n - apparmor:unconfined\n volumes:\n - netdata_config:/etc/netdata\n - netdata_lib:/var/lib/netdata\n - netdata_cache:/var/cache/netdata\n - /proc:/host/proc:ro\n - /sys:/host/sys:ro\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n netdata_config:\n netdata_lib:\n netdata_cache:\n", + + 'glances' => "version: '3.8'\nservices:\n glances:\n image: nicolargo/glances:latest-full\n restart: unless-stopped\n environment:\n GLANCES_OPT: -w\n volumes:\n - /var/run/docker.sock:/var/run/docker.sock:ro\n - /proc:/proc:ro\n - /sys:/sys:ro\n labels:\n - 'novacpx.domain={$domain}'\n", + + 'healthchecks' => (function() use ($p, $domain, $dbPass, $adminPass): string { + $email = preg_replace('/[^a-zA-Z0-9@._\-]/', '', $p['admin_email'] ?? 'admin@' . $domain); + $secret = bin2hex(random_bytes(24)); + return "version: '3.8'\nservices:\n db:\n image: postgres:16-alpine\n restart: unless-stopped\n environment:\n POSTGRES_DB: healthchecks\n POSTGRES_USER: healthchecks\n POSTGRES_PASSWORD: {$dbPass}\n volumes:\n - db_data:/var/lib/postgresql/data\n healthchecks:\n image: healthchecks/healthchecks:latest\n restart: unless-stopped\n depends_on: [db]\n environment:\n SECRET_KEY: {$secret}\n SITE_ROOT: https://{$domain}\n DEFAULT_FROM_EMAIL: hc@{$domain}\n DB: postgres\n DB_HOST: db\n DB_NAME: healthchecks\n DB_USER: healthchecks\n DB_PASSWORD: {$dbPass}\n SUPERUSER_EMAIL: {$email}\n SUPERUSER_PASSWORD: {$adminPass}\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n db_data:\n"; + })(), + + 'registry' => "version: '3.8'\nservices:\n registry:\n image: registry:2\n restart: unless-stopped\n volumes:\n - registry_data:/var/lib/registry\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n registry_data:\n", + + 'verdaccio' => "version: '3.8'\nservices:\n verdaccio:\n image: verdaccio/verdaccio:latest\n restart: unless-stopped\n volumes:\n - verdaccio_storage:/verdaccio/storage\n - verdaccio_conf:/verdaccio/conf\n - verdaccio_plugins:/verdaccio/plugins\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n verdaccio_storage:\n verdaccio_conf:\n verdaccio_plugins:\n", + + 'watchtower' => "version: '3.8'\nservices:\n watchtower:\n image: containrrr/watchtower:latest\n restart: unless-stopped\n volumes:\n - /var/run/docker.sock:/var/run/docker.sock\n labels:\n - 'novacpx.domain={$domain}'\n", + + // ── Git & CI/CD ──────────────────────────────────────────────────────── + + 'forgejo' => "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: forgejo\n MYSQL_USER: forgejo\n MYSQL_PASSWORD: {$dbPass}\n volumes:\n - db_data:/var/lib/mysql\n forgejo:\n image: codeberg.org/forgejo/forgejo:latest\n restart: unless-stopped\n depends_on: [db]\n environment:\n USER_UID: '1000'\n USER_GID: '1000'\n FORGEJO__database__DB_TYPE: mysql\n FORGEJO__database__HOST: db:3306\n FORGEJO__database__NAME: forgejo\n FORGEJO__database__USER: forgejo\n FORGEJO__database__PASSWD: {$dbPass}\n volumes:\n - forgejo_data:/data\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n db_data:\n forgejo_data:\n", + + 'woodpecker-ci' => (function() use ($domain, $adminUser): string { + $secret = bin2hex(random_bytes(16)); + return "version: '3.8'\nservices:\n woodpecker-server:\n image: woodpeckerci/woodpecker-server:latest\n restart: unless-stopped\n environment:\n WOODPECKER_OPEN: 'true'\n WOODPECKER_ADMIN: {$adminUser}\n WOODPECKER_AGENT_SECRET: {$secret}\n WOODPECKER_HOST: https://{$domain}\n volumes:\n - woodpecker_data:/var/lib/woodpecker\n labels:\n - 'novacpx.domain={$domain}'\n woodpecker-agent:\n image: woodpeckerci/woodpecker-agent:latest\n restart: unless-stopped\n depends_on: [woodpecker-server]\n environment:\n WOODPECKER_SERVER: woodpecker-server:9000\n WOODPECKER_AGENT_SECRET: {$secret}\n volumes:\n - /var/run/docker.sock:/var/run/docker.sock\nvolumes:\n woodpecker_data:\n"; + })(), + + // ── Knowledge & Publishing ───────────────────────────────────────────── + + 'bookstack' => "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: bookstack\n MYSQL_USER: bookstack\n MYSQL_PASSWORD: {$dbPass}\n volumes:\n - db_data:/var/lib/mysql\n bookstack:\n image: lscr.io/linuxserver/bookstack:latest\n restart: unless-stopped\n depends_on: [db]\n environment:\n APP_URL: https://{$domain}\n DB_HOST: db\n DB_DATABASE: bookstack\n DB_USERNAME: bookstack\n DB_PASSWORD: {$dbPass}\n volumes:\n - bookstack_data:/config\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n db_data:\n bookstack_data:\n", + + 'hedgedoc' => (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: hedgedoc\n POSTGRES_USER: hedgedoc\n POSTGRES_PASSWORD: {$dbPass}\n volumes:\n - db_data:/var/lib/postgresql/data\n hedgedoc:\n image: quay.io/hedgedoc/hedgedoc:latest\n restart: unless-stopped\n depends_on: [db]\n environment:\n CMD_DOMAIN: {$domain}\n CMD_URL_ADDPORT: 'false'\n CMD_PROTOCOL_USESSL: 'true'\n CMD_DB_URL: postgres://hedgedoc:{$dbPass}@db:5432/hedgedoc\n CMD_SESSION_SECRET: {$secret}\n CMD_ALLOW_ANONYMOUS: 'true'\n volumes:\n - hedgedoc_uploads:/hedgedoc/public/uploads\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n db_data:\n hedgedoc_uploads:\n"; + })(), + + 'freshrss' => "version: '3.8'\nservices:\n freshrss:\n image: freshrss/freshrss:latest\n restart: unless-stopped\n environment:\n CRON_MIN: '*/15'\n TZ: UTC\n volumes:\n - freshrss_data:/var/www/FreshRSS/data\n - freshrss_extensions:/var/www/FreshRSS/extensions\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n freshrss_data:\n freshrss_extensions:\n", + + 'wallabag' => (function() use ($domain, $dbPass): string { + $secret = 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: wallabag\n MYSQL_USER: wallabag\n MYSQL_PASSWORD: {$dbPass}\n volumes:\n - db_data:/var/lib/mysql\n wallabag:\n image: wallabag/wallabag:latest\n restart: unless-stopped\n depends_on: [db]\n environment:\n SYMFONY__ENV__DATABASE_DRIVER: pdo_mysql\n SYMFONY__ENV__DATABASE_HOST: db\n SYMFONY__ENV__DATABASE_NAME: wallabag\n SYMFONY__ENV__DATABASE_USER: wallabag\n SYMFONY__ENV__DATABASE_PASSWORD: {$dbPass}\n SYMFONY__ENV__DOMAIN_NAME: https://{$domain}\n SYMFONY__ENV__SECRET: {$secret}\n volumes:\n - wallabag_data:/var/www/wallabag/web/assets/images\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n db_data:\n wallabag_data:\n"; + })(), + + 'homepage' => "version: '3.8'\nservices:\n homepage:\n image: ghcr.io/gethomepage/homepage:latest\n restart: unless-stopped\n volumes:\n - homepage_config:/app/config\n - /var/run/docker.sock:/var/run/docker.sock:ro\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n homepage_config:\n", + + // ── Auth & Security ──────────────────────────────────────────────────── + + 'keycloak' => "version: '3.8'\nservices:\n db:\n image: postgres:16-alpine\n restart: unless-stopped\n environment:\n POSTGRES_DB: keycloak\n POSTGRES_USER: keycloak\n POSTGRES_PASSWORD: {$dbPass}\n volumes:\n - db_data:/var/lib/postgresql/data\n keycloak:\n image: quay.io/keycloak/keycloak:latest\n restart: unless-stopped\n depends_on: [db]\n command: start\n environment:\n KC_DB: postgres\n KC_DB_URL: jdbc:postgresql://db:5432/keycloak\n KC_DB_USERNAME: keycloak\n KC_DB_PASSWORD: {$dbPass}\n KC_HOSTNAME: {$domain}\n KEYCLOAK_ADMIN: {$adminUser}\n KEYCLOAK_ADMIN_PASSWORD: {$adminPass}\n KC_PROXY: edge\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n db_data:\n", + + 'authentik' => (function() use ($domain, $dbPass): string { + $secret = bin2hex(random_bytes(24)); + $img = 'ghcr.io/goauthentik/server:latest'; + $env = "AUTHENTIK_REDIS__HOST: redis\n AUTHENTIK_POSTGRESQL__HOST: db\n AUTHENTIK_POSTGRESQL__NAME: authentik\n AUTHENTIK_POSTGRESQL__USER: authentik\n AUTHENTIK_POSTGRESQL__PASSWORD: {$dbPass}\n AUTHENTIK_SECRET_KEY: {$secret}"; + return "version: '3.8'\nservices:\n db:\n image: postgres:16-alpine\n restart: unless-stopped\n environment:\n POSTGRES_DB: authentik\n POSTGRES_USER: authentik\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 server:\n image: {$img}\n restart: unless-stopped\n command: server\n depends_on: [db, redis]\n environment:\n {$env}\n volumes:\n - authentik_media:/media\n - authentik_templates:/templates\n labels:\n - 'novacpx.domain={$domain}'\n worker:\n image: {$img}\n restart: unless-stopped\n command: worker\n depends_on: [db, redis]\n environment:\n {$env}\n volumes:\n - authentik_media:/media\nvolumes:\n db_data:\n authentik_media:\n authentik_templates:\n"; + })(), + + 'passbolt' => "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: passbolt\n MYSQL_USER: passbolt\n MYSQL_PASSWORD: {$dbPass}\n volumes:\n - db_data:/var/lib/mysql\n passbolt:\n image: passbolt/passbolt:latest-ce\n restart: unless-stopped\n depends_on: [db]\n environment:\n APP_FULL_BASE_URL: https://{$domain}\n DATASOURCES_DEFAULT_HOST: db\n DATASOURCES_DEFAULT_DATABASE: passbolt\n DATASOURCES_DEFAULT_USERNAME: passbolt\n DATASOURCES_DEFAULT_PASSWORD: {$dbPass}\n volumes:\n - passbolt_gpg:/etc/passbolt/gpg\n - passbolt_jwt:/etc/passbolt/jwt\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n db_data:\n passbolt_gpg:\n passbolt_jwt:\n", + + // ── Analytics ───────────────────────────────────────────────────────── + + 'plausible' => (function() use ($domain, $dbPass): string { + $secret = bin2hex(random_bytes(24)); + return "version: '3.8'\nservices:\n db:\n image: postgres:16-alpine\n restart: unless-stopped\n environment:\n POSTGRES_DB: plausible\n POSTGRES_USER: plausible\n POSTGRES_PASSWORD: {$dbPass}\n volumes:\n - db_data:/var/lib/postgresql/data\n clickhouse:\n image: clickhouse/clickhouse-server:latest\n restart: unless-stopped\n volumes:\n - clickhouse_data:/var/lib/clickhouse\n plausible:\n image: ghcr.io/plausible/community-edition:v2\n restart: unless-stopped\n depends_on: [db, clickhouse]\n command: sh -c \"sleep 10 && /entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh run\"\n environment:\n BASE_URL: https://{$domain}\n SECRET_KEY_BASE: {$secret}\n DATABASE_URL: postgres://plausible:{$dbPass}@db:5432/plausible\n CLICKHOUSE_DATABASE_URL: http://clickhouse:8123/plausible_events_db\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n db_data:\n clickhouse_data:\n"; + })(), + + // ── Low-code & No-code ───────────────────────────────────────────────── + + 'baserow' => "version: '3.8'\nservices:\n db:\n image: postgres:16-alpine\n restart: unless-stopped\n environment:\n POSTGRES_DB: baserow\n POSTGRES_USER: baserow\n POSTGRES_PASSWORD: {$dbPass}\n volumes:\n - db_data:/var/lib/postgresql/data\n baserow:\n image: baserow/baserow:latest\n restart: unless-stopped\n depends_on: [db]\n environment:\n BASEROW_PUBLIC_URL: https://{$domain}\n DATABASE_HOST: db\n DATABASE_NAME: baserow\n DATABASE_USER: baserow\n DATABASE_PASSWORD: {$dbPass}\n volumes:\n - baserow_data:/baserow/data\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n db_data:\n baserow_data:\n", + + 'appsmith' => "version: '3.8'\nservices:\n appsmith:\n image: appsmith/appsmith-ce:latest\n restart: unless-stopped\n volumes:\n - appsmith_data:/appsmith-stacks\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n appsmith_data:\n", + + 'nocodb' => "version: '3.8'\nservices:\n nocodb:\n image: nocodb/nocodb:latest\n restart: unless-stopped\n volumes:\n - nocodb_data:/usr/app/data\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n nocodb_data:\n", + + // ── Communication & Support ──────────────────────────────────────────── + + 'rocketchat' => "version: '3.8'\nservices:\n mongodb:\n image: mongo:7.0\n restart: unless-stopped\n command: ['--replSet', 'rs0', '--bind_ip_all']\n volumes:\n - mongo_data:/data/db\n mongodb-init:\n image: mongo:7.0\n restart: on-failure\n depends_on: [mongodb]\n command: >-\n mongosh --host mongodb:27017 --eval\n \"rs.initiate({_id:'rs0',members:[{_id:0,host:'mongodb:27017'}]})\"\n rocketchat:\n image: registry.rocket.chat/rocketchat:latest\n restart: unless-stopped\n depends_on: [mongodb]\n environment:\n ROOT_URL: https://{$domain}\n MONGO_URL: mongodb://mongodb:27017/rocketchat?replicaSet=rs0\n MONGO_OPLOG_URL: mongodb://mongodb:27017/local?replicaSet=rs0\n volumes:\n - rocketchat_uploads:/app/uploads\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n mongo_data:\n rocketchat_uploads:\n", + + 'chatwoot' => (function() use ($domain, $dbPass): string { + $secret = bin2hex(random_bytes(32)); + $envBlock = "SECRET_KEY_BASE: {$secret}\n FRONTEND_URL: https://{$domain}\n DATABASE_URL: postgres://chatwoot:{$dbPass}@db:5432/chatwoot\n REDIS_URL: redis://redis:6379"; + return "version: '3.8'\nservices:\n db:\n image: postgres:16-alpine\n restart: unless-stopped\n environment:\n POSTGRES_DB: chatwoot\n POSTGRES_USER: chatwoot\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 chatwoot:\n image: chatwoot/chatwoot:latest\n restart: unless-stopped\n depends_on: [db, redis]\n command: bundle exec rails s\n environment:\n {$envBlock}\n volumes:\n - chatwoot_storage:/app/storage\n labels:\n - 'novacpx.domain={$domain}'\n sidekiq:\n image: chatwoot/chatwoot:latest\n restart: unless-stopped\n depends_on: [db, redis]\n command: bundle exec sidekiq\n environment:\n {$envBlock}\n volumes:\n - chatwoot_storage:/app/storage\nvolumes:\n db_data:\n chatwoot_storage:\n"; + })(), + + 'zammad' => "version: '3.8'\nservices:\n db:\n image: postgres:16-alpine\n restart: unless-stopped\n environment:\n POSTGRES_DB: zammad\n POSTGRES_USER: zammad\n POSTGRES_PASSWORD: {$dbPass}\n volumes:\n - db_data:/var/lib/postgresql/data\n zammad-railsserver:\n image: zammad/zammad-docker-compose:latest\n restart: unless-stopped\n depends_on: [db]\n command: zammad-railsserver\n environment:\n POSTGRESQL_HOST: db\n POSTGRESQL_DB: zammad\n POSTGRESQL_USER: zammad\n POSTGRESQL_PASS: {$dbPass}\n volumes:\n - zammad_data:/opt/zammad\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n db_data:\n zammad_data:\n", + + // ── Business & Productivity ──────────────────────────────────────────── + + 'invoiceninja' => (function() use ($domain, $dbPass): string { + $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: invoiceninja\n MYSQL_USER: invoiceninja\n MYSQL_PASSWORD: {$dbPass}\n volumes:\n - db_data:/var/lib/mysql\n app:\n image: invoiceninja/invoiceninja: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: invoiceninja\n DB_USERNAME: invoiceninja\n DB_PASSWORD: {$dbPass}\n volumes:\n - invoiceninja_public:/var/www/app/public\n - invoiceninja_storage:/var/www/app/storage\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n db_data:\n invoiceninja_public:\n invoiceninja_storage:\n"; + })(), + + 'linkding' => "version: '3.8'\nservices:\n linkding:\n image: sissbruecker/linkding:latest\n restart: unless-stopped\n environment:\n LD_SUPERUSER_NAME: {$adminUser}\n LD_SUPERUSER_PASSWORD: {$adminPass}\n volumes:\n - linkding_data:/etc/linkding/data\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n linkding_data:\n", + + 'mealie' => (function() use ($p, $domain, $adminPass): string { + $email = preg_replace('/[^a-zA-Z0-9@._\-]/', '', $p['admin_email'] ?? 'admin@' . $domain); + return "version: '3.8'\nservices:\n mealie:\n image: ghcr.io/mealie-recipes/mealie:latest\n restart: unless-stopped\n environment:\n ALLOW_SIGNUP: 'true'\n BASE_URL: https://{$domain}\n DEFAULT_EMAIL: {$email}\n DEFAULT_PASSWORD: {$adminPass}\n volumes:\n - mealie_data:/app/data\n labels:\n - 'novacpx.domain={$domain}'\nvolumes:\n mealie_data:\n"; + })(), + + // ── Design & Collaboration ───────────────────────────────────────────── + + 'penpot' => (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: penpot\n POSTGRES_USER: penpot\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 penpot-frontend:\n image: penpotapp/frontend:latest\n restart: unless-stopped\n volumes:\n - penpot_assets:/opt/data/assets\n labels:\n - 'novacpx.domain={$domain}'\n penpot-backend:\n image: penpotapp/backend:latest\n restart: unless-stopped\n depends_on: [db, redis]\n environment:\n PENPOT_DATABASE_URI: postgresql://db:5432/penpot\n PENPOT_DATABASE_USERNAME: penpot\n PENPOT_DATABASE_PASSWORD: {$dbPass}\n PENPOT_REDIS_URI: redis://redis:6379/0\n PENPOT_PUBLIC_URI: https://{$domain}\n PENPOT_SECRET_KEY: {$secret}\n PENPOT_FLAGS: 'enable-registration enable-login'\n volumes:\n - penpot_assets:/opt/data/assets\n penpot-exporter:\n image: penpotapp/exporter:latest\n restart: unless-stopped\n environment:\n PENPOT_PUBLIC_URI: http://penpot-frontend\n PENPOT_SECRET_KEY: {$secret}\nvolumes:\n db_data:\n penpot_assets:\n"; + })(), + + 'excalidraw' => "version: '3.8'\nservices:\n excalidraw:\n image: excalidraw/excalidraw:latest\n restart: unless-stopped\n labels:\n - 'novacpx.domain={$domain}'\n", + + '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", + default => throw new RuntimeException("No compose template for: $appKey"), }; }