Self-host Superlog for AI-triaged logs
Superlog turns OTLP traces, logs and metrics into incidents an agent triages. Run the self-hosted stack on your own VPS, and know the real footprint first.
What self-hosting Superlog actually installs
To self-host Superlog you clone the repository, bring up Postgres, ClickHouse and an OpenTelemetry collector with Docker Compose, run one database migration, then start four Node services from source. Your applications send OTLP (OpenTelemetry protocol) traces, logs and metrics to an intake port, Superlog fingerprints them, groups the repeated ones into a single incident, and an agent writes the first pass of the triage. The install takes an afternoon. The footprint and the honest limits are the parts worth reading before you start.
Superlog is Apache 2.0 licensed and lives at github.com/superloglabs/superlog. As of August 2026 it has around 1.2k stars, roughly 460 commits on main, and no release tags at all. That last point shapes the install: git checkout v1.0.0 has nothing to check out, so you pin a commit yourself or you run whatever main happened to be on the morning you cloned it.
What Superlog answers that Uptime Kuma and Langfuse do not
Self-hosted monitoring tools look interchangeable from the outside. They are not, and running the wrong one costs you a server for no benefit.
- Uptime Kuma probes your endpoints from outside and answers one question: is it up.
- Zabbix watches hosts and services on Ubuntu 24.04, with CPU, memory, disk and service state against thresholds you set.
- Langfuse traces LLM calls, recording the prompt, the model, the tokens, the latency and the cost of each one.
- Superlog takes the telemetry your ordinary services already emit and turns repeated failures into incidents.
Superlog owns a different question: something broke, what broke, and why. It has no opinion about LLM calls and it does not probe you from outside. It ingests OTLP from your normal application code and puts an agent on the triage step, which is the first pass a human on call would do anyway.
The distinction that matters for a VPS budget is the storage. Uptime Kuma runs happily on 1 GB of RAM because it stores a few thousand check results. Superlog carries a column store, because telemetry is written once and then queried by time range across millions of rows. That is what ClickHouse is for and what Postgres is not. Postgres is still in the stack, holding the small relational data: projects, users, incidents and ingest keys.
What does docker compose up -d actually start?
Three containers, and none of them is Superlog. This surprises people who expect a one command install.
postgres:16, published on host port 5434clickhouse/clickhouse-server:26.1, on 8123 for HTTP and 9000 for the native protocolotel/opentelemetry-collector-contrib:0.150.1, on 4317 for gRPC and 4318 for OTLP over HTTP
The Superlog applications run on the host, from source, started by pnpm dev. There is no production compose file in the repository as of August 2026, so a long lived install means your own systemd units around each app's start script, or the per-app Dockerfiles that ship in the tree.
Hold the path a span takes in your head, because every failure below is a break in one hop of it. Your app posts OTLP to the Superlog intake proxy. The proxy authenticates the request with your ingest key, stamps the project id on it, and forwards it to the collector. The collector strips any superlog.* attributes the client tried to set, adds superlog.project_id from the header the proxy provided, batches, and writes to ClickHouse. The web app and the API then read telemetry back from ClickHouse and everything else from Postgres.
That attribute stripping is a real multi-tenancy control, not decoration. Without it, anyone holding one valid ingest key could set superlog.project_id themselves and write into another project's data.
How big does the VPS need to be?
Plan for 4 vCPU, 8 GB of RAM and 40 GB of SSD for a single node install at low ingest volume. That is a planning floor, not a measurement, so treat it as a starting size and check it against your own traffic.
The memory goes to four places. ClickHouse is built for machines with plenty of RAM and its defaults assume that. Postgres 16 is modest here, since it holds metadata rather than telemetry. The collector is modest too. The four Node processes are not: a Vite development server plus three tsx watch processes each hold hundreds of megabytes, which is why pnpm dev on a 2 GB box is painful.
Disk is the quieter problem. pnpm install on this monorepo pulls the AWS SDK, a ClickHouse client, the OpenTelemetry SDK and a React toolchain before you have ingested a single span. ClickHouse then grows with your traffic. Measure both:
df -h /
free -m
docker stats --no-stream
docker compose exec clickhouse clickhouse-client --database superlog --query "SELECT table, formatReadableSize(sum(bytes_on_disk)) AS size FROM system.parts WHERE active AND database = 'superlog' GROUP BY table ORDER BY sum(bytes_on_disk) DESC"At low volume, a handful of services sending a few hundred spans a minute, the box is quiet and ClickHouse is idle most of the time. The load that hurts is the burst: one bad deploy producing thousands of identical errors a minute. Fingerprinting collapses those into one incident for the reader, but ClickHouse still writes every row underneath.
Retention is yours to set. The collector's ClickHouse exporter creates the tables, otel_traces, otel_logs and one table per metric type, and it only applies a time to live if the config in infra/collector/config.yaml sets one. Nothing expires on its own, so a busy month becomes a full disk unless you plan for it.
Install from a pinned commit
git clone https://github.com/superloglabs/superlog.git
cd superlog
git tag -l
git log -1 --format='%H %cs %s'git tag -l printing nothing is the expected result as of August 2026. Pick the commit you tested and stay on it:
git checkout 0d3a6c8bb63eda3493e6ba0003e7c2a70750bc1eNext, the toolchain:
node -v
corepack enable
corepack prepare pnpm@9.12.0 --activate
pnpm -vpackage.json declares engines.node as >=20.0.0 and packageManager as pnpm@9.12.0. Run the install on an older Node and pnpm stops with ERR_PNPM_UNSUPPORTED_ENGINE, naming the version it wanted. The nodejs package in the Ubuntu 24.04 archive is older than 20, so install Node 20 or newer from NodeSource or from nvm. The repository ships an .nvmrc, so nvm use picks the intended version if you have nvm.
pnpm install
docker compose up -d
docker compose psWait for the health checks instead of trusting up -d to mean ready. Postgres and ClickHouse both declare one in the compose file:
curl -sS http://127.0.0.1:8123/ping
pg_isready -h 127.0.0.1 -p 5434 -U postgresClickHouse answers Ok. and pg_isready answers accepting connections. Connection refused on 8123 means the container is still starting or has died. docker compose logs clickhouse shows which, and docker inspect $(docker compose ps -q clickhouse) | grep -i oomkilled reports true when the kernel killed it for memory, which points at the box being too small rather than at your config.
Then the migration and the applications:
pnpm --filter @superlog/db db:migrate
pnpm devNote the port: 5434, not 5432. The compose file publishes Postgres on 5434 so it does not collide with a Postgres already installed on the host, and the app .env.example files match, with DATABASE_URL=postgres://postgres:postgres@localhost:5434/superlog. Point the migration at 5432 on a box that already runs Postgres and you either get a refused connection or, worse, a migration applied to the wrong database.
pnpm dev starts the four processes listed in the repository's Procfile: api, web, worker and proxy. Each tees its output into tmp/logs/, so tail -f tmp/logs/proxy.log is where you watch ingest. The README puts the web app on http://localhost:5173, the API on http://localhost:4100 and the OTLP intake on http://localhost:4101.
Confirm what actually bound before you point anything at it:
ss -lntp | grep -E '4100|4101|5173'
curl -sS http://127.0.0.1:4101/healthThis matters later. The proxy reads its own port from the PORT environment variable and falls back to 4000 when PORT is unset. The development stack sets it for you. A systemd unit you write yourself does not, so an exporter aimed at 4101 against a proxy listening on 4000 fails with connection refused and gives you no other clue.
Send one trace, produce one error, see one incident
Create a project in the web app and copy its ingest key. The intake authenticates every request against that key, so telemetry sent without one never reaches ClickHouse.
Point any OpenTelemetry SDK at the intake using the standard environment variables:
export OTEL_SERVICE_NAME=checkout-api
export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
export OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4101
export OTEL_EXPORTER_OTLP_HEADERS='x-api-key=YOUR_INGEST_KEY'The intake reads the key from the x-api-key header, and also accepts authorization: bearer YOUR_INGEST_KEY if your exporter is easier to configure that way. It serves the three standard OTLP paths, /v1/traces, /v1/logs and /v1/metrics, plus /health.
One trap is worth naming. OTEL_EXPORTER_OTLP_ENDPOINT is a base URL and the SDK appends the signal path to it. The signal specific variables such as OTEL_EXPORTER_OTLP_TRACES_ENDPOINT are used exactly as written, with no path appended. Set the signal specific variable to http://127.0.0.1:4101 and every export posts to /, which is not a route, so nothing arrives and the SDK logs an export failure while your app looks healthy.
For a Node service the zero code path is enough to prove the pipeline:
npm install @opentelemetry/api @opentelemetry/auto-instrumentations-node
node --require @opentelemetry/auto-instrumentations-node/register server.jsNow break something on purpose. Any route that throws will do:
curl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:3000/boomCheck the hops in order, because the first gap tells you which one failed:
tail -n 50 tmp/logs/proxy.log
docker compose exec clickhouse clickhouse-client --database superlog --query 'SELECT count() FROM otel_traces'A rising count in otel_traces with an empty web app is a project mismatch, so check which project the ingest key belongs to. A flat count with activity in the proxy log points at the collector or the ClickHouse write, so read docker compose logs collector. No activity in the proxy log at all means the exporter never reached the intake: wrong port, wrong path, or a rejected key.
In the web app those repeated failures arrive as one incident rather than one row per request. Superlog fingerprints incoming signals and groups the matching ones, which is the difference between an inbox holding 4,000 identical errors and a page holding one. The agent then writes its investigation on top of that group.
The investigation step calls a model, so the worker needs a model provider configured. Take those variable names from the .env.example file inside each app directory of the commit you pinned rather than from any external write-up, because they move with main. The same applies to the GitHub and Sentry integrations, which carry their own setup documents at docs/github-app-setup.md and docs/sentry-app-setup.md, with webhook payloads documented in docs/webhooks.md.
Keep the intake private, and the agent read only
Docker publishes container ports on 0.0.0.0 by default, and those published ports bypass ufw, because Docker writes its own rules into the DOCKER-USER chain that are evaluated before ufw sees the packet. On a VPS with a public IP, the compose file as shipped puts ClickHouse HTTP on 8123 and Postgres on 5434 where the internet can reach them. The credentials in that file are development defaults: ClickHouse user default with an empty password, Postgres with postgres as both user and password.
Bind them to loopback. Every published port in the compose file takes its host side from an environment variable, so a .env in the repository root is enough:
POSTGRES_HOST_PORT=127.0.0.1:5434
CLICKHOUSE_HTTP_HOST_PORT=127.0.0.1:8123
CLICKHOUSE_TCP_HOST_PORT=127.0.0.1:9000
COLLECTOR_GRPC_HOST_PORT=127.0.0.1:4317
COLLECTOR_HTTP_HOST_PORT=127.0.0.1:4318Verify the result before you trust it, then recreate the containers:
docker compose config
docker compose up -d
ss -lntp | grep -E '5434|8123|9000|4317|4318'docker compose config prints the resolved file, so you can read 127.0.0.1:5434:5432 instead of guessing. ss should then show 127.0.0.1:5434 and never 0.0.0.0:5434. Do not try to fix this with a compose override file that re-declares ports, because Compose concatenates port lists across files rather than replacing them, so you end up with both bindings and the public one still open.
The intake needs the same care. Your ingest key travels in a header, so it needs TLS (transport layer security) in front of it: terminate TLS in nginx or Caddy ahead of the proxy, or keep ingest inside a private network or a WireGuard tunnel. The web app on 5173 is a Vite development server and has no business facing the internet at all.
Then the agent itself. Superlog's pitch is that the agent investigates and proposes a fix, and the important word is proposes. Keep it read only against production until you have watched it work on a few real incidents. Give the GitHub App read scopes and let it open pull requests that you review. An agent that reads telemetry and writes a patch is useful. An agent that can restart your services is a different level of risk, and that should be a decision you make on purpose rather than a default you inherit. Cost deserves the same attention, since every investigation is a model call: budget for agent spend on a VPS before you aim it at a noisy production system, and keep a record of what the agent actually did so a surprising pull request has an audit trail behind it.
Failures you will hit, and the strings that name them
ERR_PNPM_UNSUPPORTED_ENGINEduringpnpm installmeans Node is older than 20.node -vconfirms it in one line.ECONNREFUSED 127.0.0.1:5434during the migration means the compose stack is not up, orDATABASE_URLnames the wrong port.- ClickHouse restarting in a loop is usually memory. Read
docker compose logs clickhouse, then check the container forOOMKilledbeingtrue. - An exporter that reports success while the web app stays empty usually means the data went straight to the collector on 4318, which skips the project stamping the proxy does.
- Connection refused on 4101 in a production install means the proxy fell back to
PORT=4000. SetPORTexplicitly in the unit file. docker compose psshowing0.0.0.0:8123means your loopback bindings are not in effect. Rundocker compose configand read the resolved ports.
Flawless, HyperProbe, and where Superlog sits
This category is young, and the tools split on what the agent is allowed to touch. Flawless is an open source AI SRE (site reliability engineering) tool aimed at Kubernetes, reading from an existing Prometheus, Loki and Grafana stack rather than owning the pipeline. HyperProbe goes the other way: it is a hosted product, closed source as of August 2026, that places read only probes inside a running process to capture variable state and exposes that state to an assistant over MCP (model context protocol).
Superlog sits between the two. It owns the pipeline end to end, from OTLP intake through to ClickHouse storage, and it places the agent on the triage step rather than on the fix step. That design is exactly why self-hosting it is an infrastructure decision and not a container you forget about. Once you run Superlog, you run a column store, and it needs the same care as any other database you own.
FAQ
How much RAM does a self-hosted Superlog need?
Plan for 8 GB of RAM, 4 vCPU and 40 GB of disk for a single node at low ingest volume. The stack is Postgres plus ClickHouse plus an OpenTelemetry collector plus four Node processes, and ClickHouse expects headroom. A 1 GB or 2 GB VPS is not enough: pnpm install alone is heavy, and ClickHouse gets killed by the kernel out of memory killer under load. Measure your own numbers with docker stats --no-stream and free -m rather than trusting any published figure, including this one.
Which port do I point my OTLP exporter at?
The Superlog intake proxy, which the README puts on http://localhost:4101. It serves /v1/traces, /v1/logs and /v1/metrics, and it authenticates with your project's ingest key taken from the x-api-key header or from an authorization: bearer header. Port 4318 is the OpenTelemetry collector underneath, and exporting there directly skips the proxy, which is the component that stamps your project id onto the data. The proxy falls back to port 4000 when PORT is unset, so run ss -lntp and confirm what it bound before assuming 4101.
Does Superlog replace Uptime Kuma or Zabbix?
No. Uptime Kuma answers whether an endpoint responds from outside your network, and Zabbix watches host and service metrics against thresholds you set. Superlog consumes the traces, logs and metrics your applications emit and groups repeated failures into incidents. Keep an external uptime probe alongside it, because a probe running elsewhere still reports when the box holding your telemetry pipeline is the thing that died.
Can the Superlog agent change my production systems?
Only through permissions you grant it. Its output is an investigation and a proposed change that a human reviews. Keep the GitHub App on read scopes with pull requests at first, and keep any credentials the worker holds scoped to reading. Treat write access to production as a separate decision made deliberately, because an agent that can restart services is a much larger commitment than an agent that reads telemetry and writes a patch for review.
Should I pin a commit or track main?
Pin a commit. There are no release tags in the repository as of August 2026, so main is the only moving target on offer and it takes several commits a week. Record the SHA you tested, deploy that one, and read the diff before you move forward. git log --oneline <old-sha>..main is the review, and the per-app .env.example files are the first place to look for newly required variables after any bump.