Skip to main content

How to Self-Host OpenObserve: Datadog Alternative 2026

·OSSAlt Team
observabilitymonitoringself-hostingdockerdevops

How to Self-Host OpenObserve: The Open Source Datadog Alternative in 2026

TL;DR

OpenObserve is a Rust-built observability platform that handles logs, metrics, and traces in a single tool — and stores data with up to 140x better efficiency than Elasticsearch. Where Datadog charges $0.10–$0.25/GB/month for log ingestion plus storage, OpenObserve on a $20/month VPS can handle millions of log events per day essentially for free. It ships as a single binary or a tiny Docker image, has an OpenTelemetry-native ingestion API, and includes pre-built dashboards, alerts, and a SQL-based query language. If you're paying Datadog bills that make you wince, OpenObserve is the first alternative worth seriously evaluating.

Key Takeaways

  • Single binary: OpenObserve ships as one binary (~40MB), not a stack of 5 services like the ELK stack
  • 140x storage reduction: uses columnar storage (Parquet format) vs Elasticsearch's row-based indexes
  • OpenTelemetry native: accepts OTLP directly — no collector translation layer needed for most setups
  • Logs + Metrics + Traces: one tool, one storage backend, one query interface
  • SQL query language: query logs with SQL (SELECT * FROM logs WHERE level='error' LIMIT 100)
  • GitHub stars: 13,000+ (growing fast since its 2023 launch)
  • License: AGPL v3 (community) / commercial (enterprise features)

Why OpenObserve Instead of the ELK Stack or Loki?

The traditional self-hosted observability options have real problems:

Elasticsearch/Kibana (ELK): Powerful but resource-hungry. A production ELK stack needs 3+ nodes, 16GB+ RAM per node, and becomes complex fast. Storage efficiency is poor — indexing overhead can 3-5x your raw log size. Elasticsearch's indexing CPU cost is significant.

Grafana Loki: Much lighter than ELK, but logs only — you need Prometheus for metrics and Tempo for traces. Managing three separate systems, three retention policies, and three query languages (LogQL, PromQL, TraceQL) has real operational overhead.

