Self-Hosting Grafana: A Complete Guide to Prometheus Monitoring Dashboards

Self-Hosting Grafana: A Complete Guide to Prometheus Monitoring Dashboards

Grafana turns metrics into actionable dashboards, but you still need a collector. This guide self-hosts Grafana with Prometheus and node_exporter on Ubuntu 24.04 LTS using Docker Compose, HTTPS, provisioning, alerting contact points, backups, and day-two operations for private server monitoring.

 Grafana Prometheus monitoring stack overview

Caption: A private Grafana UI sits in front of Prometheus, which scrapes node_exporter so host metrics stay on your VPS.

Introduction

Grafana is the open-source dashboard and alerting layer most operators reach for when they want clear views of CPU, memory, disk, and application health. On its own, Grafana does not collect metrics — it queries data sources. Pairing it with Prometheus (scrape and store time series) and node_exporter (expose Linux host metrics) gives you a complete, self-hosted monitoring path without sending telemetry to a SaaS vendor.

Self-hosting this stack makes sense for homelabs, agencies, and small teams that already run Docker on a VPS. You keep metrics close to the workloads you care about, control who can open dashboards, version-control provisioning files, and rehearse backups before the board becomes mission-critical. The stack stays modest: three containers, a reverse proxy for HTTPS, and a few YAML files under /opt/monitoring.

This guide installs Grafana Open Source, Prometheus, and node_exporter on Ubuntu 24.04 LTS with Docker Compose. You will bind Grafana and Prometheus to localhost, terminate TLS on Nginx, provision a Prometheus data source, import a Node Exporter dashboard workflow, set a basic alert contact point, and finish with troubleshooting, backups, and upgrades. Replace every example.com hostname before production use.

Why Choose Grafana?

  • Dashboards operators actually use: Combine time series, gauges, tables, and variables into views tailored to your hosts and apps.
  • Prometheus as the metrics brain: Pull-based scrapes, labels, and PromQL give you a durable foundation before you expand to logs or traces.
  • node_exporter for host truth: CPU, memory, filesystem, and network metrics from the Linux host without inventing custom collectors on day one.
  • File-based provisioning: Data sources and dashboard providers can live in Git so rebuilds are repeatable.
  • Built-in alerting: Alert rules, contact points, and notification policies ship with Grafana OSS — start simple and grow.
  • Docker-friendly footprint: One Compose project, named volumes, and a reverse proxy fit a modest VPS.
  • Privacy by default: Metrics stay on your network; expose only HTTPS for the UI, not scrape ports.

Grafana visualizes; Prometheus stores; exporters produce. Start with one clean scrape target and a handful of panels before adding cAdvisor, blackbox probes, or remote write.

Prerequisites

Hardware Recommendations:

  • Comfortable lab: 2 vCPU and 2 GB RAM for Grafana + Prometheus + node_exporter on a lightly loaded host
  • Busier VPS with longer retention: 2–4 vCPU and 4 GB RAM
  • 20 GB+ SSD free for images, the Prometheus TSDB, Grafana data, and local backup staging
  • Off-server storage for Compose files, .env, and volume archives
  • Optional second host later if you want remote scrape targets

Software and Accounts:

  • Ubuntu 24.04 LTS with sudo access
  • Hostname such as grafana.example.com with DNS A/AAAA pointing at the server
  • Docker Engine with the Compose v2 plugin (Docker’s apt repository; avoid snap Docker)
  • Nginx and Certbot for HTTPS
  • curl, jq, and openssl for health checks and secrets
  • A password manager for the Grafana admin password

Security Notes:

  • Bind Grafana (3000) and Prometheus (9090) to 127.0.0.1; never publish scrape ports to the public internet
  • Keep node_exporter on the Compose network (or host network only as documented) without opening 9100 in UFW
  • Disable public sign-up; store admin credentials offline
  • Treat .env, provisioning YAML, and backup tarballs as sensitive
  • Prefer pinned image tags in production once you verify a known-good release

Patch the host and open only SSH plus web ports:

sudo apt update
sudo apt upgrade -y
sudo apt install -y ca-certificates curl gnupg jq openssl ufw nginx certbot python3-certbot-nginx

sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status

Installation Guide

This deployment keeps files under /opt/monitoring. Docker Compose runs Grafana, Prometheus, and node_exporter on a private network. Nginx terminates HTTPS for Grafana only.

