I was upgrading my Zabbix install to the latest, and decided to write an upgrade script to automate all the suggested steps. With a bit of help from ChatGPT, I came up with the following. It worked well, and should work for newer versions of Zabbix. The only thing which needs to be set would be the ZABBIX_MAJOR version, if it's different than 7.4
HTML Code:
#!/usr/bin/env bash
set -euo pipefail
# Zabbix upgrade helper for RHEL 8/9:
# - Role detection (agent2/server/proxy/web)
# - Stop server/proxy before DB backup (then ALWAYS restart after backup)
# - Back up DB + configs + web/PHP + binaries
# - Upgrade detected roles
# - DRY-RUN mode
# - backup-only / upgrade-only modes
#
# Usage:
# sudo ./upgrade-zabbix.sh
# sudo ./upgrade-zabbix.sh --dry-run
# sudo ./upgrade-zabbix.sh --backup-only
# sudo ./upgrade-zabbix.sh --upgrade-only
#
# Env overrides:
# ZABBIX_MAJOR=6.0|6.4|7.0|7.4
# BACKUP_DIR=/var/backups/zabbix
# SKIP_DB_BACKUP=1
# DRY_RUN=1
ZABBIX_MAJOR="${ZABBIX_MAJOR:-7.4}"
BACKUP_DIR="${BACKUP_DIR:-/var/backups/zabbix}"
SKIP_DB_BACKUP="${SKIP_DB_BACKUP:-0}"
DRY_RUN="${DRY_RUN:-0}"
DO_BACKUP=1
DO_UPGRADE=1
PKG_MANAGER=""
RHEL_MAJOR=""
HAS_AGENT2=0
HAS_SERVER=0
HAS_PROXY=0
HAS_WEB=0
# Track whether we stopped things during backup
STOPPED_SERVER=0
STOPPED_PROXY=0
log() { printf "\n[%s] %s\n" "$(date +'%F %T')" "$*"; }
die() { printf "\nERROR: %s\n" "$*" >&2; exit 1; }
have_cmd() { command -v "$1" >/dev/null 2>&1; }
ts_now() { date +'%Y%m%d-%H%M%S'; }
run() {
if [[ "$DRY_RUN" == "1" ]]; then
printf "[DRY-RUN] %q" "$1"
shift
for a in "$@"; do printf " %q" "$a"; done
printf "\n"
return 0
fi
"$@"
}
run_sh() {
local cmd="$1"
if [[ "$DRY_RUN" == "1" ]]; then
printf "[DRY-RUN] %s\n" "$cmd"
return 0
fi
bash -c "$cmd"
}
require_root() { [[ "${EUID}" -eq 0 ]] || die "Run as root (use sudo)."; }
detect_rhel() {
[[ -r /etc/os-release ]] || die "/etc/os-release not found"
# shellcheck disable=SC1091
. /etc/os-release
RHEL_MAJOR="$(rpm -E %rhel 2>/dev/null || true)"
[[ "$RHEL_MAJOR" =~ ^[0-9]+$ ]] || die "Unable to detect RHEL major version."
[[ "$RHEL_MAJOR" == "8" || "$RHEL_MAJOR" == "9" ]] || die "Supported: RHEL 8 or 9. Detected: RHEL ${RHEL_MAJOR}"
if command -v dnf >/dev/null 2>&1; then
PKG_MANAGER="dnf"
elif command -v yum >/dev/null 2>&1; then
PKG_MANAGER="yum"
else
die "Neither dnf nor yum found."
fi
}
# -------------------------
# Args
# -------------------------
parse_args() {
while [[ "${1:-}" != "" ]]; do
case "$1" in
-n|--dry-run) DRY_RUN=1 ;;
--backup-only) DO_BACKUP=1; DO_UPGRADE=0 ;;
--upgrade-only) DO_BACKUP=0; DO_UPGRADE=1 ;;
-h|--help)
cat <<'EOF'
Usage: sudo ./upgrade-zabbix.sh [options]
Options:
-n, --dry-run Print actions; do not modify the system
--backup-only Perform backups only (no upgrade)
--upgrade-only Perform upgrade only (no backups)
-h, --help Show this help
Environment overrides:
ZABBIX_MAJOR=6.0|6.4|7.0 (default: 6.0)
BACKUP_DIR=/var/backups/zabbix
SKIP_DB_BACKUP=1
DRY_RUN=1
EOF
exit 0
;;
*) die "Unknown argument: $1" ;;
esac
shift
done
}
# -------------------------
# Role detection
# -------------------------
is_pkg_installed() { rpm -q "$1" >/dev/null 2>&1; }
detect_roles() {
if is_pkg_installed zabbix-agent2; then HAS_AGENT2=1; fi
if is_pkg_installed zabbix-server-mysql || is_pkg_installed zabbix-server-pgsql; then HAS_SERVER=1; fi
if is_pkg_installed zabbix-proxy-mysql || is_pkg_installed zabbix-proxy-pgsql || is_pkg_installed zabbix-proxy-sqlite3; then HAS_PROXY=1; fi
if is_pkg_installed zabbix-web || is_pkg_installed zabbix-web-mysql || is_pkg_installed zabbix-web-pgsql; then HAS_WEB=1; fi
log "Detected roles: agent2=${HAS_AGENT2} server=${HAS_SERVER} proxy=${HAS_PROXY} web=${HAS_WEB}"
}
# -------------------------
# Service stop/start
# -------------------------
svc_exists() { systemctl list-unit-files --type=service 2>/dev/null | awk '{print $1}' | grep -qx "$1"; }
stop_server_proxy_for_backup() {
STOPPED_SERVER=0
STOPPED_PROXY=0
if [[ "$HAS_SERVER" == "1" ]] && svc_exists zabbix-server.service; then
if systemctl is-active --quiet zabbix-server 2>/dev/null; then
log "Stopping zabbix-server for backup"
run systemctl stop zabbix-server
STOPPED_SERVER=1
else
log "zabbix-server already stopped"
fi
fi
if [[ "$HAS_PROXY" == "1" ]] && svc_exists zabbix-proxy.service; then
if systemctl is-active --quiet zabbix-proxy 2>/dev/null; then
log "Stopping zabbix-proxy for backup"
run systemctl stop zabbix-proxy
STOPPED_PROXY=1
else
log "zabbix-proxy already stopped"
fi
fi
if [[ "$STOPPED_SERVER" == "1" || "$STOPPED_PROXY" == "1" ]]; then
log "Server/proxy stopped for backup."
else
log "No running server/proxy services to stop."
fi
}
restart_server_proxy_after_backup() {
# Only restart services we actually stopped (avoids changing admin intent)
if [[ "$STOPPED_PROXY" == "1" ]] && svc_exists zabbix-proxy.service; then
log "Restarting zabbix-proxy after backup"
run systemctl start zabbix-proxy
fi
if [[ "$STOPPED_SERVER" == "1" ]] && svc_exists zabbix-server.service; then
log "Restarting zabbix-server after backup"
run systemctl start zabbix-server
fi
}
start_server_proxy_after_upgrade() {
if [[ "$HAS_PROXY" == "1" ]] && svc_exists zabbix-proxy.service; then
log "Enabling/starting zabbix-proxy"
run systemctl enable --now zabbix-proxy || true
run systemctl restart zabbix-proxy || true
fi
if [[ "$HAS_SERVER" == "1" ]] && svc_exists zabbix-server.service; then
log "Enabling/starting zabbix-server"
run systemctl enable --now zabbix-server || true
run systemctl restart zabbix-server || true
fi
}
# -------------------------
# Backup helpers
# -------------------------
read_kv_from_conf() {
local file="$1" key="$2"
[[ -r "$file" ]] || return 1
awk -F= -v k="$key" '
$0 ~ "^[[:space:]]*#" {next}
$1 ~ "^[[:space:]]*"k"[[:space:]]*$" {
sub(/^[[:space:]]+|[[:space:]]+$/, "", $2);
print $2; exit
}' "$file"
}
detect_db_type() {
if systemctl is-active --quiet mariadb 2>/dev/null || systemctl is-active --quiet mysqld 2>/dev/null; then
echo "mysql"; return 0
fi
if systemctl is-active --quiet postgresql 2>/dev/null; then
echo "pgsql"; return 0
fi
if have_cmd mysqldump; then echo "mysql"; return 0; fi
if have_cmd pg_dump; then echo "pgsql"; return 0; fi
return 1
}
backup_db_mysql() {
local out_sql="$1"
local zconf="/etc/zabbix/zabbix_server.conf"
if [[ "$HAS_SERVER" != "1" && -r /etc/zabbix/zabbix_proxy.conf ]]; then
zconf="/etc/zabbix/zabbix_proxy.conf"
fi
local dbhost dbname dbuser dbpass
dbhost="$(read_kv_from_conf "$zconf" "DBHost" || true)"
dbname="$(read_kv_from_conf "$zconf" "DBName" || true)"
dbuser="$(read_kv_from_conf "$zconf" "DBUser" || true)"
dbpass="$(read_kv_from_conf "$zconf" "DBPassword" || true)"
dbhost="${dbhost:-localhost}"
dbname="${dbname:-zabbix}"
dbuser="${dbuser:-zabbix}"
log "DB backup (MySQL/MariaDB): host=${dbhost} db=${dbname} user=${dbuser} (from ${zconf})"
have_cmd mysqldump || die "mysqldump not found. Install mariadb/mysql client utilities or set SKIP_DB_BACKUP=1."
local extra_opts=( "--host=${dbhost}" "--user=${dbuser}" "--single-transaction" "--routines" "--triggers" "--events" )
if [[ -n "${dbpass}" ]]; then
extra_opts+=( "--password=${dbpass}" )
fi
run_sh "mysqldump $(printf '%q ' "${extra_opts[@]}") $(printf '%q' "${dbname}") | gzip -9 > $(printf '%q' "${out_sql}.gz")"
}
backup_db_pgsql() {
local out_base="$1"
local zconf="/etc/zabbix/zabbix_server.conf"
if [[ "$HAS_SERVER" != "1" && -r /etc/zabbix/zabbix_proxy.conf ]]; then
zconf="/etc/zabbix/zabbix_proxy.conf"
fi
local dbhost dbname dbuser dbpass
dbhost="$(read_kv_from_conf "$zconf" "DBHost" || true)"
dbname="$(read_kv_from_conf "$zconf" "DBName" || true)"
dbuser="$(read_kv_from_conf "$zconf" "DBUser" || true)"
dbpass="$(read_kv_from_conf "$zconf" "DBPassword" || true)"
dbhost="${dbhost:-localhost}"
dbname="${dbname:-zabbix}"
dbuser="${dbuser:-zabbix}"
log "DB backup (PostgreSQL): host=${dbhost} db=${dbname} user=${dbuser} (from ${zconf})"
have_cmd pg_dump || die "pg_dump not found. Install postgresql client utilities or set SKIP_DB_BACKUP=1."
if [[ -n "${dbpass}" ]]; then
run_sh "PGPASSWORD=$(printf '%q' "${dbpass}") pg_dump -h $(printf '%q' "${dbhost}") -U $(printf '%q' "${dbuser}") -Fc $(printf '%q' "${dbname}") > $(printf '%q' "${out_base}.dump")"
else
run_sh "pg_dump -h $(printf '%q' "${dbhost}") -U $(printf '%q' "${dbuser}") -Fc $(printf '%q' "${dbname}") > $(printf '%q' "${out_base}.dump")"
fi
}
backup_files_tar() {
local tar_path="$1"; shift
local -a paths=("$@")
local -a existing=()
for p in "${paths[@]}"; do
[[ -e "$p" ]] && existing+=("$p")
done
if [[ "${#existing[@]}" -eq 0 ]]; then
log "No matching files/dirs found for tar: ${tar_path}"
return 0
fi
log "Creating tar: ${tar_path}"
run tar -cpf "$tar_path" --xattrs --acls --selinux "${existing[@]}" 2>/dev/null || run tar -cpf "$tar_path" "${existing[@]}"
}
backup_binaries() {
local out_dir="$1"
log "Backing up package inventory (RPM lists)"
if [[ "$DRY_RUN" == "1" ]]; then
echo "[DRY-RUN] rpm -qa | sort > ${out_dir}/rpm_all.txt"
echo "[DRY-RUN] rpm -qa | grep -i '^zabbix' | sort > ${out_dir}/rpm_zabbix.txt"
echo "[DRY-RUN] (would run rpm -ql for each installed zabbix RPM)"
else
rpm -qa | sort > "${out_dir}/rpm_all.txt"
rpm -qa | grep -i '^zabbix' | sort > "${out_dir}/rpm_zabbix.txt" || true
if [[ -s "${out_dir}/rpm_zabbix.txt" ]]; then
while IFS= read -r pkg; do
rpm -ql "$pkg" > "${out_dir}/rpm_ql_${pkg}.txt" || true
done < "${out_dir}/rpm_zabbix.txt"
fi
fi
log "Backing up common Zabbix binary locations (best-effort)"
run mkdir -p "${out_dir}/bin"
local -a bin_paths=(
/usr/sbin/zabbix_server
/usr/sbin/zabbix_proxy
/usr/sbin/zabbix_agentd
/usr/sbin/zabbix_agent2
/usr/bin/zabbix_get
/usr/bin/zabbix_sender
/usr/bin/zabbix_js
)
for b in "${bin_paths[@]}"; do
[[ -x "$b" ]] && run cp -a "$b" "${out_dir}/bin/" || true
done
}
backup_all() {
local ts backup_root final
ts="$(ts_now)"
backup_root="${BACKUP_DIR}/zabbix-backup-${ts}"
final="${BACKUP_DIR}/zabbix-full-backup-${ts}.tar.gz"
log "Creating backup set at: ${backup_root}"
run mkdir -p "${backup_root}"
stop_server_proxy_for_backup
# Ensure we restart anything we stopped, even if the backup fails mid-way
trap 'rc=$?; log "Backup phase exiting (rc=$rc) - attempting to restart server/proxy if we stopped them"; restart_server_proxy_after_backup || true; exit $rc' EXIT
if [[ "${SKIP_DB_BACKUP}" == "1" ]]; then
log "Skipping DB backup (SKIP_DB_BACKUP=1)"
else
local dbtype outbase
dbtype="$(detect_db_type || true)"
outbase="${backup_root}/db/zabbix-db-${ts}"
run mkdir -p "${backup_root}/db"
case "${dbtype:-}" in
mysql) backup_db_mysql "${outbase}.sql" ;;
pgsql) backup_db_pgsql "${outbase}" ;;
*)
log "Could not confidently detect DB type; trying MySQL first if mysqldump exists, else PostgreSQL."
if have_cmd mysqldump; then
backup_db_mysql "${outbase}.sql"
elif have_cmd pg_dump; then
backup_db_pgsql "${outbase}"
else
die "No DB dump tools found (mysqldump/pg_dump). Install client tools or set SKIP_DB_BACKUP=1."
fi
;;
esac
fi
log "Backing up Zabbix/service config files"
backup_files_tar "${backup_root}/configs-${ts}.tar" \
/etc/zabbix \
/etc/httpd/conf.d/zabbix.conf \
/etc/httpd/conf.d/zabbix*.conf \
/etc/nginx/conf.d/zabbix*.conf \
/etc/php-fpm.d \
/etc/php.d \
/etc/php.ini \
/etc/opt/rh/rh-php*/php-fpm.d \
/etc/opt/rh/rh-php*/php.d \
/etc/opt/rh/rh-php*/php.ini \
/etc/systemd/system/zabbix*.service \
/usr/lib/systemd/system/zabbix*.service
log "Backing up Zabbix web/PHP files"
backup_files_tar "${backup_root}/web-${ts}.tar" \
/usr/share/zabbix \
/etc/zabbix/web \
/var/lib/zabbix \
/var/log/zabbix
backup_binaries "${backup_root}"
log "Creating consolidated archive: ${final}"
run tar -C "${BACKUP_DIR}" -czf "${final}" "$(basename "${backup_root}")"
log "Backup complete: ${final}"
# Restart whatever we stopped, then remove the EXIT trap for the caller
restart_server_proxy_after_backup
trap - EXIT
}
# -------------------------
# Upgrade steps
# -------------------------
install_zabbix_repo() {
local repo_rpm="https://repo.zabbix.com/zabbix/${ZABBIX_MAJOR}/rhel/${RHEL_MAJOR}/x86_64/zabbix-release-latest-${ZABBIX_MAJOR}.el${RHEL_MAJOR}.noarch.rpm"
log "Installing/updating Zabbix repo package: ${repo_rpm}"
run rpm -Uvh --quiet "$repo_rpm"
}
refresh_metadata() {
log "Refreshing package metadata"
run "$PKG_MANAGER" -y clean all
run "$PKG_MANAGER" -y makecache
}
upgrade_detected_roles() {
log "Upgrading/installing detected Zabbix packages"
run "$PKG_MANAGER" -y upgrade zabbix-release || true
if [[ "$HAS_AGENT2" == "1" ]]; then
run "$PKG_MANAGER" -y install zabbix-agent2 'zabbix-agent2-plugin-*' || true
run "$PKG_MANAGER" -y upgrade zabbix-agent2 'zabbix-agent2-plugin-*' zabbix-selinux-policy zabbix-get zabbix-sender || true
fi
if [[ "$HAS_SERVER" == "1" ]]; then
run "$PKG_MANAGER" -y upgrade 'zabbix-server-*' zabbix-selinux-policy || true
fi
if [[ "$HAS_PROXY" == "1" ]]; then
run "$PKG_MANAGER" -y upgrade 'zabbix-proxy-*' zabbix-selinux-policy || true
fi
if [[ "$HAS_WEB" == "1" ]]; then
run "$PKG_MANAGER" -y upgrade 'zabbix-web*' || true
fi
}
restart_services_after_upgrade() {
log "Restarting detected services (post-upgrade)"
run systemctl daemon-reload || true
if [[ "$HAS_AGENT2" == "1" ]] && svc_exists zabbix-agent2.service; then
run systemctl enable --now zabbix-agent2 || true
run systemctl restart zabbix-agent2 || true
fi
start_server_proxy_after_upgrade
}
verify() {
log "Verifying status (best-effort)"
if [[ "$HAS_SERVER" == "1" ]] && svc_exists zabbix-server.service; then
run systemctl --no-pager --full status zabbix-server || true
fi
if [[ "$HAS_PROXY" == "1" ]] && svc_exists zabbix-proxy.service; then
run systemctl --no-pager --full status zabbix-proxy || true
fi
if [[ "$HAS_AGENT2" == "1" ]] && svc_exists zabbix-agent2.service; then
run systemctl --no-pager --full status zabbix-agent2 || true
fi
log "Installed Zabbix RPMs:"
if [[ "$DRY_RUN" == "1" ]]; then
echo "[DRY-RUN] rpm -qa | grep -i '^zabbix' | sort"
else
rpm -qa | grep -i '^zabbix' | sort || true
fi
}
main() {
parse_args "$@"
require_root
detect_rhel
run mkdir -p "${BACKUP_DIR}"
detect_roles
if [[ "$DO_BACKUP" == "1" ]]; then
backup_all
else
log "Skipping backup phase (--upgrade-only selected)"
fi
if [[ "$DO_UPGRADE" == "1" ]]; then
install_zabbix_repo
refresh_metadata
upgrade_detected_roles
restart_services_after_upgrade
verify
else
log "Skipping upgrade phase (--backup-only selected)"
fi
log "Done."
[[ "$DRY_RUN" == "1" ]] && log "DRY-RUN mode: no changes were made."
}
main "$@"