🛡️ 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, and listen ports/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

  1. Create files exactly as shown.

  2. 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};"
    
  3. Launch stack: docker compose up -d

  4. Open UI: http://<host>:8080 → complete Zabbix web installer.

  5. Test failover: stop patroni-1; HAProxy should route to patroni-2 automatically.


🛠️ 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.

UA EN RU

Зв'язатися з нами

Telegram Email