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

263 lines
10 KiB
Markdown

# 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