High availability and monitoring on Linux: the concepts of redundancy & failover, Keepalived for virtual IPs, Prometheus + node_exporter + Grafana for observability, Alertmanager, along with practice and common mistakes.

After episode 28 where we built centralized authentication with LDAP and SSSD — one identity directory serving dozens of servers — you now have centralized access control. But there's a question as important as "who can get in": how do we know all these servers are still alive and healthy? A server can die without sending any notification — a failed power supply, kernel panic, or a full disk — and you only notice when a user reports "the site can't be accessed".
In episode 29 we enter two operational pillars that make infrastructure truly professional: High Availability (HA) and Monitoring. We'll learn how to keep services alive even when one server dies (Keepalived + virtual IP), how to measure all servers' health centrally (Prometheus + node_exporter + Grafana), and how to receive automatic alerts before a problem becomes a disaster (Alertmanager). At the end of the episode, you'll build a real monitoring stack across two VMs — the foundation of reliable operations.
The first principle to understand: high availability doesn't mean "never down" — it means "downtime impact is minimized with automation". There's no failure-free system; what exists are systems that design for failure and provide automatic substitutes.
Think of a passenger plane. The plane isn't designed so its engines never fail — it's designed so one engine can fail without bringing the plane down. There are two engines (redundancy), and if one dies, the other keeps the plane flying (failover). The pilot doesn't panic; the system compensates automatically.
In the server world, redundancy means running two or more nodes providing the same service. Failover means the automatic mechanism moving "leadership" from the dead node to a healthy one. The most important component in failover is detection: the replacement node must know quickly that the primary node is no longer healthy.
| HA Component | Role | Plane Analogy |
|---|---|---|
| Redundancy | More than one node provides the service | Two engines on the wings |
| Detection | Monitors the primary node's health (heartbeat) | Alarms & sensors in the cockpit |
| Failover | Automatic switch to the replacement node | Autopilot takes over |
| Virtual IP | One IP address that "follows" the active node | One parking spot, two planes that can land |
The most popular tool for two-node active/standby scenarios on Linux is Keepalived. It uses the VRRP protocol (Virtual Router Redundancy Protocol) to create a Virtual IP (VIP) that's automatically used by the healthy node. From the outside, clients only see one IP — never knowing there are two servers behind it. When the primary node dies, the standby node takes over the VIP within seconds without human intervention.
For more complex clusters (multi-node, movable resources like services, filesystems, and IPs), there's Pacemaker + Corosync — a cluster resource manager that's far more powerful but also far more complex. Keepalived is enough for most active/standby needs; Pacemaker for enterprise-scale clusters.
Note
Always remember: the VIP is only the "entry door" layer. If both nodes die together (e.g. a power outage across the whole DC), the VIP won't help either. HA protects against single node failure, not the failure of an entire location — for that you need multi-DC and the disaster recovery strategy we covered in episode 26.
We'll create two nodes — web-01 (IP 192.168.1.11) and web-02 (IP 192.168.1.12) — sharing one VIP 192.168.1.10. Both nodes run nginx. When web-01 is healthy, it holds the VIP; if it dies, web-02 takes over.
apt install -y keepalivedConfig on the primary node web-01:
vrrp_instance VI_1 {
state MASTER
interface eth0
virtual_router_id 51
priority 200
advert_int 1
virtual_ipaddress {
192.168.1.10/24 dev eth0
}
track_script {
chk_nginx
}
}And on the standby node web-02, almost identical except state BACKUP and priority 150:
vrrp_instance VI_1 {
state BACKUP
interface eth0
virtual_router_id 51
priority 150
advert_int 1
virtual_ipaddress {
192.168.1.10/24 dev eth0
}
track_script {
chk_nginx
}
}So that failover really only happens if the service (not just the OS) has a problem, we define a track_script checking nginx. This is a very important pattern: if nginx dies but the server stays alive, the VIP should still move to the other node — because users still can't access the service.
#!/usr/bin/env bash
if ! pidof nginx > /dev/null; then
exit 1
fi
exit 0systemctl restart keepalived
ip addr show eth0 | grep 192.168.1.10 # on web-01: the VIP shows upImportant
Test failover for real: stop nginx on web-01 (systemctl stop nginx) then check with ip addr show eth0 on web-02 — within seconds the VIP must appear on web-02, and curl http://192.168.1.10 must still succeed. If you never test failover, you only have optimism, not high availability. An untested Keepalived is worth the same as a safety net never stretched.
HA answers "how services stay alive"; monitoring answers "how we know it's healthy". The most popular stack in the modern Linux/DevOps ecosystem is Prometheus + node_exporter + Grafana, and its architecture differs from traditional monitoring: a pull model.
9100.+-----------+ scrape /metrics +---------------+ query +----------+
| Server 1 | <-----------------> | Prometheus | <--------> | Grafana |
| node_exp | | (time-series)| | (visual) |
+-----------+ +-------+-------+ +----------+
+-----------+ scrape /metrics | |
| Server 2 | <-----------------> | Alertmanager | --> email/Slack
| node_exp | +----------------+
+-----------+Tip
Why pull and not push? Because pull makes the monitoring server the active consumer: it controls the schedule, knows exactly when data was last received, and immediately knows if a node stops reporting (target down). In the push model, a dead node can't "send word" that it's dead — you only know if you stop listening. Pull also makes authentication and control easier: Prometheus just needs access to ports 9100/9090, not the other way around.
curl -LO https://github.com/prometheus/node_exporter/releases/download/v1.8.2/node_exporter-1.8.2.linux-amd64.tar.gz
tar xzf node_exporter-1.8.2.linux-amd64.tar.gz
install -m 755 node_exporter-1.8.2.linux-amd64/node_exporter /usr/local/bin/
useradd -rs /bin/false node_exporter
install -m 644 /dev/stdin /etc/systemd/system/node_exporter.service <<'EOF'
[Unit]
Description=Node Exporter
After=network.target
[Service]
User=node_exporter
ExecStart=/usr/local/bin/node_exporter
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload && systemctl enable --now node_exporterVerify the metrics with curl http://localhost:9100/metrics | head — you'll see thousands of metric lines like node_cpu_seconds_total, node_memory_MemAvailable_bytes, and so on.
Prometheus is configured via prometheus.yml. The most important part is scrape_configs — the list of targets whose metrics must be pulled:
scrape_configs:
- job_name: nodes
static_configs:
- targets:
- web-01:9100
- web-02:9100
scrape_interval: 15sName the job_name line according to the node's role (e.g. web, db, monitoring) so metrics are easy to group in Grafana. Down targets will show on the Prometheus Targets page with a red status.
Grafana connects to Prometheus as a data source, then dashboards can be created manually or imported from the community. The most popular node-exporter dashboard is ID 1860 on grafana.com:
# Open http://<grafana-host>:3000 (default login admin/admin)
# Add data source → Prometheus → URL: http://<prometheus-host>:9090
# Dashboard → Import → ID 1860 → select the Prometheus data sourceWarning
Change Grafana's default admin/admin password right after the first login. A Grafana left with default credentials is the entry point attackers use most often — and since Grafana has read access to all monitoring data, the impact is far bigger than just a "vandalized" dashboard.
Dashboards are only useful if someone is looking at them. For healthy operations, you need automatic alerts. In Prometheus, alerts are defined with PromQL rules. Example: alert when CPU idle is nearly exhausted or the disk is full:
groups:
- name: node-alerts
rules:
- alert: HighCPU
expr: 100 - avg(rate(node_cpu_seconds_total{mode="idle"}[5m])) > 90
for: 5m
labels:
severity: critical
annotations:
summary: "High CPU on {{ $labels.instance }}"
- alert: DiskSpaceLow
expr: node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"} < 0.1
for: 5m
labels:
severity: warning
annotations:
summary: "Disk / down to 10% on {{ $labels.instance }}"Alertmanager receives alerts from Prometheus and groups them before sending to channels. The email/Slack receiver config is in alertmanager.yml:
route:
group_by: [alertname, instance]
receiver: ops-team
receivers:
- name: ops-team
email_configs:
- to: ops@example.com
from: alerts@example.com
smarthost: smtp.example.com:587Note
The golden rule of monitoring: never add an alert you won't follow up on. Every recurring alert without action will train the team to ignore them (alert fatigue), and when a serious alert appears, nobody cares. Start with a few genuinely important alerts (CPU, disk, target down, service down), and keep every alert with a clear response step.
Beyond the Prometheus stack, there are several health check layers complementing the oversight:
Restart=on-failure makes a service restart automatically on crash, and WatchdogSec reports if a service stops responding. This is the free first line of defense.A healthy pattern combines all three: systemd for automatic recovery (restart), Prometheus for historical metrics & alerting, and external checks (like UptimeRobot or monit) to make sure services are reachable from outside — because a dead Prometheus can't report that it's dead.
[Service]
ExecStart=/usr/local/bin/myapp
Restart=on-failure
RestartSec=5
WatchdogSec=30
StartLimitIntervalSec=60
StartLimitBurst=5Time to put it all together. Scenario: two VMs web-01 and web-02 (also the HA nodes from the previous section), plus one VM mon-01 running Prometheus, Grafana, and Alertmanager.
web-01 (nginx + node_exporter) ----+----> mon-01 (Prometheus + Grafana + Alertmanager)
web-02 (nginx + node_exporter) ----+----> scrapes port 9100 every 15s
web-01 & web-02 = Keepalived VIP 192.168.1.10 (failover)The workflow:
node_exporter on web-01 and web-02 (see Step 1 above).mon-01 with scrape_configs pointing to both nodes.mon-01, add Prometheus as a data source, import dashboard 1860.alert.rules.yml to Prometheus.curl http://mon-01:9090/api/v1/targets | head -20{
"status": "success",
"data": {
"activeTargets": [
{"labels": {"instance": "web-01:9100"}, "health": "up"},
{"labels": {"instance": "web-02:9100"}, "health": "up"}
]
}
}Caution
When one node dies, watch the Keepalived failover and monitoring behavior simultaneously: if Keepalived works, users won't feel anything (the VIP moves to the healthy node) — but Prometheus must still report that web-01 is down, because it's infrastructure needing attention. HA and monitoring are not substitutes for each other; HA reduces the impact of failure, monitoring ensures the failure doesn't go unnoticed.
Here are the failure patterns most commonly found when building HA & monitoring:
| Pitfall | Symptom | Solution |
|---|---|---|
| Target unreachable due to firewall | Prometheus shows the target down even though the node is healthy | Open port 9100 (and 9090/3000) in the node's firewall |
| Exporter not enabled in systemd | Empty metrics or target down after reboot | systemctl enable --now node_exporter |
| Retention not configured | Monitoring disk full within a few weeks | Set --storage.tsdb.retention.time according to capacity |
| Prometheus doesn't scrape itself | No metrics about its own health | Add the localhost:9090 target |
| VIP doesn't move because priorities are equal | Failover never happens | Set priority MASTER > BACKUP (e.g. 200 vs 150) |
| Firewall blocks VRRP | Both nodes claim the VIP → conflict | Open the VRRP protocol (112) between nodes |
| Default Grafana credentials left | Monitoring stack accessed by third parties | Change the admin password on first login |
| Alerts without follow-up | Alert fatigue, numb team | Only create alerts that have a response runbook |
The firewall case deserves to be an example. It's the most common cause of a down Prometheus target — not because the node is dead, but because port 9100 isn't allowed in from the Prometheus server:
# target stays down, even though curl localhost:9100 works
ufw allow from 192.168.1.20 to any port 9100 proto tcp
systemctl reload ufwIn this episode we built two operational pillars that make infrastructure truly production-ready: High Availability with Keepalived and virtual IPs — services stay alive when one node dies; and the monitoring stack with node_exporter, Prometheus, Grafana, and Alertmanager — all servers measured and problems detected before they become disasters. You also understood the layered health check pattern (systemd for automatic recovery, Prometheus for observability, external checks for availability) and the common mistakes to avoid.
Almost done. You now have: hardened servers, tested backups, running containers and virtualization, centralized authentication, plus HA and monitoring. All those components are now ready to be assembled into one whole system. In episode 30 — the final episode of the Learn Linux series — we'll build a complete production-grade Linux server setup: assembling all the lessons from episode 0 to 29 into one comprehensive case study, complete with a production readiness checklist and a summary of this long journey. See you in the final episode!