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).
263 lines
10 KiB
Markdown
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 |