Why Build a VoIP Lab in 2025?
The goal isn’t to replace my mobile phone. It’s to have a playground for voice technologies that are increasingly relevant in modern applications: IVR systems, conversational AI, voice assistants, and telephony integrations.
I wanted to experiment with:
- IVR systems (Interactive Voice Response) - those “press 1 for sales” menus
- SIP protocol internals - understanding modern telephony at a deeper level
- Conversational interfaces - prototyping voice-driven applications
Setting up a proper PBX gives you a real telephony environment to test these technologies. And with Docker, I can iterate quickly without worrying about breaking a production system (because this is the disposable test environment).
The Setup: Small VPS, Big Possibilities
My testing ground:
- Small VPS: 4 vCores, 8GB RAM, 75GB storage
- OS: Ubuntu 24.04 LTS
- Stack: Docker Compose + FreePBX + MariaDB
- OVH SIP Trunk: Unlimited calls to 40+ countries (fixed lines)
- Total Cost: ~€10/month (€5 VPS + €5 OVH trunk)
The beauty of this setup? It’s completely portable. Once tested on the VPS, I can migrate the exact same Docker configuration to my home lab. Infrastructure as code isn’t just for Kubernetes clusters.
More importantly, this gives me a real SIP/RTP environment to experiment with voice technologies before integrating them into larger projects.
Docker: The Right Tool for VoIP Experimentation
I could have gone the traditional route - bare metal Asterisk installation with manual dependency management. But here’s why Docker made sense for a lab environment:
- Rapid iteration: Broke something?
docker compose down -vand rebuild - Isolation: Test different configurations without system contamination
- Reproducibility: Share exact setup via
docker-compose.yml - Portability: Same config works on VPS, home lab, or even a laptop
- Version control: Entire infrastructure in git
The only consideration? VoIP is latency-sensitive. Docker adds minimal overhead, but you need proper kernel parameters for RTP (Real-time Transport Protocol) traffic.
The Docker Compose Setup
Here’s the core - surprisingly simple for a full-featured PBX:
version: '3.8'
services:
freepbx:
image: tiredofit/freepbx:latest
container_name: freepbx
restart: unless-stopped
network_mode: host
environment:
- DB_HOST=127.0.0.1
- DB_USER=asterisk
- DB_PASS=${DB_PASSWORD}
- ADMIN_PASSWORD=${ADMIN_PASSWORD}
- TZ=Europe/Brussels
- RTP_START=10000
- RTP_FINISH=20000
- ENABLE_FAIL2BAN=TRUE
volumes:
- ./data/recordings:/var/spool/asterisk/monitor
- ./data/backup:/data/backup
- freepbx-data:/data
cap_add:
- NET_ADMIN
- NET_RAW
mariadb:
image: mariadb:10.11
container_name: freepbx-db
restart: unless-stopped
network_mode: host
environment:
- MYSQL_ROOT_PASSWORD=${DB_ROOT_PASSWORD}
- MYSQL_DATABASE=asterisk
- MYSQL_USER=asterisk
- MYSQL_PASSWORD=${DB_PASSWORD}
The .env file holds all passwords (single quotes handle special characters):
DB_ROOT_PASSWORD='YourSecurePassword123!'
DB_PASSWORD='AnotherSecurePassword456!'
ADMIN_PASSWORD='AdminPassword789!'
PUBLIC_IP='your.vps.ip.here'
HOSTNAME='your.domain.here'
The Devil in the Details: VoIP-Specific Tweaks
Kernel Parameters for RTP
VoIP audio uses RTP over UDP, which needs proper buffer sizing:
sudo tee -a /etc/sysctl.conf << EOF
net.core.rmem_max = 134217728
net.core.wmem_max = 134217728
net.ipv4.udp_mem = 65536 131072 262144
EOF
sudo sysctl -p
Firewall Configuration
sudo ufw allow 5060/udp
sudo ufw allow 5061/tcp
sudo ufw allow 10000:20000/udp
Network Mode: Host vs Bridge
I’m using network_mode: host because SIP is notoriously NAT-unfriendly. The protocol embeds IP addresses in the payload, not just headers.
Initial Testing: Making It Ring
After docker compose up -d and a 10-minute initialization:
External IP Settings (Settings → Asterisk SIP Settings):
- External Address: [Your public IP]
- Local Networks: [Your home network range]
- Rewrite Contact: Yes
Test Extensions:
- Extension 1001: Cisco SPA504G desk phone
- Extension 1002: iPhone via Zoiper softphone
First test call worked perfectly - clear audio, minimal latency.
Connecting to the Outside World
I added a SIP trunk for external connectivity:
Trunk Name: Provider
Username: [provider username]
Secret: [provider password]
SIP Server: [provider SIP server]
Registration: Send
Now the PBX can make and receive external calls, essential for testing real-world IVR scenarios.
What I Learned (The Hard Way)
1. Password Quoting in .env Files
# Breaks
DB_PASSWORD=My$ecure!Pass
# Works
DB_PASSWORD='My$ecure!Pass'
2. “Unauthorized” = Wrong Password Field
FreePBX has multiple password fields. For SIP clients, you need the “Secret” field, not “User Password.”
3. Patience During Initialization
FreePBX takes 10-15 minutes on first start. Watch docker compose logs -f and wait for “FreePBX is ready for connections.”
The Real Goal: A Voice Technology Playground
With the basic PBX working, I now have a platform to experiment with:
IVR Systems:
- Build custom voice menus
- Test different prompt strategies
- Measure completion rates
Telephony APIs:
- Experiment with Voice API
- Test integration patterns
- Build custom AGI scripts
Security Basics
- Enable SIP over TLS
- Use strong passwords (20+ characters)
- Enable Fail2Ban
- Restrict access by IP
- Regular updates
Why This Matters
Voice interfaces are increasingly important:
- Customer service automation
- Accessibility features
- IoT voice control
Having a lab for experimentation - without per-minute API costs - is invaluable.
The Unexpected Benefit
I started this to test voice technologies. I ended up learning:
- Modern SIP/RTP protocols
- NAT traversal techniques
- VoIP codecs
- Docker networking
- Asterisk dialplan
For experimentation and learning? A FreePBX lab is incredibly valuable.
Resources
- FreePBX Docker Image: tiredofit/freepbx
- FreePBX Documentation: wiki.freepbx.org
- Asterisk Documentation: docs.asterisk.org
Total setup time? About 3 hours. For a fully functional PBX ready for voice experiments? Not bad for an afternoon project.
Now, if you’ll excuse me, I have an IVR system to design.
He runs Wellmade, a product engineering studio, and still writes the code. Twenty years of building products for companies that wanted the thinking and the building done by the same person.
This article was co-written with AI — in the loop from the first draft to the last edit. The argument, the checking and the final word are mine.
