Linux host monitoring with node_exporter, Prometheus file_sd and alert rules
A production-backed Linux monitoring pattern using node_exporter 1.12.1, Prometheus file-based service discovery, recording rules and layered host alerts.
Tested on: AlmaLinux 9.8 · node_exporter 1.12.1 · Prometheus file_sd · Prometheus recording and alerting rules
Context
Linux host monitoring becomes useful when it is treated as an infrastructure contract rather than a collection of default dashboards.
The production model described here has four distinct layers:
Linux hosts
|
v
node_exporter
|
v
Prometheus file_sd target inventory
|
v
recording rules
|
v
alert rules / dashboards
The current environment monitors a mixed Linux fleet that includes mail, monitoring, observability, security, 1C and telephony roles. Some hosts also expose service-specific textfile metrics, but the Linux layer remains independent from those application checks.
This note focuses on the host-level contract.
Verified production baseline
A representative production host was verified with:
OS: AlmaLinux 9.8
node_exporter: 1.12.1
architecture: linux/amd64
service account: node_exporter
listener: one internal management address on TCP/9100
textfile collector: enabled
service state: enabled + active
The central Prometheus runtime was also verified with:
node job scrape interval: 30s
node job scrape timeout: 10s
target discovery: file_sd
file_sd refresh interval: 30s
Prometheus config validation: successful
rule files loaded: 12
Linux rule file: fm-linux.yml
Linux rules: 22
The live target used for validation reported:
job: node
health: up
scrape interval: 30s
scrape timeout: 10s
node_exporter systemd unit
The production pattern runs node_exporter under a dedicated account and binds it to a management address rather than all interfaces:
[Service]
Type=simple
User=node_exporter
Group=node_exporter
ExecStart=/usr/local/bin/node_exporter \
--web.listen-address=<node-management-ip>:9100 \
--collector.textfile.directory=/var/lib/node_exporter/textfile_collector
Restart=on-failure
RestartSec=5s
The unit is hardened with directives such as:
NoNewPrivileges=true
PrivateTmp=true
ProtectHome=true
ProtectSystem=strict
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
LockPersonality=true
RestrictSUIDSGID=true
RestrictRealtime=true
CapabilityBoundingSet=
AmbientCapabilities=
Binding to an internal address reduces exposure, but routing and firewall policy still need to restrict TCP/9100 to the monitoring path.
Version validation
Verify the binary directly rather than inferring its version from a package name:
/usr/local/bin/node_exporter --version
The verified production binary reports:
node_exporter 1.12.1
The matching build metric is also exposed through /metrics:
node_exporter_build_info{version="1.12.1",...} 1
This is useful for detecting inconsistent rollout across the fleet.
Default collectors
The representative host exposes the normal Linux collector set including:
cpu
filesystem
loadavg
meminfo
netclass
netdev
netstat
pressure
sockstat
stat
textfile
time
timex
uname
vmstat
xfs
and other hardware/filesystem collectors enabled by node_exporter defaults.
There is no production requirement to disable every collector that has no matching hardware on a specific host. Keeping the standard collector set reduces configuration drift between systems.
Collector success can be inspected with:
node_scrape_collector_success
File-based target discovery
Prometheus does not hard-code Linux nodes directly in the main configuration.
The node job uses file-based service discovery:
- job_name: node
scrape_interval: 30s
scrape_timeout: 10s
file_sd_configs:
- files:
- /etc/prometheus/targets/node/*.yml
refresh_interval: 30s
Target files group systems by operational role while preserving common labels.
A sanitized target entry looks like:
- targets:
- "<node-ip>:9100"
labels:
instance: "<node-name>"
site: "fm"
role: "mail"
os: "linux"
The key contract labels are:
instance
site
role
os
Recording and alert rules rely on those labels, so a target added without them is not equivalent to a correctly onboarded host.
Why file_sd is useful here
For a small or medium static fleet, file_sd gives several advantages over a large static_configs block:
- target inventory can be split by role;
- the main Prometheus configuration stays small;
- target changes do not require editing the scrape-job definition;
- labels are explicit and reviewable;
- inventory drift can be checked independently.
The current Linux monitoring contract expects exactly 9 FM Linux targets.
That count is intentionally monitored. Adding or removing a Linux target therefore requires updating both discovery inventory and the target-count contract.
Runtime target validation
Do not rely only on configuration files. Confirm what Prometheus actually loaded:
curl -fsS "http://<prometheus-ip>:9090/api/v1/targets?state=active" |
jq '.data.activeTargets[] | select(.labels.job == "node")'
For a healthy target, confirm:
job=node
health=up
lastError=""
scrapeInterval=30s
scrapeTimeout=10s
This catches discovery, relabeling and connectivity problems that a YAML syntax check cannot detect.
Recording-rule layer
The production fm-linux.yml creates normalized recording rules before alerts are evaluated.
The current recording metrics are:
fm:linux:up
fm:linux:cpu_usage_percent
fm:linux:memory_usage_percent
fm:linux:swap_usage_percent
fm:linux:filesystem_usage_percent
fm:linux:inode_usage_percent
fm:linux:uptime_seconds
fm:linux:load1_per_cpu
This keeps dashboard and alert expressions smaller and makes fleet-level behavior consistent.
Availability
The base availability rule is:
up{job="node",site="fm",os="linux"}
recorded as:
fm:linux:up
Most Linux nodes use a common exporter-down alert with a two-minute delay.
Some infrastructure hosts are intentionally excluded because dedicated availability alerts already exist in other rule files. This avoids duplicate notifications for the same underlying failure.
Target-count contract
The current rule checks that the production inventory contains exactly nine matching targets:
(
count(up{job="node",site="fm",os="linux"})
or vector(0)
) != 9
and holds for two minutes before firing.
This is not a capacity metric. It is an inventory-drift detector.
The rule is deliberately strict, so a planned host addition must update the expected count as part of the same change.
CPU usage
CPU usage is derived from idle CPU time over five minutes:
100 * (
1 - avg without (cpu, mode) (
rate(node_cpu_seconds_total{
job="node",
site="fm",
os="linux",
mode="idle"
}[5m])
)
)
The production thresholds are layered:
warning: > 90% and < 97% for 15m
critical: >= 97% for 5m
This avoids alerting on short CPU bursts while still escalating sustained saturation quickly.
A security-monitoring host is excluded because it has dedicated CPU policy elsewhere.
Memory usage
Memory usage is calculated from MemAvailable, not merely MemFree:
100 * (
1 -
node_memory_MemAvailable_bytes
/
node_memory_MemTotal_bytes
)
Production thresholds:
warning: > 85% and <= 92% for 15m
critical: > 92% for 5m
Using MemAvailable is important on Linux because filesystem cache is reclaimable and should not automatically be interpreted as application memory pressure.
Swap usage
Swap percentage is recorded only on systems where total swap is greater than zero.
Conceptually:
100 * (1 - node_memory_SwapFree_bytes / node_memory_SwapTotal_bytes)
combined with a guard:
node_memory_SwapTotal_bytes > 0
This avoids invalid division and avoids manufacturing a swap-usage series on hosts with no configured swap.
The current Linux rule file records swap usage but does not define a generic fleet-wide swap alert. That distinction is intentional and should not be hidden by a generic tutorial.
Filesystem usage
Filesystem utilization is based on available bytes:
100 * (
1 - node_filesystem_avail_bytes / node_filesystem_size_bytes
)
Pseudo and transient filesystem types are excluded:
tmpfs
devtmpfs
overlay
squashfs
nsfs
tracefs
debugfs
securityfs
proc
sysfs
cgroup / cgroup2
Production thresholds are:
warning: > 85% and < 93% for 15m
critical: >= 93% for 5m
One observability datastore mount is excluded because it has a dedicated storage alert in its own rule set. Again, the goal is to avoid duplicate alerts rather than force every mount into one global policy.
Read-only filesystems
A separate critical rule checks:
node_filesystem_readonly == 1
for real filesystem types and holds for one minute.
This catches a different failure class from low free space. A filesystem can remount read-only because of storage or filesystem errors while still having plenty of free capacity.
Inode utilization
Inode percentage is normalized as:
100 * (
1 - node_filesystem_files_free / node_filesystem_files
)
with the same pseudo-filesystem exclusions used for byte capacity.
Production thresholds are:
warning: > 90% and < 97% for 15m
critical: >= 97% for 5m
Tracking inodes separately is important for mail, logging and other workloads that can create many small files.
OOM killer activity
The fleet has a critical alert on OOM activity:
increase(
node_vmstat_oom_kill{
job="node",
site="fm",
os="linux"
}[10m]
) > 0
This is stronger evidence of memory exhaustion than a single high-memory snapshot because it detects an actual kernel OOM kill event.
Time synchronization
Kernel synchronization state is checked with:
node_timex_sync_status == 0
for five minutes.
This matters for log correlation, TLS, authentication, distributed systems and incident timelines. The metric represents kernel time synchronization state; it is not a replacement for separately diagnosing the active NTP/chrony service when an alert fires.
Textfile collector integrity
Several application runbooks use node_exporter’s textfile collector for local service metrics.
The Linux layer therefore includes:
node_textfile_scrape_error > 0
for five minutes.
This catches malformed .prom files independently of the application-specific metrics they contain.
Dedicated observability/security hosts are excluded where equivalent textfile checks already exist elsewhere.
Docker hosts
The validated mail host runs its application stack in Docker.
That does not prevent node_exporter from being used for host monitoring. The exporter still reports the host kernel, CPU, memory, filesystem and network namespaces visible to the host process.
On the validated host, filesystem metric types were only:
xfs
tmpfs
so Docker did not create problematic overlay filesystem series in the observed node_filesystem_* output.
Network metrics were noisier and included:
physical/VM NIC
docker0
application bridge
veth* interfaces
loopback
For host-level Grafana panels, a practical network filter is:
node_network_receive_bytes_total{
device!~"lo|docker0|br-.*|veth.*"
}
and the equivalent filter for transmit, errors and drops.
Do not disable these devices at exporter level merely to make a dashboard cleaner. Container interfaces can still be useful during incident analysis.
The current fm-linux.yml does not define generic network saturation/error alerts, so this note does not invent production network thresholds that do not exist.
Load normalized per CPU
The rule set records one-minute load divided by CPU count:
node_load1
/
count by (instance, site, role, os) (
node_cpu_seconds_total{mode="idle"}
)
as:
fm:linux:load1_per_cpu
Normalizing by CPU count makes the metric comparable between differently sized systems.
It is currently a recording metric, not a generic fleet-wide alert condition.
Uptime
Host uptime is recorded from boot time:
time() - node_boot_time_seconds
This is useful for dashboard context and post-maintenance validation.
A low uptime is not inherently a failure, so the production rule file does not treat reboot itself as a generic alert.
Prometheus-side validation
The central configuration is checked with:
/usr/local/bin/promtool check config /etc/prometheus/prometheus.yml
The verified run successfully validated the main configuration and all 12 rule files, including the 22-rule Linux file.
Syntax validation is necessary but not sufficient. After changes also inspect the runtime target state and evaluate key recording rules.
Useful checks:
count(up{job="node",site="fm",os="linux"})
fm:linux:cpu_usage_percent
fm:linux:memory_usage_percent
fm:linux:filesystem_usage_percent
node_textfile_scrape_error{job="node",site="fm",os="linux"}
Alert-policy summary
The production Linux layer currently covers:
inventory count drift critical after 2m
node_exporter unavailable critical after 2m
CPU high warning after 15m
CPU critical critical after 5m
memory high warning after 15m
memory critical critical after 5m
filesystem high warning after 15m
filesystem critical critical after 5m
filesystem read-only critical after 1m
inode high warning after 15m
inode critical critical after 5m
textfile parse error warning after 5m
OOM kill detected critical
kernel time unsynchronized warning after 5m
The exact thresholds are part of this environment’s operational policy, not universal defaults for every Linux fleet.
Avoid duplicate alerts
One notable design property is that several specialized hosts are excluded from selected generic rules when a more specific alert already exists elsewhere.
Examples include dedicated rules for:
security monitoring CPU/memory/filesystems
observability datastore capacity
selected exporter availability
selected textfile collector integrity
This keeps the monitoring hierarchy understandable:
generic Linux layer
+
role-specific layer
+
application-specific layer
A role-specific alert should replace a duplicate generic condition, not simply add another notification for the same event.
What this layer does not cover
Host monitoring does not prove that an application is working.
A Linux node can have normal CPU, memory and filesystem metrics while:
mail delivery is failing
Asterisk trunks are unavailable
PostgreSQL transactions are blocked
1C application services are unhealthy
Loki ingestion is broken
Those conditions belong to the corresponding application or observability runbooks.
The purpose of the Linux layer is to provide the common machine-level foundation beneath them.
Change procedure
When onboarding another Linux host:
- install and validate the approved node_exporter build;
- bind TCP/9100 to the intended management address;
- apply the hardened systemd unit;
- add a
file_sdtarget withinstance,site,roleandoslabels; - update the target-count contract if the inventory changes;
- run
promtool check config; - reload Prometheus;
- confirm the runtime target is
UP; - verify recording rules exist for the new instance;
- check for duplicate role-specific alerts before enabling generic notifications.
Rollback
If a target change is incorrect:
- restore the previous target file;
- restore the previous expected target count if it was changed;
- validate with
promtool; - reload Prometheus;
- confirm runtime targets and recording rules return to the previous state.
If node_exporter itself is being changed, preserve the previous binary and systemd unit so the exporter can be rolled back independently of Prometheus configuration.
Validation checklist
[ ] node_exporter version is known
[ ] node_exporter runs as the dedicated service account
[ ] TCP/9100 is bound to the intended management address
[ ] firewall/routing restricts the scrape path
[ ] file_sd target has instance/site/role/os labels
[ ] Prometheus runtime target is UP
[ ] scrape interval is 30s and timeout is 10s
[ ] expected Linux target count is correct
[ ] CPU/memory/filesystem/inode recording rules return data
[ ] textfile scrape error is zero where textfile collector is used
[ ] OOM and time-sync metrics exist on supported hosts
[ ] Docker veth/bridge devices are filtered at dashboard/query level when needed
[ ] generic alerts do not duplicate role-specific alerts
[ ] promtool validation succeeds before reload
References
- node_exporter: https://github.com/prometheus/node_exporter
- node_exporter textfile collector: https://github.com/prometheus/node_exporter#textfile-collector
- Prometheus file-based service discovery: https://prometheus.io/docs/prometheus/latest/configuration/configuration/#file_sd_config
- Prometheus recording rules: https://prometheus.io/docs/prometheus/latest/configuration/recording_rules/
- Prometheus alerting rules: https://prometheus.io/docs/prometheus/latest/configuration/alerting_rules/
- promtool: https://prometheus.io/docs/prometheus/latest/command-line/promtool/