🛡️ Building HA Zabbix with PostgreSQL + Patroni (Docker Compose)
Keep monitoring online 24/7. Stop single-point failures. This setup gives you Zabbix in high availability with a self-healing PostgreSQL cluster (Patroni) and automatic failover behind HAProxy.
💡 Why Patroni for SMBs
• Automatic failover: detects node failure and promotes a replica in seconds
• Zero manual switchover: maintenance without long outages
• Data integrity: synchronous replication option reduces data loss
• Simple scaling: add a replica, get read capacity and faster backups
• Fits tight budgets: runs in containers on existing hosts
📦 Project Layout
project/
├─ .env
├─ docker-compose.yml
├─ haproxy/
│ └─ haproxy.cfg
└─ patroni/
├─ patroni-node1.yml
└─ patroni-node2.yml
🔐 .env (example)
# App
APP_NAME=zbx-ha
TZ=Europe/Warsaw
# Zabbix
ZBX_DB_USER=zabbix
ZBX_DB_PASSWORD=StrongZbxPass!
ZBX_SERVER_NAME=Zabbix HA
# PostgreSQL superuser (for cluster bootstrap only)
PG_SUPERUSER=postgres
PG_SUPERPASS=StrongPostgresPass!
PG_REPL_USER=replicator
PG_REPL_PASS=StrongReplPass!
# HAProxy
HAPROXY_PORT=5432
# etcd (demo single node; use 3 in production)
ETCD_NAME=etcd1
ETCD_CLIENT_PORT=2379
ETCD_PEER_PORT=2380
🧠 Patroni node config (patroni/patroni-node1.yml)
Duplicate for node2 and change
name,connect_address, andlistenports/IPs.
scope: zbx-pg
name: patroni-1
restapi:
listen: 0.0.0.0:8008
connect_address: patroni-1:8008
etcd:
host: etcd:2379
bootstrap:
dcs:
postgresql:
parameters:
shared_buffers: "256MB"
max_connections: 250
wal_level: replica
hot_standby: "on"
use_pg_rewind: true
use_slots: true
synchronous_mode: false
initdb:
- encoding: UTF8
- data-checksums
users:
{{ env['PG_SUPERUSER'] }}:
password: {{ env['PG_SUPERPASS'] }}
options:
- createrole
- createdb
postgresql:
listen: 0.0.0.0:5432
connect_address: patroni-1:5432
authentication:
superuser:
username: {{ env['PG_SUPERUSER'] }}
password: {{ env['PG_SUPERPASS'] }}
replication:
username: {{ env['PG_REPL_USER'] }}
password: {{ env['PG_REPL_PASS'] }}
parameters:
wal_keep_size: "256MB"
tags:
nofailover: false
noloadbalance: false
clonefrom: false
Create patroni-node2.yml with name: patroni-2, connect_address: patroni-2:8008, and postgresql.connect_address: patroni-2:5432.
🔀 HAProxy (haproxy/haproxy.cfg)
global
daemon
maxconn 4096
log stdout format raw local0
defaults
mode tcp
timeout connect 5s
timeout client 30m
timeout server 30m
log global
option tcplog
# Patroni REST to discover the primary
resolvers docker
nameserver dns1 127.0.0.11:53
frontend pgsql-in
bind *:5432
default_backend patroni-primary
backend patroni-primary
option httpchk GET /master
http-check expect status 200
server patroni-1 patroni-1:5432 check port 8008
server patroni-2 patroni-2:5432 check port 8008
🐳 docker-compose.yml
version: "3.9"
services:
etcd:
image: quay.io/coreos/etcd:latest
container_name: ${APP_NAME}_etcd
environment:
- ETCD_NAME=${ETCD_NAME}
- ETCD_ADVERTISE_CLIENT_URLS=http://etcd:${ETCD_CLIENT_PORT}
- ETCD_LISTEN_CLIENT_URLS=http://0.0.0.0:${ETCD_CLIENT_PORT}
- ETCD_INITIAL_ADVERTISE_PEER_URLS=http://etcd:${ETCD_PEER_PORT}
- ETCD_LISTEN_PEER_URLS=http://0.0.0.0:${ETCD_PEER_PORT}
- ETCD_INITIAL_CLUSTER=${ETCD_NAME}=http://etcd:${ETCD_PEER_PORT}
- ETCD_INITIAL_CLUSTER_STATE=new
- ETCD_INITIAL_CLUSTER_TOKEN=zbx-ha-token
ports:
- "${ETCD_CLIENT_PORT}:2379"
restart: always
networks: [ zbx_net ]
patroni-1:
image: ghcr.io/zalando/patroni:latest
container_name: ${APP_NAME}_patroni1
environment:
PATRONI_KUBERNETES_LABELS: "" # disable k8s discovery
PG_SUPERUSER: ${PG_SUPERUSER}
PG_SUPERPASS: ${PG_SUPERPASS}
PG_REPL_USER: ${PG_REPL_USER}
PG_REPL_PASS: ${PG_REPL_PASS}
volumes:
- ./patroni/patroni-node1.yml:/config/patroni.yml:ro
- patroni1_data:/var/lib/postgresql/data
command: ["patroni", "/config/patroni.yml"]
depends_on: [ etcd ]
restart: always
networks: [ zbx_net ]
patroni-2:
image: ghcr.io/zalando/patroni:latest
container_name: ${APP_NAME}_patroni2
environment:
PG_SUPERUSER: ${PG_SUPERUSER}
PG_SUPERPASS: ${PG_SUPERPASS}
PG_REPL_USER: ${PG_REPL_USER}
PG_REPL_PASS: ${PG_REPL_PASS}
volumes:
- ./patroni/patroni-node2.yml:/config/patroni.yml:ro
- patroni2_data:/var/lib/postgresql/data
command: ["patroni", "/config/patroni.yml"]
depends_on: [ etcd ]
restart: always
networks: [ zbx_net ]
haproxy:
image: haproxy:2.9
container_name: ${APP_NAME}_haproxy
ports:
- "${HAPROXY_PORT}:5432"
volumes:
- ./haproxy/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
depends_on: [ patroni-1, patroni-2 ]
restart: always
networks: [ zbx_net ]
zabbix-server:
image: zabbix/zabbix-server-pgsql:alpine-7.0-latest
container_name: ${APP_NAME}_zbx_server
environment:
DB_SERVER_HOST: haproxy
DB_SERVER_PORT: 5432
POSTGRES_USER: ${ZBX_DB_USER}
POSTGRES_PASSWORD: ${ZBX_DB_PASSWORD}
ZBX_ALLOWUNSUPPORTEDDBVERSIONS: "1"
depends_on: [ haproxy ]
restart: always
networks: [ zbx_net ]
zabbix-web:
image: zabbix/zabbix-web-nginx-pgsql:alpine-7.0-latest
container_name: ${APP_NAME}_zbx_web
environment:
DB_SERVER_HOST: haproxy
DB_SERVER_PORT: 5432
POSTGRES_USER: ${ZBX_DB_USER}
POSTGRES_PASSWORD: ${ZBX_DB_PASSWORD}
ZBX_SERVER_HOST: zabbix-server
PHP_TZ: ${TZ}
ports:
- "8080:8080"
depends_on: [ zabbix-server ]
restart: always
networks: [ zbx_net ]
networks:
zbx_net:
driver: bridge
volumes:
patroni1_data:
patroni2_data:
🚀 Quick Start
-
Create files exactly as shown.
-
Export DB user for Zabbix once Patroni is up:
docker exec -it zbx-ha_patroni1 psql -U ${PG_SUPERUSER} -c "CREATE USER ${ZBX_DB_USER} WITH PASSWORD '${ZBX_DB_PASSWORD}';" docker exec -it zbx-ha_patroni1 psql -U ${PG_SUPERUSER} -c "CREATE DATABASE zabbix OWNER ${ZBX_DB_USER};" -
Launch stack:
docker compose up -d -
Open UI:
http://<host>:8080→ complete Zabbix web installer. -
Test failover: stop
patroni-1; HAProxy should route topatroni-2automatically.
🛠️ Best Practices
• Use 3× etcd nodes in production for quorum
• Backups: pgBackRest or WAL-archiving to S3/NAS
• Synchronous mode for no data loss (slower writes, safer)
• Resource limits: CPU/memory reservations per service
• Monitoring: add Zabbix Agent on DB nodes, scrape Patroni REST /metrics
• Security: secrets via Docker secrets or Vault; restrict 5432 to internal network
• Upgrades: rolling—restart one Patroni node at a time
📈 What You Gain
• Higher uptime for monitoring and alerts
• Safer database with automatic leadership and replication
• Easy maintenance with rolling updates
• Scalable design—add replicas without redesigning the stack
✅ Next Steps
• Add a third Patroni node (and 3-node etcd)
• Enable read-only replicas for reports
• Wire pgBackRest and a weekly restore drill
📧 Contact: admin@1it.pro
🌐 Website: 1it.pro
📝 Blog: blog.1it.pro
— Save this guide and deploy your HA Zabbix today.