Production deployment with Caddy (automatic HTTPS), Mosquitto (MQTT broker), tr-dashboard (standalone frontend), and Prometheus metrics — all managed by a single Docker Compose file.
Other installation methods:
- Docker Compose (all-in-one) — quick setup with bundled PostgreSQL and MQTT
- Docker with existing MQTT — connect to a broker you already run
- Build from source — compile everything yourself
- Binary releases — download a pre-built binary
Internet
│
┌──────────┴──────────┐
│ Caddy :443 │ ← auto HTTPS (Let's Encrypt / Cloudflare)
│ reverse proxy │
└──┬──────────────┬───┘
│ │
api.example.com dashboard.example.com
│ │
┌──────┴───┐ ┌─────┴──────┐
│ tr-engine │ │tr-dashboard│
│ :8080 │ │ :3000 │
└──┬────┬───┘ └────────────┘
│ │
┌──────┴┐ ┌┴─────────┐
│Postgres│ │Mosquitto │ ← trunk-recorder publishes here
│ :5432 │ │ :1883 │
└────────┘ └──────────┘
postgres-exporter :9187 ← Prometheus scrapes this
| Service | Purpose | Exposed Port |
|---|---|---|
| caddy | Reverse proxy, auto HTTPS | BIND_IP:80, BIND_IP:443 |
| tr-engine | API + web UI | internal only (behind Caddy) |
| tr-dashboard | Standalone dashboard frontend | internal only (behind Caddy) |
| postgres | Database | internal only |
| mosquitto | MQTT broker | BIND_IP:1883 |
| postgres-exporter | Prometheus metrics for PostgreSQL | BIND_IP:9187 |
- Docker and Docker Compose (v2+)
- A dedicated IP address (or host) for binding ports — set as
BIND_IP - Two DNS A records pointing to that IP (for Caddy to issue HTTPS certificates)
- trunk-recorder with the MQTT Status plugin
mkdir tr-engine && cd tr-engine
curl -sO https://raw.githubusercontent.com/trunk-reporter/tr-engine/master/docker-compose.full.yml
mv docker-compose.full.yml docker-compose.yml
curl -sO https://raw.githubusercontent.com/trunk-reporter/tr-engine/master/sample.env
cp sample.env .envEdit .env and set these values:
# REQUIRED — IP address to bind all external ports to
BIND_IP=203.0.113.10
# REQUIRED — your domain names (replace with your actual domains)
API_DOMAIN=api.example.com
DASHBOARD_DOMAIN=dashboard.example.com
# REQUIRED — MQTT credentials (trunk-recorder must use these too)
MQTT_USERNAME=trengine
MQTT_PASSWORD=your-mqtt-password-here
# STRONGLY RECOMMENDED — full API auth
ADMIN_PASSWORD=$(openssl rand -base64 32)
# Optional public read token returned by /auth-init.
AUTH_TOKEN=$(openssl rand -base64 32)
# Optional — match your TR plugin's topic prefix
MQTT_TOPICS=trengine/#Generate secure random secrets:
# Run these and paste the output into .env
openssl rand -base64 32 # → ADMIN_PASSWORD
openssl rand -base64 32 # → AUTH_TOKEN (optional public read token)
openssl rand -base64 16 # → MQTT_PASSWORDImportant: For public deployments, set
ADMIN_PASSWORDso writes require login, role checks, or API keys.AUTH_TOKENis optional in full mode; when set, it acts as a public read token returned by/auth-init.
Key variables:
| Variable | Required | Description |
|---|---|---|
BIND_IP |
Yes | IP address for all external port bindings |
API_DOMAIN |
Yes | Domain for the tr-engine API + built-in web UI |
DASHBOARD_DOMAIN |
Yes | Domain for the tr-dashboard frontend |
MQTT_USERNAME |
Yes | MQTT broker credentials (shared with trunk-recorder) |
MQTT_PASSWORD |
Yes | MQTT broker password |
ADMIN_PASSWORD |
Strongly recommended | Enables full auth, admin login, JWT sessions, and API keys |
AUTH_TOKEN |
Optional | Public read token in full mode, or shared token in token mode |
WRITE_TOKEN |
No | Deprecated legacy write token |
See sample.env for all available options. Settings like TR_DIR, transcription, file watch, and S3 storage are documented in the Docker Compose guide.
Create the config directory and files:
mkdir -p mosquittoCreate mosquitto/mosquitto.conf:
listener 1883
password_file /mosquitto/config/passwd
allow_anonymous false
Generate the password file (use the same password you put in .env):
docker run --rm -v ./mosquitto:/mosquitto/config eclipse-mosquitto:2 \
mosquitto_passwd -b -c /mosquitto/config/passwd trengine YOUR_MQTT_PASSWORDReplace YOUR_MQTT_PASSWORD with the value you set for MQTT_PASSWORD in .env.
Create the config directory:
mkdir -p caddyCreate caddy/Caddyfile (replace the domain names with yours):
api.example.com {
reverse_proxy tr-engine:8080
}
dashboard.example.com {
reverse_proxy tr-dashboard:3000
}The dashboard and built-in web pages discover auth mode through GET /api/v1/auth-init, so Caddy does not need to inject auth headers. If you are upgrading from an older config that has an @no_auth injection block, it is harmless to leave in place during migration, but it is no longer required.
Create two A records pointing to your BIND_IP:
| Record | Type | Value | Proxy |
|---|---|---|---|
api.example.com |
A | 203.0.113.10 |
Cloudflare proxied OK |
dashboard.example.com |
A | 203.0.113.10 |
Cloudflare proxied OK |
Cloudflare users: Set SSL/TLS mode to Full (not Full Strict, unless you've configured Caddy with Cloudflare origin certificates). Caddy handles the HTTPS certificate on the origin side.
Non-Cloudflare users: Caddy automatically obtains Let's Encrypt certificates. Just make sure ports 80 and 443 are reachable from the internet.
If your MQTT broker needs a DNS name (e.g. for trunk-recorder to connect by hostname):
| Record | Type | Value | Proxy |
|---|---|---|---|
mqtt.example.com |
A | 203.0.113.10 |
DNS only (gray cloud) |
MQTT is raw TCP — it cannot be proxied through Cloudflare.
docker compose up -dOn first run:
- PostgreSQL initializes and tr-engine auto-applies the database schema
- Caddy obtains HTTPS certificates (may take 30-60 seconds)
- Mosquitto starts with authentication enabled
- tr-engine connects to PostgreSQL and Mosquitto
# Check all services are running
docker compose ps
# Check tr-engine logs
docker compose logs tr-engine --tail 20
# Health check (use the API domain or localhost via docker)
curl -s https://api.example.com/api/v1/health | python3 -m json.tool
# Test MQTT connection from trunk-recorder's perspective
mosquitto_pub -h YOUR_BIND_IP -p 1883 -u trengine -P 'YOUR_MQTT_PASSWORD' -t test -m helloAccess your deployment:
- API + Web UI:
https://api.example.com - Dashboard:
https://dashboard.example.com
In your trunk-recorder config.json:
{
"plugins": [
{
"name": "MQTT Status",
"library": "libmqtt_status_plugin.so",
"broker": "tcp://YOUR_BIND_IP:1883",
"topic": "trengine/feeds",
"unit_topic": "trengine/units",
"console_logs": true,
"username": "trengine",
"password": "YOUR_MQTT_PASSWORD"
}
]
}Replace YOUR_BIND_IP and YOUR_MQTT_PASSWORD with your values. Systems and talkgroups auto-populate once trunk-recorder connects.
This guide covers the full-stack-specific setup. For shared configuration topics, see the Docker Compose guide:
- TR auto-discovery (TR_DIR) — auto-import talkgroup names and watch for new calls
- File watch mode (WATCH_DIR) — ingest calls from TR's audio directory
- Filesystem audio (TR_AUDIO_DIR) — serve audio directly from TR's filesystem
- Transcription (STT) — automatic speech-to-text for call recordings
- Custom web UI files — override the built-in web UI
To add volume mounts for these features, edit the tr-engine service in your docker-compose.yml — the commented-out examples are already there.
docker compose pull && docker compose up -dAll persistent data lives in bind-mounted directories (pgdata/, audio/) and named volumes (mosquitto-data, caddy-data). Check release notes for schema migrations.
# All services
docker compose logs -f
# Individual services
docker compose logs -f tr-engine
docker compose logs -f caddy
docker compose logs -f mosquitto# Stop (data preserved)
docker compose down
# Stop and delete all data (fresh start)
docker compose down -vCaddy certificate errors: Check that your DNS records are pointing to BIND_IP and that ports 80/443 are reachable. Run docker compose logs caddy to see certificate issuance status. If using Cloudflare, ensure SSL mode is set to Full.
MQTT authentication failures: Verify that the username/password in your TR config.json matches what's in the Mosquitto password file. The password file is generated separately from .env — regenerate it if you change MQTT_PASSWORD:
docker run --rm -v ./mosquitto:/mosquitto/config eclipse-mosquitto:2 \
mosquitto_passwd -b -c /mosquitto/config/passwd trengine NEW_PASSWORD
docker compose restart mosquittoWrite operations failing (403): Log in with an editor/admin user, or use an API key with write access. For upload plugins, create a tre_... API key and use it as the plugin API key. The legacy WRITE_TOKEN path still works during the deprecation period.
Dashboard can't reach the API: The tr-dashboard container connects to tr-engine via the Docker network (http://tr-engine:8080). Check that TR_AUTH_TOKEN in the compose file matches your AUTH_TOKEN.
Web UI prompts for token: In token mode, enter AUTH_TOKEN. In full mode, log in with your admin user. If you intended open mode, make sure both AUTH_TOKEN and ADMIN_PASSWORD are unset and the service has been restarted with docker compose up -d.