# Restore a FreePBX Backup to a New Server: Tested on FreePBX 17

Source: https://srvscripts.com/guides/restore-a-freepbx-backup/
Updated: 2026-10-18
Publisher: srvScripts (https://srvscripts.com/)

A backup you have never restored is a hope, not a plan. To restore a FreePBX backup to a new server for this guide, we took a working FreePBX 17 lab server with extensions, a SIP trunk, outbound and inbound routes and call records, backed it up, wiped the server back to a blank Debian 12 and restored it. Then we compared the result line by line with the original and made real test calls.

**Short answer:** Block the web interface to everyone but you, install the same FreePBX version on the new server, copy the backup into a new private folder the asterisk user owns, and run `fwconsole backup --restore=FILE` from that folder, then `fwconsole reload`. Extensions, trunks, routes, the admin user and the call records came back. Three things did not: the `_custom.conf` files, call recordings, and anything a backup run from `/root` silently skipped. Check the backup log for “returned no data” before you rely on it.

**Scope of this test, in brief:**

- **Tested on:** one lab server: Debian 12, FreePBX 17.0.33.2, Asterisk 22.11.0, Backup module 17.0.13, restored onto the same VPS reinstalled blank, with the same versions and the same IP address ([details](#what-we-tested)).
- **Not in the backup:** `_custom.conf` files and call recordings. Copy them separately ([how](#copy-back-what-the-backup-left-out)).
- **Failure checks:** "returned no data" for core, cdr or cel in the backup log; a restore that exits 0 but logs "There were errors"; a before/after fingerprint diff ([backup checks](#before-you-start-make-sure-the-backup-contains-everything), [comparison](#compare-the-restored-server-with-the-original)).
- **Not tested:** a new server with a different public IP ([checklist only](#moving-to-a-different-ip-address-not-tested)), a different FreePBX version or OS, commercial modules, voicemail and recording restores, audio from real phones.
- **Download:** the fingerprint script and our lab’s before and after outputs: [freepbx-restore-kit.zip](https://srvscripts.com/wp-content/uploads/srvs-examples/freepbx-restore/freepbx-restore-kit.zip) ([README](https://srvscripts.com/wp-content/uploads/srvs-examples/freepbx-restore/README.txt)).

In short: Block the web interface to everyone but you, install the same FreePBX version on the new server, copy the backup into a new private folder the asterisk user owns, and run fwconsole backup –…

## What we tested

| Step | Result on our lab |
| --- | --- |
| Source server | Debian 12, FreePBX 17.0.33.2, Asterisk 22.11.0, 77 modules: 3 PJSIP extensions, a custom PJSIP trunk, 3 outbound routes, 1 inbound DID route, 998 CDR rows |
| Backup | FreePBX Backup module 17.0.13, all modules, local storage: a 69.8 MB .tar.gz in 7 seconds |
| New server | The same VPS reinstalled with a blank Debian 12, then the official FreePBX 17 installer (open-source modules only): same versions as the source |
| Restore | fwconsole backup --restore: 3 min 13 s, exit code 0, “There were errors during the restore process” (voicemail, no visible effect) |
| Compared with the original | Extensions, devices, trunk, routes, inbound route, extension settings, admin user and CDR all identical; _custom.conf files and recordings missing |
| Test calls | Outbound through the trunk: answered. Inbound to the DID: rang extension 1001 and answered. |

The server kept its IP address, so we did not need `--skipremotenat`. We did not test a restore onto a different FreePBX version, commercial modules, or a server with a different public IP (there is an untested checklist for that below).

## Before you start: make sure the backup contains everything

This is the step most guides skip, and it is where our first attempt went wrong. We started the backup with `fwconsole backup --backup=ID` from a folder under `/root`. FreePBX runs the database dumps as the asterisk user in the current folder, which that user cannot open. The core, CDR and CEL modules were skipped, the command still exited with code 0, and the archive was only 0.2% smaller (69.62 MB against 69.79 MB). That backup had no extensions and no trunks.

```
cd /tmp                                   # never run fwconsole backup from /root
fwconsole backup --backup=BACKUP-ID 2>&1 | tee /root/backup.log
grep -E "returned no data|provided cwd" /root/backup.log     # anything other than empty modules?
tar -tzf /var/spool/asterisk/backup/FILE.tar.gz | grep -E "modulejson/Core.json|Devices.sql|cdr.sql"
```

“returned no data” is normal for modules you never configured (on our lab: callback, miscdests, setcid, ttsengines). It is not normal for core, cdr or cel. We saw this with a manual run from a root shell. Scheduled runs start from the asterisk user’s crontab, so they should not hit it, but the same log check costs nothing.

Two things are not in the backup by default, so copy them separately or add them to the job as custom directories:

- **Your `_custom.conf` files** in /etc/asterisk (pjsip_custom.conf, extensions_custom.conf, logger_logfiles_custom.conf and the others). Our backup contained none of them.

- **Call recordings** in /var/spool/asterisk/monitor. Sangoma’s documentation says to add that folder as a custom directory if you want them.

Our [FreePBX off-site backup script](/scripts/freepbx-offsite-backup/) copies the whole of /etc/asterisk with the databases, which is how we got the custom files back in this test.

## Install the same FreePBX version on the new server

**Block the web interface before you start the installer.** A fresh FreePBX has no admin user: whoever opens the web interface first creates one. Our new server was in that state for about 8 minutes, from the end of the installer until the restore brought the old admin account back, and a quick restore does not close that window. So close it first: in your provider’s firewall, allow ports 80 and 443 only from your own IP address, for IPv4 and IPv6 (and SSH only from your address too, if you can). Do not count on the FreePBX firewall: the installer with `--opensourceonly` removes the firewall module. Without a provider firewall, these nftables rules on the new server do the same:

```
ME=198.51.100.7                     # your own public IPv4 address
nft add table inet fpbx_setup
nft add chain inet fpbx_setup input '{ type filter hook input priority -10 ; policy accept ; }'
nft add rule inet fpbx_setup input iifname lo accept
nft add rule inet fpbx_setup input tcp dport '{ 80, 443 }' ip saddr $ME accept
nft add rule inet fpbx_setup input tcp dport '{ 80, 443 }' drop     # everyone else, all of IPv6 too
nft list table inet fpbx_setup       # check again when the installer has finished
```

Reach the web interface from your address, or through an SSH tunnel: `ssh -L 8080:127.0.0.1:80 root@NEW-SERVER`, then open `http://127.0.0.1:8080/admin`. From another machine, check that the block works: `curl -m 5 -sI http://NEW-SERVER/admin/` must time out. Remove the rules (`nft delete table inet fpbx_setup`) only when you have logged in with your restored admin account and the firewall you keep is in place. We tested these rules on the lab after the restore: a second server got no answer on ports 80 and 443 until its address was allowed, and the local path the tunnel uses kept working. We did not run the installer with the rules in place, so list the table again when it has finished.

Use the same FreePBX major version and the same operating system as the old server: for FreePBX 17 that is Debian 12. Our [FreePBX 17 install guide](/guides/install-freepbx-17-debian-12/) has the full steps. In this test the official installer (`sng_freepbx_debian_install.sh`, SCRIPTVER 1.15) with `--opensourceonly` stopped at “Removing commercial modules”, exactly as in our earlier install: line 1293 passes the list of commercial modules to `fwconsole ma -f remove`, and it exited with code 123 because the oracle_connector module failed to uninstall. FreePBX itself was already installed at that point. We added `|| true` to the end of that one line, so a failed removal no longer stops the script, and ran it again with `--skipversion`; it finished in under 5 minutes. Afterwards `fwconsole ma list` showed framework, core and backup enabled and `fwconsole reload` completed. Only do this for that line and that error: a newer installer may have fixed it, and any other failure needs its own answer (the installer log is in `/var/log/pbx/`).

```
fwconsole -V                                    # same framework version as the old server?
asterisk -rx "core show version"
fwconsole ma list | grep -E " backup | core | framework "
```

Keep the web interface blocked from the start of this section until the restore has brought back your admin account and you have logged in with it.

[](https://srvscripts.com/wp-content/uploads/2026/10/srvscripts-lab-rst-01-blank-state.png)The new server after a fresh FreePBX 17 install: same versions as the old one, no configuration.

## Restore a FreePBX backup with fwconsole

A backup contains your SIP, manager and database passwords, so it must not be readable by other users of the server, which a copy in `/tmp` with default permissions usually is. Copy it into a new private folder that only root and the asterisk user can open, and restore from that folder:

```
D=$(mktemp -d /tmp/fpbx-restore.XXXXXX)        # new folder, mode 700
chown asterisk:asterisk "$D"
install -m 600 -o asterisk -g asterisk /root/freepbx-backup.tar.gz "$D/freepbx-backup.tar.gz"
cd "$D"
fwconsole backup --restore="$D/freepbx-backup.tar.gz" > "$D/restore.log" 2>&1; echo "restore exit code: $?"
tail -5 "$D/restore.log"            # read this before you go on
```

Go on only when the exit code is 0 and the end of the log shows no error you cannot explain. Then, as a separate step:

```
fwconsole reload
```

Check the exit code and the end of the log before `fwconsole reload`: the restore can return 0 and still end with “There were errors during the restore process”, as ours did (see below). We ran these commands on the lab twice (with `tee` instead of the log redirect), with umask 022 and 077 and the backup in a folder under `/root`: both restores exited 0 and gave the same result, with the folder at mode 700 and the backup at 600, both owned by asterisk. When you no longer need them, delete the folder and the log in it with `rm -rf "$D"`; the original backup stays where you copied it from.

If the new server has a different public IP or network, add `--skipremotenat`, set the NAT settings by hand afterwards and work through the checklist for a different IP address below. Our older [fwconsole backup and restore reference](/guides/freepbx-backup-restore-cli/) lists the other restore options.

Read the end of the log, not only the exit code. Ours ended with “There were errors during the restore process” and still returned 0. The error came from the voicemail module (`Undefined array key "settings"`) on a system with no voicemail boxes, and we found nothing missing afterwards; on a system with voicemail, check the mailboxes by hand.

[](https://srvscripts.com/wp-content/uploads/2026/10/srvscripts-lab-rst-02-restore-summary.png?v=1792213037)The end of the restore log: exit code 0, but “There were errors during the restore process”.

## Compare the restored server with the original

Before the backup we saved a fingerprint of the configuration: versions, extensions, devices, trunks, routes, inbound routes, a few extension settings, the custom files, the CDR count and the PJSIP endpoints. Running the same script after the restore and comparing the two shows exactly what came back:

```
# on the old server, before the backup: save state.sh in a folder, read it, then
bash ./state.sh > before.txt
# copy before.txt to the new server, for example: scp before.txt root@NEW-SERVER:/root/
# on the new server, after the restore and fwconsole reload, in the folder with state.sh and before.txt
bash ./state.sh > after.txt
diff before.txt after.txt
```

This is the script we used. It only reads, and it prints no password: SIP settings are limited to a few keywords and the custom files appear as checksums. It does print extension names, trunk dial strings, routes and caller ID settings, so keep its output private or review it before you share it. Change `1002` to one of your extensions. Download: [state.sh](https://srvscripts.com/wp-content/uploads/srvs-examples/freepbx-restore/state.sh), or the [restore kit](https://srvscripts.com/wp-content/uploads/srvs-examples/freepbx-restore/freepbx-restore-kit.zip) with our lab’s outputs before the backup, on the blank server, after the restore and after copying the custom files back.

```
#!/bin/bash
# state.sh - configuration fingerprint of a FreePBX server (no secrets): compare before backup and after restore
echo "== versions"; fwconsole -V 2>/dev/null | tail -1; asterisk -rx "core show version" | cut -d" " -f1-2
echo "== modules enabled: $(fwconsole ma list 2>/dev/null | grep -c Enabled)"
echo "== extensions"; mysql -N asterisk -e "select extension, name from users order by extension"
echo "== devices tech"; mysql -N asterisk -e "select id, tech from devices order by id"
echo "== trunks"; mysql -N asterisk -e "select trunkid, name, tech, channelid from trunks order by trunkid"
echo "== outbound routes"; mysql -N asterisk -e "select r.name, group_concat(p.match_pattern_pass order by p.match_pattern_pass) from outbound_routes r left join outbound_route_patterns p using(route_id) group by r.route_id order by r.name"
echo "== inbound routes"; mysql -N asterisk -e "select extension, cidnum, description, destination from incoming order by extension"
echo "== ext 1002 settings"; mysql -N asterisk -e "select keyword, data from sip where id=\"1002\" and keyword in (\"context\",\"dtmfmode\",\"max_contacts\",\"direct_media\",\"rtp_symmetric\",\"outbound_concurrency_limit\",\"callerid\") order by keyword"
echo "== voicemail boxes: $(grep -cE "^[0-9]+ =>" /etc/asterisk/voicemail.conf 2>/dev/null)"
echo "== custom files"; for f in pjsip_custom.conf pjsip.transports_custom.conf extensions_custom.conf logger_logfiles_custom.conf; do printf "%s %s\n" "$f" "$(sed "s/password=.*/password=x/" /etc/asterisk/$f 2>/dev/null | md5sum | cut -c1-12)"; done
echo "== cdr rows: $(mysql -N asteriskcdrdb -e "select count(*) from cdr")"
echo "== recordings: $(find /var/spool/asterisk/monitor -type f 2>/dev/null | wc -l)"
echo "== pjsip endpoints"; asterisk -rx "pjsip show endpoints" | awk "\$1==\"Endpoint:\" && \$2 !~ /^
