Orpheus Deployment Quick Reference¶
Run all make targets as your normal user — never sudo make. Targets
that need elevated privileges (installing systemd units, deploying to
/opt/orpheus/) invoke sudo internally for just those steps, and
make install actively rejects running as root.
First-Time Setup (Jetson)¶
# 1. Clone repository
cd ~ && mkdir -p runtime/orpheus
cd runtime/orpheus
git clone https://github.com/scottchronicity/orpheus.git .
# 2. Install platform library (REQUIRED FIRST)
cd platform/orpheus-common
make install-service
# 3. Configure for your deployment
sudo nano /opt/orpheus/config/orpheus.yaml
# 4. Install and start everything else
cd ~/runtime/orpheus
make services-install # every component, including the classifiers and correlator
make services-start
Install the whole set. Audio-motion without audio-events, bird-detection,
crow-detection and event-correlator gives you a station that records clips
and never identifies anything — see
Installation for what each
component does and how to install a subset deliberately.
Root Makefile Commands (Recommended)¶
The repository includes a root Makefile for easy orchestration across all services and agents:
# Quick deployment from root directory
cd ~/runtime/orpheus
make status-all # Check status of all services
make update-all # Update all services and agents after git pull
make logs-all # Stream logs from all Orpheus services
# Full installation (first-time setup)
make services-install # Install all systemd services
make services-start # Start all services
# Service control
make services-stop # Stop all services
make services-restart # Restart all services
Updating After Git Pull¶
One-time cleanup: removing the legacy orpheus-dashboard service¶
The orpheus-dashboard service was retired — all of its
responsibilities now live in orpheus-ui (services/orpheus_ui/),
which serves the UI (now on port 8082). Existing Jetson deployments
still have the systemd unit installed and enabled from a previous
release. After your first git pull past commit 10c32ce, do this
on each host before running make update-all:
sudo systemctl stop orpheus-dashboard
sudo systemctl disable orpheus-dashboard
sudo rm -f /etc/systemd/system/orpheus-dashboard.service
sudo systemctl daemon-reload
status-all should then show orpheus-ui listening on port 8082
without the failed-state orpheus-dashboard entry. This is a one-
time step — once the unit file is gone, future make update-all runs
proceed normally.
Update Platform + Config¶
cd ~/runtime/orpheus/platform/orpheus-common
git pull
make install-service # Redeploys library, and leaves an existing
# /opt/orpheus/config/orpheus.yaml alone
make update is not the config path. It depends on force-update-config,
which overwrites the live config, and which cannot currently run at all —
it copies config/orpheus.yaml relative to platform/orpheus-common/, a
directory that does not exist in the repo. Edit
/opt/orpheus/config/orpheus.yaml in place instead.
Update Specific Service¶
cd ~/runtime/orpheus/services/orpheus_ui
make update # pulls the whole monorepo, reinstalls, rebuilds the frontend
# (needs Node on PATH) and restarts. `make update-all` is the
# orchestrated version across every component.
Update Agent¶
cd ~/runtime/orpheus/agents/orpheus-agent-audio-motion
git pull
make install-service
sudo systemctl restart orpheus-agent-audio-motion
Check Status¶
# All services
systemctl status orpheus-backplane orpheus-ui orpheus-agent-audio-motion orpheus-agent-video-snapshotter orpheus-agent-video-timelapser
# Logs
sudo journalctl -u orpheus-agent-audio-motion -f
sudo journalctl -u orpheus-agent-video-timelapser -f
# Dashboard
http://<jetson-ip>:8082
After a deploy: check retention¶
make services-install installs and enables orpheus-storage-sweep.timer along
with the platform library. It is the only thing on the station that deletes a
recording — no agent trims its own directory — so it is worth one look after every
deploy:
systemctl status orpheus-storage-sweep.timer # must be active, next elapse within 15 min
make storage-report # per-category table; deletes nothing
The orpheus-storage-sweep.service the timer triggers is a Type=oneshot and
reads inactive (dead) between runs. That is correct — make verify-deploy
checks the timer rather than the one-shot for exactly this reason, and reports the
last run's result.
It runs from /opt/orpheus/platform/orpheus-common/venv, which the platform's
own installer builds; make storage-report uses that same interpreter when one
is deployed, so the dry run answers for the code that will do the deleting
rather than for your working tree. make verify-deploy checks that every
installed unit's ExecStart binary exists — a one-shot with a missing
interpreter is invisible to is-active, since it is idle between runs either
way.
storage-report should show each category under its ceiling with "nothing to do".
For the first 24 hours after the timer is installed
(storage.retention.first_run_grace_hours) the sweep is report-only: it writes
its report, logs at CRITICAL if it would have removed anything, and deletes
nothing. That is the window in which to correct a ceiling you disagree with.
orpheus-storage-sweep --force starts enforcement early;
sudo systemctl disable --now orpheus-storage-sweep.timer or
storage.retention.sweep_enabled: false stops all deletion. See
Data & retention.
Architecture¶
/opt/orpheus/
├── config/
│ └── orpheus.yaml # Single source of truth
├── platform/
│ └── orpheus-common/ # Shared library
├── services/
│ ├── orpheus_ui/
│ └── orpheus-backplane/
└── agents/
├── orpheus-agent-audio-motion/
├── orpheus-agent-video-snapshotter/
└── orpheus-agent-video-timelapser/
Each service has its own venv but shares orpheus-common + config
Config Search Order¶
The full search order is documented once, in
Installation — it begins with
$ORPHEUS_CONFIG_PATH and $ORPHEUS_CONFIG_DIR, and notes that there is no
~/.config/orpheus/ lookup.
Service Names¶
orpheus-backplane- Messaging backplane (NATS + JetStream by default; the mosquitto fallback installs under the same unit name, so dependencies resolve either way)orpheus-ui- Web UI (FastAPI + React)orpheus-agent-audio-motion- Audio motion detectororpheus-agent-video-snapshotter- Periodic camera snapshotsorpheus-agent-video-timelapser- Timelapse video generationorpheus-storage-sweep.timer- Retention. Drives aType=oneshotorpheus-storage-sweep.serviceevery 15 minutes; enable the timer, never the service, or it runs once at boot and never again
Systemd Service Design Principles¶
All Orpheus services follow these patterns:
Time Synchronization¶
Most services include After=time-sync.target so the system clock is synchronized
before they start. Three units do not yet: orpheus-agent-video-snapshotter,
orpheus-agent-video-timelapser, and orpheus-bluetooth-autoconnect. Ordering
against the clock prevents:
- Log timestamps showing Unix epoch (1970)
- Incorrect timestamps in recorded events
- Issues with certificate validation
Loose Coupling¶
Services use Wants= instead of Requires= for inter-service dependencies:
# ✅ Correct: Loose coupling
After=network.target time-sync.target orpheus-backplane.service
Wants=orpheus-backplane.service
# ❌ Incorrect: Tight coupling
After=network.target orpheus-backplane.service
Requires=orpheus-backplane.service
Why Wants= over Requires=:
- Services can start even if dependencies aren't running
- If the backplane crashes, agents aren't forcibly stopped
- Dashboard remains accessible for diagnostics even if the backplane is down
- Each service handles connection failures gracefully in code
Subscriptions Survive a Cold or Flapping Backplane¶
Loose coupling only works if a service that starts before the backplane ends up genuinely subscribed once the backplane appears. The NATS bus enforces that:
- Each connection attempt is bounded by
connect_timeout(10s).nats-pydoes not fail fast on a cold broker — it sits in its own retry loop and never returns — so a service starting first pauses for up to that long, then comes up disconnected and keeps retrying in the background. An unbounded attempt is why a cold-broker start used to end as a hard connect failure rather than the documented self-healing start. - A
subscribe()issued before the connection exists is buffered and applied on attach. - A registration the broker refuses is retried by a background sweep, and again
after any reconnect.
nats-pyrestores only the subscriptions it registered itself, so one it never accepted would otherwise stay dead for the life of the process. - Every application logs which subjects it restored, and warns (at
warning) about any that are still not registered while the connection is up.
Check what a running service is actually subscribed to — not just whether it is
connected — via the UI's /api/diagnostics/bus, whose subscriptions block
reports subscribed, pending, and requested. A non-empty pending while
connected is true means that service is not receiving those subjects.
Starting the backplane before its consumers is still the right order (it avoids a window of dropped publishes), but the cost of getting it wrong is now a short recoverable gap rather than a service that silently serves stale data until someone restarts it.
Restart Policy¶
All services use Restart=always or Restart=on-failure with appropriate RestartSec= delays to handle transient failures without overwhelming the system.
Systemd Journal Logging¶
Orpheus services automatically integrate with systemd journal for production logging.
Requirements¶
To enable journal integration on Jetson, install systemd-python:
sudo apt-get install -y libsystemd-dev pkg-config
/opt/orpheus/<component>/venv/bin/pip install systemd-python>=235
This step is optional - services will work without it, logging to stdout/stderr. With systemd-python installed, logs are sent directly to the journal with proper metadata and also to stdout for backwards compatibility.
Viewing Logs¶
# Follow logs for a service
sudo journalctl -u orpheus-ui -f
# View logs from the last hour
sudo journalctl -u orpheus-agent-audio-motion --since "1 hour ago"
# View logs from all Orpheus services
sudo journalctl -u "orpheus-*" -f
Log Behavior¶
- Development mode (not under systemd): Logs to console with timestamps
- Production mode (under systemd without systemd-python): Logs to stdout/stderr, captured by journal
- Production mode (under systemd with systemd-python): Logs directly to journal with structured metadata + stdout
See INSTALLATION.md for full details.