Self-Hosted VoIP PBX with FreePBX and Asterisk on Linux: From Zero to Working SIP Trunk

Networking tutorial - IT technology blog
Networking tutorial - IT technology blog

The Night the Phone System Died

It was 11:47 PM on a Tuesday. My phone lit up — the CEO. “Our business phone lines are all down. We have a sales call with a major client at 9 AM tomorrow.” I pulled up our hosted PBX dashboard and found the familiar nightmare: the provider’s service was completely unreachable, no ETA, support ticket queue backed up three days.

By the following weekend, I had a fully functional self-hosted PBX on a $12/month VPS — SIP trunks from a budget provider, internal extensions for the whole team, call routing that actually made sense. That setup has been running in production for 14 months. The only downtime: two scheduled maintenance windows on the VPS.

Here’s exactly how to replicate it.

What You’re Actually Building

Get the mental model straight before touching any commands.

Asterisk is the open-source telephony engine — it handles call processing, audio bridging, and protocol translation. Think of it as the kernel of your phone system.

FreePBX is the web interface that wraps Asterisk. Without it, configuring Asterisk means hand-editing .conf files and hoping you got the syntax right. FreePBX turns that into a reasonably sane GUI backed by a database.

The call flow works like this:

  • SIP Trunk: Your connection to the outside world — a VoIP provider gives you a real phone number that routes calls into your server over the internet
  • Extensions: Internal numbers (101, 102, etc.) assigned to softphones or IP desk phones on your network
  • Dialplan: Rules that decide where calls go — inbound from the trunk routes to extension 101, a ring group, or an IVR menu

A 1 vCPU/2 GB VPS handles around 20–30 simultaneous G.711 calls without straining. That covers most small teams comfortably.

Prerequisites

  • A Linux server — Rocky Linux 9 or Ubuntu 22.04 LTS, at least 1 vCPU and 1 GB RAM
  • A public IP address (or static private IP for LAN-only use)
  • A SIP trunk account — this guide uses VoIP.ms (per-minute billing, no contracts, roughly $0.009/minute for US calls)
  • Firewall access to open UDP 5060 (SIP) and UDP 10000–20000 (RTP audio)

Installing FreePBX

Open the required firewall ports first. Asterisk needs them the moment it starts, so do this before the installer runs.

On Rocky Linux 9:

firewall-cmd --permanent --add-port=80/tcp
firewall-cmd --permanent --add-port=443/tcp
firewall-cmd --permanent --add-port=5060/udp
firewall-cmd --permanent --add-port=5061/tcp
firewall-cmd --permanent --add-port=10000-20000/udp
firewall-cmd --reload

On Ubuntu 22.04, install the core dependencies first:

apt update && apt install -y \
  git curl wget \
  php8.1 php8.1-curl php8.1-xml php8.1-mysql php8.1-mbstring \
  apache2 mariadb-server asterisk asterisk-config

systemctl enable --now asterisk mariadb apache2

Then pull the FreePBX installer and run it:

cd /usr/src
wget http://mirror.freepbx.org/modules/_cache/freepbx-17.0-latest.tgz
tar xfz freepbx-17.0-latest.tgz
cd freepbx
./install -n

The install takes 10–15 minutes. When it finishes, open http://YOUR_SERVER_IP in a browser and run through the setup wizard. Set a strong admin password now — before anything else.

Lock It Down Immediately

SIP scanners will find your server within hours of it going live. In the FreePBX GUI, go to Admin → System Admin → Intrusion Detection and enable the fail2ban integration. It auto-blocks IPs after repeated failed registration attempts, which shuts down most automated toll-fraud attacks before they get anywhere.

Configuring Your First SIP Trunk

Head to Connectivity → Trunks → Add Trunk → Add SIP (chan_pjsip) Trunk.

For VoIP.ms, the settings look like this:

Trunk Name: voipms-main

