> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sevalla.com/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> These are the docs for Sevalla, a platform for deploying applications, databases, static sites, object storage and load balancers. The dashboard is at https://app.sevalla.com.
> Bold text in steps is an exact dashboard label.
> To act on a Sevalla account, prefer the Sevalla MCP server (https://mcp.sevalla.com/mcp, see https://docs.sevalla.com/ai-agents/overview.md), then the Sevalla CLI (https://docs.sevalla.com/developer-tools/cli.md), then the REST API (https://api-docs.sevalla.com, OpenAPI spec at https://api.sevalla.com/v3/openapi.json).
> Every page index is at https://docs.sevalla.com/llms.txt.

# Deploy Langfuse

> Self-host Langfuse 4 on Sevalla from its official images, with PostgreSQL, Redis, a private ClickHouse application on a disk, and object storage for events, media and exports.

Langfuse is an open-source observability platform for LLM applications: your code sends a trace of each request, with its model calls, tokens and cost, and Langfuse stores it and shows it in a web interface. Self-hosted Langfuse 4 runs on Sevalla as two applications from the official Docker images, `langfuse/langfuse` (the web interface and API) and `langfuse/langfuse-worker` (which processes incoming traces), with a PostgreSQL database, a Redis database, a bucket in object storage, and ClickHouse as a third, private application with a disk.

## At a glance

| | |
| - | - |
| Source | The `langfuse/langfuse:4.72.0`, `langfuse/langfuse-worker:4.72.0` and `clickhouse/clickhouse-server:26.8.36.3` Docker images |
| Build | None: applications from a Docker image skip the build |
| Start command | Each image's own. The web image applies the PostgreSQL and ClickHouse migrations, then starts the server |
| Port | Web: the `PORT` environment variable, `8080`. Worker: `8080`, HTTP traffic off. ClickHouse: `8123` with HTTP traffic off, and private port `9000` |
| Processes | One web process in each of the three applications |
| Services | PostgreSQL 17, Redis 7.2 and the ClickHouse application over internal connections; one object storage bucket |
| Disk | 10 GB at `/var/lib/clickhouse`, on ClickHouse |
| Health check | `GET /api/public/ready` returns `200` on the web application (readiness probe) |
| Size | **S2** for each application; **DB1** for both databases in the tests |

### Environment variables

The `langfuse` and `langfuse-worker` applications get the same variables. Applications from a Docker image have no build, so every variable is available during runtime only.

| Variable | Required | Available during | Secret | Value |
| - | - | - | - | - |
| `DATABASE_URL` | Yes | Runtime | Yes | `DB_URL` from the `langfuse-db` internal connection, renamed. Langfuse accepts `postgres://` and `postgresql://` URLs |
| `REDIS_HOST`, `REDIS_PORT` | Yes | Runtime | No | From the `langfuse-redis` internal connection |
| `REDIS_AUTH` | Yes | Runtime | Yes | `REDIS_PASSWORD` from the `langfuse-redis` internal connection, renamed |
| `CLICKHOUSE_HOST` | Yes | Runtime | No | `APP_HOST` from the `langfuse-clickhouse` internal connection, renamed |
| `NEXTAUTH_URL` | Yes | Runtime | No | The web application's address, for example `https://langfuse-x7k2m.sevalla.app` |
| `NEXTAUTH_SECRET` | Yes | Runtime | Yes | Generate with `openssl rand -base64 32` |
| `SALT` | Yes | Runtime | Yes | Generate with `openssl rand -base64 32` |
| `ENCRYPTION_KEY` | Yes | Runtime | Yes | Generate with `openssl rand -hex 32` |
| `CLICKHOUSE_URL` | Yes | Runtime | No | `http://${CLICKHOUSE_HOST}:8123` |
| `CLICKHOUSE_MIGRATION_URL` | Yes | Runtime | No | `clickhouse://${CLICKHOUSE_HOST}:9000` |
| `CLICKHOUSE_USER` | Yes | Runtime | No | `langfuse` |
| `CLICKHOUSE_PASSWORD` | Yes | Runtime | Yes | The ClickHouse application's password |
| `CLICKHOUSE_CLUSTER_ENABLED` | Yes | Runtime | No | `false` |
| `S3_ENDPOINT`, `S3_BUCKET` | Yes | Runtime | No | The bucket's **Endpoint** and **Bucket name** |
| `S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY` | Yes | Runtime | Yes | The bucket's **Access key** and **Secret key** |
| `LANGFUSE_S3_EVENT_UPLOAD_BUCKET`, `_PREFIX`, `_REGION`, `_ENDPOINT`, `_ACCESS_KEY_ID`, `_SECRET_ACCESS_KEY` | Yes | Runtime | Yes, the keys | Where raw events go: `${S3_BUCKET}`, `events/`, `auto`, `${S3_ENDPOINT}` and the two keys |
| `LANGFUSE_S3_MEDIA_UPLOAD_` with the same six suffixes | No | Runtime | Yes, the keys | Media attachments, under `media/` |
| `LANGFUSE_S3_BATCH_EXPORT_ENABLED` and `LANGFUSE_S3_BATCH_EXPORT_` with the same six suffixes | No | Runtime | Yes, the keys | `true`, and batch exports under `exports/` |
| `AUTH_DISABLE_SIGNUP` | No | Runtime | No | `true` once your team has accounts, on the web application |
| `TELEMETRY_ENABLED` | No | Runtime | No | `false` stops anonymous usage statistics |
| `LANGFUSE_INIT_ORG_ID`, `_ORG_NAME`, `_PROJECT_ID`, `_PROJECT_NAME`, `_PROJECT_PUBLIC_KEY`, `_PROJECT_SECRET_KEY`, `_USER_EMAIL`, `_USER_NAME`, `_USER_PASSWORD` | No | Runtime | Yes, the keys and the password | Create the first user, organization, project and API keys when the web application starts. See [Deploy with the API](#deploy-with-the-api) |
| `PORT` | No | Runtime | No | Set by Sevalla to the web process port, `8080` |

The `langfuse-clickhouse` application has two variables of its own:

| Variable | Required | Available during | Secret | Value |
| - | - | - | - | - |
| `CLICKHOUSE_USER` | Yes | Runtime | No | `langfuse` |
| `CLICKHOUSE_PASSWORD` | Yes | Runtime | Yes | Generate with `openssl rand -hex 24` |

## How Langfuse runs on Sevalla

| Component | Runs on Sevalla as | What Langfuse uses it for | Tested size |
| - | - | - | - |
| Langfuse web | Application from `langfuse/langfuse:4.72.0`, public | The web interface and the API that SDKs send traces to. It runs the database migrations when it starts. | **S2** (1 vCPU, 2 GB) |
| Langfuse worker | Application from `langfuse/langfuse-worker:4.72.0`, HTTP traffic off | Reads queued traces and writes them to ClickHouse; runs exports and evaluations | **S2** |
| ClickHouse | Application from `clickhouse/clickhouse-server:26.8.36.3`, HTTP traffic off, with a disk | Traces, observations and scores | **S2** and a 10 GB disk |
| PostgreSQL 17 | Database | Users, organizations, projects, API keys, prompts and settings | **DB1** |
| Redis 7.2 | Database | The queue between web and worker, and a cache | **DB1** |
| Object storage | Bucket | Every incoming event, media attachments and batch exports | |

A trace travels this way: your application sends it over HTTPS to the web application, which writes the raw event to the bucket and queues a reference to it in Redis. The worker picks it up, reads the event from the bucket and writes it to ClickHouse, where the web interface reads it. Everything except the web application's domain stays on the private network, over [internal connections](https://docs.sevalla.com/applications/networking.md#internal-connections).

The web and worker applications keep no state of their own, so each can run more than one instance (two web instances were tested). ClickHouse can't: a process with a [disk](https://docs.sevalla.com/applications/disks.md) runs one instance, and Sevalla stops it before starting the new one on each of its deployments. In the tests, that took from 20 seconds to 2 minutes, and the worker dropped the traces it couldn't write in that time. [Update ClickHouse](#update-clickhouse) shows how to deploy it without losing traces.

## Before you begin

* A Sevalla company with a payment method: sizes above **H1** and **DB1** need one.
* Python 3.11 or later on your computer, to send a test trace.
* `openssl`, to generate secrets. macOS and most Linux distributions include it.

Create every service in this guide in the same **Data center**: internal connections work only within one location.

## Create the databases and the bucket

**1. Create the PostgreSQL database**

In the sidebar, click **Databases**, then click **Create database**. Under **Database**, click **PostgreSQL** and choose version **17**. Keep the generated credentials, enter `langfuse-db` as the **Display name**, choose the **Data center** and a **Size**, then click **Create database**.

**2. Create the Redis database**

Click **Create database** again. Click **Redis**, choose version **7.2**, enter `langfuse-redis` as the **Display name**, choose the same **Data center** and a **Size**, then click **Create database**.

**3. Create the bucket**

In the sidebar, click **Object storage**, then click **Create object storage**. Enter `langfuse-blobs` as the **Name**, pick a **Location** and click **Create object storage**. On the bucket's **Settings** tab, note the **Endpoint**, the **Bucket name** (for example `langfuse-blobs-q8w2e`), the **Access key** and the **Secret key**. See [Connection values](https://docs.sevalla.com/object-storage/connect.md#connection-values).

Langfuse 4 needs PostgreSQL 15 or later and Redis 7 or later with the `noeviction` memory policy. The Redis 7.2 database in the tests reported `maxmemory-policy` `noeviction` and a memory limit of 200 MB on **DB1**: when it's full, Redis rejects new writes instead of dropping queued jobs. The guide was tested with **DB1** for both databases; for production, Langfuse recommends 2 CPU and 4 GB of memory for PostgreSQL and 1 CPU and 1.5 GB for Redis, which **DB4** and **DB3** cover. See [Database sizes](https://docs.sevalla.com/databases/overview.md#sizes).

One bucket holds everything, in three folders: `events/`, `media/` and `exports/`.

## Run ClickHouse as a private application

ClickHouse isn't a Sevalla managed database, so it runs as an application from the official image, with a disk for its data and no public traffic. Generate its password first. A hexadecimal password keeps characters that would break Langfuse's ClickHouse connection URL out of it:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
openssl rand -hex 24
```

**1. Create the application**

In the sidebar, click **Applications**, then click **Create** > **Application**. Under **Source**, click **Docker image** and enter `clickhouse/clickhouse-server:26.8.36.3`. Keep **None (public image)** as the **Registry credential**. Enter `langfuse-clickhouse` as the **Name**, choose the **Data center** and the **S2** size, then click **Create application**.

**2. Add the variables**

Click the **Environment variables** tab, then **Add environment variables**. Add `CLICKHOUSE_USER` with the value `langfuse`, and `CLICKHOUSE_PASSWORD` with the password you generated, then click **Add environment variables**.

**3. Set the port and turn HTTP traffic off**

Click the **Networking** tab. In **Expose HTTP traffic**, click **Update port**, enter `8123` and click **Update port**. Then click **Disable**, and **Disable HTTP** in the dialog.

**4. Expose the native port**

In **Private ports**, click **Expose private port**. Keep the web process as the **Process**, enter `9000` as the **Port**, and click **Expose port**.

**5. Add the disk**

Click the **Disks** tab, then **Create disk**. Keep the web process, enter `/var/lib/clickhouse` as the **Path**, choose **10 GB** and click **Create disk**.

**6. Deploy**

Click the **Overview** tab, then click **Deploy now** under **Recent deployments**, and **Deploy**.

| Setting | Why |
| - | - |
| `clickhouse/clickhouse-server:26.8.36.3` | Langfuse 4 needs ClickHouse 25.12 or later. 26.8 is a long-term support release, and the full version keeps a redeployment from pulling a newer one. |
| `CLICKHOUSE_USER` and `CLICKHOUSE_PASSWORD` | The image creates this user, with every permission Langfuse needs, instead of the default user without a password. |
| Port `8123` | ClickHouse's HTTP interface, which Langfuse reads and writes through. Other services reach the web process port on its internal hostname. |
| HTTP traffic off | The application's domains stop answering, so nobody on the internet reaches the database. It stays reachable on the private network. |
| Private port `9000` | ClickHouse's native protocol, which Langfuse's migrations use |
| Disk at `/var/lib/clickhouse` | ClickHouse's data folder. Without a disk, every deployment starts with an empty database. |

To check it, open the application's **Web terminal** tab and run:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
clickhouse-client --user langfuse --password "$CLICKHOUSE_PASSWORD" -q "SELECT version(), timezone()"
```

It prints `26.8.36.3` and `UTC`. Langfuse needs ClickHouse to run in UTC, which is the image's default.

Without the dashboard, run the same query with [Run a command in a process](https://api-docs.sevalla.com/v3/applications/run-a-command-in-a-process) (`execProcessCommandV3`), with the IDs from [Deploy with the API](#deploy-with-the-api):

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s -X POST https://api.sevalla.com/v3/applications/$CH_APP_ID/processes/$CH_PROCESS_ID/exec \
  -H "Authorization: Bearer $SEVALLA_API_KEY" -H "Content-Type: application/json" \
  -d '{"command": ["sh", "-c", "clickhouse-client --user langfuse --password \"$CLICKHOUSE_PASSWORD\" -q \"SELECT version(), timezone()\""]}' \
  | jq -r .stdout
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
26.8.36.3	UTC
```

On **S2**, ClickHouse used 520 MB of memory idle and at most 860 MB after storing 22,600 observations. For production, Langfuse recommends 2 CPU and 8 GB of memory, the **M3** size. See [Instance sizes](https://docs.sevalla.com/applications/scaling.md#instance-sizes).

![The Private networking section with the Internal hostnames, Connected services and Private ports lists](https://assets.sevalla.com/screenshots/light/app-networking-private.png)

## Deploy the Langfuse web application

### Generate the secrets

Langfuse needs three secrets. Generate each one and keep them: both Langfuse applications use the same values.

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
openssl rand -base64 32   # NEXTAUTH_SECRET
openssl rand -base64 32   # SALT
openssl rand -hex 32      # ENCRYPTION_KEY
```

| Secret | What it protects | If you change it |
| - | - | - |
| `NEXTAUTH_SECRET` | Signs the sessions of signed-in users | Everyone has to sign in again |
| `SALT` | Hashes the API keys stored in PostgreSQL | Every existing API key stops working |
| `ENCRYPTION_KEY` | Encrypts stored credentials, such as model provider keys | Langfuse can't read the stored credentials |

### Create and connect the application

**1. Create the application**

In the sidebar, click **Applications**, then click **Create** > **Application**. Under **Source**, click **Docker image** and enter `langfuse/langfuse:4.72.0`. Enter `langfuse` as the **Name**, choose the same **Data center** and the **S2** size, then click **Create application**. Note the domain under the application's name, for example `langfuse-x7k2m.sevalla.app`.

**2. Connect PostgreSQL**

Click the **Networking** tab. Under **Connected services**, click **Add internal connection**. Choose `langfuse-db` as the **Service** and turn on **Add environment variables to the application**. Rename `DB_URL` to `DATABASE_URL`, then click **Add internal connection**.

**3. Connect Redis**

Click **Add internal connection** again. Choose `langfuse-redis`, turn on **Add environment variables to the application**, rename `REDIS_PASSWORD` to `REDIS_AUTH`, and click **Add internal connection**.

**4. Connect ClickHouse**

Click **Add internal connection** again. Choose `langfuse-clickhouse`, turn on **Add environment variables to the application**, rename `APP_HOST` to `CLICKHOUSE_HOST`, and click **Add internal connection**.

**5. Add the Langfuse variables**

Click the **Environment variables** tab, then **Add environment variables**. Paste the block below into the first key field, replace the placeholders, and click **Add environment variables**.

**6. Add a readiness probe**

Click the **Processes** tab, click **⋯** on the web process, then **Update process**. Under **Health checks**, turn on **Readiness probe**, set the **Path** to `/api/public/ready`, keep the **Port** at `8080`, and click **Update process**.

**7. Deploy**

Click the **Overview** tab, then click **Deploy now** under **Recent deployments**, and **Deploy**.

```bash .env theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
NEXTAUTH_URL=https://langfuse-x7k2m.sevalla.app
NEXTAUTH_SECRET=<nextauth-secret>
SALT=<salt>
ENCRYPTION_KEY=<encryption-key>

CLICKHOUSE_URL=http://${CLICKHOUSE_HOST}:8123
CLICKHOUSE_MIGRATION_URL=clickhouse://${CLICKHOUSE_HOST}:9000
CLICKHOUSE_USER=langfuse
CLICKHOUSE_PASSWORD=<clickhouse-password>
CLICKHOUSE_CLUSTER_ENABLED=false

S3_ENDPOINT=https://<account-id>.r2.cloudflarestorage.com
S3_BUCKET=langfuse-blobs-q8w2e
S3_ACCESS_KEY_ID=<access-key>
S3_SECRET_ACCESS_KEY=<secret-key>

LANGFUSE_S3_EVENT_UPLOAD_BUCKET=${S3_BUCKET}
LANGFUSE_S3_EVENT_UPLOAD_PREFIX=events/
LANGFUSE_S3_EVENT_UPLOAD_REGION=auto
LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT=${S3_ENDPOINT}
LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID=${S3_ACCESS_KEY_ID}
LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY=${S3_SECRET_ACCESS_KEY}

LANGFUSE_S3_MEDIA_UPLOAD_BUCKET=${S3_BUCKET}
LANGFUSE_S3_MEDIA_UPLOAD_PREFIX=media/
LANGFUSE_S3_MEDIA_UPLOAD_REGION=auto
LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT=${S3_ENDPOINT}
LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID=${S3_ACCESS_KEY_ID}
LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY=${S3_SECRET_ACCESS_KEY}

LANGFUSE_S3_BATCH_EXPORT_ENABLED=true
LANGFUSE_S3_BATCH_EXPORT_BUCKET=${S3_BUCKET}
LANGFUSE_S3_BATCH_EXPORT_PREFIX=exports/
LANGFUSE_S3_BATCH_EXPORT_REGION=auto
LANGFUSE_S3_BATCH_EXPORT_ENDPOINT=${S3_ENDPOINT}
LANGFUSE_S3_BATCH_EXPORT_ACCESS_KEY_ID=${S3_ACCESS_KEY_ID}
LANGFUSE_S3_BATCH_EXPORT_SECRET_ACCESS_KEY=${S3_SECRET_ACCESS_KEY}
```

| Variables | What to enter |
| - | - |
| `NEXTAUTH_URL` | The application's address, with `https://` and without a trailing slash. Langfuse builds its sign-in redirects and links from it. |
| `NEXTAUTH_SECRET`, `SALT`, `ENCRYPTION_KEY` | The three secrets you generated |
| `CLICKHOUSE_URL`, `CLICKHOUSE_MIGRATION_URL` | Keep them as they are. `${CLICKHOUSE_HOST}` is a [reference](https://docs.sevalla.com/applications/environment-variables.md#reference-another-variable) to the variable the internal connection added: ClickHouse's internal hostname. |
| `CLICKHOUSE_PASSWORD` | The password you gave the ClickHouse application |
| `CLICKHOUSE_CLUSTER_ENABLED` | `false`: ClickHouse runs as a single server, not a cluster |
| `S3_ENDPOINT`, `S3_BUCKET`, `S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY` | The bucket's **Endpoint**, **Bucket name**, **Access key** and **Secret key** |
| `LANGFUSE_S3_*` | Keep them as they are. Events, media attachments and batch exports share the bucket, each in its own folder, with the region `auto` that object storage uses. |

Langfuse also sends anonymous usage statistics to its developers. To turn that off, add `TELEMETRY_ENABLED=false`.

When the application starts, it applies the PostgreSQL and ClickHouse migrations; on a new installation that took 15 seconds. The readiness probe keeps the instance out of traffic until Langfuse is ready, so the deployment reaches **Deployed** only then. Open the domain: Langfuse shows its sign-in page. The health endpoint answers without signing in:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s https://langfuse-x7k2m.sevalla.app/api/public/health
```

```json theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
{"status":"OK","version":"4.72.0"}
```

To follow a deployment from a script, read its `status` with [Get deployment](https://api-docs.sevalla.com/v3/applications/get-deployment) (`getDeploymentV3`) until it's `success` or `failed`, and its log with [Get application deployment logs](https://api-docs.sevalla.com/v3/applications/get-application-deployment-logs) (`getApplicationDeploymentLogsV3`). A Docker image deployment has no build, and its log shows `✅ The image is available.`, then `🚀 Web has been deployed successfully.` The logs operation returns the last hour unless you pass `from`.

## Deploy the Langfuse worker

The worker uses the same variables as the web application. It serves only a health check, so it gets no public traffic.

**1. Create the application**

Create another application from the **Docker image** `langfuse/langfuse-worker:4.72.0`, named `langfuse-worker`, in the same **Data center**, with the **S2** size.

**2. Connect the three services**

On its **Networking** tab, add an internal connection to `langfuse-db`, to `langfuse-redis` and to `langfuse-clickhouse`, each with **Add environment variables to the application** turned off.

**3. Copy the variables**

Open the `langfuse` application's **Environment variables** tab and click **Copy all**. On the worker's **Environment variables** tab, click **Add environment variables**, paste into the first key field, and click **Add environment variables**. The worker now has the same connection variables, secrets and bucket settings.

**4. Turn HTTP traffic off**

On the worker's **Networking** tab, in **Expose HTTP traffic**, click **Disable**, then **Disable HTTP**.

**5. Deploy**

Click the **Overview** tab, then click **Deploy now** under **Recent deployments**, and **Deploy**.

The worker's [runtime logs](https://docs.sevalla.com/applications/logs.md) show `Listening: http://...:8080` when it's ready. [Get application runtime logs](https://api-docs.sevalla.com/v3/applications/get-application-runtime-logs) (`getApplicationRuntimeLogsV3`) finds the line, with `WORKER_APP_ID` from [Deploy with the API](#deploy-with-the-api):

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s "https://api.sevalla.com/v3/applications/$WORKER_APP_ID/runtime-logs?search=Listening" \
  -H "Authorization: Bearer $SEVALLA_API_KEY" | jq -r '.[].message'
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
2026-10-30T05:06:11.526Z info 	Listening: http://langfuse-worker-x7k2m-web-7b47444748-72jrp:8080
```

## Create an account and API keys

**1. Sign up**

Open the sign-up page, for example `https://langfuse-x7k2m.sevalla.app/auth/sign-up`. Enter a **Name**, an **Email** and a **Password** with letters, numbers and special characters, then click **Sign up**. Langfuse offers a newsletter; click **Skip**.

**2. Create an organization and a project**

Enter an **Organization name**, for example `Acme`, and click **Create**. Then enter a **Project name**, for example `storefront`, and click **Create**.

**3. Create API keys**

In the project, click **Settings**, then **API Keys** and **Create new API keys**. Enter a **Name** and click **Create API key**.

**4. Copy the keys**

The dialog shows the **Secret Key**, the **Public Key** and a `.env` snippet with `LANGFUSE_SECRET_KEY`, `LANGFUSE_PUBLIC_KEY` and `LANGFUSE_BASE_URL`. Copy them now: Langfuse shows the secret key only once.

Anyone who can open the domain can create an account until you turn sign-up off. Once your team has accounts, add `AUTH_DISABLE_SIGNUP=true` to the web application and deploy it: the sign-up form then answers `Sign up is disabled.` This also blocks invited people who don't have an account yet, so create their accounts first.

## Send a test trace

This script records a trace with a span and a generation, without calling a model, so it needs only the Langfuse keys. In an empty folder:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
python3 -m venv .venv
source .venv/bin/activate
pip install langfuse
```

This installs the Langfuse Python SDK 5, which needs a Langfuse 4 server. Create the script:

```python trace.py theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
from langfuse import get_client

langfuse = get_client()

with langfuse.start_as_current_observation(
    as_type="span",
    name="answer-support-question",
    input={"question": "Where is order 1042?"},
) as span:
    with langfuse.start_as_current_observation(
        as_type="generation",
        name="draft-reply",
        model="claude-haiku-5-5",
        input=[{"role": "user", "content": "Where is order 1042?"}],
    ) as generation:
        generation.update(
            output="Order 1042 shipped today and arrives on Friday.",
            usage_details={"input": 14, "output": 11},
        )
    span.update(output="Order 1042 shipped today and arrives on Friday.")
    trace_id = langfuse.get_current_trace_id()

langfuse.flush()
print(langfuse.get_trace_url(trace_id=trace_id))
```

Set the three values from the API keys dialog and run it:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
export LANGFUSE_PUBLIC_KEY=pk-lf-...
export LANGFUSE_SECRET_KEY=sk-lf-...
export LANGFUSE_BASE_URL=https://langfuse-x7k2m.sevalla.app
python trace.py
```

The script prints the trace's address, for example `https://langfuse-x7k2m.sevalla.app/project/.../traces/0f90cdc4ea1efdeee7d37a48506ae617`. Open it: the trace shows the span `answer-support-question` with the generation `draft-reply` under it, 25 tokens, and the cost Langfuse calculates from its price list for `claude-haiku-5-5`. **Tracing** in the project's sidebar lists every observation. The trace also confirms the whole path: the event file is in the bucket under `events/`, and the worker wrote it to ClickHouse. To check the same from a script, read the trace's observations with Langfuse's [Observations API](https://api.reference.langfuse.com), with the trace ID from the address:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s -u "$LANGFUSE_PUBLIC_KEY:$LANGFUSE_SECRET_KEY" \
  "https://langfuse-x7k2m.sevalla.app/api/public/v2/observations?traceId=0f90cdc4ea1efdeee7d37a48506ae617" \
  | jq -r '.data[] | "\(.type) \(.name)"' | sort
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
GENERATION draft-reply
SPAN answer-support-question
```

## Trace your model calls

Langfuse's integrations record model calls without manual spans. For the OpenAI SDK, import the client from `langfuse.openai` instead of `openai`:

```python openai_trace.py theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
from langfuse import get_client
from langfuse.openai import OpenAI

client = OpenAI()

response = client.responses.create(
    model="gpt-5.4-nano",
    input="Write a one-line status update for order 1042, which shipped today.",
)
print(response.output_text)

get_client().flush()
```

Run it with `pip install openai` done and `OPENAI_API_KEY` set next to the three Langfuse variables. The trace shows an `OpenAI-generation` with the input and output tokens (22 and 12 in the test) and their cost. Change `model` to the model your application uses. For other frameworks, see [Langfuse integrations](https://langfuse.com/integrations).

## Update Langfuse

Langfuse releases new versions often. Updates within major version 4 apply their own migrations when the web application starts; for a new major version, follow Langfuse's [upgrade guides](https://langfuse.com/self-hosting/upgrade). Keep the web and worker applications on the same version.

**1. Check the backups**

Take a [manual backup](https://docs.sevalla.com/databases/backups.md#create-a-manual-backup) of `langfuse-db` on its **Backups** tab. Newer versions change the database schema when they start, and the backup lets you go back.

**2. Update the web application**

On the `langfuse` application's **Settings** tab, in the **Source** section, click **Update source**. Change the **Docker image** to the new version, for example `langfuse/langfuse:4.73.0`, click **Update source**, and deploy.

**3. Update the worker**

Change the worker's image to the same version, for example `langfuse/langfuse-worker:4.73.0`, and deploy it.

With the readiness probe, the web application kept answering throughout its deployment in the tests: 154 requests during a redeployment all returned `200`.

## Update ClickHouse

A deployment of the ClickHouse application stops it, and traces the worker can't write while it's down are lost after 3 attempts (the worker logs `ClickhouseWriter: Max attempts reached, dropped 1 events_full record(s)`). Stop the worker first, so new traces wait in the Redis queue instead:

**1. Stop the worker**

On the worker's **Processes** tab, click **⋯** on the web process, then **Update process**. Set **Instance count** to `0`, click **Update process**, and deploy the worker.

**2. Deploy ClickHouse**

Change ClickHouse's image in **Update source** if you're upgrading it, then deploy the `langfuse-clickhouse` application, and wait for **Deployed**.

**3. Start the worker**

Set the worker's **Instance count** back to `1` and deploy it.

In the test, all 20 traces sent while ClickHouse restarted this way appeared in Langfuse once the worker was back. Deploy ClickHouse this way for any change: an environment variable, a size or a new version. Check the ClickHouse versions Langfuse supports before you upgrade it.

## Back up Langfuse

| Data | Where it is | Backup |
| - | - | - |
| Users, projects, API keys, prompts | PostgreSQL | Daily [database backups](https://docs.sevalla.com/databases/backups.md) |
| Traces, observations, scores | ClickHouse, on the disk | Daily [disk backups](https://docs.sevalla.com/applications/disks.md#backups), kept for 7 days. To restore one, contact support. |
| Raw events, media, exports | The bucket | Stored in object storage. Langfuse keeps raw events so it can process them again. |

The bucket grows with every trace. To limit it, delete old objects under `events/` on the bucket's **Objects** tab; Langfuse's [blob storage guide](https://langfuse.com/self-hosting/deployment/infrastructure/blobstorage#bucket-lifecycle-policies) explains how long to keep them.

## Size and scale Langfuse

| Process | Measured | Tested size | Langfuse's production minimum |
| - | - | - | - |
| Web | 870 MB after start, at most 1.2 GB | **S2** | 2 CPU, 4 GB (**S3**), and at least 2 instances |
| Worker | 680 MB idle, at most 850 MB after 10,000 traces | **S2** (on **S1**, it ran at 85% of the memory limit) | 2 CPU, 4 GB (**S3**) |
| ClickHouse | 520 MB idle, at most 860 MB | **S2** with a 10 GB disk | 2 CPU, 8 GB (**M3**) |

To run more web or worker instances, set the **Instance count** of their web process; Langfuse keeps sessions and queues in PostgreSQL and Redis, so any instance can answer any request. ClickHouse scales up only: give it a larger size, and [grow its disk](https://docs.sevalla.com/applications/disks.md#update-a-disk) before it fills. The disk's **Usage** shows on the **Disks** tab.

## Add a custom domain

To serve Langfuse from your own domain, such as `langfuse.example.com`, [add the domain](https://docs.sevalla.com/domains/overview.md) on the web application's **Domains** tab. Then set `NEXTAUTH_URL` to `https://langfuse.example.com` on both Langfuse applications and deploy them: Langfuse builds its sign-in redirects and links from `NEXTAUTH_URL`. Custom domains need a web process larger than **H1**.

## Troubleshoot

| Symptom | Cause | Fix |
| - | - | - |
| Sign-up shows `Please choose a secure password by combining letters, numbers, and special characters.` | The password has only letters and digits | Add a special character |
| The domain returns `503` right after the first deployment | The web application is still applying migrations, and the web process has no readiness probe | Wait about 30 seconds, and [add the readiness probe](#create-and-connect-the-application) |
| Traces sent while ClickHouse was deploying are missing; the worker's logs show `ClickhouseWriter: Max attempts reached` | The worker couldn't reach ClickHouse | Stop the worker before you deploy ClickHouse. See [Update ClickHouse](#update-clickhouse). |
| The Python SDK prints `Queue full, dropping Span.` | A script creates spans faster than the SDK sends them | Call `langfuse.flush()` every few hundred traces in batch scripts |
| [Create database](https://api-docs.sevalla.com/v3/databases/create-database) answers `Request doesn't match the schema` | A field breaks the operation's rules, for example a `db_name` shorter than 2 characters | Check each field against the operation's reference. The Redis database in [Deploy with the API](#deploy-with-the-api) uses `db_name: "langfuse"` |

## Deploy with the API

This sequence deploys the same Langfuse installation with the [Sevalla API](https://docs.sevalla.com/developer-tools/api.md), for an agent or a script. It needs:

* An [API key](https://docs.sevalla.com/developer-tools/api-keys.md) that can create applications, databases and object storage, in `SEVALLA_API_KEY`.
* `curl`, `jq` and `openssl`, in a bash shell that keeps variables between commands: each step uses the IDs the earlier ones saved.

Each step saves the IDs the next ones need in shell variables. If a variable is `null`, its call failed: run the call again without the `| jq` part to read the error.

The sequence also sets the `LANGFUSE_INIT_*` variables, Langfuse's [headless initialization](https://langfuse.com/self-hosting/administration/headless-initialization): the web application creates the first user, the `Acme` organization, the `storefront` project and its API keys when it starts, so no step needs the Langfuse sign-up page. It sets the same variables as the dashboard steps, with values in place of the `${...}` references. The CLI line of each step names the [Sevalla CLI](https://docs.sevalla.com/developer-tools/cli.md) command, and the MCP line the [Sevalla MCP server](https://docs.sevalla.com/ai-agents/overview.md) tool that does the same.

**1. Pick the location**

[List clusters](https://api-docs.sevalla.com/v3/resources/list-clusters) (`listClustersV3`) lists the locations; pick one by name:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s https://api.sevalla.com/v3/resources/clusters \
  -H "Authorization: Bearer $SEVALLA_API_KEY" | jq -r '.[] | "\(.id)  \(.display_name)"'
CLUSTER_ID=$(curl -s https://api.sevalla.com/v3/resources/clusters \
  -H "Authorization: Bearer $SEVALLA_API_KEY" \
  | jq -r '.[] | select(.location == "eu-frankfurt-1") | .id')
```

CLI: `sevalla resources clusters`. MCP: `get_platform_options`.

**2. Look up the sizes**

[List process resource types](https://api-docs.sevalla.com/v3/resources/list-process-resource-types) (`listProcessResourceTypesV3`), [List database resource types](https://api-docs.sevalla.com/v3/resources/list-database-resource-types) (`listDatabaseResourceTypesV3`) and [List disk resource types](https://api-docs.sevalla.com/v3/resources/list-disk-resource-types) (`listDiskResourceTypesV3`) return the size IDs:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
S2=$(curl -s https://api.sevalla.com/v3/resources/process-resource-types \
  -H "Authorization: Bearer $SEVALLA_API_KEY" | jq -r '.[] | select(.name == "s2") | .id')
DB1=$(curl -s https://api.sevalla.com/v3/resources/database-resource-types \
  -H "Authorization: Bearer $SEVALLA_API_KEY" | jq -r '.[] | select(.name == "db1") | .id')
DISK_10GB=$(curl -s https://api.sevalla.com/v3/resources/disk-resource-types \
  -H "Authorization: Bearer $SEVALLA_API_KEY" | jq -r '.[] | select(.size_gib == 10) | .id')
```

CLI: `sevalla resources process-types`, `sevalla resources db-types`, `sevalla resources disk-types`. MCP: `get_platform_options`.

**3. Generate the secrets**

The later steps put them in environment variables:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
PG_PASSWORD=$(openssl rand -hex 16)
REDIS_PASSWORD=$(openssl rand -hex 16)
CLICKHOUSE_PASSWORD=$(openssl rand -hex 24)
NEXTAUTH_SECRET=$(openssl rand -base64 32)
SALT=$(openssl rand -base64 32)
ENCRYPTION_KEY=$(openssl rand -hex 32)
LANGFUSE_PUBLIC_KEY=pk-lf-$(openssl rand -hex 16)
LANGFUSE_SECRET_KEY=sk-lf-$(openssl rand -hex 16)
ADMIN_PASSWORD=$(openssl rand -hex 12)-Lf1
```

**4. Create the PostgreSQL database**

[Create database](https://api-docs.sevalla.com/v3/databases/create-database) (`createDatabaseV3`):

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
PG_ID=$(curl -s -X POST https://api.sevalla.com/v3/databases \
  -H "Authorization: Bearer $SEVALLA_API_KEY" -H "Content-Type: application/json" \
  -d "$(jq -n --arg cluster "$CLUSTER_ID" --arg size "$DB1" --arg password "$PG_PASSWORD" \
    '{display_name: "langfuse-db", type: "postgresql", version: "17", cluster_id: $cluster,
      resource_type_id: $size, db_name: "langfuse", db_user: "langfuse", db_password: $password}')" \
  | jq -r .id)
```

CLI: `sevalla databases create --name langfuse-db --type postgresql --db-version 17 --cluster "$CLUSTER_ID" --resource-type "$DB1" --db-name langfuse --db-user langfuse --db-password "$PG_PASSWORD"`. MCP: `create_database`.

**5. Create the Redis database**

[Create database](https://api-docs.sevalla.com/v3/databases/create-database) (`createDatabaseV3`) again:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
REDIS_ID=$(curl -s -X POST https://api.sevalla.com/v3/databases \
  -H "Authorization: Bearer $SEVALLA_API_KEY" -H "Content-Type: application/json" \
  -d "$(jq -n --arg cluster "$CLUSTER_ID" --arg size "$DB1" --arg password "$REDIS_PASSWORD" \
    '{display_name: "langfuse-redis", type: "redis", version: "7.2", cluster_id: $cluster,
      resource_type_id: $size, db_name: "langfuse", db_password: $password}')" \
  | jq -r .id)
```

CLI: `sevalla databases create --name langfuse-redis --type redis --db-version 7.2 --cluster "$CLUSTER_ID" --resource-type "$DB1" --db-name langfuse --db-password "$REDIS_PASSWORD"`. MCP: `create_database`.

**6. Create the bucket**

[Create object storage bucket](https://api-docs.sevalla.com/v3/object-storages/create-object-storage-bucket) (`createObjectStorageV3`) returns the endpoint, the bucket name and the keys:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
BUCKET=$(curl -s -X POST https://api.sevalla.com/v3/object-storage \
  -H "Authorization: Bearer $SEVALLA_API_KEY" -H "Content-Type: application/json" \
  -d '{"display_name": "langfuse-blobs"}')
S3_ENDPOINT=$(echo "$BUCKET" | jq -r .endpoint)
S3_BUCKET=$(echo "$BUCKET" | jq -r .bucket_name)
S3_ACCESS_KEY_ID=$(echo "$BUCKET" | jq -r .access_key)
S3_SECRET_ACCESS_KEY=$(echo "$BUCKET" | jq -r .secret_key)
BUCKET_ID=$(echo "$BUCKET" | jq -r .id)
```

CLI: `sevalla object-storage create --name langfuse-blobs`. MCP: `sevalla_api_create` with `createObjectStorageV3`.

**7. Create the ClickHouse application**

[Create application](https://api-docs.sevalla.com/v3/applications/create-application) (`createApplicationV3`) with a Docker image source, then [List processes](https://api-docs.sevalla.com/v3/applications/list-processes) (`listProcessesV3`) for its web process and internal hostname:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
CH_APP_ID=$(curl -s -X POST https://api.sevalla.com/v3/applications \
  -H "Authorization: Bearer $SEVALLA_API_KEY" -H "Content-Type: application/json" \
  -d "$(jq -n --arg cluster "$CLUSTER_ID" \
    '{display_name: "langfuse-clickhouse", cluster_id: $cluster, source: "dockerImage",
      docker_image: "clickhouse/clickhouse-server:26.8.36.3"}')" \
  | jq -r .id)
CH_PROCESS=$(curl -s https://api.sevalla.com/v3/applications/$CH_APP_ID/processes \
  -H "Authorization: Bearer $SEVALLA_API_KEY" | jq '.data[] | select(.type == "web")')
CH_PROCESS_ID=$(echo "$CH_PROCESS" | jq -r .id)
CLICKHOUSE_HOST=$(echo "$CH_PROCESS" | jq -r .internal_hostname)
```

CLI: `sevalla apps create --name langfuse-clickhouse --cluster "$CLUSTER_ID" --source dockerImage --docker-image clickhouse/clickhouse-server:26.8.36.3`, then `sevalla apps processes list --app-id "$CH_APP_ID"`. MCP: `create_application` with `deploy: false`, then `get_application`.

**8. Set the port, turn HTTP off and set the size**

[Update process](https://api-docs.sevalla.com/v3/applications/update-process) (`updateProcessV3`). `is_ingress_enabled: false` is the dashboard's **Disable HTTP**. The API creates the web process at **S1**, so the size is set here:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s -X PATCH https://api.sevalla.com/v3/applications/$CH_APP_ID/processes/$CH_PROCESS_ID \
  -H "Authorization: Bearer $SEVALLA_API_KEY" -H "Content-Type: application/json" \
  -d "$(jq -n --arg size "$S2" '{port: 8123, is_ingress_enabled: false, resource_type_id: $size}')" \
  | jq -c '{port, is_ingress_enabled, resource_type_name}'
```

```json theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
{"port":8123,"is_ingress_enabled":false,"resource_type_name":"s2"}
```

CLI: not available for the port and HTTP traffic; `sevalla apps processes update "$CH_PROCESS_ID" --app-id "$CH_APP_ID" --resource-type "$S2"` sets the size. MCP: `sevalla_api_update` with `updateProcessV3` and the same body.

**9. Expose the native port**

[Create private port](https://api-docs.sevalla.com/v3/applications/create-private-port) (`createApplicationPrivatePortV3`):

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s -X POST https://api.sevalla.com/v3/applications/$CH_APP_ID/private-ports \
  -H "Authorization: Bearer $SEVALLA_API_KEY" -H "Content-Type: application/json" \
  -d "$(jq -n --arg process "$CH_PROCESS_ID" '{process_id: $process, port: 9000}')" | jq -r .port
```

CLI: `sevalla apps private-ports create "$CH_APP_ID" --process-id "$CH_PROCESS_ID" --port 9000`. MCP: `sevalla_api_create` with `createApplicationPrivatePortV3`.

**10. Add the disk**

[Attach or update process disk](https://api-docs.sevalla.com/v3/applications/attach-or-update-process-disk) (`updateProcessDiskV3`):

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s -X PUT https://api.sevalla.com/v3/applications/$CH_APP_ID/processes/$CH_PROCESS_ID/disk \
  -H "Authorization: Bearer $SEVALLA_API_KEY" -H "Content-Type: application/json" \
  -d "$(jq -n --arg disk "$DISK_10GB" '{resource_type_id: $disk, mount_path: "/var/lib/clickhouse"}')" \
  | jq -c '{size_gib, mount_path}'
```

```json theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
{"size_gib":10,"mount_path":"/var/lib/clickhouse"}
```

CLI: `sevalla apps processes disk update "$CH_PROCESS_ID" --app-id "$CH_APP_ID" --resource-type "$DISK_10GB" --mount-path /var/lib/clickhouse`. MCP: `sevalla_api_update` with `updateProcessDiskV3`.

**11. Add ClickHouse's variables**

[Create environment variable](https://api-docs.sevalla.com/v3/applications/create-environment-variable) (`createEnvironmentVariableV3`), once per variable:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
for pair in "CLICKHOUSE_USER=langfuse" "CLICKHOUSE_PASSWORD=$CLICKHOUSE_PASSWORD"; do
  curl -s -X POST https://api.sevalla.com/v3/applications/$CH_APP_ID/env-vars \
    -H "Authorization: Bearer $SEVALLA_API_KEY" -H "Content-Type: application/json" \
    -d "$(jq -n --arg k "${pair%%=*}" --arg v "${pair#*=}" '{key: $k, value: $v}')" | jq -r .key
done
```

CLI: `sevalla apps env-vars create "$CH_APP_ID" --key CLICKHOUSE_USER --value langfuse`, once per variable. MCP: `set_environment_variables`.

**12. Deploy ClickHouse**

[Trigger deployment](https://api-docs.sevalla.com/v3/applications/trigger-deployment) (`triggerDeploymentV3`):

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
CH_DEPLOYMENT_ID=$(curl -s -X POST https://api.sevalla.com/v3/applications/$CH_APP_ID/deployments \
  -H "Authorization: Bearer $SEVALLA_API_KEY" -H "Content-Type: application/json" -d '{}' | jq -r .id)
```

CLI: `sevalla apps deployments trigger "$CH_APP_ID"`. MCP: `trigger_deployment`.

**13. Wait for the databases**

[Get database](https://api-docs.sevalla.com/v3/databases/get-database) (`getDatabaseV3`) until both are `ready`, then keep their internal addresses:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
for db in $PG_ID $REDIS_ID; do
  for i in $(seq 1 60); do [ "$(curl -s https://api.sevalla.com/v3/databases/$db \
    -H "Authorization: Bearer $SEVALLA_API_KEY" | jq -r .status)" = ready ] && break; sleep 15; done
done
PG=$(curl -s https://api.sevalla.com/v3/databases/$PG_ID -H "Authorization: Bearer $SEVALLA_API_KEY")
REDIS=$(curl -s https://api.sevalla.com/v3/databases/$REDIS_ID -H "Authorization: Bearer $SEVALLA_API_KEY")
DATABASE_URL="postgres://langfuse:$PG_PASSWORD@$(echo "$PG" | jq -r .internal_hostname):$(echo "$PG" | jq -r .internal_port)/langfuse"
REDIS_HOST=$(echo "$REDIS" | jq -r .internal_hostname)
REDIS_PORT=$(echo "$REDIS" | jq -r .internal_port)
```

CLI: `sevalla databases get "$PG_ID"`. MCP: `get_database`.

**14. Create the web application**

[Create application](https://api-docs.sevalla.com/v3/applications/create-application) (`createApplicationV3`), then [List processes](https://api-docs.sevalla.com/v3/applications/list-processes) (`listProcessesV3`) and [List application domains](https://api-docs.sevalla.com/v3/applications/list-application-domains) (`listApplicationDomainsV3`) for its address. The domain appears a moment after the application, so the last command waits for it:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
WEB_APP_ID=$(curl -s -X POST https://api.sevalla.com/v3/applications \
  -H "Authorization: Bearer $SEVALLA_API_KEY" -H "Content-Type: application/json" \
  -d "$(jq -n --arg cluster "$CLUSTER_ID" \
    '{display_name: "langfuse", cluster_id: $cluster, source: "dockerImage", docker_image: "langfuse/langfuse:4.72.0"}')" \
  | jq -r .id)
WEB_PROCESS_ID=$(curl -s https://api.sevalla.com/v3/applications/$WEB_APP_ID/processes \
  -H "Authorization: Bearer $SEVALLA_API_KEY" | jq -r '.data[] | select(.type == "web") | .id')
for i in $(seq 1 30); do LANGFUSE_HOST=$(curl -s https://api.sevalla.com/v3/applications/$WEB_APP_ID/domains \
    -H "Authorization: Bearer $SEVALLA_API_KEY" | jq -r '.data[] | select(.is_primary) | .name')
  [ -n "$LANGFUSE_HOST" ] && break; sleep 2; done
```

CLI: `sevalla apps create --name langfuse --cluster "$CLUSTER_ID" --source dockerImage --docker-image langfuse/langfuse:4.72.0`, `sevalla apps domains list --app-id "$WEB_APP_ID"`. MCP: `create_application` with `deploy: false`, then `get_application`.

**15. Write the variables file**

Both Langfuse applications get these variables. The file holds secrets: delete it at the end.

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
cat > langfuse.env <<EOF
NEXTAUTH_URL=https://$LANGFUSE_HOST
NEXTAUTH_SECRET=$NEXTAUTH_SECRET
SALT=$SALT
ENCRYPTION_KEY=$ENCRYPTION_KEY
DATABASE_URL=$DATABASE_URL
REDIS_HOST=$REDIS_HOST
REDIS_PORT=$REDIS_PORT
REDIS_AUTH=$REDIS_PASSWORD
CLICKHOUSE_URL=http://$CLICKHOUSE_HOST:8123
CLICKHOUSE_MIGRATION_URL=clickhouse://$CLICKHOUSE_HOST:9000
CLICKHOUSE_USER=langfuse
CLICKHOUSE_PASSWORD=$CLICKHOUSE_PASSWORD
CLICKHOUSE_CLUSTER_ENABLED=false
LANGFUSE_S3_EVENT_UPLOAD_BUCKET=$S3_BUCKET
LANGFUSE_S3_EVENT_UPLOAD_PREFIX=events/
LANGFUSE_S3_EVENT_UPLOAD_REGION=auto
LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT=$S3_ENDPOINT
LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID=$S3_ACCESS_KEY_ID
LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY=$S3_SECRET_ACCESS_KEY
LANGFUSE_S3_MEDIA_UPLOAD_BUCKET=$S3_BUCKET
LANGFUSE_S3_MEDIA_UPLOAD_PREFIX=media/
LANGFUSE_S3_MEDIA_UPLOAD_REGION=auto
LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT=$S3_ENDPOINT
LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID=$S3_ACCESS_KEY_ID
LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY=$S3_SECRET_ACCESS_KEY
LANGFUSE_S3_BATCH_EXPORT_ENABLED=true
LANGFUSE_S3_BATCH_EXPORT_BUCKET=$S3_BUCKET
LANGFUSE_S3_BATCH_EXPORT_PREFIX=exports/
LANGFUSE_S3_BATCH_EXPORT_REGION=auto
LANGFUSE_S3_BATCH_EXPORT_ENDPOINT=$S3_ENDPOINT
LANGFUSE_S3_BATCH_EXPORT_ACCESS_KEY_ID=$S3_ACCESS_KEY_ID
LANGFUSE_S3_BATCH_EXPORT_SECRET_ACCESS_KEY=$S3_SECRET_ACCESS_KEY
EOF
cat > langfuse-init.env <<EOF
LANGFUSE_INIT_ORG_ID=acme
LANGFUSE_INIT_ORG_NAME=Acme
LANGFUSE_INIT_PROJECT_ID=storefront
LANGFUSE_INIT_PROJECT_NAME=storefront
LANGFUSE_INIT_PROJECT_PUBLIC_KEY=$LANGFUSE_PUBLIC_KEY
LANGFUSE_INIT_PROJECT_SECRET_KEY=$LANGFUSE_SECRET_KEY
LANGFUSE_INIT_USER_EMAIL=admin@example.com
LANGFUSE_INIT_USER_NAME=Admin
LANGFUSE_INIT_USER_PASSWORD=$ADMIN_PASSWORD
EOF
```

Use your own email address in `LANGFUSE_INIT_USER_EMAIL`: it signs in to Langfuse with `ADMIN_PASSWORD`.

**16. Connect the web application**

[Create internal connection](https://api-docs.sevalla.com/v3/applications/create-internal-connection) (`createApplicationInternalConnectionV3`) to the two databases and ClickHouse. Through the API, a connection adds no variables; the file has them.

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
for target in "$PG_ID database" "$REDIS_ID database" "$CH_APP_ID app"; do
  set -- $target
  curl -s -X POST https://api.sevalla.com/v3/applications/$WEB_APP_ID/internal-connections \
    -H "Authorization: Bearer $SEVALLA_API_KEY" -H "Content-Type: application/json" \
    -d "$(jq -n --arg id "$1" --arg type "$2" '{target_id: $id, target_type: $type}')" | jq -r .id
done
```

CLI: `sevalla apps internal-connections create "$WEB_APP_ID" --target-id "$PG_ID" --target-type database`, once per service. MCP: `sevalla_api_create` with `createApplicationInternalConnectionV3`.

**17. Add the web application's variables**

[Create environment variable](https://api-docs.sevalla.com/v3/applications/create-environment-variable) (`createEnvironmentVariableV3`) for each line of both files:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
cat langfuse.env langfuse-init.env | while IFS= read -r line; do
  curl -s -X POST https://api.sevalla.com/v3/applications/$WEB_APP_ID/env-vars \
    -H "Authorization: Bearer $SEVALLA_API_KEY" -H "Content-Type: application/json" \
    -d "$(jq -n --arg k "${line%%=*}" --arg v "${line#*=}" '{key: $k, value: $v}')" | jq -r .key
done | wc -l
```

It prints `41`, one per variable. CLI: `sevalla apps env-vars create "$WEB_APP_ID" --key <key> --value <value>`, once per variable. MCP: `set_environment_variables`, up to 50 variables per call.

**18. Set the size and the readiness probe**

[Update process](https://api-docs.sevalla.com/v3/applications/update-process) (`updateProcessV3`). Send every probe field: without `timeoutSeconds`, the API stores 120 seconds instead of the dashboard's 1.

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s -X PATCH https://api.sevalla.com/v3/applications/$WEB_APP_ID/processes/$WEB_PROCESS_ID \
  -H "Authorization: Bearer $SEVALLA_API_KEY" -H "Content-Type: application/json" \
  -d "$(jq -n --arg size "$S2" '{resource_type_id: $size, readiness_probe: {httpGet: {path: "/api/public/ready", port: 8080},
    initialDelaySeconds: 0, periodSeconds: 10, timeoutSeconds: 1, successThreshold: 1, failureThreshold: 3}}')" \
  | jq -c '{resource_type_name, path: .readiness_probe.httpGet.path, timeout: .readiness_probe.timeoutSeconds}'
```

```json theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
{"resource_type_name":"s2","path":"/api/public/ready","timeout":1}
```

CLI: `sevalla apps processes update "$WEB_PROCESS_ID" --app-id "$WEB_APP_ID" --resource-type "$S2"`; not available for the probe. MCP: `sevalla_api_update` with `updateProcessV3`.

**19. Deploy the web application**

[Trigger deployment](https://api-docs.sevalla.com/v3/applications/trigger-deployment) (`triggerDeploymentV3`), then [Get deployment](https://api-docs.sevalla.com/v3/applications/get-deployment) (`getDeploymentV3`) until it finishes:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
WEB_DEPLOYMENT_ID=$(curl -s -X POST https://api.sevalla.com/v3/applications/$WEB_APP_ID/deployments \
  -H "Authorization: Bearer $SEVALLA_API_KEY" -H "Content-Type: application/json" -d '{}' | jq -r .id)
for i in $(seq 1 60); do STATUS=$(curl -s https://api.sevalla.com/v3/applications/$WEB_APP_ID/deployments/$WEB_DEPLOYMENT_ID \
    -H "Authorization: Bearer $SEVALLA_API_KEY" | jq -r .status)
  case $STATUS in success|failed|cancelled|skipped) true ;; *) false ;; esac && break; sleep 10; done
echo "$STATUS"
```

It prints `success`. If it prints `failed`, read the deployment's log with [Get application deployment logs](https://api-docs.sevalla.com/v3/applications/get-application-deployment-logs) (`getApplicationDeploymentLogsV3`). CLI: `sevalla apps deployments trigger "$WEB_APP_ID"`, `sevalla apps deployments get "$WEB_DEPLOYMENT_ID" --app-id "$WEB_APP_ID"`. MCP: `trigger_deployment`, `get_deployment`, `get_deployment_logs`.

**20. Create the worker**

[Create application](https://api-docs.sevalla.com/v3/applications/create-application) (`createApplicationV3`) and [List processes](https://api-docs.sevalla.com/v3/applications/list-processes) (`listProcessesV3`):

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
WORKER_APP_ID=$(curl -s -X POST https://api.sevalla.com/v3/applications \
  -H "Authorization: Bearer $SEVALLA_API_KEY" -H "Content-Type: application/json" \
  -d "$(jq -n --arg cluster "$CLUSTER_ID" \
    '{display_name: "langfuse-worker", cluster_id: $cluster, source: "dockerImage", docker_image: "langfuse/langfuse-worker:4.72.0"}')" \
  | jq -r .id)
WORKER_PROCESS_ID=$(curl -s https://api.sevalla.com/v3/applications/$WORKER_APP_ID/processes \
  -H "Authorization: Bearer $SEVALLA_API_KEY" | jq -r '.data[] | select(.type == "web") | .id')
```

CLI: `sevalla apps create --name langfuse-worker --cluster "$CLUSTER_ID" --source dockerImage --docker-image langfuse/langfuse-worker:4.72.0`. MCP: `create_application` with `deploy: false`.

**21. Connect the worker**

[Create internal connection](https://api-docs.sevalla.com/v3/applications/create-internal-connection) (`createApplicationInternalConnectionV3`), as for the web application:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
for target in "$PG_ID database" "$REDIS_ID database" "$CH_APP_ID app"; do
  set -- $target
  curl -s -X POST https://api.sevalla.com/v3/applications/$WORKER_APP_ID/internal-connections \
    -H "Authorization: Bearer $SEVALLA_API_KEY" -H "Content-Type: application/json" \
    -d "$(jq -n --arg id "$1" --arg type "$2" '{target_id: $id, target_type: $type}')" | jq -r .id
done
```

CLI: `sevalla apps internal-connections create "$WORKER_APP_ID" --target-id "$PG_ID" --target-type database`, once per service. MCP: `sevalla_api_create` with `createApplicationInternalConnectionV3`.

**22. Add the worker's variables**

[Create environment variable](https://api-docs.sevalla.com/v3/applications/create-environment-variable) (`createEnvironmentVariableV3`) for each line of `langfuse.env`, then delete the files:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
while IFS= read -r line; do
  curl -s -X POST https://api.sevalla.com/v3/applications/$WORKER_APP_ID/env-vars \
    -H "Authorization: Bearer $SEVALLA_API_KEY" -H "Content-Type: application/json" \
    -d "$(jq -n --arg k "${line%%=*}" --arg v "${line#*=}" '{key: $k, value: $v}')" | jq -r .key
done < langfuse.env | wc -l
rm langfuse.env langfuse-init.env
```

It prints `32`. CLI: `sevalla apps env-vars create`, once per variable. MCP: `set_environment_variables`.

**23. Turn the worker's HTTP off and set its size**

[Update process](https://api-docs.sevalla.com/v3/applications/update-process) (`updateProcessV3`):

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s -X PATCH https://api.sevalla.com/v3/applications/$WORKER_APP_ID/processes/$WORKER_PROCESS_ID \
  -H "Authorization: Bearer $SEVALLA_API_KEY" -H "Content-Type: application/json" \
  -d "$(jq -n --arg size "$S2" '{is_ingress_enabled: false, resource_type_id: $size}')" \
  | jq -c '{is_ingress_enabled, resource_type_name}'
```

```json theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
{"is_ingress_enabled":false,"resource_type_name":"s2"}
```

CLI: not available for HTTP traffic; `sevalla apps processes update` sets the size. MCP: `sevalla_api_update` with `updateProcessV3`.

**24. Deploy the worker**

[Trigger deployment](https://api-docs.sevalla.com/v3/applications/trigger-deployment) (`triggerDeploymentV3`) and wait, as for the web application:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
WORKER_DEPLOYMENT_ID=$(curl -s -X POST https://api.sevalla.com/v3/applications/$WORKER_APP_ID/deployments \
  -H "Authorization: Bearer $SEVALLA_API_KEY" -H "Content-Type: application/json" -d '{}' | jq -r .id)
for i in $(seq 1 60); do STATUS=$(curl -s https://api.sevalla.com/v3/applications/$WORKER_APP_ID/deployments/$WORKER_DEPLOYMENT_ID \
    -H "Authorization: Bearer $SEVALLA_API_KEY" | jq -r .status)
  case $STATUS in success|failed|cancelled|skipped) true ;; *) false ;; esac && break; sleep 10; done
echo "$STATUS"
```

It prints `success`. CLI: `sevalla apps deployments trigger "$WORKER_APP_ID"`. MCP: `trigger_deployment`, `get_deployment`.

**25. Check the installation**

The web application answers, the project exists, and a trace goes through the worker into ClickHouse:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s https://$LANGFUSE_HOST/api/public/health
curl -s -u "$LANGFUSE_PUBLIC_KEY:$LANGFUSE_SECRET_KEY" https://$LANGFUSE_HOST/api/public/projects | jq -c '.data[] | {id, name}'
LANGFUSE_BASE_URL=https://$LANGFUSE_HOST LANGFUSE_PUBLIC_KEY=$LANGFUSE_PUBLIC_KEY \
  LANGFUSE_SECRET_KEY=$LANGFUSE_SECRET_KEY python trace.py
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
{"status":"OK","version":"4.72.0"}
{"id":"storefront","name":"storefront"}
https://langfuse-x7k2m.sevalla.app/project/storefront/traces/784900dc5281304e5ea6b71914745ccd
```

`trace.py` is the script in [Send a test trace](#send-a-test-trace), run in its virtual environment. A few seconds later, the [Observations API](https://api.reference.langfuse.com) returns the span and the generation from ClickHouse:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
TRACE_ID=784900dc5281304e5ea6b71914745ccd
curl -s -u "$LANGFUSE_PUBLIC_KEY:$LANGFUSE_SECRET_KEY" \
  "https://$LANGFUSE_HOST/api/public/v2/observations?traceId=$TRACE_ID" | jq -r '.data[] | "\(.type) \(.name)"' | sort
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
GENERATION draft-reply
SPAN answer-support-question
```

Use the trace ID that `trace.py` printed. Sign in to `https://$LANGFUSE_HOST` with `LANGFUSE_INIT_USER_EMAIL` and `ADMIN_PASSWORD` to see the trace in the interface.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.