<# .SYNOPSIS DashCaddy Bootstrap Script - Runs after NSIS install to set up Docker, WSL, and services .DESCRIPTION This script handles the heavy lifting: 1. Installs Docker Desktop via winget (if not present) 2. Enables WSL2 (if needed, handles reboot) 3. Pulls DashCaddy Docker images 4. Starts services via docker compose 5. Registers auto-start 6. Creates config.yaml .PARAMETER Action Install (default), Stop, Start, Restart, Uninstall .EXAMPLE .\bootstrap.ps1 .\bootstrap.ps1 -Action Stop #> param( [ValidateSet('Install', 'Stop', 'Start', 'Restart', 'Uninstall')] [string]$Action = 'Install' ) $ErrorActionPreference = 'Stop' $ProgressPreference = 'SilentlyContinue' $InstallDir = $PSScriptRoot $AppDataDir = Join-Path $env:LOCALAPPDATA 'DashCaddy' $DataDir = Join-Path $AppDataDir 'data' $ConfigDir = Join-Path $AppDataDir 'config' $LogDir = Join-Path $AppDataDir 'logs' $ComposeFile = Join-Path $InstallDir 'docker-compose.yml' $ConfigFile = Join-Path $ConfigDir 'config.yaml' function Write-Log { param([string]$Message, [string]$Level = 'INFO') $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' $logMessage = "[$timestamp] [$Level] $Message" Write-Host $logMessage $logFile = Join-Path $LogDir "bootstrap-$(Get-Date -Format 'yyyyMMdd').log" Add-Content -Path $logFile -Value $logMessage } function Ensure-Directories { Write-Log "Creating directories..." @($DataDir, $ConfigDir, $LogDir) | ForEach-Object { if (-not (Test-Path $_)) { New-Item -ItemType Directory -Path $_ -Force | Out-Null Write-Log "Created: $_" } } } function Check-Docker { Write-Log "Checking Docker Desktop..." try { $dockerVersion = docker version --format '{{.Server.Version}}' 2>$null if ($dockerVersion) { Write-Log "Docker Desktop found: v$dockerVersion" return $true } } catch {} return $false } function Install-DockerDesktop { Write-Log "Installing Docker Desktop via winget..." # Check if winget is available if (-not (Get-Command winget -ErrorAction SilentlyContinue)) { Write-Log "winget not found, trying Microsoft Store..." 'WARN' # Fallback: direct download $dockerUrl = 'https://desktop.docker.com/win/main/amd64/Docker%20Desktop%20Installer.exe' $installerPath = Join-Path $env:TEMP 'DockerDesktopInstaller.exe' Invoke-WebRequest -Uri $dockerUrl -OutFile $installerPath Write-Log "Downloaded Docker Desktop installer" Start-Process -FilePath $installerPath -ArgumentList 'install', '--quiet' -Wait return } # Install via winget try { winget install --id Docker.DockerDesktop --accept-source-agreements --accept-package-agreements --silent Write-Log "Docker Desktop installed successfully" } catch { Write-Log "winget install failed, trying direct download..." 'WARN' $dockerUrl = 'https://desktop.docker.com/win/main/amd64/Docker%20Desktop%20Installer.exe' $installerPath = Join-Path $env:TEMP 'DockerDesktopInstaller.exe' Invoke-WebRequest -Uri $dockerUrl -OutFile $installerPath Start-Process -FilePath $installerPath -ArgumentList 'install', '--quiet' -Wait } } function Enable-WSL2 { Write-Log "Checking WSL2..." $wslStatus = wsl --status 2>&1 if ($wslStatus -match 'WSL 2') { Write-Log "WSL2 already enabled" return } Write-Log "Enabling WSL2..." try { # Enable WSL and Virtual Machine Platform dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart | Out-Null dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart | Out-Null # Set WSL2 as default wsl --set-default-version 2 | Out-Null # Install Ubuntu if not present if (-not (wsl -l -q | Where-Object { $_ -eq 'Ubuntu' })) { Write-Log "Installing Ubuntu..." wsl --install -d Ubuntu | Out-Null } Write-Log "WSL2 enabled. A reboot is required." $global:RebootRequired = $true } catch { Write-Log "Failed to enable WSL2: $_" 'ERROR' throw } } function Ensure-DockerRunning { Write-Log "Ensuring Docker is running..." $maxAttempts = 30 $attempt = 0 while ($attempt -lt $maxAttempts) { if (Check-Docker) { Write-Log "Docker is running" return } $attempt++ Write-Log "Waiting for Docker... (attempt $attempt/$maxAttempts)" Start-Sleep -Seconds 5 } throw "Docker failed to start after $maxAttempts attempts" } function Create-DockerCompose { Write-Log "Creating docker-compose.yml..." # Read DNS provider from config $dnsProvider = "coredns" if (Test-Path $ConfigFile) { $config = Get-Content $ConfigFile | ForEach-Object { if ($_ -match '^(\w+):\s*(.*)$') { @{$matches[1] = $matches[2].Trim()} } } | ForEach-Object { $_ } if ($config.ContainsKey("dns_provider")) { $dnsProvider = $config["dns_provider"] } } $composeContent = @" version: '3.8' services: dashcaddy-api: image: dashcaddy/dashcaddy-api:latest container_name: dashcaddy-api restart: unless-stopped ports: - "127.0.0.1:3001:3001" volumes: - ${DataDir}:/opt/dashcaddy/data - ${AppDataDir}/config.yaml:/opt/dashcaddy/config.yaml:ro - //./pipe/docker_engine:/var/run/docker.sock:ro environment: - NODE_ENV=production - DATA_DIR=/opt/dashcaddy/data - CONFIG_FILE=/opt/dashcaddy/config.yaml healthcheck: test: ["CMD", "node", "-e", "require('http').get('http://localhost:3001/health', (r) => { process.exit(r.statusCode === 200 ? 0 : 1); }).on('error', () => process.exit(1))"] interval: 30s timeout: 10s retries: 3 start_period: 15s networks: - dashcaddy-net caddy: image: caddy:2.10-alpine container_name: dashcaddy-caddy restart: unless-stopped ports: - "80:80" - "443:443" volumes: - ${DataDir}/caddy/Caddyfile:/etc/caddy/Caddyfile:ro - ${DataDir}/caddy/data:/data - ${DataDir}/caddy/config:/config networks: - dashcaddy-net depends_on: - dashcaddy-api $(Get-DnsServiceCompose $dnsProvider $DataDir) networks: dashcaddy-net: driver: bridge "@ $composeContent | Set-Content -Path $ComposeFile -Encoding UTF8 Write-Log "docker-compose.yml created at $ComposeFile (DNS: $dnsProvider)" } function Get-DnsServiceCompose { param( [string]$Provider, [string]$DataDir ) switch ($Provider.ToLower()) { "technitium" { return @" technitium: image: technitium/dns-server:latest container_name: dashcaddy-technitium restart: unless-stopped ports: - "53:53/udp" - "53:53/tcp" - "5380:5380" volumes: - ${DataDir}/technitium:/etc/dns environment: - DNS_SERVER_DOMAIN=local networks: - dashcaddy-net " } "coredns" { return @" coredns: image: coredns/coredns:latest container_name: dashcaddy-coredns restart: unless-stopped ports: - "53:53/udp" - "53:53/tcp" volumes: - ${DataDir}/coredns/Corefile:/etc/coredns/Corefile:ro networks: - dashcaddy-net " } "cloudflare" { return "" # Cloudflare is external, no local container needed } "route53" { return "" # Route53 is external, no local container needed } default { return @" coredns: image: coredns/coredns:latest container_name: dashcaddy-coredns restart: unless-stopped ports: - "53:53/udp" - "53:53/tcp" volumes: - ${DataDir}/coredns/Corefile:/etc/coredns/Corefile:ro networks: - dashcaddy-net " } } } function Create-Config { Write-Log "Creating config.yaml..." if (-not (Test-Path $ConfigFile)) { $configContent = @" domain: local email: admin@local docker_data: $DataDir dns_provider: coredns auto_start: true auto_update: false "@ $configContent | Set-Content -Path $ConfigFile -Encoding UTF8 Write-Log "config.yaml created" } } function Create-Caddyfile { Write-Log "Creating Caddyfile..." $caddyDir = Join-Path $DataDir 'caddy' if (-not (Test-Path $caddyDir)) { New-Item -ItemType Directory -Path $caddyDir -Force | Out-Null } $caddyfilePath = Join-Path $caddyDir 'Caddyfile' if (-not (Test-Path $caddyfilePath)) { $caddyContent = @" { admin :2019 email admin@local } # Dashboard status.local { reverse_proxy dashcaddy-api:3001 tls internal } # API api.local { reverse_proxy dashcaddy-api:3001 tls internal } # Catch-all for other services (configured via API) import dashcaddy_services "@ $caddyContent | Set-Content -Path $caddyfilePath -Encoding UTF8 Write-Log "Caddyfile created" } } function Create-Corefile { Write-Log "Creating Corefile..." $corednsDir = Join-Path $DataDir 'coredns' if (-not (Test-Path $corednsDir)) { New-Item -ItemType Directory -Path $corednsDir -Force | Out-Null } $corefilePath = Join-Path $corednsDir 'Corefile' if (-not (Test-Path $corefilePath)) { $corefileContent = @" .:53 { forward . 1.1.1.1 8.8.8.8 log errors cache 30 } local:53 { file /etc/coredns/db.local log errors } "@ $corefileContent | Set-Content -Path $corefilePath -Encoding UTF8 Write-Log "Corefile created" } } function Create-TechnitiumConfig { Write-Log "Creating Technitium config..." $techDir = Join-Path $DataDir 'technitium' if (-not (Test-Path $techDir)) { New-Item -ItemType Directory -Path $techDir -Force | Out-Null } # Technitium uses a config file - minimal setup, most config via API Write-Log "Technitium config directory created" } function Register-AutoStart { Write-Log "Registering auto-start..." $runKey = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Run' $appPath = Join-Path $InstallDir 'DashCaddy.exe' Set-ItemProperty -Path $runKey -Name 'DashCaddy' -Value "`"$appPath`" --minimized" -Force Write-Log "Auto-start registered" } function Unregister-AutoStart { Write-Log "Unregistering auto-start..." $runKey = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Run' Remove-ItemProperty -Path $runKey -Name 'DashCaddy' -ErrorAction SilentlyContinue Write-Log "Auto-start unregistered" } function Pull-Images { Write-Log "Pulling Docker images..." # Read DNS provider to know which images to pull $dnsProvider = "coredns" if (Test-Path $ConfigFile) { $config = Get-Content $ConfigFile | ForEach-Object { if ($_ -match '^(\w+):\s*(.*)$') { @{$matches[1] = $matches[2].Trim()} } } | ForEach-Object { $_ } if ($config.ContainsKey("dns_provider")) { $dnsProvider = $config["dns_provider"] } } $images = @( 'dashcaddy/dashcaddy-api:latest', 'caddy:2.10-alpine' ) # Add DNS provider image switch ($dnsProvider.ToLower()) { "technitium" { $images += 'technitium/dns-server:latest' } "coredns" { $images += 'coredns/coredns:latest' } default { $images += 'coredns/coredns:latest' } } foreach ($image in $images) { Write-Log "Pulling $image..." docker pull $image 2>&1 | ForEach-Object { Write-Log $_ 'DEBUG' } } Write-Log "All images pulled" } function Start-Services { Write-Log "Starting services..." Ensure-DockerRunning Create-DockerCompose Create-Config Create-Caddyfile # Create DNS-specific configs $dnsProvider = "coredns" if (Test-Path $ConfigFile) { $config = Get-Content $ConfigFile | ForEach-Object { if ($_ -match '^(\w+):\s*(.*)$') { @{$matches[1] = $matches[2].Trim()} } } | ForEach-Object { $_ } if ($config.ContainsKey("dns_provider")) { $dnsProvider = $config["dns_provider"] } } switch ($dnsProvider.ToLower()) { "coredns" { Create-Corefile } "technitium" { Create-TechnitiumConfig } default { Create-Corefile } } Pull-Images Write-Log "Running docker compose up..." docker compose -f $ComposeFile up -d 2>&1 | ForEach-Object { Write-Log $_ 'DEBUG' } # Wait for health Write-Log "Waiting for services to be healthy..." $maxWait = 120 $waited = 0 while ($waited -lt $maxWait) { $status = docker compose -f $ComposeFile ps --format json 2>$null | ConvertFrom-Json $healthy = $status | Where-Object { $_.Health -eq 'healthy' -or $_.State -eq 'running' } if ($healthy.Count -eq $status.Count) { Write-Log "All services healthy" break } Start-Sleep -Seconds 5 $waited += 5 } Register-AutoStart Write-Log "Services started successfully" } function Stop-Services { Write-Log "Stopping services..." if (Test-Path $ComposeFile) { docker compose -f $ComposeFile down 2>&1 | ForEach-Object { Write-Log $_ 'DEBUG' } } Unregister-AutoStart Write-Log "Services stopped" } function Restart-Services { Stop-Services Start-Services } # ─── Main ─── Write-Log "=== DashCaddy Bootstrap Started (Action: $Action) ===" Ensure-Directories switch ($Action) { 'Install' { if (-not (Check-Docker)) { Install-DockerDesktop } Enable-WSL2 if ($global:RebootRequired) { Write-Log "REBOOT REQUIRED - WSL2 was enabled. Please reboot and run bootstrap again." 'WARN' Write-Host "" Write-Host "==========================================" -ForegroundColor Yellow Write-Host " REBOOT REQUIRED" -ForegroundColor Yellow Write-Host "==========================================" -ForegroundColor Yellow Write-Host "WSL2 was enabled. Please reboot your computer" Write-Host "and then run DashCaddy from Start Menu." Write-Host "==========================================" -ForegroundColor Yellow exit 0 } Start-Services } 'Stop' { Stop-Services } 'Start' { Start-Services } 'Restart' { Restart-Services } 'Uninstall' { Stop-Services } } Write-Log "=== DashCaddy Bootstrap Completed ==="