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).
This commit is contained in:
@@ -0,0 +1,263 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user