PearlPBX2

PearlPBX2 — Quick Start

Two ways to get PearlPBX2 running, depending on what you’re doing. See README.md for the full feature list, architecture, and configuration reference.

Requirements

What you get out of the box

A fresh install seeds a working demo PBX, not an empty database. After the first migration/deploy run, you already have:

Extensions What it is
201–210 10 SIP users (ppbxuser201…ppbxuser210), random passwords
211–220 10 WebRTC (wss) SIP users (webrtcuser…webrtcuser10) on transport-wss, random passwords
130 Echo test
131 / 132 Log a queue member in / out (PauseQueueMember)
140 Test the IVR from an internal phone
141 Sales queue
142 Support queue

Plus:

This is seeded by manage.py seed_quickstart, which only runs once, on a fresh install — see Notes / next steps for how that’s guarded.

Fastest start

One command, no manual steps — picks up where Option 1 / Option 2 below leave off, and prints a generated admin login/password at the end.

# Production install (Debian/Ubuntu) — clones to /opt/PearlPBX2
curl -fsSL https://pearlpbx2.com/quickstart-install.sh | sudo bash

# Docker playground (any OS with Docker) — clones to ./PearlPBX2
curl -fsSL https://pearlpbx2.com/quickstart-docker.sh | bash

Read on for what each option actually does, or to run the steps by hand.

Option 1: Ansible (production install)

Requires a Debian or Ubuntu host (the playbook uses apt and systemd directly — no other distros are supported).

Provisions Asterisk (compiled from source), PostgreSQL, Redis, nginx, systemd units, and the firewall on a fresh host — no manual steps:

git clone https://github.com/radetsky/PearlPBX2.git
cd PearlPBX2
sudo ./install.sh

install.sh installs Ansible itself if needed and runs ansible/install.yml (add -v for verbose Ansible output), logging to /var/log/pearlpbx2-install.log. When it finishes, create the admin user with:

./manage.sh createsuperuser

then open https://<your-host>/admin/.

To pull in updates later, run sudo ./update.sh from the same directory — it runs ansible/update.yml, which keeps a rollback history (rollback.sh) in case an update needs to be reverted. update.sh never re-seeds the quick-start data described above, so it’s safe to run on a live system.

See ansible/group_vars/all.yml for the variables it uses (install dir, DB name, Asterisk version, etc.) and ansible/roles/ for what each role does.

TLS certificate

By default install.sh issues a self-signed certificate, so browsers warn on first visit. For a trusted Let’s Encrypt certificate, re-run:

sudo ./install.sh --domain <your-domain> --email <[email protected]m>

Optional: MP3 backup sync

Edit /etc/PearlPBX/syncmp3/env and set BACKUP_DIR (e.g. a mounted NAS or iSCSI volume). Sync runs daily at 03:00 via cron.

Optional: Slack server alerts

  1. Create a Slack Incoming Webhook: https://api.slack.com/apps → New App → Incoming Webhooks
  2. Paste the webhook URL into /etc/PearlPBX/system_monitor/env.

Alerts fire every 15 minutes (disk, CPU, memory, services).

Optional: Slack notification for unmatched inbound calls

  1. Create a Slack Incoming Webhook: https://api.slack.com/apps → New App → Incoming Webhooks
  2. Paste the webhook URL into /etc/PearlPBX/AGI/env.
  3. Add to your catch-all dialplan extension:
    AGI(unmatched_call.py,${CALLERID(num)},${EXTEN},${CHANNEL});
    See services/agi/README.md — it also covers missed_call.py.

If the install was interrupted

git clone https://github.com/radetsky/PearlPBX2.git
cd PearlPBX2

cp env.sample .env
# Edit .env — set DB credentials, then generate and fill in:
#   DJANGO_SECRET_KEY:       openssl rand -hex 50
#   ASTERISK_MANAGER_SECRET: openssl rand -base64 48

docker compose up -d
docker compose exec pearlpbx2 python manage.py createsuperuser

Open http://localhost:8000/admin/, log in, and go straight to Apply Changes — the quick-start data above is already in the database, it just needs to be pushed out to Asterisk once.

Notes:

Get your SIP credentials

The 201–210 passwords are generated randomly and only ever stored in the database. In the admin, go to SIP Users, open 201 (or any other), and reveal the Password field — these instructions use 201 and 202.

Register two softphones

In any SIP client (Zoiper, Linphone, a desk phone, …), register two accounts using the credentials above:

Connect a WebRTC user

The seed also created 10 WebRTC (wss) SIP users, webrtcuser…webrtcuser10 (extensions 211–220), on the transport-wss transport. Their passwords are in the same place as 201–210’s — SIP Users in the admin.

