Skip to content

Instantly share code, notes, and snippets.

@danielehrhardt
Last active October 7, 2025 10:39
Show Gist options
  • Save danielehrhardt/c1858637b6adfdd83d8fe988ac747d4f to your computer and use it in GitHub Desktop.
Save danielehrhardt/c1858637b6adfdd83d8fe988ac747d4f to your computer and use it in GitHub Desktop.
Ubuntu 24.04: Zabbix 7.4 + MySQL end-to-end installer
#!/usr/bin/env bash
# Ubuntu 24.04: Zabbix 7.4 + MySQL end-to-end installer with detailed logging
set -Eeuo pipefail
# ---------- Config ----------
REPO_DEB_URL="https://repo.zabbix.com/zabbix/7.4/release/ubuntu/pool/main/z/zabbix-release/zabbix-release_latest_7.4+ubuntu24.04_all.deb"
REPO_DEB_FILE="/tmp/zabbix-release_latest_7.4+ubuntu24.04_all.deb"
TZ="Europe/Berlin" # PHP timezone for Zabbix UI
SECRET_FILE="/root/zabbix-db-secret.txt" # Stores generated DB password and access URL
LOG_DIR="/var/log"
STAMP="$(date +%Y%m%d_%H%M%S)"
LOG_FILE="${LOG_DIR}/zabbix_install_${STAMP}.log"
# Make locale predictable so tools like tr/grep behave
export LC_ALL=C
export LANG=C
# ---------- Logging helpers ----------
mkdir -p "$LOG_DIR" || true
# Redirect all stdout/stderr to both console and log
exec > >(tee -a "$LOG_FILE") 2>&1
PS4='+ [${BASH_SOURCE##*/}:${LINENO}] '
log() { printf '\n[%s] %s\n' "$(date -Is)" "$*"; }
step() { printf '\n=== [%s] %s ===\n' "$(date -Is)" "$*"; }
die() { echo "ERROR: $*" >&2; exit 1; }
# Run a command, log it, capture output, and fail loudly if non-zero
run() {
echo "\$ $*"
# shellcheck disable=SC2068
eval $* 2>&1 | tee -a "$LOG_FILE"
local rc=${PIPESTATUS[0]}
if (( rc != 0 )); then
die "Command failed (rc=${rc}): $*"
fi
}
# Trap for uncaught errors
trap 'echo; echo "!!! An error occurred. See log: ${LOG_FILE}"; echo' ERR
# ---------- Functions ----------
require_root() {
step "Checking for root privileges"
if [[ $EUID -ne 0 ]]; then
die "Please run as root (e.g., sudo bash $0)"
fi
}
gen_password() {
# Generate a MySQL/INI-friendly 24-char secret; robust against locale issues
step "Generating random database password"
local p=""
if command -v openssl >/dev/null 2>&1; then
p="$(LC_ALL=C openssl rand -base64 48 | tr -dc 'A-Za-z0-9@#%^*_+=-' | head -c 24 || true)"
fi
if [ -z "$p" ] || [ "${#p}" -lt 16 ]; then
p="$(LC_ALL=C tr -dc 'A-Za-z0-9@#%^*_+=-' </dev/urandom | head -c 24 || true)"
fi
if [ -z "$p" ] || [ "${#p}" -lt 16 ]; then
p="$(date +%s%N | sha256sum | cut -c1-24)"
fi
echo "$p"
}
install_base_packages() {
step "Updating APT and installing base packages"
export DEBIAN_FRONTEND=noninteractive
run apt-get update -y
run apt-get install -y wget lsb-release gnupg ca-certificates apt-transport-https curl
}
install_zabbix_repo() {
step "Installing Zabbix repository package"
run wget -O "$REPO_DEB_FILE" "$REPO_DEB_URL"
run dpkg -i "$REPO_DEB_FILE"
run apt-get update -y
}
install_mysql_and_zabbix() {
step "Installing MySQL server and Zabbix components"
run apt-get install -y mysql-server
run systemctl enable --now mysql
run systemctl is-active --quiet mysql
run apt-get install -y \
zabbix-server-mysql zabbix-frontend-php zabbix-apache-conf zabbix-sql-scripts zabbix-agent
}
mysql_hardening_minimal() {
step "Ensuring MySQL listens locally and is reachable"
# On Ubuntu 24.04 MySQL uses unix socket auth for root; this is fine.
# Just make sure service is up and basic connectivity works.
run mysql --protocol=socket -e "SELECT VERSION();" || true
# Ensure bind-address keeps MySQL local (security best practice for single-host Zabbix)
local cnf="/etc/mysql/mysql.conf.d/mysqld.cnf"
if grep -q '^[#[:space:]]*bind-address' "$cnf"; then
run sed -i 's/^[#[:space:]]*bind-address.*/bind-address = 127.0.0.1/' "$cnf"
else
echo "bind-address = 127.0.0.1" >> "$cnf"
fi
run systemctl restart mysql
run systemctl is-active --quiet mysql
}
create_db_user_and_import() {
local ZABBIX_DB_PASS="$1"
step "Creating Zabbix database and user; importing schema"
# Create DB and user using root socket auth
run mysql -e "CREATE DATABASE IF NOT EXISTS zabbix CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;"
run mysql -e "CREATE USER IF NOT EXISTS 'zabbix'@'localhost' IDENTIFIED BY '${ZABBIX_DB_PASS}';"
run mysql -e "GRANT ALL PRIVILEGES ON zabbix.* TO 'zabbix'@'localhost';"
run mysql -e "FLUSH PRIVILEGES;"
run mysql -e "SET GLOBAL log_bin_trust_function_creators = 1;"
# Import schema + data
local schema="/usr/share/zabbix/sql-scripts/mysql/server.sql.gz"
[[ -f "$schema" ]] || die "Schema file not found at ${schema}"
run zcat "$schema" \| mysql --default-character-set=utf8mb4 -uzabbix -p"$(printf %q "$ZABBIX_DB_PASS")" zabbix
# Disable again
run mysql -e "SET GLOBAL log_bin_trust_function_creators = 0;"
}
configure_zabbix_server() {
local ZABBIX_DB_PASS="$1"
local CFG="/etc/zabbix/zabbix_server.conf"
step "Configuring /etc/zabbix/zabbix_server.conf"
# Ensure DB settings
if grep -qE '^[#[:space:]]*DBName=' "$CFG"; then
run sed -i 's/^[#[:space:]]*DBName=.*/DBName=zabbix/' "$CFG"
else
echo "DBName=zabbix" >> "$CFG"
fi
if grep -qE '^[#[:space:]]*DBUser=' "$CFG"; then
run sed -i 's/^[#[:space:]]*DBUser=.*/DBUser=zabbix/' "$CFG"
else
echo "DBUser=zabbix" >> "$CFG"
fi
if grep -qE '^[#[:space:]]*DBPassword=' "$CFG"; then
run sed -i "s/^[#[:space:]]*DBPassword=.*/DBPassword=${ZABBIX_DB_PASS}/" "$CFG"
else
echo "DBPassword=${ZABBIX_DB_PASS}" >> "$CFG"
fi
}
configure_php_timezone_for_zabbix() {
local CONF="/etc/zabbix/apache.conf"
step "Ensuring PHP timezone is set to ${TZ} for Zabbix UI"
if [[ -f "$CONF" ]]; then
if grep -qE '^\s*php_value\s+date\.timezone' "$CONF"; then
run sed -i "s#^\s*php_value\s\+date\.timezone.*# php_value date.timezone ${TZ}#" "$CONF"
else
cat >>"$CONF" <<EOF
# Set PHP timezone for Zabbix UI
<Directory "/usr/share/zabbix">
php_value date.timezone ${TZ}
</Directory>
EOF
fi
else
echo "WARN: ${CONF} not found; skipping timezone injection"
fi
}
restart_and_enable_services() {
step "Restarting and enabling Zabbix server/agent and Apache"
run systemctl restart zabbix-server zabbix-agent apache2
run systemctl enable zabbix-server zabbix-agent apache2
run systemctl is-active --quiet zabbix-server
run systemctl is-active --quiet zabbix-agent
run systemctl is-active --quiet apache2
}
maybe_configure_ufw() {
step "Configuring UFW (if active)"
if command -v ufw >/dev/null 2>&1 && ufw status | grep -q "Status: active"; then
run ufw allow 80/tcp || true
run ufw allow 10050/tcp || true
run ufw allow 10051/tcp || true
else
echo "UFW not active; skipping firewall changes."
fi
}
save_secrets() {
local ZABBIX_DB_PASS="$1"
step "Saving secrets to ${SECRET_FILE}"
umask 077
cat > "$SECRET_FILE" <<EOF
Zabbix MySQL credentials (generated $(date -Is))
-----------------------------------------------
DB Host : localhost
DB Name : zabbix
DB User : zabbix
DB Pass : ${ZABBIX_DB_PASS}
Zabbix Web (Apache):
http://$(hostname -I | awk '{print $1}')/zabbix
Log file for this installation:
${LOG_FILE}
EOF
chmod 600 "$SECRET_FILE"
echo "Secrets written."
}
final_checks() {
step "Final checks"
echo "- MySQL status:"; systemctl --no-pager --full status mysql | sed -n '1,12p' || true
echo "- Zabbix server status:"; systemctl --no-pager --full status zabbix-server | sed -n '1,12p' || true
echo "- Zabbix agent status:"; systemctl --no-pager --full status zabbix-agent | sed -n '1,12p' || true
echo "- Apache status:"; systemctl --no-pager --full status apache2 | sed -n '1,12p' || true
}
# ---------- Main ----------
main() {
step "Starting Zabbix 7.4 + MySQL installation (log: ${LOG_FILE})"
require_root
install_base_packages
install_zabbix_repo
install_mysql_and_zabbix
mysql_hardening_minimal
ZABBIX_DB_PASS="$(gen_password)"
echo "Generated password length: ${#ZABBIX_DB_PASS}" # visible sanity check
create_db_user_and_import "$ZABBIX_DB_PASS"
configure_zabbix_server "$ZABBIX_DB_PASS"
configure_php_timezone_for_zabbix"
restart_and_enable_services
maybe_configure_ufw
save_secrets "$ZABBIX_DB_PASS"
final_checks
step "Done ✅"
echo "Open the Zabbix UI at: http://$(hostname -I | awk '{print $1}')/zabbix"
echo "Credentials saved at: ${SECRET_FILE}"
echo "Installer log: ${LOG_FILE}"
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment