5 Self-Hosted Monitoring Tools Worth Running on Your Own Hardware in 2026

The Monitoring Problem No SaaS Tool Solves for You

Most SaaS monitoring products are built around the assumption that you’re running fleets of identical cloud VMs serving HTTP traffic. Plug in the agent, get CPU and memory graphs, call it done. That model breaks completely when your stack is an Ollama inference server hammering a 32GB VRAM workstation, an n8n instance running inside Docker with a dozen active workflows, and a PM2-managed Node engine firing on cron schedules. The per-host billing model punishes you twice: once for the cost, and again because the default dashboards surface nothing you actually care about.

The metrics that actually matter for this kind of setup don’t exist in any default SaaS template. GPU VRAM headroom is the first thing I check — if nvidia-smi shows the model context is eating 28GB of 32GB and a second request hits, latency spikes aren’t a mystery anymore, they’re a predictable consequence. Container restart loops in Docker are a close second: a workflow executor that silently restarts every 40 minutes will look fine in an uptime check but corrupt half your pipeline runs. PM2 cron jobs fail quietly — no exit code surfaces unless you’re explicitly scraping the process list. And reverse proxy 502 bursts that last under 30 seconds will disappear from any monitoring tool polling at 1-minute intervals.

The evaluation criteria for the five tools below are deliberately narrow. Install complexity matters because a monitoring setup that takes three days to configure is a monitoring setup you’ll abandon. Resource footprint matters because the host you’re monitoring is already doing real work — a 600MB resident-memory agent on the same box as an inference server is a bad trade. And the tool either surfaces ops-relevant data in under an hour of configuration, or it doesn’t make the list. No partial credit for “you can build a dashboard that shows this if you write enough PromQL.”

For context on what pipeline-level observability looks like — specifically how n8n workflow success and failure rates translate into something you can alert on — the framing in Workflow Automation in 2026: n8n, Zapier, and Self-Hosted Pipelines is worth reading alongside this. The monitoring layer and the automation layer solve different problems, but their failure modes overlap more than most ops writeups acknowledge. A cron pipeline that silently stops producing output looks identical to a cron pipeline that never ran — you need both execution telemetry and system metrics to tell them apart.

Prometheus + Grafana: The Standard Stack That Earns Its Complexity

The thing most people get wrong about this stack: Prometheus and Grafana don’t integrate — they coexist. Prometheus scrapes exporters on a pull model and stores the resulting time-series locally. Grafana queries that store and draws pictures. There’s no magic glue, no plugin that connects them invisibly. You wire them together at the datasource level, and you feel every seam when something breaks. That transparency is actually the argument for using it. When a dashboard shows nothing, you can bisect the problem: is the exporter up? Is Prometheus scraping it? Is the Grafana datasource pointed at the right port? Each layer is independently interrogatable.

Here’s a minimum viable setup that actually runs. The provisioned datasource avoids the “click around in the UI” step that gets lost the moment someone rebuilds the container:

# docker-compose.yml
services:
  prometheus:
    image: prom/prometheus:v2.51.0
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus_data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.retention.time=90d'
      - '--storage.tsdb.wal-compression'  # cuts WAL disk use 30-40%
    ports:
      - "9090:9090"

  grafana:
    image: grafana/grafana:10.4.2
    volumes:
      - grafana_data:/var/lib/grafana
      - ./grafana/provisioning:/etc/grafana/provisioning
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=changeme
    ports:
      - "3000:3000"
    depends_on:
      - prometheus

volumes:
  prometheus_data:
  grafana_data:
# prometheus.yml
global:
  scrape_interval: 15s

scrape_configs:
  - job_name: 'node'
    static_configs:
      - targets: ['node_exporter:9100']

  - job_name: 'cadvisor'
    static_configs:
      - targets: ['cadvisor:8080']
# grafana/provisioning/datasources/prometheus.yaml
apiVersion: 1
datasources:
  - name: Prometheus
    type: prometheus
    url: http://prometheus:9090  # container name, not localhost
    isDefault: true
    editable: false

