const fs = require('fs'); const path = require('path'); const { DEFAULT_PORTS } = require('../shared/constants'); /** * CaddyfileGenerator - Creates Caddyfile and docker-compose.yml for DashCaddy * * Generates production-grade configs that match the patterns used by the * running DashCaddy deployment (CORS snippets, admin origins, PKI, etc.) * * DISK SAFETY: All generated configs include sensible defaults for storage * limits — health retention, stats caps, and memory limits — so a fresh * install will never silently fill a user's disk. */ class CaddyfileGenerator { /** * Normalize a Windows path to forward slashes for Caddyfile/Docker Compose */ _p(s) { return s.replace(/\\/g, '/'); } /** * Generate the CORS snippet block used by the API */ _corsSnippets() { return `(cors-preflight) { @preflight method OPTIONS header @preflight { Access-Control-Allow-Origin "*" Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS" Access-Control-Allow-Headers "*" Access-Control-Max-Age "600" } respond @preflight 204 } (cors-allow) { header { Access-Control-Allow-Origin "*" Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS" Access-Control-Allow-Headers "*" Access-Control-Max-Age "600" } } `; } /** * Generate the dashcaddy_auth snippet for SSO */ _authSnippet(apiPort) { return `# DashCaddy SSO auth snippet (dashcaddy_auth) { @needsAuth not path /dashcaddy-sso forward_auth @needsAuth localhost:${apiPort} { uri /api/v1/auth/gate/{args[0]} copy_headers Authorization X-Api-Key X-App-Cookie X-Emby-Token X-Plex-Token } handle /dashcaddy-sso { rewrite * /api/v1/auth/sso-exchange reverse_proxy localhost:${apiPort} } } `; } /** * Generate the dashboard site block content (shared across all modes) */ _dashboardBlock(dashboardPath, tier, apiPort) { let block = ''; block += ` root * ${this._p(dashboardPath)}\n`; block += ` encode gzip\n\n`; // API proxy for intermediate/advanced tiers if (tier !== 'basic') { block += ` # API proxy to Docker container\n`; block += ` handle /api/* {\n`; block += ` reverse_proxy localhost:${apiPort}\n`; block += ` }\n\n`; } // SPA fallback with file_server block += ` # Static site + SPA fallback\n`; block += ` handle {\n`; block += ` @notFile not file {path}\n`; block += ` rewrite @notFile /index.html\n`; block += ` file_server\n`; block += ` }\n`; return block; } /** * Generate Caddyfile for local mode (IP:Port, no TLS) */ generateCaddyfile(options) { const { dashboardPath, port = 8080, tier = 'basic', apiPort = DEFAULT_PORTS.API } = options; const adminPort = DEFAULT_PORTS.CADDY_ADMIN; let caddyfile = `# DashCaddy Caddyfile - Local Mode # Generated by DashCaddy Installer { admin localhost:${adminPort} { origins localhost localhost:${adminPort} host.docker.internal host.docker.internal:${adminPort} } auto_https off } `; if (tier !== 'basic') { caddyfile += this._corsSnippets() + '\n'; } caddyfile += `:${port} {\n`; caddyfile += this._dashboardBlock(dashboardPath, tier, apiPort); caddyfile += `}\n`; return caddyfile; } /** * Generate Caddyfile for public domain mode (Let's Encrypt TLS) */ generateCaddyfileWithDomain(options) { const { dashboardPath, domain, email = '', tier = 'basic', apiPort = DEFAULT_PORTS.API } = options; const adminPort = DEFAULT_PORTS.CADDY_ADMIN; let caddyfile = `# DashCaddy Caddyfile - Public Domain Mode # Generated by DashCaddy Installer { admin localhost:${adminPort} { origins localhost localhost:${adminPort} host.docker.internal host.docker.internal:${adminPort} } `; if (email) { caddyfile += ` email ${email}\n`; } caddyfile += `}\n\n`; if (tier !== 'basic') { caddyfile += this._corsSnippets() + '\n'; caddyfile += this._authSnippet(apiPort) + '\n'; } caddyfile += `${domain} {\n`; caddyfile += this._dashboardBlock(dashboardPath, tier, apiPort); caddyfile += `}\n`; return caddyfile; } /** * Generate Caddyfile for custom TLD mode (internal CA) */ generateCaddyfileCustomTLD(options) { const { dashboardPath, installPath, tld = '.home', caName = 'DashCaddy Local CA', tier = 'basic', apiPort = DEFAULT_PORTS.API } = options; const adminPort = DEFAULT_PORTS.CADDY_ADMIN; const dashboardDomain = `dashcaddy${tld}`; const certsPath = installPath ? this._p(path.join(installPath, 'certs')) : './certs'; let caddyfile = `# DashCaddy Caddyfile - Custom TLD Mode (${tld}) # Generated by DashCaddy Installer { storage file_system { root ${certsPath} } admin localhost:${adminPort} { origins localhost localhost:${adminPort} host.docker.internal host.docker.internal:${adminPort} } pki { ca local { name "${caName}" root_cn "${caName} Root CA" intermediate_cn "${caName} Intermediate CA" } } } `; if (tier !== 'basic') { caddyfile += this._corsSnippets() + '\n'; caddyfile += this._authSnippet(apiPort) + '\n'; } caddyfile += `# Dashboard - ${dashboardDomain}\n`; caddyfile += `${dashboardDomain} {\n`; caddyfile += ` tls internal\n\n`; caddyfile += this._dashboardBlock(dashboardPath, tier, apiPort); caddyfile += `}\n`; return caddyfile; } /** * Write Caddyfile to disk */ async writeCaddyfile(content, outputPath) { try { await fs.promises.writeFile(outputPath, content, 'utf8'); return { success: true, path: outputPath }; } catch (error) { return { success: false, error: error.message }; } } /** * Create complete Caddyfile setup * @param {string} installPath - Installation directory * @param {Object} options - Configuration options */ async createCaddyfileSetup(installPath, options = {}) { try { const dashboardPath = path.join(installPath, 'sites', 'status'); const caddyfilePath = path.join(installPath, 'Caddyfile'); let content; if (options.domainMode === 'public') { content = this.generateCaddyfileWithDomain({ dashboardPath, domain: options.publicDomain, email: options.email, tier: options.tier, apiPort: options.apiPort }); } else if (options.domainMode === 'custom-tld') { content = this.generateCaddyfileCustomTLD({ dashboardPath, installPath, tld: options.tld, caName: options.caName || 'DashCaddy Local CA', tier: options.tier, apiPort: options.apiPort }); } else { content = this.generateCaddyfile({ dashboardPath, ...options }); } const result = await this.writeCaddyfile(content, caddyfilePath); if (!result.success) { throw new Error(result.error); } return { success: true, path: caddyfilePath, content }; } catch (error) { return { success: false, error: error.message }; } } /** * Generate docker-compose.yml for running the API server. * * DISK SAFETY: Includes env vars for health retention, stats caps, and * memory limits derived from the disk budget the user selected during * install. These prevent the disk-explosion bugs seen in early versions. * * @param {string} installPath - Installation directory * @param {Object} options - Configuration options * @param {number} options.apiPort - API server port * @param {string} options.lanIP - Host LAN IP address * @param {string} options.tailscaleIP - Host Tailscale IP address * @param {string} options.domainMode - Domain mode (local, public, custom-tld) * @param {Object} [options.disk] - Disk budget settings * @param {number} [options.disk.healthRetentionDays=14] - Health history retention * @param {number} [options.disk.healthMaxEntries=500] - Max health entries per service * @param {number} [options.disk.healthCheckInterval=30000] - Health check interval (ms) * @param {number} [options.disk.statsMaxEntries=2000] - Max container stats entries * @param {number} [options.disk.auditMaxEntries=1000] - Max audit log entries * @param {number} [options.disk.backupLimitGB=10] - Backup storage limit * @param {string} [options.dockerDataPath] - Docker data root override * @param {number} [options.memoryLimitMB=1024] - Container memory limit */ generateDockerCompose(installPath, options = {}) { const apiPort = options.apiPort || DEFAULT_PORTS.API; const adminPort = DEFAULT_PORTS.CADDY_ADMIN; const p = this._p.bind(this); // Disk budget settings with safe defaults const disk = options.disk || {}; const healthRetentionDays = disk.healthRetentionDays || 14; const healthMaxEntries = disk.healthMaxEntries || 500; const healthCheckInterval = disk.healthCheckInterval || 30000; const statsMaxEntries = disk.statsMaxEntries || 2000; const auditMaxEntries = disk.auditMaxEntries || 1000; const backupLimitGB = disk.backupLimitGB || 10; const memoryLimitMB = options.memoryLimitMB || 1024; // Core volume mounts let volumes = ` - ${p(installPath)}/Caddyfile:/caddyfile:rw - ${p(installPath)}/services.json:/app/services.json:rw - ${p(installPath)}/dns-credentials.json:/app/dns-credentials.json:rw - ${p(installPath)}/config.json:/app/config.json:rw - ${p(installPath)}/credentials.json:/app/credentials.json:rw - ${p(installPath)}/.encryption-key:/app/.encryption-key:rw - ${p(installPath)}/notifications.json:/app/notifications.json:rw - ${p(installPath)}/sites/status/assets:/app/assets:rw - /var/run/docker.sock:/var/run/docker.sock`; // Add CA cert mount for custom-tld mode if (options.domainMode === 'custom-tld') { volumes += `\n - ${p(installPath)}/certs/pki/authorities/local:/app/pki:ro`; } // Environment variables — disk safety baked in let envVars = ` - CADDYFILE_PATH=/caddyfile - CADDY_ADMIN_URL=http://host.docker.internal:${adminPort} - ASSETS_PATH=/app/assets - CREDENTIALS_FILE=/app/credentials.json - NODE_ENV=production # --- Disk Safety --- - HEALTH_HISTORY_RETENTION=${healthRetentionDays} - HEALTH_MAX_ENTRIES=${healthMaxEntries} - HEALTH_CHECK_INTERVAL=${healthCheckInterval} - CONTAINER_STATS_MAX_ENTRIES=${statsMaxEntries} - AUDIT_MAX_ENTRIES=${auditMaxEntries} - BACKUP_MAX_STORAGE_BYTES=${backupLimitGB * 1024 * 1024 * 1024}`; if (options.domainMode === 'custom-tld') { envVars += `\n - CA_CERT_PATH=/app/pki/root.crt`; } if (options.lanIP) { envVars += `\n - HOST_LAN_IP=${options.lanIP}`; } if (options.tailscaleIP) { envVars += `\n - HOST_TAILSCALE_IP=${options.tailscaleIP}`; } const dockerCompose = `services: dashcaddy-api: build: . container_name: dashcaddy-api ports: - "${apiPort}:${apiPort}" volumes: ${volumes} environment: ${envVars} extra_hosts: - "host.docker.internal:host-gateway" restart: unless-stopped # Memory limit prevents OOM during startup when all managers init mem_limit: ${memoryLimitMB}m memswap_limit: ${(memoryLimitMB * 2)}m `; return dockerCompose; } /** * Write docker-compose.yml to disk * @param {string} installPath - Installation directory * @param {Object} options - Configuration options */ async createDockerCompose(installPath, options = {}) { try { const content = this.generateDockerCompose(installPath, options); const apiDir = path.join(installPath, 'sites', 'dashcaddy-api'); await fs.promises.mkdir(apiDir, { recursive: true }); const outputPath = path.join(apiDir, 'docker-compose.yml'); await fs.promises.writeFile(outputPath, content, 'utf8'); return { success: true, path: outputPath, content }; } catch (error) { return { success: false, error: error.message }; } } } module.exports = CaddyfileGenerator;