Files
dashcaddy/CROSS-PLATFORM.md
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

10 KiB

DashCaddy — Cross-Platform Architecture

Design Principle

Single codebase, single container image, runs everywhere.

  • One Dockerfile → multi-arch image (linux/amd64, linux/arm64, windows/amd64)
  • One docker-compose.yml with profiles → dev / prod / windows
  • One config.yaml → all runtime configuration
  • Platform-specific paths resolved at runtime via platform-paths.js

Platform Matrix

Feature Linux (DNS2, VPS, Raspberry Pi) macOS (Intel/ARM) Windows (WSL2) Windows (Native Containers)
Docker Engine Native Docker Desktop / Colima Docker Desktop (WSL2 backend) Docker Engine (Windows containers)
Caddy Native (systemd) Native (launchd) Inside WSL2 container Native Windows binary
Data Directory /opt/dashcaddy/data ~/dockerdata/dashcaddy /mnt/e/dockerdata/dashcaddy (or E:\dockerdata\dashcaddy) E:\dockerdata\dashcaddy
Caddy Config /etc/dashcaddy/Caddyfile ~/dockerdata/dashcaddy/caddy/Caddyfile /mnt/e/dockerdata/dashcaddy/caddy/Caddyfile E:\dockerdata\dashcaddy\caddy\Caddyfile
Tailscale Native Native Native (Windows) or WSL2 Native Windows
DNS (CoreDNS) Native container Native container WSL2 container Windows container (limited)

Path Resolution Strategy

All paths flow through platform-paths.js:

// platform-paths.js — single source of truth
const paths = {
  // Base dirs (env-overridable)
  caddyBase: process.env.CADDY_BASE || (isWindows ? 'C:/caddy' : '/etc/dashcaddy'),
  dockerData: process.env.DOCKER_DATA || (isWindows ? 'E:/dockerdata' : '/opt/dockerdata'),
  
  // Derived paths
  servicesFile: process.env.SERVICES_FILE || path.join(paths.caddyBase, 'services.json'),
  dataDir: process.env.DATA_DIR || path.dirname(paths.servicesFile),
  
  // Container paths (fixed inside container)
  containerUpdatesDir: '/app/updates',
  containerFrontendDir: '/app/dashboard',
  containerAssetsDir: '/app/assets',
};

Rule: No hardcoded paths in application code. Ever.

Docker Multi-Arch Build

# .dockerignore excludes: node_modules, .git, dist, *.log, .env*, coverage, *.md
# Buildx command:
# docker buildx build --platform linux/amd64,linux/arm64,windows/amd64 \
#   -t dashcaddy/dashcaddy-api:latest --push .

Windows Container Specifics

  • Base image: mcr.microsoft.com/windows/servercore:ltsc2022 (for Caddy) + mcr.microsoft.com/dotnet/runtime:8.0-nanoserver-ltsc2022 (for Node.js via pkg or native)
  • Alternative: Use node:20-nanoserver-ltsc2022 but it's large (~2GB)
  • Recommended: Build Node.js app with pkg into single .exe, run in minimal Windows container
  • Caddy Windows binary: caddy_windows_amd64.exe downloaded at build time

Build Pipeline (GitHub Actions)

# .github/workflows/docker.yml
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: docker/setup-buildx-action@v3
      - uses: docker/build-push-action@v5
        with:
          platforms: linux/amd64,linux/arm64,windows/amd64
          push: true
          tags: dashcaddy/dashcaddy-api:${{ github.sha }}

Runtime Platform Detection

// In any module:
const { isWindows, isLinux, dataDir, resolveAssetsPath } = require('./platform-paths');

// Writing runtime data:
const fs = require('fs');
const logFile = path.join(dataDir, 'audit-log.json');
fs.writeFileSync(logFile, JSON.stringify(entry));

// Reading assets:
const assetPath = resolveAssetsPath(process.env.ASSETS_DIR);

Data Persistence Guarantees

Platform Data Location Survives Recreate?
Linux /opt/dashcaddy/data (bind mount) Yes
macOS ~/dockerdata/dashcaddy (bind mount) Yes
Windows WSL2 /mnt/e/dockerdata/dashcaddy (bind mount) Yes
Windows Native E:\dockerdata\dashcaddy (bind mount) Yes

Critical: platform-paths.assertSafe() runs at startup in production mode. If dataDir resolves to an image-layer path (e.g., /app/src), container refuses to start with clear error.

Caddy Integration

Linux/macOS/WSL2

  • Caddy runs inside the DashCaddy container (single container, multiple processes via supervisord or s6)
  • OR: Caddy runs on host, DashCaddy API in container (current DNS2 model)
  • Recommended for v2: Single container with s6-overlay — simpler, atomic deploys

Windows Native

  • Caddy runs as Windows service (NSSM) or inside container
  • DashCaddy API runs in Windows container
  • Shared volume: E:\dockerdata\dashcaddy\caddy\Caddyfile

DNS Provider Abstraction

