deployment-automation
Deployment Automation Skill
Guides deployment of Muni software across heterogeneous platforms: rovers (ARM64) and depot (x86_64).
Overview
Muni has two deployment targets with different strategies:
| Target | Platform | Method | Build Tool | Deploy Tool |
|---|---|---|---|---|
| Rovers | aarch64 (ARM64) | Binary + systemd | cross / cargo |
deploy.sh (scp + ssh) |
| Depot | x86_64 | Docker containers | Docker | docker-compose |
Key Files
Rover Deployment:
bvr/firmware/deploy.sh- Deployment scriptbvr/firmware/config/bvr.toml- Runtime configurationbvr/firmware/config/*.service- Systemd service units
Depot Deployment:
depot/docker-compose.yml- Service orchestrationdepot/.env- Environment variablesdepot/*/Dockerfile- Service images
Rover Deployment
Cross-Compilation Setup
Install Cross
Requirements:
- Rust 1.83+
- Docker (for cross)
crosstool
# Install cross tool
cargo install cross --git https://github.com/cross-rs/cross
# Verify installation
cross --version
Why cross?
- Handles cross-compilation toolchains automatically
- Uses Docker to provide consistent build environment
- Supports aarch64-unknown-linux-gnu target
- Easier than setting up native cross-compilation
Alternative: Native cargo
# Add target
rustup target add aarch64-unknown-linux-gnu
# Install GCC cross-compiler (macOS)
brew install aarch64-unknown-linux-gnu
# Build (may not work for complex dependencies)
cargo build --release --target aarch64-unknown-linux-gnu
⚠️ Native cross-compilation limitations:
- May fail with system dependencies (e.g., GStreamer)
- Requires manual toolchain setup
- Platform-specific quirks
✅ cross is recommended for reliability
Cross.toml Configuration
# bvr/firmware/Cross.toml (if needed)
[build]
default-target = "aarch64-unknown-linux-gnu"
[target.aarch64-unknown-linux-gnu]
# Use custom Docker image with dependencies
image = "ghcr.io/cross-rs/aarch64-unknown-linux-gnu:latest"
Deploy Script Usage
Basic Deployment
cd bvr/firmware
# Deploy bvrd binary only (default)
./deploy.sh frog-0
# Deploy without restarting service
./deploy.sh frog-0 --no-restart
# Deploy to specific user
./deploy.sh frog-0 --user admin
Full Deployment
# Deploy everything (--all)
# - bvrd binary
# - muni CLI tool
# - Config file (bvr.toml)
# - Systemd services (bvrd, can, kiosk)
# - Sync timer (bvr-sync)
./deploy.sh frog-0 --all
# Equivalent to:
./deploy.sh frog-0 --cli --config --services --sync
Partial Deployment
# Deploy bvrd + CLI tool
./deploy.sh frog-0 --cli
# Deploy and update config
./deploy.sh frog-0 --config
# Install/update systemd services only
./deploy.sh frog-0 --services
# Install sync timer only
./deploy.sh frog-0 --sync
Deploy Script Internals
What deploy.sh does:
-
Build Phase:
# Uses cross or cargo to build for aarch64 cross build --release --target aarch64-unknown-linux-gnu --bin bvrd -
Pre-Deploy Phase:
# Stop service before deploying ssh $REMOTE "sudo systemctl stop bvrd" -
Deploy Phase:
# Copy binary to rover scp target/aarch64-unknown-linux-gnu/release/bvrd $REMOTE:/tmp/ ssh $REMOTE "sudo mv /tmp/bvrd /usr/local/bin/ && sudo chmod +x /usr/local/bin/bvrd" # Copy config (if --config) scp config/bvr.toml $REMOTE:/tmp/ ssh $REMOTE "sudo mv /tmp/bvr.toml /etc/bvr/" # Install services (if --services) scp config/*.service $REMOTE:/tmp/ ssh $REMOTE "sudo mv /tmp/*.service /etc/systemd/system/ && sudo systemctl daemon-reload" -
Post-Deploy Phase:
# Start/restart service (unless --no-restart) ssh $REMOTE "sudo systemctl start bvrd" ssh $REMOTE "sudo systemctl status bvrd"
Systemd Service Management
Service Units
bvrd.service (main daemon):
[Unit]
Description=BVR Daemon
After=network.target can.service
Requires=can.service
[Service]
Type=simple
User=cam
ExecStart=/usr/local/bin/bvrd
Restart=always
RestartSec=5
Environment="RUST_LOG=info"
WorkingDirectory=/home/cam
[Install]
WantedBy=multi-user.target
can.service (CAN bus setup):
[Unit]
Description=CAN Bus Setup
Before=bvrd.service
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/bin/setup-can.sh
ExecStop=/usr/local/bin/teardown-can.sh
[Install]
WantedBy=multi-user.target
bvr-sync.service (session sync):
[Unit]
Description=BVR Session Sync
After=network.target
[Service]
Type=oneshot
User=cam
ExecStart=/usr/local/bin/bvr-sync
Environment="RUST_LOG=info"
bvr-sync.timer (periodic sync):
[Unit]
Description=BVR Session Sync Timer
Requires=bvr-sync.service
[Timer]
OnBootSec=5min
OnUnitActiveSec=15min
[Install]
WantedBy=timers.target
Service Commands
# Start service
ssh frog-0 "sudo systemctl start bvrd"
# Stop service
ssh frog-0 "sudo systemctl stop bvrd"
# Restart service
ssh frog-0 "sudo systemctl restart bvrd"
# Check status
ssh frog-0 "sudo systemctl status bvrd"
# Enable on boot
ssh frog-0 "sudo systemctl enable bvrd"
# Disable on boot
ssh frog-0 "sudo systemctl disable bvrd"
# View logs
ssh frog-0 "sudo journalctl -u bvrd -f"
# View last 100 lines
ssh frog-0 "sudo journalctl -u bvrd -n 100"
# Reload service files after editing
ssh frog-0 "sudo systemctl daemon-reload"
Configuration Management
bvr.toml (runtime configuration):
# Rover identification
[rover]
id = "frog-0"
name = "Frog Zero"
# Network configuration
[network]
discovery_url = "http://10.0.0.1:4860"
teleop_bind = "0.0.0.0:4850"
video_bind = "0.0.0.0:4851"
# CAN bus configuration
[can]
interface = "can0"
bitrate = 500000
# GPS configuration
[gps]
port = "/dev/ttyUSB0"
baudrate = 115200
Deploying config changes:
# Edit config locally
vim bvr/firmware/config/bvr.toml
# Deploy config
./deploy.sh frog-0 --config
# Restart service to reload config
./deploy.sh frog-0 --config # Restarts by default
Troubleshooting Rover Deployment
Build Failures
cross not found:
# Install cross
cargo install cross --git https://github.com/cross-rs/cross
Docker not running:
# Start Docker
# macOS: Start Docker Desktop
# Linux: sudo systemctl start docker
Compilation errors:
# Clean build
cargo clean
cross build --release --target aarch64-unknown-linux-gnu
# Check Rust version
rustup show
# Update Rust
rustup update
Deployment Failures
SSH connection failed:
# Test SSH
ssh frog-0
# Check Tailscale
tailscale status
# Check SSH keys
ssh-add -l
Permission denied on rover:
# User needs sudo without password for:
# - systemctl
# - mv to /usr/local/bin/, /etc/bvr/, /etc/systemd/system/
# Add to sudoers
ssh frog-0 "echo '$USER ALL=(ALL) NOPASSWD: /bin/systemctl, /bin/mv' | sudo tee /etc/sudoers.d/$USER"
Service won't start:
# Check service status
ssh frog-0 "sudo systemctl status bvrd"
# View logs
ssh frog-0 "sudo journalctl -u bvrd -n 100"
# Check binary permissions
ssh frog-0 "ls -l /usr/local/bin/bvrd"
# Check config file
ssh frog-0 "cat /etc/bvr/bvr.toml"
# Test binary manually
ssh frog-0 "/usr/local/bin/bvrd"
Rollback Procedure
# Keep previous binary
ssh frog-0 "sudo cp /usr/local/bin/bvrd /usr/local/bin/bvrd.backup"
# Deploy new version
./deploy.sh frog-0
# If new version fails, rollback:
ssh frog-0 "sudo systemctl stop bvrd"
ssh frog-0 "sudo mv /usr/local/bin/bvrd.backup /usr/local/bin/bvrd"
ssh frog-0 "sudo systemctl start bvrd"
Depot Deployment
Docker Compose Deployment
Initial Setup
cd depot
# Create .env file from example
cp .env.example .env
# Edit environment variables
vim .env
.env file:
# Authentication
CONSOLE_PASSWORD=your_secure_password
GRAFANA_ADMIN_PASSWORD=your_grafana_password
# InfluxDB
INFLUXDB_ADMIN_TOKEN=your_influxdb_token
INFLUXDB_ORG=muni
INFLUXDB_BUCKET=muni
# Storage
SESSIONS_PATH=/data/sessions
MAPS_PATH=/data/maps
# Retention
RETENTION_DAYS=30
Build and Start
# Build all services
docker compose build
# Start all services
docker compose up -d
# View logs
docker compose logs -f
# Check status
docker compose ps
Service Profiles
Base services (always run):
- console (web UI)
- discovery (rover tracking)
- dispatch (mission planning)
- map-api (map serving)
- influxdb (metrics)
- grafana (dashboards)
- postgres (dispatch database)
- sftp (session sync)
GPU profile (optional):
# Start with GPU support for 3D reconstruction
docker compose --profile gpu up -d
# Enables:
# - splat-worker (Gaussian splatting)
# - mapper (map processing)
RTK profile (optional):
# Start with RTK base station support
docker compose --profile rtk up -d
# Enables:
# - rtk-base (GNSS base station)
# - gps-status (RTK monitoring)
Multiple profiles:
# Run everything
docker compose --profile gpu --profile rtk up -d
Service Updates
Update Single Service
# Pull latest code
git pull
# Rebuild service
docker compose build discovery
# Restart service
docker compose up -d discovery
# View logs
docker compose logs -f discovery
Update All Services
# Pull latest code
git pull
# Rebuild all
docker compose build
# Restart all
docker compose up -d
# Check for issues
docker compose ps
docker compose logs -f
Rolling Updates
# Update services one at a time
for service in discovery dispatch map-api gps-status; do
echo "Updating $service..."
docker compose build $service
docker compose up -d $service
sleep 5
docker compose ps $service
done
Database Migrations
Dispatch service (PostgreSQL):
Migrations run automatically on service startup (see depot/dispatch/src/main.rs).
Manual migration (if needed):
# Connect to database
docker compose exec postgres psql -U postgres -d dispatch
# Run migration SQL
\i /path/to/migration.sql
# Restart service
docker compose restart dispatch
Reset database (DESTRUCTIVE):
# Stop services
docker compose down
# Remove database volume
docker volume rm depot_postgres-data
# Restart services (migrations run automatically)
docker compose up -d
Environment Configuration
Updating Environment Variables
# Edit .env
vim depot/.env
# Restart affected services
docker compose up -d
Which services need restart after .env changes?
- Console:
CONSOLE_PASSWORD,CONSOLE_USERNAME - InfluxDB:
INFLUXDB_*variables - Grafana:
GRAFANA_*variables - Discovery:
SESSIONS_PATH - Dispatch:
DATABASE_URL
Secrets Management
Development:
# Use .env file
# chmod 600 .env to restrict access
Production:
# Use Docker secrets (Swarm mode)
docker secret create database_password /path/to/secret.txt
# Reference in docker-compose.yml:
services:
dispatch:
secrets:
- database_password
secrets:
database_password:
external: true
Troubleshooting Depot Deployment
Service Won't Start
# Check logs
docker compose logs -f <service>
# Check status
docker compose ps <service>
# Check exit code
docker inspect --format='{{.State.ExitCode}}' depot-<service>
# Run service with shell override
docker compose run --rm <service> sh
Network Issues
# List networks
docker network ls
# Inspect network
docker network inspect depot_default
# Test connectivity
docker compose exec console ping discovery
docker compose exec console wget -O- http://discovery:4860/health
Volume Issues
# List volumes
docker volume ls
# Inspect volume
docker volume inspect depot_sessions-data
# Check permissions
docker compose exec discovery ls -la /data/sessions
Health Check Failing
# Check health status
docker inspect depot-discovery | jq '.[0].State.Health'
# Test health endpoint manually
docker compose exec discovery wget -O- http://localhost:4860/health
# Check service logs
docker compose logs -f discovery
Port Conflicts
# Check if port is in use
lsof -i :4860
# Change port in docker-compose.yml:
ports:
- "4870:4860" # Map host 4870 to container 4860
Out of Disk Space
# Check disk usage
df -h
# Check Docker disk usage
docker system df
# Clean up
docker system prune -a --volumes
# Remove old images
docker image prune -a
# Remove unused volumes
docker volume prune
Backup and Restore
Backup
# Backup volumes
docker run --rm \
-v depot_postgres-data:/data \
-v $(pwd):/backup \
alpine tar czf /backup/postgres-backup.tar.gz /data
# Backup .env
cp depot/.env depot/.env.backup
# Backup config files
tar czf depot-config-backup.tar.gz depot/.env depot/docker-compose.yml depot/grafana/
Restore
# Stop services
docker compose down
# Remove old volume
docker volume rm depot_postgres-data
# Create new volume
docker volume create depot_postgres-data
# Restore data
docker run --rm \
-v depot_postgres-data:/data \
-v $(pwd):/backup \
alpine tar xzf /backup/postgres-backup.tar.gz -C /
# Restore config
tar xzf depot-config-backup.tar.gz
# Start services
docker compose up -d
CI/CD Integration
GitHub Actions
name: Deploy Depot
on:
push:
branches: [main]
paths:
- 'depot/**'
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build images
run: |
cd depot
docker compose build
- name: Push to registry
run: |
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login -u "${{ secrets.REGISTRY_USERNAME }}" --password-stdin
docker compose push
- name: Deploy to server
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_KEY }}
script: |
cd /opt/depot
docker compose pull
docker compose up -d
GitLab CI
stages:
- build
- deploy
build:
stage: build
script:
- cd depot
- docker compose build
- docker compose push
deploy:
stage: deploy
only:
- main
script:
- ssh $DEPLOY_USER@$DEPLOY_HOST "cd /opt/depot && docker compose pull && docker compose up -d"
Deployment Checklists
New Rover Setup
- Install Ubuntu 22.04 on Jetson Orin NX
- Install Tailscale and join network
- Set hostname (e.g.,
frog-0) - Create user account with sudo access
- Install CAN tools:
sudo apt install can-utils - Configure CAN interface in
/etc/network/interfaces - Copy
bvr/firmware/config/bvr.tomland customize - Deploy with
--all:./deploy.sh frog-0 --all - Enable services:
ssh frog-0 "sudo systemctl enable bvrd can" - Test: check discovery service shows rover online
New Depot Setup
- Clone repository
- Install Docker and Docker Compose
- Create
depot/.envfrom example - Configure environment variables
- Create data directories:
/data/sessions,/data/maps - Set permissions:
sudo chown -R 1000:1000 /data/ - Start services:
docker compose up -d - Check health:
docker compose ps - Access console:
http://localhost - Configure Grafana dashboards
- Set up SSL (if exposing to internet)
Deployment Smoke Tests
Rover:
- Service running:
ssh frog-0 "systemctl is-active bvrd" - Registers with discovery: check console UI
- CAN bus active:
ssh frog-0 "ip link show can0" - GPS receiving:
ssh frog-0 "journalctl -u bvrd | grep GPS" - Teleop works: connect via console
Depot:
- All services healthy:
docker compose ps - Console accessible:
curl http://localhost/health - Discovery API works:
curl http://localhost:4860/rovers - Database connected:
docker compose exec postgres pg_isready - InfluxDB receiving metrics: check Grafana
- Grafana dashboards load
References
- Cross documentation
- Docker Compose reference
- Systemd service docs
- CLAUDE.md - Project conventions
- depot/README.md - Depot architecture
- bvr/firmware/README.md - Firmware build guide
More from ecto/muni
firmware-review
Reviews Rust firmware code for the BVR (Base Vectoring Rover) with focus on safety-critical systems, CAN bus protocol compliance, motor control logic, state machine correctness, and embedded testing patterns. Use when reviewing BVR firmware changes, debugging actuator control, testing motor communication, validating safety mechanisms, checking async patterns, or evaluating control system modifications. Covers watchdog implementation, e-stop handling, rate limiting, VESC motor controller integration, and Tokio async runtime patterns.
10mcu-embedded-review
Reviews embedded Rust firmware for RP2350 (Raspberry Pi Pico 2 W) and ESP32-S3 (Heltec) microcontrollers with focus on Embassy async runtime, memory constraints, LED control, CAN attachment protocols, and SLCAN bridging. Use when reviewing MCU firmware changes, debugging LED controller issues, evaluating Embassy async patterns, checking static memory allocation, validating SLCAN implementations, or assessing hardware-specific code for WS2812 LEDs, CAN peripherals, and tool attachments. Covers no_std environments, PIO state machines, RMT peripherals, and USB CDC serial communication.
10console-frontend-review
Reviews React/TypeScript code for the depot console web application with focus on real-time rover teleoperation, state management, WebSocket communication, and 3D visualization. Use when reviewing console frontend changes, debugging teleop UI issues, optimizing rendering performance, validating WebSocket protocols, checking React Three Fiber implementations, or evaluating state management patterns. Covers Zustand store architecture, binary protocol encoding, input handling, page visibility safety, memory management, and 360-degree video streaming.
4depot-services-review
Reviews Rust microservices in the depot/ directory with focus on Axum web framework, SQLx database patterns, Docker deployment, and WebSocket communication. Use when reviewing depot service changes (discovery, dispatch, map-api, gps-status, mapper), adding new endpoints, modifying database schemas, implementing WebSocket protocols, or debugging service integration issues. Covers Tokio async patterns, REST/WebSocket API design, PostgreSQL migrations, error handling, health checks, and Docker multi-stage builds.
4integration-testing
Guides end-to-end testing, mocking, and simulation for the Muni codebase. Use when writing integration tests, setting up test environments, creating mock CAN bus, testing WebSocket protocols, validating database fixtures, or debugging test failures. Covers Rust test patterns (tokio::test, integration tests), TypeScript testing (Vitest), mock infrastructure (Docker Compose for tests), CAN bus simulation, WebSocket test clients, database seeding, and Rerun recording validation. Essential for ensuring components work together correctly.
4documentation-automation
Automatically maintains project documentation including CHANGELOG.md, README files, inline code documentation, and cross-references. Use proactively after implementing features, fixing bugs, making API changes, or completing significant work. Updates CHANGELOG.md with conventional commit format, adds README sections for new features, generates inline documentation for new functions/structs/components, and ensures documentation cross-references are up-to-date. Covers Rust doc comments, TypeScript JSDoc, conventional commits (feat/fix/docs/refactor/test/chore), and multi-level README organization.
4