# 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`: ```javascript // 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 ```dockerfile # .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) ```yaml # .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 ```javascript // 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 ```javascript // 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 ```bash # 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 ```yaml # .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