// src/dns/providers/index.js
const providers = {
  coredns: require('./coredns'),
  technitium: require('./technitium'),
  cloudflare: require('./cloudflare'),
  route53: require('./route53'),
  // Add new providers here — no other code changes
};

module.exports = function getProvider(name) {
  const p = providers[name];
  if (!p) throw new Error(`Unknown DNS provider: ${name}`);
  return p;
};

Config-driven: config.yaml → dns.provider: "coredns"

Tailscale Integration

Platform Method
Linux tailscale up in container (needs NET_ADMIN + /dev/net/tun)
macOS Host Tailscale + host.docker.internal
Windows WSL2 Host Tailscale (Windows) + WSL2 auto-proxy
Windows Native tailscale.exe in container (Windows container)

Unified approach: Tailscale runs on host, containers reach it via host.docker.internal:PORT or Tailscale IP. No container-side Tailscale needed.

Windows-Specific Considerations

File System

  • Use E:/dockerdata (network share) for all persistent data
  • C: drive only for Docker Desktop WSL VHD (C:/dockerdata/DockerDesktopWSL/)
  • Path separator: platform-paths.js normalizes to POSIX internally

Permissions

  • No chmod/chown on Windows — rely on Docker volume permissions
  • Encryption key file: icacls to restrict to SYSTEM + Administrators (installer handles)

Networking

  • host.docker.internal works on Docker Desktop (Windows/macOS)
  • On Linux: --add-host=host.docker.internal:host-gateway (Docker 20.04+)
  • Caddy admin API: http://host.docker.internal:2019 (Windows/macOS) vs http://localhost:2019 (Linux)

Testing Cross-Platform

# Local multi-arch test (requires buildx + qemu)
docker run --rm --platform linux/amd64 dashcaddy/dashcaddy-api:latest node -e "console.log('amd64 ok')"
docker run --rm --platform linux/arm64 dashcaddy/dashcaddy-api:latest node -e "console.log('arm64 ok')"
# Windows: requires Windows runner (GitHub Actions windows-latest)

# Integration test matrix (run in CI)
# - Linux: full stack (Caddy + API + Dashboard + CoreDNS)
# - Windows WSL2: same stack inside Ubuntu WSL
# - Windows Native: API + Caddy in Windows containers (limited DNS)

Migration Path (Current → Unified)

Current Target
/opt/dashcaddy/start.sh docker compose --profile prod up -d
Multiple JSON configs (services.json, config.json, dns-credentials.json) Single config.yaml
Manual Caddyfile edit + caddy-apply Auto-generated from config.yaml + services.json
platform-paths.js with hardcoded fallbacks Pure env-driven, no fallbacks to image-layer paths
Custom esbuild + manual node build.js Vite (frontend) + tsc/esbuild (backend)
Separate installer repo (dashcaddy-installer) Single repo, install.sh / install.ps1 at root

Environment Variable Reference

Variable Description Default (Linux) Default (Windows)
CADDY_BASE Caddy config root /etc/dashcaddy C:/caddy
DOCKER_DATA Docker volumes root /opt/dockerdata E:/dockerdata
SERVICES_FILE Services JSON path /etc/dashcaddy/services.json C:/caddy/services.json
DATA_DIR Runtime data dir /opt/dashcaddy/data E:/dockerdata/dashcaddy
CONFIG_FILE Main config /opt/dashcaddy/data/config.json E:/dockerdata/dashcaddy/config.json
CADDY_ADMIN_URL Caddy API endpoint http://localhost:2019 http://host.docker.internal:2019
DASHCADDY_UPDATES_DIR In-container updates /app/updates /app/updates
DASHCADDY_FRONTEND_DIR In-container dashboard /app/dashboard /app/dashboard
ASSETS_DIR In-container assets /app/assets /app/assets
SKIP_DATA_DIR_GUARD Bypass safety check 0 0 (dev only)
NODE_ENV production | development production production

CI/CD Pipeline

# .github/workflows/ci.yml
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node: [20, 22]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: ${{ matrix.node }} }
      - run: npm ci
      - run: npm run lint
      - run: npm run test:ci
  
  build-frontend:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
      - run: cd status && npm ci && npm run build
      - uses: actions/upload-artifact@v4
        with: { name: dashboard-dist, path: status/dist/ }
  
  docker:
    needs: [test, build-frontend]
    runs-on: ubuntu-latest
    steps:
      - uses: docker/setup-buildx-action@v3
      - uses: docker/build-push-action@v5
        with:
          platforms: linux/amd64,linux/arm64
          push: ${{ github.event_name == 'push' }}
          tags: dashcaddy/dashcaddy-api:${{ github.sha }}
  
  windows-build:
    needs: test
    runs-on: windows-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build Windows container
        run: |
          docker build -f Dockerfile.windows -t dashcaddy/dashcaddy-api:${{ github.sha }}-windows .

Quick Reference: Adding a New Platform

  1. Add platform to platform-paths.js (base paths + isXYZ flag)
  2. Add --platform to buildx command
  3. Add CI job for that platform
  4. Test installer script on that platform
  5. Update INSTALL.md and this doc