1. Install Docker Engine

sudo apt-get update -qqy
sudo apt-get install ca-certificates curl -qqy
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt-get update -qqy
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin -qqy

sudo usermod -aG docker "$USER"
newgrp docker

docker --version
docker compose version
docker run --rm hello-world

If the Docker group is inactive in your shell, log out and back in before continuing.

2. Create the Project Directory

sudo mkdir -p /opt/monitoring/{prometheus,grafana/provisioning/datasources,grafana/provisioning/dashboards,grafana/dashboards,backups}
sudo chown -R "$USER":"$USER" /opt/monitoring
chmod 700 /opt/monitoring
chmod 700 /opt/monitoring/backups
cd /opt/monitoring

Keep this tree private — .env holds the admin password and may later hold webhook URLs.

3. Generate Secrets and Create .env

cd /opt/monitoring

GRAFANA_ADMIN_PASSWORD_VALUE="$(openssl rand -base64 36 | tr -d '\n')"

cat > .env <<EOF
GF_SERVER_DOMAIN=grafana.example.com
GF_SERVER_ROOT_URL=https://grafana.example.com/
GF_SECURITY_ADMIN_USER=admin
GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD_VALUE}
GF_USERS_ALLOW_SIGN_UP=false
GF_AUTH_ANONYMOUS_ENABLED=false
GF_ANALYTICS_REPORTING_ENABLED=false
GF_LOG_LEVEL=info
EOF

chmod 600 .env
printf 'Initial Grafana admin password: %s\n' "${GRAFANA_ADMIN_PASSWORD_VALUE}"

Save the password in your password manager before you continue.

4. Write Prometheus Configuration

Create /opt/monitoring/prometheus/prometheus.yml:

global:
  scrape_interval: 15s
  evaluation_interval: 15s

scrape_configs:
  - job_name: prometheus
    static_configs:
      - targets: ["prometheus:9090"]

  - job_name: node
    static_configs:
      - targets: ["node-exporter:9100"]

This scrapes Prometheus itself and the Compose service named node-exporter. Adjust job names and labels when you add more hosts later.

5. Provision the Grafana Prometheus Data Source

Create /opt/monitoring/grafana/provisioning/datasources/prometheus.yml:

apiVersion: 1

datasources:
  - name: Prometheus
    type: prometheus
    access: proxy
    url: http://prometheus:9090
    isDefault: true
    editable: false

Create /opt/monitoring/grafana/provisioning/dashboards/default.yml:

apiVersion: 1

providers:
  - name: Local dashboards
    orgId: 1
    folder: Operations
    type: file
    disableDeletion: false
    updateIntervalSeconds: 30
    allowUiUpdates: true
    options:
      path: /var/lib/grafana/dashboards

You can drop exported dashboard JSON files into /opt/monitoring/grafana/dashboards/ after first login; Grafana will pick them up on the provider interval.

6. Create compose.yaml

services:
  grafana:
    image: grafana/grafana:latest
    container_name: grafana
    restart: unless-stopped
    env_file: ./.env
    ports:
      - "127.0.0.1:3000:3000"
    volumes:
      - grafana_data:/var/lib/grafana
      - ./grafana/provisioning:/etc/grafana/provisioning:ro
      - ./grafana/dashboards:/var/lib/grafana/dashboards:ro
    depends_on:
      - prometheus
    networks:
      - monitoring
    healthcheck:
      test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:3000/api/health || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 5

  prometheus:
    image: prom/prometheus:latest
    container_name: prometheus
    restart: unless-stopped
    command:
      - "--config.file=/etc/prometheus/prometheus.yml"
      - "--storage.tsdb.path=/prometheus"
      - "--storage.tsdb.retention.time=15d"
      - "--web.enable-lifecycle"
    ports:
      - "127.0.0.1:9090:9090"
    volumes:
      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - prometheus_data:/prometheus
    networks:
      - monitoring

  node-exporter:
    image: quay.io/prometheus/node-exporter:latest
    container_name: node-exporter
    restart: unless-stopped
    command:
      - "--path.rootfs=/host"
    pid: host
    volumes:
      - "/:/host:ro,rslave"
    networks:
      - monitoring
    # No published ports — Prometheus scrapes over the Compose network only

networks:
  monitoring:
    name: monitoring

volumes:
  grafana_data:
    name: grafana_data
  prometheus_data:
    name: prometheus_data

Official Grafana docs also document grafana/grafana-enterprise (free to run with optional paid features). This guide uses grafana/grafana for a clear OSS image. The node_exporter flags follow the Prometheus project’s container guidance: mount the host root and set --path.rootfs=/host so metrics reflect the host, not only the container.

 Grafana Prometheus Compose topology

Caption: Nginx publishes HTTPS for Grafana while Prometheus scrapes node_exporter on a private Docker network; scrape ports stay on localhost or internal DNS.

7. Start the Stack

cd /opt/monitoring
docker compose config
docker compose pull
docker compose up -d
docker compose ps
docker compose logs --since=2m

Health checks:

curl -s http://127.0.0.1:3000/api/health | jq
curl -s http://127.0.0.1:9090/-/ready
curl -s http://127.0.0.1:9090/api/v1/targets | jq '.data.activeTargets[] | {job: .labels.job, health: .health}'

Expect Grafana health JSON, Prometheus ready, and both scrape jobs reporting up. If a target is down, confirm service names match prometheus.yml and that containers share the monitoring network.

8. Configure Nginx and HTTPS

Create /etc/nginx/sites-available/grafana.example.com:

server {
    listen 80;
    listen [::]:80;
    server_name grafana.example.com;

    location / {
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_pass http://127.0.0.1:3000;
    }
}

Enable the site and request a certificate:

sudo ln -s /etc/nginx/sites-available/grafana.example.com /etc/nginx/sites-enabled/grafana.example.com
sudo nginx -t
sudo systemctl reload nginx

sudo certbot --nginx -d grafana.example.com
sudo systemctl reload nginx

Open https://grafana.example.com and sign in with the generated admin credentials. Leave Prometheus on localhost (or tunnel via SSH) unless you deliberately proxy it behind authentication.

Configuration

Key Grafana environment settings in this stack:

  • GF_SERVER_ROOT_URL — public HTTPS URL for links and redirects
  • GF_SECURITY_ADMIN_USER / GF_SECURITY_ADMIN_PASSWORD — initial admin account
  • GF_USERS_ALLOW_SIGN_UP=false — no anonymous account creation
  • GF_AUTH_ANONYMOUS_ENABLED=false — dashboards stay private
  • Provisioning mounts under /etc/grafana/provisioning
  • Persistent Grafana state in the grafana_data volume

Prometheus retention is set to 15d via --storage.tsdb.retention.time. Raise it only after you confirm disk headroom. Reloading config without a full restart:

curl -X POST http://127.0.0.1:9090/-/reload

Requires --web.enable-lifecycle as shown in the Compose file. After editing prometheus.yml, always validate syntax before reload:

docker compose exec prometheus promtool check config /etc/prometheus/prometheus.yml

 Prometheus scrape path to node_exporter

Caption: Prometheus pulls metrics on a schedule from node_exporter; Grafana queries Prometheus over the same private network.

Usage

First Login

  1. Open https://grafana.example.com
  2. Sign in as admin with the password from .env
  3. Change the admin password when prompted
  4. Confirm Connections → Data sources lists Prometheus (provisioned) and that Save & test succeeds
  5. In Prometheus UI (http://127.0.0.1:9090 via SSH tunnel), open Status → Targets and verify prometheus and node are UP

Build a First Host Dashboard

In Grafana:

  1. Dashboards → New → New dashboard → Add visualization
  2. Query Prometheus, for example:
    • CPU busy ratio: 100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)
    • Memory available: node_memory_MemAvailable_bytes
    • Root filesystem free: node_filesystem_avail_bytes{mountpoint="/"}
  3. Set units (percent, bytes) and save the dashboard under the Operations folder
  4. Optionally import a community Node Exporter full dashboard from grafana.com by ID after reviewing the JSON — pin the data source to your provisioned Prometheus

Alerting Smoke Test

  1. Alerting → Contact points → New — add email or a webhook you control
  2. Alerting → Alert rules → New alert rule — for example, fire when node_filesystem_avail_bytes{mountpoint="/"} stays below a threshold for 5 minutes
  3. Attach a notification policy that routes to your contact point
  4. Use Test on the contact point, then confirm a firing rule appears in Alert rules

Do not point production alerts at a personal inbox you ignore. Use a channel your on-call path actually reads.

