Add an HA controller
For production, run multiple controllers with Raft consensus. Three-node clusters are the common production minimum, with 2-of-3 quorum surviving a single host loss.
After your single-controller deployment is healthy, follow the steps below for each additional controller you want to add.
Additional requirements for adding an HA controller:
- The new controller host needs
dockeranddocker compose(same as the bootstrap host) plus7zip(p7zip-fullon Debian/Ubuntu,p7zipon RHEL/Fedora) to extract the encrypted join package. - The new host does not need the
kzitibinary. The join package is self-contained and includes the installer. - You will be transferring two things to the new host: the encrypted join package, and the extraction password. These should travel through different channels.
- The new controller needs its own DNS A record (e.g.
ziti2.example.com) and its router needs one too (e.g.ziti2-router.example.com), with ports1280/tcp,6262/tcp, and3022/tcpopen inbound.
On the existing controller
Generate a join package for the new controller:
kziti deploy ha create-join-package \
--output /tmp/join.zip \
--node-name ziti-c-2 \
--controller-host ziti2.example.com \
--router-host ziti2-router.example.com \
--admin-password '<your-admin-password>'
Notes on the flags:
--node-nameis the cluster member name. It must be unique within the cluster.--controller-hostand--router-hostare the FQDNs of the new controller and its router — not the existing one.- Add
--controller-ip <new-controller-ip>if the new controller's hostname does not resolve from inside the existing controller container. This injects anextra_hostsentry into a docker-compose override on the existing host so it can reach the new node by name during the join.
kziti produces an AES-encrypted zip at the path you supplied with --output, and prints a randomly generated extraction password.
Transfer the zip to the new host:
scp /tmp/join.zip user@ziti2.example.com:/tmp/
Send the extraction password through a separate secure channel — a password manager, an encrypted message, or similar. Do not put the password in the same email or the same chat as the zip.
On the new controller host
Install 7zip if it is not already present:
# Debian / Ubuntu
apt-get install -y p7zip-full
# RHEL / Fedora
dnf install -y p7zip
Extract the encrypted package and run the bundled installer:
7z x -p<extraction-password> /tmp/join.zip -o/tmp/kziti-join
bash /tmp/kziti-join/install.sh
If the bootstrap controller's hostname does not resolve from this new host, add --existing-controller-ip <bootstrap-ip> to the install.sh invocation so the new node can reach the bootstrap controller during join.
The new controller starts as a non-voter, syncs Raft state from the existing cluster, and self-promotes to a voting member once it is caught up.
Verify cluster health
On any controller host, list cluster members:
docker compose -f /opt/kziti/docker-compose.yml exec ziti-controller \
ziti agent cluster list
The new node should appear with voter: true once promotion completes (typically within a minute of join).