If you’ve been using Icinga 2 for a while, you probably know the built-in checks cover a lot of ground: disk space, CPU, memory, ping. But sooner or later you’ll run into something specific to your setup that no existing plugin handles. That’s where writing your own plugin comes in.
The good news? It’s simpler than it sounds. Icinga 2 doesn’t care what language your plugin is written in. It just runs the script, reads the exit code, and displays the output. That’s it.
In this post we’ll build a real-world example: a plugin that checks whether a fresh backup file exists in a directory. If the newest backup is older than expected, we want to know about it.
What Icinga 2 Expects From a Plugin
Every Icinga 2 plugin follows the same simple contract:
| Exit Code | Meaning |
|---|---|
| 0 | OK |
| 1 | WARNING |
| 2 | CRITICAL |
| 3 | UNKNOWN |
The plugin should print one line of output (optionally followed by | perfdata for graphs) and exit with one of the codes above. That’s the entire interface.
Create the Plugin
We want to check: is there a backup file in a given directory that is newer than X minutes?
Here’s the full PHP plugin:
#!/usr/bin/env php
<?php
const OK = 0, WARNING = 1, CRITICAL = 2, UNKNOWN = 3;
$opts = getopt("d:p:w:c:");
$dir = $opts['d'] ?? null;
$pattern = $opts['p'] ?? '*.sql.gz';
$warning = (int)($opts['w'] ?? 90);
$critical = (int)($opts['c'] ?? 120);
function quit(int $code, string $msg): void {
echo $msg . PHP_EOL;
exit($code);
}
if (!$dir || !is_dir($dir)) {
quit(UNKNOWN, "UNKNOWN - Directory not found: $dir");
}
$files = glob("$dir/$pattern");
if (empty($files)) {
quit(CRITICAL, "CRITICAL - No files matching '$pattern' in $dir");
}
$newest = array_reduce($files, fn($a, $b) => filemtime($b) > filemtime($a) ? $b : $a, $files[0]);
$age = round((time() - filemtime($newest)) / 60);
$name = basename($newest);
$perf = "age={$age}min;{$warning};{$critical};0;";
if ($age >= $critical) {
quit(CRITICAL, "CRITICAL - '$name' is $age min old | $perf");
} elseif ($age >= $warning) {
quit(WARNING, "WARNING - '$name' is $age min old | $perf");
} else {
quit(OK, "OK - '$name' is $age min old | $perf");
}
The script accepts four arguments:
-d: the directory to check (required)-p: file pattern, defaults to*.sql.gz-w: warning threshold in minutes, defaults to 90-c: critical threshold in minutes, defaults to 120
You can test it directly from the command line:
php check_backup_age.php -d /var/backups/database -p "*.sql.gz" -w 90 -c 120 # OK - 'backup-2026-05-11_10-00.sql.gz' is 23 min old | age=23min;90;120;0;
Once it works, copy it to your Icinga 2 plugin directory and make it executable:
cp check_backup_age.php /usr/lib/nagios/plugins/ chmod +x /usr/lib/nagios/plugins/check_backup_age.php
Create the Custom CheckCommand
Now we need to tell Icinga 2 about our plugin. The CheckCommand object maps the script and its arguments into something Icinga 2 can use:
object CheckCommand "check_backup_age" {
command = [ PluginDir + "/check_backup_age.php" ]
arguments = {
"-d" = {
value = "$backup_dir$" // The service parameter would then be defined as 'vars.backup_dir = "/path/to/dir"'
required = true
}
"-p" = {
value = "$backup_pattern$"
required = false
}
"-w" = {
value = "$backup_warning$"
required = false
}
"-c" = {
value = "$backup_critical$"
required = false
}
}
// defaults
vars.backup_pattern = "*.sql.gz"
vars.backup_warning = 90
vars.backup_critical = 120
}
The $backup_dir$ syntax is how Icinga 2 passes variables from the service definition into the command. The default values at the bottom mean you only need to override what’s different per service. You can also define a custom plugin directory as a constant, as described in the docs.
Place this file in /etc/icinga2/conf.d/ or wherever you keep your custom commands.
Create the Host and Service
With the CheckCommand in place, we can now define the actual service:
object Host "your-server" {
display_name = "Your Server"
address = "127.0.0.1"
check_command = "hostalive"
}
object Service "backup-database-hourly" {
host_name = "your-server"
display_name = "Hourly Database Backup"
check_command = "check_backup_age"
check_interval = 30m
retry_interval = 5m
max_check_attempts = 3
vars.backup_dir = "/var/backups/database"
vars.backup_critical = 130 // override the critical
}
The check runs every 30 minutes. If the newest backup is older than 90 minutes, Icinga 2 raises a warning. After 130 minutes it becomes critical. With max_check_attempts = 3, Icinga 2 won’t immediately alert on the first failed check — it tries three times before sending a notification, which avoids false alarms from temporary hiccups.
Finally, save the config, validate and reload Icinga 2 — and you’re ready to go.
icinga2 daemon --validate systemctl reload icinga2
Wrapping Up
Writing a custom Icinga 2 plugin really comes down to three things: a script that prints a status line and exits with the right code, a CheckCommand that describes how to run it, and a Service that wires everything together.
Once you understand this pattern, you can monitor almost anything whatever your infrastructure monitoring setup needs. The built-in plugins are a great starting point, However, if your environment has any specific requirements, write your own plugin and a custom check to cover them.






