Monitoring server performance is an essential step to ensure reliability, stability, and efficient resource usage. Prometheus is an open-source monitoring and alerting toolkit widely used for this purpose. This step-by-step guide covers Linux Server Performance Monitoring with Prometheus, Node Exporter, and Alertmanager.
Prometheus can track key metrics such as CPU usage, memory consumption, disk I/O, and network activity. These metrics are usually exposed by exporters like Node Exporter, which integrates easily with Prometheus.
You can practice this setup on any Linux server or cloud VM. If you don’t have one yet, you can quickly spin up a server from PerLod Hosting, which provides ready-to-use Linux environments ideal for monitoring labs.
What You Will Build: Linux Server Performance Monitoring with Prometheus
Before we dive into the steps, here is what you will build in this guide.
- Prometheus: The database and collector of metrics.
- Node Exporter: Runs on servers to expose CPU, memory, disk, and network stats.
- Recording Rules: Save heavy queries as ready-to-use metrics.
- Alertmanager: Sends alerts (Slack, email, etc.), groups and silences them.
- Blackbox Exporter (optional): Checks websites, APIs, or endpoints.
- Grafana (optional): Dashboards to visualize everything.
Prometheus, Alertmanager, Grafana, and Blackbox will be set up on the Prometheus host (monitoring server). Node Exporter will be set up on every target server.
Prerequisites for Performance Monitoring with Prometheus
Before installing Prometheus, you must make sure your server is ready. If these basics are missing, the setup may fail or give wrong results.
1. You need a VM or host for Prometheus with 2 vCPU, 4GB RAM, 20–50GB SSD or NVMe for TSDB or WAL to start.
2. Root or sudo access.
3. Open ports:
- Prometheus: 9090
- Alertmanager: 9093
- Node Exporter: 9100
- Blackbox Exporter: 9115
4. Time sync (chrony or systemd-timesyncd): Metrics are timestamped, so clocks must be accurate.
Set up Prometheus for Testing on One Machine
This step is for testing. You can set up Prometheus, Node Exporter, Alertmanager, and Grafana quickly using Docker Compose, all on one machine. It’s not production-ready, but it lets you learn fast.
Create a Docker Compose YAML file with your desired text editor:
Add the following configuration to the file. Key parts include:
- image: prom/prometheus:latest. It tells Docker to use the Prometheus container image.
- ports: ["9090:9090"]. It makes Prometheus web UI available on your host machine’s port 9090.
- volumes: It mounts configs from your host to the container.
- command: It passes flags to the Prometheus process.
1services:2 prometheus:3 image: prom/prometheus:latest4 container_name: prometheus5 ports: ["9090:9090"]6 volumes:7 - ./prometheus:/etc/prometheus8 - promdata:/prometheus9 command:10 - --config.file=/etc/prometheus/prometheus.yml11 - --storage.tsdb.path=/prometheus12 - --storage.tsdb.retention.time=15d13 - --web.enable-lifecycle14 - --storage.tsdb.wal-compression15 restart: unless-stopped16 17 node-exporter:18 image: prom/node-exporter:latest19 container_name: node-exporter20 network_mode: host21 pid: host22 volumes:23 - /proc:/host/proc:ro24 - /sys:/host/sys:ro25 - /:/rootfs:ro26 command:27 - --path.procfs=/host/proc28 - --path.sysfs=/host/sys29 - --path.rootfs=/rootfs30 - --collector.filesystem.ignored-mount-points="^/(sys|proc|dev|host|etc)($$|/)"31 restart: unless-stopped32 33 alertmanager:34 image: prom/alertmanager:latest35 container_name: alertmanager36 ports: ["9093:9093"]37 volumes:38 - ./alertmanager:/etc/alertmanager39 command: ["--config.file=/etc/alertmanager/alertmanager.yml"]40 restart: unless-stopped41 42 blackbox:43 image: prom/blackbox-exporter:latest44 container_name: blackbox45 ports: ["9115:9115"]46 volumes:47 - ./blackbox:/etc/blackbox_exporter48 restart: unless-stopped49 50 grafana:51 image: grafana/grafana:latest52 container_name: grafana53 ports: ["3000:3000"]54 environment:55 - GF_SECURITY_ADMIN_USER=admin56 - GF_SECURITY_ADMIN_PASSWORD=admin57 volumes:58 - grafana_data:/var/lib/grafana59 restart: unless-stopped60 61volumes:62 promdata: {}63 grafana_data: {}
After creating minimal configs, you can run the Docker Compose container:
Tip: For production, pin image tags to specific versions after testing.
Install Prometheus Natively on Linux (Systemd)
In this step, you can install Prometheus natively on Linux, managed by systemd. This is more reliable for production than Docker.
Create a dedicated user and directories on your Prometheus host with the following commands:
sudo useradd --no-create-home --shell /usr/sbin/nologin prometheus || truesudo mkdir -p /etc/prometheus /var/lib/prometheussudo chown -R prometheus:prometheus /etc/prometheus /var/lib/prometheus
Then, download the latest version of Prometheus with the following command. Replace the version with the latest stable. This is an example version:
VER="2.53.0"cd /tmpcurl -fL -O https://github.com/prometheus/prometheus/releases/download/v${VER}/prometheus-${VER}.linux-amd64.tar.gz
Then, extract and install Prometheus with the following commands:
sudo tar -xzf prometheus-${VER}.linux-amd64.tar.gz -C /tmpcd /tmp/prometheus-${VER}.linux-amd64sudo install -o root -g root -m 0755 prometheus promtool /usr/local/bin/sudo cp -r console_libraries consoles /etc/prometheus/
Next, you must configure a basic Prometheus YAML file. Create the file with your desired text editor:
nano /etc/prometheus/prometheus.yml
Add the following basic configuration to the file:
1global:2 scrape_interval: 15s3 evaluation_interval: 15s4 5alerting:6 alertmanagers:7 - static_configs:8 - targets: ["localhost:9093"]9 10rule_files:11 - /etc/prometheus/rules.yml12 13scrape_configs:14 - job_name: prometheus15 static_configs:16 - targets: ["localhost:9090"]17 18 - job_name: node_exporter19 static_configs:20 - targets: [21 "localhost:9100"22 ]
The "global.scrape_interval: 15s" means Prometheus scrapes metrics every 15 seconds.
To run Prometheus as a service, you must create the systemd unit file:
nano /etc/systemd/system/prometheus.service
Add the following config to the file:
1[Unit]2Description=Prometheus TSDB3Wants=network-online.target4After=network-online.target5 6[Service]7User=prometheus8Group=prometheus9Type=simple10ExecStart=/usr/local/bin/prometheus \11--config.file=/etc/prometheus/prometheus.yml \12--storage.tsdb.path=/var/lib/prometheus \13--storage.tsdb.retention.time=15d \14--storage.tsdb.wal-compression \15--web.enable-lifecycle16ExecReload=/bin/kill -HUP $MAINPID17Restart=on-failure18 19[Install]20WantedBy=multi-user.target
Then, start and enable the service with the following commands:
sudo systemctl daemon-reloadsudo systemctl enable --now prometheussudo systemctl status prometheus --no-pager -l
To verify that Prometheus and Node Exporter are up and running, you can navigate to the following URL:
http://PROMETHEUS_HOST:9090/targets
Install Node Exporter on Every Linux Server You Want to Monitor
Now you must install Node Exporter on every Linux server you want to monitor. It collects CPU, memory, disk, and network metrics and exposes them on port 9100.
First, create a non-login user for Node Exporter with the following command:
sudo useradd --no-create-home --shell /usr/sbin/nologin nodeusr || true
Then, download the latest binary package of Node Exporter. This is an example version; update it if needed.
VER="1.8.1"cd /tmpcurl -fL -O https://github.com/prometheus/node_exporter/releases/download/v${VER}/node_exporter-${VER}.linux-amd64.tar.gz
Extract the downloaded file and install it with the following commands:
sudo tar -xzf node_exporter-${VER}.linux-amd64.tar.gz -C /usr/local/bin --strip-components=1 node_exporter-${VER}.linux-amd64/node_exporter sudo chown root:root /usr/local/bin/node_exporter
To run Node Exporter as a service, create the systemd unit file:
nano /etc/systemd/system/node_exporter.service
Add the following content to it:
1[Unit]2Description=Prometheus Node Exporter3Wants=network-online.target4After=network-online.target5 6[Service]7User=nodeusr8Group=nodeusr9Type=simple10ExecStart=/usr/local/bin/node_exporter \11 --web.listen-address=":9100" \12 --collector.tcpstat \13 --collector.processes \14 --collector.filesystem.ignored-mount-points="^/(sys|proc|dev|run|var/lib/docker/.+|snap)($$|/)" \15 --collector.filesystem.mount-points-exclude="^/(sys|proc|dev|run|var/lib/docker/.+|snap)($$|/)"16Restart=on-failure17 18[Install]19WantedBy=multi-user.target
Then, start and enable Node Exporter with:
sudo systemctl daemon-reloadsudo systemctl enable --now node_exportersudo systemctl status node_exporter --no-pager -l
To test the metric locally, you can run:
curl -fsS http://localhost:9100/metrics | head
It shows the first lines of metrics to confirm it works.
Now you can add this host’s IP:9100 to the Prometheus node_exporter job’s targets list. Remember to allow TCP 9100 from the Prometheus server only.
Set up Blackbox Exporter (Optional)
While Node Exporter shows internal health like CPU, disk, and memory, Blackbox Exporter checks from the “outside”. For example, can your website be reached? Can it be pinged?
Create the Blackbox config file:
nano /etc/blackbox_exporter/blackbox.yml
Add the following configuration to the file:
1modules:2 http_2xx:3 prober: http4 timeout: 5s5 http:6 preferred_ip_protocol: "ip4"7 icmp:8 prober: icmp9 timeout: 3s
- http_2xx: Checks if an HTTP endpoint returns a success code (200).
- icmp: Checks if a host responds to ping.
Then, you must add this Prometheus scrape job for Blackbox to the prometheus.yml file:
1- job_name: blackbox_http2 metrics_path: /probe3 params:4 module: [http_2xx]5 static_configs:6 - targets:7 - https://example.com/8 - https://api.example.com/health9 relabel_configs:10 - source_labels: [__address__]11 target_label: __param_target12 - source_labels: [__param_target]13 target_label: instance14 - target_label: __address__15 replacement: 127.0.0.1:9115
Signals you must watch for include:
probe_success, probe_http_status_code, probe_duration_seconds, probe_dns_lookup_time_seconds, probe_tcp_connection_duration_seconds
Prometheus Recording Rules (Pre-computed Metrics)
Prometheus queries can get heavy, especially with rate() or avg by. Recording rules save results into new pre-computed metrics, so queries and dashboards are faster.
Create the recording rules file on the Prometheus host:
nano /etc/prometheus/rules.yml
Then, add the following rules to the file:
1groups:2- name: sre-core3 interval: 15s4 rules:5 - record: node:cpu_utilization:avg5m6 expr: 100 * (1 - avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])))7 8 - record: node:cpu_iowait:avg5m9 expr: 100 * avg by (instance) (rate(node_cpu_seconds_total{mode="iowait"}[5m]))10 11 - record: node:cpu_steal:avg5m12 expr: 100 * avg by (instance) (rate(node_cpu_seconds_total{mode="steal"}[5m]))13 14 - record: node:load1_per_core15 expr: node_load1 / count by (instance) (node_cpu_seconds_total{mode="system"})16 17 - record: node:memory_used_percent18 expr: 100 * (1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)19 20 - record: node:disk_util_percent21 expr: 100 * max by (instance, device) (22 rate(node_disk_io_time_seconds_total{device!~"loop|ram|fd|sr.*"}[5m])23 )24 25 - record: node:disk_read_latency_ms26 expr: 1000 * (rate(node_disk_read_time_seconds_total{device!~"loop|ram|fd|sr.*"}[5m])27 / rate(node_disk_reads_completed_total{device!~"loop|ram|fd|sr.*"}[5m]))28 29 - record: node:disk_write_latency_ms30 expr: 1000 * (rate(node_disk_write_time_seconds_total{device!~"loop|ram|fd|sr.*"}[5m])31 / rate(node_disk_writes_completed_total{device!~"loop|ram|fd|sr.*"}[5m]))32 33 - record: node:fs_used_percent34 expr: 100 * (1 - node_filesystem_avail_bytes{fstype!~"tmpfs|overlay|aufs|squashfs"} \35 / node_filesystem_size_bytes{fstype!~"tmpfs|overlay|aufs|squashfs"})36 37 - record: node:net_rx_bytes_per_s38 expr: sum by (instance, device) (rate(node_network_receive_bytes_total{device!~"lo"}[5m]))39 40 - record: node:net_tx_bytes_per_s41 expr: sum by (instance, device) (rate(node_network_transmit_bytes_total{device!~"lo"}[5m]))42 43 - record: node:tcp_retrans_per_s44 expr: rate(node_netstat_Tcp_RetransSegs[5m])45 46 - record: blackbox:http_availability47 expr: avg by (instance) (probe_success)48 49 - record: blackbox:http_duration_seconds50 expr: avg by (instance) (probe_duration_seconds)
Check if the rules file is valid with the command below:
promtool check rules /etc/prometheus/rules.yml
Next, run the following command to reload the config without restarting:
curl -X POST http://localhost:9090/-/reload
sudo systemctl reload prometheus
Set up Alerts for Prometheus that Catch Bottlenecks Early
Alerts notify you when something’s wrong, like high CPU, full disk, or a host going down. Prometheus checks alert rules and sends them to Alertmanager.
Create the alerts file and then include this file under rule_files in prometheus.yml:
nano /etc/prometheus/alerts.yml
Add the following alerts to the file:
1groups:2- name: node-alerts3 rules:4 - alert: InstanceDown5 expr: up == 06 for: 5m7 labels: { severity: critical }8 annotations:9 summary: "Instance down ({{ $labels.instance }})"10 description: "No scrape targets responding for 5m."11 12 - alert: HighCPU13 expr: node:cpu_utilization:avg5m > 8514 for: 10m15 labels: { severity: warning }16 annotations:17 summary: "High CPU on {{ $labels.instance }}"18 description: "CPU > 85% (avg 5m) for 10m; check hot processes and scaling."19 20 - alert: HighCPU_IOWait21 expr: node:cpu_iowait:avg5m > 1022 for: 10m23 labels: { severity: warning }24 annotations:25 summary: "High IO wait on {{ $labels.instance }}"26 description: "CPU waiting on disk > 10% for 10m; suspect disk bottleneck."27 28 - alert: LoadExceedsCores29 expr: node:load1_per_core > 1.030 for: 10m31 labels: { severity: warning }32 annotations:33 summary: "CPU saturation on {{ $labels.instance }}"34 description: "Load1 per core > 1 indicates runnable queue backlog."35 36 - alert: MemoryPressure37 expr: node:memory_used_percent > 9038 for: 10m39 labels: { severity: warning }40 annotations:41 summary: "Memory pressure on {{ $labels.instance }}"42 description: "Available memory < 10% for 10m; check caches/process leaks."43 44 - alert: DiskUtilHigh45 expr: node:disk_util_percent > 8046 for: 10m47 labels: { severity: warning }48 annotations:49 summary: "Disk busy on {{ $labels.instance }} ({{ $labels.device }})"50 description: "Disk io_time > 80% for 10m; investigate latency & queue."51 52 - alert: DiskLatencyHigh53 expr: (node:disk_read_latency_ms > 50) or (node:disk_write_latency_ms > 50)54 for: 10m55 labels: { severity: warning }56 annotations:57 summary: "Disk latency high on {{ $labels.instance }}"58 description: "Average disk latency > 50 ms; suspect underlying storage."59 60 - alert: FilesystemFilling61 expr: predict_linear(node_filesystem_avail_bytes{fstype!~"tmpfs|overlay|aufs|squashfs"}[6h], 4*3600) < 062 for: 15m63 labels: { severity: warning }64 annotations:65 summary: "Filesystem filling soon on {{ $labels.instance }}"66 description: "Projected to fill in < 4h. Act before outage."67 68 - alert: NetworkErrors69 expr: rate(node_network_receive_errs_total[5m]) > 0 or rate(node_network_transmit_errs_total[5m]) > 070 for: 5m71 labels: { severity: warning }72 annotations:73 summary: "NIC errors on {{ $labels.instance }}"74 description: "Persistent NIC errors/drops; check cabling, MTU, driver."75 76- name: blackbox-alerts77 rules:78 - alert: EndpointDown79 expr: blackbox:http_availability < 180 for: 2m81 labels: { severity: critical }82 annotations:83 summary: "Endpoint down ({{ $labels.instance }})"84 description: "Blackbox probe failing."85 86 - alert: SlowEndpoint87 expr: blackbox:http_duration_seconds > 188 for: 5m89 labels: { severity: warning }90 annotations:91 summary: "Slow endpoint ({{ $labels.instance }})"92 description: "End-to-end latency >1s; check DNS/TCP/SSL/app."
You can reload and validate the configuration with the following commands:
promtool check rules /etc/prometheus/alerts.ymlcurl -X POST http://localhost:9090/-/reload
Set up Alertmanager for Prometheus
Prometheus fires alerts, but Alertmanager decides what to do. For example, send to Slack or email, group similar alerts, and silence alerts temporarily.
Create the user, download, and set up Alertmanager with the following commands:
sudo useradd --no-create-home --shell /usr/sbin/nologin alertmanager || trueVER="0.27.0"cd /tmpcurl -fL -O https://github.com/prometheus/alertmanager/releases/download/v${VER}/alertmanager-${VER}.linux-amd64.tar.gzsudo tar -xzf alertmanager-${VER}.linux-amd64.tar.gz -C /tmpcd /tmp/alertmanager-${VER}.linux-amd64sudo install alertmanager amtool /usr/local/bin/sudo mkdir -p /etc/alertmanagersudo chown -R alertmanager:alertmanager /etc/alertmanager
Then, create the Alertmanager YAML file:
nano /etc/alertmanager/alertmanager.yml
Add the following content to the file with Slack and email examples:
1global:2 resolve_timeout: 5m3 4route:5 receiver: default6 group_by: [alertname, instance]7 group_wait: 30s8 group_interval: 3m9 repeat_interval: 4h10 11receivers:12 - name: default13 slack_configs:14 - send_resolved: true15 api_url: "https://hooks.slack.com/services/XXX/YYY/ZZZ"16 channel: "#alerts"17 title: "{{ .CommonLabels.alertname }}: {{ .CommonLabels.instance }}"18 text: "{{ range .Alerts }}*{{ .Annotations.summary }}*\n{{ .Annotations.description }}\n{{ end }}"19 email_configs:20 - to: ops@example.com21 from: monitor@example.com22 smarthost: smtp.example.com:58723 auth_username: monitor@example.com24 auth_identity: monitor@example.com25 auth_password: "REDACTED"
To run Alertmanager as a service, create a systemd unit file:
nano /etc/systemd/system/alertmanager.service
1[Unit]2Description=Prometheus Alertmanager3After=network-online.target4Wants=network-online.target5 6[Service]7User=alertmanager8Group=alertmanager9Type=simple10ExecStart=/usr/local/bin/alertmanager \11 --config.file=/etc/alertmanager/alertmanager.yml \12 --storage.path=/var/lib/alertmanager13Restart=on-failure14 15[Install]16WantedBy=multi-user.target
Then start and enable Alertmanager with the following commands:
sudo mkdir -p /var/lib/alertmanager && sudo chown alertmanager:alertmanager /var/lib/alertmanagersudo systemctl daemon-reloadsudo systemctl enable --now alertmanagersudo systemctl status alertmanager --no-pager -l
Add Grafana Dashboard (Optional)
Grafana gives you nice dashboards and graphs. Install Grafana from the official repository. Then:
- Point a Prometheus data source at
http://PROMETHEUS_HOST:9090.
- Import a community Node Exporter dashboard and a Blackbox dashboard for quick visibility.
- Build panels with the recording rules to keep dashboards fast.
Useful PromQL Queries
PromQL is Prometheus’s query language. This step shows useful queries for CPU, disk, memory, and more. All queries are aggregated by instance so you see which server is hot.
CPU:
100 * (1 - avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m]))) 100 * avg by (instance) (rate(node_cpu_seconds_total{mode="iowait"}[5m]))100 * avg by (instance) (rate(node_cpu_seconds_total{mode="steal"}[5m])) node_load1 / count by (instance) (node_cpu_seconds_total{mode="system"})
100 * (1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)
100 * max by (instance, device) (rate(node_disk_io_time_seconds_total{device!~"loop|ram|fd|sr.*"}[5m])) 1000 * (rate(node_disk_read_time_seconds_total[5m]) / rate(node_disk_reads_completed_total[5m]))1000 * (rate(node_disk_write_time_seconds_total[5m]) / rate(node_disk_writes_completed_total[5m])) 100 * (1 - node_filesystem_avail_bytes{fstype!~"tmpfs|overlay|aufs|squashfs"} / node_filesystem_size_bytes{fstype!~"tmpfs|overlay|aufs|squashfs"})
sum by (instance, device) (rate(node_network_receive_bytes_total{device!~"lo"}[5m]))sum by (instance, device) (rate(node_network_transmit_bytes_total{device!~"lo"}[5m])) rate(node_network_receive_errs_total[5m])rate(node_network_transmit_errs_total[5m])rate(node_network_receive_drop_total[5m])rate(node_network_transmit_drop_total[5m])rate(node_netstat_Tcp_RetransSegs[5m])
avg by (instance) (probe_success)max by (instance, phase) (probe_duration_seconds)
Check Bottlenecks Systematically: USE And RED
There are 2 troubleshooting frameworks, including USE and RED. They help you check bottlenecks systematically instead of guessing.
USE (Utilization, Saturation, Errors): For system resources. Example: CPU usage %, load average, disk latency, and network errors.
RED (Rate, Errors, Duration): For services. Example: request rate, error rate, and response duration.
Conclusion
You now have a complete path from a local Docker lab to a production, systemd-based monitoring stack. Prometheus collects metrics, Node Exporter exposes host signals, recording rules precompute critical SRE signals, Alertmanager routes and silences alerts, and Blackbox and Grafana complete end-to-end checks and visualization.
Looking for reliable Linux hosting to run this stack in production? Try PerLod Bare Metal Hosting, which offers optimized servers for monitoring and observability workloads.
We hope you enjoy this guide on Linux Server Performance Monitoring with Prometheus. Subscribe to X and Facebook channels to get the latest articles and news.
For further reading:
Move from Shared Hosting to VPS without Downtime
JMeter VPS Load Testing: Advanced Step-By-Step Guide
Linux kernel live patching with Zero Downtime