Guides deployment of Muni software to rovers (aarch64 cross-compilation, systemd) and depot (Docker Compose). Use when deploying firmware updates, installing services, configuring environments, troubleshooting deployment failures, or setting up new rovers/depot instances. Covers cross-compilation with the `cross` tool, usage of the deploy.sh script, systemd service management, Docker Compose profiles, environment variables, and rollback procedures.
Guides deployment of Muni software across heterogeneous platforms: rovers (ARM64) and depot (x86_64).
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 |
Rover Deployment:
bvr/firmware/deploy.sh - Deployment scriptbvr/firmware/config/bvr.toml - Runtime configurationbvr/firmware/config/*.service - Systemd service unitsDepot Deployment:
depot/docker-compose.yml - Service orchestrationdepot/.env - Environment variablesdepot/*/Dockerfile - Service imagesRequirements:
cross tool# Install cross tool
cargo install cross --git https://github.com/cross-rs/cross
# Verify installation
cross --version
Why cross?
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:
✅ cross is recommended for reliability
# 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"
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
# 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
# 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
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"
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
# 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"
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
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
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"
# 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"
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 all services
docker compose build
# Start all services
docker compose up -d
# View logs
docker compose logs -f
# Check status
docker compose ps
Base services (always run):
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
# 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
# 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
# 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
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
# Edit .env
vim depot/.env
# Restart affected services
docker compose up -d
Which services need restart after .env changes?
CONSOLE_PASSWORD, CONSOLE_USERNAMEINFLUXDB_* variablesGRAFANA_* variablesSESSIONS_PATHDATABASE_URLDevelopment:
# 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
# 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
# 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
# List volumes
docker volume ls
# Inspect volume
docker volume inspect depot_sessions-data
# Check permissions
docker compose exec discovery ls -la /data/sessions
# 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
# Check if port is in use
lsof -i :4860
# Change port in docker-compose.yml:
ports:
- "4870:4860" # Map host 4870 to container 4860
# 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 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/
# 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
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
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"
frog-0)sudo apt install can-utils/etc/network/interfacesbvr/firmware/config/bvr.toml and customize--all: ./deploy.sh frog-0 --allssh frog-0 "sudo systemctl enable bvrd can"depot/.env from example/data/sessions, /data/mapssudo chown -R 1000:1000 /data/docker compose up -ddocker compose pshttp://localhostRover:
ssh frog-0 "systemctl is-active bvrd"ssh frog-0 "ip link show can0"ssh frog-0 "journalctl -u bvrd | grep GPS"Depot:
docker compose pscurl http://localhost/healthcurl http://localhost:4860/roversdocker compose exec postgres pg_isreadySearch for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer