Deployment
This guide covers single-host deployment in detail; for cluster deployments see Containers & Kubernetes below.
This guide covers deploying OpenRobOps for production use.
Architecture Components
A production OpenRobOps deployment consists of four services:
| Service | Technology | Port | Notes |
|---|---|---|---|
| Web App | Meteor 3 (Node.js) | 3000 | UI, REST API, WebSocket pub/sub |
| MongoDB | MongoDB | 3001 | Primary data store |
| MQTT Broker | Mosquitto + go-auth | 1883 / 9001 | Robot agent communication |
| Ingest | Node.js (Babel) | — | Telemetry processing |
Docker Compose Setup
The MQTT broker runs via Docker Compose. From the mqtt/ directory:
docker compose up -d
The docker-compose.yml uses the ghcr.io/openrobops/mosquitto-go-auth image, which provides Mosquitto with a MongoDB-backed authentication plugin. The container runs with network_mode: host so the auth plugin can reach Meteor's MongoDB on localhost:3001.
Mosquitto Configuration
The broker is configured in mqtt/mosquitto/mosquitto.conf:
- Port 1883 — standard MQTT protocol
- Port 9001 — WebSocket protocol (for browser-based clients)
- Authentication — uses the
mosquitto-go-authplugin querying themqtt_credentialscollection in MongoDB - Password hashing — PBKDF2 with SHA-512, 100,000 iterations, 16-byte salt
Configuration with Terraform
OpenRobOps uses Terraform to generate settings.json files for the web app and ingest service. This ensures consistent configuration across services.
# Preview settings changes
./scripts/generate-settings.sh --plan
# Generate/apply settings
./scripts/generate-settings.sh
# Force-rotate all generated secrets (invalidates connected robots)
./scripts/generate-settings.sh --clean
Secrets (the MQTT master password, credential encryption key, peer key, and
robot API key) are Terraform random_* resources persisted in state — re-running
the script preserves them; only --clean rotates them.
Key Configuration Variables
Defined in terraform/variables.tf:
| Variable | Description |
|---|---|
hostname | Host used in generated service URLs |
mqtt_port / mqtt_websocket_port | MQTT broker ports |
mqtt_master_username | Username for the ingest service's MQTT superuser |
mongo_url / mongo_db | MongoDB connection for ingest |
peer_client_url | URL ingest uses to reach the web app's peer API |
oauth_google_client_id / oauth_google_secret | Google OAuth credentials |
oauth_github_client_id / oauth_github_secret | GitHub OAuth credentials |
smtp_url | SMTP server for passwordless email login |
admin_emails | Emails auto-granted the admin role on first login (see Access Control) |
upstream_* | Upstream forwarding settings (see Upstream Forwarding) |
Override defaults in terraform/local.tfvars. Secrets such as
mqtt_master_password and the credential encryption key are not variables —
they are auto-generated by Terraform.
Environment Overview
Settings Files
| File | Used By | Generated By |
|---|---|---|
app/settings.json | Meteor app server | Terraform |
ingest/settings.json | Ingest service | Terraform |
terraform/local.tfvars | Terraform | Manual (user creates) |
Ports Reference
| Port | Protocol | Service |
|---|---|---|
| 3000 | HTTP/WS | Web app (Meteor) |
| 3001 | MongoDB wire | MongoDB (Meteor-managed) |
| 1883 | MQTT | Mosquitto broker |
| 9001 | WebSocket | Mosquitto broker (WS) |
Running in Production
Starting Services
The quickest way to launch all three services locally is the helper script, which
opens a tmux session named oro with one tab per service (each running its run.sh):
./scripts/start-local-env.sh
This requires tmux. Stop everything with ./scripts/stop-local-env.sh.
To run the components independently (or on separate hosts), start each one directly:
# MQTT broker
cd mqtt && ./run.sh
# Web app + MongoDB
cd app && ./run.sh
# Ingest service
cd ingest && ./run.sh
Monitoring
- Web app logs — stdout from the Meteor process
- Ingest logs — stdout from the Node.js process, includes MQTT connection status
- MQTT broker logs — Docker container logs (
docker compose logs -f)
Settings Validation
The web app validates settings.json on startup and refuses to start if
anything is invalid, reporting every problem at once
(Invalid application settings — refusing to start: ...). The checked
invariants:
- the
mqttsettings object is present mqtt.credentialEncryptionKeyis exactly 64 hex characters (an AES-256-GCM key)mqtt.brokersis non-emptymqtt.defaultBrokerIdnames one of the brokers
Terraform-generated settings always satisfy these — a validation failure usually means a hand-edited file. Note the ingest service is not covered by this check; it logs and continues instead.
Versions & Releases
The app and ingest are versioned and released together from a git tag (see
RELEASING.md in the repo). The running version appears in the startup log
and in the Settings sidebar footer. Telemetry relayed upstream carries an
agentVersion stamped with a +oro-<version> suffix (see
Upstream Forwarding).
Containers & Kubernetes
CI builds and publishes container images to GHCR on main and release tags:
ghcr.io/openrobops/oro-app and ghcr.io/openrobops/oro-ingest (build cache
also lives in GHCR). Local equivalents: scripts/build-app-image.sh,
scripts/build-ingest-image.sh, and scripts/smoke-test-app-image.sh.
Kubernetes manifests ship in k8s/ (namespaces, deployments, services,
ingress) for the app, ingest, and Mosquitto:
kubectl apply -f k8s/app/ -f k8s/ingest/ -f k8s/mosquitto/
Configuration differs from the single-host layout:
| Component | Config source |
|---|---|
| App | METEOR_SETTINGS env (the settings.json JSON) from a Secret; ROOT_URL from a ConfigMap |
| Ingest | Secret mounted as /app/settings.json |
| Mosquitto | Config from its manifest/secret |
Copy each secret.example.yaml to a real Secret with your generated values
before applying. Container images run the same code as the local processes, so
the settings content itself is unchanged.
:::caution Websocket timeouts
The app serves Meteor DDP over long-lived websockets. Whatever fronts it must
allow long-running connections: the sample ingress sets nginx's
proxy-read-timeout/proxy-send-timeout to 3600s, but on GKE with a
GCE-class ingress those annotations are silently ignored and the load
balancer's 30-second default kills every session, leaving clients in a
reconnect/resubscribe loop. On GKE, attach a BackendConfig with
timeoutSec: 3600 to the oro-app Service instead (see the note in
k8s/app/ingress.yaml).
:::
Security Considerations
- Change all default passwords generated by Terraform
- Restrict MongoDB access to localhost or trusted networks
- Use TLS termination (reverse proxy) in front of port 3000 for production
- Configure OAuth with proper redirect URIs for your production domain
- The MQTT broker should not be directly exposed to the public internet without TLS
Next Steps
- System Architecture — detailed component diagram
- Access Control — authentication and authorization
- Project Structure — repository layout