Self-Host Langfuse for LLM Observability
Langfuse is an open-source LLM engineering platform: it captures traces of your LLM calls (prompts, completions, latency, token usage, cost) and layers evaluation, prompt management, and analytics on top. This guide deploys Langfuse v3 on Railway and sends it a first trace. Self-hosting it keeps all trace data, including prompts that may contain user data, inside infrastructure you control.
Langfuse v3 is a multi-service application: a web server, a background worker, Postgres for transactional data, ClickHouse for trace analytics, Redis for queues and caching, and S3-compatible object storage for raw event uploads. Railway runs each component as a separate service with shared private networking, so the whole stack fits in one project.
Langfuse only observes LLM calls; it does not run models, so it is a standard CPU workload. Railway has no GPU instances, and this stack does not need one.
What we will set up
- Langfuse web and worker as separate Railway services
- PostgreSQL, ClickHouse, and Redis databases
- A Railway storage bucket for event uploads
- A public domain for the Langfuse UI
One-click deploy from a template
Railway has a Langfuse v3 template that provisions all six services (web, worker, Postgres, ClickHouse, Redis, and MinIO for object storage) in one step.
After deploying, open the web service's domain, create your admin account, and skip to Send traces from your application.
If you prefer to set things up manually, or want to use Railway's native storage buckets instead of MinIO, continue below.
Manual deployment
1. Create the project and databases
- Create a new project on Railway.
- Add PostgreSQL: click + New > Database > PostgreSQL.
- Add Redis: click + New > Database > Redis.
2. Deploy ClickHouse
Langfuse stores traces and observations in ClickHouse.
- Click + New > Docker Image and enter
clickhouse/clickhouse-server:24. - Set these variables on the service:
| Variable | Value |
|---|---|
CLICKHOUSE_DB | default |
CLICKHOUSE_USER | clickhouse |
CLICKHOUSE_PASSWORD | A random string (use openssl rand -hex 32) |
- Attach a volume mounted at
/var/lib/clickhouse. Without a volume, all trace data is lost on every redeploy.
ClickHouse needs no public domain. Langfuse reaches it over private networking on port 8123 (HTTP) and 9000 (native TCP, used for migrations).
3. Create a storage bucket
Langfuse writes every incoming trace event to object storage before the worker ingests it into ClickHouse. Railway storage buckets are private and S3-compatible, which is what Langfuse expects.
- Click + New > Bucket and pick a region.
- Note the bucket's Credentials tab. It exposes
BUCKET,ACCESS_KEY_ID,SECRET_ACCESS_KEY,REGION, andENDPOINTas referenceable variables, and tells you whether the bucket uses virtual-hosted or path-style URLs.
The region is locked after creation, so create the bucket in the same region as your services.
4. Generate secrets
Langfuse requires three secrets. Generate them locally:
openssl rand -hex 32 # NEXTAUTH_SECRET
openssl rand -hex 32 # SALT
openssl rand -hex 32 # ENCRYPTION_KEY (must be exactly 64 hex characters)5. Deploy the Langfuse web service
- Click + New > Docker Image and enter
langfuse/langfuse:3. - Add the following variables, using reference variables for the databases and bucket. The examples assume services named
Postgres,Redis,ClickHouse, andBucket; adjust to your service names.
| Variable | Value |
|---|---|
DATABASE_URL | ${{Postgres.DATABASE_URL}} |
CLICKHOUSE_URL | http://clickhouse.railway.internal:8123 |
CLICKHOUSE_MIGRATION_URL | clickhouse://clickhouse.railway.internal:9000 |
CLICKHOUSE_USER | ${{ClickHouse.CLICKHOUSE_USER}} |
CLICKHOUSE_PASSWORD | ${{ClickHouse.CLICKHOUSE_PASSWORD}} |
CLICKHOUSE_CLUSTER_ENABLED | false |
REDIS_CONNECTION_STRING | ${{Redis.REDIS_URL}} |
LANGFUSE_S3_EVENT_UPLOAD_BUCKET | ${{Bucket.BUCKET}} |
LANGFUSE_S3_EVENT_UPLOAD_REGION | ${{Bucket.REGION}} |
LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT | ${{Bucket.ENDPOINT}} |
LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID | ${{Bucket.ACCESS_KEY_ID}} |
LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY | ${{Bucket.SECRET_ACCESS_KEY}} |
LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE | false (or true if your bucket's Credentials tab says path-style) |
NEXTAUTH_URL | The public URL of this service, e.g. https://langfuse-production-xxxx.up.railway.app |
NEXTAUTH_SECRET | The first secret from step 4 |
SALT | The second secret from step 4 |
ENCRYPTION_KEY | The third secret from step 4 |
The private hostnames (clickhouse.railway.internal) come from the service names in your project. Private networking is scoped per environment, so the web service, worker, and databases must live in the same environment.
- Langfuse listens on port 3000. Generate a public domain for the service in Settings > Networking and set
NEXTAUTH_URLto this domain once it exists.
The web service runs Postgres and ClickHouse migrations on startup, so the first deploy takes a few minutes.
6. Deploy the Langfuse worker
The worker ingests events from the bucket into ClickHouse and runs evaluations.
- Click + New > Docker Image and enter
langfuse/langfuse-worker:3. - Copy the same variables from the web service.
NEXTAUTH_URLis not needed on the worker, butSALTandENCRYPTION_KEYmust match the web service exactly. - Do not generate a public domain. The worker listens on port 3030 for internal health checks only and talks to everything else over private networking.
7. Create your account
Open the web service's public domain, sign up (the first user becomes the instance admin), create an organization and a project, and copy the project's public and secret API keys from Settings > API Keys.
Send traces from your application
Install the Langfuse SDK in the application you want to observe:
npm install langfusePoint it at your self-hosted instance and record a trace:
import { Langfuse } from "langfuse";
const langfuse = new Langfuse({
baseUrl: process.env.LANGFUSE_HOST, // https://your-langfuse.up.railway.app
publicKey: process.env.LANGFUSE_PUBLIC_KEY,
secretKey: process.env.LANGFUSE_SECRET_KEY,
});
async function main(): Promise<void> {
const trace = langfuse.trace({
name: "support-answer",
userId: "user_123",
input: { question: "How do I reset my password?" },
});
const generation = trace.generation({
name: "draft-answer",
model: "claude-sonnet-4-5",
input: [{ role: "user", content: "How do I reset my password?" }],
});
// ... call your LLM provider here ...
const answer = "Go to Settings > Security and click Reset Password.";
generation.end({
output: answer,
usage: { input: 12, output: 14 },
});
trace.update({ output: { answer } });
await langfuse.flushAsync();
}
main().catch((err) => {
console.error(err);
process.exit(1);
});If the application also runs on Railway in the same project and environment, set LANGFUSE_HOST to the web service's private address (http://langfuse.railway.internal:3000, adjusting for your service name). Trace ingestion then never leaves the private network and incurs no egress charges.
Langfuse also ships framework integrations (OpenAI SDK wrapper, LangChain callbacks, OpenTelemetry) that use the same three environment variables.
Operational notes
- Memory: ClickHouse and the Langfuse web container use the most memory. Railway bills RAM by usage at $10 per GB per month; watch the metrics tab and set limits with headroom rather than guessing. The bucket is cheaper by comparison, holding raw events at $0.015 per GB per month with free egress.
- Backups: enable scheduled backups on the ClickHouse volume, and rely on the Postgres database's backups for transactional data.
- Upgrades: the
langfuse/langfuse:3andlangfuse/langfuse-worker:3tags track the v3 line. Redeploy both services together; the web service applies migrations on startup. - Single node: run one ClickHouse replica with
CLICKHOUSE_CLUSTER_ENABLED=false. A single node handles large trace volumes before clustering is worth the complexity.
Next steps
- Route LLM traffic through a gateway and trace it in one place
- Run ClickHouse for product analytics for volume sizing and query patterns that apply to trace data too
- Private networking for how internal DNS and environment scoping work
- Storage buckets for presigned URLs, credentials, and S3 client configuration