=== General Settings ===
Username: your_voipms_username
Secret: your_voipms_password
SIP Server: denver.voip.ms    # pick the closest POP
SIP Port: 5060

=== Advanced (pjsip) ===
Authentication: Outbound
Registration: Send
Match Inbound Auth: Username

After saving, open Admin → Asterisk Logfiles and watch for:

Registered SIP 'voipms-main' at denver.voip.ms

That line means the trunk is live. If you see Authentication failed instead, check two things: your credentials, and whether your VoIP.ms account uses username/password auth. IP-based auth is the other option, under your account’s “Authentication Type” setting — if that’s active, username/password won’t work.

Adding Internal Extensions

Open Applications → Extensions → Add Extension → Add New PJSIP Extension.

Extension: 101
Display Name: Alice
Secret: use-a-strong-random-password

Repeat for each user. Then set up a softphone — Zoiper and MicroSIP (Windows) are free, Linphone works on Linux — with these settings:

Domain/Server: YOUR_SERVER_IP
Username: 101
Password: the-secret-you-set
Transport: UDP
Port: 5060

Once registered, extensions dial each other directly by number. No trunk involved, no per-minute cost — calls stay inside Asterisk entirely.

Routing Inbound and Outbound Calls

Inbound Routes

Inbound routes live under Connectivity → Inbound Routes → Add Incoming Route:

Description: Main Line
DID Number: +15551234567    # your number from VoIP.ms
Destination: Extensions → 101

Need after-hours handling? Add a Time Condition under Applications → Time Conditions, then route incoming calls through it. Business hours go to extension 101, evenings drop into voicemail.

Outbound Routes

For outbound, pull up Connectivity → Outbound Routes → Add Outbound Route:

Route Name: external-calls
Trunk Sequence: voipms-main

Dial Patterns:
  Match Pattern: NXXNXXXXXX    # 10-digit US numbers
  Match Pattern: 1NXXNXXXXXX  # 11-digit with country code

Apply changes, dial an external number from extension 101, and confirm it rings through.

Debugging One-Way Audio (the Classic 2 AM Problem)

One-way audio is the most common headache on self-hosted VoIP. You hear the caller, they can’t hear you — or the reverse. It’s almost always NAT.

# Connect to Asterisk CLI and watch a live call
asterisk -rvvv

# During a call, look for RTP stream lines
# Both send AND receive should appear:
# [VERBOSE] RTP Packet send: ...
# [VERBOSE] RTP Packet received: ...

The fix is telling Asterisk your public IP. In FreePBX, go to Settings → Asterisk SIP Settings → General SIP Settings:

External IP: your.public.ip.here
Local Networks: 192.168.1.0/24   # your LAN subnet

Without this, your server advertises a private IP in the SDP offer. The remote end tries to send audio to that private address. Nothing arrives.

For trunk registration issues, check directly from the CLI:

pjsip show registrations
# Should show:
# voipms-main    Registered    denver.voip.ms:5060

What to Add Next

The base system covers the essentials. These four additions are worth doing before you call it production-ready:

  • Call recording: Enable per-extension under each extension’s settings. Recordings land in /var/spool/asterisk/monitor/ and play back directly from the FreePBX recordings module
  • SIP TLS + SRTP: Encrypt signaling (port 5061) and audio between phones and the server — critical for any extension connecting from outside the office
  • Scheduled backups: Set these up under Admin → Backup & Restore before you forget. Rebuilding FreePBX from scratch without a backup is a painful afternoon
  • Module updates: Check monthly via Admin → Module Admin → Check Online. An internet-exposed SIP server is an active target — security patches matter

Total running cost: around $12/month for a 2 vCPU/2 GB VPS plus per-minute trunk charges. A team doing 2,000 minutes a month on VoIP.ms pays roughly $18 all-in. The hosted service this replaced ran $180/month flat. And it still went dark at midnight before a major client call. This setup hasn’t.

Share: