Primary, replica, and MariaDB replication · As of September 2026 (Server 1.0.31, Agent 1.0.36)

Setting up high availability

This document describes how to connect a second ZeroPortal node as a replica to an existing server, what the two nodes do in operation, how to update a cluster, and what to do on a failure. It assumes a server is already installed (see Administration Handbook, chapter 3) and goes deeper into that handbook's chapter 11.


1. What is this?

ZeroPortal has two roles. The Primary is the node whose database writes: every change in the portal, every device registration, and every licence booking lands here. A Replica keeps a continuously updated copy of this database and delivers packages and policies to clients from it. Every freshly installed node starts out as a standalone Primary; a Replica comes into being when a second node joins the first with an invitation token.

The copy comes from MariaDB's binlog replication: the Primary writes every change to its binary log, the Replica reads that log and applies it to its own database. Three things deliberately stay per node and are not replicated: node configuration (role, node ID, maintenance mode, LDAP settings), the list of registered nodes, and the App-V reporting tables. Only the Primary therefore accepts App-V reports; a Replica forwards them there.

A Replica is fully usable day to day. Reads always come from the local copy. The node quietly forwards writes to the Primary; there is no read-only mode and no persistent notice. Only when the Primary is unreachable does the yellow bar No Primary reachable appear on every page.

The Primary checks every registered node every 10 seconds through its health endpoint. Each Replica reports to the Primary by heartbeat every 5 minutes; if a heartbeat fails, it retries every minute. You see both under Global Configuration → High Availability.


2. What is this good for?

What a replica does not speed up is writes: they always end up at the Primary, the Replica only forwards them. If the Primary is missing, this mainly affects you as the administrator: changes in the portal, newly added devices, and licence bookings wait until it is back or a replica has been promoted. Users notice nothing, their sync keeps running. The one exception is App-V reporting: client reports are writes too, and wait along with the rest. There is no automatic role switch.

A single node, by the way, also carries large fleets; sizing guidance is in the Administration Handbook, chapter 2. Load balancing is a welcome side effect of high availability, not a reason on its own for a second node.


3. The moves in short

Everything runs through Global Configuration → High Availability on whichever node is named. No SQL, no command line. The detailed paths are in the sections that follow.

Adding a second node

  1. On the Primary: Generate new token, copy the token.
  2. On the new node: Set up as replica…, enter the Primary's URL and the token, Join as replica.
  3. On the new node: Start wizard…. It sets up database replication on both sides and fetches the existing data at the end.

The Primary has failed

Don't rush. Clients keep being served by the replicas, packages and policies arrive unchanged. Only changes in the portal, new devices, reports, and licence bookings wait. If the Primary comes back soon, the replicas catch up the gap on their own.

Making a replica the Primary

If it doesn't come back, promote a replica. Two clicks, a bit over a minute:

  1. On the replica: Reset / promote to PrimaryPromote to Primary…, confirm. The data is already there, nothing gets copied.
  2. Reconnect the remaining replicas: promoting clears the node list. On the new Primary, generate a token and let each replica join as above.

You do not need to touch the agents, as long as the new Primary's address is in the group policy's server list. They switched to a reachable node long ago.

Bringing the old Primary back

It returns as a replica: generate a token on the new Primary, on the old node Set up as replica…, run the wizard. Its stale database gets overwritten in the process.

Replication is stuck or has broken off

On the replica: Advanced / manual setup, Replica tab, Full Sync from Primary…. This is the standard fix for any discrepancy, even after a longer outage.

Updating nodes

Primary first: install the MSI, then run the database update on the same node. Only after that the replicas, one by one, each in Maintenance mode first. Reason: schema changes happen on the Primary and reach the replicas through replication; a replica updated first simply keeps running on the old schema until then.


4. Setting it up, step by step

All steps happen in the portal under Global Configuration → High Availability; you need level Manage in the High Availability area, or a full administrator account (see Users, permissions, licences and logs).

4.1 Prerequisites

Replication password and MariaDB paths in the configuration tool, step 2

4.2 Generate an invitation token on the Primary

On the Primary: Global Configuration → High AvailabilityInvitation token section → Generate new tokenCopy. Note the Primary's base URL with port while you're there, e.g. https://portal1.example.com:8080.

The token is valid for 24 hours and for exactly one join; a new token invalidates an existing one.

4.3 Let the second node join

On the future replica node, open the same page (it still shows as Primary):

  1. Set up as replica… → enter the Primary's base URLTest. The response names the Primary's role and database name; if the test fails, the URL, port, certificate, or firewall is wrong.
  2. Paste the invitation token. This node's base URL is pre-filled; that is the address the Primary reaches this node at.
  3. Join as replica. Under This node, the role now reads Replica; on the Primary, the node appears under Registered nodes with Replication: Not started. The database is only coupled by the next step.

4.4 Run the HA setup wizard

The wizard sets up MariaDB replication on both nodes (my.ini, firewall, database service restart, coupling). You start it on the replica:

  1. In the Replication wizard section, click Start wizard…. The HA Setup Wizard window shows the planned steps and four fields:
Field Meaning
Primary DB host Host or IP of the Primary's MariaDB (pre-filled from the base URL)
Port MariaDB port of the Primary, default 3306
Primary ID server-id of the Primary; convention 1. Only change it if the Primary was recently promoted and its my.ini still carries a different ID
Server ID of this node unique value ≥ 2, suggested from the node ID; must differ from the Primary ID
  1. Confirm Start wizard. The wizard shows each of its steps with its result. If something goes wrong, it rolls the my.ini back on both nodes and stops; if only a firewall step fails, it carries on.
  2. Close the window. If the replica had no package data yet, the full sync starts right away instead (section 4.5).

While the wizard runs, the Primary is without a database for a few seconds (MariaDB restart). Plan the setup for a quiet time.

4.5 Full sync: copying existing data to the replica

Replication only carries changes from the binlog position it started at. Full sync fetches a complete dump from the Primary, imports it locally, and starts replication from exactly the position the dump belongs to.

The whole local database content is replaced, the portal on this node is briefly unreachable, and the process cannot be undone. Node configuration, the node list, and the App-V reporting tables are excluded from the dump; the replica keeps its role and node ID.

A full sync is needed again later if a replica has been disconnected from the Primary for more than seven days, was restored from a snapshot, or its SQL thread has stopped over conflicting data (section 6.4).

4.6 Check the result


5. Rules: when does what apply

5.1 What gets written where, and when

What Rule
Reading packages, policies, assignments (agent sync) locally on every node
Changes in the portal, enrollment, licence booking, audit, App-V reports Replica forwards to the Primary; fails after 5 s, then a 30 s fast-fail, then retries
Role, node ID, maintenance mode, LDAP settings per node, not replicated
List of registered nodes Primary is authoritative; a replica's page asks the Primary live
Primary checks every registered node every 10 s; the Registered nodes display too
Replica heartbeat every 5 min, every minute after a failure; overdue after interval + 1 min
Invitation token 24 h, one-time
IO thread "Connecting" after a Primary restart tolerated up to 10 min, then the Database replication stopped alarm fires
Binary log on the Primary 7 days; a replica missing longer needs a full sync

Leave the Write routing switch under This node on a replica set to Active. Disabled silently drops every forwarded write; it is a diagnostic switch only.

5.2 Status indicators under Registered nodes

The dot sums up the state (maintenance over reachability over replication), the tooltip names the cause: Blue maintenance mode; Green reachable, fresh heartbeat, lag under 60 s; Yellow temporary (node not responding but heartbeat fresh; heartbeat overdue; lag 60–299 s); Red action needed (unreachable and heartbeat overdue; SQL thread stopped; lag 300 s or more; on a replica: Primary unreachable); Grey not checked yet.

Replication column: OK: <date> or <n> s: <date> = both threads running, n seconds behind; IO stopped → sections 6.3 and 6.4; SQL stopped → end maintenance or section 6.4; Not started → HA setup wizard (on the replica, the text is a link). Only a stopped SQL thread turns the dot red. After a Primary restart, MariaDB reports the IO thread as "Connecting" and reconnects on its own; only a real error (invalid binlog position 1236, wrong replication password, TLS) stops it.

5.3 Maintenance mode

Maintenance mode pauses the SQL thread on a replica: changes from the Primary keep arriving but are not applied until maintenance ends. The dot turns blue, the node stays reachable for clients. Meant for work on the replica where the database should not be running (updating program files, maintaining MariaDB).

5.4 If the Primary fails

Replica without a Primary
Portal on the replica answers normally; tasks that need the Primary retry in the background
Delivery (MSIX, App-V, policies) keeps running, reading is local
Administration, enrollment, reports, licence booking wait; every write fails after 5 s, then fast-fails for 30 s, then retries
Visible as No Primary reachable bar on every page (with a timestamp); /api/health/status returns primaryReachable: false; email Primary not reachable (sent by the replica)
Detected at the next heartbeat, or right away on the first forwarded write; heartbeat every minute after that
Primary returns on its own; email Primary reachable again and an audit entry Cluster.PrimaryRecovered with the outage window
Primary does not return promote a replica, section 6.2

The Primary cannot report its own failure; this one message is sent by the replica, every other cluster message comes from the Primary.

Limits: exactly one Primary, any number of replicas, each with its own server ID; no automatic failover — promotion is a deliberate step by the administrator; more than 7 days disconnected, or a snapshot rollback of the replica, needs a full sync.

5.5 Removing a node

Either way, MariaDB replication keeps running. If the node should not rejoin the cluster, afterwards run Advanced / manual setupReplica tab → Reset replica (RESET REPLICA ALL) on it, or run the promotion wizard (section 6.2), which also sets server-id to 1.


6. Operations and troubleshooting

6.1 Updating the cluster

Schema changes happen on the Primary and reach the replicas through replication; ZDBInstallUpdate still runs on every node, because that is where it syncs the node-local tables.

  1. Primary: install the MSI, then run the database update: "C:\Program Files\ZeroPortal\ZDBInstallUpdate.exe" "C:\Program Files\ZeroPortal\appsettings.json" --admin-user root --admin-password <password> Check or start the ZeroPortal service. The replicas keep running on the old state.
  2. Replicas, one after another: install the MSI, run ZDBInstallUpdate with the same command line, check the service. Expected message there: Schema migration skipped on this replica (deferred to the Primary) and LOCAL_TABLES: ….
  3. Check: the Primary's HA page, every node green, Replication column OK; the version in each node's footer.

The configuration tool does the same in step 4 - Database Install / Update. A replica accidentally updated first reports REPLICA_SCHEMA_DEFERRED=1 (exit code 19) and keeps running on the old schema until the Primary catches up; replication stays intact. For a MariaDB update, the order reverses (replicas first, Primary last; Administration Handbook, chapter 12).

6.2 Primary lost: promoting a replica

The click path is in section 3. Three things that don't fit there:

A DNS alias or load balancer pointing at the old Primary needs updating. The agents need no change, as long as both addresses are in the Publishing servers policy.

6.3 After a hard failure of the Primary

If the Primary was switched off without a clean shutdown, the replicas afterwards sometimes show IO stopped with `Got fatal error 1236 … start replication from position

file size`: the replica remembers a binlog position that no longer exists after the restart. Resume all does not help here; the fix is Full sync from Primary… on the affected replica. To prevent it: shut the Primary down cleanly, or at least stop the MariaDB service first.

6.4 Restarting or resetting replication

Situation Fix
SQL stopped after maintenance End maintenance mode (Off or Resume all)
SQL stopped or Stopped for no clear reason Resume all on the Primary, or START REPLICA; on the replica
Last error Duplicate entry, Can't find record, doesn't exist The replica has conflicting data (the Replica tab shows A full sync may be needed): Full sync from Primary…
IO stopped with Access denied Replication password does not match between nodes: set it the same on both nodes in configuration-tool step 2, run step 4 on the Primary, restart the HA setup wizard
IO stopped with error 1236 Section 6.3
Reset replication completely Replica tab → Reset replica (RESET REPLICA ALL), then the HA setup wizard

6.5 Backing up my.ini

The wizard and promotion write to MariaDB's my.ini (in the data folder next to bin, or my.cnf) and first save my.ini.zeroportal.bak next to it; every further run overwrites that copy. Back up the state before HA setup yourself beforehand. Restore: Advanced / manual setupmy.cnf tab → Restore backup, then Restart on this node (MariaDB service).

6.6 Logs and messages

Portal log: C:\ProgramData\NickIT\ZPServer\Logs\ZeroPortalLogs.log (older installs: C:\Windows\Temp\ZeroPortalLogs); the Warning level shows only transitions and errors, detail only at Information (configuration tool, step 2, Log level; turn it back down afterwards). Cluster lines start with NodeReachability:, PrimaryLink:, Heartbeat:, HA:, SetupWizard:, and PromoteWizard:. Set up email notifications (appendix 7.4) under Global Configuration → Configuration → Email notifications; every one also lands in the Audit log as Alert.Raised. For monitoring without signing in: appendix 7.5.

6.7 Common errors

