Committed by Hermes autonomous QA sprint 2026-08-13. These files were modified during the Aug 12 sprint but never committed.
12 KiB
DashCaddy Project Guidelines for AI Assistants
HARD RULE: Docker Storage on E: Drive
ALL Docker container data, volumes, bind mounts, and app configs MUST use E:/dockerdata/ via bind mounts or CIFS volumes. No exceptions.
- E: is a network share (
\\Sami-pc\e_share) shared across all home network computers - The ONLY thing allowed on C: is the Docker Desktop WSL engine VHD (
C:/dockerdata/DockerDesktopWSL/) — this is the absolute bare minimum WSL2 requires (local NTFS). WSL2 cannot create VHDs on network shares. - Keep C: Docker usage under 5GB
- When deploying new containers, always use
E:/dockerdata/<app-name>/for bind mount paths - For CIFS volumes in docker-compose, use
//Sami-pc/e_share/dockerdata/...as the device path
CRITICAL: Production is on DNS2 (not this machine)
DashCaddy runs on DNS2 (100.121.150.22 via Tailscale / 194.233.88.206 public).
SSH in with: ssh root@100.121.150.22
Production Layout on DNS2
/opt/dashcaddy/ # git repo (auto-updated)
├── dashcaddy-api/
│ ├── *.js # API server source
│ └── data/
│ ├── services.json # LIVE services list
│ ├── config.json # LIVE DashCaddy config
│ ├── dns-credentials.json # DNS API credentials
│ └── credentials.json # Encrypted app credentials
├── status/ # Dashboard frontend (built)
│ ├── index.html
│ ├── dist/ # Bundled JS (core/features/onboarding/init)
│ ├── js/ # Source JS (also served statically)
│ ├── css/
│ └── assets/
├── ca/ # DashCA static site
├── updates/ # Auto-updater staging + history
└── start.sh # Container launch script (run by @reboot cron)
Docker Container
- Name:
dashcaddy-api - Image:
dashcaddy-dashcaddy-api:latest - Port:
127.0.0.1:3001(Caddy proxies to it) - Started by:
/opt/dashcaddy/start.shvia root@rebootcron
Key container mounts:
| Container path | Host path |
|---|---|
/app/data/ |
/opt/dashcaddy/dashcaddy-api/data/ |
/app/assets |
/opt/dashcaddy/status/assets |
/caddyfile |
/etc/caddy/Caddyfile |
/app/backups |
/opt/dashcaddy/backups |
Caddy
- Config:
/etc/caddy/Caddyfile(git-guarded — edit then runcaddy-apply) - Admin API:
http://localhost:2019(NOT 2021) - TLS storage:
/var/lib/caddy/ - Static files: Caddy serves
/opt/dashcaddy/status/forstatus.sami
Development Files (for editing)
e:/CaddyCerts/sites/
├── dashcaddy-api/ # API server source (NOT caddy-api/)
│ ├── server.js
│ ├── src/app.js # Express app factory
│ ├── routes/ # Route handlers
│ ├── middleware.js
│ └── ...
└── status/ # Dashboard frontend source
├── index.html # HTML template (~853 lines)
├── js/ # Source JS modules
├── css/
├── dist/ # Built output (run node build.js)
└── build.js # Build script (uses esbuild)
When Making Changes
To add/remove services from dashboard:
Edit /opt/dashcaddy/dashcaddy-api/data/services.json on DNS2 directly,
OR use the dashboard UI at https://status.sami.
To modify Caddy reverse proxy rules:
ssh root@100.121.150.22
# Edit /etc/caddy/Caddyfile
caddy-apply "reason for change" # validates + reloads + git commits
To modify API server code:
- Edit
e:/CaddyCerts/sites/dashcaddy-api/locally scpchanged files toroot@100.121.150.22:/opt/dashcaddy/dashcaddy-api/- Rebuild container:
ssh root@100.121.150.22 "bash /opt/dashcaddy/start.sh"
To modify dashboard frontend:
- Edit source in
e:/CaddyCerts/sites/status/js/orstatus/index.html - Build:
cd e:/CaddyCerts/sites/status && node build.js - Deploy:
scp -r dist/ index.html sw.js root@100.121.150.22:/opt/dashcaddy/status/
To modify DashCA:
Edit files in e:/CaddyCerts/sites/ca/, then:
- Regenerate:
cd e:/CaddyCerts/sites/ca/scripts && bash generate-all.sh - Deploy:
scp -r e:/CaddyCerts/sites/ca/* root@100.121.150.22:/opt/dashcaddy/ca/
DashCA - Certificate Authority Distribution
Purpose: One-click CA cert install page so *.sami domains are trusted on all devices.
Access: https://ca.sami
Certificate Info:
- CN: Sami Home Network Root CA
- Algorithm: ECDSA P-256 with SHA-256
- Valid Until: Dec 22, 2034
- Fingerprint:
08:98:A5:63:F5:A1:A2:58:5F:02:D7:A8:A2:54:87:E6:BC:33:96:21:29:0E
Certificate Source (on DNS2):
- Root CA:
/etc/ssl/sami-ca/root.crt - Intermediate CA: auto-generated by Caddy at
/var/lib/caddy/pki/authorities/local/
API Endpoints
GET /api/ca/info— certificate metadataGET /api/health/ca— CA expiration health (healthy/warning/critical)
Key Services
| Service | Where | Port | Notes |
|---|---|---|---|
| Caddy (HTTPS) | DNS2 | 443 | Reverse proxy |
| Caddy Admin | DNS2 | 2019 | Caddy API |
| DashCaddy API | DNS2 | 3001 | Dashboard backend (container) |
| Technitium DNS (primary) | DNS2 | 5380 | 100.121.150.22 |
| Technitium DNS (secondary) | DNS1 (this PC) | 5380 | 100.71.97.12 |
SSO Architecture
import dashcaddy_auth <serviceId> in the Caddyfile expands to a forward_auth gate that:
- Checks the DashCaddy TOTP session (cookie domain
.sami— shared across all*.sami) - Injects credentials (API key, Basic Auth, app cookies) into upstream request headers
For client-side auto-login (chat, Plex, Jellyfin, Emby):
- Caddy redirects
path /to/dashcaddy-login /dashcaddy-loginproxies toGET /api/v1/auth/login-page?service=<id>on the API- That page's JS fetches
/dashcaddy-api/api/auth/app-token/<id>and stores the token inlocalStorage
Common Mistakes to Avoid
- Wrong API source dir: It's
dashcaddy-api/, NOTcaddy-api/(old name, no longer exists) - Wrong services file: Edit the one in
/opt/dashcaddy/dashcaddy-api/data/on DNS2, not the dev copy - Caddyfile edits without caddy-apply: Always use
caddy-apply— it validates, reloads, and git-commits - Caddy admin port: It's 2019, not 2021
- Frontend changes without build: Edit JS source, then
node build.js, then deploydist/ - DNS2 Tailscale IP:
100.121.150.22(NOT the old100.104.4.5or100.74.102.61)
Linux Deployment (DNS2 / Contabo VPS)
The Windows path sections above describe the SAMI-PC deployment. DashCaddy also runs as a Docker container on Linux (DNS2 = 194.233.88.206 / Tailscale 100.121.150.22). The Linux deployment uses a different layout driven by start.sh and docker run bind mounts.
Production paths (Linux)
/opt/dashcaddy/
├── dashcaddy-api/ # Built image source (rebuilt on update)
│ ├── Dockerfile
│ └── ...
├── status/ # Dashboard frontend SOURCE (build context)
├── credentials.json # Encrypted credentials (mounted to /app/data)
├── .encryption-key # AES key (mounted to /app/data)
└── services.json # Live service list (mounted to /app/data)
/var/www/dashcaddy-status/ # Dashboard frontend LIVE (served by Caddy)
# Built bundle output from status/ — NOT the source
# tree, NOT the docker build context
/etc/dashcaddy/
└── Caddyfile # Active Caddy configuration
/root/.dashcaddy/ # Per-user state, credentials backup, license
Container mount points (Linux)
| Container path | Host path |
|---|---|
/app/data/credentials.json |
/opt/dashcaddy/credentials.json |
/app/data/.encryption-key |
/opt/dashcaddy/.encryption-key |
/app/data/services.json |
/opt/dashcaddy/services.json |
/caddyfile |
/etc/dashcaddy/Caddyfile |
Note: the app must auto-resolve both /app/data/... AND the older /app/... layout (where files mounted directly to /app/). The credential-manager.js and crypto-utils.js modules handle this fallback. This is intentional — fresh installs get /app/data/, legacy installs keep working without env-var overrides.
Three-filesystem frontend trap (Linux)
The dashboard frontend lives on three separate paths that get confused:
- Source —
/opt/dashcaddy/status/— what you edit - Live —
/var/www/dashcaddy-status/— what Caddy serves to browsers - Build context —
/opt/dashcaddy/dashcaddy-api/— whatdocker builduses
Editing /opt/dashcaddy/status/index.html and restarting the container does nothing visible until you run the build (which writes to /var/www/dashcaddy-status/). Always rebuild + container-recreate together. See the dashcaddy skill § Deploy cycle for the exact sequence.
Common commands (Linux)
# Edit Caddyfile then reload (no restart needed)
curl -X POST http://localhost:2019/load \
-H "Content-Type: text/caddyfile" \
--data-binary @/etc/dashcaddy/Caddyfile
# View container logs
docker logs dashcaddy-api --tail 200
# Rebuild + restart after API code change
cd /opt/dashcaddy && git pull
cd /opt/dashcaddy/dashcaddy-api && docker build -t dashcaddy-api:local .
docker stop dashcaddy-api && docker rm dashcaddy-api
# (then re-run the container with the mount table above)
# Edit a service in the live list
vi /opt/dashcaddy/services.json # live-reloaded by the watcher
Differences from Windows
| Concern | Windows (SAMI-PC) | Linux (DNS2) |
|---|---|---|
| Drive letter | C:/, E:/ |
/opt/, /etc/, /var/www/ |
| Network share for state | \\Sami-pc\e_share |
(none — all local) |
| Docker engine | Docker Desktop on WSL2 | Docker Engine on host |
| Backend admin | PowerShell | bash + curl |
| Caddyfile reload | POST to localhost:2019/load |
POST to localhost:2019/load (same) |
| Caddy admin port | 2019 | 2019 |
| Self-update | host-side PowerShell updater | host-side bash updater (start.sh) |
| Tailscale | Same 100.x.x.x magic DNS |
Same |
| DNS server | DNS2 (100.74.102.61) primary | DNS2 (100.121.150.22 / 194.233.88.206) — is the primary |
Linux-specific gotchas
- Caddy needs
network_mode: host(or--network host) so it can bind :80 and :443 directly. Bridge mode + port mapping also works, butnetwork_mode: hostis simpler for a single-host setup. credentials.jsonpermissions matter — file mode0600, owned by the same UID the container runs as. If the host root creates it but the container runs asnode(uid 1000), the API will fail to read it. Eitherchown 1000:1000or run the container as--user 0.- Don't use
localhostin the API's CORS_ORIGINS — it conflicts with the Tailscale IP. Use the actualhttps://dashcaddy<your-tld>URL. - Tailscale cert provisioning — set
TS_AUTHKEYin/etc/dashcaddy/tailscale.env(mode 0600) before first start. Without it, the magic DNS hostname will resolve but TLS will fail.
Project Info
- Name: DashCaddy
- Version: 1.15.0 (current; CHANGELOG.md
[Unreleased]tracks the next bump) - Purpose: Unified management for Docker + Caddy + DNS
- Local TLD (Windows):
.sami - Local TLD (Linux, DNS2):
.home(default; configurable viasiteConfig.tld) - Repo:
/opt/dashcaddy/on DNS2 (git, auto-updated by self-updater)