The retention math is the part nobody thinks about until they’re watching df -h scroll upward. At 15-second scrape intervals, a single exporter like node_exporter generates roughly 1–2 MB of TSDB data per day. Forty exporters — a realistic number once you’re scraping per-container metrics with cAdvisor across several hosts — means 40–80 MB/day, which compounds quickly against a 90-day retention window. The --storage.tsdb.wal-compression flag pairs with --storage.tsdb.retention.time=90d and meaningfully reduces the write-ahead log on disk. If you’re pushing past that, the Prometheus docs point toward Thanos or VictoriaMetrics for remote storage, but that’s a different problem entirely.

Pick this stack when the requirement is a permanent, queryable, auditable record of your infrastructure — something you can run ad-hoc PromQL against at 2am to figure out what happened three weeks ago. The query language has a learning curve that’s real but finite. Once you can write rate(http_requests_total[5m]) and understand why the range vector matters, the model clicks. If the requirement is instead “get a dashboard running before the next standup,” this stack will frustrate you. The provisioning alone assumes you understand what a datasource is and why it needs to be inside the Grafana container’s filesystem on startup. That’s not a criticism — it’s a signal about which kind of operator this tool is designed for.

VictoriaMetrics: Prometheus-Compatible, Half the RAM

Most people discover VictoriaMetrics the wrong way — they see “Prometheus-compatible” and assume it’s just a faster remote storage backend. The more useful mental model: the single-node binary is the storage and the scrape target rolled into one process. Drop it in place of Prometheus, point your existing prometheus.yml at it, and your Grafana dashboards keep working without a single panel edit. MetricsQL is a superset of PromQL, so queries that work in Prometheus work here too — the extension functions are additive, not breaking.

The RAM story is real but requires nuance. On a scrape config hitting 50 targets at a 15-second interval, VictoriaMetrics single-node typically sits under 200 MB RSS. Prometheus doing the same work tends to land in the 400–600 MB range, sometimes higher depending on how aggressive your recording rules are. The official docs claim up to 7x less RAM, which is technically achievable under ideal conditions — low cardinality, compact label sets. Where the gap closes is high-cardinality data: if you’re scraping something that emits per-request or per-user label dimensions, both tools suffer proportionally and the ratio shrinks toward 2–3x. Still meaningful on a constrained machine, but not magic.

The configuration model is deliberately minimal, which is either refreshing or annoying depending on your background. Retention is a process flag, not a YAML stanza:

# docker-compose fragment
services:
  victoriametrics:
    image: victoriametrics/victoria-metrics:v1.101.0
    command:
      - "--storageDataPath=/victoria-metrics-data"
      - "--retentionPeriod=12"   # months, not days — easy to misread
      - "--httpListenAddr=:8428"
    ports:
      - "8428:8428"
    volumes:
      - vm-data:/victoria-metrics-data

If you want the collection layer decoupled from storage — useful when you’re scraping dozens of targets and want independent scaling — vmagent is the sidecar that handles scraping and remote_write forwarding. It accepts the same scrape_configs format Prometheus does. The split also means you can restart the storage node for maintenance without dropping scrape coverage, which Prometheus’s monolithic design doesn’t give you cleanly.

Pick VictoriaMetrics over Prometheus when at least one of these is true: you’re running on hardware where RAM has real consequences (a mini-PC lab node, a shared VPS where you’re paying per GB), you have existing Grafana dashboards you’re not willing to rewrite, or you need retention beyond 30 days without provisioning proportionally large disk. Prometheus’s default storage compresses reasonably well but still scales linearly with retention window. VictoriaMetrics’s storage engine compresses more aggressively — the on-disk footprint for the same dataset is noticeably smaller, which matters when you’re trying to keep 6–12 months of metrics on a 50 GB volume.

Netdata: Useful First, Configurable Later

The install story here is genuinely unusual. Most monitoring tools require you to configure a scrape target, set up a service, restart things three times, and then realize you forgot to open a firewall port. Netdata’s kickstart script does none of that:

# drops a working dashboard at http://your-host:19999 — no follow-up config required
wget -O /tmp/netdata-kickstart.sh https://my-netdata.io/kickstart.sh && sh /tmp/netdata-kickstart.sh

Under two minutes to a live dashboard. It auto-discovers Docker containers by reading the socket, picks up systemd unit states, and surfaces metrics for Nginx, Postgres, Redis, and most common services without a single config file edit. If you’ve ever bootstrapped Prometheus from scratch — writing a prometheus.yml, finding the right exporters, wiring up Grafana datasources, building dashboards — the contrast is jarring. That zero-to-visible speed matters most during incidents when you inherit a broken machine and need situational awareness immediately.

Where Netdata genuinely outpaces a default Prometheus stack is metric resolution and pre-built visualizations. Prometheus node_exporter collects CPU steal time, disk latency percentiles, and TCP retransmit rates — but at a 15-second scrape interval by default, and without shipping any dashboards. You get the data; you build the panels. Netdata ships those visualizations pre-wired at one-second resolution. CPU steal spikes that last four seconds show up clearly. A Prometheus setup scraping every 15 seconds can miss those entirely, or flatten them into a near-invisible bump. For the specific problem of catching short-duration resource contention on shared infrastructure, that resolution difference is decisive.

The retention story is where you have to make a deliberate decision before committing. Netdata’s native dbengine is designed for short-term forensics — excellent for the last few hours, workable for a few days, not designed for 90-day trending or capacity planning queries. Your two options for extending retention both have costs. First, configure a prometheus remote_write target and push metrics into your existing Prometheus/VictoriaMetrics stack:

# /etc/netdata/exporting.conf
[prometheus_remote_write:my_victoria]
    enabled = yes
    destination = http://victoriametrics:8428/api/v1/write
    remote write URL path = /api/v1/write
    # send everything; filter later at query time
    send charts matching = *

Second option is Netdata Cloud, which streams your metrics to their hosted infrastructure. For operators running self-hosted specifically because they want data on their own hardware — air-gapped environments, compliance constraints, or just preference — that option is a non-starter. The remote_write path keeps you in control, but now you’re running two systems and the “zero config” advantage is diluted. Pick Netdata when you need immediate visibility with no setup friction, when you’re monitoring a secondary node where building a full Prometheus stack isn’t worth the overhead, or when per-second granularity is the actual requirement rather than long-term trend analysis.

Uptime Kuma: Exactly One Job, Done Well

The Prometheus stack will tell you your p99 latency drifted 40ms — but if Nginx is spitting 502s at every visitor while your internal health endpoint still responds on port 8080, you might not find out until someone complains. Uptime Kuma solves exactly that gap: external black-box polling that tests what a real client actually sees. It doesn’t try to be a metrics platform. That restraint is the whole point.

Check types cover the common cases without bloat: HTTP/HTTPS with configurable expected status codes and keyword matching, TCP port reachability, DNS record resolution, and Docker container up/down status. If you need distributed tracing or cardinality-rich time series, look elsewhere. If you need to know whether your VPN endpoint is accepting connections from the outside, Uptime Kuma answers that in under thirty seconds of setup.

The deploy is one command:

docker run -d \
  --restart=always \
  -p 3001:3001 \
  -v uptime-kuma:/app/data \
  --name uptime-kuma \
  louislam/uptime-kuma:1

Everything — monitors, alert configs, notification channels, status history — lives in a single SQLite file inside that named volume. Backup strategy is literally cp uptime-kuma.db uptime-kuma.db.bak. No external database to manage, no schema migrations to babysit across upgrades. The tradeoff is that SQLite doesn’t scale to hundreds of monitors with sub-second polling intervals, but for a self-hosted stack watching thirty to fifty endpoints that ceiling is not a real concern.

The alerting integrations are where it earns its slot alongside a full metrics stack. Telegram, Discord, Slack, and generic webhooks are all first-class. The webhook path drops cleanly into n8n — POST the payload to an n8n webhook trigger, route on monitor name or status, and you have conditional logic, escalation delays, or incident log writes without touching Uptime Kuma’s internals at all. Configuration for any of these takes five minutes, not fifty.

One gotcha that doesn’t surface until you try the Docker container monitor: it requires mounting the Docker socket into the Uptime Kuma container.

docker run -d \
  --restart=always \
  -p 3001:3001 \
  -v uptime-kuma:/app/data \
  -v /var/run/docker.sock:/var/run/docker.sock \
  --name uptime-kuma \
  louislam/uptime-kuma:1

Mounting /var/run/docker.sock gives the container effective root on the host — any process inside it can spawn, stop, or inspect any container on the machine. On a firewalled home-lab box where you control all ingress, that risk profile is manageable. On a VPS with a public IP and Uptime Kuma’s web UI exposed directly (even behind basic auth), evaluate carefully. The container status monitor is convenient; it isn’t worth a compromised host to get it.

