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).
10 KiB
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.ymlwith 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 viapkgor native) - Alternative: Use
node:20-nanoserver-ltsc2022but it's large (~2GB) - Recommended: Build Node.js app with
pkginto single.exe, run in minimal Windows container - Caddy Windows binary:
caddy_windows_amd64.exedownloaded 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
supervisordors6) - 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.jsnormalizes to POSIX internally
Permissions
- No
chmod/chownon Windows — rely on Docker volume permissions - Encryption key file:
icaclsto restrict toSYSTEM+Administrators(installer handles)
Networking
host.docker.internalworks 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) vshttp://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
- Add platform to
platform-paths.js(base paths +isXYZflag) - Add
--platformto buildx command - Add CI job for that platform
- Test installer script on that platform
- Update
INSTALL.mdand this doc