PearlPBX2
← All posts

Asterisk in Docker vs install.sh: NAT, TLS and WebRTC

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/asterisk backup — 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 down keeps it; down -v wipes everything, including the database.
  • There’s nothing to update or roll back with: update.sh and rollback.sh -n rely 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.