Loki + Promtail: Log Aggregation That Doesn’t Eat Your Disk

The real reason to add Loki to a self-hosted stack isn’t log storage — it’s log recall. docker logs --tail 100 n8n gives you the last hundred lines right now, but if your n8n workflow executor threw a 500 at 2 AM and you’re looking at it at 9 AM, those lines are gone. Either you have log rotation eating them, or the buffer scrolled past them hours ago. Tracking that down with grep across rotated /var/lib/docker/containers/**/*-json.log files is painful enough once that you’ll set up Loki immediately afterward. The architectural reason Loki doesn’t balloon your disk is that it indexes only labels — not the full log text. The log lines themselves get compressed and stored as chunks; the index is tiny. Full-text search comes at query time via regex on the stored chunks, not via an inverted index the way Elasticsearch works. That trade-off means slower arbitrary searches but dramatically lower storage overhead for typical DevOps log volumes.

Getting Promtail to auto-discover Docker containers takes about 20 lines of config. The key is docker_sd_configs, which reads the Docker socket and emits a scrape target per running container. Relabeling pulls container_name and com.docker.compose.service out of the discovered metadata and turns them into Loki labels:

server:
  http_listen_port: 9080

positions:
  filename: /tmp/positions.yaml

clients:
  - url: http://loki:3100/loki/api/v1/push

scrape_configs:
  - job_name: docker
    docker_sd_configs:
      - host: unix:///var/run/docker.sock
        refresh_interval: 15s
    relabel_configs:
      # keep the bare container name (strips leading slash Docker adds)
      - source_labels: [__meta_docker_container_name]
        regex: "/(.*)"
        target_label: container_name
      # pull the Compose service label if it exists
      - source_labels: [__meta_docker_container_label_com_docker_compose_service]
        target_label: compose_service
      # standard log path Promtail needs to tail
      - source_labels: [__meta_docker_container_log_stream]
        target_label: stream

Mount /var/run/docker.sock read-only into the Promtail container and this works without touching individual container configs. After it’s running, {container_name="n8n"} in Grafana’s Explore view returns every log line that n8n has emitted since Promtail started. You can narrow it to {container_name="n8n"} |= "500" or pipe through a regex for specific workflow IDs. The label cardinality stays low because container names and Compose service names are a short, stable set — exactly what Loki’s index is designed for.

The part the official getting-started docs consistently underplay: Loki will accumulate index chunks indefinitely unless you explicitly configure the compactor to run retention. The boltdb-shipper compactor defaults to compaction only, not deletion. You need two separate config blocks to actually enforce retention:

compactor:
  working_directory: /loki/compactor
  shared_store: filesystem
  compaction_interval: 10m
  retention_enabled: true        # this flag is the non-obvious one
  retention_delete_delay: 2h
  retention_delete_worker_count: 150

limits_config:
  retention_period: 744h         # 31 days; set per-tenant or globally here

Without retention_enabled: true in the compactor block, the retention_period value in limits_config does nothing. The chunks and index keep growing. Disk fills up on a timeline that depends on your log volume, but on a moderately busy Docker host running a dozen containers, expect several gigabytes per week before compression. With retention enabled and 31 days configured, disk usage plateaus and stays there. Check that it’s actually deleting by watching loki_compactor_deleted_chunks_total in Prometheus — if that counter never moves, the compactor config isn’t being picked up.

The right time to add Loki is exactly when you already have Prometheus and Grafana running. You add Loki as a second datasource in Grafana’s datasource settings — same UI, same dashboards panel editor, just a different query language (LogQL instead of PromQL). The marginal operational cost is one more container plus Promtail, and the compactor config above. What you get back is the ability to correlate a spike in an HTTP 500 metric on a Prometheus graph with the exact log lines that caused it, in the same Grafana window, without switching tools. That’s the payoff: not fancy log analytics, just not being blind at 2 AM.

Picking the Right Tool for Your Actual Setup

The honest answer to “which tool should I use” is that the decision almost always comes down to two constraints that people don’t state up front: how much idle RAM they can spare on the host, and whether they already have a query/visualization layer or need one bundled in. Everything else — cardinality limits, retention policies, integrations — is secondary until you’ve cleared those two gates.

Here’s how the five tools stack up across the dimensions that actually matter for a self-hosted setup:

