🚨 Nagios Core Guide

A how-to manual: install, where the files are, hosts and services, plugins, soft and hard states, NRPE, notifications, acknowledge and downtime, passive checks, the web interface and its security, all tested on a lab

Video: Nagios Core (sysadmin video)
Prefer to watch? This video runs the guide's main commands on a real Ubuntu 24.04 machine, with on-screen captions and no voiceover. Click to play, or watch it on YouTube. It is part of the Troubleshooting and Performance playlist.
Nagios Core: the monitoring server that checks things and tells you
sudo apt install nagios4 apache2 monitoring-plugins nagios-nrpe-plugin
sudo nagios4 -v /etc/nagios4/nagios.cfg

Nagios Core runs small programs called plugins on a schedule, reads the exit code of each one, and turns the result into a state (OK, WARNING, CRITICAL, UNKNOWN). When a state changes and stays changed, it tells the right people. That is the whole idea; the rest is configuration. It does not draw pretty graphs on its own and it does not discover your network. You tell it what exists (hosts), what to check on each (services), who to tell (contacts) and when (time periods), in plain text files.

Every example below ran for real: Nagios Core 4.4.6 (the nagios4 package of Ubuntu 24.04), monitoring-plugins 2.3.5, NRPE 4.1.0, Apache 2.4.58, in two Docker containers on a private network: "nag" (the Nagios server) and "web1" (a web server with nginx, sshd and the NRPE agent). A third host, web2, was pointed at an address that does not exist, to see what a dead host looks like. A container has no systemd, so the lab started the daemons by hand; commands that need systemd are marked "not run in the lab". Times were shortened (one-minute check intervals, three attempts) so that outages played out in seconds; the shipped defaults are slower, and each section says so.

How Nagios thinks: hosts, services, plugins and states

Four things to learn first.

  host       a machine or device with an address: web1, a switch, a printer
  service    one thing checked on a host: PING, HTTP, SSH, "Root disk". A service always belongs to a host
  plugin     a small program that does one check, prints one line, and exits with a code
  contact    a person (or a group) who is told when something changes

The exit code of the plugin is the state:

  exit 0   OK
  exit 1   WARNING
  exit 2   CRITICAL
  exit 3   UNKNOWN      (the plugin could not tell: bad arguments, timeout, cannot connect to the thing that holds the answer)

Hosts have their own states: UP, DOWN and UNREACHABLE (down because something on the path to it is down).

A state is also SOFT or HARD. A problem is SOFT when it was seen but is not yet confirmed (attempt 1 of 3, 2 of 3). It becomes HARD when it has been seen the full number of attempts in a row (max_check_attempts). Only HARD changes send notifications. This is what keeps a single lost packet from waking you. The lab saw exactly that on web1 (details in "Soft and hard states").

Install on Ubuntu 24.04

Four packages cover a working server with a web interface and remote checks:

$ sudo apt install nagios4 apache2 monitoring-plugins nagios-nrpe-plugin
$ nagios4 --version | head -2
Nagios Core 4.4.6
Copyright (c) 2009-present Nagios Core Development Team and Community Contributors

  nagios4              4.4.6-4ubuntu0.24.04.1     the daemon, the web pages (CGI programs) and the default configuration
  apache2              2.4.58                     the web server for the pages
  monitoring-plugins   2.3.5-1ubuntu3             the plugins (the project was renamed from "nagios-plugins")
  nagios-nrpe-plugin   4.1.0-1ubuntu3             check_nrpe, to run checks on other machines

Ubuntu 24.04 ships Nagios Core 4.4.6. The upstream project has a newer release; installing it means building from source, which was not run here. The packaged version is the one this guide describes.

Two things the lab needed that you should check on a real machine:

1. The web pages are CGI programs, and Apache must have mod_cgi enabled. In the lab it was not, and the pages did not run: Apache sent the programs as file downloads, a screen of binary garbage. The fix is one command:

$ sudo a2enmod cgi
$ sudo systemctl restart apache2        (not run in the lab: no systemd in a container)

2. The daemon must be running. On a normal server the package installs a systemd unit: systemctl status nagios4 shows it (not run in the lab, where the daemon was started by hand with nagios4 -d /etc/nagios4/nagios.cfg).

Before you start, check the configuration. The packaged one is valid out of the box:

$ nagios4 -v /etc/nagios4/nagios.cfg
Checking objects...
        Checked 8 services.
        Checked 1 hosts.
        Checked 1 host groups.
        Checked 0 service groups.
        Checked 1 contacts.
        Checked 1 contact groups.
        Checked 180 commands.
        Checked 5 time periods.
...
Total Warnings: 0
Total Errors:   0

Things look okay - No serious problems were detected during the pre-flight check

The web interface is at http://SERVER/nagios4/. Out of the box it is open to private networks only and has no login (see "The web interface and its security" before you change that).

Where everything lives

