Docker Compose Deployment
This directory contains Docker configuration files for deploying SmartSpectra OnPrem using Docker Compose.
Quick Start
Option A: Build from Source
# 1. Copy configuration files
cp docker-compose.example.yml docker-compose.yml
cp .env.example .env # Optional: Edit .env to customize
# 2. Build images (first time only)
docker compose build
# 3. Start all services
docker compose up
# 4. Access the dashboard
# Open http://localhost:8080 in your browser
# 5. Stop services
docker compose down
Option B: Use Pre-built Images
If you received a pre-built image tar file:
# 1. Load the images
docker load -i smartspectra-images.tar
# 2. Copy the example compose file
cp docker-compose.example.yml docker-compose.yml
# 3. Start all services
docker compose up -d
# 4. Access the dashboard
# Open http://localhost:8080 in your browser
Prerequisites
- Docker 20.10+ with Docker Compose V2 plugin installed
- Camera device available at /dev/video0 (or customize in docker-compose.yml)
- At least 4GB RAM available
- At least 10GB disk space
Install Docker (Ubuntu 22.04)
# Install Docker (includes Docker Compose V2)
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
# Verify Docker Compose V2 is installed
docker compose version
# Add your user to docker group (logout/login required)
sudo usermod -aG docker $USER
Architecture
┌──────────────────────────────────────────────────────────┐
│ nginx (Optional) │
│ HTTPS Reverse Proxy │
│ ports: 443, 80 │
└──────────────────────┬───────────────────────────────────┘
│
┌──────────────┴──────────────┐
│ │
┌───────▼────────┐ ┌─────────▼─────────────────┐
│ Frontend │ │ Browser (User) │
│ (React + Node) │ │ http://localhost:5173 │
│ Gateway │ └────────────────────────────┘
│ :8080, :5173 │ ▲
└───────┬────────┘ │ WebSocket
│ Pub commands │
│ Sub metrics │
┌───────▼──────────────────────────────────────────────────┐
│ Redis :6379 │
│ Channels: │
│ • physiology:metrics:core (from server) │
│ • physiology:metrics:edge (from server) │
│ • physiology:command_queue (from gateway) │
└────┬──────────────────────────────┬────────────────────┬─┘
│ Sub metrics │ Sub commands │
│ │ │
┌────▼─────────────────┐ ┌────────▼───────────────┐ │
│ physiology_server │ │ Python Backend │ │
│ :50051 │◄──┤ (Command Bridge) │ │
│ + grpc_core_server │ │ • Monitors commands │ │
│ :50052 │ │ • Calls gRPC │ │
│ • Publishes metrics │ │ • Saves metrics │ │
└──────────────────────┘ └────────────────────────┘ │
│
Pub metrics ◄─┘
Data Flow:
- User → Frontend Gateway: WebSocket (commands like "start recording")
- Frontend → Redis: Publishes commands to command_queue
- Python Backend → Redis: Subscribes to command_queue
- Python Backend → physiology_server: gRPC calls (StartRecording, etc.)
- physiology_server → Redis: Publishes metrics to metrics:core/edge
- Frontend Gateway → Redis: Subscribes to metrics channels
- Frontend Gateway → Browser: WebSocket (real-time metrics)
Services
1. Redis
- Purpose: Pub/sub backend for metrics streaming and command queue
- Image: redis:7-alpine
- Port: 6380:6379 (mapped to 6380 on host to avoid conflict with system Redis)
- Internal Port: 6379 (services connect using redis:6379)
- Health Check: redis-cli ping
- Channels (with default physiology:metrics prefix):
- physiology:metrics:core_metrics - Core metrics from physiology_server
- physiology:metrics:edge_metrics - Edge metrics from physiology_server
- physiology:metrics:command_queue - Commands from frontend gateway
- physiology:metrics:recording_state - Recording state updates
- ⚠️ CRITICAL: All services must use the same Redis key prefix (default: physiology:metrics)
2. Physiology Server
- Purpose: Core processing and metrics generation
- Build: From local .deb and .whl files
- Ports: 50051 (gRPC API), 50052 (internal Core gRPC)
- Requires: Camera device, grpc_core_server.py volume mount
- Function: Publishes metrics to Redis, accepts gRPC commands
3. Python Backend
- Purpose: Command bridge between UI and physiology_server
- Build: From local .whl files + Python samples
- Script: redis_ipc_metrics_saving_client.py
- Function:
- Monitors Redis command_queue for UI commands (async, non-blocking)
- Forwards commands to physiology_server via gRPC
- Collects metrics from Redis pub/sub
- Saves metrics to JSONL and JSON files
- Critical: Without this service, UI commands (Start/Stop Recording) won't work
4. Frontend
- Purpose: React dashboard + WebSocket gateway
- Build: From typescript/samples/react-dashboard
- Ports: 8080 (gateway), 5173 (Vite dev server)
- Serves:
- WebSocket API at /ws (metrics + commands)
- MJPEG HUD stream at /hud.mjpg
- Health check at /api/health
- React dashboard static files
- Function: Bridges browser WebSocket to Redis pub/sub
5. Nginx (Optional)
- Purpose: HTTPS reverse proxy
- Image: nginx:alpine
- Ports: 443 (HTTPS), 80 (HTTP redirect)
- Proxies: All traffic to frontend gateway
Configuration
Environment Variables
The easiest way to configure the deployment is using a .env file:
# 1. Copy the example file
cp .env.example .env
# 2. Edit values (optional - defaults work for most cases)
nano .env
# 3. Start services - they'll automatically use .env values
docker compose up
Key variables in .env:
# ⚠️ CRITICAL: Redis prefix - must be the same for ALL services
REDIS_PREFIX=physiology:metrics
# Redis connection (internal Docker network)
REDIS_HOST=redis
REDIS_PORT=6379
# Camera configuration
CAMERA_DEVICE=/dev/video0
INPUT_TRANSFORM_MODE=ccw90
# Node environment
NODE_ENV=production # or 'development'
# Port mappings (change if conflicts exist)
REDIS_HOST_PORT=6380
GATEWAY_HOST_PORT=8080
Why use .env?
- Single source of truth - change REDIS_PREFIX once, applies to all services
- Prevents configuration drift - no need to update 3 places in docker-compose.yml
- Easy to review - see all config in one file
- Version control friendly - commit .env.example, gitignore .env
- Avoids prefix mismatches - the #1 cause of "UI commands don't work" issues
Without .env file: Docker Compose uses built-in defaults (${VAR:-default} syntax), which work for most deployments.
Customizing docker-compose.yml
Change Camera Device
physiology-server:
devices:
- /dev/video1:/dev/video0 # Use camera 1 instead of 0
Or use privileged mode to access all devices:
physiology-server:
privileged: true # Access all devices (less secure)
Enable Redis Password
redis:
command: redis-server --requirepass ${REDIS_PASSWORD}
physiology-server:
command: >
physiology_server
--redis_password=${REDIS_PASSWORD}
# ... other flags
Change Ports
frontend:
ports:
- "9090:8080" # Gateway on port 9090
- "3000:5173" # Vite on port 3000
Add Resource Limits
physiology-server:
deploy:
resources:
limits:
cpus: '2.0'
memory: 4G
reservations:
cpus: '1.0'
memory: 2G
Building Images
Build All Services
Build Individual Service
# Physiology server
docker compose build physiology-server
# Frontend
docker compose build frontend
No-Cache Build
docker compose build --no-cache
Running Services
Start All Services (Foreground)
Start All Services (Background)
Start Specific Service
docker compose up redis
docker compose up physiology-server
docker compose up frontend
View Logs
# All services
docker compose logs -f
# Specific service
docker compose logs -f physiology-server
# Last 100 lines
docker compose logs --tail=100 frontend
Check Service Status
Stop Services
# Graceful stop
docker compose stop
# Force stop
docker compose kill
# Stop and remove containers
docker compose down
# Stop and remove volumes
docker compose down -v
Restart Service
docker compose restart physiology-server
Testing
1. Verify All Services Running
Expected output:
NAME STATUS PORTS
smartspectra-redis Up (healthy) 0.0.0.0:6380->6379/tcp
smartspectra-physiology Up (healthy) 0.0.0.0:50051-50052->50051-50052/tcp
smartspectra-python-backend Up
smartspectra-frontend Up (healthy) 0.0.0.0:5173->5173/tcp, 0.0.0.0:8080->8080/tcp
2. Test Redis Connection
docker exec smartspectra-redis redis-cli ping
# Expected: PONG
3. Test Physiology Server
# Check logs for "Ready to accept connections"
docker compose logs physiology-server | grep -i "ready"
# Test gRPC endpoint (if grpc_health_probe installed)
docker exec smartspectra-physiology grpc_health_probe -addr=:50051
4. Test Python Backend
# Check if running
docker compose ps python-backend
# Expected: Up
# Check logs
docker compose logs python-backend | tail -20
# Verify it's connected to Redis and physiology_server
docker compose logs python-backend | grep -i "connected\|subscrib"
5. Test Frontend Gateway
# Health check
curl http://localhost:8080/api/health
# Expected: {"status":"ok"}
# WebSocket (requires wscat)
npm install -g wscat
wscat -c ws://localhost:8080/ws
5. Test Dashboard
# Open in browser
xdg-open http://localhost:5173 # Linux
open http://localhost:5173 # macOS
6. End-to-End Test
- Open dashboard: http://localhost:5173
- Click "Start Recording" toggle in header
- Behind the scenes:
- Gateway publishes command to Redis (command_queue)
- Python backend receives command from Redis
- Python backend calls physiology_server.StartRecording() via gRPC
- physiology_server starts publishing metrics to Redis
- Gateway forwards metrics to browser via WebSocket
- Verify metrics appearing in charts (heart rate, breathing, etc.)
- Verify HUD video stream shows camera feed
- Click "Stop Recording" and verify metrics stop updating
- Check metrics files created: ls -la recordings/metrics.jsonl
Troubleshooting
Container Won't Start
Check logs:
docker compose logs physiology-server
Common issues:
- grpc_core_server.py not found → Check volume mount
- Camera not found → Check device mapping or use privileged
- Python import errors → Rebuild image with --no-cache
Redis Connection Refused
Symptoms:
- redis.exceptions.ConnectionError
- ECONNREFUSED redis:6379
Fixes:
Wait for Redis to be healthy:
Check network:
docker network inspect smartspectra_smartspectra-net
- Verify hostname (use redis not localhost in containers)
Camera Not Found
Symptoms:
- Failed to open video device
- physiology_server logs show camera errors
Fixes:
Check device exists on host:
Verify permissions:
# Host permissions
sudo chmod 666 /dev/video0
# Or add user to video group
sudo usermod -aG video $USER
Use privileged mode:
physiology-server:
privileged: true
UI Commands Don't Work (Start/Stop Recording)
Symptoms:
- Clicking "Start Recording" does nothing or requires multiple presses
- python-backend shows no command activity
Root Cause:
Redis key prefix mismatch between services.
Fix:
Verify all services use the same prefix:
docker compose logs physiology-server | grep "key_prefix"
docker exec smartspectra-python-backend printenv REDIS_PREFIX
docker exec smartspectra-frontend printenv REDIS_PREFIX
# All should show: physiology:metrics
If mismatched, update .env file:
# Edit .env
REDIS_PREFIX=physiology:metrics
Recreate containers:
docker compose down
docker compose up -d
WebSocket Won't Connect
Symptoms:
- Browser console: WebSocket connection failed
- Dashboard shows "Disconnected"
Fixes:
Check frontend is running:
docker compose ps frontend
curl http://localhost:8080/api/health
- Check browser console for exact error
Verify Redis is accessible from frontend:
docker exec smartspectra-frontend ping -c 1 redis
Port Already in Use
Symptoms:
- bind: address already in use
Fixes:
Find what's using the port:
sudo lsof -i :6380 # Redis (host port)
sudo lsof -i :50051 # physiology_server
sudo lsof -i :8080 # Gateway
sudo lsof -i :5173 # Vite dev server
Redis Port Conflict (6379): The default compose file uses port 6380 on the host to avoid conflicts with system Redis. If you still have conflicts:
redis:
ports:
- "6381:6379" # Use a different host port
- Stop the conflicting service or change ports in docker-compose.yml
Out of Disk Space
Symptoms:
Fixes:
# Clean up unused containers/images
docker system prune -a
# Remove old volumes
docker volume prune
# Check disk usage
docker system df
Exporting Docker Images for Distribution
To create pre-built Docker images for offline/air-gapped deployment:
# Export all images to a single tar file
./export_docker_images.sh [output_file]
# Example
./export_docker_images.sh ./smartspectra-images.tar
This will:
- Build all Docker images from docker-compose.example.yml
- Pull the Redis image
- Export everything to a single tar file
- Create a README with loading instructions
Default output: smartspectra-images.tar (~1.5-2 GB)
Loading on target system:
docker load -i smartspectra-images.tar
Use case: Distribute to customers without requiring them to build from source or have internet access.
Production Deployment
For production deployment, make these changes:
1. Use Production Mode
frontend:
environment:
- NODE_ENV=production
command: node backend/server.js # Don't use dev mode
2. Enable Restart Policies
all-services:
restart: always # or 'unless-stopped'
3. Add Logging
physiology-server:
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
4. Use Redis Password
redis:
command: redis-server --requirepass ${REDIS_PASSWORD}
5. Set Resource Limits
physiology-server:
deploy:
resources:
limits:
memory: 4G
6. Monitor Services
# Check health regularly
watch docker compose ps
# Monitor logs
docker compose logs -f
Scaling
Frontend (Horizontal Scaling)
# Run 3 frontend instances
docker compose up --scale frontend=3 -d
Note: For production, you'd add a load balancer (e.g., nginx, HAProxy) to distribute traffic.
physiology-server (NO Scaling)
⚠️ Cannot scale physiology-server - camera device conflict
For multiple cameras, run separate stacks:
# Camera 0
docker compose -p cam0 up -d
# Camera 1 (different config)
CAMERA_DEVICE=/dev/video1 REDIS_PREFIX=physiology_cam1 \
docker compose -p cam1 up -d
Development vs Production
Development Setup
# docker-compose.dev.yml
services:
physiology-server:
volumes:
- ./grpc_core_server.py:/app/grpc_core_server.py # Hot reload
frontend:
command: npm run dev # Vite hot reload
volumes:
- ./typescript/samples/react-dashboard/src:/app/src
Run:
docker compose -f docker-compose.yml -f docker-compose.dev.yml up
Production Setup
# docker-compose.prod.yml
services:
physiology-server:
restart: always
frontend:
environment:
- NODE_ENV=production
command: node backend/server.js
Run:
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
Backup and Restore
Backup Volumes
# Backup Redis data
docker run --rm \
-v smartspectra_redis-data:/data \
-v $(pwd):/backup \
alpine tar czf /backup/redis-backup.tar.gz /data
# Backup recordings
docker run --rm \
-v $(pwd)/recordings:/recordings \
-v $(pwd):/backup \
alpine tar czf /backup/recordings-backup.tar.gz /recordings
Restore Volumes
# Restore Redis data
docker run --rm \
-v smartspectra_redis-data:/data \
-v $(pwd):/backup \
alpine tar xzf /backup/redis-backup.tar.gz -C /
Uninstall
# Stop and remove all containers
docker compose down
# Remove volumes (CAUTION: deletes all data)
docker compose down -v
# Remove images
docker rmi smartspectra/physiology-server:latest
docker rmi smartspectra/frontend:latest
docker rmi redis:7-alpine
Further Reading
- Main README - General OnPrem documentation
- QUICKSTART - Native installation guide
- TypeScript Guide - Frontend development
- Redis IPC Guide - Redis configuration
- Examples Guide - Python client examples