WebRTC users authenticate with MD5 digest (auth_type=md5), not a plaintext password — most WebRTC SIP stacks (SIP.js, JsSIP) compute the digest HA1 from username/realm/secret themselves and never send the secret over the wire. The seed already sets this; just pass the same secret as the password in your WebRTC client.

Docker Compose: the asterisk container publishes port 8088 straight to the host, so a browser client connects directly, unencrypted:

ws://<docker-host>:8088/asterisk/ws

(prefix=asterisk comes from the http.conf that docker/asterisk-bootstrap.sh seeds on first boot.) No proxy needed — the Docker sandbox isn’t meant for production traffic, so plain ws is fine.

Ansible (bare-metal): Asterisk’s HTTP/WebSocket server only listens on 127.0.0.1:8088 (contrib/configs/http.conf) and port 8088 is never opened in the firewall — all external traffic, WebRTC signaling included, goes through nginx on 443. The nginx role proxies location /asterisk/ to http://127.0.0.1:8088/asterisk/, so a browser client uses the same host and port as the admin UI, over TLS:

wss://<your-host>/asterisk/ws

sudo ./install.sh sets this up on a fresh host — nothing to configure by hand. On a system installed before this proxy existed, sudo ./update.sh won’t add it (it doesn’t touch nginx or http.conf); re-run sudo ./install.sh to pick up the new nginx config, and edit bindaddr in /etc/asterisk/http.conf to 127.0.0.1 by hand.

A WebSocket connection to an untrusted certificate cannot be “click-through” accepted the way a page load can, so wss:// needs a certificate the browser actually trusts. The installer gets one automatically, in order of preference:

  1. A domain, if you have one — pass it and the installer requests a normal 90-day Let’s Encrypt certificate via HTTP-01:
    sudo ./install.sh --domain pbx.example.com --email [email protected]
  2. No domain, but a public IP — the installer requests a Let’s Encrypt IP-address certificate automatically (6-day lifetime, renewed every few hours by a systemd timer). Nothing to pass; this is what a bare sudo ./install.sh gets you on most VPS hosts.
  3. Neither reachable (no public IP, or port 80 blocked upstream) — falls back to a self-signed certificate; your browser will warn once and you click through.

Check which one you ended up with: cat /etc/PearlPBX/tls-mode. See Admin Guide → TLS certificates for renewal details and how to add a domain later.

Apply Changes

Nothing above reaches Asterisk until you push it. In the admin UI go to Admin → Apply Changes (/admin/apply). This regenerates extensions.ael, pjsip.conf, queues.conf, queuerules.conf, manager.conf, musiconhold.conf, and confbridge.conf from the database, backs up the previous versions, and reloads Asterisk. Run this once now, and again after any change made through the admin UI.

Test call scenarios

Do these in order — each one builds on the last.

# Scenario Dial Expect / verify
1 Echo test 130 You hear your own voice looped back. asterisk -rx "core show channels" shows the call.
2 Internal call 201 dials 202 202 rings. asterisk -rx "pjsip show endpoints" shows both as Avail.
3 Transfer Mid-call, blind-transfer with # or attended with *, target 202 or 141 See Transfer below
4 Outbound to the outside world Dial any external number Fails until you replace the example trunk. Then asterisk -rx "pjsip show registrations" should show myprovider as Registered.
5 IVR from an internal phone 140, then press 1 or 2 You land in Sales or Support. asterisk -rx "queue show Sales" shows the call.
6 Production inbound call Call your DID from the outside world Routed via Incoming → ivr-main. asterisk -rx "pjsip set logger on" to watch the SIP trace live.

(Docker Compose: prefix each asterisk -rx "..." with docker compose exec asterisk.)

Transfer

Transfer feature codes come from contrib/configs/features.conf: # = blind transfer, * = attended transfer ([featuremap] section). Both require the t/T options on the Dial() the call came in on — already present on every seeded extension (e.g. Dial(PJSIP/${EXTEN}@myprovider,120,tT)).

You can call from extension 201 to 202, and mid-call, press #130# to transfer your call to the echo test. Your call should end, and the other party should hear the demo-echotest greeting.

Once a transfer is initiated, Asterisk resolves the dialed digits through the TRANSFER_CONTEXT global variable rather than the channel’s own context — seeded to:

globals {
    TRANSFER_CONTEXT=Outgoing;
}

This is why users are on the Outgoing routing table: it’s what lets a transfer target be either an internal extension (_2XX) or an external number (_X.) via myprovider, from a single, consistent context.

Replace the example trunk

myprovider (csbc.myprovider.net / test / secret) is a placeholder — it will never register. In Admin → SIP Uplinks and Peers → myprovider, replace:

Then Apply Changes and check:

asterisk -rx "pjsip show registrations"

myprovider should move from Rejected/No response to Registered.

Notes / next steps