Productivity

Prometheus Grafana Docker Compose Tuning for 2GB VPS OOM

Prometheus Grafana Docker Compose Tuning for 2GB VPS OOM

A 2GB VPS costs around $6–$12/month in 2026. It’s the default entry point for self-hosted monitoring. And it’s exactly the size where a default docker compose up with Prometheus and Grafana will eat your RAM, trigger the OOM killer at 3 AM, and leave you staring at a blank dashboard wondering what happened.

The good news: this is a solved problem. The bad news: most tutorials skip the tuning part entirely.

Key Takeaways

  • A default Prometheus deployment with a 15-second scrape interval can consume 400–700MB of RAM on its own, leaving almost no headroom on a 2GB VPS running Grafana and Node Exporter simultaneously.
  • The Linux OOM killer targets the process with the highest oom_score — almost always Prometheus — making memory limits in docker-compose.yml a critical safety mechanism, not optional tuning.
  • Increasing the scrape interval from 15s to 60s reduces Prometheus TSDB write load by roughly 75%, with minimal impact on alerting accuracy for infrastructure-level metrics.
  • Grafana’s memory footprint drops significantly when anonymous access is enabled, server-side image rendering is disabled, and unused data sources are removed at startup.
  • A properly tuned stack can run comfortably within 1.4GB of RAM, leaving ~600MB for the OS and other processes.

The 2GB Wall: Why Default Configs Fail

Prometheus wasn’t designed for constrained environments. It was built by SoundCloud engineers to handle millions of time series at scale, and its defaults reflect that origin. The global.scrape_interval defaults to 15s. TSDB retention defaults to 15d. Both are fine on a 16GB dedicated server. On a 2GB VPS, they’re a slow-motion crash.

A typical self-hosted stack — Prometheus, Grafana, Node Exporter, and maybe cAdvisor — will consume memory roughly like this on a fresh deploy with default settings:

ContainerDefault RAM UsageNotes
Prometheus400–700MBGrows with scrape frequency and series count
Grafana200–350MBSpikes on dashboard render
Node Exporter20–40MBStable, low footprint
cAdvisor100–200MBSignificant if enabled
Total720MB–1.29GBPlus OS overhead (~300–500MB)

That math puts you at 1–1.8GB before any workload runs on the host. The OOM killer kicks in when the kernel can’t find free pages. It doesn’t give warnings. It just kills the highest-scoring process and moves on.

According to the Grafana Cloud documentation on Docker Compose Linux deployments, the recommended approach for resource-constrained environments starts with adjusting scrape_interval and setting explicit mem_limit values in Compose — but these options are buried in footnotes, not the main setup flow.


The Core Tuning Levers

Scrape Interval: The Biggest Bang-Per-Config-Line

The relationship between scrape interval and memory pressure is direct. Prometheus writes a new data point for every metric on every scrape. At 15s, that’s 4 writes per minute per series. At 60s, it’s 1. For a Node Exporter setup tracking ~800 default metrics, that’s the difference between ~3,200 writes/minute and ~800 writes/minute.

Change this in prometheus.yml:

global:
  scrape_interval: 60s
  evaluation_interval: 60s

For alerting on things like CPU spikes or disk fill rates, 60-second resolution is entirely adequate. You’d only need 15s for high-frequency application metrics like p99 latency on a busy API — and those workloads shouldn’t live on a 2GB VPS anyway.

Memory Limits in Docker Compose

Docker doesn’t automatically protect containers from each other’s memory appetite. Without explicit limits, Prometheus can consume all available RAM and starve Grafana mid-render.

Add hard limits in your docker-compose.yml:

services:
  prometheus:
    image: prom/prometheus:latest
    mem_limit: 512m
    memswap_limit: 512m
  grafana:
    image: grafana/grafana:latest
    mem_limit: 300m
    memswap_limit: 300m

Setting memswap_limit equal to mem_limit disables swap for that container, which prevents thrashing. If Prometheus hits 512MB, Docker sends SIGKILL — cleaner than letting the OOM killer pick its own victim at random.

Prometheus TSDB Retention and Chunk Size

Two more flags that matter on small instances:

command:
  - '--storage.tsdb.retention.time=7d'
  - '--storage.tsdb.wal-compression'

