Zabbix homelab setup
This document describes the current Zabbix setup used in the homelab, including:
- Zabbix Server deployed with Docker Compose
- External PostgreSQL database hosted on the central PostgreSQL server
- Linux host monitoring using
zabbix-agent2 - Docker monitoring via
zabbix-agent2 - Alert forwarding to n8n using a Zabbix webhook media type
Architecture overview
graph TD A[Linux / Docker Hosts] -->|Active checks TCP/10051| B[Zabbix Server] B -->|SQL| C[Central PostgreSQL Server] B -->|Webhook POST| D[n8n Webhook] D -->|Telegram Bot| E[Telegram] A -->|Optional passive checks TCP/10050| B A -->|Docker socket| F[Docker Engine]
Components
| Component | Purpose |
|---|---|
| Zabbix Server | Main monitoring engine |
| Zabbix Web | Web UI for monitoring/configuration |
| PostgreSQL | External central database |
zabbix-agent2 | Linux host monitoring agent |
| Docker plugin | Container discovery and monitoring through Agent 2 |
| n8n | Alert workflow/notification processing |
| Telegram | Final alert destination |
1. PostgreSQL database setup
The Zabbix database is hosted on the central PostgreSQL server rather than as a Docker container.
The database was created manually through Adminer.
Example database name:
zabbixCreate Zabbix PostgreSQL user
Run the following in Adminer while connected as a PostgreSQL admin user.
Replace the password with a long random value.
CREATE USER zabbix WITH PASSWORD 'CHANGE_THIS_TO_A_LONG_RANDOM_PASSWORD';
GRANT CONNECT ON DATABASE zabbix TO zabbix;
ALTER DATABASE zabbix OWNER TO zabbix;Then connect/select the zabbix database in Adminer and run:
GRANT USAGE, CREATE ON SCHEMA public TO zabbix;
ALTER SCHEMA public OWNER TO zabbix;
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO zabbix;
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO zabbix;
GRANT ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA public TO zabbix;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT ALL PRIVILEGES ON TABLES TO zabbix;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT ALL PRIVILEGES ON SEQUENCES TO zabbix;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT ALL PRIVILEGES ON FUNCTIONS TO zabbix;PostgreSQL access requirements
Ensure the central PostgreSQL server allows connections from the Docker host running Zabbix.
Check:
- PostgreSQL is listening on the required interface
pg_hba.confallows the Zabbix Docker host- Firewall allows PostgreSQL TCP/5432 from the Zabbix host only
Recommended security rule:
Zabbix Docker Host -> PostgreSQL Server TCP/5432Avoid allowing all homelab VLANs to access PostgreSQL.
2. Zabbix server Docker Compose
The Zabbix server uses an external PostgreSQL database, so there is no PostgreSQL container in the Compose stack.
docker-compose.yml
services:
zabbix-server:
image: zabbix/zabbix-server-pgsql:alpine-7.4.12
container_name: zabbix-server
restart: unless-stopped
environment:
DB_SERVER_HOST: ${DB_SERVER_HOST}
DB_SERVER_PORT: ${DB_SERVER_PORT}
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
ZBX_CACHESIZE: 128M
ZBX_HISTORYCACHESIZE: 64M
ZBX_HISTORYINDEXCACHESIZE: 32M
ZBX_TRENDCACHESIZE: 32M
ZBX_VALUECACHESIZE: 128M
ZBX_STARTPINGERS: 5
ZBX_STARTDISCOVERERS: 2
ZBX_STARTHTTPPOLLERS: 5
ports:
- "10051:10051"
networks:
- zabbix
zabbix-web:
image: zabbix/zabbix-web-nginx-pgsql:alpine-7.4.12
container_name: zabbix-web
restart: unless-stopped
depends_on:
- zabbix-server
environment:
DB_SERVER_HOST: ${DB_SERVER_HOST}
DB_SERVER_PORT: ${DB_SERVER_PORT}
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
ZBX_SERVER_HOST: zabbix-server
ZBX_SERVER_PORT: 10051
ZBX_SERVER_NAME: ${ZBX_SERVER_NAME}
PHP_TZ: ${PHP_TZ}
ports:
- "8080:8080"
networks:
- zabbix
networks:
zabbix:
name: zabbix.env
DB_SERVER_HOST=postgres.example.internal
DB_SERVER_PORT=5432
POSTGRES_DB=zabbix
POSTGRES_USER=zabbix
POSTGRES_PASSWORD=CHANGE_THIS_TO_THE_REAL_PASSWORD
ZBX_SERVER_NAME=Homelab Zabbix
PHP_TZ=Europe/LisbonDeploy
docker compose pull
docker compose up -dOpen the web UI:
http://<docker-host-ip>:8080Default login:
User: Admin
Password: zabbixChange the default password immediately.
3. Linux agent installation
For Linux hosts, install zabbix-agent2 directly on the host.
This is preferred over running the agent as a container because it gives better visibility into:
- Host CPU/RAM/disk
- Filesystems and mounts
- Network interfaces
systemdservices- Docker containers, when Docker is present
Ubuntu 24.04 agent installation
Install the Zabbix 7.4 repository package:
wget https://repo.zabbix.com/zabbix/7.4/release/ubuntu/pool/main/z/zabbix-release/zabbix-release_latest_7.4+ubuntu24.04_all.deb
sudo dpkg -i zabbix-release_latest_7.4+ubuntu24.04_all.deb
sudo apt updateInstall Agent 2:
sudo apt install zabbix-agent2Proxmox VE agent installation
For Proxmox hosts based on Debian 12, install the Zabbix 7.4 repository package:
wget https://repo.zabbix.com/zabbix/7.4/release/debian/pool/main/z/zabbix-release/zabbix-release_latest_7.4+debian12_all.deb
sudo dpkg -i zabbix-release_latest_7.4+debian12_all.deb
sudo apt update
sudo apt install zabbix-agent2Then edit the agent configuration:
sudo vi /etc/zabbix/zabbix_agent2.confEnable and restart the service:
sudo systemctl enable --now zabbix-agent2
sudo systemctl restart zabbix-agent2
sudo systemctl status zabbix-agent2Agent configuration
Edit:
sudo vim /etc/zabbix/zabbix_agent2.confSet the following values:
Server=zabbix-server.example.internal
ServerActive=zabbix-server.example.internal
Hostname=uat
HostMetadata=linux,dockerExplanation:
| Setting | Purpose |
|---|---|
Server | Zabbix server allowed to perform passive checks |
ServerActive | Zabbix server used for active checks and auto-registration |
Hostname | Hostname Zabbix will use for this agent |
HostMetadata | Metadata used by Zabbix auto-registration rules |
In this example, the host metadata is:
linux,dockerThis can be matched in Zabbix auto-registration actions to automatically assign groups, templates, and tags.
Enable and restart agent
sudo systemctl enable --now zabbix-agent2
sudo systemctl restart zabbix-agent2Check status:
systemctl status zabbix-agent2Check logs:
sudo journalctl -u zabbix-agent2 -n 100 --no-pagerExpected behaviour:
- Agent starts successfully
- No repeated connection errors to
zabbix-server.example.internal - Host appears in Zabbix via auto-registration once the action is configured
4. Docker monitoring with Agent 2
Zabbix Agent 2 can monitor Docker containers using the Docker plugin.
The agent talks to Docker through:
/var/run/docker.sockTo allow the zabbix user to read Docker data:
sudo usermod -aG docker zabbix
sudo systemctl restart zabbix-agent2Test access:
sudo -u zabbix docker psExpected result:
Docker containers are listedIf permission is denied, Docker monitoring will not work.
Security note
Adding the zabbix user to the docker group gives the user high privileges on the host. In a homelab this may be acceptable, but access to the Zabbix agent should be restricted.
Recommended firewalling:
Linux Host -> Zabbix Server TCP/10051 # active checks
Zabbix Server -> Linux Host TCP/10050 # only if passive checks are usedDo not expose agent port TCP/10050 broadly across VLANs.
5. Zabbix auto-registration
Auto-registration uses active checks and host metadata.
The agent must have:
ServerActive=zabbix-server.example.internal
Hostname=uat
HostMetadata=linux,dockerExample auto-registration matching logic:
| Metadata contains | Action |
|---|---|
linux | Add host to Linux group and link Linux Agent template |
docker | Link Docker by Zabbix Agent 2 template |
Suggested templates:
Linux by Zabbix agent active
Docker by Zabbix agent 26. n8n webhook alerting
Zabbix sends alerts to n8n using a custom webhook media type.
The flow is:
Zabbix Trigger Action -> Zabbix Webhook Media Type -> n8n Webhook -> TelegramCreate n8n webhook
In n8n:
- Create a workflow
- Add a Webhook node
- Method:
POST - Path:
zabbix - Copy the production webhook URL
Example:
https://n8n.example.internal/webhook/zabbixUse the production URL for real alerts, not the test URL.
7. Zabbix media type for n8n
The n8n media type was created by cloning the built-in Telegram media type.
This is useful because the Telegram media type already has good Zabbix event handling logic for:
- Problem events
- Recovery events
- Update events
- Event source validation
- HTTP proxy support
- Message templates
- Zabbix alert tags
Create the media type
In Zabbix UI:
Alerts -> Media typesClone the existing:
TelegramRename the cloned media type to:
n8n WebhookThen remove the Telegram-specific parameters and logic.
Remove parameters such as:
api_token
api_chat_id
api_parse_mode
message_thread_idThe n8n media type only needs to POST the Zabbix alert payload to the n8n webhook URL.
Media type parameters
Use these parameters.
| Name | Value |
|---|---|
webhook_url | https://n8n.example.internal/webhook/zabbix |
alert_subject | {ALERT.SUBJECT} |
alert_message | {ALERT.MESSAGE} |
event_source | {EVENT.SOURCE} |
event_value | {EVENT.VALUE} |
event_update_status | {EVENT.UPDATE.STATUS} |
event_id | {EVENT.ID} |
event_name | {EVENT.NAME} |
event_status | {EVENT.STATUS} |
event_severity | {EVENT.SEVERITY} |
event_nseverity | {EVENT.NSEVERITY} |
event_opdata | {EVENT.OPDATA} |
event_date | {EVENT.DATE} |
event_time | {EVENT.TIME} |
event_duration | {EVENT.DURATION} |
event_update_message | {EVENT.UPDATE.MESSAGE} |
event_recovery_status | {EVENT.RECOVERY.STATUS} |
event_recovery_date | {EVENT.RECOVERY.DATE} |
event_recovery_time | {EVENT.RECOVERY.TIME} |
host_name | {HOST.NAME} |
host_ip | {HOST.IP} |
host_conn | {HOST.CONN} |
trigger_id | {TRIGGER.ID} |
trigger_name | {TRIGGER.NAME} |
trigger_url | {TRIGGER.URL} |
zabbix_url | https://zabbix.yourdomain.local |
http_proxy | leave empty |
Message templates
The message templates are important.
Without message templates, n8n may receive incomplete alert data such as:
Zabbix EVENT
Severity: N/A
Problem: No event name received
Event ID: N/AAdd these templates under:
Alerts -> Media types -> n8n Webhook -> Message templatesProblem template
Type:
ProblemSubject:
Problem: {EVENT.NAME}Message:
Problem started at {EVENT.TIME} on {EVENT.DATE}
Problem name: {EVENT.NAME}
Host: {HOST.NAME}
Severity: {EVENT.SEVERITY}
Operational data: {EVENT.OPDATA}
Original problem ID: {EVENT.ID}Example Telegram output from a real Zabbix alert:
Problem: Linux: uat01 has been restarted (uptime < 10m)
Problem started at 15:16:10 on 2026.07.26
Problem name: Linux: uat01 has been restarted (uptime < 10m)
Host: uat01
Severity: Warning
Operational data: 00:01:18
Original problem ID: 257Problem recovery template
Type:
Problem recoverySubject:
Resolved: {EVENT.NAME}Message:
Problem resolved at {EVENT.RECOVERY.TIME} on {EVENT.RECOVERY.DATE}
Problem name: {EVENT.NAME}
Host: {HOST.NAME}
Original severity: {EVENT.SEVERITY}
Duration: {EVENT.DURATION}
Original problem ID: {EVENT.ID}Problem update template
Type:
Problem updateSubject:
Update: {EVENT.NAME}Message:
Problem updated at {EVENT.UPDATE.TIME} on {EVENT.UPDATE.DATE}
Problem name: {EVENT.NAME}
Host: {HOST.NAME}
Update message: {EVENT.UPDATE.MESSAGE}
Updated by: {USER.FULLNAME}
Original problem ID: {EVENT.ID}Webhook script
The cloned Telegram script can be simplified to send a JSON payload to n8n instead of sending a Telegram message directly.
Use this JavaScript in the Zabbix webhook media type:
const CLogger = function(serviceName) {
this.serviceName = serviceName;
this.INFO = 4;
this.WARN = 3;
this.ERROR = 2;
this.log = function(level, msg) {
Zabbix.log(level, '[' + this.serviceName + '] ' + msg);
};
};
const CWebhook = function(value) {
try {
params = JSON.parse(value);
if (['0', '1', '2', '3', '4'].indexOf(params.event_source) === -1) {
throw 'Incorrect "event_source" parameter given: ' + params.event_source + '. Must be 0-4.';
}
if (['0', '3', '4'].indexOf(params.event_source) !== -1 && ['0', '1'].indexOf(params.event_value) === -1) {
throw 'Incorrect "event_value" parameter given: ' + params.event_value + '. Must be 0 or 1.';
}
if (['0', '3', '4'].indexOf(params.event_source) !== -1) {
if (params.event_source === '0' && ['0', '1'].indexOf(params.event_update_status) === -1) {
throw 'Incorrect "event_update_status" parameter given: ' + params.event_update_status + '. Must be 0 or 1.';
}
if (params.event_source === '4') {
if (['0', '1', '2', '3', '4', '5'].indexOf(params.event_update_nseverity) !== -1
&& params.event_update_nseverity != params.event_nseverity) {
params.event_nseverity = params.event_update_nseverity;
params.event_severity = params.event_update_severity;
params.event_update_status = '1';
}
}
}
this.runCallback = function(name, params) {
if (typeof this[name] === 'function') {
return this[name].apply(this, [params]);
}
};
this.handleEvent = function(source, event) {
const alert = { source: source, event: event };
return [
this.runCallback('on' + source + event, alert),
this.runCallback('on' + event, alert),
this.runCallback('onEvent', alert)
];
};
this.handleEventless = function(source) {
const alert = { source: source, event: null };
return [
this.runCallback('on' + source, alert),
this.runCallback('onEvent', alert)
];
};
this.run = function() {
var results = [];
if (typeof this.httpProxy === 'string' && this.httpProxy.trim() !== '') {
this.request.setProxy(this.httpProxy);
}
const types = {
'0': 'Trigger',
'1': 'Discovery',
'2': 'Autoreg',
'3': 'Internal',
'4': 'Service'
};
if (['0', '3', '4'].indexOf(this.params.event_source) !== -1) {
var event = (this.params.event_update_status === '1')
? 'Update'
: ((this.params.event_value === '1') ? 'Problem' : 'Resolve');
results = this.handleEvent(types[this.params.event_source], event);
}
else if (typeof types[this.params.event_source] !== 'undefined') {
results = this.handleEventless(types[this.params.event_source]);
}
else {
throw 'Unexpected "event_source": ' + this.params.event_source;
}
for (idx in results) {
if (typeof results[idx] !== 'undefined') {
return JSON.stringify(results[idx]);
}
}
};
this.httpProxy = params.http_proxy;
this.params = params;
this.runCallback('onCheckParams', {});
}
catch (error) {
throw 'Webhook processing failed: ' + error;
}
};
const CParamValidator = {
isType: function(value, type) {
if (type === 'array') {
return Array.isArray(value);
}
return (typeof value === type);
},
isDefined: function(value) {
return !CParamValidator.isType(value, 'undefined');
},
isEmpty: function(value) {
if (!CParamValidator.isType(value, 'string')) {
throw 'Value "' + value + '" must be a string to be checked for emptiness.';
}
return (value.trim() === '');
},
ifMatch: function(value, regex) {
return (new RegExp(regex)).test(value);
},
checkURL: function(value) {
if (CParamValidator.isEmpty(value)) {
throw 'URL value must be a non-empty string.';
}
if (!CParamValidator.ifMatch(value, '^(http|https):\\/\\/.+')) {
throw 'URL value must contain a schema.';
}
return value;
},
check: function(key, rule, params) {
if (!CParamValidator.isDefined(params[key])) {
throw 'Required parameter "' + key + '" was not found.';
}
var value = params[key];
switch (rule.type) {
case 'string':
if (!CParamValidator.isType(value, 'string')) {
throw 'Value "' + key + '" must be a string.';
}
if (rule.required === true && CParamValidator.isEmpty(value)) {
throw 'Value "' + key + '" must be a non-empty string.';
}
if (rule.url === true) {
value = CParamValidator.checkURL(value);
}
break;
default:
throw 'Unexpected validation type "' + rule.type + '" for "' + key + '".';
}
params[key] = value;
return this;
},
validate: function(rules, params) {
if (!CParamValidator.isType(params, 'object') || CParamValidator.isType(params, 'array')) {
throw 'Incorrect parameters value. The value must be an object.';
}
for (var key in rules) {
CParamValidator.check(key, rules[key], params);
}
}
};
const CHttpRequest = function(logger) {
this.request = new HttpRequest();
this.logger = (typeof logger === 'object' && logger !== null) ? logger : Zabbix;
this.addHeaders = function(value) {
var headers = [];
if (typeof value === 'object' && value !== null) {
if (!Array.isArray(value)) {
Object.keys(value).forEach(function(key) {
headers.push(key + ': ' + value[key]);
});
}
else {
headers = value;
}
}
else if (typeof value === 'string') {
value.split('\r\n').forEach(function(header) {
headers.push(header);
});
}
for (var idx in headers) {
this.request.addHeader(headers[idx]);
}
};
this.setProxy = function(proxy) {
this.request.setProxy(proxy);
};
this.plainRequest = function(method, url, data) {
var resp = null;
method = method.toLowerCase();
this.logger.log(4, 'Sending ' + method + ' request to: ' + url);
this.logger.log(4, 'Payload: ' + JSON.stringify(data));
if (['get', 'post', 'put', 'patch', 'delete', 'trace'].indexOf(method) !== -1) {
resp = this.request[method](url, data);
}
else if (['connect', 'head', 'options'].indexOf(method) !== -1) {
resp = this.request[method](url);
}
else {
throw 'Unexpected method. Method ' + method + ' is not supported.';
}
this.logger.log(4, 'HTTP status: ' + this.request.getStatus());
this.logger.log(4, 'Response: ' + resp);
return resp;
};
this.jsonRequest = function(method, url, data) {
this.addHeaders('Content-Type: application/json');
return this.plainRequest(method, url, JSON.stringify(data));
};
this.getStatus = function() {
return this.request.getStatus();
};
};
var serviceLogName = 'n8n Webhook',
Logger = new CLogger(serviceLogName),
N8N = CWebhook;
N8N.prototype.onCheckParams = function () {
CParamValidator.validate(
{
webhook_url: {type: 'string', required: true, url: true},
alert_subject: {type: 'string', required: false},
alert_message: {type: 'string', required: false},
event_source: {type: 'string', required: true},
event_value: {type: 'string', required: true},
event_update_status: {type: 'string', required: false}
},
this.params
);
this.params.webhook_url = CParamValidator.checkURL(this.params.webhook_url);
};
N8N.prototype.buildPayload = function(alert) {
return {
source: 'zabbix',
alert: {
source: alert.source,
event: alert.event
},
status: alert.event,
subject: this.params.alert_subject,
message: this.params.alert_message,
event: {
id: this.params.event_id,
name: this.params.event_name,
status: this.params.event_status,
value: this.params.event_value,
source: this.params.event_source,
severity: this.params.event_severity,
nseverity: this.params.event_nseverity,
opdata: this.params.event_opdata,
date: this.params.event_date,
time: this.params.event_time,
duration: this.params.event_duration,
update_status: this.params.event_update_status,
update_message: this.params.event_update_message,
recovery_status: this.params.event_recovery_status,
recovery_date: this.params.event_recovery_date,
recovery_time: this.params.event_recovery_time
},
host: {
name: this.params.host_name,
ip: this.params.host_ip,
conn: this.params.host_conn
},
trigger: {
id: this.params.trigger_id,
name: this.params.trigger_name,
url: this.params.trigger_url
},
zabbix: {
url: this.params.zabbix_url
}
};
};
N8N.prototype.onEvent = function(alert) {
Logger.log(Logger.INFO, 'Source: ' + alert.source + '; Event: ' + alert.event);
Logger.log(Logger.INFO, 'Webhook URL: ' + this.params.webhook_url);
var payload = this.buildPayload(alert);
var response = this.request.jsonRequest('POST', this.params.webhook_url, payload);
var status = this.request.getStatus();
if (status < 200 || status >= 300) {
Logger.log(Logger.WARN, 'HTTP code: ' + status);
throw 'n8n webhook failed with HTTP status ' + status + ': ' + response;
}
return {
tags: {
'__n8n_last_status': String(status)
}
};
};
try {
var hook = new N8N(value);
hook.request = new CHttpRequest(Logger);
return hook.run();
}
catch (error) {
Logger.log(Logger.WARN, 'notification failed: ' + error);
throw 'Sending failed: ' + error;
}8. Enable alert delivery in Zabbix
For Zabbix to actually send alerts to n8n, three things must be enabled:
- The n8n webhook media type must be enabled.
- The Zabbix user must have the n8n media type assigned and enabled.
- A trigger action must be enabled to send messages to that user/media type.
Enable the media type
In Zabbix UI:
Alerts -> Media typesOpen the cloned n8n media type and make sure it is enabled.
Example:
Name: n8n Webhook
Type: Webhook
Status: EnabledIf the media type is disabled, the action can match but no notification will be sent.
Add media to Zabbix user
Create a dedicated Zabbix user for n8n alerts or use an existing alerting user.
Example user:
n8n-alertsThen add the media type to that user:
Users -> Users -> n8n-alerts -> MediaMedia settings:
Type: n8n Webhook
Send to: n8n
When active: 1-7,00:00-24:00
Use if severity: select desired severities
Enabled: yesThe Send to value is mandatory in Zabbix, but for this custom webhook it can be a dummy value such as:
n8nThe real destination is controlled by the media type parameter:
webhook_urlEnable trigger action
In Zabbix UI:
Alerts -> Actions -> Trigger actionsMake sure this built-in action is enabled:
Report problems to Zabbix administratorsThis is important because Zabbix will not send problem notifications unless a trigger action matches the event and sends a message to a user/media type.
For the homelab n8n setup, either update the built-in action to send to the n8n alert user/media type, or create a dedicated action.
9. Create dedicated trigger action for n8n
A dedicated action keeps the n8n alert flow separate from the default administrator notifications.
In Zabbix UI:
Alerts -> Actions -> Trigger actionsCreate an action:
Name: Send problems to n8n
Status: EnabledSuggested condition:
Trigger severity >= WarningOperations
Send message
Send to users: n8n-alerts
Send only to: n8n WebhookRecovery operations
Also configure recovery operations so n8n receives resolved events:
Recovery operations -> Send message
Send to users: n8n-alerts
Send only to: n8n WebhookUpdate operations
Also configure update operations if manual acknowledgement/update comments should be sent to n8n:
Update operations -> Send message
Send to users: n8n-alerts
Send only to: n8n WebhookQuick alert checklist
If Telegram/n8n does not receive alerts, check these first:
Alerts -> Media types -> n8n Webhook -> Enabled
Users -> Users -> n8n-alerts -> Media -> Enabled
Alerts -> Actions -> Trigger actions -> Report problems to Zabbix administrators -> Enabled
Alerts -> Actions -> Trigger actions -> Send problems to n8n -> Enabled10. n8n Telegram message
The n8n workflow receives the Zabbix payload under:
$json.bodySince Zabbix already formats the alert subject and message through the media type message templates, the Telegram node can stay simple.
Example Telegram message:
{{ $json.body.subject }}
{{ $json.body.message }}
This message was sent automatically with n8nThis keeps formatting in Zabbix and leaves n8n responsible only for routing and delivery.
11. Troubleshooting
Zabbix webhook error: Could not resolve host: undefined
This means the webhook script tried to call an undefined URL.
Most likely cause:
The media type parameter webhook_url is missing or misspelled.Fix:
Alerts -> Media types -> n8n Webhook -> ParametersEnsure there is a parameter named exactly:
webhook_urlLowercase.
n8n receives unresolved macros
Example:
{
"host": {
"name": "{HOST.NAME}",
"ip": "{HOST.IP}"
}
}This can happen when using the Zabbix media type test because there is no real trigger event context.
To properly test:
- Trigger a real Zabbix problem
- Let the Trigger Action send the alert
- Check the payload received in n8n
Agent does not auto-register
Check on the Linux host:
sudo journalctl -u zabbix-agent2 -n 100 --no-pagerVerify:
ServerActive=zabbix-server.example.internal
Hostname=uat
HostMetadata=linux,dockerAlso confirm the Linux host can reach the Zabbix server:
nc -vz zabbix-server.example.internal 1005112. Security considerations
Network access
Allow only required traffic:
| Source | Destination | Port | Purpose |
|---|---|---|---|
| Linux hosts | Zabbix server | TCP/10051 | Active checks / auto-registration |
| Zabbix server | Linux hosts | TCP/10050 | Passive checks, if used |
| Zabbix server | PostgreSQL server | TCP/5432 | Database access |
| Zabbix server | n8n | HTTPS/443 | Webhook alerts |
Secrets
Do not commit real secrets to Git:
- PostgreSQL password
- n8n webhook URL, if considered sensitive
- Telegram bot token
Use CI/CD secrets or host-local .env files.
Docker monitoring
The zabbix user being added to the docker group should be treated as privileged access.
Only enable Docker monitoring on hosts where it is needed.
13. Future improvements
Potential improvements:
- Manage
zabbix-agent2installation with Ansible - Template
zabbix_agent2.confusing host variables - Use Zabbix API, Terraform, or Ansible for host groups, templates, and auto-registration actions
- Add Proxmox monitoring using the
Proxmox VE by HTTPtemplate - Add TLS/reverse proxy in front of Zabbix Web
- Add database backups for the Zabbix PostgreSQL database
- Consider TimescaleDB later if history volume grows significantly