How to Monitor a Linux Dedicated Server With Prometheus and Grafana

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 --version
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.

sudo mkdir -p /opt/server-monitoring/prometheus
cd /opt/server-monitoring

5. Step 2: Configure Prometheus

Create the Prometheus configuration file:

sudo nano prometheus/prometheus.yml

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

sudo nano docker-compose.yml
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. latest is convenient in a tutorial, but an unexpected image update can change metric names or behavior. Check each project's release page, replace latest with a specific version tag, and test upgrades on a non-critical server first.

7. Step 4: Start the Stack and Verify Exporters

cd /opt/server-monitoring
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:

sudo docker compose logs --tail=50

Verify Node Exporter

curl -s http://127.0.0.1:9100/metrics | head -n 20

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

curl -s http://127.0.0.1:8080/metrics | grep -m 5 container_

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:

up

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-IP

    Then open http://localhost:3000 for Grafana and http://localhost:9090 for 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 logs prometheus
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 /metrics endpoint with curl.

  • Search Prometheus for container_cpu_usage_seconds_total.

  • Inspect the real labels on those series. If image or name are 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.

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.

Find Your Perfect Server

AI-powered · Instant results

Ask KW Servers AI
Instantly match you to the perfect dedicated server

How can I help you today?

Try asking for specific hardware, locations, or budgets.

Ryzen 9 in Germany

High-performance compute nodes in EU

128GB RAM Servers

Ideal for heavy virtualization

Budget Gaming

Low-latency servers under $100/mo

10TB Storage Arrays

Secure backup and archiving