DEV Community

Elder Fernandes
Elder Fernandes

Posted on Originally published at selfhoststack-8z4.pages.dev

The Ultimate Self-Hosted Observability Stack: Prometheus + Grafana + Loki + Uptime Kuma

The Ultimate Self-Hosted Observability Stack: Prometheus + Grafana + Loki + Uptime Kuma

Vendor lock-in on APM and monitoring platforms like Datadog, New Relic, and Dynatrace is notorious for unpredictable billing spikes. Adding custom tags, ingestion volume, or retained log indices can quickly turn a $200/mo budget into a $2,000/mo surprise invoice.

With this open-source Docker Compose observability pipeline, you get:

  • System Metrics & Dashboards: Prometheus + Node Exporter + cAdvisor visualized in Grafana.
  • Structured Log Aggregation: Promtail shipping container logs to Loki without heavy ElasticSearch overhead.
  • External Uptime & HTTP Checks: Uptime Kuma with instant Telegram, Discord, and webhook incident alerts.
  • Resource Footprint: Runs reliably on a modest VPS (4GB–8GB RAM) for dozens of microservices.

High-Level Architecture

[ Docker Containers & Linux Host ]
        |
        +---> Node Exporter (CPU, RAM, Disk, Network) ----+
        |                                                  |
        +---> cAdvisor (Container CPU, Memory, I/O) ------+---> Prometheus (Metrics Storage)
        |                                                  |         |
        +---> Promtail (Container Docker Logs) ---------> Loki       v
                                                            |---> Grafana (Unified Dashboard)
                                                            |
[ External Web Services / APIs ] ------------------------------> Uptime Kuma (Status & Alerts)
Enter fullscreen mode Exit fullscreen mode

1. Production Docker Compose (docker-compose.yml)

version: "3.8"

networks:
  monitoring-net:
    driver: bridge

volumes:
  prometheus-data:
  grafana-data:
  loki-data:
  uptime-kuma-data:
  caddy-data:
  caddy-config:

services:
  # --- Reverse Proxy with Auto HTTPS ---
  caddy:
    image: caddy:2-alpine
    container_name: monitor-caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy-data:/data
      - caddy-config:/config
    networks:
      - monitoring-net
    depends_on:
      - grafana
      - uptime-kuma

  # --- Metrics Scraper & Time-Series DB ---
  prometheus:
    image: prom/prometheus:v2.51.0
    container_name: prometheus
    restart: unless-stopped
    command:
      - "--config.file=/etc/prometheus/prometheus.yml"
      - "--storage.tsdb.path=/prometheus"
      - "--storage.tsdb.retention.time=30d"
      - "--web.enable-lifecycle"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - prometheus-data:/prometheus
    networks:
      - monitoring-net

  # --- Host Metrics Collector ---
  node-exporter:
    image: prom/node-exporter:v1.7.0
    container_name: node-exporter
    restart: unless-stopped
    volumes:
      - /proc:/host/proc:ro
      - /sys:/host/sys:ro
      - /:/rootfs:ro
    command:
      - "--path.procfs=/host/proc"
      - "--path.rootfs=/rootfs"
      - "--path.sysfs=/host/sys"
      - "--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)"
    networks:
      - monitoring-net

  # --- Docker Container Metrics ---
  cadvisor:
    image: gcr.io/cadvisor/cadvisor:v0.49.1
    container_name: cadvisor
    restart: unless-stopped
    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
    networks:
      - monitoring-net

  # --- Log Aggregation Engine ---
  loki:
    image: grafana/loki:3.0.0
    container_name: loki
    restart: unless-stopped
    command: -config.file=/etc/loki/local-config.yaml
    volumes:
      - ./loki-config.yaml:/etc/loki/local-config.yaml:ro
      - loki-data:/loki
    networks:
      - monitoring-net

  # --- Log Shipper (Promtail) ---
  promtail:
    image: grafana/promtail:3.0.0
    container_name: promtail
    restart: unless-stopped
    volumes:
      - /var/log:/var/log:ro
      - /var/lib/docker/containers:/var/lib/docker/containers:ro
      - ./promtail-config.yaml:/etc/promtail/config.yml:ro
    command: -config.file=/etc/promtail/config.yml
    networks:
      - monitoring-net
    depends_on:
      - loki

  # --- Visualization Dashboard ---
  grafana:
    image: grafana/grafana:10.4.0
    container_name: grafana
    restart: unless-stopped
    environment:
      - GF_SECURITY_ADMIN_USER=admin
      - GF_SECURITY_ADMIN_PASSWORD=ChangeThisSecurePassword!
      - GF_USERS_ALLOW_SIGN_UP=false
    volumes:
      - grafana-data:/var/lib/grafana
    networks:
      - monitoring-net

  # --- Synthetic Uptime & Public Status Page ---
  uptime-kuma:
    image: louislam/uptime-kuma:1.23.13-alpine
    container_name: uptime-kuma
    restart: unless-stopped
    volumes:
      - uptime-kuma-data:/app/data
    networks:
      - monitoring-net