The Debian and Ubuntu package is laid out like this (the upstream tarball uses /usr/local/nagios instead):

  /etc/nagios4/nagios.cfg           the main file: which other files to read, where the logs and status files go, global switches
  /etc/nagios4/conf.d/              your own object files: put every new host and service here (it starts empty)
  /etc/nagios4/objects/             the shipped definitions: commands.cfg, contacts.cfg, templates.cfg, timeperiods.cfg, localhost.cfg and some examples (printer, switch, windows)
  /etc/nagios4/cgi.cfg              settings for the web pages, including the login switch
  /etc/nagios4/htdigest.users       the web users (digest authentication)
  /etc/nagios4/resource.cfg         $USER1$ and friends, the macros for paths and secrets: $USER1$=/usr/lib/nagios/plugins
  /etc/nagios-plugins/config/       one command file per plugin family (ping, http, nrpe...), read by the main file
  /usr/lib/nagios/plugins/          the plugins themselves
  /etc/apache2/conf-available/nagios4-cgi.conf   the Apache side
  /var/log/nagios4/nagios.log       the log: every state change, notification and command
  /var/lib/nagios4/status.dat       the current state of everything (rewritten every 10 seconds)
  /var/lib/nagios4/retention.dat    state kept across restarts
  /var/lib/nagios4/rw/nagios.cmd    the command file (a named pipe): how you give Nagios orders from outside

The main file is a list of includes. These are the lines that matter in the shipped nagios.cfg:

cfg_dir=/etc/nagios-plugins/config
cfg_dir=/etc/nagios4/conf.d
cfg_file=/etc/nagios4/objects/commands.cfg
cfg_file=/etc/nagios4/objects/contacts.cfg
cfg_file=/etc/nagios4/objects/timeperiods.cfg
cfg_file=/etc/nagios4/objects/templates.cfg
cfg_file=/etc/nagios4/objects/localhost.cfg

cfg_dir reads every .cfg file in the folder, so adding a monitored host is "drop a file into conf.d". Keep your files there and leave the shipped ones alone.

The packaged configuration already monitors the server itself (localhost) with eight services: PING, Root Partition, Current Users, Total Processes, Current Load, Swap Usage, SSH and HTTP.

Check the config, and reload safely

Nagios reads its configuration at start and when told to reload. A mistake does not stop at a warning. Always run the pre-flight check first:

$ sudo nagios4 -v /etc/nagios4/nagios.cfg
...
Total Warnings: 0
Total Errors:   0

Then reload (the daemon re-reads everything without losing state):

$ sudo systemctl reload nagios4        (not run in the lab; the lab sent the signal directly: kill -HUP PID)

Why the order matters, as it happened in the lab: a reload was sent with a broken file without checking first. The daemon did not keep running on the old configuration. It stopped:

Error: Service check command 'check_dummy!3!No backup report received yet' specified in service 'Nightly backup' for host 'web1' not defined anywhere!
Bailing out due to errors encountered while running the pre-flight check.  Run Nagios from the command line with the -v option to verify your config before restarting.

and from that moment nothing was monitored, nothing alerted, and the web pages went silent until it was started again. So: validate, then reload, in one line you can paste or script:

nagios4 -v /etc/nagios4/nagios.cfg && sudo systemctl reload nagios4

The lab used this small script for the same idea (it only signals when the check is clean):

#!/bin/bash
nagios4 -v /etc/nagios4/nagios.cfg >/tmp/preflight.txt 2>&1 || { grep -E "Error|Total" /tmp/preflight.txt; echo "NOT reloading"; exit 1; }
kill -HUP "$(cat /var/run/nagios4/nagios4.pid)" && echo "reload signal sent"

What the errors look like. Real ones from the lab, each from a small deliberate mistake:

a service on a host that is not defined (typo in host_name):
Error: Could not find any host matching 'web9' (config file '/etc/nagios4/conf.d/bad.cfg', starting on line 1)
Error: Failed to expand host list 'web9' for service 'Typo host' (/etc/nagios4/conf.d/bad.cfg:1)

a template name that does not exist:
Error: Template 'no-such-template' specified in service definition could not be not found (config file '/etc/nagios4/conf.d/bad.cfg', starting on line 8)

a misspelt directive (addres instead of address):
Error: Invalid host object directive 'addres'.
Error: Could not add object property in file '/etc/nagios4/conf.d/bad.cfg' on line 19.
Error: Failed to locate notification_period '24x7' for host 'web1'!
Error: Could not register host (config file '/etc/nagios4/conf.d/web1.cfg', starting on line 1)

Read the first error and fix that one. The last two lines of the third case are not about web1: once a file fails to parse, Nagios cannot finish reading the objects, and the checks that follow blame innocent files. It also stops at parse errors before it looks at references, so the typo in host_name in the same file was not reported until the directive was fixed. Fix, validate, repeat.

A name rule from the lab: service descriptions cannot contain some characters. "Load (NRPE)" was refused:

Error: The description string for service 'Load (NRPE)' on host 'web1' contains one or more illegal characters.

"Load" was accepted. Keep names to letters, digits, spaces, dashes and underscores.

Plugins: run them by hand first

A plugin is an ordinary program. You can run it in a shell, look at the output and the exit code, and tune the thresholds before you give it to Nagios. This is the best habit in monitoring: if it does not work by hand, it will not work from Nagios either.

$ P=/usr/lib/nagios/plugins
$ $P/check_http -H 172.21.0.3 ; echo "exit=$?"
HTTP OK: HTTP/1.1 200 OK - 857 bytes in 0.001 second response time |time=0.000933s;;;0.000000;10.000000 size=857B;;;0;
exit=0
$ $P/check_http -H 172.21.0.3 -u /nothing.html ; echo "exit=$?"
HTTP WARNING: HTTP/1.1 404 Not Found - 321 bytes in 0.001 second response time |time=0.000823s;;;0.000000;10.000000 size=321B;;;0;
exit=1
$ $P/check_tcp -H 172.21.0.3 -p 5666 ; echo "exit=$?"
TCP OK - 0.000 second response time on 172.21.0.3 port 5666|time=0.000177s;;;0.000000;10.000000
exit=0
$ $P/check_ping -H 172.21.0.3 -w 100,20% -c 500,60% -p 3 ; echo "exit=$?"
PING OK - Packet loss = 0%, RTA = 0.12 ms|rta=0.123000ms;100.000000;500.000000;0.000000 pl=0%;20;60;0;
exit=0
$ $P/check_ssh 172.21.0.3
SSH OK - OpenSSH_9.6p1 Ubuntu-3ubuntu13.20 (protocol 2.0) | time=0.016762s;;;0.000000;10.000000

