- Python 63.7%
- Shell 36.3%
| Grafana | ||
| scripts | ||
| templates | ||
| tools | ||
| .gitattributes | ||
| .gitignore | ||
| .gitlab-ci.yml | ||
| CHANGELOG.md | ||
| install.sh | ||
| LICENSE | ||
| mailcow-monitor.service | ||
| mailcow-monitor.timer | ||
| MAILCOW-MONITORING-DOKU.md | ||
| mailcow-zabbix.conf | ||
| README.de.md | ||
| README.md | ||
| test-complete.sh | ||
| uninstall.sh | ||
| UPDATE.de.md | ||
| UPDATE.md | ||
| update.sh | ||
Mailcow Monitoring v1.2 for Zabbix
Complete monitoring solution for Mailcow-Dockerized with Zabbix Agent 2. 246 metrics, 71 triggers, 19 dashboards — secure by design, installed in 5 minutes.
🇩🇪 Deutsche Version | 🇩🇪 Ausführliche Dokumentation
Architecture
systemd timer (60s) → mailcow-collector.py (root)
→ /run/mailcow-monitor/monitor.json (chmod 640, group zabbix)
→ Zabbix Agent 2 (zabbix user) → mailcow-reader.sh → reads JSON
The collector runs as root (needs Docker/MySQL access) and writes metrics to a JSON file readable only by root and the zabbix group. The Zabbix Agent only reads that file — no Docker access, no sudo, no UnsafeUserParameters required.
What's Monitored
| Module | Metrics | Description |
|---|---|---|
| Postfix | 16 | Queue, connections, deferred/bounced, SASL failures |
| Postfix Logs | 11 | Relay denied, RBL rejects, TLS errors, quota warnings |
| Postscreen | 9 | Pass/reject/DNSBL/pregreet (auto-detected) |
| Dovecot | 10 | Connections, login failures, IMAP disconnects |
| Rspamd | 14 | Spam/ham ratio, reject rate, greylist, actions |
| Rspamd Bayes | 5 | Training status: untrained → low → good → excellent |
| Security | 13 | Fail2ban, RBL blacklist, DNS records, open relay |
| Security Audit | 6 | DANE/TLSA, MTA-STS, TLS-RPT, BIMI — score 0-7 |
| Disk | 15 | Root, Docker, vmail, log partitions |
| Mailboxes & Domains | 10 | Quota usage, top 5 mailboxes |
| Mailflow | 28 | Received/delivered/bounced + anomaly detection |
| ClamAV | 8 | Signature age, DB version, scan status |
| Watchdog | 18 | Health status for all 15 Mailcow services |
| Docker | 7+LLD | CPU, RAM, restarts per container |
| TLS/Certificates | 10 | HTTPS, IMAPS, Submission — days until expiry |
| Backup | 9 | Age, size, count, missing backups |
| + 6 more | ... | SOGo, Quarantine, Queue Age, Sync Jobs, Updates, Aliases |
Total: 246 UserParameters · 303 template items · 71 triggers · 19 dashboards
Key Features
Anomaly Detection
Instead of fixed thresholds, 5 baseline triggers use trendavg() to learn what's normal over a week and alert on deviations:
| Metric | Spike | Drop |
|---|---|---|
| Received | >5× weekly avg | <20% weekly avg |
| Rejected | >10× weekly avg | — |
| Bounced | >5× weekly avg | — |
| Deferred | >5× weekly avg | — |
Security Audit Score (0-7)
Checks SPF, DKIM, DMARC plus DANE/TLSA, MTA-STS, TLS-RPT and BIMI. Trigger alerts when score drops below 3.
Low-Level Discovery
4 LLD rules automatically discover and monitor all domains, mailboxes, sync jobs and Docker containers individually.
Requirements
- Mailcow-Dockerized (running)
- Zabbix Server + Zabbix Agent 2
- Zabbix 7.0
- zabbix-get (
apt install zabbix-get— needed for test script validation) - Python 3, git, dig (dnsutils), openssl, netcat
- pflogsumm (
apt install pflogsumm)
Zabbix Agent Configuration
All 246 items are active agent checks. The agent fetches them itself from the
address in ServerActive — Server= only governs passive queries and is not
sufficient. If ServerActive is missing, or points at 127.0.0.1 while your
Zabbix server runs elsewhere, not a single item will collect data — silently.
Items and triggers show up in the frontend, graphs stay empty.
In /etc/zabbix/zabbix_agent2.conf:
Server=<your-zabbix-server-ip>,127.0.0.1 # passive: who may query this agent
ServerActive=<your-zabbix-server-ip> # ACTIVE: without this, no data at all
Hostname=<host name as configured in Zabbix>
127.0.0.1inServer=is only needed so thattest-complete.shandzabbix_get -s 127.0.0.1work.Hostnamemust match the host's technical name in Zabbix exactly, otherwise the server rejects the active checks.- Each parameter may appear once uncommented — duplicates prevent the agent from starting.
Validate the config before restarting:
zabbix_agent2 -T -c /etc/zabbix/zabbix_agent2.conf
systemctl restart zabbix-agent2
Note:
test-complete.shqueries the UserParameters passively viazabbix_get. It therefore reports success even whenServerActiveis wrong and nothing is actually being collected. Always verify in Monitoring → Latest data.
Security
- The collector runs as root via a systemd timer; the Zabbix agent only reads a
JSON file and needs no privileges at all — no sudo rules,
UnsafeUserParameters=0. - State lives in
/run/mailcow-monitor(root:zabbix,0750), created by systemd. Up to v1.2 it lived in world-writable/var/tmp. monitor.jsonis written0640(collectorumask 0027; unitGroup=zabbix,RuntimeDirectoryMode=0750), so the mailbox / domain / address data it contains is readable only by root and thezabbixservice — not by every local account. This requires thezabbixgroup to exist (standard with zabbix-agent2; adjustGroup=in the unit if yours differs).- The Mailcow database password is passed to
docker execvia a0600env file (collector) or theMYSQL_PWDenvironment (check_*/sync_jobsscripts), never on the command line —/proc/<pid>/cmdlineis world-readable. - The systemd unit is hardened (
NoNewPrivileges,ProtectSystem=full,PrivateTmp,ProtectHome,PrivateDevices,RestrictAddressFamilies, and more). Note: the collector reaches the Docker socket as root, which is the dominant residual risk — the namespace hardening is defense-in-depth, not a barrier against a compromise of the collector itself. - Values from the database never reach a shell: nothing parameterised uses
shell=True.
Installation
1. Get the code
git clone https://github.com/linuser/Mailcow-Zabbix-Monitoring.git
cd Mailcow-Zabbix-Monitoring
Without git:
curl -sL https://github.com/linuser/Mailcow-Zabbix-Monitoring/archive/refs/heads/main.tar.gz | tar xz
cd Mailcow-Zabbix-Monitoring-main
2. Adjust paths if needed
The installer uses two variables at the top of install.sh:
MAILCOW_DIR="/opt/mailcow-dockerized" # Path to your Mailcow installation
BACKUP_PATH="/opt/backup" # Path to your Mailcow backup directory
If your Mailcow is installed elsewhere or your backups are in a different location, edit these before running the installer.
3. Run the installer
sudo ./install.sh
It installs the collector and check scripts, sets up the systemd timer, writes the
UserParameter config — and checks your agent's ServerActive/Hostname. Take that
check seriously: without a correct ServerActive, none of the 246 items will ever
collect data (see Zabbix Agent Configuration).
4. Import the template
-
Data collection → Templates → Import
-
Select
templates/mailcow-complete-monitoring.yaml -
Tick ☑ Create new and ☑ Update existing for all object types. Leave Delete missing off.
-
The template row must then show: Items 246 | Triggers 63 | Dashboards 19
No number next to Triggers means "Create new" was not ticked.
-
Link the template to your Mailcow host: "Mailcow by Zabbix agent 2"
Data appears within about a minute. If it does not, ServerActive is the first
thing to check — not patience.
5. Where to find the dashboards
The 19 dashboards are template dashboards: they belong to the host, not to the global dashboard list.
Monitoring → Hosts → your host's row → Dashboards → "01 - Postfix"
They will not appear under Monitoring → Dashboards, and they render empty under Data collection → Templates → Dashboards — there is no host context there.
Verify
sudo ./test-complete.sh
This queries the UserParameters passively via
zabbix_get. It reports success even whenServerActiveis wrong and Zabbix is collecting nothing. Always confirm in Monitoring → Latest data as well.
Force Immediate Data
systemctl start mailcow-monitor.service # fresh JSON
systemctl restart zabbix-agent2 # force re-check
Update
cd ~/Mailcow-Zabbix-Monitoring
git pull
sudo ./update.sh --check # shows what would change, changes nothing
sudo ./update.sh
update.sh backs up the current scripts to /var/backups/mailcow-zabbix/<date>/,
replaces only what actually differs, clears the RBL cache, restarts the timer and
then verifies the collected values:
OK 246 metrics in the JSON
OK Postfix running: 1
OK Rspamd scanned: 73890
Roll back with sudo ./update.sh --rollback.
Then re-import the template in Zabbix:
- Data collection → Templates → Import
- Select
templates/mailcow-complete-monitoring.yaml - Tick ☑ Create new and ☑ Update existing for all object types. Leave Delete missing off.
- Verify the template row shows: Items 246 | Triggers 63 | Dashboards 19
Do not tick "Update existing" alone. Zabbix then skips objects that do not exist yet — and still reports "Imported successfully". That is how installations ended up with 0 of 71 triggers while items worked fine.
Existing history is preserved: Zabbix matches the template by its UUID (stable), so it updates in place — this holds even when the template's name changes.
If items show "Not supported" after the update:
- Data collection → Hosts → your host → Items
- Filter by "Not supported"
- Select all → Enable
Alternatively, install from a release archive instead of git — see UPDATE.md.
File Structure
mailcow-monitoring/
├── install.sh # Installer
├── uninstall.sh # Uninstaller
├── mailcow-zabbix.conf # 246 UserParameters
├── test-complete.sh # Validation script (246 keys)
├── templates/
│ └── mailcow-complete-monitoring.yaml # Zabbix 7.0 template
├── scripts/
│ ├── mailcow-collector.py # Main collector (22 modules)
│ ├── mailcow-reader.sh # JSON reader
│ ├── check_dns.sh # DNS (SPF/DKIM/DMARC)
│ ├── check_tls.sh # TLS/certificate checks
│ ├── check_rbl.sh # RBL blacklist check
│ ├── check_ptr.sh # PTR record check
│ ├── check_open_relay.sh # Open relay check
│ ├── check_security_audit.sh # DANE/MTA-STS/TLS-RPT/BIMI
│ ├── sync_jobs_check.sh # IMAP sync jobs
│ ├── postfix_stats_docker.sh # Postfix queue stats
│ └── postfix_log_analysis.sh # Postfix logs + Postscreen
├── mailcow-monitor.service # systemd oneshot
├── mailcow-monitor.timer # systemd timer (60s)
├── LICENSE # MIT
├── CHANGELOG.md
├── MAILCOW-MONITORING-DOKU.md # Detailed docs (German)
├── README.md # This file
└── README.de.md # German README
Uninstall
sudo ./uninstall.sh
License
MIT — free to use, modify and redistribute; keep the copyright notice. See LICENSE for details.
© 2026 Alexander Fox | PlaNet Fox — Created with Open Source and ❤