Full P25 transcription stack in a single docker compose up. Includes trunk-recorder, tr-engine, tr-dashboard, imbe-asr, PostgreSQL, and Mosquitto.
Security defaults changed. There is no default database password, MQTT requires a login, and Caddy's bind address (
BIND_IP) must be set explicitly. Existing installs: follow Security defaults changed before upgrading.
| Service | Image | Purpose |
|---|---|---|
trunk-recorder |
ghcr.io/trunk-reporter/trunk-recorder |
P25 scanner with all common plugins |
tr-engine |
ghcr.io/trunk-reporter/tr-engine |
Backend API + call ingestion + transcription routing |
tr-dashboard |
ghcr.io/trunk-reporter/tr-dashboard |
Live web UI |
imbe-asr |
ghcr.io/trunk-reporter/imbe-asr-server |
IMBE vocoder → text (no audio reconstruction) |
postgres |
postgres:17-alpine |
Database |
mosquitto |
eclipse-mosquitto:2 |
MQTT broker |
git clone https://github.com/trunk-reporter/tr-stack.git
cd tr-stack
# 1. Create secrets: database password + MQTT login (there are no defaults)
cp sample.env .env && chmod 600 .env
export MQTT_USERNAME=trengine MQTT_PASSWORD=$(openssl rand -hex 16)
sed -i -e "s|^POSTGRES_PASSWORD=.*|POSTGRES_PASSWORD=$(openssl rand -hex 24)|" \
-e "s|^MQTT_USERNAME=.*|MQTT_USERNAME=$MQTT_USERNAME|" \
-e "s|^MQTT_PASSWORD=.*|MQTT_PASSWORD=$MQTT_PASSWORD|" .env
# same login for trunk-recorder's MQTT plugins in config.json
sed -i "s|CHANGE_ME_MQTT_PASSWORD|$MQTT_PASSWORD|g" config.json
# same login for the broker (chown: mosquitto can't read a root-owned file)
docker run --rm -e MQTT_USERNAME -e MQTT_PASSWORD -v "$PWD/mosquitto:/mosquitto/config" eclipse-mosquitto:2 \
sh -c 'mosquitto_passwd -c -b /mosquitto/config/passwd "$MQTT_USERNAME" "$MQTT_PASSWORD" && chown mosquitto:mosquitto /mosquitto/config/passwd'
# 2. Edit .env — set BIND_IP and SITE_ADDRESS
# BIND_IP: the address Caddy listens on, usually your server's LAN IP
# SITE_ADDRESS: http://YOUR_SERVER_IP (what your browser uses to reach it)
# 3. Edit config.json — set your SDR source, frequency, and system details
# See: https://trunkrecorder.com/docs/intro for trunk-recorder config reference
# 4. Add your talkgroup CSV
mkdir -p talkgroups
cp /path/to/your/talkgroups.csv talkgroups/talkgroups.csv
# 5. Start
docker compose up -d
# 6. Watch logs
docker compose logs -f tr-engineCompose refuses to start until POSTGRES_PASSWORD, MQTT_PASSWORD and BIND_IP are set and mosquitto/passwd exists. That's deliberate — see Security Defaults.
Dashboard: http://YOUR_SERVER_IP (served by Caddy on port 80)
API: http://YOUR_SERVER_IP/api/v1
Auth is disabled by default — no login required for trusted local installs. See Securing for Public Access before setting BIND_IP=0.0.0.0 or otherwise exposing this externally.
Copy sample.env to .env and set your values. Minimum required (the Quick Start fills in the secrets for you):
BIND_IP=192.168.1.100 # address Caddy listens on (LAN IP, 127.0.0.1, or 0.0.0.0)
SITE_ADDRESS=http://192.168.1.100 # your server's IP or hostname
POSTGRES_PASSWORD=... # openssl rand -hex 24
MQTT_USERNAME=trengine
MQTT_PASSWORD=... # openssl rand -hex 16, must match mosquitto/passwd and config.jsonAuth is in open mode by default when both AUTH_TOKEN and ADMIN_PASSWORD are unset. The dashboard is accessible without login. See Securing for Public Access to enable auth.
If you're exposing the stack to the internet, use full auth:
ADMIN_PASSWORD= # dashboard login password
# Optional public read token returned by /api/v1/auth-init:
# AUTH_TOKEN= # openssl rand -base64 32When ADMIN_PASSWORD is set, tr-engine runs in full mode. The dashboard uses JWT login for writes and can use the optional AUTH_TOKEN as a public read token through /api/v1/auth-init. Caddy does not inject auth headers.
Edit config.json to match your SDR hardware and radio system. Key fields:
sources— your SDR device, center frequency, sample rate, gainsystems— P25 control channel frequency, system type, talkgroup CSV pathplugins— pre-configured for MQTT + DVCF, updatebrokerif using external MQTT
The broker in the plugin config points to the internal mosquitto container — leave as-is unless you're using an external broker. Each MQTT plugin (mqtt_status, mqtt_dvcf, mqtt_avcf) needs the broker login in its username/password fields; the Quick Start's sed replaces the CHANGE_ME_MQTT_PASSWORD placeholders with your MQTT_PASSWORD.
Place your talkgroup CSV at talkgroups/talkgroups.csv. RadioReference format works directly.
Models download from Hugging Face on first run (~560MB for P25 fine-tuned model). The download happens inside the container on startup — check logs with docker compose logs imbe-asr.
To pre-download:
pip install huggingface_hub
python3 -c "
from huggingface_hub import snapshot_download
snapshot_download('trunk-reporter/imbe-asr-base-512d-p25', local_dir='data/models')
"To use the large model (better accuracy on clean speech, 290M params):
python3 -c "
from huggingface_hub import snapshot_download
snapshot_download('trunk-reporter/imbe-asr-large-1024d', local_dir='data/models')
"Then set IMBE_ASR_LM_ALPHA=0.7 and IMBE_ASR_LM_BETA=2.0 in .env.
The stack runs on Raspberry Pi 5 (arm64) using CPU-only inference. A docker-compose.pi.yml override handles all Pi-specific configuration automatically.
- Raspberry Pi 5 (4GB+ RAM recommended, 8GB ideal)
- 64-bit Raspberry Pi OS (Bookworm or later)
- Docker Engine + Docker Compose v2 installed
- RTL-SDR or compatible SDR dongle
# Same setup steps as Quick Start, then:
docker compose -f docker-compose.yml -f docker-compose.pi.yml up -dThe Pi override:
- Switches imbe-asr to the
:cpuimage tag (multi-arch, no GPU drivers needed) - Uses the smaller
imbe-asr-base-512dmodel by default (lower memory, faster on ARM) - Sets
IMBE_ASR_DEVICE=cpu - Removes GPU reservation and privileged mode
RTL-SDR is the most common SDR for Pi deployments. Your config.json source settings will differ from x86 setups — make sure to set the correct device index and gain for your dongle. See the trunk-recorder docs for source configuration.
CPU inference on Pi is significantly slower than GPU. Expect higher latency on transcriptions. The base model (imbe-asr-base-512d) is recommended over the P25-tuned or large models to keep inference time reasonable. Monitor memory usage — if the Pi runs out of RAM, consider reducing IMBE_ASR_BEAM_WIDTH in .env.
Note: The GPU section below does not apply to Pi deployments.
The default stack runs imbe-asr on CPU so first-run works without NVIDIA drivers. For GPU acceleration, use the GPU override:
docker compose -f docker-compose.yml -f docker-compose.gpu.yml up -ddocker compose pull && docker compose up -dIf you're pulling a new version of this repo, read Security defaults changed first.
| Service | Host port | Bind address | Notes |
|---|---|---|---|
| Caddy (dashboard + API) | HTTP_PORT (80) |
BIND_IP (required) |
tr-engine's API is served at /api through Caddy |
| Mosquitto MQTT | 1883 | MQTT_BIND_IP (default 127.0.0.1) |
Login required. Set MQTT_BIND_IP only for trunk-recorders on other hosts |
| PostgreSQL | — | never published | Use docker compose exec postgres psql -U trengine trengine |
| tr-engine, tr-dashboard, imbe-asr | — | never published | Reached through Caddy / the compose network |
- No default database password.
POSTGRES_PASSWORDmust be set in.env, or compose refuses to start. - PostgreSQL is never published on the host.
- MQTT always requires a login.
mosquitto/mosquitto.confhasallow_anonymous falseand readsmosquitto/passwd; tr-engine usesMQTT_USERNAME/MQTT_PASSWORD, trunk-recorder uses theusername/passwordfields inconfig.json. - No accidental all-interface binds. Caddy binds to
BIND_IP, which you must set. Mosquitto binds to127.0.0.1unless you setMQTT_BIND_IP. Prefer a LAN or VPN address over0.0.0.0. .envandmosquitto/passwdare gitignored. Don't commit them.
To change the MQTT password: update MQTT_PASSWORD in .env and the three password fields in config.json, rerun the docker run ... mosquitto_passwd command from the Quick Start, then docker compose up -d && docker compose restart mosquitto trunk-recorder.
Earlier versions of this stack defaulted the database password to trengine (or change-me from sample.env), allowed anonymous MQTT, and bound Caddy to every interface. When you update an existing install:
-
Rotate the database password inside PostgreSQL first. Postgres only reads
POSTGRES_PASSWORDwhen it createsdata/db, so changing.envalone locks tr-engine out. Your current password is whateverPOSTGRES_PASSWORDwas in.envwhen the database was created:change-meif you kept the sample value, ortrengineif the variable was unset. Rotate it while the old stack is still running:NEW_PG_PASSWORD=$(openssl rand -hex 24) docker compose exec postgres psql -U trengine -d trengine \ -c "ALTER ROLE trengine PASSWORD '$NEW_PG_PASSWORD'" sed -i "s|^POSTGRES_PASSWORD=.*|POSTGRES_PASSWORD=$NEW_PG_PASSWORD|" .env grep -q '^POSTGRES_PASSWORD=' .env || echo "POSTGRES_PASSWORD=$NEW_PG_PASSWORD" >> .env
The command uses the container's local socket, so it doesn't need the old password. If you've already pulled the new
docker-compose.ymland it refuses to parse, prefix thedocker compose execline withPOSTGRES_PASSWORD=placeholder MQTT_PASSWORD=placeholder BIND_IP=127.0.0.1;execonly runs a command in the existing container. Keeping the old value in.envalso works, since PostgreSQL isn't published, but rotate it as soon as you can. -
Create the MQTT login. Set
MQTT_USERNAME/MQTT_PASSWORDin.env, put the same values in theusername/passwordfields of the MQTT plugins inconfig.json, and createmosquitto/passwdwith thedocker run ... mosquitto_passwdcommand from the Quick Start.mosquitto/mosquitto.conffrom this repo now requires a login; if you edited your copy, setallow_anonymous falseandpassword_file /mosquitto/config/passwd. Remote trunk-recorders need the same login in their plugin config. -
Choose
BIND_IP. It used to default to0.0.0.0. Set it to your server's LAN IP (or0.0.0.0if you really want every interface, withADMIN_PASSWORDset). If remote trunk-recorders publish to this broker, you should already haveMQTT_BIND_IPset; keep it. -
docker compose pull && docker compose up -d, then checkdocker compose logs tr-engine --tail 30formqtt connected, subscribingand no database errors.
Full step-by-step setup: docs.luxprimatech.com/#/imbe-asr-setup
See the Trunk Reporter Roadmap for the cross-repo project tracker with priorities and phases.
- tr-docker — trunk-recorder image source + CI
- tr-engine — backend source
- imbe-asr — ASR model source
- tr-plugin-dvcf — DVCF plugin
- symbolstream — live streaming plugin