Documentation forWeb Help Desk

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

  1. 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.

  2. 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.