12 KiB
SvxPortal web — installation HOWTO
This is the web front-end only for a SvxReflector: node map, monitor, last-heard, talkgroup list, user accounts and the reflector admin pages. It is plain HTML/JavaScript plus a small PHP backend that stores accounts in Redis. No build step, no framework, no composer.
What you need on the server:
| Component | Why |
|---|---|
| nginx | serves the static files and passes *.php to PHP-FPM |
| PHP 8.4 with FPM | the auth/ backend (login, accounts, admin API). Developed and tested on 8.4 |
PHP extensions: redis, mbstring, openssl, json, session |
used by auth/lib.php |
| Redis 6+ | stores accounts, sessions, bans, talkgroup requests |
| A way to send e-mail | signup verification and password reset (local MTA or an SMTP relay, see step 5) |
| TLS certificate | the session cookie is Secure; login does not work over plain HTTP |
| A running SvxReflector | we use GeuReflector (SvxReflector with trunking); install/svxreflector.conf.example is for that build |
Everything below assumes Debian 12/13 or Ubuntu 24.04, PHP 8.4 and that the
site will live at https://svxportal.example.com. Replace the host name
everywhere you see it.
1. Packages
sudo apt install nginx php8.4-fpm php8.4-redis php8.4-mbstring php8.4-cli redis-server certbot openssl
Check the PHP version you actually got (php -v) and use that number instead
of 8.4 in the file names below.
2. Copy the web files
sudo mkdir -p /var/www/html/svxportal
sudo rsync -a --exclude .git --exclude install --exclude INSTALL.md ./ /var/www/html/svxportal/
sudo chown -R www-data:www-data /var/www/html/svxportal
sudo find /var/www/html/svxportal -type d -exec chmod 2775 {} +
sudo find /var/www/html/svxportal -type f -exec chmod 0644 {} +
3. Redis (dedicated instance, unix socket)
sudo cp install/redis-svxportal.conf /etc/redis/redis-svxportal.conf
sudo mkdir -p /var/lib/redis-svxportal
sudo chown redis:redis /var/lib/redis-svxportal /etc/redis/redis-svxportal.conf
sudo chmod 640 /etc/redis/redis-svxportal.conf
Generate a password and put it in the config:
openssl rand -hex 32
sudo sed -i "s/^requirepass .*/requirepass PASTE-THE-VALUE-HERE/" /etc/redis/redis-svxportal.conf
sudo systemctl enable --now redis-server@svxportal
The socket is created with mode 770 and group redis, so the web user must be
in that group:
sudo usermod -aG redis www-data
Test:
redis-cli -s /run/redis-svxportal/redis.sock -a 'PASTE-THE-VALUE-HERE' ping # -> PONG
4. PHP-FPM
Two pools are used. The default pool (www) handles all normal pages. A
second small pool handles only auth/admin_reflector_api.php, so its longer
timeout and separate process limits do not affect the rest of the site.
sudo cp install/php-fpm-svxadmin.conf /etc/php/8.4/fpm/pool.d/svxadmin.conf
Hardening of the default pool is recommended. Edit
/etc/php/8.4/fpm/pool.d/www.conf and add at the end:
php_admin_value[disable_functions] = passthru,shell_exec,system,popen,proc_open,proc_close,pcntl_exec,dl,phpinfo,show_source
php_admin_value[expose_php] = Off
Do not add exec to that list. The portal uses exec() in the default
pool too: openssl req to read the e-mail out of a pending CSR, and the sudo
helpers from the node pages (auth/node_auth_api.php, auth/node_cmd_api.php,
auth/admin_nodes.php). With exec disabled those pages return errors.
Then:
sudo systemctl restart php8.4-fpm
ls -l /run/php/ # both php8.4-fpm.sock and php8.4-fpm-svxadmin.sock must exist
5. Web app config (auth/config.php)
cd /var/www/html/svxportal
sudo -u www-data cp auth/config.php.example auth/config.php
sudo chmod 640 auth/config.php
sudoedit auth/config.php
Set:
redis.unix_socket->/run/redis-svxportal/redis.sockredis.password-> the value from step 3base_url->https://svxportal.example.com(used in the e-mail links)mail.from-> the sender address for verification / reset mails
E-mail: with only mail.from set, PHP's mail() is used, which needs a
working local MTA (/usr/sbin/sendmail, e.g. msmtp-mta or postfix).
If you do not have one, add an SMTP relay instead and no MTA is needed:
'mail' => [
'from' => 'no-reply@example.com',
'from_name' => 'SvxPortal',
'host' => 'smtp.example.com', // SSL SMTP (implicit TLS)
'port' => 465,
'user' => 'smtp-user',
'pass' => 'smtp-password',
],
config.php is listed in .gitignore and is never served by nginx (all
*.php go to PHP-FPM, which just executes return [...]). Keep it mode 640.
6. Talkgroup list file
The talkgroup list is a JSON file the portal reads and writes:
sudo cp install/tmpfiles-svxportal-tglist.conf /etc/tmpfiles.d/svxportal-tglist.conf
sudo systemd-tmpfiles --create /etc/tmpfiles.d/svxportal-tglist.conf
This creates /etc/svxlink/tglist.json owned by www-data. The path is
tglist_file in config.php; change both if you want it elsewhere.
6b. Optional: follower box that mirrors another portal's talkgroup list
If you run several reflectors and want ONE talkgroup list, one portal is the
master (its list is edited in the web) and the others pull it four times a day
with a systemd timer. Nothing but curl and python3 is involved.
sudo cp install/systemd/svxportal-tglist-sync.service install/systemd/svxportal-tglist-sync.timer /etc/systemd/system/
sudo cp install/systemd/tglist-sync.sh /etc/svxlink/tglist-sync.sh && sudo chmod 755 /etc/svxlink/tglist-sync.sh
sudo cp install/systemd/default-svxportal-tglist-sync /etc/default/svxportal-tglist-sync
sudoedit /etc/default/svxportal-tglist-sync # UPSTREAM = the master's https://.../tglist.php
sudo systemctl daemon-reload
sudo systemctl enable --now svxportal-tglist-sync.timer
sudo systemctl start svxportal-tglist-sync.service && journalctl -u svxportal-tglist-sync -n 3
On the master, or on a standalone box, do not install this (or leave
UPSTREAM empty; the script then exits without touching the file).
Other systemd notes:
svxreflector.serviceis used unmodified from the SvxLink package. No drop-in is needed for the portal.- Redis runs from Debian's
redis-server@.servicetemplate (step 3); no unit file to add. - There are no cron jobs. PHP session cleanup is Debian's stock
phpsessionclean.timer.
7. nginx + TLS
sudo mkdir -p /var/www/letsencrypt
sudo cp install/nginx-svxportal.conf /etc/nginx/sites-available/svxportal
sudo sed -i 's/svxportal\.example\.com/YOUR.HOST.NAME/g' /etc/nginx/sites-available/svxportal
Get a certificate first (the config references it):
sudo certbot certonly --webroot -w /var/www/letsencrypt -d YOUR.HOST.NAME
If certbot has not created /etc/letsencrypt/options-ssl-nginx.conf and
ssl-dhparams.pem, run sudo certbot --nginx once or remove those two lines.
sudo ln -s /etc/nginx/sites-available/svxportal /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
Open https://YOUR.HOST.NAME/ — the page should render. Click Sign in and
you should get a proper "wrong password" style error, not a 500. A 500 here
almost always means Redis (wrong socket/password, or www-data not in group
redis).
Also edit these files in the web root for your own site name: robots.txt,
sitemap.xml and the <title>/meta tags at the top of index.html.
8. First admin account
Accounts normally self-register and verify by e-mail. The first admin is
created from the command line with the bundled script; it reuses the
portal's own library, so it writes exactly what signup.php would:
cd /var/www/html/svxportal
sudo -u www-data php svx-mkadmin.php
Answer the prompts (callsign, e-mail, password, admin level). Levels:
| Level | Meaning |
|---|---|
| 1 | normal user |
| 2 | admin: accounts, nodes, certificates, talkgroups |
| 3 | super admin: may also see reflector/MQTT secrets and manage other admins |
Log in on the web page with that callsign. The Admin menu appears.
9. Live data API — what this repository does NOT contain
The dashboard (node list, map, monitor, last-heard, trunks) fetches JSON and
Server-Sent Events from /svxportal/api/..., which nginx proxies to
127.0.0.1:8098. That backend service is not included here. Without it
the account/admin/talkgroup pages work, but the front page's node list, map,
monitor, last-heard and trunks views simply stay empty. There is no error
banner: the API helper returns null on a failed fetch and the page keeps its
empty tables. Check the browser console (404 on /svxportal/api/status) if
you are unsure.
Endpoints the UI expects:
| Path | Used by |
|---|---|
/svxportal/api/status |
node table, topology |
/svxportal/api/stream |
live updates (SSE) |
/svxportal/api/lastheard |
last-heard list |
/svxportal/api/monitor-nodes |
monitor view |
/svxportal/api/local-trunks |
trunks view |
You need to provide something that serves these, for example a small service
that reads your reflector's HTTP_SRV_PORT status endpoint
(GeuReflector serves /status there)
and republishes it. The static reflector status page in help.html describes the fields.
10. Reflector admin features (optional, need sudo helpers)
The pages Admin → Reflector, Certificates, Node control and MQTT
talk to the running svxreflector on the same machine. They do it through
small root-owned helper scripts called with sudo from
auth/admin_reflector_api.php, auth/node_cmd_api.php and
auth/node_auth_api.php:
Helper (expected in /usr/local/sbin/) |
Purpose |
|---|---|
svxreflector-pty |
writes NODE BLOCK/UNBLOCK and CA commands to the reflector's COMMAND_PTY |
svxreflector-systemctl |
restart/reload the svxreflector service |
svxreflector-users |
add/remove [USERS]/[PASSWORDS] entries in svxreflector.conf |
svx-mqtt-provision |
create/remove per-node MQTT accounts |
Those helpers and their /etc/sudoers.d/ rules are not part of this
repository; they are specific to how each reflector box is set up. Write
your own for your environment (validate the callsign argument strictly, allow
www-data to run only that one script with NOPASSWD), or skip this: all
other pages work without them and the admin API returns a clear error when a
helper is missing.
The certificate pages additionally read /etc/svxlink/svxreflector.conf to
find CERT_PKI_DIR and watch pending_csrs/ in it, so www-data needs read
access to the conf and the PKI directories (never to the private keys).
install/svxreflector.conf.example shows a complete reflector config as used
with this portal: PKI/CA paths, COMMAND_PTY, HTTP_SRV_PORT, the
[USERS]/[PASSWORDS] layout the helper edits, MQTT, and trunk/satellite
sections. All secrets and hostnames in it are placeholders.
11. Checklist when something does not work
- 500 on any
auth/*.php:sudo tail -f /var/log/nginx/svxportal-error.logandjournalctl -u php8.4-fpm. Usually Redis auth or missingphp-redis. - "Not connected" banner, empty node list: no backend on
127.0.0.1:8098(step 9). - No verification e-mail:
mail()without an MTA silently fails. Configure the SMTP relay (step 5) and checkjournalctl -u php8.4-fpmforsmtp:lines. - Login works but is lost on next page: site not on HTTPS (cookie is
Secure) orbase_urlwrong. - Admin API says "exec disabled":
admin_reflector_api.phpwas served by the default pool. Check the nginxlocation =block and thatphp8.4-fpm-svxadmin.sockexists.