Tool Primary Use Case Storage Model Approx. Idle RAM Retention Flexibility Biggest Operational Gotcha
Prometheus Metrics scraping + alerting Local TSDB (pull-based) ~250–400 MB Fixed retention flag; no tiered storage natively High-cardinality label sets will eat RAM fast; no horizontal scale without remote write
VictoriaMetrics Long-term metrics storage Custom columnar TSDB ~50–120 MB (single-node) Per-metric TTL, downsampling, configurable at ingest MetricsQL is close to PromQL but not identical — existing dashboards need audit
Grafana Unified query + visualization No native metrics store ~150–250 MB Depends entirely on backend datasource Dashboard state stored in SQLite by default — back it up before any container restart
Netdata Real-time system telemetry In-memory ring buffer + optional DB engine ~150–300 MB (scales with metrics count) Short by default; DB engine required for >1 day Cloud-connected by default; disable claim or it phones home on first run
Uptime Kuma Endpoint + uptime checks SQLite ~60–100 MB Configurable per-monitor history trim No native metric federation; alerting is push-only with no query interface
Loki Log aggregation + querying Object store or local filesystem chunks ~100–200 MB (single-binary) Retention via compactor; per-stream rules possible LogQL regex on high-volume streams is slow without good label strategy upfront

The decision path by constraint is fairly mechanical once you’re honest about your situation. RAM-limited host running under 4 GB free — go VictoriaMetrics over Prometheus, full stop. The idle footprint difference is real and compounds when you’re also running Grafana, a Node process, and Docker overhead on the same box. Need immediate visibility into a new host with zero config written — Netdata is the only one that installs and shows you useful data in under five minutes; everything else requires you to define scrape targets or shipping configs first. Need uptime alerting wired into an existing webhook or n8n flow — Uptime Kuma’s webhook output is dead simple and requires no intermediate exporter layer. Need to correlate “the API started failing” with “what was in the logs at that exact timestamp” — Loki alongside whichever metrics backend you chose, queried together inside Grafana. That combination is the only way to close that loop without grep-ing through raw log files by hand.

For a single-workstation home lab running Ollama, Docker services, and a Node automation engine — which is exactly my setup — the stack I’d recommend and actually run is: Uptime Kuma handling external endpoint checks and pushing alerts to n8n webhooks, VictoriaMetrics as the metrics backend because it handles Prometheus remote_write and stays light, Grafana as the single query interface pointed at both VictoriaMetrics and Loki, and Loki added the moment you’ve had one debugging session where you were correlating container restarts to application errors without correlated timestamps. That last condition sounds vague until it happens, and then Loki goes in immediately. The docker-compose fragment to wire VictoriaMetrics as Prometheus remote_write target looks like this:

# prometheus.yml scrape config with remote_write to VictoriaMetrics
# VictoriaMetrics accepts the /api/v1/write endpoint natively
remote_write:
  - url: http://victoriametrics:8428/api/v1/write
    queue_config:
      max_samples_per_send: 10000
      # tune this down on low-traffic hosts to reduce write amplification
      batch_send_deadline: 5s

One boundary worth being explicit about: none of these tools — not Netdata, not VictoriaMetrics, not any combination of them — will give you per-request Ollama inference latency or VRAM fragmentation state between model loads out of the box. Ollama exposes /api/tags for loaded model state and you can scrape process-level GPU stats via nvidia-smi --query-gpu=memory.used,memory.free --format=csv,noheader on a cron or as a custom exporter, but stitching that into meaningful per-request latency tracking requires either a sidecar exporter or middleware instrumentation at the API layer. That’s a different problem and a separate article — don’t let the absence of it here suggest any of these five tools are incomplete. They’re solving infrastructure visibility, not ML observability.


Disclaimer: This article is for informational purposes only. The views and opinions expressed are those of the author(s) and do not necessarily reflect the official policy or position of Sonic Rocket or its affiliates. Always consult with a certified professional before making any financial or technical decisions based on this content.


Eric Woo

Written by Eric Woo

Self-Hosted AI & Automation Engineer

Eric runs his own self-hosted stack: local LLM pipelines on Ollama with dual-model VRAM scheduling on a single 32GB workstation, n8n workflows in Docker, and a TypeScript automation engine that publishes to WordPress on cron. He writes about the systems he actually operates — configs, failure modes, and GPU bills included.

Leave a Comment