How to Deploy OpenObserve on a VPS
Updated Sep 2026
verified on Ubuntu 26.04 · Sep 2026Self-host OpenObserve, a single-binary store for logs, metrics and traces, on a VPS with Docker Compose. Generate root credentials, keep it on loopback behind Caddy, and set retention before the default ten years.
- A VPS with 1 GB RAM or more (2 GB+ once real log volume arrives)
- A fresh Ubuntu 24.04 or 26.04 server with root/sudo SSH access
- A domain you can point at the server
- Docker Engine + Compose installed (see the base guide below)
What OpenObserve is
OpenObserve is an observability backend written in Rust and shipped as a single binary. It ingests logs, metrics, traces and frontend RUM events, stores them as compressed Parquet files, and lets you search them with SQL from its own web UI. It speaks the protocols your tools already produce. Logs arrive as JSON or through Elasticsearch-style bulk requests. Traces arrive over OTLP. Metrics arrive through Prometheus remote write. So Fluent Bit, Vector, the OpenTelemetry Collector or a Prometheus server can send to it without a custom agent.
For a single VPS, the selling point is that there is nothing else to run. There is no Elasticsearch cluster, no ClickHouse and no separate metadata database, just one container and one data directory. SigNoz vs OpenObserve compares it with the ClickHouse-based SigNoz. If you only need host metrics and dashboards, Prometheus is the narrower tool.
One design fact to know before you send anything: OpenObserve's README states that ingested data is immutable. Individual records cannot be modified or deleted. Data only goes away when it ages out of retention. Keep secrets and personal data out of your logs at the source, because you can't scrub them later.
Server sizing
- RAM: our catalog lists 512 MB as the minimum. In our install check (GCP e2-standard-2, Ubuntu 26.04, September 2026), OpenObserve with no data idled at about 238 MB of RAM and took about 556 MB of Docker disk. Memory rises with ingest rate and with the size of the queries you run, so plan on 2 GB or more once real log volume arrives.
- Disk: this is where the growth goes. Parquet compression keeps the data
small, but volume and retention decide the total. Start with 40 GB of SSD and
watch
dufor the first week. - CPU: one or two vCPUs handle a small fleet's logs comfortably. Heavy full-text searches over long time ranges are what consume CPU.
Keep it on its own box, apart from the services that send it data. The Best VPS for Monitoring page ranks hosts for this.
Prepare the server
This guide assumes Docker Engine and the Compose plugin are installed. If not, work through Docker & Compose on Ubuntu first. Open SSH and the reverse-proxy ports only:
sudo ufw allow OpenSSH
sudo ufw allow 80
sudo ufw allow 443
sudo ufw --force enable
sudo ufw status verbose
OpenObserve's HTTP port (5080) will be bound to 127.0.0.1 below. Docker's
published ports bypass ufw, so the loopback bind is what actually keeps it
private, not the firewall.
Paid link — we earn a commission if you shop through it.
Generate the root credentials
OpenObserve creates its root user from two environment variables,
ZO_ROOT_USER_EMAIL and ZO_ROOT_USER_PASSWORD, on first startup only.
After that they're stored in its metadata and are no longer required. The
password must be 8–128 characters with an uppercase letter, a lowercase
letter, a digit and a symbol, or the server refuses to start. Don't reuse the
Complexpass#123 example from the quick-start.
Put them in an .env file that only you can read:
mkdir -p ~/openobserve && cd ~/openobserve
if [ ! -f .env ]; then
printf 'ZO_ROOT_USER_EMAIL=admin@example.com\nZO_ROOT_USER_PASSWORD=%s\n' \
"$(openssl rand -hex 12)Aa1#" > .env
fi
chmod 600 .env
cut -d= -f1 .env
The generated password is 24 random hex characters with Aa1# appended, so
the complexity rule is always met. Change the email to yours. It is your login.
Read the password with cat ~/openobserve/.env when you first sign in.
Install OpenObserve (Docker Compose)
This is the open-source image from OpenObserve's public registry, pinned to a release so upgrades happen when you choose:
cd ~/openobserve
cat > docker-compose.yml <<'YAML'
services:
openobserve:
image: public.ecr.aws/zinclabs/openobserve:v1.0.4
container_name: openobserve
restart: unless-stopped
env_file: .env
environment:
ZO_DATA_DIR: /data
ZO_COMPACT_DATA_RETENTION_DAYS: "30"
ZO_TELEMETRY: "false"
ports:
# Loopback only: Caddy is the public entry point.
- "127.0.0.1:5080:5080"
volumes:
- openobserve-data:/data
volumes:
openobserve-data:
YAML
docker compose up -d
Three settings are worth explaining:
ZO_COMPACT_DATA_RETENTION_DAYS: "30". The default is 3650 days, ten years. On a VPS disk that is a slow-motion outage, so set a number you can afford. The documented minimum is 3. You can also set retention per stream in the UI.ZO_TELEMETRY: "false". OpenObserve sends anonymous usage telemetry by default. This turns it off.ZO_DATA_DIR: /data. Parquet files and metadata live here, on a named volume that survives container recreation.
Wait for the health check, then prove ingestion works by sending two log lines
to a stream called smoke in the default organisation:
cd ~/openobserve
for i in $(seq 1 30); do
curl -fs http://127.0.0.1:5080/healthz && break
sleep 2
done
echo
set -a; . ./.env; set +a
curl -s -u "$ZO_ROOT_USER_EMAIL:$ZO_ROOT_USER_PASSWORD" \
http://127.0.0.1:5080/api/default/smoke/_json \
-d '[{"level":"info","message":"hello from the VPS"},{"level":"warn","message":"second line"}]'
echo
A response listing the smoke stream with "successful":2 means OpenObserve
accepted both records. Open the UI and they appear under Logs once you pick
the smoke stream.
HTTPS + domain
Point an A record for logs.example.com at the server and put
Caddy in front of the loopback port:
logs.example.com {
reverse_proxy 127.0.0.1:5080
}
If alerts and links should carry the public URL, set ZO_WEB_URL (for example
https://logs.example.com) in the environment: block and recreate the
container. The docs describe it as the UI URL used for redirects and alert
links.
Sending data
Every ingestion endpoint takes the organisation in the path (default unless
you create others) and uses HTTP basic auth. The Data Sources page in the
UI shows ready-made snippets with your credentials filled in. The endpoints you
will use most:
- Logs (JSON):
POST /api/default/<stream>/_json, as in the smoke test. - Traces (OTLP over HTTP):
POST /api/default/traces, the endpoint an OpenTelemetry Collector'sotlphttpexporter targets. - Metrics:
POST /api/default/prometheus/api/v1/write. Add it as aremote_writeURL in an existing Prometheus.
Don't use the root account for shippers. Create a separate user for ingestion in the UI, so a leaked agent config doesn't hand out admin access. Senders on other machines should go through the HTTPS domain, never a published 5080.
Backups
Everything is in the openobserve-data volume: the Parquet data and the
metadata (users, streams, dashboards, alerts). Stop the container for a
consistent copy:
cd ~/openobserve
docker compose stop
docker run --rm -v openobserve_openobserve-data:/data -v "$PWD":/backup alpine \
tar czf /backup/openobserve-$(date +%F).tar.gz -C /data .
docker compose start
ls -lh ~/openobserve/*.tar.gz
The volume name gets the compose project prefix (openobserve_). Check it with
docker volume ls. Copy the archive off the box. For larger installs,
OpenObserve can keep its data in S3-compatible object storage instead of local
disk. That changes the backup story to the bucket's own versioning, so read the
storage docs before relying on it.
Upgrades
Change the image tag in docker-compose.yml to the new release, back up, then:
cd ~/openobserve
docker compose pull
docker compose up -d
Read the release notes before a major version jump, and keep the backup until you've confirmed old data still searches correctly.
Troubleshooting
The container exits immediately on first start. Read docker compose logs openobserve. A password that fails the complexity rule is the usual cause.
The server refuses to create the root user with it.
Login fails after you changed .env. The root credentials are only read on
first startup. Editing .env later changes nothing. Change the password in the
UI instead.
Ingest returns 401. Check the basic-auth credentials the shipper sends, and
the organisation name in the path (/api/default/...).
The disk is filling faster than expected. Lower
ZO_COMPACT_DATA_RETENTION_DAYS or set per-stream retention in the UI. Remember
that retention only drops whole time ranges, and nothing inside them can be
deleted selectively.
Verification + next steps
You're done when https://logs.example.com loads over a valid certificate,
you can sign in as the root user, the smoke stream shows two records, and a
backup archive sits somewhere off the box.
Next: point a log shipper at a real stream, create an ingestion-only user, and set per-stream retention for anything noisy. For metrics dashboards on top of Prometheus, see Grafana.