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
- Option 1 (Ansible): a bare Debian or Ubuntu host, that’s it.
install.shinstalls Ansible itself, andinstall.ymlprovisions everything else — Python, PostgreSQL, Redis, and Asterisk 22 (compiled from source) — on the target host. Nothing to install by hand first. - Option 2 (Docker Compose): just Docker and Docker Compose. The stack (Python 3.10+/Django 5.2, PostgreSQL 14+, Redis 7+, Asterisk 22+) runs entirely in containers.
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:
- Two queues, Sales and Support, with all 10 users as members
- An English IVR (
ivr-main): “press 1 for Sales, press 2 for Support”, 2 retries on timeout/invalid digit before it falls back to Sales - An example SIP trunk,
myprovider(csbc.myprovider.net, usertest/secret) — a placeholder to replace with your real provider (see Replace the example trunk) - Two routing tables: Incoming (any inbound call → the IVR) and Outgoing
(all 20 SIP users —
201–210and the WebRTC211–220— route through it: internal extensions, the130–142numbers, and anything else out viamyprovider)
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
- Create a Slack Incoming Webhook: https://api.slack.com/apps → New App → Incoming Webhooks
- 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
- Create a Slack Incoming Webhook: https://api.slack.com/apps → New App → Incoming Webhooks
- Paste the webhook URL into
/etc/PearlPBX/AGI/env. - Add to your catch-all dialplan extension:
SeeAGI(unmatched_call.py,${CALLERID(num)},${EXTEN},${CHANNEL});services/agi/README.md— it also coversmissed_call.py.
If the install was interrupted
- If
aptwas interrupted:sudo dpkg --configure -a - If the env file is corrupt:
sudo rm /etc/PearlPBX/PearlPBX2/env, then re-runinstall.sh
Option 2: Docker Compose (recommended — evaluation, development)
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:
pearlpbx2runs migrations and the quick-start seed automatically on every start (docker-entrypoint.sh) — no manualmigratestep needed. The seed only acts once; subsequent restarts are a no-op.asterisk-initseeds a minimal AMI-enabledmanager.confbefore Asterisk’s first boot, sofastagi/dashboard-listenercome up connected immediately; “Apply Changes” later overwrites it with the real generated config using the same credentials.- The callback daemon is opt-in (it originates real outbound calls):
docker compose --profile callback up -d callback-service. - A
docker-compose.override.ymlis picked up automatically and gives thepearlpbx2container source hot-reload for local development; rundocker compose -f docker-compose.yml up -dto skip it.
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:
- Server: your PBX host/IP
- Transport: UDP, port
5060 - Username / Password:
ppbxuser201/ the password from the admin (and likewise for 202)
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:
- 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] - 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.shgets you on most VPS hosts. - 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:
registration_uri/contact_uri/match_hosts— your provider’s SBC hostnameusername/secret— your account credentials
Then Apply Changes and check:
asterisk -rx "pjsip show registrations"
myprovider should move from Rejected/No response to Registered.
Notes / next steps
- International calls are blocked by default. The
Outgoingtable routes_00X!(international prefixes) into theinternational-callscontext, which just hangs up — a safe default so a fresh install with example credentials never becomes a toll-fraud vector. Remove or repoint thatDialplanExtensiononce you’re ready to allow them. Outgoingvs the legacyPEARLPBXtable.PEARLPBXstill exists (it’ssettings.PEARLPBX_DEFAULT_ROUTING_TABLE, used as a fallback elsewhere) but no SIP user is routed through it anymore after the quick-start seed runs — they’re all onOutgoing.- Multiple trunks / least-cost routing. The seeded
outbound-externalcontext dialsmyproviderdirectly. For more than one trunk, switch toAGI(agi://127.0.0.1:4573/dial-trunk-group,...)— seeservices/fastagi/README.mdfor trunk groups with failover. - Music on hold for the queues uses
contrib/moh/on_hold_music.wav— replace the files under thedefaultMOH class with your own. - Call recording is not enabled by default; see admin-guide.md for
MixMonitorsetup. - The quick-start seed itself is
manage.py seed_quickstart— it checks whether anyQueue,SIPPeer, orIncoming/Outgoingrouting table already exists and does nothing if so, so it’s safe even if invoked by hand. Ansible’sinstall.ymlcalls it once, gated on the same “first install on this host” check used for secret generation;update.ymlnever calls it at all.