Enter fullscreen mode Exit fullscreen mode

2. Configuration Files

prometheus.yml

global:
  scrape_interval: 15s
  evaluation_interval: 15s

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

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

  - job_name: "cadvisor"
    static_configs:
      - targets: ["cadvisor:8080"]
Enter fullscreen mode Exit fullscreen mode

loki-config.yaml

auth_enabled: false

server:
  http_listen_port: 3100
  grpc_listen_port: 9096

common:
  instance_addr: 127.0.0.1
  path_prefix: /loki
  storage:
    filesystem:
      chunks_directory: /loki/chunks
      rules_directory: /loki/rules
  replication_factor: 1
  ring:
    kvstore:
      store: inmemory

schema_config:
  configs:
    - from: 2024-01-01
      store: tsdb
      object_store: filesystem
      schema: v13
      index:
        prefix: index_
        period: 24h

limits_config:
  reject_old_samples: true
  reject_old_samples_max_age: 168h
  retention_period: 30d
Enter fullscreen mode Exit fullscreen mode

Caddyfile

grafana.yourdomain.com {
    encode gzip zstd
    reverse_proxy grafana:3000
}

status.yourdomain.com {
    encode gzip zstd
    reverse_proxy uptime-kuma:3001
}
Enter fullscreen mode Exit fullscreen mode

3. Recommended Grafana Dashboards to Import

Once Grafana is online, go to Dashboards -> Import and paste these official Community Dashboard IDs:

  1. Node Exporter Full (ID: 1860): Complete server stats (CPU utilization, RAM saturation, disk I/O, network bandwidth).
  2. cAdvisor Docker Monitoring (ID: 14282): Per-container memory consumption, CPU limits, network drops.
  3. Loki Docker Logs (ID: 13639): Live log streaming with real-time regex search.

4. Setting Up Alerting in Uptime Kuma

  1. Open https://status.yourdomain.com.
  2. Go to Settings -> Notifications -> Add Notification.
  3. Select Telegram / Discord / Slack / Webhook.
  4. Set check interval to 30s and retry count to 2.
  5. Create a public status page to display uptime SLA to your clients with zero extra subscription fees.

Summary & Cost Comparison

  • Cloud APM & Log SaaS (Datadog / New Relic): $250 - $2,500/month.
  • Self-Hosted Stack: $10 - $20/month on a Hetzner CPX31 (4 vCPU, 8GB RAM).
  • Annual Savings: $2,800 to $29,000+ per year.

Production Stacks & Blueprints

Want curated Docker Compose configs, security hardening, automated backups, and reverse proxy blueprints for 30+ top open-source tools?

👉 Check out SelfHostStack or grab the turnkey Self-Hosted Starter Stack Pack ($29).

Top comments (0)