WHD multi-instance install on Windows servers with an external database
Multiple Windows servers with one shared external database.
What you are building
This guide sets up WHD across three Windows servers that all share one external database. Microsoft SQL Server is used in the examples below.
|
Server |
Address |
Job |
|---|---|---|
|
Daemon |
10.XX.XX.80 |
Runs background work and owns schema migration |
|
UI node 1 |
10.XX.XX.81 |
Serves web pages and API traffic only |
|
UI node 2 |
10.XX.XX.129 |
Serves web pages and API traffic only |
|
Database |
10.XX.XX.235 |
Shared external SQL Server |
Replace these addresses with your own throughout.
The one rule that matters most
Only one server may be the daemon node. If more than one server runs daemon work, you get duplicate background jobs, duplicate outbound email, and upgrade-time migration contention.
UI-only nodes must keep all of these settings:
whd.node.daemon-enabled=false
spring.liquibase.enabled=false
whd.license.cluster-secondary=true
whd.license.cluster-primary-url=https://10.XX.XX.80
Before you begin
You need:
-
three Windows servers that can reach each other and the database server;
-
the same WHD installer on all three nodes;
-
a shared external database already created;
-
a database user that can create tables in that database;
-
Administrator access on all three Windows servers;
-
access to copy conf\.whd.properties from the daemon node to the UI-only nodes.
A note about installer behavior
Different validated Windows builds have shown two startup patterns:
-
the installer finishes and tells you to run whd_start.bat, or
-
the installer auto-starts the services immediately.
The steps in this guide are written so they work in either case:
-
let the daemon node reach the setup wizard and complete it;
-
on UI-only nodes, stop the services before the node is allowed to stay on the shared database;
-
then apply the UI role, copy conf\.whd.properties, and start the node in the correct role.
Step 1 - Install the daemon node
Do all of this on the daemon node (10.XX.XX.80).
Confirm the server is clean
Open an elevated PowerShell session and run:
Get-Service WHD-PostgreSQL, WHD-Backend, WHD-Frontend, WHD-Caddy -ErrorAction SilentlyContinue
Test-Path 'C:\Program Files\WebHelpDesk'
What you should see:
-
no WHD services yet, or only stale services you intend to replace;
-
False from Test-Path on a clean machine.
If WHD is already installed, remove it first from Apps and Features or by using the existing uninstaller before continuing.
Install the package
Start-Process -Wait -FilePath 'C:\Share\WebHelpDesk-<version>-x64.exe' -Verb RunAs
Follow the installer prompts.
Confirm the services exist
Get-Service WHD-PostgreSQL, WHD-Backend, WHD-Frontend, WHD-Caddy
If the installer did not start the application automatically, start it now:
& 'C:\Program Files\WebHelpDesk\whd_start.bat'
Confirm the daemon node is reachable locally
Get-Service WHD-PostgreSQL, WHD-Backend, WHD-Frontend, WHD-Caddy
Invoke-WebRequest -UseBasicParsing -Uri 'http://127.0.0.1:8081/actuator/health'
Get-Content 'C:\Program Files\WebHelpDesk\conf\whd.env' | Select-String '^(WHD_HTTPS_PORT|WHD_DOMAIN)='
What you should see: all services present, backend health responding, and the current local hostname / port configuration.
Step 2 - Connect the daemon node to the external database
Open WHD in a browser
Browse to https://10.XX.XX.80 or https://10.XX.XX.80:<port> if your local WHD HTTPS port is not 443.
Enter the external database details
Use your real values. Example:
|
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 |
Confirm what the wizard saved
Get-Content 'C:\Program Files\WebHelpDesk\conf\.whd.properties' |
Select-String '^whd\.db\.(embedded|host|port|name|username|subprotocol)='
Invoke-WebRequest -UseBasicParsing -Uri 'http://127.0.0.1:8081/actuator/health'
What matters most is:
whd.db.embedded=NO
That tells the next step this node is using the shared external database and should not depend on the local embedded PostgreSQL service.
Step 3 - Finish setting up the daemon node
Apply the daemon role
powershell.exe -ExecutionPolicy Bypass -File "C:\Program Files\WebHelpDesk\scripts\whd-set-node-role.ps1" -Role daemon
What you should see in the script output:
-
Database: external (shared server)
-
whd.node.daemon-enabled=true
-
spring.liquibase.enabled=true
-
whd.license.cluster-secondary=false
-
embedded PostgreSQL stopped and disabled
Restart and verify
Restart-Service WHD-Backend -Force
Start-Service WHD-Frontend, WHD-Caddy
Start-Sleep -Seconds 90
Invoke-WebRequest -UseBasicParsing -Uri 'http://127.0.0.1:8081/actuator/health'
Get-Service WHD-PostgreSQL
Get-Content 'C:\Program Files\WebHelpDesk\conf\application.properties' |
Select-String '^(whd\.node\.daemon-enabled|spring\.liquibase\.enabled|whd\.license\.cluster-)='
What you should see:
-
backend health returns UP;
-
WHD-PostgreSQL is Stopped and typically Disabled;
-
the daemon-related properties are present.
Step 4 - Install each UI-only node
Do this on UI node 1 (10.XX.XX.81), then repeat on UI node 2 (10.XX.XX.129).
Install the package
Start-Process -Wait -FilePath 'C:\Share\WebHelpDesk-<version>-x64.exe' -Verb RunAs
Stop the WHD services before keeping the node in the cluster
Run this immediately after install if the services auto-started. If they are already stopped, the command is still safe:
Stop-Service WHD-Caddy, WHD-Frontend, WHD-Backend, WHD-PostgreSQL -Force -ErrorAction SilentlyContinue
Get-Service WHD-PostgreSQL, WHD-Backend, WHD-Frontend, WHD-Caddy
Apply the UI-only role
powershell.exe -ExecutionPolicy Bypass -File "C:\Program Files\WebHelpDesk\scripts\whd-set-node-role.ps1" -Role ui -PrimaryUrl https://10.XX.XX.80
Copy the shared database configuration from the daemon node
If the daemon node is accessible through the default admin share:
Copy-Item -LiteralPath '\\10.XX.XX.80\c$\Program Files\WebHelpDesk\conf\.whd.properties' `
-Destination 'C:\Program Files\WebHelpDesk\conf\.whd.properties' -Force
Get-Content 'C:\Program Files\WebHelpDesk\conf\.whd.properties' |
Select-String '^whd\.db\.(embedded|host|port|name|username|subprotocol)='
If you cannot use the admin share, copy the same file by your normal secure file-transfer method.
4.5 - Confirm the UI-only properties are correct
Get-Content 'C:\Program Files\WebHelpDesk\conf\application.properties' |
Select-String '^(whd\.node\.daemon-enabled|spring\.liquibase\.enabled|whd\.license\.cluster-)='
Get-Service WHD-PostgreSQL
What you should see:
-
whd.node.daemon-enabled=false
-
spring.liquibase.enabled=false
-
whd.license.cluster-secondary=true
-
WHD-PostgreSQL stopped / disabled
Start the UI-only node
Start-Service WHD-Backend
Start-Service WHD-Frontend, WHD-Caddy
Start-Sleep -Seconds 90
Invoke-WebRequest -UseBasicParsing -Uri 'http://127.0.0.1:8081/actuator/health'
Repeat Step 4 on the second UI-only node after the first one is healthy.
Step 5 - Check the finished system
Run this from any machine that can reach the three nodes:
$nodes = '10.XX.XX.80','10.XX.XX.81','10.XX.XX.129'
foreach ($node in $nodes) {
$response = Invoke-WebRequest -SkipCertificateCheck -UseBasicParsing -Uri ("https://{0}/" -f $node)
"{0}: {1}" -f $node, $response.StatusCode
}
All three should answer.
Then confirm they share the same data by creating a ticket on one node and reading it from the others.
Step 6 - Give users one web address (optional)
You may use any load balancer that supports:
-
sticky sessions when the login flow requires them;
-
keeping the daemon node out of the user-facing pool; and
-
preserving the client-facing host and port when forwarding traffic.
The shipped cluster template is:
-
C:\Program Files\WebHelpDesk\conf\Caddyfile.cluster-lb
Step 7 - Upgrade an existing external-database cluster
Use this section when all nodes are already part of a working Windows multi-instance cluster and you want to move them to a newer installer build.
Upgrade the daemon first, then each UI-only node one at a time.
Record the current role settings before you change anything
Run this on every node:
hostname
Get-Content 'C:\Program Files\WebHelpDesk\conf\application.properties' |
Select-String '^(whd\.node\.daemon-enabled|spring\.liquibase\.enabled|whd\.license\.cluster-)='
Get-Content 'C:\Program Files\WebHelpDesk\conf\.whd.properties' |
Select-String '^whd\.db\.(embedded|host|port|name|username|subprotocol)='
Get-Service WHD-PostgreSQL, 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 nodes: whd.node.daemon-enabled=false, spring.liquibase.enabled=false, whd.license.cluster-secondary=true
Place the new installer on each node
Example:
Copy-Item 'C:\Share\WebHelpDesk-<new-version>-x64.exe' '\\10.XX.XX.80\c$\Temp\WebHelpDesk-<new-version>-x64.exe'
Copy-Item 'C:\Share\WebHelpDesk-<new-version>-x64.exe' '\\10.XX.XX.81\c$\Temp\WebHelpDesk-<new-version>-x64.exe'
Copy-Item 'C:\Share\WebHelpDesk-<new-version>-x64.exe' '\\10.XX.XX.129\c$\Temp\WebHelpDesk-<new-version>-x64.exe'
Upgrade the daemon node first
Run on 10.XX.XX.80:
Start-Process -Wait -FilePath 'C:\Temp\WebHelpDesk-<new-version>-x64.exe' -Verb RunAs
powershell.exe -ExecutionPolicy Bypass -File "C:\Program Files\WebHelpDesk\scripts\whd-set-node-role.ps1" -Role daemon
Get-Service WHD-Backend, WHD-Frontend, WHD-Caddy | Where-Object Status -ne 'Running' | Start-Service
Restart-Service WHD-Backend -Force
Start-Service WHD-Frontend, WHD-Caddy -ErrorAction SilentlyContinue
Start-Sleep -Seconds 90
Invoke-WebRequest -UseBasicParsing -Uri 'http://127.0.0.1:8081/actuator/health'
Get-Service WHD-PostgreSQL
Get-Content 'C:\Program Files\WebHelpDesk\conf\application.properties' |
Select-String '^(whd\.node\.daemon-enabled|spring\.liquibase\.enabled|whd\.license\.cluster-)='
Upgrade UI-only node 1
Run on 10.XX.XX.81:
Start-Process -Wait -FilePath 'C:\Temp\WebHelpDesk-<new-version>-x64.exe' -Verb RunAs
Stop-Service WHD-Caddy, WHD-Frontend, WHD-Backend, WHD-PostgreSQL -Force -ErrorAction SilentlyContinue
powershell.exe -ExecutionPolicy Bypass -File "C:\Program Files\WebHelpDesk\scripts\whd-set-node-role.ps1" -Role ui -PrimaryUrl https://10.XX.XX.80
if (-not (Test-Path 'C:\Program Files\WebHelpDesk\conf\.whd.properties')) {
Copy-Item -LiteralPath '\\10.XX.XX.80\c$\Program Files\WebHelpDesk\conf\.whd.properties' `
-Destination 'C:\Program Files\WebHelpDesk\conf\.whd.properties' -Force
}
Start-Service WHD-Backend
Start-Service WHD-Frontend, WHD-Caddy
Start-Sleep -Seconds 90
Invoke-WebRequest -UseBasicParsing -Uri 'http://127.0.0.1:8081/actuator/health'
Get-Content 'C:\Program Files\WebHelpDesk\conf\application.properties' |
Select-String '^(whd\.node\.daemon-enabled|spring\.liquibase\.enabled|whd\.license\.cluster-)='
Get-Service WHD-PostgreSQL
Before you continue to the second UI-only node, confirm this upgraded node still shows:
-
whd.node.daemon-enabled=false
-
spring.liquibase.enabled=false
-
WHD-PostgreSQL stopped / disabled
Upgrade UI-only node 2
Run on 10.XX.XX.129 and repeat the same commands from Upgrade UI-only node 1.
Confirm the whole upgraded cluster is healthy
$nodes = '10.XX.XX.80','10.XX.XX.81','10.XX.XX.129'
foreach ($node in $nodes) {
$response = Invoke-WebRequest -SkipCertificateCheck -UseBasicParsing -Uri ("https://{0}/" -f $node)
"{0}: {1}" -f $node, $response.StatusCode
}
Then confirm each node still points at the same shared external database:
Get-Content 'C:\Program Files\WebHelpDesk\conf\.whd.properties' |
Select-String '^whd\.db\.(embedded|host|port|name|username|subprotocol)='
If something goes wrong
|
What you see |
What it means |
What to do |
|---|---|---|
|
The setup wizard appears again after upgrade |
conf\.whd.properties was not preserved or not copied back |
Restore or re-copy conf\.whd.properties, then restart services |
|
A UI-only node starts WHD-PostgreSQL |
The node is not using the cluster service layout |
Re-run whd-set-node-role.ps1 -Role ui -PrimaryUrl https://10.XX.XX.80 |
|
A UI-only node comes back with daemon-enabled=true |
The role was not re-applied after upgrade |
Re-run the role script, then restart backend/frontend/Caddy |
|
The backend does not start after upgrade on an external-DB node |
Service dependencies were restored to the standalone layout |
Re-run whd-set-node-role.ps1 with the correct role so WHD-Backend no longer depends on WHD-PostgreSQL |
|
The load balancer redirects users to an individual node |
The load balancer is not preserving the client-facing host and port |
Follow WHD_MULTI_INSTANCE_CADDY_SETUP.md or apply the same rule in your own load balancer |
Known limitations
-
Upgrade the daemon first, then the UI-only nodes one at a time.
-
Re-apply the node role after every cluster upgrade.
-
On builds that do not include the UI-node Liquibase fix, verify UI-only nodes still keep spring.liquibase.enabled=false before returning them to service.
-
The daemon node must stay out of the user-facing load-balancer pool.