Both commands give you the same PearlPBX2 — the same admin panel, the same queues, the same dashboard. The difference isn’t in the product. It’s in everything around it: TLS, backups, the firewall, cron, and how media traffic actually reaches your phone.
Two commands
# Docker sandbox — clones into ./PearlPBX2
curl -fsSL https://pearlpbx2.com/quickstart-docker.sh | bash
# Production install — clones into /opt/PearlPBX2
curl -fsSL https://pearlpbx2.com/quickstart-install.sh | sudo bash
The first one takes as long as Docker needs to pull images. The second takes 15–30 minutes, because Asterisk 22 is compiled from source. Both print an admin login and a generated password at the end.
What each path actually installs
quickstart-install.sh is a wrapper around Ansible. Nine roles install PostgreSQL, Redis,
Asterisk 22, nginx with TLS, systemd units for the services, nftables rules, and cron. The
install is fully unattended: it detects the SSH port and logs it before the firewall rules are
applied, without asking for confirmation.
quickstart-docker.sh checks that Docker and Compose v2 are present, generates a .env, and
brings up the stack: Django, andrius/asterisk:22, PostgreSQL 17, Redis 7, FastAGI, and the
dashboard listener. Django listens on port 8000 with no nginx and no TLS, running in
DEVMODE=Development. Asterisk gets published ports 5060/udp, 5060/tcp, 10000-10099/udp,
and 8088/tcp on the bridge network. Demo data is already seeded: users 201–210, an echo test on
130, an IVR on 140, and queues 141 and 142.
SIP, NAT, and why it depends on your OS
This is where the real fault line runs, and it has nothing to do with PearlPBX2 — it’s about how Docker itself is built on different systems.
On Linux, the container network is a regular interface on the host. The 172.x subnet is
routable, so a softphone on the same machine can reach Asterisk directly or through the published
ports — either way works.
On macOS and Windows, Docker Desktop keeps containers inside a hidden Linux VM. The container
subnet isn’t visible from the host at all — only what’s published to localhost works. SIP
signaling gets through fine because port 5060 is published. Media breaks in the classic way
instead: Asterisk puts the address it sees for itself — the container’s address — into the SDP,
and the softphone tries to send RTP into nowhere.
The fix is the same one used for any NAT sitting in front of Asterisk: set
external_media_address and external_signaling_address on the PJSIP transport to an address the
client can actually reach, and rely on symmetric RTP so Asterisk replies to wherever the packets
came from. In other words, “SIP in Docker on a laptop” isn’t broken — it just needs the same NAT
configuration as any installation sitting behind a router.
One side effect worth knowing about in advance: a hundred published UDP ports for RTP noticeably
slows down docker compose up on Docker Desktop.
Why WebRTC behaves differently
WebRTC doesn’t fall into this trap, which is exactly why the Docker sandbox is a good way to demo browser-based calls.
A browser doesn’t trust the SDP address blindly the way a regular softphone does: it gathers ICE candidates, sends media first, and Asterisk replies to whichever address the packets arrived from. There’s no external address to configure by hand. On top of that, WebRTC media is always encrypted — DTLS-SRTP isn’t something you can forget to turn on, it’s a spec requirement.
One caveat: Compose publishes port 8088, which is ws, not wss. That’s enough on localhost,
because browsers treat localhost as a secure context and grant microphone access anyway. The
moment you want to connect from another machine, you need a WSS transport and a real
certificate — exactly what the production path sets up for you.
What’s completely missing in Docker: cron
This is the least obvious difference, and the most expensive one to discover six months in. Ansible installs seven recurring jobs; the Docker stack has none of them:
- PostgreSQL backup — daily at 01:30.
/etc/asteriskbackup — daily at 02:30.- wav → mp3 recording conversion — every 15 minutes.
- mp3 offload — daily at 03:00.
- System monitoring — every 15 minutes.
- Asterisk log rotation — daily.
- Longest-calls email report — daily at 06:00, if recipients are configured.
In practice this means recordings in the sandbox stay in wav and disk usage grows linearly with call volume, there’s no database backup, and nobody trims the Asterisk logs. For a week of testing, none of this matters. For a system people rely on every day, it does.
Smaller differences still worth knowing
- Timezone. Ansible sets
Europe/Kyiv; containers default to UTC — you’ll see this in CDRs and reports. - The callback daemon is disabled by default in Docker: it places real outbound calls, so it has
to be enabled explicitly with
--profile callback. - Data lives in named volumes.
docker compose downkeeps it;down -vwipes everything, including the database. - There’s nothing to update or roll back with:
update.shandrollback.sh -nrely on a deployment ledger that simply doesn’t exist on the Docker path.
Which one to pick
To see what it looks like, sketch out a call flow, show a colleague WebRTC calling, or poke at the dialplan — Docker. That’s exactly what it’s built for, and it runs on any OS.
To take real calls, keep recordings, and sleep at night — install.sh on a clean Debian or
Ubuntu box. The difference isn’t in PearlPBX2 itself; it’s in who’s watching the backups, the
disk, and the certificates. On the production path, that’s already handled.