Overview
Prometheus and Grafana get most of the homelab monitoring attention, but they're built around pull-based metrics scraping and PromQL dashboards. Zabbix takes a different, older, and in some ways more complete approach: it's a single all-in-one platform that handles agent-based polling, SNMP, trap-based push metrics, trigger evaluation, escalation chains, and templated auto-discovery out of the box — no separate exporter ecosystem to assemble. For infrastructure that isn't already emitting Prometheus-format metrics (network switches, UPS units, Windows servers, legacy appliances), Zabbix's agent + SNMP model is usually less work to stand up.
This build deploys Zabbix 7.0 LTS (the current long-term-support release, patched through 7.0.19) with PostgreSQL as the backend, using the official Docker images rather than a from-source install. It covers enrolling a Linux host and an SNMP-only device, tuning triggers so a single flapping service doesn't page you five times, and wiring alert actions to a Discord webhook instead of email.
Architecture
┌──────────────┐ agent poll :10050 ┌─────────────────────┐
│ Linux host │──────────────────────▶│ │
│ (zabbix-agent2)│ │ Zabbix Server │
└──────────────┘ │ :10051 (trapper) │
┌──────────────┐ SNMP UDP :161 │ │──▶ PostgreSQL 16
│ Switch/UPS │──────────────────────▶│ history · triggers │ (history + config)
└──────────────┘ └──────────┬───────────┘
│ API/RPC
┌─────────▼──────────┐
│ Zabbix Web (nginx) │──▶ Discord webhook
│ :8080 dashboards │ (alert actions)
└─────────────────────┘
Zabbix Server does the polling/trapping and trigger evaluation; Zabbix Web is a stateless frontend that reads/writes the same PostgreSQL database and has no monitoring logic of its own. Agents are pull-polled by default, but can also push data proactively ("active checks") to reduce server-side connection overhead on larger fleets.
Step-by-Step Build
1. Lay out the Docker Compose stack
# docker-compose.yml
services:
postgres-server:
image: postgres:16-alpine
container_name: zabbix-db
restart: unless-stopped
environment:
POSTGRES_USER: zabbix
POSTGRES_PASSWORD: ${ZABBIX_DB_PASSWORD}
POSTGRES_DB: zabbix
volumes:
- ./data/postgres:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U zabbix"]
interval: 10s
retries: 5
zabbix-server:
image: zabbix/zabbix-server-pgsql:alpine-7.0-latest
container_name: zabbix-server
restart: unless-stopped
depends_on:
postgres-server:
condition: service_healthy
environment:
DB_SERVER_HOST: postgres-server
POSTGRES_USER: zabbix
POSTGRES_PASSWORD: ${ZABBIX_DB_PASSWORD}
POSTGRES_DB: zabbix
volumes:
- ./data/server-alertscripts:/usr/lib/zabbix/alertscripts
ports:
- "10051:10051"
zabbix-web:
image: zabbix/zabbix-web-nginx-pgsql:alpine-7.0-latest
container_name: zabbix-web
restart: unless-stopped
depends_on:
- zabbix-server
environment:
DB_SERVER_HOST: postgres-server
POSTGRES_USER: zabbix
POSTGRES_PASSWORD: ${ZABBIX_DB_PASSWORD}
POSTGRES_DB: zabbix
ZBX_SERVER_HOST: zabbix-server
PHP_TZ: America/Vancouver
ports:
- "8080:8080"
zabbix-agent2:
image: zabbix/zabbix-agent2:alpine-7.0-latest
container_name: zabbix-agent2
restart: unless-stopped
environment:
ZBX_HOSTNAME: docker-host-01
ZBX_SERVER_HOST: zabbix-server
privileged: true
network_mode: hostThe official images auto-run schema migration on first boot — no manual zcat create.sql.gz | psql step like a from-source install requires. Requires Docker Compose 2.24.0+; run docker compose up -d and give the server container a minute to finish the initial DB import before hitting the web UI.
2. First login and change the default password
# wait for zabbix-web healthy, then browse to :8080
# default credentials: Admin / zabbixChange the Admin password immediately under Users → Admin → Change password — the default is well-known and Zabbix's own hardening guide calls it out as the first thing to fix on a new install.
3. Enroll a Linux host via Agent2
On the target host (or as a sidecar container per service above), point the agent at the server:
# /etc/zabbix/zabbix_agent2.conf
Server=zabbix-server.example.lan
ServerActive=zabbix-server.example.lan
Hostname=docker-host-01In the Zabbix frontend: Data collection → Hosts → Create host, set the hostname to match Hostname above, attach it to a host group (e.g. "Docker hosts"), and link the Linux by Zabbix agent template — this template ships pre-built items (CPU, memory, disk, network) and triggers, so there's no manual item authoring for baseline OS metrics.
4. Add an SNMP-only device (switch, UPS, etc.)
Devices that can't run an agent (managed switches, PDUs, printers) go through SNMP instead:
- Create host → set SNMP interface, port 161, community string (SNMPv2c) or SNMPv3 credentials.
- Link a matching template — Zabbix ships official templates for common vendors under Network devices (e.g. "Cisco IOS SNMP").
- Confirm data is arriving: Monitoring → Latest data, filter by host, check for non-stale timestamps.
5. Tune triggers to avoid alert fatigue
The default templates alert on raw thresholds, which flaps on transient spikes. Wrap trigger expressions in a time-window aggregate instead of a point-in-time check:
# Bad: fires on any single high reading
last(/docker-host-01/system.cpu.load[percpu,avg1])>5
# Better: sustained load over 5 minutes
min(/docker-host-01/system.cpu.load[percpu,avg1],5m)>5
Set trigger severity deliberately (Warning vs. High vs. Disaster) — actions can filter on severity, which is how you keep "disk 90% full" out of the same notification channel as "host unreachable."
6. Route alerts to Discord instead of email
Zabbix has no native Discord media type, but a webhook-based one is straightforward via a JavaScript alert script:
- Alerts → Media types → Create media type → type Webhook.
- Add a
URLparameter pointing at the Discord webhook URL, and a script that POSTs{"content": "{ALERT.SUBJECT}\n{ALERT.MESSAGE}"}as JSON. - Users → Admin → Media → add the new media type.
- Alerts → Actions → Trigger actions → create a condition (e.g. severity ≥ Warning) and an operation that sends via the Discord media type.
Test with a manufactured trigger (temporarily lower a threshold) before trusting it for a real incident.
Testing
- Agent connectivity:
docker compose exec zabbix-server zabbix_server -R config_cache_reload, then Monitoring → Latest data — confirm data points are landing with recent timestamps, not just "Zabbix agent is available" ping items. - SNMP reachability:
snmpwalk -v2c -c <community> <device-ip>from the server host — if this fails outside Zabbix, the item will fail inside it too. - Trigger logic: force a threshold breach (e.g.
stress-ng --cpu 4 --timeout 300son a test host) and confirm the trigger fires in Monitoring → Problems within the expected evaluation window, then resolves cleanly after load drops. - Alert delivery: fire a manual test event and confirm the Discord message lands in the right channel with the right severity color — silent failures here usually mean the webhook script's exit code isn't
0. - Database growth:
docker compose exec postgres-server psql -U zabbix -c "SELECT pg_size_pretty(pg_database_size('zabbix'));"— history/trends tables grow fast with short polling intervals; check this weekly for the first month.
Deployment Notes
- Set housekeeping (Administration → General → Housekeeping) to trim history/trends retention — the defaults keep raw history for 31 days, which is generous for a homelab-scale install and adds up in PostgreSQL disk usage.
- Back up the PostgreSQL volume, not just the Zabbix config export — host/item/trigger definitions live in the same database as historical data.
- For fleets beyond ~500 monitored items, consider TimescaleDB as the PostgreSQL storage backend (
postgres-serverimage swap totimescale/timescaledb) — Zabbix has first-class support for it and it meaningfully reduces history-table bloat. network_mode: hoston the agent container is the simplest way to get accurate host-level network/interface metrics; it does trade away Docker's network isolation for that container specifically.
Extensions
- Combine with the site's Prometheus + Grafana build — Zabbix can scrape a Prometheus endpoint as an HTTP agent item, letting one alerting/escalation engine cover both agent-based and Prometheus-native services.
- Layer on the Traefik reverse proxy build to put the web frontend behind real TLS instead of a bare
:8080. - Use Zabbix's built-in network discovery rules to auto-register new hosts on a subnet instead of manually creating each one.
- Script
report_generate-style capacity reports (host-group scoped PDF exports) for a monthly infrastructure capacity review without touching Grafana at all.
Sources: Zabbix Installation from Containers, zabbix/zabbix-docker GitHub, zabbix-server-pgsql on Docker Hub, zabbix-web-nginx-pgsql on Docker Hub