Files
dashcaddy/installer/windows/bootstrap.ps1
T
Hermes 86e4c9fc81 wip: Windows desktop app scaffold (WinUI 3/.NET 8) + NSIS installer + docs
Owner decision pending (STATE.md DC-100 tick): adopt/ship/park.
Preserved from fragile git stash to named branch 2026-08-23.
NOTE: requires a Windows build machine (WinUI XAML compiler + MSIX do not
cross-build on Linux) — see WINDOWS_APP_BUILD.md in this tree.
Secret-scanned clean 2026-08-23 (no keys/tokens/PEM in tree).
2026-08-22 23:50:34 -07:00

532 lines
15 KiB
PowerShell

<#
.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 ==="