Scaling¶
Phased plan to grow from a small portal (MAX_ACTIVE_CALLS=2 on ~1 GB RAM) to 10+ concurrent calls per instance.
Checklist¶
- Confirm DMR backend supports 10+ concurrent MMDVM logins from one public IP
- Resize portal VM to 8 GB+ RAM / 4 vCPU; set
MAX_ACTIVE_CALLS=10 - Add startup orphan container sweep in sip-dmr-exchange (PR)
- Load test 10 simultaneous calls; monitor
/stats, packet loss, bridge boot time - When >10 or HA needed: second portal node, SIP LB, partitioned
DMR_LOCAL_PORTranges
Current architecture¶
flowchart LR
PBX[Your PBX] -->|UDP 5060 INVITE| Exchange[sip-dmr-exchange]
Exchange -->|docker.sock| Docker[Docker daemon]
Docker -->|spawn per call| Bridge1[sip-dmr-bridge]
Docker --> BridgeN[sip-dmr-bridge N]
Exchange -->|SIP B2BUA media| Bridge1
Bridge1 -->|MMDVM UDP| DMR[DMR server]
Per call cost (measured):
- ~130–150 MB RAM per bridge container
- ~10–15% CPU per active bridge under TX/RX
- ~5–15 s cold-start until Docker HEALTHCHECK passes
Software limits today:
- One
sip-dmr-bridgecontainer per call via localdocker.sock MAX_ACTIVE_CALLSenv cap; DMR local ports 62040–64040- Exchange B2BUA — RTP hairpins through exchange process
- In-memory state — no clustering; orphan containers possible after exchange restart
Phase 1 — Single bigger host (10–15 calls)¶
1. Size the VM¶
| Calls | Suggested RAM | Notes |
|---|---|---|
| 10 | 8 GB | ~1.5 GB bridges + ~1 GB exchange + OS headroom |
| 15 | 12–16 GB | Buffer for spikes and build cache |
CPU: 4 vCPU minimum for 10 concurrent bridges.
2. Exchange tuning¶
In .env:
MAX_ACTIVE_CALLS=10(or 12 with headroom)HOST_IP= public VIP- Default DMR port range is sufficient at this scale
3. Ops¶
- Thin rebuild for routine deploys (avoid full PJSIP compile on box)
- Post-deploy: compare
docker pswith/bridgesfor orphans - Monitor
GET /stats— alert memory > 85% - Periodic
docker system pruneon small disks
4. Optional code improvements (sip-dmr-exchange PRs)¶
| Change | Why |
|---|---|
| Startup orphan reconciliation | Kill bridge containers not tracked after exchange restart |
mem_limit="512m" on bridge containers |
Prevent one runaway bridge OOMing the host |
| DMR port bind-verify before assign | Avoid rare UDP collisions |
5. Validate¶
- 10 simultaneous test calls from PBX
- Watch MMDVM logs for packet loss
- Confirm DMR server accepts 10 concurrent gateway logins from one IP
Phase 2 — Multi-node¶
flowchart TB
PBX[PBX] --> SIPLB[SIP LB]
SIPLB --> NodeA[portal-a]
SIPLB --> NodeB[portal-b]
NodeA --> BridgesA[bridges A]
NodeB --> BridgesB[bridges B]
BridgesA --> DMR[DMR server]
BridgesB --> DMR
Option A — N identical portal nodes¶
- Each VM: exchange + Docker + optional nginx dashboard
- PBX trunk → SIP proxy (Kamailio/OpenSIPS) with call stickiness
- Per node:
MAX_ACTIVE_CALLS=5, partitionedDMR_LOCAL_PORT_*
Option B — Dedicated bridge workers¶
- Exchange provisions bridges on remote hosts via new backend (SSH/API)
- More engineering; exchange dials worker bridge network
Option C - Explore Open DMR / VAFM HomeBrew DMR¶
- Would probably require a refactor of sip-dmr-bridge, unlikely option
Option D - Optimise current stack¶
- Optimise sip-dmr-bridge for less RAM usage, CPU usage
Software milestones¶
- Orphan reconciliation + metrics
NODE_DMR_PORT_START/NODE_DMR_PORT_ENDper node- Optional Prometheus
/metrics - Later: Kubernetes or remote Docker backend
PBX / network¶
- SIP LB in front of worker pool
- Firewall UDP 5060 from PBX to each worker
- Same
SIP_AUTH_TOKENon all workers HOST_IPper worker = that node's public IP
Suggested rollout¶
- Confirm DMR backend capacity for target concurrent logins
- Resize VM; set
MAX_ACTIVE_CALLS=10; merge orphan sweep PR - Load test; tune memory limits
- Add second node + SIP LB when single host is insufficient
Last update: August 17, 2026 22:08:04
Last updated by: Shane Daley
Last updated by: Shane Daley
Help improve this page by contributing to our documentation.