# sparkDash — remote-monitoring deployment (runs on a Raspberry Pi, watches a # remote DGX Spark over SSH). This is NOT the upstream compose file: the # upstream one runs sparkDash *on* the Spark and needs privileged/pid:host plus # nvidia-smi bind mounts, none of which exist or make sense on a Pi. # Recover the original any time with: git show HEAD:docker-compose.yml services: sparkdash: build: context: . dockerfile: Dockerfile platforms: - linux/arm64 image: sparkdash:local container_name: sparkDash # Run as the host user that owns ./config — set this to your own # `id -u`:`id -g`, e.g. in a docker-compose.override.yml. # Required: with `cap_drop: ALL` below, root in the container loses # CAP_DAC_OVERRIDE and cannot create files in a dir it does not own — # that is what causes "EACCES ... /app/config/.secrets-key". # Bonus: files written into ./config end up owned by you, not root. user: "${SPARKDASH_UID:-1000}:${SPARKDASH_GID:-1000}" # unless-stopped: starts on boot, but STAYS DOWN after `docker compose stop` # / `down` until you bring it back. `always` would resurrect it on reboot. restart: unless-stopped # Bridge network + one published port. No host network, no privileged, no # host PID namespace — the Pi is only an SSH/HTTP client here. # Trade-off: Wake-on-LAN does NOT work from a bridge network (magic packets # are broadcast and get dropped by NAT). If you want the Wake button, swap # the `ports:` block for `network_mode: host`. # Second trade-off, for monitoring this Pi itself: interface counters come # from /sys (accurate), but sysfs carries no IP addresses, so the Network # panel shows "—" for the IP column. `network_mode: host` fills those in. ports: - "5555:5555" # mDNS (.local) does not resolve inside containers — glibc in the image has # no nss-mdns and Docker's DNS will not do multicast. Pin the Spark here so # you can still type the hostname in the UI (nicer "Open ComfyUI" links, # which resolve in your *browser*, where mDNS does work). # If the Spark's IP changes, update this line and `docker compose up -d`. # A DHCP reservation on your router avoids that entirely. # Put your own mapping in a docker-compose.override.yml (gitignored): # services: # sparkdash: # extra_hosts: # - "my-spark.local:192.168.1.100" volumes: # Config + encrypted secrets (sparks.json, sparks-secrets.json, # .secrets-key) survive restarts. - ./config:/app/config # No SSH key is mounted on purpose — auth is username/password, entered in # the UI (Add/Edit Spark → auth "password"). sshpass ships in the image; # the password is stored AES-256-GCM encrypted in # config/sparks-secrets.json and is never returned by the API. # Note the decryption key lives beside it at config/.secrets-key, so root # on this Pi can recover the password. See SPARKDASH_SECRETS_KEY below. # Host filesystem, read-only — needed to monitor *this* Pi (add it in the # UI with device type "Raspberry Pi" and "This host" checked). All three # are world-readable, so this works with `cap_drop: ALL` and uid 1000: # /proc → CPU usage, load, meminfo, per-process CPU/RSS # /sys → SoC temperature, clock, fan RPM, under-voltage alarm, NIC counters # / → statfs for real host free space (rbind, so submounts follow) # Drop these three lines if you only want to watch remote hosts. - /proc:/host/proc:ro - /sys:/host/sys:ro - /:/host/root:ro environment: - BIND_HOST=0.0.0.0 - PORT=5555 - NODE_ENV=production # uid 1000 has no home in the image. ssh needs a writable HOME for # known_hosts (StrictHostKeyChecking=accept-new); pointing it at the # config volume also persists pinned host keys across recreates. - HOME=/app/config # Where the read-only host mounts above land inside the container. - HOST_PROC_PATH=/host/proc - HOST_SYS_PATH=/host/sys - HOST_ROOT_PATH=/host/root # Optional: move the secrets-encryption key out of ./config so the # ciphertext and its key are not in the same directory. Put the value in a # .env file next to this compose file (gitignored) and uncomment: # - SPARKDASH_SECRETS_KEY=${SPARKDASH_SECRETS_KEY} # Default port offered for a new Spark's LLM panel — LiteLLM. # Add 8000 (ds4-server) as a second port in the UI. - LLM_PORT=4000 - COMFY_PORT=8188 # Slower polls than the on-Spark defaults: every remote metric domain # spawns a fresh `ssh` process, so this is the main knob for Pi CPU and # for load on the Spark. Raise further if the Pi runs warm. - POLL_INTERVAL_GPU=5000 - POLL_INTERVAL_CPU=5000 - POLL_INTERVAL_NETWORK=10000 - POLL_INTERVAL_STORAGE=30000 - POLL_INTERVAL_BANDWIDTH=10000 - POLL_INTERVAL_LLM=5000 - POLL_INTERVAL_COMFY=5000 - POLL_INTERVAL_LIVENESS=15000 # Hardening — nothing in here needs extra capabilities. security_opt: - no-new-privileges:true cap_drop: - ALL # Keep it boxed in on a shared Pi. mem_limit: 512m cpus: 1.5 # Don't let logs eat the SD card. logging: driver: json-file options: max-size: "10m" max-file: "3" healthcheck: test: [ "CMD", "node", "-e", "fetch('http://127.0.0.1:5555/api/settings').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))", ] interval: 30s timeout: 5s retries: 3 start_period: 20s