Dropping retention from 15 days to 7 days cuts on-disk storage roughly in half, which also reduces memory used by the TSDB head block. WAL compression is free performance — available since Prometheus 2.11, it reduces WAL size by ~50% according to the Prometheus project changelog.

Grafana: Trim the Fat

Grafana’s default config assumes a multi-user, plugin-heavy environment. On a 2GB VPS, strip it down:

[users]
allow_sign_up = false

[auth.anonymous]
enabled = true
org_role = Viewer

[rendering]
# Don't enable server-side rendering unless needed

Disabling the image renderer alone saves 150–200MB. The docker.recipes Prometheus-Grafana stack template explicitly excludes the renderer for exactly this reason.


Configuration Trade-offs: Conservative vs. Default

SettingDefault ConfigTuned for 2GB VPSTrade-off
scrape_interval15s60sLower time resolution
TSDB retention15d7dShorter history
Grafana mem_limitNone300MBHarder crash on spike
Prometheus mem_limitNone512MBPrevents OOM cascade
WAL compressionOffOnTiny CPU cost
cAdvisorOften includedRemove or limitLess container insight

The tuned config trades granularity for stability. For most infrastructure monitoring use cases — uptime, disk, CPU trends, memory — that trade-off is worth it.

This approach can fail when your application layer generates high-cardinality metrics. If you’re scraping a service that emits per-user or per-request labels, even a 60s interval won’t save you. The series count, not the interval alone, drives memory growth. In those cases, metric_relabel_configs to drop labels you don’t actually query is the fix — not more tuning of the global interval.


Running It Safely: Practical Scenarios

Scenario 1 — Solo developer, personal projects: You want visibility without babysitting. Set scrape_interval: 60s, drop retention to 7d, add mem_limit to both containers. Enable anonymous viewer access in Grafana. Total RAM budget: ~850MB. Comfortable headroom.

Scenario 2 — Small production service (under 50 req/s): Add a lightweight app exporter — the official Redis or Postgres exporter each adds ~20–30MB. Keep Prometheus under 512MB with the retention and scrape tuning above. Monitor container_memory_usage_bytes via cAdvisor — but if cAdvisor itself climbs above 180MB, cut it and use Node Exporter’s systemd metrics instead.

Scenario 3 — Still getting OOM-killed despite tuning: Check dmesg | grep -i oom first. If Prometheus is the victim, its oom_score_adj can be lowered in the Compose file:

oom_score_adj: -500

That makes the kernel deprioritize Prometheus when looking for a kill target. It buys time — but the real fix is reducing series cardinality with metric_relabel_configs to drop high-cardinality labels you don’t actually query. Adjusting the score without fixing the root cause just shifts which process dies next.


What to Expect Over the Next 6–12 Months

Prometheus 3.x (active development as of Q1 2026) is pushing native histogram support and improved TSDB memory management. Early benchmarks from the OpenMetrics working group suggest memory reduction of 15–25% for typical infrastructure workloads in high-cardinality scenarios. That’ll matter for 2GB deployments.

Grafana Labs continues improving Grafana’s own memory profile — the 10.x series reduced idle memory by ~20% compared to 9.x. These gains compound over time.

Three things worth watching:

  • --storage.tsdb.head-chunks-write-queue-size tuning in Prometheus 3.x for low-memory modes
  • Grafana’s Alloy agent (replacing Grafana Agent) as a lighter Prometheus-compatible scraper
  • DigitalOcean and Hetzner both offering 4GB tiers at near-2GB pricing — the budget ceiling for “small VPS” monitoring is shifting upward, which changes the calculus on what’s worth tuning vs. what’s worth paying for

A tuned Prometheus and Grafana stack on a 2GB VPS isn’t magic. It’s four config changes: set scrape_interval to 60s, cap both containers with mem_limit, drop retention to 7d, and enable WAL compression. Do those four things and the OOM killer stops being your on-call engineer.

What’s the monitoring metric you find yourself checking at 3 AM most often? That’s the one worth keeping at 15s scrape resolution. Everything else can wait a minute.

References

  1. How to Monitor NVIDIA GPUs with Prometheus & Grafana (Docker Guide)
  2. Monitoring a Linux host with Prometheus, Node Exporter, and Docker Compose | Grafana Cloud documenta
  3. Prometheus + Grafana Docker Compose - Ready to Deploy | Docker Recipes

Photo by Ales Nesetril on Unsplash