WHD multi-instance install on Linux servers with an external database
Multiple Linux servers with one shared external database.
This guide sets up Web Help Desk across three Linux servers that all share one external database — Microsoft SQL Server in this example.
You do not need to be a Linux expert. Every command is written out, and each step tells you what you should see.
What you are building
|
Server |
Address |
Job |
|---|---|---|
|
Master |
10.XX.XX.80 |
Does all the background work — e-mail, reports, reminders |
|
Slave 1 |
10.XX.XX.81 |
Serves web pages to users only |
|
Slave 2 |
10.XX.XX.129 |
Serves web pages to users only |
|
Database |
10.XX.XX.235 |
Microsoft SQL Server — shared by all three |
Replace these addresses with your own throughout.
The one rule that matters most
one server may do the background work. That is the Master. If two servers do it, customers get duplicate e-mails and duplicate tickets.
The installer handles this for you when you use the --role option, so the main thing to get right is simply using the correct role on each server.
Why an external database is easier
Because the database lives on its own server, you do not need to open a database up to the network, edit database permission files, or rewrite connection settings on each server. Every server is configured the same way and points at the same place.
Before you begin
You need:
-
Three Linux servers that can reach each other and the database server
-
The same Web Help Desk package on all three — the installer file must be identical, or the saved database password will not work on the other servers
-
A database already created on your MySQL/Postgres/MSSQL Server (the wizard does not create it for you)
-
A database user with permission to create tables in that database
-
root access on all three servers
-
This document is for a fresh installation (not for upgrade)
Step 1 — Install the Master
Do all of this on the Master (10.XX.XX.80).
Confirm the server is clean
rpm -qa | grep -i webhelpdesk
ls /opt/webhelpdesk 2>&1
What you should see: nothing from the first command, and No such file or directory from the second. If Web Help Desk is already installed, remove it first with sudo bash /opt/webhelpdesk/uninstall.sh.
Confirm the database server can be reached
Do this before installing — it saves a lot of confusion later.
timeout 5 bash -c 'cat < /dev/null> /dev/tcp/10.XX.XX.235/1433' && echo "Database reachable" || echo "BLOCKED"
What you should see: Database reachable.
If you see BLOCKED, the database server is not accepting connections from this server. Fix that before going any further, or nothing below will work.
Install the package
sudo rpm -ivh /root/WebHelpDesk-<version>-linux.rpm
What you should see: two progress bars and no errors. This only copies files onto the server; nothing is configured or started yet.
Run the installer as the Master
cd /opt/webhelpdesk
sudo bash install.sh --role daemon
What you should see near the top:
Mode : FRESH
Node role : DAEMON (standalone / single daemon node)
and at the end:
[INFO] Backend : healthy (200)
[INFO] Frontend : healthy (307)
[INFO] Caddy : healthy (307)
This takes several minutes. The slowest part is "Verifying services are healthy". Let it finish — do not press Ctrl+C.
Confirm it started
systemctl is-active whd-pgsql whd-backend whd-frontend whd-caddy
curl -s http://127.0.0.1:8081/actuator/health; echo
grep -E '^WHD_HTTPS_PORT|^WHD_DOMAIN' /opt/webhelpdesk/conf/whd.env
What you should see: four lines saying active, then {"status":"UP","groups":["liveness","readiness"]}, then your address and web port. Make a note of WHD_HTTPS_PORT — you will need it later.
Step 2 — Connect the Master to the external database
Open Web Help Desk in a browser
Go to https://10.XX.XX.80 (use the port from Step 1.5 if it is not 443).
Your browser will warn you the certificate is not trusted. That is expected — the installer created a temporary certificate. Continue past the warning.
Enter your database details
The setup wizard asks for:
|
Field |
Value |
|---|---|
|
Database type |
Microsoft SQL Server |
|
Host |
10.XX.XX.235 |
|
Port |
1433 |
|
Database name |
your existing empty database |
|
Username / password |
a user who can create tables |
Let the wizard finish
The wizard tests the connection, then creates all the tables. On a new database this takes several minutes. Do not reload the page.
Confirm what was saved
sudo grep -E '^whd\.db\.(embedded|host|port|name|username|subprotocol)' /opt/webhelpdesk/conf/.whd.properties
curl -s http://127.0.0.1:8081/actuator/health; echo
What you should see:
whd.db.embedded=NO
whd.db.host=10.XX.XX.235
whd.db.name=whd
whd.db.port=1433
whd.db.subprotocol=mssql
whd.db.username=whd_user
whd.db.embedded=NO is the important line. It tells the next step that this server no longer needs its own built-in database.
Step 3 — Finish setting up the Master
Apply the Master role
sudo /opt/webhelpdesk/scripts/whd-set-node-role.sh --role daemon
What you should see:
[--] Database: external (shared server)
[OK] whd.node.daemon-enabled=true
[OK] spring.liquibase.enabled=true
[OK] whd.license.cluster-secondary=false
[OK] Removed the whd-pgsql dependency from /etc/systemd/system/whd-backend.service
[OK] Embedded PostgreSQL stopped and disabled — this node uses the shared external database
Turning off the built-in database on the Master is correct. The Master now uses the external database like every other server, so the built-in one is not needed.
Restart and check
sudo systemctl restart whd-backend
sleep 90
curl -s http://127.0.0.1:8081/actuator/health; echo
systemctl is-active whd-pgsql
grep -E 'daemon-enabled|liquibase|cluster-' /opt/webhelpdesk/conf/application.properties
What you should see: {"status":"UP"...}, then inactive for the built-in database, then the three settings above.
If it says OUT_OF_SERVICE, wait another minute and try again. Web Help Desk reports this while it is still finishing its start-up checks. Against an external SQL Server this regularly takes longer than 60 seconds. It is only a problem if it still this after about five minutes.
The Master is now ready.
Step 4 — Install each Slave
Do this on Slave 1 (10.XX.XX.81), then repeat on Slave 2 (10.XX.XX.129).
Confirm the server is clean and can reach the database
rpm -qa | grep -i webhelpdesk
timeout 5 bash -c 'cat < /dev/null> /dev/tcp/10.XX.XX.235/1433' && echo "Database reachable" || echo "BLOCKED"
Install the package
sudo rpm -ivh /root/WebHelpDesk-<version>-linux.rpm
Use exactly the same installer file as the Master. If the versions differ, the database password copied from the master below will not work.
Run the installer as a Slave
cd /opt/webhelpdesk
sudo bash install.sh --role ui --primary-url https://10.XX.XX.80
What you should see:
Node role : UI (multi-instance UI-only node)
[INFO] Applying UI-only node role before first start...
[INFO] UI-only node: services are installed but NOT started.
services This server does not yet know where the shared database is. Starting it now would create a second, unwanted database. You start it later in this process.
Copy the database settings from the Master
scp root@10.XX.XX.80:/opt/webhelpdesk/conf/.whd.properties /opt/webhelpdesk/conf/.whd.properties
sudo chown whd:whd /opt/webhelpdesk/conf/.whd.properties
sudo chmod 600 /opt/webhelpdesk/conf/.whd.properties
sudo grep -E '^whd\.db\.(embedded|host|name)' /opt/webhelpdesk/conf/.whd.properties
No changes are needed to this file. Because the database is external, the settings are already correct for every server.
You will be asked to confirm an SSH fingerprint the first time — answer yes, then enter the Master's root password. Answering no cancels the copy without an obvious error, and the Slave will then fail to start because it is still pointing at its own empty database.
Start the Slave
sudo systemctl start whd-backend whd-frontend whd-caddy
sleep 90
curl -s http://127.0.0.1:8081/actuator/health; echo
What you should see: {"status":"UP","groups":["liveness","readiness"]}.
Confirm it is behaving as a Slave
grep -E 'daemon-enabled|liquibase|cluster-' /opt/webhelpdesk/conf/application.properties
systemctl is-active whd-pgsql
sudo journalctl -u whd-backend -n 300 --no-pager | grep -icE 'EmailTicketScheduledTask|ReportScheduler'
What you should see: daemon-enabled=false, liquibase.enabled=false, cluster-secondary=true; the built-in database inactive; and the last command printing 0 — no background jobs on this server.
Now repeat Step 4 on Slave 2.
Step 5 — Confirm the finished system
From any of the three servers:
for ip in 10.XX.XX.80 10.XX.XX.81 10.XX.XX.129; do
echo -n "$ip: "; curl -sk -o /dev/null -w "%{http_code}\n" https://$ip/
done
All three should answer.
Then confirm they share data: create a ticket on one server, and check it appears on the other two.
Step 6 — Give users one web address (optional)
now users know all three server addresses. This part puts a single address in front of the two Slaves, spreads work between takes a Slave out of service automatically if it stops responding.
Skip this part if you already own a load balancer (F5, NGINX, AWS)just apply the two rules below to it instead.
Two rules you must not break
-
Users must stay on the same server for their whole visit. Web Help Desk keeps part of your sign-in in the memory of whichever server you landed on. If requests are spread across servers at random, users are logged out constantly. The settings below do this with a cookie.
-
Never put the Master in the pool. It does the background work. Only the two Slaves serve users.
Prepare the log file
The load balancer cannot create its own log file, so create it first:
sudo touch /opt/webhelpdesk/log/caddy-lb-access.log
sudo chown whd:whd /opt/webhelpdesk/log/caddy-lb-access.log
sudo chmod 640 /opt/webhelpdesk/log/caddy-lb-access.log
If you skip this the load balancer fails to start with permission denied.
Create the configuration
Run this on the Master. The load balancer uses port 9443, because the Master's own web server already uses 443.
sudo tee /opt/webhelpdesk/conf/Caddyfile.lb> /dev/null <<'EOF'
{
admin off
http_port 9080
https_port 9443
}
:9443 {
tls /opt/webhelpdesk/conf/cert.pem /opt/webhelpdesk/conf/key.pem
log {
output file /opt/webhelpdesk/log/caddy-lb-access.log {
roll_size 50MiB
roll_keep 10
}
}
reverse_proxy {
to 10.XX.XX.81:443
to 10.XX.XX.129:443
lb_policy cookie whd_lb
health_uri /actuator/health/readiness
health_interval 15s
health_timeout 5s
health_status 200
transport http {
tls
tls_insecure_skip_verify
read_timeout 300s
write_timeout 300s
}
header_up Host {hostport}
header_up X-Real-IP {remote_host}
header_up -REMOTE_USER
header_up -X-FORWARDED-USER
header_up -X-REMOTE-USER
}
}
EOF
sudo chown whd:whd /opt/webhelpdesk/conf/Caddyfile.lb
/opt/webhelpdesk/bin/caddy validate --config /opt/webhelpdesk/conf/Caddyfile.lb --adapter caddyfile
What you should see: Valid configuration.
Keep header_up Host {hostport}. The validator reports it as unnecessary, but it is required when the servers behind the load balancer use HTTPS. Without it, users who open the load balancer are redirected to one individual server's address — losing the single address, losing failover, and producing a certificate warning.
About tls_insecure_skip_verify: each server made its own certificate during installation, and the load balancer does not trust them. This lets it connect anyway, which is fine on a private network. Install the same CA-signed certificate on every server and you can remove that line.
Open the port
sudo firewall-cmd --permanent --add-port=9443/tcp && sudo firewall-cmd --reload
Start it
sudo -u whd nohup /opt/webhelpdesk/bin/caddy run \
--config /opt/webhelpdesk/conf/Caddyfile.lb --adapter caddyfile \
> /opt/webhelpdesk/log/caddy-lb.log 2>&1 &
sleep 5
sudo ss -lntp | grep 9443
What you should see: a line showing caddy listening on 9443. Note the process number shown — sudo kill <number> stops it.
Confirm it works and sends users to the right place
curl -skI https://10.XX.XX.80:9443/ | grep -iE '^location|^set-cookie'
What you should see:
location: https://10.XX.XX.80:9443/login?returnUrl=%2F
set-cookie: whd_lb=...
The address in location must be the load balancer. If it shows an individual server instead, header_up Host {hostport} is missing from Step 6.2.
Confirm work is shared between the Slaves
for i in $(seq 1 20); do
curl -sk -D - -o /dev/null https://10.XX.XX.80:9443/ \
| grep -i '^set-cookie: whd_lb' \
| sed 's/.*whd_lb=\([^;]*\).*/\1/'
done | sort | uniq -c
What you should see: two different values, one per Slave, in a roughly even split.
If you only see one, check both Slaves are ready first — a Slave that is still starting correctly receives no traffic:
curl -sk -o /dev/null -w "81=%{http_code}\n" https://10.XX.XX.81/actuator/health/readiness
curl -sk -o /dev/null -w "129=%{http_code}\n" https://10.XX.XX.129/actuator/health/readiness
Both must print 200.
Confirm a server failuredoes not take the site down
Stop Web Help Desk on one Slave:
sudo systemctl stop whd-backend # run this ON the Slave
Wait about 20 seconds, then on the Master:
curl -sk https://10.XX.XX.80:9443/api/v1/public/license/expired; echo
tail -3 /opt/webhelpdesk/log/caddy-lb.log
What you should see: data still returned, and the log noting the stopped server:
"unexpected status code","status_code":502,"host":"10.XX.XX.81:443"
Start it again on the Slave:
sudo systemctl start whd-backend
After about a minute the log shows it returning by itself:
"unexpected status code","status_code":503,"host":"10.XX.XX.81:443"
"host is up","host":"10.XX.XX.81:443"
502 means stopped, 503 means still starting, host is up means serving again. You do not need to do anything to bring a recovered server back.
Sign in through the load balancer
Open https://10.XX.XX.80:9443, sign in, and click through several pages.
Important — what happens if a server fails while people are signed in. The site stays available. Users are moved to the remaining server automatically, and they do not have to type their password again;their sign-in token is stored in the browser and is accepted by any server.
What they will see is a brief interruption: the page they were on may show the sign-in screen once. Refreshing the page returns them to where they were, without entering credentials.
Anything unsaved at that moment — a half-completed ticket, an approval being reviewed — may still be lost, so plan restarts for quiet periods.
Step 7 - Upgrade an existing external-database cluster
Use this section when .80, .81, and .129 are already running as a working cluster and you want to move them to a newer RPM.
Upgrade the daemon first, then each UI-only node one at a time.
Confirm the current role settings before you touch anything
Run this on every node:
hostname -f
grep -E 'daemon-enabled|liquibase|cluster-' /opt/webhelpdesk/conf/application.properties
systemctl is-active whd-pgsql whd-backend whd-frontend whd-caddy
What you should see:
-
daemon node: whd.node.daemon-enabled=true, spring.liquibase.enabled=true, whd.license.cluster-secondary=false
-
UI-only node: whd.node.daemon-enabled=false, spring.liquibase.enabled=false, whd.license.cluster-secondary=true
Copy the new RPM to all nodes
Use the same RPM on all three nodes:
scp /root/WebHelpDesk-<new-version>-linux.rpm root@10.XX.XX.80:/root/
scp /root/WebHelpDesk-<new-version>-linux.rpm root@10.XX.XX.81:/root/
scp /root/WebHelpDesk-<new-version>-linux.rpm root@10.XX.XX.129:/root/
Upgrade the daemon node first
Run on 10.XX.XX.80:
sudo rpm -Uvh /root/WebHelpDesk-<new-version>-linux.rpm
cd /opt/webhelpdesk
sudo bash install.sh --upgrade
sudo /opt/webhelpdesk/scripts/whd-set-node-role.sh --role daemon
sudo systemctl restart whd-backend
sleep 90
curl -s http://127.0.0.1:8081/actuator/health; echo
systemctl is-active whd-pgsql
grep -E 'daemon-enabled|liquibase|cluster-' /opt/webhelpdesk/conf/application.properties
What you should see:
-
backend health UP
-
whd-pgsql still inactive because this cluster uses a shared external database
-
whd.node.daemon-enabled=true
-
spring.liquibase.enabled=true
Upgrade UI-only node 1
Run on 10.XX.XX.81:
sudo rpm -Uvh /root/WebHelpDesk-<new-version>-linux.rpm
cd /opt/webhelpdesk
sudo bash install.sh --upgrade --role ui --primary-url https://10.XX.XX.80
test -f /opt/webhelpdesk/conf/.whd.properties || sudo scp root@10.XX.XX.80:/opt/webhelpdesk/conf/.whd.properties /opt/webhelpdesk/conf/.whd.properties
sudo chown whd:whd /opt/webhelpdesk/conf/.whd.properties
sudo chmod 600 /opt/webhelpdesk/conf/.whd.properties
sleep 90
curl -s http://127.0.0.1:8081/actuator/health; echo
grep -E 'daemon-enabled|liquibase|cluster-' /opt/webhelpdesk/conf/application.properties
systemctl is-active whd-pgsql
sudo journalctl -u whd-backend -n 300 --no-pager | grep -icE 'EmailTicketScheduledTask|ReportScheduler'
What you should see:
-
backend health UP
-
whd.node.daemon-enabled=false
-
spring.liquibase.enabled=false
-
whd.license.cluster-secondary=true
-
whd-pgsql inactive
-
the last command prints 0
Upgrade UI-only node 2
Run on 10.XX.XX.129:
sudo rpm -Uvh /root/WebHelpDesk-<new-version>-linux.rpm
cd /opt/webhelpdesk
sudo bash install.sh --upgrade --role ui --primary-url https://10.XX.XX.80
test -f /opt/webhelpdesk/conf/.whd.properties || sudo scp root@10.XX.XX.80:/opt/webhelpdesk/conf/.whd.properties /opt/webhelpdesk/conf/.whd.properties
sudo chown whd:whd /opt/webhelpdesk/conf/.whd.properties
sudo chmod 600 /opt/webhelpdesk/conf/.whd.properties
sleep 90
curl -s http://127.0.0.1:8081/actuator/health; echo
grep -E 'daemon-enabled|liquibase|cluster-' /opt/webhelpdesk/conf/application.properties
systemctl is-active whd-pgsql
sudo journalctl -u whd-backend -n 300 --no-pager | grep -icE 'EmailTicketScheduledTask|ReportScheduler'
Expected result: the same as UI-only node 1.
Confirm the whole upgraded cluster is healthy
for ip in 10.XX.XX.80 10.XX.XX.81 10.XX.XX.129; do
echo -n "$ip: "; curl -sk -o /dev/null -w "%{http_code}\n" https://$ip/
done
All three should answer.
Then confirm the daemon is still the only node doing background work:
sudo journalctl -u whd-backend -n 300 --no-pager | grep -icE 'EmailTicketScheduledTask|ReportScheduler'
ssh root@10.XX.XX.81 "sudo journalctl -u whd-backend -n 300 --no-pager | grep -icE 'EmailTicketScheduledTask|ReportScheduler'"
ssh root@10.XX.XX.129 "sudo journalctl -u whd-backend -n 300 --no-pager | grep -icE 'EmailTicketScheduledTask|ReportScheduler'"
What you should see: the daemon prints a non-zero number, and each UI-only node prints 0.
If a UI-only node loses its role after upgrade
Re-apply the UI-only role and check again:
sudo /opt/webhelpdesk/scripts/whd-set-node-role.sh --role ui --primary-url https://10.XX.XX.80
sudo systemctl restart whd-backend whd-frontend whd-caddy
sleep 90
grep -E 'daemon-enabled|liquibase|cluster-' /opt/webhelpdesk/conf/application.properties
This is safe to run again. The role script is idempotent.
Troubleshooting
|
What you see |
What it means |
What to do |
|---|---|---|
|
BLOCKED when testing the database |
The server cannot reach SQL Server |
Check firewalls and that SQL Server accepts network connections |
|
Login failed for user in the wizard |
Wrong password, or SQL Server only allows Windows logins |
Enable SQL logins, re-check the password |
|
CREATE TABLE permission denied |
The database user cannot create tables |
Give the user owner rights on that database |
|
OUT_OF_SERVICE that never becomes UP |
The server cannot use the database |
sudo journalctl -u whd-backend -n 50 --no-pager |
|
A Slave shows background jobs running |
The role was not applied |
Re-run whd-set-node-role.sh --role ui --primary-url https://10.XX.XX.80 |
|
Everything is slow to start |
Normal with an external database |
Allow up to 5 minutes |
Known limitations
-
The database is a single point of failure. If the SQL Server is unavailable, no server can serve pages. Ask your database team about making it highly available.
-
Servers must be started in order. Upgrade and start the Master first, then let it finish completely before starting any Slave. Start Slaves one at a time rather than simultaneously.
-
A server failing briefly interrupts the people using it. The site stays available and users are moved to the remaining server without having to type their password again. They may see the sign-in screen once; refreshing returns them to where they were. Unsaved work at that moment may be lost. Confirmed in testing.
-
Re-check node settings after an upgrade. Confirm the settings in "Confirm it is behaving as a slave" above are still in place on every Slave before starting the slave.