How to self-host Openfuse (Langfuse on GreptimeDB) with Docker Compose. The store is GreptimeDB: gRPC :4001 for ingest writes, MySQL wire :4002 for reads and migrations. There is no ClickHouse in this build.
For local development (no app containers), see development. For monitoring, performance/compaction, backup, and upgrades, see operations.
There are two starting points:
- Evaluation / quickstart —
cp .env.quickstart.example .env. This ships working dev defaults (insecure, public) plus an auto-provisioned demo project, so the bundled Compose files boot with zero edits. Do not use it on a network you do not control. - Real deployment —
cp .env.prod.example .env, then set your own values for the secrets below.
| Variable | Purpose | How to set |
|---|---|---|
NEXTAUTH_SECRET |
Signs NextAuth session tokens. | openssl rand -base64 32 |
SALT |
Salts the hash of project API keys. | openssl rand -base64 32 |
ENCRYPTION_KEY |
Encrypts sensitive data at rest; must be 256-bit (64 hex chars). | openssl rand -hex 32 |
POSTGRES_PASSWORD + DATABASE_URL |
Postgres password (the server's and the app's must match). | Pick a strong password; use it for POSTGRES_PASSWORD and inside DATABASE_URL. |
REDIS_AUTH |
Redis password (the server's and the app's must match). | Pick a strong password. |
GREPTIME_USER / GREPTIME_PASSWORD |
GreptimeDB credentials. A non-empty password turns on enforced auth. | User defaults to openfuse. Set a strong GREPTIME_PASSWORD for any real deployment (see "GreptimeDB authentication" below). |
The source of truth for every GREPTIME_* variable is packages/shared/src/env.ts; .env.prod.example is the full deploy-time reference.
GreptimeDB variables:
| Variable | Default | Notes |
|---|---|---|
GREPTIME_GRPC_URL |
localhost:4001 |
Ingest SDK endpoint (writes). One address, or comma-separated frontends for a cluster. |
GREPTIME_SQL_HOST |
localhost |
MySQL-wire host (reads + migrations). |
GREPTIME_SQL_PORT |
4002 |
MySQL-wire port. |
GREPTIME_SQL_READ_ONLY_HOST |
(unset) | Optional dedicated read host; falls back to GREPTIME_SQL_HOST. |
GREPTIME_DB |
openfuse |
Target database. |
GREPTIME_USER / GREPTIME_PASSWORD |
openfuse / "" |
Empty password = unauthenticated single node; set a password to enforce auth. |
GREPTIME_SQL_MAX_OPEN_CONNECTIONS |
25 |
MySQL read-pool size. |
GREPTIME_RAW_EVENTS_TABLE |
raw_events |
Write-path source-of-truth table. |
LANGFUSE_GREPTIME_TTL |
730d |
Database-level retention, applied automatically at startup (see §2). |
For a Compose deployment these point at the greptimedb service name. The non-GreptimeDB requirements (Postgres, Redis, NEXTAUTH_SECRET, SALT, ENCRYPTION_KEY, …) are unchanged from upstream Langfuse and also live in .env.prod.example; for the full set and their meaning, see Langfuse · Configuration.
GreptimeDB only enforces credentials when it is started with a static user provider; a node started without one accepts any connection. The Compose files wire this for you through a small entrypoint (docker/greptimedb/entrypoint.sh):
GREPTIME_PASSWORDempty (default) — thegreptimedbcontainer starts without a user provider, so it is unauthenticated. Fine for a local single node you fully control (quickstart, dev). Do not expose it.GREPTIME_PASSWORDset — the container writes auser=passwordcredentials file (mode600) and starts with--user-provider=static_user_provider:file:…, so the password is actually enforced. The app authenticates with the sameGREPTIME_USER(defaultopenfuse) andGREPTIME_PASSWORD, so the app and server values must match. Set both in.envfor any real deployment, just likePOSTGRES_PASSWORDandREDIS_AUTH.
The fork ships a small server config at docker/greptimedb/config.toml, mounted read-only and passed via --config-file. It keeps GreptimeDB's defaults and carries commented performance-tuning hints (cache and write-buffer sizes default to Auto, sized from machine memory — right for a single node). Tune it per the GreptimeDB performance-tuning guide; see also operations.
Ingestion and eval-generated scores persist to GreptimeDB raw_events, not to a blob store. Every remaining object-storage consumer supports a local-file backend, so a stock deployment needs no MinIO/S3:
| Variable | App default | Bundled Compose | Local backend |
|---|---|---|---|
LANGFUSE_MEDIA_STORAGE_BACKEND |
s3 |
local |
local + LANGFUSE_MEDIA_LOCAL_PATH |
LANGFUSE_EVENT_STORAGE_BACKEND |
s3 |
local |
local + LANGFUSE_EVENT_LOCAL_PATH |
LANGFUSE_BATCH_EXPORT_STORAGE_BACKEND |
s3 |
local |
local + LANGFUSE_BATCH_EXPORT_LOCAL_PATH |
The application default for these variables is s3, but this repo's docker-compose.yml overrides them to local (${...:-local}), so the bundled stack starts with no object store. Each local backend shares a filesystem volume between web and worker (the Compose files wire langfuse_event_data and langfuse_batch_export_data); LANGFUSE_EVENT_STORAGE_BACKEND covers both the OTel carrier and the eval blob store. Batch exports on the local backend are served by an authenticated web download route (signed, time-limited token, re-validated against the export row) instead of an S3 presigned URL, so the link in the export email and the exports page works without a bucket. The Compose files put MinIO behind a s3 profile (docker compose --profile s3 up), so the default stack starts no object store.
The Compose files use Docker named volumes for every stateful path. Treat these as the deployment's data directory set:
| Volume | Mounted path | Stores |
|---|---|---|
langfuse_postgres_data |
/var/lib/postgresql/data |
Postgres application state: users, projects, API keys, prompts, config |
langfuse_greptimedb_data |
/greptimedb_data |
GreptimeDB analytics store: traces, observations, scores, dashboards |
langfuse_redis_data |
/data |
Redis queue state |
langfuse_media_data |
/langfuse_media_data |
Local media uploads when LANGFUSE_MEDIA_STORAGE_BACKEND=local |
langfuse_event_data |
/langfuse_event_data |
Local OTel carrier and eval blobs when LANGFUSE_EVENT_STORAGE_BACKEND=local |
langfuse_batch_export_data |
/langfuse_batch_export_data |
Local batch-export files when LANGFUSE_BATCH_EXPORT_STORAGE_BACKEND=local |
langfuse_minio_data |
/data in minio |
Optional MinIO bucket data when the s3 profile is enabled |
Runtime logs are not written to a separate application log directory by default. Web, worker, the standalone supervisor, Postgres, Redis, and GreptimeDB all write to container stdout/stderr; collect them through docker compose logs or your Docker logging driver. If you enable GreptimeDB file logging or replace Docker named volumes with bind mounts, keep the log directory outside the container's writable layer and include it in the same backup/retention plan as langfuse_greptimedb_data.
Do not run docker compose down -v on a real deployment unless you intentionally want to delete the service data. docker compose down removes containers and networks but keeps the named volumes; down -v removes them. If you replace the named volumes with bind mounts, keep the same container paths above and make sure the app containers can write to the media/event paths. In the split topology, both langfuse-web and langfuse-worker must mount the same langfuse_media_data, langfuse_event_data, and langfuse_batch_export_data storage.
Both schemas are applied by the container entrypoint when the app starts — you do not bootstrap anything by hand for a normal deployment:
- Postgres migrations run from the
langfuse-web(and standalone) entrypoint, gated byLANGFUSE_AUTO_POSTGRES_MIGRATION_DISABLED. - GreptimeDB schema runs from the same entrypoint, gated by
LANGFUSE_AUTO_GREPTIME_MIGRATION_DISABLED. It applies everypackages/shared/greptime/migrations/*.sqlplus the database-level retention TTL (ALTER DATABASE ... SET 'ttl'fromLANGFUSE_GREPTIME_TTL, default730d, covering every table at once).
Both are idempotent and fail-closed: the GreptimeDB runner re-applies the full set on every start (there is no migration ledger), tolerating only the one common non-idempotent re-run error (ADD COLUMN on an existing column), and a Postgres advisory lock serialises concurrent web replicas so two containers never migrate at once. If a migration fails, the container exits rather than serving against an un-migrated store. To change retention later, set LANGFUSE_GREPTIME_TTL and restart.
The langfuse-worker image does not run migrations (it relies on web/standalone having applied them first), matching upstream Langfuse.
You only need this if you set LANGFUSE_AUTO_GREPTIME_MIGRATION_DISABLED=true or are bootstrapping a host without the app containers (e.g. local dev — see development). It needs Node + pnpm via corepack (pnpm install once first) and, from the host, the localhost override for the container-only service name:
pnpm install
GREPTIME_GRPC_URL=localhost:4001 \
GREPTIME_SQL_HOST=localhost \
pnpm --filter=@langfuse/shared run greptime:migrateThere are two topologies. Both read the same .env, both auto-migrate on startup (§2), and both persist GreptimeDB to a named volume. Start with standalone; move to the split topology when you need to scale web and worker independently — that is a deployment change, not a data migration (the stores outlive the app containers).
tma1ai/openfuse-standalone runs both the web server and the worker in one container under a process supervisor — the GreptimeDB-standalone analogue for Openfuse. docker-compose.standalone.yml wires it to Postgres, Redis, and GreptimeDB.
Run the published image:
OPENFUSE_STANDALONE_IMAGE=tma1ai/openfuse-standalone:1.0.0-beta.1 \
docker compose -f docker-compose.standalone.yml up -d --pull alwaysThen open http://localhost:3000.
Or build from this repo:
docker compose -f docker-compose.standalone.yml up -dThe supervisor treats the two processes as one unit: if either exits, the container stops so your restart policy restarts the whole thing. To keep the image tag across restarts, put this in .env:
OPENFUSE_STANDALONE_IMAGE=tma1ai/openfuse-standalone:1.0.0-beta.1The production stack runs web and worker as separate, independently scalable services: langfuse-web, langfuse-worker, greptimedb, postgres, redis (minio is defined but gated behind the s3 profile, so it does not start by default). GreptimeDB runs in standalone mode and persists to the langfuse_greptimedb_data volume.
Run the published images:
OPENFUSE_WEB_IMAGE=tma1ai/openfuse-web:1.0.0-beta.1 \
OPENFUSE_WORKER_IMAGE=tma1ai/openfuse-worker:1.0.0-beta.1 \
docker compose up -d --pull alwaysOr build web/worker from this repo:
docker compose up -dThe split Compose file defines both build: and image: for web/worker, so --pull always is what makes it pull the published images rather than build locally; a plain docker compose up builds. Validate the file first with docker compose config -q.
Images are published to Docker Hub by the release-images.yml workflow on each v* git tag: tma1ai/openfuse-web, tma1ai/openfuse-worker, tma1ai/openfuse-standalone. The current preview is 1.0.0-beta.1. A v* tag always publishes the exact semver (e.g. 1.0.0-beta.1) and a commit-SHA tag. The floating major.minor and major tags and latest are published only for stable releases — the workflow skips all of them for any SemVer pre-release (-alpha / -beta / -rc), which get only the exact {{version}} and commit-SHA tags. So during the beta, latest does not move; pin an explicit tag. To upgrade later, bump the pinned tag and re-run docker compose up -d --pull always.
After bringing the stack up: create an org/project/user, ingest a trace with any Langfuse SDK, and confirm it appears in the trace list, an observation/score lands, a dashboard renders, and deletion works. Then restart the stack (docker compose restart) and confirm the data survives (GreptimeDB persists to its named volume).
A full clean-checkout Compose smoke test and an image-based smoke test against the published tma1ai/openfuse-* images are part of the release checklist; see docs/greptimedb-migration/06-pre-release-report.md.