This document covers two scenarios:
- Updating an existing device with a new deployment package (the primary use case for this guide)
- Installing the CA certificate on client devices (phones, tablets, laptops)
For initial device setup (flashing a new CM5 and completing the captive-portal setup from a phone), see CM5/SETUP.md. The CM5 image includes all application code, Docker images, systemd units, and Python dependencies baked in — map data is not baked in; it's uploaded via the PWA Maps page after first boot (see DOCS/UpdatingMaps.md). No deployment package transfer is needed for a fresh device, and no SSH, keyboard, or monitor is required or supported for initial setup. The CM5 must be the WiFi variant.
The deployment zip is used to update devices that already have a base image flashed and configured. It is created on your development machine:
./create-deployment-package.sh --version=1.0.0This produces trailcurrent-deployment-1.0.0.zip containing:
images/*.tar— 6 pre-built ARM64 Docker images:frontend,backend,mosquitto(custom),mongodb,photon(geocoding), andvalhalla(routing). Photon and Valhalla are behind themapscompose profile — they only start when a map bundle is installed.docker-compose.yml— Service orchestrationconfig/— Mosquitto configurationlocal_code/— Python bridges + watchers (CAN-to-MQTT, deployment watcher, map watcher)firmware/wired/— MCU firmware binaries (if available)scripts/— SSL certificate generation.env.example— Environment variable templatedeploy.sh— Deployment orchestratorPI_DEPLOYMENT.md— This file
Note:
build-and-save-images.shmust be run before creating a deployment package. It builds the ARM64 Docker images and saves them as tar files inimages/. See README.md.
For devices flashed with the current CM5 image, no deployment package is
needed. The image includes all application artifacts baked in. On first
boot the device brings up a Headwaters-XXXX WiFi access point and serves
a branded captive-portal setup page. The customer joins from their phone
and completes setup entirely through the portal — MQTT and admin passwords,
CA certificate installation, and startup of all services. See
CM5/SETUP.md for the complete flashing and setup procedure.
The WiFi variant of the CM5 is required. There is no fallback setup path that uses SSH, a keyboard, a monitor, or a serial console.
If you are working with an older image that does not include baked-in artifacts, you can still deploy manually using the steps below.
For devices running older images without baked-in application artifacts:
-
Transfer the deployment package to the Pi:
scp trailcurrent-deployment-1.0.0.zip [email protected]:~
-
SSH to the Pi and extract:
ssh [email protected] unzip trailcurrent-deployment-1.0.0.zip
-
Run the deployment script:
chmod +x deploy.sh ./deploy.sh
On first run,
deploy.shwill:- Create
.envfrom.env.exampleand ask you to edit it (then re-run) - Generate TLS certificates automatically using
scripts/generate-certs.sh - Install the CA certificate to the system trust store (for host-side TLS verification)
- Load all Docker images from tar files
- Start all services
- Set up the CAN-to-MQTT bridge
- Set up the deployment watcher (for cloud OTA updates)
- Deploy MCU firmware via OTA (if firmware is included)
- Create
-
Edit
.envwith your credentials (first run only):nano .env # Set these values: # MQTT_USERNAME / MQTT_PASSWORD # ADMIN_PASSWORD # TLS_CERT_HOSTNAME=headwaters.local # ENCRYPTION_KEY=$(openssl rand -hex 32)
Then re-run
./deploy.sh. -
Map data: No manual step here. Map bundles are uploaded via the PWA Maps page after first boot — see PLANS/Offline-Maps-Migration.md for the architecture and build/maps/README.md for how to build a region-specific bundle. The device boots into a healthy "Map Data not Loaded" state until the first upload lands.
-
Install the CA certificate on phones/tablets (for PWA home screen icon):
TrailCurrent uses a self-signed TLS certificate. Browsers will let you tap through the certificate warning, but iOS requires the CA to be trusted at the OS level for the PWA "Add to Home Screen" icon to work. Without this step the home screen icon will show a generic letter instead of the TrailCurrent logo.
Copy the CA certificate from the Pi:
scp [email protected]:~/data/keys/ca.crt ~/Desktop/TrailCurrent-CA.crt
iOS (iPhone / iPad):
- Transfer
TrailCurrent-CA.crtto the device (AirDrop, email, or iCloud Drive) - Open the file — iOS will show "Profile Downloaded"
- Go to Settings > General > VPN & Device Management and install the profile
- Go to Settings > General > About > Certificate Trust Settings and enable full trust for TrailCurrent-CA
- Force close Safari (swipe up from the app switcher)
- Go to Settings > Apps > Safari > Clear History and Website Data to flush any cached certificate state
- Reopen Safari, navigate to the app, and use Share > Add to Home Screen
If you are replacing a previously installed CA (e.g., after regenerating certificates), you must first remove the old profile before installing the new one: Settings > General > VPN & Device Management > TrailCurrent-CA > Remove Profile. Then follow steps 1-7 above.
Android:
- Transfer
TrailCurrent-CA.crtto the device - Open the file — Android will prompt to install it as a CA certificate
- Follow the on-screen prompts (you may need to set a screen lock if you haven't already)
macOS:
- Double-click
TrailCurrent-CA.crtto add it to Keychain Access - Find TrailCurrent-CA in the System keychain, double-click it
- Expand Trust and set to Always Trust
Windows:
- Double-click
TrailCurrent-CA.crt> Install Certificate - Choose Local Machine > Place in: Trusted Root Certification Authorities
This only needs to be done once per device. The CA certificate is valid for 10 years and will trust any server certificates generated by your Pi.
- Transfer
-
Access the application:
https://trailcurrent01.local
When deploying a new version:
-
Transfer new zip to Pi:
scp trailcurrent-deployment-1.1.0.zip [email protected]:~
-
SSH in, extract, and deploy:
ssh [email protected] unzip -o trailcurrent-deployment-1.1.0.zip ./deploy.sh
On updates,
deploy.shwill:- Stop existing services
- Update the system CA trust store if certificates were renewed
- Load updated Docker images
- Preserve your
.env, certificates, and map tiles - Restart all services
- Restart the deployment watcher service
- Update MCU firmware if new firmware is included
These items are PRESERVED and never deleted by deploy.sh:
.env— Device-specific secrets and settings- MQTT credentials, admin password, encryption keys, hostname
data/keys/— TLS certificates- CA certificate: 10-year validity — no need to regenerate or re-install on devices
- Server certificate: ~2-year validity (825 days, required by Apple/iOS)
- To renew the server cert:
./scripts/generate-certs.sh 2(uses existing CA, no need to re-install CA on devices)
data/maps/versions/<version>/— Map bundles uploaded via the PWA Maps page (~130 GB per bundle for North America; one current + one previous version retained for rollback)data/maps/current— Symlink to the active bundle version, updated atomically bymap-watcheron successful uploaddata/firmware/— Peripheral module firmware payloadsdata/deployments/— OTA deployment package history- MongoDB data volume — All application state
CRITICAL: Never delete data/ directory during updates!
- Docker container images (loaded from new tar files)
- Application code (backend, frontend, etc.)
- Container configurations (
config/) - Python local code (
local_code/) - MCU firmware (if included in package)
# All containers running
docker compose ps
# No errors in logs
docker compose logs --tail=20
# CAN-to-MQTT bridge running
sudo systemctl status cantomqtt.service
# Deployment watcher running (for cloud OTA updates)
sudo systemctl status deployment-watcher.service
# Map watcher running (for PWA map bundle uploads)
sudo systemctl status map-watcher.service
# API responding
curl -k https://localhost/api/health
# Web UI accessible
curl -k -o /dev/null -s -w "%{http_code}" https://localhost/# Check logs for specific service
docker compose logs <service-name>
# Services: backend, frontend, mosquitto, mongodb
# Restart all containers
docker compose down && docker compose up -d --no-build
# Photon (search) and Valhalla (routing) are behind the `maps` profile
# and only start when a map bundle is installed. To include them:
docker compose --profile maps up -dThe photon and valhalla containers are profile-gated — they only start
when data/maps/current exists (a bundle has been uploaded and applied).
If either service returns 503:
# Are they running?
docker ps | grep -E "photon|valhalla"
# If not, and a bundle IS installed:
docker compose --profile maps up -d photon valhallaIf no bundle is installed yet, upload one via the PWA Maps page — see DOCS/UpdatingMaps.md.
# Check service status
sudo systemctl status cantomqtt.service
sudo journalctl -u cantomqtt.service -f
# Verify CAN bus interface
ip link show can0
# Check local_code .env has correct external hostname
grep MQTT_BROKER_URL ~/local_code/.env# Check service status and logs
sudo systemctl status deployment-watcher.service
sudo journalctl -u deployment-watcher.service -f
# Verify cloud is configured via the PWA (Settings > Cloud Configuration)
# The watcher logs will show "Cloud not enabled" or "config incomplete" if not set updf -h
docker system prune -f # Removes unused images, preserves data/nslookup trailcurrent01.local
ping trailcurrent01.local- Initial Device Setup: CM5/SETUP.md
- Firmware Integration: FIRMWARE_SETUP.md
- OTA System Details: OTA_DEPLOYMENT_IMPLEMENTATION.md (includes MCU firmware OTA and cloud-to-Pi deployment watcher)
- Development: README.md
- Maps (tiles, search, routing): DOCS/UpdatingMapTiles.md (during the offline-maps migration, tiles/search/routing all ship as one map bundle uploaded via the PWA Maps page — Nominatim is no longer used)