Testing Checklist

  • docker compose ps shows grafana, prometheus, and node-exporter healthy
  • https://grafana.example.com loads over a valid certificate
  • Provisioned Prometheus data source tests green
  • Both Prometheus targets are UP
  • At least one dashboard panel shows live host metrics
  • Contact point test notification arrives
  • Reboot the VPS and confirm the stack returns with restart: unless-stopped

Screenshots and Visuals

The visuals in this guide are original architecture diagrams — not scraped product UI. They show the HTTPS edge, Compose topology, scrape path, and backup/alert maintenance flow so you can reason about the stack before it becomes critical path.

 Monitoring backup and alert maintenance flow

Caption: Day-two ops: back up Grafana and Prometheus volumes, keep Compose and prometheus.yml in Git, and verify alert contact points after upgrades.

Troubleshooting

  • Grafana 502 from Nginx: Container still starting or not bound to 127.0.0.1:3000 — check docker compose ps and ss -tlnp | grep 3000
  • Data source test fails: Wrong URL (use http://prometheus:9090 from inside the Compose network, not localhost), or Prometheus not on the monitoring network
  • node job DOWN: Service name mismatch (node-exporter vs config), exporter crash, or missing host mounts — inspect docker compose logs node-exporter
  • Empty panels / no data: Wrong metric names, time range too short after first start, or scrape interval not elapsed — verify targets and try a raw PromQL in Explore
  • Permission errors on Grafana volume: Avoid arbitrary host UID remaps unless you intentionally follow Grafana bind-mount docs; named volumes are simpler on Ubuntu
  • Prometheus disk growth: Lower --storage.tsdb.retention.time, drop high-cardinality jobs, and monitor volume size under /var/lib/docker
  • Forgot admin password: Prefer Grafana CLI reset inside the container after a volume backup — do not delete grafana_data as a shortcut
  • Certbot / Nginx failures: Confirm DNS points at the server and port 80 is reachable for HTTP-01 challenges
  • node_exporter shows container-ish metrics: Confirm --path.rootfs=/host and the /:/host:ro,rslave mount match official container guidance

Scaling, Securing, and Next Steps

Back up both volumes plus config before upgrades:

cd /opt/monitoring
docker compose stop

sudo mkdir -p /var/backups/monitoring
docker run --rm \
  -v grafana_data:/data:ro \
  -v /var/backups/monitoring:/backup \
  alpine:3.20 \
  sh -c 'cd /data && tar czf /backup/grafana-data.tgz .'

docker run --rm \
  -v prometheus_data:/data:ro \
  -v /var/backups/monitoring:/backup \
  alpine:3.20 \
  sh -c 'cd /data && tar czf /backup/prometheus-data.tgz .'

cp compose.yaml .env prometheus/prometheus.yml /var/backups/monitoring/
rsync -a /var/backups/monitoring/ backup-user@backup.example.net:/srv/backups/monitoring/

docker compose start

Schedule that flow with cron or a systemd timer. Rehearse restore on a scratch VPS: recreate volumes from the archives, restore Compose and config, docker compose up -d, confirm data sources and a sample dashboard. Treat backup archives as sensitive.

For upgrades, read Grafana and Prometheus release notes, take a fresh backup, pin image tags when ready, then:

cd /opt/monitoring
docker compose pull
docker compose up -d
docker compose ps
docker compose logs --since=2m

Harden next: put Grafana behind SSO or VPN if required, add scrape targets for other hosts (or a federation path), consider cAdvisor for container metrics, keep 9090 and 9100 off the public firewall, and store provisioning YAML in Git so the monitoring plane is rebuildable.

The outcome of this guide is a private Grafana + Prometheus + node_exporter stack on Ubuntu 24.04 with HTTPS for the UI, provisioned data sources, host metrics on dashboards, a rehearsable backup path, and a starter alert contact point. From here, pin image versions, expand scrape jobs deliberately, and document who owns on-call for firing alerts.

Need this done on your server?

I deploy and harden Laravel, CodeCanyon, and open-source apps on cPanel or VPS, and offer monthly Server Watch retainers. Hire for deploy · Care plan

References

Share:

Get new posts in your inbox

No spam. One short email per new article — practical PHP, Laravel, devops, and AI-assisted workflows.

Comments

Powered by GitHub Discussions via Giscus. A free GitHub account is required.