feat(deploy): Docker packaging with multi-stage build and compose orchestration
- Dockerfile: golang:1.23-bookworm builder → distroless/static-debian12:nonroot - docker-compose.yml: host networking (required for mDNS), Traefik labels, resource limits - VERSION: 0.1.0 for image tagging - .dockerignore: excludes docs, build artifacts, IDE files - .gitignore: standard Go/ESP-IDF ignores Key decisions: - Host networking required: Docker bridge blocks mDNS multicast 224.0.0.251 - distroless/static-debian12:nonroot: no shell, minimal attack surface, UID 65532 - Firmware via volume mount: users provide their own binaries for OTA - Traefik labels disabled by default: enable SPAXEL_TRAEFIK_ENABLE=true for TLS Complete: Phase 1 Docker packaging — all foundation items now done Remaining: Phase 2 signal processing (baseline, deltaRMS, Fresnel zones)
This commit is contained in:
parent
8230ef4222
commit
3f2962f945
6 changed files with 251 additions and 1 deletions
37
.dockerignore
Normal file
37
.dockerignore
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# Git
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# Documentation
|
||||
docs/
|
||||
*.md
|
||||
!README.md
|
||||
|
||||
# Marathon logs
|
||||
.marathon/
|
||||
|
||||
# Firmware build artifacts (use volume mount instead)
|
||||
firmware/build/
|
||||
firmware/managed_components/
|
||||
firmware/.cache/
|
||||
|
||||
# IDE and editor files
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Test and coverage
|
||||
*.test
|
||||
*.out
|
||||
coverage.txt
|
||||
|
||||
# Temporary files
|
||||
tmp/
|
||||
temp/
|
||||
*.tmp
|
||||
28
.gitignore
vendored
Normal file
28
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
# Marathon logs
|
||||
.marathon/logs/
|
||||
|
||||
# IDE and editor files
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Firmware build artifacts
|
||||
firmware/build/
|
||||
firmware/managed_components/
|
||||
firmware/.cache/
|
||||
|
||||
# Test and coverage
|
||||
*.test
|
||||
*.out
|
||||
coverage.txt
|
||||
|
||||
# Temporary files
|
||||
tmp/
|
||||
temp/
|
||||
*.tmp
|
||||
44
Dockerfile
Normal file
44
Dockerfile
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
# Spaxel Mothership Dockerfile
|
||||
# Multi-stage build for minimal production image
|
||||
|
||||
# Stage 1: Build the Go binary
|
||||
FROM golang:1.23-bookworm AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy Go module files first for better caching
|
||||
COPY mothership/go.mod mothership/go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
# Copy source code
|
||||
COPY mothership/ ./
|
||||
|
||||
# Build the binary
|
||||
# CGO_ENABLED=0 because we use pure-Go SQLite (modernc.org/sqlite)
|
||||
ARG VERSION=dev
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build \
|
||||
-ldflags="-s -w -X main.version=${VERSION}" \
|
||||
-o spaxel ./cmd/mothership
|
||||
|
||||
# Stage 2: Minimal runtime image
|
||||
FROM gcr.io/distroless/static-debian12:nonroot
|
||||
|
||||
# Copy the binary
|
||||
COPY --from=builder /app/spaxel /spaxel
|
||||
|
||||
# Copy dashboard static files (served from filesystem at runtime)
|
||||
COPY dashboard/ /dashboard/
|
||||
|
||||
# Create firmware directory (users should mount their own firmware volume)
|
||||
# The container will serve firmware binaries for OTA from /firmware/
|
||||
VOLUME ["/data", "/firmware"]
|
||||
|
||||
# Expose HTTP/WebSocket port
|
||||
EXPOSE 8080
|
||||
|
||||
# Health check (distroless has wget)
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||
["wget", "-q", "-O-", "http://localhost:8080/healthz"]
|
||||
|
||||
# Run as non-root (distroless default is UID 65532)
|
||||
ENTRYPOINT ["/spaxel"]
|
||||
55
PROGRESS.md
55
PROGRESS.md
|
|
@ -13,10 +13,63 @@ Goal: Bare-minimum loop from ESP32 to browser. Zero-config with passive radar an
|
|||
| BLE scanning | **Done** | Core 0 concurrent with WiFi |
|
||||
| Mothership WebSocket ingestion | **Done** | See iteration 1 below |
|
||||
| Dashboard skeleton | **Done** | See iteration 3 below |
|
||||
| Docker packaging | Not started | |
|
||||
| Docker packaging | **Done** | See iteration 4 below |
|
||||
|
||||
### Iteration Log
|
||||
|
||||
#### Iteration 4 — 2026-03-26
|
||||
|
||||
**Completed:** Docker packaging
|
||||
|
||||
Implemented container deployment with:
|
||||
|
||||
- **Dockerfile:** Multi-stage build for minimal production image
|
||||
- Stage 1: `golang:1.23-bookworm` builder with module caching
|
||||
- Stage 2: `distroless/static-debian12:nonroot` runtime (no shell, runs as UID 65532)
|
||||
- CGO_ENABLED=0 build (pure-Go SQLite compatible)
|
||||
- Dashboard static files copied to `/dashboard/`
|
||||
- Firmware directory as volume mount point (`/firmware/`)
|
||||
- Built-in healthcheck via wget
|
||||
|
||||
- **docker-compose.yml:** Production-ready orchestration
|
||||
- `network_mode: host` for mDNS multicast discovery (required for ESP32 nodes)
|
||||
- Volume mounts: `spaxel-data` for persistence, `./firmware` for OTA binaries
|
||||
- Environment variables: TZ, mDNS config, optional MQTT
|
||||
- Resource limits: 512m RAM, 2 CPUs (scalable to 1g for 16+ nodes)
|
||||
- 35s stop_grace_period for graceful shutdown
|
||||
- Ulimits: 4096/8192 file descriptors for node connections
|
||||
- Traefik labels (disabled by default, enable for TLS/proxy)
|
||||
|
||||
- **Supporting files:**
|
||||
- `VERSION` — 0.1.0 for image tagging
|
||||
- `.dockerignore` — Excludes docs, build artifacts, IDE files
|
||||
|
||||
**Key design decisions:**
|
||||
- Host networking is REQUIRED for mDNS — Docker bridge blocks multicast 224.0.0.251
|
||||
- distroless image minimizes attack surface (no shell, no package manager)
|
||||
- Firmware binaries not bundled — users mount their own `/firmware` volume
|
||||
- Traefik labels included but disabled — enable for production TLS
|
||||
|
||||
**Files created:**
|
||||
```
|
||||
Dockerfile
|
||||
docker-compose.yml
|
||||
VERSION
|
||||
.dockerignore
|
||||
```
|
||||
|
||||
**Phase 1 Status:** COMPLETE
|
||||
|
||||
All Phase 1 items implemented:
|
||||
- ✅ ESP32 firmware skeleton
|
||||
- ✅ Passive radar support
|
||||
- ✅ BLE scanning
|
||||
- ✅ Mothership WebSocket ingestion
|
||||
- ✅ Dashboard skeleton
|
||||
- ✅ Docker packaging
|
||||
|
||||
**Next:** Phase 2 — Signal Processing (baseline, deltaRMS, Fresnel zones)
|
||||
|
||||
#### Iteration 3 — 2026-03-26
|
||||
|
||||
**Completed:** Dashboard skeleton with 3D visualization
|
||||
|
|
|
|||
1
VERSION
Normal file
1
VERSION
Normal file
|
|
@ -0,0 +1 @@
|
|||
0.1.0
|
||||
87
docker-compose.yml
Normal file
87
docker-compose.yml
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
# Spaxel Docker Compose
|
||||
# Production deployment configuration
|
||||
#
|
||||
# IMPORTANT: network_mode: host is REQUIRED for mDNS to work.
|
||||
# mDNS uses multicast address 224.0.0.251 (link-local), which Docker
|
||||
# bridge networking blocks. With host networking, the container shares
|
||||
# the host's network interfaces and mDNS multicasts reach the LAN.
|
||||
#
|
||||
# Side effect: 'ports' mapping is ignored in host mode — port 8080 is
|
||||
# directly exposed on the host.
|
||||
|
||||
services:
|
||||
spaxel:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
VERSION: ${VERSION:-dev}
|
||||
# Alternative: use pre-built image
|
||||
# image: ghcr.io/spaxel/spaxel:latest
|
||||
|
||||
# Host networking required for mDNS discovery by ESP32 nodes
|
||||
network_mode: host
|
||||
|
||||
# Alternative (if host mode is not desired): disable mDNS and require
|
||||
# nodes to use cached ms_ip NVS key (manual IP entry during provisioning).
|
||||
# Set SPAXEL_MDNS_ENABLED=false to skip the mDNS advertisement entirely.
|
||||
|
||||
volumes:
|
||||
- spaxel-data:/data # SQLite, baselines, floor plans, CSI buffer
|
||||
- ./firmware:/firmware:ro # Firmware binaries for OTA (read-only)
|
||||
|
||||
environment:
|
||||
# Timezone - required for correct diurnal baseline hours
|
||||
TZ: ${TZ:-UTC}
|
||||
# mDNS service name (must match firmware ms_mdns NVS key)
|
||||
SPAXEL_MDNS_NAME: ${SPAXEL_MDNS_NAME:-spaxel}
|
||||
# Set to false if using bridge networking (nodes must use cached IP)
|
||||
SPAXEL_MDNS_ENABLED: ${SPAXEL_MDNS_ENABLED:-true}
|
||||
# Optional MQTT integration (uncomment to enable)
|
||||
# SPAXEL_MQTT_BROKER: mqtt://homeassistant.local:1883
|
||||
# SPAXEL_MQTT_USERNAME: mosquitto
|
||||
# SPAXEL_MQTT_PASSWORD: secret
|
||||
# Debug logging (uncomment for troubleshooting)
|
||||
# SPAXEL_LOG_LEVEL: debug
|
||||
|
||||
restart: unless-stopped
|
||||
|
||||
# Allow full 30s graceful shutdown
|
||||
stop_grace_period: 35s
|
||||
|
||||
ulimits:
|
||||
nofile:
|
||||
soft: 4096 # One fd per node connection + SQLite handles
|
||||
hard: 8192
|
||||
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-q", "-O-", "http://localhost:8080/healthz"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 512m # Increase to 1g for 16+ node fleets
|
||||
cpus: "2.0"
|
||||
reservations:
|
||||
memory: 128m
|
||||
cpus: "0.5"
|
||||
|
||||
# Traefik labels for reverse proxy with TLS
|
||||
# Uncomment and customize for your Traefik setup
|
||||
labels:
|
||||
- "traefik.enable=${TRAEFIK_ENABLE:-false}"
|
||||
# - "traefik.http.routers.spaxel.rule=Host(`spaxel.example.com`)"
|
||||
# - "traefik.http.routers.spaxel.entrypoints=websecure"
|
||||
# - "traefik.http.routers.spaxel.tls.certresolver=letsencrypt"
|
||||
# - "traefik.http.services.spaxel.loadbalancer.server.port=8080"
|
||||
# # Extend Traefik timeout for long-lived WebSocket connections
|
||||
# - "traefik.http.routers.spaxel.middlewares=spaxel-ws-timeout"
|
||||
# - "traefik.http.middlewares.spaxel-ws-timeout.headers.respondingTimeouts.readTimeout=3600s"
|
||||
|
||||
volumes:
|
||||
spaxel-data:
|
||||
driver: local
|
||||
Loading…
Add table
Reference in a new issue