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

FieldNode 1 (10.0.0.11)Node 2 (10.0.0.12)Node N
external_ip10.0.0.1110.0.0.12Node’s own IP
[cluster].peers[10.0.0.12][10.0.0.11]All other nodes
[sipflow]local moderemote → Node 1remote → Node 1
wholesale [cluster].peersnode-2 @ 10.0.0.12node-1 @ 10.0.0.11All other nodes

Must be identical across all nodes:

FieldWhy
database_urlShared database
console.session_secretSession cookie validation
wholesale api_keyInter-node API authentication
realmsNAT fix needs all node IPs
rtp_start_port / rtp_end_portMust match across cluster
addonsSame feature set on every node
Trunk/route TOML filesIdentical 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