Complete Guide: Setting Up FreePBX with VPS, Docker, and VPN (CGNAT Bypass Solution)
Complete FreePBX Setup Guide – Docker, VPN, and Twilio Integration Requirements / Setup Digital Ocean or whatever cloud provider you choose Docker (FreePBX) Nginx Setup OVPN Configuration IP Tables Setup YeaLink Phone configuration, the (YeaLinkT45w) was used in this tutorial Twi
Complete FreePBX Setup Guide – Docker, VPN, and Twilio Integration
Requirements / Setup
Digital Ocean or whatever cloud provider you choose
Docker (FreePBX)
Nginx Setup
OVPN Configuration
IP Tables Setup
YeaLink Phone configuration, the (YeaLinkT45w) was used in this tutorial
Cost: $12 Dollars a month (Needed to upgrade because of Ram requirement for FreePBX. It could have ran ok but would probably have issues in the future.)
Docker Setup
Summary of Docker and Docker Compose Setup
This document outlines the steps taken to install Docker and configure a persistent FreePBX container using Docker Compose.
1. Goal
The objective was to run a FreePBX instance inside a Docker container while ensuring that all of its critical data (configurations, settings, logs, etc.) would be saved on the host machine. This prevents data loss if the container is ever removed or re-created.
2. Installation
First, I installed the necessary software on the VPS.
Docker Engine: The core service that runs containers.
Docker Compose: A tool that makes it easy to define and manage multi-container applications using a single YAML file.
# Update package list
sudo apt update
Install Docker and Docker Compose
sudo apt install docker.io docker-compose -y
3. Creating the Host Directory Structure
To keep the persistent data organized, I created a specific directory structure in your home folder on the VPS.
/root/docker-freepbx/data: This folder on the host machine is “mounted” into the container. All of FreePBX’s database files, settings, and configurations are stored here.
/root/docker-freepbx/logs: This folder is used to store the Asterisk log files, making them easily accessible for troubleshooting from the host.
4. The docker-compose.yml Configuration File
This is the most important file. It serves as the blueprint for your entire FreePBX deployment. I created it inside the ~/docker-freepbx/ directory.
image: tiredofit/freepbx:15.0: Specifies the exact FreePBX image to download from Docker Hub.
ports: Maps ports from the host VPS to the container. For example, – “80:80” maps the host’s port 80 to the container’s port 80, making the web UI accessible.
volumes: This is what makes your data persistent. It links the host directories we created (/root/docker-freepbx/data) to the corresponding data directories inside the container.
restart: always: Tells Docker to automatically restart the container if it ever crashes or if the server reboots.
5. The Critical Permissions Fix
This was a major troubleshooting step that solved the problem of settings (like SIP secrets) not saving.
Problem: The FreePBX web UI could not write its settings to the database files.
Cause: The directories on the host (/root/docker-freepbx/data) were owned by the root user, but the web server process inside the container was running as a different, non-root user (ID 1000). This created a permissions conflict.
Solution: I stopped the container and changed the ownership of the data directories on the host to match the user ID inside the container.
These are the most common commands for managing your Docker setup. They should be run from within the ~/docker-freepbx/ directory.
Start the system:docker-compose up -d
Stop the system:docker-compose down
Restart a specific container:docker restart freepbx
View running containers:docker ps
View logs for a container:docker logs freepbx
Access a container’s command line:docker exec -it freepbx bash
NginX Setup
Objective
Serve your FreePBX Docker container (HTTPS on port 8443) via your public domain pbx.yourdomain.com using NGINX as a reverse proxy and Let’s Encrypt SSL certificates with automatic renewal. This now includes DNS A record setup instructions.
🌐 1. DNS Provider Configuration
You must configure your domain to point to your server’s public IP before installing and using SSL.
🛠️ Steps:
Log into your DNS provider’s control panel (e.g., Cloudflare, Namecheap, GoDaddy).
Go to DNS Settings for your domain (yourdomain.com).
Add an A Record:
Type: A
Name: pbx (this makes pbx.yourdomain.com)
Value: <Cloud Provider IP> (your VPS public IP)
TTL: Auto or 5 mins
Proxy status: DNS only (⚠️ turn off proxy/CDN if using Cloudflare or it may interfere with validation)
✅ Once added, allow a few minutes for DNS to propagate globally.
VPN Server and Certificate Setup Possible Problems
The initial goal was to create a secure VPN tunnel for the phone.
Problem: The phone could not connect to the VPN.
Cause: The OpenVPN server service was not running. This was due to multiple issues, including the server.conf file not existing, and then missing paths to the dh.pem and ta.key files.
Solution & Key Steps:
Install Tools: I started with openvpn and easy-rsa installed on the Ubuntu VPS.
<li><strong>Create Certificate Authority (CA):</strong> I used Easy-RSA to init-pki and build-ca, creating the master certificate that would sign all other certificates. <div class="code-block">./easyrsa init-pki</div></li><li><strong>Create Client Keys:</strong> I generated a certificate request and private key for the Yealink phone (<span class="inline-code">./easyrsa gen-req yealink-t45w nopass</span>).</li><li><strong>Signed Client Keys:</strong> I then signed the phone’s request with the CA (<span class="inline-code">./easyrsa sign-req client yealink-t45w</span>) to create a valid certificate.</li><li><strong>Create Server Keys:</strong> I also did the same for the server itself, creating a server certificate, private key, and Diffie-Hellman parameters (<span class="inline-code">./easyrsa build-server-full server nopass</span> and <span class="inline-code">./easyrsa gen-dh</span>).</li><li><strong>Create TLS-Auth Key:</strong> For added security, I generated a static key to protect against DoS attacks. <div class="code-block">sudo openvpn –genkey –secret /etc/openvpn/server/ta.key</div></li><li><strong>Start and Enable Service:</strong> <div class="code-block">sudo systemctl start openvpn-server@server.service
dev tun
proto udp
remote YOUR_VPS_IP 1194
resolv-retry infinite
nobind
persist-key
persist-tun
ca keys/ca.crt
cert keys/yealink-t45w.crt
key keys/yealink-t45w.key
tls-auth keys/ta.key 1
cipher AES-256-CBC
verb 3
<li>Create tar file: <div class="code-block">tar -cf yealink-vpn.tar vpn.cnf keys/</div></li>
7.2 Configure Phone
Access phone web interface (usually http://phone-ip)
Go to Network → VPN
Upload the .tar file using the first upload button
Enable VPN
7.3 Register Extension
Go to Account → Register
Configure Account 1:
Line Active: ON
Label: 101
Display Name: Office Phone
Register Name: 101
Username: 101
Password: Your extension secret
Server Host: 172.18.0.2 (Docker container IP)
Yealink Phone Configuration & Quirks
This phase focused on getting the client-side configuration correct for the phone’s specific firmware behavior.
Problem: The phone would not connect to the VPN, or would connect but have no network access. I discovered the phone was mishandling the configuration package.
Cause: Yealink firmware can be very specific and buggy about the format and contents of the VPN configuration package.
Solution & Key Steps:
Initial Attempt (.tar with vpn.cnf): My first attempt at creating and uploading an openvpn.tar file with vpn.cnf in the root and keys in a keys/ subdirectory. The phone appeared to upload it but only processed the .cnf file, ignoring the essential keys.
<li><strong>Troubleshooting Detour 1 (.ovpn):</strong> Suspecting a filename issue, I renamed vpn.cnf to vpn.ovpn inside the .tar archive. This caused the phone to fail the upload entirely, proving it was specifically looking for vpn.cnf.</li><li><strong>Troubleshooting Detour 2 (Inline .ovpn):</strong> I bypassed the .tar method and created a single, all-in-one .ovpn file with all keys embedded. The phone’s “Import” function would not accept this file format either.</li><li><strong>Final Solution (Corrected .tar):</strong> I finally concluded the phone required the .tar format with the vpn.cnf and keys/ structure. The initial failure was actually due to the server-side configuration being incomplete at the time. Once the server was fully working, the original, correctly structured .tar file was accepted and processed properly.</li><li><strong>Factory Reset:</strong> When the phone failed to save the SIP password correctly, I performed a factory reset to clear any stuck or partially provisioned settings. This resolved the password saving issue.</li>
yealink tar file config setup
Twilio Configuration / Setup
Summary of Twilio Elastic SIP Trunk Configuration
This document outlines the final, correct steps to configure a Twilio Elastic SIP Trunk for use with a Dockerized FreePBX instance. This setup uses IP-based authentication, which is secure and does not require registering the trunk with a username and password.
Part 1: Twilio Account Configuration
Twilio Account Setup
First, you’ll need a Twilio account and a phone number:
Sign up for a Twilio account at twilio.com
Purchase a phone number from Twilio (about $1/month)
Navigate to the Elastic SIP Trunking section in your Twilio Console
6.2 Create IP Access Control List (Required for Security)
Twilio uses IP-based authentication instead of username/password. You must whitelist your VPS IP:
In Twilio Console, go to Elastic SIP Trunking → Authentication → IP Access Control Lists
Click Create new IP Access Control List
Name it something descriptive like “FreePBX Server”
Click Create
Now add your VPS IP:
Click on your new ACL
Click Add IP Address
Enter your VPS public IP with /32 subnet (e.g., YOUR_VPS_IP/32)
Click Add IP Address
6.3 Create the Elastic SIP Trunk
Go to Elastic SIP Trunking → Trunks
Click Create new SIP Trunk
Give it a friendly name like “PBX-Trunk”
Click Create
6.4 Configure Trunk Termination (Outbound Calls)
This tells Twilio where to receive calls FROM your PBX:
Click on your new trunk
Go to the Termination tab
Under Termination SIP URI:
Click Add new Termination SIP URI
Create a unique name (e.g., mypbx.pstn.ashburn.twilio.com)
Select your region (e.g., Ashburn for US East)
Priority: 10
Weight: 10
Click Save
Under Authentication:
Select IP Access Control Lists
Choose the ACL you created earlier
Click Save
6.5 Configure Trunk Origination (Inbound Calls)
This tells Twilio where to send calls TO your PBX:
Stay in your trunk settings, go to the Origination tab
Click Add new Origination URI
Configure:
Origination SIP URI: sip:YOUR_VPS_IP:5060
Priority: 10
Weight: 10
Enabled: Yes
Click Add
6.6 Assign Phone Number to Trunk
Go to the Numbers tab in your trunk
Click Add an Existing Number
Select your Twilio phone number from the dropdown
Click Add
FreePBX Setup
FreePBX Setup Guide: Twilio PJSIP Trunk
This document details the final, working configuration for a PJSIP trunk connecting FreePBX to a Twilio Elastic SIP Trunk. It includes the trunk settings, the necessary call routing, and an explanation of the troubleshooting steps that led to this specific configuration.
Prerequisite: This guide assumes your core network settings under Settings -> Asterisk SIP Settings are correct (External IP and Local Networks are defined).
Part 1: The PJSIP Trunk Configuration
This is the main connection that handles call signaling between your PBX and Twilio.
Outbound CallerID: Your Twilio number in E.164 format: +1XXXXXXXXXX
“pjsip Settings” -> “General” Sub-Tab
Authentication: None
Registration: None
SIP Server: yourname.pstn.ashburn.twilio.com (The regional Termination URI from your Twilio dashboard)
Context: from-pstn
“pjsip Settings” -> “Advanced” Sub-Tab
From Domain: yourname.pstn.ashburn.twilio.com (Must match the SIP Server)
Direct Media: No
Contact User: 101 (Your extension number)
Send RPID/PAI: Send P-Asserted-Identity header
Summary
FreePBX Core Setup: Asterisk SIP Settings
This section covers the essential network configuration for the Asterisk engine that powers FreePBX. These settings are the foundation for ensuring calls, especially those crossing different networks (like from a VPN or the public internet), have two-way audio.
Navigation:
In the FreePBX web UI, go to Settings -> Asterisk SIP Settings.
Click on the Chan PJSIP Settings tab.
1. External IP Address
This is the single most important setting for NAT traversal. You must tell FreePBX what its public IP address is so it can correctly construct SIP packets. When a call is made, Asterisk uses this IP address in the SIP/SDP headers to tell the other party where to send the audio (RTP) stream back to.
Setting: External IP Address
Your Value: [YOUR_VPS_PUBLIC_IP]
Why it’s important: If this is blank or incorrect, Asterisk will use its private Docker IP (172.18.0.2) in the audio headers. An external device (like Twilio’s servers or your phone on a different network) has no way to route audio to that private address, resulting in one-way audio.
2. Local Networks
This field tells FreePBX which IP address ranges it should consider “friendly” or “local.” Any device connecting from an IP in this list will be treated as if it’s on the same local network, which allows for direct media paths and proper NAT handling.
Setting: Local Networks
Your Values (each on a new line):
127.0.0.1/32
172.18.0.0/16
10.8.0.0/24
Explanation of each network:
127.0.0.1/32: This is the server’s “localhost” or loopback address. It’s essential for internal communication within the server itself.
172.18.0.0/16: This is the private network created by Docker for your FreePBX container. Adding it ensures proper communication between the container and the host.
10.8.0.0/24: This is your OpenVPN client network. Adding this was the critical step that allowed your Yealink phone to register and be treated as a trusted local device.
Final Configuration View:
After verifying these settings are correct, you must click Submit and then the red Apply Config button. For these core network settings, it is always recommended to perform a full restart of the service (docker restart freepbx) to ensure they are loaded cleanly.
FreePBX Setup Guide: Inbound Call Routing
This document provides a detailed breakdown of how to configure FreePBX to correctly handle incoming calls from your Twilio SIP trunk and route them to your internal extension.
1. The Goal
The objective is to create a rule that tells FreePBX: “When a call arrives from the public telephone network via the Twilio trunk for a specific phone number, send that call to this specific internal extension.”
Without this rule, FreePBX receives the call but has no instructions on what to do with it, which results in the caller hearing a ringback tone while no internal phones actually ring.
2. The Two Key Components
Getting inbound routing to work correctly involves two separate but related configurations in FreePBX.
Component A: The Trunk’s Context
The first step is to ensure that calls coming from your Twilio trunk are correctly identified as being from the “outside world.” This is handled by the Context setting on the trunk itself.
Navigation: Connectivity -> Trunks -> Edit your Twilio trunk -> pjsip Settings tab -> General sub-tab.
Setting: Context
Your Value: from-pstn
Why it’s important: The from-pstn context is a special, pre-defined entry point in FreePBX’s dialplan. It tells the system to immediately look at your Inbound Routes to find a match for the dialed number. If this is set incorrectly, the call will never reach the Inbound Routes section.
Component B: The Inbound Route Rule
This is the specific instruction that connects a public phone number to an internal destination.
<li><strong>DID Number (The Most Critical Field):</strong> <ul> <li>DID stands for “Direct Inward Dialing.” This field must contain the phone number exactly as Twilio sends it to your PBX.</li> <li><strong>Your Value:</strong> +1XXXXXXXXXX</li> <li><strong>Why it’s important:</strong> During the torturous troubleshooting session, I confirmed from logs that Twilio sends the number in the full E.164 format, which includes the country code and a + sign. If this field was set to XXXXXXXXXX or 1XXXXXXXXXX, FreePBX would not find a match and would not know what to do with the call.</li> </ul></li><li><strong>Set Destination:</strong> <ul> <li>This tells FreePBX where to send the call after it finds a match on the DID Number.</li> <li><strong>Your Value:</strong> I set the destination to Extension and selected your internal extension, 101.</li> </ul></li>
Final Configuration View:
Component A: Route Settings
This tab gives the route a name and, most importantly, links it to the correct trunk.
Route Name:
A friendly name for your reference.
Your Value: Twilio-Outbound
<li><strong>Trunk Sequence for Matched Routes:</strong> <ul> <li>This tells FreePBX which trunk(s) to use when a dialed number matches the rules on this route.</li> <li><strong>Your Value:</strong> You must select your Twilio trunk from the dropdown menu.</li> </ul></li>
Component B: Dial Patterns
This tab is where the magic happens. It defines what a valid outbound number looks like and how to reformat it before sending it to the trunk. This was the critical fix for the Invalid phone number error from Twilio.
The Goal: To take a dialed number like 1XXXXXXXXXX and transform it into +1XXXXXXXXXX.
The Rule: You will fill out one row in the “Dial Patterns that will use this Route” table.
prepend field: +
Why it’s important: This adds the + character to the beginning of the number, which is required by Twilio for E.164 formatting.
<li><strong>prefix field:</strong> (leave blank) <ul> <li>We are not stripping any digits from what the user dials.</li> </ul></li><li><strong>match pattern field:</strong> 1NXXNXXXXXX <ul> <li><strong>Why it’s important:</strong> This pattern matches numbers that start with 1, followed by a digit from 2-9 (N), followed by any digit (X), and so on, for a total of 11 digits. This ensures that only valid North American numbers are sent to this route, preventing accidental calls to other internal extensions.</li> </ul></li>
Final Configuration View:
3. Final Step: Apply Config
After creating or modifying the Outbound Route, you must click the red Apply Config button at the top right of the FreePBX interface. This makes the new rule live. Now, when you dial an 11-digit number from your Yealink phone, FreePBX will find this matching rule, format the number correctly, and send the call to Twilio successfully.
3. Final Step: Apply Config
After creating or modifying the Inbound Route, you must click the red Apply Config button at the top right of the FreePBX interface. This makes the new rule live in the system. Once applied, any call to +1XXXXXXXXXX that arrives from the Twilio trunk will be immediately directed to ring extension 101.
Final Configuration Guide: FreePBX Extension & Yealink Phone
This document provides the final, working steps to create a PJSIP extension in FreePBX and correctly configure your Yealink T45W phone to register to it through the VPN.
Part 1: The FreePBX Extension Setup
This is where you create the “phone line” on your PBX.
1. Navigate to the Extensions Menu
In your FreePBX web UI, go to Applications -> Extensions.
2. Create the New Extension
Click the + Add Extension button.
From the dropdown, select + Add New PJSIP Extension.
3. Fill in the Extension Details
User Extension: Enter the extension number (e.g., 101).
Display Name: Enter a descriptive name (e.g., Albert VPN Phone).
Secret: Enter the password for the extension. To avoid issues with special characters, it’s best to use a strong password with only letters and numbers (e.g., TestPassword1234). This is the password you will enter into the phone.
4. Configure Advanced NAT Settings
Click the Advanced tab for the extension.
Set the following fields to Yes:
Rewrite Contact
RTP Symmetric
Force RPort
These settings are crucial for ensuring two-way audio works correctly through the VPN and firewall.
5. Save and Apply
Click the Submit button at the bottom of the page.
Click the red Apply Config button that appears at the top right.
Part 2: The Yealink Phone Configuration
This is where you tell your phone how to log into the extension you just created.
1. Navigate to the Account Page
Log into your Yealink phone’s web UI.
Click on the Account tab at the top.
On the left, click on Register, then select Account 1.
2. Enter the Registration Credentials
Fill out the form using the exact information from the FreePBX extension.
Yealink Field
What to Enter
Explanation
Line Active
ON
Enables this phone line.
Label
101 – Albert
The text that appears on your phone’s screen.
Display Name
Albert
Your internal Caller ID name.
Register Name
101
The “User Extension” you set in FreePBX.
Username
101
Also the “User Extension” from FreePBX.
Password
TestPassword1234
The exact “Secret” you set in FreePBX for extension 101.
Server Host
172.18.0.2
The private IP address of your FreePBX Docker container.
3. Confirm and Verify
Scroll to the bottom of the page and click the Confirm button.
The Register Status at the top of the page should change to “Registered”.
The Yealink phone screen should also show the new label and be ready to make calls.
Note: Be sure to set the following options to “NO” so that you can make the configurations changes.
This guide explains how to resolve the error preventing access to the voicemail system (*97). The issue is caused by a module conflict that stops the main voicemail application from loading.
Step 1: Edit the Asterisk Modules Configuration File
Since the option is not available in the web interface, so you will need to disable the conflicting module directly in its configuration file. This requires running a command on your VPS to edit the file inside the Docker container.
On your VPS command line, execute the following command. This will open the Asterisk modules file in the nano text editor.
<li>Inside the nano editor, use your arrow keys to scroll down to the section under the [modules] header.</li><li>Add the following new line anywhere in the list of noload directives. A good place is right after autoload=yes. <div class="code-block">noload = res_mwi_external.so</div> <p>This change explicitly tells Asterisk to not load this specific module when it starts, which will prevent the conflict and allow the main app_voicemail module to load correctly.</p></li><li>Save the file and exit the editor by pressing Ctrl + X, then Y, and then Enter. You will be returned to your regular VPS command prompt.</li>
Step 2: The Final Restart
Now You have to perform a full, clean restart of Asterisk to apply this change. The Apply Config button in the GUI is not sufficient for this.
On your VPS command line, run this command:
docker restart freepbx
<li>Wait about 60 seconds for the container and all its services to fully restart.</li>
Step 3: Test Your Voicemail
Pick up your Yealink phone and dial *97.
This time, the conflicting module will be disabled, app_voicemail will load correctly, and the call will connect to the voicemail system, allowing you to set up your mailbox.
If your ISP doesn’t use CGNAT, you can host FreePBX on a Raspberry Pi 5 at home:
One-time hardware cost: ~$80-100 (Raspberry Pi 5 + accessories)
Monthly telephony costs: Same $4.45/month
No VPS fees: Save $12/month
Total Monthly Cost: Only $4.45/month
When to use each setup:
Use VPS: If your ISP uses CGNAT or you need guaranteed uptime
Use Raspberry Pi: If you have a public IP and reliable home internet
Troubleshooting Tips
One-Way Audio
Check External IP in Asterisk SIP Settings
Verify Local Networks include VPN subnet
Ensure NAT settings on extension are enabled
Phone Won’t Register
Verify VPN connection is active
Check firewall rules allow traffic
Try factory reset on phone if password won’t save
Calls Fail with “Invalid Number”
Verify outbound route dial pattern
Ensure number includes country code
Check trunk configuration matches Twilio settings
No Inbound Calls
Verify DID number matches exactly (including +)
Check trunk context is set to from-pstn
Ensure inbound route is configured
Security Considerations
Change Default Passwords: Always change the default FreePBX admin password
Firewall Rules: Only open necessary ports
VPN Security: Use strong certificates and consider implementing fail2ban
Regular Updates: Keep your VPS, Docker images, and FreePBX modules updated
Backup Configuration: Regularly backup your FreePBX data directory
Bonus: Mobile Access with Talkyto
One of the best features of using Twilio is the Talkyto mobile app (available for iOS and Android). This app provides additional functionality beyond your desk phone:
Talkyto Features:
Text Messaging: Send and receive SMS messages using your Twilio number
MMS Support: Handle image messages and multimedia content
Mobile Calling: Make outbound calls from your smartphone using your Twilio number
Real-time Notifications: Get instant alerts for incoming texts and calls
Cross-Platform: Works on both iOS and Android devices
Setting Up Talkyto:
Download the Talkyto app from your app store
Log in with your Twilio account credentials
Select your phone number from the list
You’re ready to use your business number on the go!
This means you’re not tied to your desk phone – you can handle business communications from anywhere, making this solution even more valuable for remote work and travel.
Conclusion
This setup provides a professional VoIP system that bypasses CGNAT restrictions, offers secure remote access, and costs only $4.45/month for phone service (including Talkyto). The combination of VPS hosting, Docker containerization, VPN tunneling, and Twilio’s reliable SIP trunking creates a robust communication solution suitable for home offices or small businesses.
With the addition of the Talkyto mobile app, you get a complete unified communications system – desk phone at home/office via VPN, plus mobile access for calls and texts on the go. The initial setup requires some technical knowledge, but once configured, the system is stable and requires minimal maintenance. The ability to use both physical VoIP phones and mobile devices with the same number makes this solution particularly valuable for modern remote work scenarios.
For users without CGNAT restrictions, consider using a Raspberry Pi 5 instead of a VPS to reduce the total monthly cost from ~$16.45 to just $4.45.
These observational values were auto-extracted from this report and are not a blocklist or a maliciousness verdict. Values shown are defanged for safe viewing; downloads contain live values. YARA origin was not recorded for this older report. Review the file header and original source before use.
Newsletter
The House-Of-L Brief.
Two short reads a day on markets, cyber threats, AI, and geopolitics. A "why it matters" line on every story.
readers get The Brief
Double opt-in. One-click unsubscribe on every issue. We never share your address.
We use Google Analytics to measure site traffic, which sets cookies. No ads and no cross-site tracking. See our privacy policy.