OpenObserve: One binary, three signal types, one SQL-like query language. Not a replacement for every Loki use case (Loki's label-based indexing is better for very high-cardinality scenarios), but for the majority of self-hosters, OpenObserve's unified approach wins on simplicity.


Architecture Options

Single Node (Most Self-Hosters)

Your Apps
    ↓ (OTLP/HTTP or Fluent Bit or Vector)
OpenObserve (single container)
    ↓
Local disk or S3-compatible storage

Single-node handles millions of log events per day on a $20/month VPS. Suitable for teams up to ~50 engineers.

Cluster Mode (High Availability)

OpenObserve supports distributed mode for production clusters — separate ingester, querier, and compactor nodes backed by S3/MinIO. Most self-hosters don't need this.


Self-Hosting with Docker Compose

docker-compose.yml

version: '3.8'

services:
  openobserve:
    image: public.ecr.aws/zinclabs/openobserve:latest
    container_name: openobserve
    restart: unless-stopped
    ports:
      - "5080:5080"       # Web UI + HTTP API
      - "5081:5081"       # gRPC (OTLP traces)
    environment:
      ZO_ROOT_USER_EMAIL: "admin@example.com"
      ZO_ROOT_USER_PASSWORD: "changeme-strong-password"
      ZO_DATA_DIR: "/data"
      ZO_TELEMETRY: "false"             # Disable usage telemetry
      # Optional: Use S3 for storage instead of local disk
      # ZO_S3_BUCKET_NAME: "my-observability-bucket"
      # ZO_S3_REGION_NAME: "us-east-1"
      # ZO_S3_ACCESS_KEY: "AKIAIOSFODNN7EXAMPLE"
      # ZO_S3_SECRET_KEY: "secret"
    volumes:
      - openobserve_data:/data

  # Optional: Fluent Bit for log shipping
  fluent-bit:
    image: fluent/fluent-bit:latest
    volumes:
      - ./fluent-bit.conf:/fluent/etc/fluent-bit.conf:ro
      - /var/log:/var/log:ro      # Ship host logs
    depends_on:
      - openobserve

volumes:
  openobserve_data:

Start It

docker compose up -d
# Access UI at http://localhost:5080
# Login with the email/password from ZO_ROOT_USER_EMAIL/ZO_ROOT_USER_PASSWORD

Shipping Logs to OpenObserve

If you're already using the OTel Collector:

# otel-collector.yaml — add an OpenObserve exporter
exporters:
  otlphttp/openobserve:
    endpoint: http://openobserve:5080/api/default/
    headers:
      Authorization: "Basic BASE64(email:password)"
    compression: gzip

service:
  pipelines:
    logs:
      receivers: [otlp, filelog]
      processors: [batch]
      exporters: [otlphttp/openobserve]
    metrics:
      receivers: [otlp, prometheus]
      processors: [batch]
      exporters: [otlphttp/openobserve]
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [otlphttp/openobserve]

Option 2: Fluent Bit (For Existing Log Files)

# fluent-bit.conf
[SERVICE]
    Flush         5
    Log_Level     info

[INPUT]
    Name          tail
    Path          /var/log/*.log,/var/log/app/*.log
    Tag           app.logs
    Refresh_Interval 5
    Mem_Buf_Limit 50MB

[OUTPUT]
    Name          http
    Match         *
    Host          openobserve
    Port          5080
    URI           /api/default/logs/_json
    Format        json
    Header        Authorization Basic BASE64(email:password)
    Header        Content-Type application/json
    compress      gzip

Option 3: Vector (For High-Volume Pipelines)

# vector.toml
[sources.app_logs]
type = "file"
include = ["/var/log/app/*.log"]

[sources.docker_logs]
type = "docker_logs"

[sinks.openobserve]
type = "http"
inputs = ["app_logs", "docker_logs"]
uri = "http://openobserve:5080/api/default/logs/_json"
method = "post"
encoding.codec = "json"
auth.strategy = "basic"
auth.user = "admin@example.com"
auth.password = "changeme-strong-password"

Option 4: Direct HTTP API (For Custom Applications)

// Send structured logs directly from your app
async function sendLog(level: string, message: string, metadata: object) {
  const log = {
    level,
    message,
    timestamp: new Date().toISOString(),
    service: 'my-api',
    ...metadata,
  }

  await fetch('http://openobserve:5080/api/default/logs/_json', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Basic ' + btoa('admin@example.com:password'),
    },
    body: JSON.stringify([log]),
  })
}

// In your Express error handler:
app.use((err, req, res, next) => {
  sendLog('error', err.message, {
    stack: err.stack,
    path: req.path,
    method: req.method,
    statusCode: err.status ?? 500,
  })
  res.status(err.status ?? 500).json({ error: err.message })
})

Querying Logs with SQL

OpenObserve uses SQL for log queries — a major DX improvement over LogQL or Lucene syntax:

-- Basic queries
SELECT * FROM logs WHERE level = 'error' LIMIT 100

-- Aggregate error counts by service
SELECT service, COUNT(*) as error_count
FROM logs
WHERE level = 'error'
  AND _timestamp >= NOW() - INTERVAL 1 HOUR
GROUP BY service
ORDER BY error_count DESC

-- Find slow API requests
SELECT path, method, duration_ms, user_id
FROM logs
WHERE duration_ms > 500
  AND _timestamp >= NOW() - INTERVAL 24 HOURS
ORDER BY duration_ms DESC
LIMIT 50

-- Search log message text
SELECT *
FROM logs
WHERE message LIKE '%database connection%'
  AND level IN ('error', 'warn')
  AND _timestamp >= NOW() - INTERVAL 6 HOURS

-- Count events per minute (for trend analysis)
SELECT
  date_trunc('minute', _timestamp) as minute,
  COUNT(*) as events
FROM logs
WHERE _timestamp >= NOW() - INTERVAL 1 HOUR
GROUP BY minute
ORDER BY minute

Setting Up Alerts

OpenObserve has a built-in alerting system with Slack, PagerDuty, and webhook destinations:

// POST /api/default/alerts — Create an alert via API
{
  "name": "High Error Rate",
  "stream": "logs",
  "query": {
    "sql": "SELECT COUNT(*) as error_count FROM logs WHERE level='error' AND _timestamp >= NOW() - INTERVAL 5 MINUTES",
    "start_time": "now-5m",
    "end_time": "now"
  },
  "condition": {
    "column": "error_count",
    "operator": ">",
    "value": 50
  },
  "duration": 5,
  "frequency": 1,
  "destination": "slack-webhook"
}

Configure via the UI: Alerts → Create Alert → Set query → Set threshold → Choose destination.


Production Setup: Nginx Reverse Proxy with TLS

server {
    listen 443 ssl http2;
    server_name observe.yourdomain.com;

    ssl_certificate /etc/letsencrypt/live/observe.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/observe.yourdomain.com/privkey.pem;

    # Restrict UI access to your team IPs
    allow 10.0.0.0/8;
    allow 192.168.0.0/16;
    deny all;

    location / {
        proxy_pass http://localhost:5080;
        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;

        # Required for large log ingestion payloads
        proxy_read_timeout 300s;
        client_max_body_size 50m;
    }
}

Pre-Built Dashboards and Visualizations

OpenObserve ships with a dashboard system similar to Grafana — you can build visualizations using its query builder or SQL editor. For common patterns, import community dashboards:

# Import a Node.js dashboard via API
curl -X POST http://localhost:5080/api/default/dashboards \
  -H "Authorization: Basic BASE64(email:password)" \
  -H "Content-Type: application/json" \
  -d @nodejs-dashboard.json

The UI supports line charts, bar charts, heatmaps, stat panels, and tables. For teams deeply invested in Grafana's visualization ecosystem, OpenObserve also exposes a Grafana-compatible data source plugin — meaning you can use Grafana for dashboards while using OpenObserve for storage and query.


Cost Comparison: Datadog vs OpenObserve Self-Hosted

ScenarioDatadogOpenObserve (Self-Hosted)
1GB logs/day~$90/month$0 (fits on $6/month VPS)
10GB logs/day~$900/month~$20/month (VPS)
100GB logs/day~$9,000/month~$60/month (VPS + storage)
Metrics (100K series)~$250/month$0
APM traces~$400/month$0

The self-hosted infrastructure cost is almost entirely your VPS. Storage is cheap (S3/Backblaze B2) and OpenObserve's Parquet columnar format compresses logs aggressively.


OpenObserve vs Grafana Stack (Loki + Prometheus + Tempo)

AspectOpenObserveGrafana Stack
Setup complexityLow (1 container)High (3+ services)
Query languageSQLLogQL/PromQL/TraceQL
Resource usageLowMedium-High
VisualizationBuilt-inGrafana (excellent)
EcosystemGrowingMassive
Best forSimplicity, unifiedPower users, existing Grafana

Shipping Application Traces (APM)

OpenObserve accepts distributed traces via OTLP. Here's how to instrument a Node.js app:

// instrumentation.ts — run before your app starts
import { NodeSDK } from '@opentelemetry/sdk-node'
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http'
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'

const traceExporter = new OTLPTraceExporter({
  url: 'http://openobserve:5080/api/default/traces',
  headers: {
    Authorization: 'Basic ' + Buffer.from('admin@example.com:password').toString('base64'),
  },
})

const sdk = new NodeSDK({
  serviceName: 'my-api',
  traceExporter,
  instrumentations: [
    getNodeAutoInstrumentations({
      '@opentelemetry/instrumentation-http': { enabled: true },
      '@opentelemetry/instrumentation-express': { enabled: true },
      '@opentelemetry/instrumentation-pg': { enabled: true },
    }),
  ],
})

sdk.start()

Once running, every HTTP request, database query, and external API call generates a trace visible in OpenObserve's trace viewer. You can correlate traces with logs using the trace_id field — click a log line and jump to its trace.


Data Retention and Storage Management

OpenObserve lets you configure retention per stream:

# Set 30-day retention via API
curl -X PUT http://localhost:5080/api/default/streams/logs/settings \
  -H "Authorization: Basic BASE64(email:password)" \
  -H "Content-Type: application/json" \
  -d '{
    "data_retention": 30,
    "max_query_range": 7
  }'

For S3 storage, OpenObserve automatically compacts old data into Parquet files and applies lifecycle rules. A typical production setup:

  • Hot tier (last 7 days): local SSD for fast queries
  • Cold tier (7-90 days): S3 or MinIO (cheap object storage)
  • Archive (90+ days): Glacier or Backblaze B2 deep archive

For most self-hosters, a simple local disk with 30-day retention is sufficient.


Backup and Disaster Recovery

Since OpenObserve stores data as files (Parquet + WAL), backup is straightforward:

#!/bin/bash
# backup-openobserve.sh — daily backup cron

BACKUP_DATE=$(date +%Y%m%d)
BACKUP_DIR="/backups/openobserve/$BACKUP_DATE"

# Stop ingestion briefly for clean snapshot (optional — OZ handles concurrent writes)
docker compose stop openobserve

# Rsync data to backup location
rsync -av /opt/openobserve/data/ $BACKUP_DIR/

docker compose start openobserve

# Upload to S3/B2
rclone copy $BACKUP_DIR b2:my-backups/openobserve/$BACKUP_DATE

# Cleanup backups older than 7 days locally
find /backups/openobserve -type d -mtime +7 -exec rm -rf {} +

echo "Backup complete: $BACKUP_DIR"

Methodology

  • GitHub stars and community data from github.com/openobserve/openobserve, March 2026
  • Storage efficiency comparisons from OpenObserve documentation and community benchmarks
  • Pricing data from Datadog pricing page (datadoghq.com/pricing), March 2026
  • OpenObserve version: latest (check GitHub releases for current version)

Explore more open source Datadog alternatives on OSSAlt — community ratings, self-hosting difficulty, and feature comparisons.

Related: Best Open Source Alternatives to Datadog 2026 · Grafana + Prometheus + Loki: Self-Hosted Observability Stack 2026 · How to Self-Host Prometheus + Grafana 2026

Comments