Wholesale Cluster Configuration Reference
Complete configuration reference for a 3-node Wholesale cluster with MySQL or PostgreSQL. Each node’s config is shown in full, highlighting only the fields that differ between nodes.
Network Topology
┌──────────────────────────────────────────────────────────────┐
│ SIP Clients / Carrier Gateways │
│ (via SIP Load Balancer or direct to node IPs) │
└──────────┬──────────────────────────────────┬───────────────┘
│ │
┌──────▼──────┐ ┌──────▼──────┐
│ Node 1 │◄── SIP MESSAGE ──►│ Node 2 │
│ 10.0.0.11 │ │ 10.0.0.12 │
│ Primary │ │ Secondary │
│ + SipFlow │ │ │
└──────┬──────┘ └──────┬──────┘
│ │
│ ┌──────────────┐ │
└────────►│ MySQL │◄────────┘
│ 10.0.0.20 │
└──────────────┘
- SipFlow: co-located on Node 1 only (local mode)
- 2-node minimum for HA; 3+ recommended for production
Node 1 — 10.0.0.11 (Primary + SipFlow)
config.toml
# ── General ────────────────────────────────────────────────────
http_addr = "0.0.0.0:8080"
# https_addr = "0.0.0.0:8443" # Uncomment for HTTPS
database_url = "mysql://rustpbx:WsDbP@ss2026@10.0.0.20:3306/rustpbx"
external_ip = "10.0.0.11" # ← Node-specific
log_level = "info"
# log_file = "/var/log/rustpbx.log"
# log_rotation = "daily"
# ── RTP ────────────────────────────────────────────────────────
rtp_start_port = 20000 # UDP range for RTP media
rtp_end_port = 40000
# ── WebRTC (optional) ─────────────────────────────────────────
# webrtc_port_start = 30000
# webrtc_port_end = 40000
# ── SIP Proxy ──────────────────────────────────────────────────
[proxy]
addr = "0.0.0.0"
udp_port = 5060
# tcp_port = 5060 # SIP over TCP
# tls_port = 5061 # SIP over TLS (requires ssl_certificate)
# ws_port = 5062 # SIP over WebSocket
modules = ["acl", "auth", "registrar", "call"]
media_proxy = "auto" # all | auto | nat | none | bypass
max_concurrency = 1000
# SIP realms for NAT fix — list all node IPs + public VIP
realms = ["10.0.0.11", "10.0.0.12", "203.0.113.100"]
# Addons
addons = ["wholesale"]
# Config file locations
generated_dir = "/static/docs/wholesale/config"
routes_files = ["config/routes/*.toml"]
trunks_files = ["config/trunks/*.toml"]
queues_files = ["config/queue/*.toml"]
# DoS protection
[proxy.dos]
enabled = true
max_cps_per_ip = 50
max_concurrent_per_ip = 200
scan_detection = true
# ── Console (Web Admin) ───────────────────────────────────────
[console]
session_secret = "wholesale-cluster-secret-2026" # ← MUST be identical across all nodes
allow_registration = false
# base_path = "/" # Change if behind a path prefix
# api_prefix = "/api" # API route prefix
# ── Core Cluster (registration/presence sync) ──────────────────
[cluster]
peers = [ # ← Node-specific: list OTHER nodes only
{ addr = "10.0.0.12", sip_port = 5060, ami_port = 8080 },
]
# ── SipFlow (local mode on this node) ──────────────────────────
[sipflow]
[sipflow.local]
subdirs = "daily"
flush_count = 100
flush_interval_secs = 5
id_cache_size = 10000
# ── Recording (optional) ───────────────────────────────────────
[recording]
enabled = false
# auto_start = false
# type = "local" # local | http | s3
# ── CDR ────────────────────────────────────────────────────────
[callrecord]
enabled = true
config/wholesale.toml
# ── Wholesale Cluster ──────────────────────────────────────────
[cluster]
[[cluster.peers]]
name = "node-2"
url = "http://10.0.0.12:8080"
api_key = "wholesale-cluster-api-key-2026" # ← MUST be identical across all nodes
# ── Route Cache ────────────────────────────────────────────────
[route_cache]
capacity = 10000
ttl_secs = 30
# ── Circuit Breaker ────────────────────────────────────────────
[circuit_breaker]
failure_threshold = 5
open_duration_secs = 30
half_open_probes = 1
failure_codes = [503, 408, 504]
# ── Sliding Window ASR/ACD Monitoring ──────────────────────────
[sliding_window]
enabled = true
window_secs = 300
max_events_per_trunk = 10000
asr_alert_threshold = 30.0
min_calls_for_stats = 10
Node 2 — 10.0.0.12 (Secondary)
config.toml
# ── General ────────────────────────────────────────────────────
http_addr = "0.0.0.0:8080"
database_url = "mysql://rustpbx:WsDbP@ss2026@10.0.0.20:3306/rustpbx"
external_ip = "10.0.0.12" # ← DIFFERS: Node 2's IP
log_level = "info"
# ── RTP ────────────────────────────────────────────────────────
rtp_start_port = 20000
rtp_end_port = 40000
# ── SIP Proxy ──────────────────────────────────────────────────
[proxy]
addr = "0.0.0.0"
udp_port = 5060
modules = ["acl", "auth", "registrar", "call"]
media_proxy = "auto"
max_concurrency = 1000
realms = ["10.0.0.11", "10.0.0.12", "203.0.113.100"]
addons = ["wholesale"]
generated_dir = "/static/docs/wholesale/config"
routes_files = ["config/routes/*.toml"]
trunks_files = ["config/trunks/*.toml"]
queues_files = ["config/queue/*.toml"]
[proxy.dos]
enabled = true
max_cps_per_ip = 50
max_concurrent_per_ip = 200
scan_detection = true
# ── Console ────────────────────────────────────────────────────
[console]
session_secret = "wholesale-cluster-secret-2026" # ← IDENTICAL
allow_registration = false
# ── Core Cluster ───────────────────────────────────────────────
[cluster]
peers = [ # ← DIFFERS: list OTHER nodes
{ addr = "10.0.0.11", sip_port = 5060, ami_port = 8080 },
]
# ── SipFlow (remote mode — connects to Node 1) ────────────────
[sipflow]
[sipflow.remote]
flush_interval_secs = 5
[[sipflow.remote.nodes]]
udp = "10.0.0.11:6060"
http = "http://10.0.0.11:6060"
# ── Recording ──────────────────────────────────────────────────
[recording]
enabled = false
# ── CDR ────────────────────────────────────────────────────────
[callrecord]
enabled = true
config/wholesale.toml
[cluster]
[[cluster.peers]]
name = "node-1"
url = "http://10.0.0.11:8080"
api_key = "wholesale-cluster-api-key-2026" # ← IDENTICAL
[route_cache]
capacity = 10000
ttl_secs = 30
[circuit_breaker]
failure_threshold = 5
open_duration_secs = 30
half_open_probes = 1
failure_codes = [503, 408, 504]
[sliding_window]
enabled = true
window_secs = 300
max_events_per_trunk = 10000
asr_alert_threshold = 30.0
min_calls_for_stats = 10
Per-Node Differences Summary
| Field | Node 1 (10.0.0.11) | Node 2 (10.0.0.12) | Node N |
|---|---|---|---|
external_ip | 10.0.0.11 | 10.0.0.12 | Node’s own IP |
[cluster].peers | [10.0.0.12] | [10.0.0.11] | All other nodes |
[sipflow] | local mode | remote → Node 1 | remote → Node 1 |
wholesale [cluster].peers | node-2 @ 10.0.0.12 | node-1 @ 10.0.0.11 | All other nodes |
Must be identical across all nodes:
| Field | Why |
|---|---|
database_url | Shared database |
console.session_secret | Session cookie validation |
wholesale api_key | Inter-node API authentication |
realms | NAT fix needs all node IPs |
rtp_start_port / rtp_end_port | Must match across cluster |
addons | Same feature set on every node |
| Trunk/route TOML files | Identical call routing |
PostgreSQL Variant
Replace the database_url in each node’s config.toml:
database_url = "postgres://rustpbx:WsDbP@ss2026@10.0.0.20:5432/rustpbx"
PostgreSQL Setup
# Install
docker run -d --name postgres \
--restart always \
-e POSTGRES_DB=rustpbx \
-e POSTGRES_USER=rustpbx \
-e POSTGRES_PASSWORD=WsDbP@ss2026 \
-v /data/postgres:/var/lib/postgresql/data \
-p 5432:5432 \
postgres:16
# Verify
psql -h 10.0.0.20 -U rustpbx -d rustpbx -c "SELECT 1"
# Tune parameters (postgresql.conf)
# shared_buffers = 4GB
# max_connections = 300
# effective_cache_size = 12GB
# work_mem = 64MB
PostgreSQL CDR Partitioning
-- Monthly partitioning (PG 10+)
CREATE TABLE wholesale_cdrs (
id BIGSERIAL,
call_id TEXT,
tenant_id BIGINT,
trunk_id BIGINT,
caller TEXT,
callee TEXT,
call_start TIMESTAMPTZ,
call_end TIMESTAMPTZ,
talk_duration INTEGER,
billable_duration INTEGER,
sell_rate DOUBLE PRECISION,
sell_price DOUBLE PRECISION,
buy_rate DOUBLE PRECISION,
buy_price DOUBLE PRECISION,
profit DOUBLE PRECISION,
sip_code INTEGER,
answered BOOLEAN
) PARTITION BY RANGE (call_start);
CREATE TABLE wholesale_cdrs_202605 PARTITION OF wholesale_cdrs
FOR VALUES FROM ('2026-05-01') TO ('2026-06-01');
CREATE TABLE wholesale_cdrs_202606 PARTITION OF wholesale_cdrs
FOR VALUES FROM ('2026-06-01') TO ('2026-07-01');
CREATE TABLE wholesale_cdrs_default PARTITION OF wholesale_cdrs DEFAULT;
SipFlow on Node 1
Since SipFlow runs in local mode on Node 1, it uses the embedded SipFlow within the RustPBX process — no separate binary needed.
Node 2 (and any additional nodes) connect to Node 1’s SipFlow via the remote backend:
# Node 2's config.toml — remote SipFlow pointing to Node 1
[sipflow]
[sipflow.remote]
flush_interval_secs = 5
[[sipflow.remote.nodes]]
udp = "10.0.0.11:6060"
http = "http://10.0.0.11:6060"
For SipFlow to accept remote UDP on Node 1, RustPBX’s SipFlow local backend listens by default. The remote nodes need to know Node 1’s SipFlow UDP and HTTP ports.
For higher SipFlow availability, deploy a standalone `sipflow` binary on a separate server and configure all nodes in `remote` mode. See [SipFlow Cluster](/static/docs/addons/sipflow) for details.
Docker Compose Reference
A docker-compose.yml for the full 2-node cluster:
version: "3.8"
services:
mysql:
image: mysql:8.4-oracle
restart: always
environment:
MYSQL_ROOT_PASSWORD: RootP@ss2026
MYSQL_DATABASE: rustpbx
MYSQL_CHARACTER_SET_SERVER: utf8mb4
MYSQL_COLLATION_SERVER: utf8mb4_unicode_ci
volumes:
- mysql_data:/var/lib/mysql
ports:
- "3306:3306"
command: >
--max-connections=500
--innodb-buffer-pool-size=4G
--character-set-server=utf8mb4
node1:
image: docker.cnb.cool/miuda.ai/rustpbx:0.4.5
restart: always
network_mode: host
volumes:
- /static/docs/wholesale/node1/config.toml:/app/config.toml
- /static/docs/wholesale/node1/config:/app/config
depends_on:
- mysql
node2:
image: docker.cnb.cool/miuda.ai/rustpbx:0.4.5
restart: always
network_mode: host
volumes:
- /static/docs/wholesale/node2/config.toml:/app/config.toml
- /static/docs/wholesale/node2/config:/app/config
depends_on:
- mysql
volumes:
mysql_data:
Directory structure:
/static/docs/wholesale/
├── docker-compose.yml
├── node1/
│ ├── config.toml ← Node 1 config (external_ip=10.0.0.11, local SipFlow)
│ └── config/
│ ├── wholesale.toml ← Wholesale cluster config
│ ├── trunks/ ← Shared trunk definitions
│ ├── routes/ ← Shared route definitions
│ └── acl/ ← Shared ACL rules
└── node2/
├── config.toml ← Node 2 config (external_ip=10.0.0.12, remote SipFlow)
└── config/
├── wholesale.toml
├── trunks/ ← Same as node1
├── routes/
└── acl/
Quick Start Commands
# 1. Create directory structure
mkdir -p node1/config/{trunks,routes,acl} node2/config/{trunks,routes,acl}
# 2. Write configs (use the full examples above)
# ... write config.toml and wholesale.toml for each node ...
# 3. Sync shared config files
cp -r node1/config/trunks/* node2/config/trunks/
cp -r node1/config/routes/* node2/config/routes/
cp -r node1/config/acl/* node2/config/acl/
# 4. Start the cluster
docker compose up -d
# 5. Wait for Node 1 to finish migrations, then verify
sleep 10
curl http://10.0.0.11:8080/healthz
curl http://10.0.0.12:8080/healthz
# 6. Create superuser (once, on any node)
docker compose exec node1 /app/rustpbx --create-superuser admin Admin@2026