Symptom Cause Fix
Test on joining fails; yellow dot with SSL certificate validation failed URL, port, portal port blocked by a firewall, or an untrusted certificate Check the URL and port; set SslSettings:IgnoreSslErrors in appsettings.json to true (default), or use a trusted certificate
Joining: Database name mismatch Database name differs between the nodes Create a database with the same name on the replica (configuration tool, steps 2 and 4)
Joining: No valid invitation token / Invalid invitation token Token expired (24 h), already used, or copied wrong Generate new token on the Primary
Joining: Cannot join itself The Primary URL points at this node Enter the other server's URL
Wizard: my.cnf not found; server_id stays old after promotion Bin Path missing in appsettings.json, or MariaDB was moved Configuration tool, step 2, Auto-detect, save, restart the service; if needed, set server-id = 1 by hand and restart MariaDB
Wizard: timeout at Wait for Primary MariaDB; IO running: No with error connecting Port 3306 to the Primary is blocked, or bind-address = 127.0.0.1 Test-NetConnection <primary> -Port 3306; check the my.cnf tab on the Primary; check the DB host in the wizard
Wizard: Server-ID conflict Both nodes carry the same server-id Pick a different Server ID of this node in the wizard
Wizard: Could not fetch binlog position log_bin is off on the Primary, usually because MariaDB was not restarted after writing my.ini my.cnf tab → Restart on Primary
IO running: No with Access denied; SQL stopped with Duplicate entry; error 1236 see section 6.4 Section 6.4
Yellow dot, tooltip Heartbeat overdue The replica hasn't checked in for longer than interval + 1 min Check the replica's service and network; it clears within a minute after a Primary restart
Service won't start on a replica after the update, log shows Unknown column ZDBInstallUpdate was not run on the replica Section 6.1
Manual: Get binlog position from Primary reports Access denied The button uses the local app credentials; on the Primary the password differs, or the user lacks BINLOG MONITOR Use the HA setup wizard (it uses zeroportal_repl), or run configuration-tool step 4 on the Primary. The error text names zeroportal_app; that means the app user from the connection string, zeroportal_dbuser by default
No Primary reachable bar on the replica Primary down, network, or portal port Section 5.4; if the Primary stays away: section 6.2

7. Appendix: technical reference

7.1 Entries in appsettings.json

Key Meaning
ReplicationSettings:ReplPassword Password of the replication user zeroportal_repl; same on every node (configuration tool, step 2, Repl. Password)
MySqlSettings:BinPath Folder with mysql.exe/mysqldump.exe; my.ini is looked for relative to it (step 2, Bin Path)
MySqlSettings:DataPath MariaDB's data directory (step 2, Data Path)
SslSettings:IgnoreSslErrors true (default): nodes accept each other's self-signed certificates
Logging:Console:LogLevel:System.Net.Http.HttpClient.HaClient Warning: no line per heartbeat in the log; add this on older installs if the log grows from Start processing HTTP request

Search order for my.ini, starting from Bin Path's parent folder: data\my.cnf, data\my.ini, my.cnf, my.ini.

7.2 Firewall and service

Rule name MariaDB HA Replication (ZeroPortal) :3306, created only if missing:

New-NetFirewallRule -DisplayName "MariaDB HA Replication (ZeroPortal) :3306" -Direction Inbound -Protocol TCP -LocalPort 3306 -Action Allow -Profile Domain,Private

Service restart: service names MariaDB, MySQL, mariadbd in that order, Restart-Service -Force.

7.3 ZDBInstallUpdate

"C:\Program Files\ZeroPortal\ZDBInstallUpdate.exe" "<path>\appsettings.json" --admin-user <user> --admin-password <pw> [--create-app-user] [--repl-password <pw>] [--check] [--force-migrate]
Output / exit code Meaning
NODE_ROLE=Primary / Proxy Detected role of the node
REPLICA_SCHEMA_DEFERRED=1, exit 19 Replica: pending schema changes come from the Primary; only node-local tables were synced
LOCAL_TABLES: … Sync of the node-local tables
Exit 0 Succeeded, or up to date with --check
Exit 13 (only --check) Schema changes or a sync are pending
Exit 18 Schema updated, but one node-local table could not be synced
--force-migrate Apply schema changes locally despite the replica role (only if the Primary is lost); a full sync is needed afterwards

7.4 Messages and audit

Message Sender Trigger
Cluster node unreachable / Cluster node reachable again Primary A node's health endpoint stops or resumes answering
Database replication stopped Primary A replica's IO or SQL thread stopped, or the IO thread has been "Connecting" for more than 10 min
Primary not reachable (sent by the replica) / Primary reachable again (sent by the replica) Replica A heartbeat or a forwarded write fails, or succeeds again
Failover: this node became Primary Promoted node Promoted to Primary

Audit entries: Alert.Raised for every message sent, Cluster.PrimaryRecovered with the outage window.

7.5 PowerShell module and API

Cmdlet Purpose
Get-ZeroPortalHealth [-TargetUrl <url>] Role, node ID, primaryReachable of a node
Get-ZeroPortalReplicationStatus [-TargetUrl <url>] IO/SQL state, lag, last error of a replica
Get-ZeroPortalClusterNode [-Name <pattern>] Node list of the connected node
Remove-ZeroPortalClusterNode -NodeId <guid> or -Name <name> Remove an entry from the node list (same as the bin icon)

Aliases: Get-ZPClusterNode, Remove-ZPClusterNode. Set the base URL first with Set-AppvZeroPortalCmdLetConfiguration.

Route Authentication Content
GET /api/health/status none Role, node ID, primaryReachable (replica only)
GET /api/health/replication none ioRunning, ioConnecting, sqlRunning, lagSeconds, lastError, lastIoError