The output has two parts, split at the | sign. Before it, the text people read. After it, the performance data: name=value;warn;crit;min;max, which graphing tools read. Nagios shows the first part and keeps the second.

Thresholds: -w is the warning level and -c the critical level. The simple form is "alert when the value is above this":

$ $P/check_procs -C apache2 -w 10 -c 20 ; echo "exit=$?"
PROCS OK: 6 processes with command name 'apache2' | procs=6;10;20;0;
exit=0

Ranges say more. The syntax is the same in all plugins that follow the standard (the lab ran them on check_procs, which saw 6 apache2 processes):

  -w 10            alert if the value is outside 0 to 10 (above 10)
  -w 1:2           alert if the value is outside 1 to 2 (too few or too many)
  -w 1:            alert if the value is below 1
  -w '~:3'         alert if the value is above 3 (the ~ means minus infinity)
  -w @1:10         alert if the value is INSIDE 1 to 10 (the @ turns the range around)

$ $P/check_procs -C apache2 -w 1:2 -c 1:3 ; echo "exit=$?"
PROCS CRITICAL: 6 processes with command name 'apache2' | procs=6;1:2;1:3;0;
exit=2
$ $P/check_procs -C apache2 -w @1:10 -c 20 ; echo "exit=$?"
PROCS WARNING: 6 processes with command name 'apache2' | procs=6;@1:10;20;0;
exit=1

Gotcha: write '~:3' in quotes. The shell turns an unquoted ~ into your home directory, and the lab saw exactly that: -w ~:3 reached the plugin as /root:3 and was reported as procs=6;/root:3;/root:4;0.

Gotcha for disks: check_disk thresholds are about FREE space, not used space. On a disk that is 77 percent free:

$ $P/check_disk -w 20% -c 10% -p / ; echo "exit=$?"
DISK OK - free space: / 685143MiB (77% inode=97%);| /=213844492288B;785791949209;884015942860;0;982239936512
exit=0
$ $P/check_disk -w 99% -c 98% -p / ; echo "exit=$?"
DISK CRITICAL - free space: / 685143MiB (77% inode=97%);| /=213844492288B;9822399365;19644798730;0;982239936512
exit=2

A plugin of your own can be any program: print one line, exit 0, 1, 2 or 3. check_dummy returns whatever you tell it, which is handy for testing:

$ $P/check_dummy 3 "plugin exit codes" ; echo "exit=$?"
UNKNOWN: plugin exit codes
exit=3

Each plugin has --help with the whole list of options.

Defining hosts and services

Objects are written as define blocks. A minimal monitored web server is one host and a few services. This is the file the lab used (/etc/nagios4/conf.d/web1.cfg, shortened intervals as noted):

define host {
    use                 linux-server
    host_name           web1
    alias               Web server 1
    address             172.21.0.3
    hostgroups          web-servers
    check_interval      1               ; the template says 5
    max_check_attempts  3               ; the template says 10
    notification_period 24x7            ; the template says workhours
}

define hostgroup {
    hostgroup_name  web-servers
    alias           Web servers
}

define service {
    use                   local-service
    host_name             web1
    service_description   HTTP
    check_command         check_http
    check_interval        1
    max_check_attempts    3
}

define service {
    use                   local-service
    host_name             web1
    service_description   SSH
    check_command         check_ssh
    check_interval        1
    max_check_attempts    3
}

Reading it:
  use          inherit everything from a template (next paragraph), then override what you list
  check_command  the name of a command object. check_http and check_ssh come from the plugin command files in /etc/nagios-plugins/config
  ; text       a comment

Templates carry the defaults, so each host and service stays short. The shipped ones, in objects/templates.cfg, with the values that matter:

  linux-server     check every 5 min, retry every 1 min, max_check_attempts 10, notifications only in "workhours", contact group admins
  local-service    check every 5 min, retry every 1 min, max_check_attempts 4
  generic-contact  notifies by email, all states, 24x7

Read those numbers: a host that stops answering is only declared DOWN (HARD) after 10 failed checks one minute apart, so about ten minutes, and a linux-server host sends no notification outside Monday to Friday 9 to 5 (workhours: monday to friday 09:00-17:00, no weekends). Plenty of sites are surprised by one or the other. Change them for what you want, per host or in your own template.

Groups save repeating yourself. A service attached to a host group applies to every member, present and future:

define service {
    use                   local-service
    hostgroup_name        web-servers
    service_description   Web response
    check_command         check_http!-w 1 -c 3
}

check_http!-w 1 -c 3 passes the text after the ! as $ARG1$ to the command definition. In the lab, adding the host web2 to the group web-servers gave it the Web response service with no extra lines. A service group collects services (for the web page and for reports):

define servicegroup {
    servicegroup_name   web-checks
    alias               Web checks
    members             web1,HTTP,web1,Web response     ; host,service pairs
}

