Linux dedicated server monitoring turns a server you only check when something breaks into one you can understand at a glance. In this tutorial, you will deploy Prometheus, Node Exporter, cAdvisor, and Grafana with Docker Compose, then build a dashboard covering CPU usage, RAM, disk usage, disk I/O, network traffic, system load, uptime, filesystem usage, and Docker container metrics.
What you will have at the end: a working Grafana dashboard, a secured monitoring stack, and a starter set of alert rules for your dedicated server.
1. Why Linux Dedicated Server Monitoring Matters
A dedicated server gives you exclusive access to physical hardware: every CPU core, every gigabyte of RAM, every NVMe drive, and the full network port. That exclusivity is the reason to choose a dedicated server, but it also means nobody else is watching those resources for you.
A server can be online and still be unhealthy. Common examples:
-
CPU usage stays high because of a runaway process or a heavy scheduled job.
-
Available memory drops until the kernel starts swapping or the OOM killer stops an application.
-
A filesystem approaches 100% capacity, or runs out of inodes while free space still looks fine.
-
Disk latency rises during backups, database writes, or log processing.
-
A network interface saturates during a traffic spike or large transfer.
-
One Docker container slowly consumes more memory than the rest of the workload combined.
-
The server reboots unexpectedly, and nobody notices.
Command-line tools such as top, htop,
free, df, iostat, and ss are excellent
for live troubleshooting, but they show the present moment. Prometheus adds historical
time-series data, and Grafana visualizes it, so you can see trends, compare load before and
after a deployment, and get alerted before a small problem becomes an outage.
This approach works for bare metal servers, database servers, application servers, virtualization hosts, game servers, and any dedicated server running Docker workloads.
2. How the Monitoring Stack Works
Four open-source components make up this setup. Each has one job.
| Component | Role | What it provides |
|---|---|---|
| Node Exporter | Host metrics exporter | CPU, memory, load, filesystems, disk statistics, network statistics, uptime |
| cAdvisor | Container metrics exporter | Per-container CPU, memory, network and filesystem usage |
| Prometheus | Metrics collection and time-series database | Scrapes exporters on a schedule, stores data, answers PromQL queries |
| Grafana | Visualization and alerting layer | Dashboards, panels, variables, alert rules |
The data flows in one direction:
Linux dedicated server
├── Node Exporter (host metrics, port 9100) ──┐
└── cAdvisor (container metrics, port 8080) ──┤
▼
Prometheus (port 9090)
│ PromQL queries
▼
Grafana (port 3000)
▼
Monitoring dashboard
Prometheus uses a pull model: it periodically scrapes the
/metrics HTTP endpoint of each exporter and stores the samples. Grafana never
talks to the exporters directly; it queries Prometheus using PromQL.
3. Prerequisites
You will need:
-
A Linux dedicated server running Ubuntu 22.04 or newer (the commands work on most Debian-based distributions)
-
Root or sudo access
-
Docker Engine and the Docker Compose plugin installed. If you have not done this yet, follow our guide on how to install Docker on a Linux dedicated server
-
At least 2 GB of free RAM and enough disk space for Prometheus data (plan roughly 1–2 GB per month for a single server at a 15-second scrape interval, more if you add many targets)
-
A firewall such as UFW, or a network-level firewall
Verify Docker before you continue:
docker compose version
Both commands should print a version number.
4. Step 1: Create the Monitoring Directory
Keep the whole stack in one directory so it is easy to back up and move.
cd /opt/server-monitoring
5. Step 2: Configure Prometheus
Create the Prometheus configuration file:
Add this configuration:
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: "prometheus"
static_configs:
- targets: ["prometheus:9090"]
- job_name: "node-exporter"
static_configs:
- targets: ["host.docker.internal:9100"]
- job_name: "cadvisor"
static_configs:
- targets: ["cadvisor:8080"]
What does scrape_interval do? It tells Prometheus how often
to collect metrics. Fifteen seconds is a sound starting point for dedicated server
monitoring. Shorter intervals give finer detail but increase storage use and CPU overhead.
Longer intervals save space but can hide short spikes.
Why host.docker.internal? Node Exporter runs on the host
network so it can see real network interfaces. Prometheus runs in the Compose network, so it
reaches Node Exporter through the host gateway, which the Compose file below maps for you.
6. Step 3: Write the Docker Compose File
services:
prometheus:
image: prom/prometheus:latest
container_name: prometheus
restart: unless-stopped
extra_hosts:
- "host.docker.internal:host-gateway"
ports:
- "127.0.0.1:9090:9090"
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus_data:/prometheus
command:
- "--config.file=/etc/prometheus/prometheus.yml"
- "--storage.tsdb.path=/prometheus"
- "--storage.tsdb.retention.time=30d"
node-exporter:
image: prom/node-exporter:latest
container_name: node-exporter
restart: unless-stopped
network_mode: host
pid: host
command:
- "--path.rootfs=/host"
volumes:
- "/:/host:ro,rslave"
cadvisor:
image: gcr.io/cadvisor/cadvisor:latest
container_name: cadvisor
restart: unless-stopped
ports:
- "127.0.0.1:8080:8080"
privileged: true
devices:
- /dev/kmsg
volumes:
- "/:/rootfs:ro"
- "/var/run:/var/run:ro"
- "/sys:/sys:ro"
- "/var/lib/docker:/var/lib/docker:ro"
- "/dev/disk:/dev/disk:ro"
grafana:
image: grafana/grafana:latest
container_name: grafana
restart: unless-stopped
ports:
- "127.0.0.1:3000:3000"
volumes:
- grafana_data:/var/lib/grafana
volumes:
prometheus_data:
grafana_data:
A few choices in this file are deliberate:
-
Ports bound to 127.0.0.1. Docker-published ports bypass UFW rules, so binding to localhost is the reliable way to keep Prometheus, cAdvisor, and Grafana off the public internet. You will reach them through an SSH tunnel or a reverse proxy (see Step 5).
-
--storage.tsdb.retention.time=30d. This caps how long Prometheus keeps data, so the time-series database does not grow until it fills your disk. Adjust it to your needs.
-
pid: host and network_mode: host on Node Exporter. These give it an accurate view of host processes and real network interfaces.
-
Pin your image versions for production.
latestis convenient in a tutorial, but an unexpected image update can change metric names or behavior. Check each project's release page, replacelatestwith a specific version tag, and test upgrades on a non-critical server first.
7. Step 4: Start the Stack and Verify Exporters
sudo docker compose up -d
sudo docker compose ps
You should see four running containers: prometheus, node-exporter, cadvisor, and grafana. If any are restarting, read their logs:
Verify Node Exporter
You should see Prometheus-formatted text with metric names beginning in
node_, for example node_cpu_seconds_total,
node_memory_MemAvailable_bytes, node_filesystem_avail_bytes,
node_network_receive_bytes_total and node_boot_time_seconds.
Verify cAdvisor
You should see metrics such as
container_cpu_usage_seconds_total, container_memory_usage_bytes,
and container_network_receive_bytes_total.
Verify Prometheus targets
Open an SSH tunnel from your own computer (see Step 5 for details), then
browse to http://localhost:9090 and go to Status → Target
health. The prometheus, node-exporter, and cadvisor targets should all show UP.
You can also run this query in the expression browser:
A value of 1 means Prometheus scraped the target successfully. A value of 0 means the scrape failed.
8. Step 5: Secure Access to the Monitoring Stack
Monitoring endpoints reveal hostnames, interfaces, mount points, container names, and resource patterns. Treat them as sensitive.
-
1. Access the dashboards through an SSH tunnel (the simplest secure option). Run this on your local computer:
ssh -L 3000:127.0.0.1:3000 -L 9090:127.0.0.1:9090 your-user@SERVER-IPThen open
http://localhost:3000for Grafana andhttp://localhost:9090for Prometheus. -
2. Restrict port 9100. Node Exporter uses the host network, so it listens on all interfaces by default. Allow only Docker's internal ranges to reach it and block everyone else:
sudo ufw allow from 172.16.0.0/12 to any port 9100 proto tcp
sudo ufw deny 9100/tcp -
3. For team access, use a reverse proxy with HTTPS and authentication in front of Grafana rather than opening port 3000 directly.
-
4. Change the default Grafana password immediately. Grafana's initial login is admin / admin and it prompts you to set a new password on first sign-in. Use a strong, unique one.
-
5. Keep the stack updated and remove access for accounts that no longer need it.
9. Step 6: Connect Grafana to Prometheus
-
Open Grafana at
http://localhost:3000(through your SSH tunnel). -
Go to Connections → Data sources → Add new data source.
-
Select Prometheus.
-
Set the URL to
http://prometheus:9090. -
Click Save & test.
Use the service name prometheus, not the server's
public IP. Docker Compose resolves service names inside its own network, and it keeps
Grafana traffic private.
10. Step 7: Build the Grafana Dashboard (PromQL Queries)
Create a new dashboard (Dashboards → New → New dashboard → Add visualization), choose the Prometheus data source, and add one panel per query below. A readable structure is:
Linux Dedicated Server Monitoring ├── Server Overview (stat panels) ├── CPU ├── Memory ├── Load & Uptime ├── Disk & Filesystem ├── Network └── Docker Containers
Test each query in the Prometheus expression browser first. If it returns data there, it will work in Grafana.
CPU usage
Overall CPU utilization as a percentage:
100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)
Panel type: Time series | Unit: Percent (0-100) | Legend:
{{instance}}
Per-core CPU usage helps you spot one saturated core hiding behind a moderate average:
100 - (rate(node_cpu_seconds_total{mode="idle"}[5m]) * 100)
RAM and memory usage
100 * (1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)
Unit: Percent (0-100). To show available memory in human-readable form:
node_memory_MemAvailable_bytes
Unit: Bytes (IEC), so Grafana displays MiB or GiB automatically.
Swap pressure is worth its own panel:
rate(node_vmstat_pswpin[5m]) + rate(node_vmstat_pswpout[5m])
Disk usage (root filesystem)
100 * (1 - node_filesystem_avail_bytes{fstype!~"tmpfs|overlay|squashfs", mountpoint="/"}
/ node_filesystem_size_bytes{fstype!~"tmpfs|overlay|squashfs", mountpoint="/"})
To see every mounted filesystem, remove the mountpoint="/"
filter and set the legend to {{mountpoint}}.
Disk I/O
Disk capacity and disk performance are separate questions. A drive can have plenty of free space and still be saturated.
Read and write throughput (unit Bytes/sec (IEC)):
rate(node_disk_read_bytes_total[5m]) rate(node_disk_written_bytes_total[5m])
IOPS (operations per second):
rate(node_disk_reads_completed_total[5m]) rate(node_disk_writes_completed_total[5m])
Disk busy time, a useful saturation signal:
rate(node_disk_io_time_seconds_total[5m]) * 100
On NVMe dedicated servers, look at throughput, IOPS and busy time together. Capacity alone says very little about storage behavior. Databases, backup jobs, log processing and virtualization workloads are the usual sources of heavy I/O.
Network traffic
Receive and transmit rates, excluding loopback and Docker virtual interfaces (unit Bytes/sec (IEC)):
sum by (instance) (rate(node_network_receive_bytes_total{device!~"lo|docker.*|veth.*|br-.*"}[5m]))
sum by (instance) (rate(node_network_transmit_bytes_total{device!~"lo|docker.*|veth.*|br-.*"}[5m]))
For a server with several physical interfaces or a bonded uplink, drop
the sum by and keep the device label to see each interface separately.
To view throughput in bits per second (how network ports are usually rated), multiply by 8 and set the unit to bits/sec.
System load average
node_load1 node_load5 node_load15
A load number means nothing without the CPU count. A load of 4 is fully busy on a 4-core server and light on a 64-core server. Normalize it:
node_load1 / count without (cpu, mode) (node_cpu_seconds_total{mode="idle"})
A value near or above 1 means the server has as much runnable work as it has cores.
Server uptime
time() - node_boot_time_seconds
Use a Stat panel with the unit set to seconds or Grafana's duration format. A sudden drop in uptime tells you the server rebooted.
Filesystem and inode monitoring
Free space by filesystem:
node_filesystem_avail_bytes{fstype!~"tmpfs|overlay|squashfs"}
Inode usage percentage. A filesystem can run out of inodes while free space still looks healthy, especially with millions of small files:
100 * (1 - node_filesystem_files_free{fstype!~"tmpfs|overlay|squashfs"}
/ node_filesystem_files{fstype!~"tmpfs|overlay|squashfs"})
Docker container metrics
cAdvisor provides these. Label names can vary between cAdvisor versions and container runtimes, so inspect the labels in your own Prometheus before building complex queries.
Container CPU (percent of one core):
sum by (name) (rate(container_cpu_usage_seconds_total{image!=""}[5m])) * 100
Container memory (unit Bytes (IEC)):
sum by (name) (container_memory_working_set_bytes{image!=""})
container_memory_working_set_bytes is generally a better
measure of real memory pressure than container_memory_usage_bytes, because the
latter includes reclaimable page cache.
Container network traffic:
sum by (name) (rate(container_network_receive_bytes_total{image!=""}[5m]))
sum by (name) (rate(container_network_transmit_bytes_total{image!=""}[5m]))
Container filesystem usage:
sum by (name) (container_fs_usage_bytes{image!=""})
Container filesystem usage and host filesystem usage are related but not identical, so read them as separate views.
Recommended dashboard layout
Put a health overview at the top, then detail below it:
| Row | Panels |
|---|---|
| Overview | CPU %, Memory %, Root disk %, Normalized load, Uptime, Targets up |
| CPU & Memory | CPU over time, per-core CPU, memory over time, available memory, swap activity |
| Storage | Filesystem usage, disk read, disk write, IOPS, disk busy time, inode usage |
| Network | Receive, transmit, packets, interface errors |
| Containers | CPU, memory, network receive/transmit, filesystem |
Keep host-level and container-level metrics in separate rows. A focused dashboard is easier to read under pressure than one with fifty panels.
Make the dashboard reusable with variables
If you later monitor several dedicated servers from one Prometheus instance, create an instance variable using:
label_values(node_uname_info, instance)
Then filter panels with {instance=~"$instance"}. You can
add variables for job, filesystem, network interface and container in the same way.
11. Step 8: Add Alerts
A dashboard only helps when someone is looking at it. Alerts tell you when to look. In Grafana, go to Alerting → Alert rules and create rules from these starter queries:
| Condition | Query |
|---|---|
| High CPU |
100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 90
|
| High memory |
100 * (1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) > 90
|
| Root filesystem nearly full |
100 * (1 - node_filesystem_avail_bytes{mountpoint="/",fstype!~"tmpfs|overlay|squashfs"} / node_filesystem_size_bytes{mountpoint="/",fstype!~"tmpfs|overlay|squashfs"}) > 90
|
| Disk will fill within 24 hours |
predict_linear(node_filesystem_avail_bytes{mountpoint="/"}[6h], 24*3600) < 0
|
| Target down | up == 0 |
Set a pending period (for example 5–10 minutes) so short spikes do not trigger notifications. Thresholds are starting points, not rules: the right values depend on your workload, hardware, and what is normal for your server. Every alert should have a documented reason and a response procedure; otherwise it becomes noise.
12. How to Read the Metrics Together
The real value of Linux dedicated server monitoring comes from correlation.
| What you see | What to investigate |
|---|---|
| High CPU + high load | Sustained CPU pressure: heavy processes, recent deployments, scheduled jobs, per-core saturation |
| High load + moderate CPU | Processes waiting on I/O: disk latency, storage busy time, network or external dependencies |
| High memory + rising swap activity | Memory-heavy applications, container limits, database configuration, possible memory leaks |
| Low free disk + high disk writes | Logs, backups, database files, temporary files, container data |
| High network traffic | Application traffic, backups, file transfers, specific containers, unexpected connections |
| Uptime reset | Unplanned reboot: check journalctl -b -1,
hardware logs and power events |
13. Prometheus and Grafana vs. Traditional Linux Commands
They complement each other rather than compete.
| Tool | Best for |
|---|---|
top / htop |
Live process and CPU/memory inspection |
free |
Current memory statistics |
df / du |
Filesystem capacity and directory sizes |
iostat |
Live disk I/O statistics |
ss |
Network sockets |
| Prometheus | Historical time-series metrics |
| Grafana | Dashboards, trends and alerting |
Use Grafana to find when and where something changed, then use the command-line tools to find why.
14. Troubleshooting
A Prometheus target shows DOWN
sudo docker compose ps
curl -s http://127.0.0.1:9100/metrics | head
curl -s http://127.0.0.1:8080/metrics | head
If the Node Exporter target is down, confirm the UFW rule from Step 5
allows Docker's address range, and confirm host.docker.internal resolves inside
the Prometheus container.
Grafana cannot connect to Prometheus
Check that the Prometheus container is running
(sudo docker compose ps prometheus) and that the data source URL is exactly
http://prometheus:9090. Using the server's public IP instead of the service
name is a common cause of failures.
Host metrics work, but container metrics are missing
-
Confirm the cAdvisor container is running and check its logs.
-
Confirm the Docker paths are mounted as shown in the Compose file.
-
Open cAdvisor's
/metricsendpoint withcurl. -
Search Prometheus for
container_cpu_usage_seconds_total. -
Inspect the real labels on those series. If
imageornameare empty in your environment, adjust the selectors.
Disk usage in Grafana does not match df
Small differences are normal. df reports usable space for
non-root users, while the PromQL query uses available bytes. Also make sure you are
filtering out tmpfs, overlay, and squashfs
filesystems.
15. Best Practices for Long-Term Monitoring
-
Monitor trends, not just current values. History shows whether a server is slowly getting busier.
-
Track capacity and performance separately. Disk space and disk I/O answer different questions, and so do memory used and memory available.
-
Keep scrape intervals sensible. Start at 15 seconds and tune from there.
-
Isolate monitoring for critical infrastructure. Running Prometheus and Grafana on a separate server means you still have visibility when the monitored server fails.
-
Plan storage and retention. Prometheus disk use grows with scrape frequency, number of targets, metric cardinality, and retention period. Back up Grafana dashboards, and consider remote storage for long-term data.
-
Protect every endpoint. Use firewall rules, private networking, a VPN, or an authenticated reverse proxy with HTTPS.
-
Document your alerts. An alert without a runbook is noise.
-
Test upgrades before production. Pin image versions and roll out changes to a staging server first.
16. Frequently Asked Questions
What is the best way to monitor a Linux dedicated server?
For most teams, the combination of Prometheus, Node Exporter, and Grafana is the standard open-source choice. Node Exporter exposes host metrics, Prometheus stores them as time-series data, and Grafana visualizes them and sends alerts. Add cAdvisor if the server runs Docker containers.
Do I need Docker to run Prometheus and Grafana?
No. Both can be installed as native packages or binaries. Docker Compose is used here because it keeps the four components together, is easy to reproduce, and is simple to upgrade.
How much overhead does Prometheus monitoring add to a dedicated server?
For a single server at a 15-second scrape interval, the overhead is small: Node Exporter typically uses little CPU and memory, while Prometheus storage grows with the number of metrics and your retention period. Measure your own stack, since exporters, collectors enabled, and container counts all affect it.
Which metrics matter most for a dedicated server?
Start with CPU utilization, available memory, filesystem usage (including inodes), disk I/O, network throughput, load average normalized by core count, and uptime. Add container metrics if you run Docker.
Can I monitor more than one dedicated server with this setup?
Yes. Install Node Exporter on each additional server, add them as scrape targets
in prometheus.yml, and use an instance variable in Grafana to
switch between servers.
Should Prometheus run on the same server it monitors?
It works for learning and small setups, but for critical production infrastructure a separate monitoring server is safer, because monitoring stays available if the primary server goes down.
How do I keep Prometheus from filling my disk?
Set a retention period with --storage.tsdb.retention.time (or a size
limit with --storage.tsdb.retention.size), and alert on filesystem
usage for the volume that stores Prometheus data.
Conclusion
You now have a complete Linux dedicated server monitoring stack: Node Exporter for host metrics, cAdvisor for Docker containers, Prometheus for storage and queries, and Grafana for dashboards and alerts. From here you can add more servers, more exporters (for databases, web servers or SMART disk health), HTTPS, long-term metrics storage and alert routing to email, Slack or a paging tool.
Good monitoring is most valuable when it is boring: the graphs are flat, the alerts are quiet, and when something changes you already know where to look.
Running this on hardware that matches your workload makes the numbers easier to interpret. If you are planning a new deployment, explore dedicated server hosting or NVMe dedicated servers from KW Servers, and browse more guides in our dedicated server tutorials.
KW Servers Recommended Tutorials
Php, Control Panel, Linux, Dedicated Server, Web
How to Use MultiPHP INI Editor in WHM?
Master the MultiPHP INI Editor in WHM to customize PHP settings per version. This step-by-step guide helps you optimize performance and manage directives with ease using Basic and Editor modes.
Plesk, Control Panel, Web, Network
How to Set NS (Nameserver) Records in Plesk – Step-by-Step Guide
Learn how to configure NS (Nameserver) records in Plesk for seamless DNS management. This guide covers setup, best practices, and troubleshooting to ensure optimal domain resolution and performance.
Control Panel, Dedicated Server, Web, Mysql, Security
What is cPanel? The Complete Guide to Dedicated Server Management (2026)
Master cPanel on your dedicated server. Learn file management, DNS config, email setup, database administration, and security features in this 2026 guide by KW Servers.
Discover KW Servers Dedicated Server Locations
KW Servers servers are available around the world, providing diverse options for hosting websites. Each region offers unique advantages, making it easier to choose a location that best suits your specific hosting needs.