How to Self-Host OpenObserve: Datadog Alternative 2026
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
Option 1: OpenTelemetry Collector (Recommended)
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
| Scenario | Datadog | OpenObserve (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)
| Aspect | OpenObserve | Grafana Stack |
|---|---|---|
| Setup complexity | Low (1 container) | High (3+ services) |
| Query language | SQL | LogQL/PromQL/TraceQL |
| Resource usage | Low | Medium-High |
| Visualization | Built-in | Grafana (excellent) |
| Ecosystem | Growing | Massive |
| Best for | Simplicity, unified | Power 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