Commands connect a name to a plugin line. Your own are one definition:

define command {
    command_name    check_dummy_msg
    command_line    $USER1$/check_dummy $ARG1$ "$ARG2$"
}

and a service uses it as check_command check_dummy_msg!3!No backup report received yet. $USER1$ is /usr/lib/nagios/plugins from resource.cfg. Other macros, such as $HOSTADDRESS$, are filled in by Nagios for each host.

Soft and hard states, in real time

This is what the log records when something breaks, with check_interval 1 and max_check_attempts 3 (the times are converted from the log's seconds-since-1970 to hh:mm:ss, UTC). nginx was stopped on web1 and the HTTP check was run three times (the lab used forced checks to make it quicker; on its own, Nagios does the same with the retry interval):

$ sudo nginx -s stop            (on web1)
15:26:44 SERVICE ALERT: web1;HTTP;CRITICAL;SOFT;1;connect to address 172.21.0.3 and port 80: Connection refused
15:26:48 SERVICE ALERT: web1;HTTP;CRITICAL;SOFT;2;connect to address 172.21.0.3 and port 80: Connection refused
15:26:52 SERVICE NOTIFICATION: nagiosadmin;web1;HTTP;CRITICAL;notify-service-by-email;connect to address 172.21.0.3 and port 80: Connection refused
15:26:52 SERVICE ALERT: web1;HTTP;CRITICAL;HARD;3;connect to address 172.21.0.3 and port 80: Connection refused

SOFT 1, SOFT 2: seen, not confirmed, nobody told. HARD 3: confirmed, and the notification is sent in the same second. When the service comes back:

$ sudo nginx                    (on web1)
15:27:34 SERVICE NOTIFICATION: nagiosadmin;web1;HTTP;OK;notify-service-by-email;HTTP OK: HTTP/1.1 200 OK - 857 bytes in 0.001 second response time
15:27:34 SERVICE ALERT: web1;HTTP;OK;HARD;3;HTTP OK: HTTP/1.1 200 OK - 857 bytes in 0.001 second response time

A host works the same way. web2 pointed at an address nobody uses:

15:30:12 HOST ALERT: web2;DOWN;SOFT;1;CRITICAL - Host Unreachable (172.21.0.99)
15:30:15 HOST ALERT: web2;DOWN;SOFT;2;CRITICAL - Host Unreachable (172.21.0.99)
15:30:21 HOST NOTIFICATION: nagiosadmin;web2;DOWN;notify-host-by-email;CRITICAL - Host Unreachable (172.21.0.99)
15:30:21 HOST ALERT: web2;DOWN;HARD;3;CRITICAL - Host Unreachable (172.21.0.99)

The time to a HARD state is roughly (max_check_attempts - 1) x retry_interval after the first failure. With the shipped defaults (10 and 1 minute for a host) a dead server is only announced about nine minutes after the first failed check. Choose the numbers on purpose: fewer attempts for critical servers, more for flaky links.

Flapping. When a state keeps switching, Nagios stops treating each change as news. The lab started and stopped nginx quickly and the log said:

15:28:10 SERVICE FLAPPING ALERT: web1;HTTP;STARTED; Service appears to have started flapping (24.1% change >= 20.0% threshold)

Notifications are held while it flaps. If you see this on a real service, the thing is unstable or the check is too sensitive.

Checking other machines with NRPE

The plugins on the Nagios server can reach network services (HTTP, SSH, ping, TCP). To see inside another machine (load, disk, processes), run the plugin there and ask for the result. NRPE (Nagios Remote Plugin Executor) is that bridge: an agent (nrpe) on the target, and check_nrpe on the server.

Target machine (web1):

$ sudo apt install nagios-nrpe-server monitoring-plugins-basic monitoring-plugins-standard

Edit /etc/nagios/nrpe.cfg and allow the Nagios server (comma separated, localhost first):

allowed_hosts=127.0.0.1,::1,172.21.0.2

The agent ships with a few commands defined in the same file, each one a plugin line with fixed arguments:

command[check_users]=/usr/lib/nagios/plugins/check_users -w 5 -c 10
command[check_load]=/usr/lib/nagios/plugins/check_load -r -w .15,.10,.05 -c .30,.25,.20
command[check_hda1]=/usr/lib/nagios/plugins/check_disk -w 20% -c 10% -p /dev/hda1
command[check_zombie_procs]=/usr/lib/nagios/plugins/check_procs -w 5 -c 10 -s Z
command[check_total_procs]=/usr/lib/nagios/plugins/check_procs -w 150 -c 200

Look at check_load: those thresholds (.15 scaled) are so low that a perfectly quiet machine reports WARNING. The lab did, in its first minute:

$ /usr/lib/nagios/plugins/check_nrpe -H 172.21.0.3 -c check_load ; echo "exit=$?"
LOAD WARNING - scaled load average: 0.06, 0.08, 0.09 - total load average: 0.50, 0.65, 0.73|load1=0.500;;;0; scaled_load1=0.062;0.150;0.300;0; ...
exit=1

Do not edit the shipped lines. Put your own in /etc/nagios/nrpe_local.cfg, which the main file includes at the end. A second definition of the same name wins:

command[check_root]=/usr/lib/nagios/plugins/check_disk -w 20% -c 10% -p /
command[check_load]=/usr/lib/nagios/plugins/check_load -w 4,3,2 -c 8,6,4

Restart the agent (sudo systemctl restart nagios-nrpe-server; not run in the lab, which started and restarted nrpe -c /etc/nagios/nrpe.cfg -d by hand). Then, from the Nagios server:

$ /usr/lib/nagios/plugins/check_nrpe -H 172.21.0.3
NRPE v4.1.0
$ /usr/lib/nagios/plugins/check_nrpe -H 172.21.0.3 -c check_load
LOAD OK - total load average: 0.59, 0.55, 0.67|load1=0.590;4.000;8.000;0; load5=0.550;3.000;6.000;0; load15=0.670;2.000;4.000;0;

The first command with no -c is the way to test the connection: it prints the agent version. Then the Nagios side. The lab defined one command and used it for services:

define command {
    command_name    check_nrpe_cmd
    command_line    $USER1$/check_nrpe -H $HOSTADDRESS$ -c $ARG1$
}

define service {
    use                   local-service
    host_name             web1
    service_description   Load
    check_command         check_nrpe_cmd!check_load
}

define service {
    use                   local-service
    host_name             web1
    service_description   Root disk
    check_command         check_nrpe_cmd!check_root
}

(The package also installs check_nrpe and check_nrpe_nossl with the same idea.) Errors you will see, each reproduced in the lab:

CHECK_NRPE: Error - Could not connect to 172.21.0.3: Connection reset by peer       exit 2: the server is not in allowed_hosts of the agent (or a firewall is in the way)
NRPE: Command 'no_such_cmd' not defined                                                exit 3: the name after -c is not a command[...] line on the agent
CHECK_NRPE: Receive header underflow - only 0 bytes received (4 expected).             exit 3: arguments were sent (-a 1 2 3) and the agent refused them. Agents refuse arguments by default, which is the safe setting (dont_blame_nrpe=0)

Keep dont_blame_nrpe at 0 and define fixed commands. Allowing arguments lets whoever can reach the agent choose what it runs. An alternative that needs no agent is check_by_ssh (the plugin is there; it was not run in the lab). Also not run: encryption settings of NRPE, which matter when the agent is reachable from untrusted networks, so restrict port 5666 to the Nagios server with a firewall.

Notifications: contacts, mail and time periods

The shipped contact is one person, and the shipped command sends mail through /usr/bin/mail:

define contact {
    contact_name   nagiosadmin
    use            generic-contact
    alias          Nagios Admin
    email          nagios@localhost
}

define contactgroup {
    contactgroup_name  admins
    alias              Nagios Administrators
    members            nagiosadmin
}

Change the email line to a real address. Then make sure the server can send mail, because Nagios does not: it runs the command and moves on. The shipped service command is

/usr/bin/printf "%b" "***** Nagios *****\n\nNotification Type: $NOTIFICATIONTYPE$\n\nService: $SERVICEDESC$\nHost: $HOSTALIAS$ ..." | /usr/bin/mail -s "** $NOTIFICATIONTYPE$ Service Alert: $HOSTALIAS$/$SERVICEDESC$ is $SERVICESTATE$ **" $CONTACTEMAIL$

Test that command's last part by hand first. The lab installed a local-only mail setup (sudo apt install postfix mailutils, choosing "Local only") and sent a test:

$ echo "lab test body" | mail -s "lab test" nagios@localhost
$ ls -l /var/mail/nagios
-rw------- 1 nagios mail 2057 Oct 14 15:25 /var/mail/nagios

For real mail you need an MTA that delivers outside (postfix as a satellite or relay, or msmtp); that setup was not run here. If the test does not arrive, no alert will.

What an alert looks like when it arrives (from the lab):

Subject: ** PROBLEM Service Alert: Web server 1/HTTP is CRITICAL **

***** Nagios *****

Notification Type: PROBLEM

Service: HTTP
Host: Web server 1
Address: 172.21.0.3
State: CRITICAL

Date/Time: Wed Oct 14 15:26:52 UTC 2026

Additional Info:

connect to address 172.21.0.3 and port 80: Connection refused

The subjects seen in the lab, for the four message types: PROBLEM, RECOVERY, ACKNOWLEDGEMENT and, for a host, ** PROBLEM Host Alert: web2 is DOWN **.

Which states notify is set in the contact (service_notification_options w,u,c,r,f,s means warning, unknown, critical, recovery, flapping, scheduled downtime) and in the host or service (notification_options, notification_interval, notification_period). The time periods are plain definitions:

define timeperiod { name 24x7       timeperiod_name 24x7       sunday 00:00-24:00 ... saturday 00:00-24:00 }
define timeperiod { name workhours  timeperiod_name workhours  monday 09:00-17:00 ... friday 09:00-17:00 }
define timeperiod { name none       timeperiod_name none }     ; no time is a good time

Some services should not page anyone. The shipped localhost SSH and HTTP services have notifications_enabled 0 for exactly that reason. Escalations (tell someone else if nobody reacts) and dependencies (do not alert about the web site when its database is down) exist as host and service escalation and dependency objects; they were not run in the lab.

Working a problem: acknowledge, downtime, forced checks and passive results

Everything in the web interface that changes something is an external command: one line written to the command file, /var/lib/nagios4/rw/nagios.cmd. It is a named pipe owned by nagios, readable by the web server group (check_external_commands=1 in nagios.cfg). You can write to it from a script. The lab used this one-line helper (run it as the nagios user or root):

#!/bin/bash
printf '[%lu] %s\n' "$(date +%s)" "$1" > /var/lib/nagios4/rw/nagios.cmd

Check now. Do not wait for the next interval:

ncmd "SCHEDULE_FORCED_SVC_CHECK;web1;HTTP;$(date +%s)"

Acknowledge. Tell Nagios you know. It stops repeating the notification for that problem until the state changes. Arguments: host; service; sticky; notify; persistent; author; comment.

$ ncmd "ACKNOWLEDGE_SVC_PROBLEM;web1;HTTP;1;1;1;admin;nginx stopped on purpose, config change"
15:27:15 EXTERNAL COMMAND: ACKNOWLEDGE_SVC_PROBLEM;web1;HTTP;1;1;1;admin;nginx stopped on purpose, config change
15:27:15 SERVICE NOTIFICATION: nagiosadmin;web1;HTTP;ACKNOWLEDGEMENT (CRITICAL);notify-service-by-email;connect to address 172.21.0.3 and port 80: Connection refused;admin;ngi...

Colleagues get a message with your comment (Subject: ** ACKNOWLEDGEMENT Service Alert: Web server 1/HTTP is CRITICAL **), and the next forced check sent no second alert. When the service recovered, the acknowledgement cleared by itself and the RECOVERY mail followed.

Schedule downtime for planned work. Arguments: host; service; start; end; fixed; trigger; duration; author; comment (times in seconds since 1970).

$ N=$(date +%s); E=$((N+1800))
$ ncmd "SCHEDULE_SVC_DOWNTIME;web1;HTTP;$N;$E;1;0;1800;admin;nginx upgrade window"
15:27:45 SERVICE DOWNTIME ALERT: web1;HTTP;STARTED; Service has entered a period of scheduled downtime
15:27:48 SERVICE ALERT: web1;HTTP;CRITICAL;SOFT;1;connect to address 172.21.0.3 and port 80: Connection refused
15:27:52 SERVICE ALERT: web1;HTTP;CRITICAL;SOFT;2;...
15:27:56 SERVICE ALERT: web1;HTTP;CRITICAL;HARD;3;...

Nginx was stopped during the window. The alerts were logged, but no email was sent: the last subject in the mailbox stayed the earlier RECOVERY. The state data confirmed it (scheduled_downtime_depth was 1 while the window was open). End a downtime early with DEL_DOWNTIME_BY_HOST_NAME;web1;HTTP, which the lab did:

[1791991690] EXTERNAL COMMAND: DEL_DOWNTIME_BY_HOST_NAME;web1;HTTP
[1791991690] SERVICE DOWNTIME ALERT: web1;HTTP;CANCELLED; Scheduled downtime for service has been cancelled.

Passive checks: the thing being watched reports in, instead of Nagios going to look. Right for jobs that run on their own schedule (a nightly backup), for events, and for hosts Nagios cannot reach. Define the service so it expects results and complains when they stop coming:

define service {
    use                     local-service
    host_name               web1
    service_description     Nightly backup
    check_command           check_dummy_msg!3!No backup report received yet
    active_checks_enabled   0
    passive_checks_enabled  1
    check_freshness         1
    freshness_threshold     86400          ; seconds: with no report for a day, Nagios runs the check command (here it returns UNKNOWN; the stale case was not run in the lab)
    max_check_attempts      1
}

and the backup job ends with one line:

$ ncmd "PROCESS_SERVICE_CHECK_RESULT;web1;Nightly backup;0;BACKUP OK - 14.2 GB written in 312 s"
15:29:06 EXTERNAL COMMAND: PROCESS_SERVICE_CHECK_RESULT;web1;Nightly backup;0;BACKUP OK - 14.2 GB written in 312 s
SERVICE web1       Nightly backup OK        HARD 1/1  BACKUP OK - 14.2 GB written in 312 s

The number after the service name is the exit code, 0 to 3. From another machine, the same message can arrive through NSCA or NRDP, add-ons that were not run in the lab.

Other commands you will use: DISABLE_SVC_NOTIFICATIONS and ENABLE_SVC_NOTIFICATIONS (host;service), DISABLE_HOST_SVC_CHECKS, SCHEDULE_HOST_DOWNTIME, SCHEDULE_FORCED_HOST_CHECK (used for web2 above). The list is in the project documentation under "External Commands".

The web interface and its security

The pages are at http://SERVER/nagios4/. The useful ones: Tactical Overview (tac.cgi, one screen of counts), Hosts and Services (status.cgi), Problems, Service Detail, Reports, and the Process Info page with the global switches. Clicking a service gives the same commands as the previous section, as forms.

The packaged defaults are a compromise and the file says so: authentication is off (use_authentication=0 in cgi.cfg), and Apache only allows private address ranges (Require ip ::1/128 fc00::/7 ... 10.0.0.0/8 127.0.0.0/8 172.16.0.0/12 192.168.0.0/16). Anyone on your LAN can see everything, and by default only the page that submits commands (cmd.cgi) is protected by a login stanza, which in the packaged file ends in "Require all granted", i.e. open. Turn on real login before the server is reachable by anyone you do not trust.

1. Create a user (Apache digest authentication; the realm name must match: Nagios4):

$ sudo apt install apache2-utils
$ sudo htdigest -c /etc/nagios4/htdigest.users Nagios4 nagiosadmin
New password:
Re-type new password:
$ ls -l /etc/nagios4/htdigest.users
-rw-r----- 1 nagios www-data 53 Oct 14 15:29 /etc/nagios4/htdigest.users

Without -c the command adds a user to the existing file. (-c creates a new file, replacing the old one, so use it for the first user only.) What a user may see and do is set by user name in cgi.cfg: authorized_for_all_services=nagiosadmin, authorized_for_system_commands=nagiosadmin and so on (all of them name nagiosadmin by default), so the first user should keep that name or you edit those lines.

2. In /etc/nagios4/cgi.cfg:

use_authentication=1

3. In /etc/apache2/conf-available/nagios4-cgi.conf, replace the "Require ip ..." line in the DirectoryMatch block by the login stanza, and in the cmd.cgi block change "Require all granted" to "Require valid-user":

    AuthDigestDomain "Nagios4"
    AuthDigestProvider file
    AuthUserFile "/etc/nagios4/htdigest.users"
    AuthName "Nagios4"
    AuthType Digest
    Require valid-user

$ sudo apache2ctl configtest
Syntax OK
$ sudo systemctl reload apache2          (the lab used apache2ctl graceful)

Test it like a client does (HTTP status codes from the lab):

$ curl -s -o /dev/null -w "HTTP %{http_code}\n" http://localhost/nagios4/
HTTP 401
$ curl -s -o /dev/null -w "HTTP %{http_code}\n" --digest -u nagiosadmin:wrong http://localhost/nagios4/
HTTP 401
$ curl -s -o /dev/null -w "HTTP %{http_code}\n" --digest -u nagiosadmin:labpass http://localhost/nagios4/
HTTP 200
cmd.cgi without a login:  HTTP 401        with a login:  HTTP 200
tac.cgi without a login:  HTTP 401        with a login:  HTTP 200

The lab password was a throwaway. Digest authentication does not hide the traffic: put the pages behind HTTPS (a certificate on Apache, or a reverse proxy) if they cross a network you do not control. That setup was not run in the lab.

The same pages have a JSON interface, which is the easy way to read state from scripts (statusjson.cgi, with the same login):

$ curl -s --digest -u nagiosadmin:PASSWORD "http://localhost/cgi-bin/nagios4/statusjson.cgi?query=servicelist&details=true"
$ curl -s --digest -u nagiosadmin:PASSWORD "http://localhost/cgi-bin/nagios4/statusjson.cgi?query=service&hostname=web1&servicedescription=HTTP"

Each service has a numeric status: 1 pending, 2 ok, 4 warning, 8 unknown, 16 critical. For hosts: 1 pending, 2 up, 4 down, 8 unreachable. A small script turns that into a readable table. As used in the lab (host states and service states, SOFT or HARD with the attempt count):

HOST    localhost  UP          PING OK - Packet loss = 0%, RTA = 0.08 ms
HOST    web1       UP          PING OK - Packet loss = 0%, RTA = 0.13 ms
HOST    web2       DOWN        CRITICAL - Host Unreachable (172.21.0.99)
SERVICE web1       HTTP           OK        HARD 1/3  HTTP OK: HTTP/1.1 200 OK - 857 bytes in 0.001 second res
SERVICE web1       Nightly backup OK        HARD 1/1  BACKUP OK - 14.2 GB written in 312 s
SERVICE web1       Web response   PENDING   HARD 1/3  Service check scheduled for Wed Oct 14 15:30:46 UTC 2026

Logs, status and statistics

The log is plain text, one line per event, with the time as seconds since 1970 in square brackets:

[1791991604] SERVICE ALERT: web1;HTTP;CRITICAL;SOFT;1;connect to address 172.21.0.3 and port 80: Connection refused
[1791991665] SERVICE DOWNTIME ALERT: web1;HTTP;STARTED; Service has entered a period of scheduled downtime

date -d @1791991604 turns one into a readable time. The lab used a helper to print the last lines of events (alerts, notifications, external commands) with the times converted; a plain grep does the same:

$ grep -E "ALERT|NOTIFICATION|EXTERNAL COMMAND" /var/log/nagios4/nagios.log | tail

The log rotates daily by default (log_rotation_method=d) into /var/log/nagios4/archives. Every notification is logged (log_notifications=1), including who was told and with which command, which makes "did it really send anything?" a one-line answer. If a mail is missing but the NOTIFICATION line is there, the problem is mail delivery, not Nagios.

nagios4stats prints counters from the status file:

$ nagios4stats
Nagios Stats 4.4.6
CURRENT STATUS DATA
------------------------------------------------------
Status File:                            /var/lib/nagios4/status.dat
Status File Age:                        0d 0h 0m 2s
Status File Version:                    4.4.6

Program Running Time:                   0d 0h 0m 41s
Nagios PID:                             15205

Total Services:                         14
Services Checked:                       14
Services Scheduled:                     13
Services Actively Checked:              13
Services Passively Checked:             1

"Services Scheduled" counts checks Nagios runs by itself; the passive one is not among them. Further down it reports latency (how late checks start) and execution time, each as minimum / maximum / average:

Active Service Latency:                 0.000 / 0.001 / 0.000 sec
Active Service Execution Time:          0.003 / 4.124 / 0.770 sec
Passive Service Latency:                0.965 / 0.965 / 0.965 sec
Active Host Execution Time:             0.007 / 3.095 / 1.037 sec

If the latency keeps growing, the server cannot keep up: use fewer or slower checks, longer intervals, or look at max_concurrent_checks (0, the default, means unlimited).

State is kept across restarts: retain_state_information=1 writes it to retention.dat and the daemon reads it back at start.

Cheat sheet

Install             sudo apt install nagios4 apache2 monitoring-plugins nagios-nrpe-plugin   then sudo a2enmod cgi if pages download
Validate            nagios4 -v /etc/nagios4/nagios.cfg           ALWAYS before a reload; a bad reload stops the daemon
Reload / status     sudo systemctl reload nagios4   systemctl status nagios4
Your config         /etc/nagios4/conf.d/*.cfg     (host, service, hostgroup, servicegroup, command, contact, timeperiod)
Plugins             /usr/lib/nagios/plugins/check_*    run by hand, look at the exit code (echo $?)
Exit codes          0 OK   1 WARNING   2 CRITICAL   3 UNKNOWN
Thresholds          -w 10   -w 1:2   -w 1:   -w '~:3'   -w @1:10      (check_disk: FREE space)
Remote checks       agent: nagios-nrpe-server, allowed_hosts, nrpe_local.cfg     server: check_nrpe -H HOST -c NAME   (-H only to test)
States              OK WARNING CRITICAL UNKNOWN  /  host UP DOWN UNREACHABLE  /  SOFT then HARD after max_check_attempts
Command file        printf '[%lu] CMD;args\n' $(date +%s) > /var/lib/nagios4/rw/nagios.cmd
Force a check       SCHEDULE_FORCED_SVC_CHECK;host;service;time      SCHEDULE_FORCED_HOST_CHECK;host;time
Acknowledge         ACKNOWLEDGE_SVC_PROBLEM;host;service;sticky;notify;persistent;author;comment
Downtime            SCHEDULE_SVC_DOWNTIME;host;service;start;end;fixed;trigger;duration;author;comment
Passive result      PROCESS_SERVICE_CHECK_RESULT;host;service;code;output
Web login           sudo htdigest -c /etc/nagios4/htdigest.users Nagios4 USER   +  use_authentication=1 in cgi.cfg   +  Require valid-user in Apache
Log                 /var/log/nagios4/nagios.log      state: /var/lib/nagios4/status.dat      nagios4stats
Defaults to change  host max_check_attempts 10 and "workhours" in linux-server; nrpe check_load thresholds; the web login

Common problems

Web pages download as files, or show binary text    mod_cgi is not enabled: sudo a2enmod cgi and restart Apache
Nothing is monitored after a reload              the reload hit a config error and Nagios bailed out. Read the end of /var/log/nagios4/nagios.log, run nagios4 -v, fix, start again
Error: Could not find any host matching 'X'      the host_name in a service is misspelt or the host is in a file that is not included (it must be under conf.d)
Template 'X' ... could not be found            the name after use does not exist: check the spelling and that the file defining it is included
Invalid ... directive 'addres'                   a typo. The errors after it are usually noise: fix the first
Service description contains illegal characters  parentheses and other symbols: use letters, digits, spaces, - and _
A check shows PENDING for a long time          checks are spread over the interval after a start; wait one interval, or force a check
UNKNOWN with "(No output returned from plugin)"  the plugin could not run: run the command line by hand as the nagios user (sudo -u nagios /usr/lib/nagios/plugins/check_X ...)
HTTP CRITICAL - Unable to open TCP socket      the service is down, or a firewall or wrong address; test with curl from the Nagios server
CHECK_NRPE: Could not connect ... reset by peer  the Nagios server is not in allowed_hosts of the agent, or port 5666 is blocked
NRPE: Command 'X' not defined                  no command[X]= line on the agent, or the agent was not restarted after editing
Receive header underflow ... 0 bytes received   you sent arguments (-a ...); agents refuse them by default. Define fixed commands instead
Quiet servers report WARNING for load          the NRPE default check_load thresholds are tiny: override in nrpe_local.cfg
A dead host takes ten minutes to show DOWN     max_check_attempts 10 and retry_interval 1 in the linux-server template
No mail but a NOTIFICATION line in the log     the notification command ran: mail delivery is the problem; test mail by hand as the nagios user
No alerts on weekends or at night              the template says workhours: set notification_period 24x7 on the host and service
A check never goes HARD                        max_check_attempts is too high for the interval, or the state is flapping between OK and a problem
Passive result ignored                         the service must have passive_checks_enabled 1 and the name must match exactly (host;service)
Pages say you are not authorized               the user is not listed in the authorized_for_* lines of cgi.cfg (all of them are nagiosadmin by default)

Tips

Run the plugin by hand first, as the nagios user, every time. If it fails there, Nagios cannot fix it.
Put your own files in conf.d and leave the shipped files alone. One file per host (or group of hosts) is easy to diff and to generate.
Validate before every reload. Make it a habit that is one command: nagios4 -v ... && reload.
Use templates and host groups. A new web server should be one host definition, with its services coming from the group.
Choose max_check_attempts and the intervals on purpose, per host class. The defaults are generic and slow.
Keep check_command arguments in the service, not in the command, so one command serves many services.
Prefer a few checks that matter (is the site up, is the disk filling, is the backup fresh) over hundreds that nobody reads. Alert fatigue is the real failure mode.
Use acknowledgements and downtime for real. A dashboard full of known problems hides the new one.
Passive checks are good for batch jobs: the "no news is bad news" freshness check catches the job that did not run.
Monitor the monitor: check Nagios from outside (a simple HTTP check from another place, or a dead man's switch that stops arriving), because a stopped Nagios sends no alerts about itself.
Protect the web interface (login, then HTTPS) and the NRPE port (firewall) before you give them an address anybody can reach.
Test notifications on purpose: stop a test service, watch SOFT, HARD and the mail, then bring it back and watch the RECOVERY.