> ## 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 Qdrant

> Run the Qdrant vector database on Sevalla from its official image: a private application with a disk and an API key, a Python client, snapshots in object storage, and measured memory use.

Qdrant is an open-source vector database: it stores vectors (embeddings) with JSON payloads and finds the ones most similar to a query vector. On Sevalla, Qdrant runs as an application from the official Docker image, with its data on a disk and HTTP traffic turned off, so only your own applications reach it, over an [internal connection](https://docs.sevalla.com/applications/networking.md#internal-connections): its REST API on port `6333` and its gRPC API on port `6334`. This guide deploys Qdrant, connects a Python application to it, backs it up to object storage, and measures how much memory a collection needs.

## At a glance

| | |
| - | - |
| Source | The `qdrant/qdrant:v1.20.0-unprivileged` Docker image for Qdrant; a Git repository for the `catalog` client |
| Build | Qdrant: none, applications from a Docker image skip the build. Client: [Ingot](https://docs.sevalla.com/applications/builds/ingot.md) (Python 3.14, pip), or the [FastAPI Dockerfile](https://docs.sevalla.com/guides/python/fastapi/deploy.md#deploy-with-a-dockerfile) |
| Start command | Qdrant: the image's own. Client: `uvicorn app.main:app --host 0.0.0.0 --port "$PORT"`, which Ingot detects |
| Port | Qdrant: `6333` (REST) with HTTP traffic off, and private port `6334` (gRPC). Client: the `PORT` environment variable, `8080` by default |
| Processes | Qdrant: web process. Client: web process; `Snapshot` cron job (`python snapshot.py create`, `0 3 * * *`) |
| Services | The client reaches Qdrant over an internal connection; Qdrant stores snapshots in an object storage bucket |
| Disk | 10 GB at `/qdrant/storage`, on Qdrant |
| Health check | `GET /readyz` on port `6333` returns `200` on Qdrant (readiness probe) |
| Size | **S1** for Qdrant; **H1** for the client and the cron job |

### Environment variables

On the `qdrant` application, which has no build, every variable is available during runtime:

| Variable | Required | Available during | Secret | Value |
| - | - | - | - | - |
| `QDRANT__SERVICE__API_KEY` | Yes | Runtime | Yes | Generate with `openssl rand -hex 32` |
| `QDRANT__TELEMETRY_DISABLED` | No | Runtime | No | `true` stops anonymous usage statistics |
| `QDRANT__STORAGE__SNAPSHOTS_CONFIG__SNAPSHOTS_STORAGE` | No | Runtime | No | `s3`, to store snapshots in object storage |
| `QDRANT__STORAGE__SNAPSHOTS_CONFIG__S3_CONFIG__BUCKET` | With `s3` | Runtime | No | The bucket's **Bucket name** |
| `QDRANT__STORAGE__SNAPSHOTS_CONFIG__S3_CONFIG__REGION` | With `s3` | Runtime | No | `auto` |
| `QDRANT__STORAGE__SNAPSHOTS_CONFIG__S3_CONFIG__ENDPOINT_URL` | With `s3` | Runtime | No | The bucket's **Endpoint** |
| `QDRANT__STORAGE__SNAPSHOTS_CONFIG__S3_CONFIG__ACCESS_KEY` | With `s3` | Runtime | Yes | The bucket's **Access key** |
| `QDRANT__STORAGE__SNAPSHOTS_CONFIG__S3_CONFIG__SECRET_KEY` | With `s3` | Runtime | Yes | The bucket's **Secret key** |

On the `catalog` client:

| Variable | Required | Available during | Secret | Value |
| - | - | - | - | - |
| `QDRANT_HOST` | Yes | Runtime | No | `APP_HOST` from the `qdrant` internal connection, renamed |
| `QDRANT_URL` | Yes | Runtime | No | `http://${QDRANT_HOST}:6333` |
| `QDRANT_API_KEY` | Yes | Runtime | Yes | Qdrant's `QDRANT__SERVICE__API_KEY` |
| `PORT` | No | Runtime | No | Set by Sevalla to the web process port, `8080` |

## How Qdrant runs on Sevalla

| Setting | Value | Why |
| - | - | - |
| Docker image | `qdrant/qdrant:v1.20.0-unprivileged` | The official image, in the variant that runs as an unprivileged user (UID 1000) instead of root. The version tag keeps a redeployment from pulling a newer release. |
| Web process port | `6333` | Qdrant's REST API. Other services reach the web process port on the application's internal hostname. |
| HTTP traffic | Off | The application's domains stop answering, so the database isn't on the internet |
| Private port | `6334` | Qdrant's gRPC API, for clients that prefer it |
| Disk | `/qdrant/storage`, 10 GB | Qdrant's data folder. Without a disk, every deployment starts empty. |
| `QDRANT__SERVICE__API_KEY` | A random key | Every request must send it, even on the private network |
| Readiness probe | `/readyz` on port `6333` | The deployment finishes only when Qdrant has loaded its collections |
| Size | **S1** (0.5 vCPU, 1 GB) | Enough for 100,000 vectors of 768 dimensions. See [Size Qdrant](#size-qdrant). |

A process with a [disk](https://docs.sevalla.com/applications/disks.md) runs one instance, and Sevalla stops it before it starts the new one on each deployment: In the tests, a deployment of Qdrant took about 20 seconds, during which requests to it failed. This guide runs Qdrant as a single node.

## Before you begin

* A Sevalla company with a payment method, and a GitHub, GitLab or Bitbucket account for the client application's repository. See [Git providers](https://docs.sevalla.com/git/overview.md).
* Python 3.14 and Docker on your computer, to build and check the client application.

## Deploy Qdrant

Generate the API key:

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

**1. Create the application**

In the sidebar, click **Applications**, then click **Create** > **Application**. Under **Source**, click **Docker image** and enter `qdrant/qdrant:v1.20.0-unprivileged`. Keep **None (public image)** as the **Registry credential**. Enter `qdrant` as the **Name**, choose a **Data center** and the **S1** size, then click **Create application**.

**2. Add the variables**

Click the **Environment variables** tab, then **Add environment variables**. Add `QDRANT__SERVICE__API_KEY` with the key you generated, and `QDRANT__TELEMETRY_DISABLED` with the value `true` to stop Qdrant from sending anonymous usage statistics. 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 `6333` and click **Update port**. Then click **Disable**, and **Disable HTTP** in the dialog.

**4. Expose the gRPC port**

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

**5. Add the disk**

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

**6. Add the 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 `/readyz` and the **Port** to `6333`, and click **Update process**. Qdrant answers `/readyz` without the API key.

**7. Deploy**

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

![The Expose port sheet with the Process and Port fields](https://assets.sevalla.com/screenshots/light/app-networking-port-expose.png)

When the deployment is **Deployed**, the application's [runtime logs](https://docs.sevalla.com/applications/logs.md) show `Qdrant gRPC listening on 6334` and `Qdrant HTTP listening on 6333`. [Get application runtime logs](https://api-docs.sevalla.com/v3/applications/get-application-runtime-logs) (`getApplicationRuntimeLogsV3`) finds them, with the application 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/$QDRANT_APP_ID/runtime-logs?search=listening%20on" \
  -H "Authorization: Bearer $SEVALLA_API_KEY" | jq -r '.[].message'
```

````text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
2026-10-30T05:02:22.470618Z  INFO qdrant::actix: Qdrant HTTP listening on 6333
2026-10-30T05:02:22.470639Z  INFO actix_server::server: starting service: "actix-web-service-0.0.0.0:6333", workers: 15, listening on: 0.0.0.0:6333
2026-10-30T05:02:22.474299Z  INFO qdrant::tonic: Qdrant gRPC listening on 6334
``` Its domain answers `404`, because HTTP traffic is off. The **Networking** tab lists the web process's internal hostname, for example `qdrant-x7k2m-web.qdrant-x7k2m.svc.cluster.local`.

### Why Qdrant stays private

Qdrant has no users or per-collection permissions by default: anyone with the API key can read, overwrite and delete every collection. With HTTP traffic off, a request has to come from one of your services with an internal connection to Qdrant, and it still needs the key. The traffic stays on the private network in the data center, but it's plain HTTP, not HTTPS, so `qdrant-client` prints `UserWarning: Api key is used with an insecure connection.` on that connection. To reach Qdrant from outside Sevalla, see [Make Qdrant public](#make-qdrant-public).

## Connect an application

This FastAPI application creates a `products` collection, adds five products, and returns the products most similar to one of them. It reaches Qdrant over the internal connection.

### Create the project

```bash
mkdir catalog && cd catalog
python3 -m venv .venv
source .venv/bin/activate
pip install "fastapi[standard]" qdrant-client
mkdir app && touch app/__init__.py
````

```text .gitignore theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
.venv/
__pycache__/
.env
```

```python app/main.py theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
import os
from contextlib import asynccontextmanager

from fastapi import FastAPI
from qdrant_client import QdrantClient, models

COLLECTION = "products"

client = QdrantClient(
    url=os.environ.get("QDRANT_URL", "http://localhost:6333"),
    api_key=os.environ.get("QDRANT_API_KEY"),
)

# Four-dimensional demo vectors. In your application, an embedding model makes them.
PRODUCTS = [
    (1, "Rain jacket", [0.9, 0.1, 0.1, 0.2]),
    (2, "Wool sweater", [0.8, 0.3, 0.1, 0.1]),
    (3, "Trail running shoes", [0.1, 0.9, 0.3, 0.1]),
    (4, "Hiking boots", [0.3, 0.8, 0.2, 0.2]),
    (5, "Camping stove", [0.1, 0.2, 0.9, 0.4]),
]


@asynccontextmanager
async def lifespan(app: FastAPI):
    if not client.collection_exists(COLLECTION):
        client.create_collection(
            COLLECTION,
            vectors_config=models.VectorParams(size=4, distance=models.Distance.COSINE),
        )
    client.upsert(
        COLLECTION,
        points=[
            models.PointStruct(id=id, vector=vector, payload={"name": name})
            for id, name, vector in PRODUCTS
        ],
    )
    yield


app = FastAPI(lifespan=lifespan)


@app.get("/")
def index():
    return {"products": client.count(COLLECTION).count}


@app.get("/products/{product_id}/similar")
def similar(product_id: int):
    hits = client.query_points(COLLECTION, query=product_id, limit=2).points
    return [{"name": hit.payload["name"], "score": round(hit.score, 3)} for hit in hits]
```

* The client reads Qdrant's address and key from `QDRANT_URL` and `QDRANT_API_KEY`, which you set on Sevalla.
* At startup, the application creates the collection if it doesn't exist, with the vector size and distance of your embeddings, then adds the products. `upsert` replaces points with the same ID, so every instance and every restart can run it.
* `query_points` with a point ID searches with that point's vector and leaves the point itself out of the results.

To check it on your computer, start Qdrant in Docker and the application with the same key:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
docker run -d -p 6333:6333 -e QDRANT__SERVICE__API_KEY=local-dev-key qdrant/qdrant:v1.20.0-unprivileged
QDRANT_API_KEY=local-dev-key fastapi dev
```

`http://127.0.0.1:8000/products/1/similar` returns `[{"name":"Wool sweater","score":0.966},{"name":"Hiking boots","score":0.488}]`. Press `Ctrl+C` to stop the application.

### Prepare the project for Sevalla

Pin the Python version and list the dependencies with the versions you tested:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
echo "3.14" > .python-version
pip freeze > requirements.txt
```

[Ingot](https://docs.sevalla.com/applications/builds/ingot.md) reads the Python version from `.python-version`, installs `requirements.txt` with pip, finds `app = FastAPI()` in `app/main.py`, and starts Uvicorn on the port in `PORT`. See [Deploy a FastAPI application](https://docs.sevalla.com/guides/python/fastapi/deploy.md) for the details. Commit everything and push it to your Git provider:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
git init
git add .
git commit -m "Connect to Qdrant"
git branch -M main
git remote add origin https://github.com/acme/catalog.git
git push -u origin main
```

### Deploy the application

**1. Create the application**

In the sidebar, click **Applications**, then click **Create** > **Application**. Under **Source**, click **Private repository**, then choose the **Git provider**, the **Repository** and the **Branch**. Under **Location**, choose the same **Data center** as Qdrant. Keep the **Size** at **H1** or choose a larger one, then click **Create application**.

**2. Connect Qdrant**

Click the **Networking** tab. Under **Connected services**, click **Add internal connection**. Choose `qdrant` as the **Service**, turn on **Add environment variables to the application**, and clear **Available during build**. Rename `APP_HOST` to `QDRANT_HOST`, then click **Add internal connection**.

**3. Add the variables**

Click the **Environment variables** tab, then **Add environment variables**. Add `QDRANT_URL` with the value `http://${QDRANT_HOST}:6333`, and `QDRANT_API_KEY` with Qdrant's API key. Clear **Available during buildtime** and click **Add environment variables**.

**4. Deploy**

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

`${QDRANT_HOST}` is a [reference](https://docs.sevalla.com/applications/environment-variables.md#reference-another-variable) to Qdrant's internal hostname, which the internal connection saved. Both variables are runtime only, so the build never sees the key.

### Verify the connection

1. The deployment's **Build log** shows Ingot's build plan:

   ```text Build log theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
   ╭─ ■ FastAPI · Python 3.14 ───────────────────────────────────────────────────────────────────╮
   │ Python           3.14                                                .python-version "3.14" │
   │ Package manager  pip                                                 requirements.txt       │
   │ Framework        FastAPI                                             app/main.py            │
   │ Start            uvicorn app.main:app --host 0.0.0.0 --port "$PORT"  FastAPI                │
   ├─ Image ─────────────────────────────────────────────────────────────────────────────────────┤
   │ Build            python:3.14-slim-trixie                                                    │
   │ Runtime          python:3.14-slim-trixie                                                    │
   │ User             1000:1000                                                                  │
   │ Init             tini                                                                       │
   │ Port             $PORT, default 8080                                                        │
   ╰────────────────────────────────────────────────────────────────────────────── Ingot v0.4.1 ─╯
   ```

2. When the deployment is **Deployed**, open the application's domain, for example `catalog-x7k2m.sevalla.app`. It returns `{"products":5}`.

3. Open `/products/1/similar`. It returns `[{"name":"Wool sweater","score":0.966},{"name":"Hiking boots","score":0.488}]`: the search ran in Qdrant, over the private network.

The same checks from a terminal:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s https://catalog-x7k2m.sevalla.app/
curl -s https://catalog-x7k2m.sevalla.app/products/1/similar
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
{"products":5}
[{"name":"Wool sweater","score":0.966},{"name":"Hiking boots","score":0.488}]
```

A script reads the deployment's `status` with [Get deployment](https://api-docs.sevalla.com/v3/applications/get-deployment) (`getDeploymentV3`) until it's `success` or `failed`, and the build log, with the build plan above, with [Get application deployment logs](https://api-docs.sevalla.com/v3/applications/get-application-deployment-logs) (`getApplicationDeploymentLogsV3`). The logs operation returns the last hour unless you pass `from`.

To use gRPC, which sends vectors in a compact binary format and suits large uploads, create the client with `QdrantClient(url=..., api_key=..., prefer_grpc=True)`. It connects to port `6334` on the same hostname, the private port you exposed.

To build the application from a Dockerfile instead of Ingot, use the one in [Deploy a FastAPI application](https://docs.sevalla.com/guides/python/fastapi/deploy.md#deploy-with-a-dockerfile): it works for this project unchanged, including the cron job in [Back up Qdrant](#back-up-qdrant).

## Back up Qdrant

Sevalla backs up the disk every day and keeps each backup for 7 days; restoring one rolls the whole disk back, through [support](https://docs.sevalla.com/applications/disks.md#backups). To restore a single collection, or to keep backups longer, store Qdrant's snapshots in [object storage](https://docs.sevalla.com/object-storage/overview.md).

### Store snapshots in object storage

**1. Create a bucket**

In the sidebar, click **Object storage**, then click **Create object storage**. Enter `qdrant-snapshots` as the **Name**, pick a **Location** and click **Create object storage**. On its **Settings** tab, note the **Endpoint**, the **Bucket name**, the **Access key** and the **Secret key**.

**2. Point Qdrant at the bucket**

On the `qdrant` application's **Environment variables** tab, add the variables below, with the bucket's values. Then deploy the application.

```bash .env theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
QDRANT__STORAGE__SNAPSHOTS_CONFIG__SNAPSHOTS_STORAGE=s3
QDRANT__STORAGE__SNAPSHOTS_CONFIG__S3_CONFIG__BUCKET=qdrant-snapshots-q8w2e
QDRANT__STORAGE__SNAPSHOTS_CONFIG__S3_CONFIG__REGION=auto
QDRANT__STORAGE__SNAPSHOTS_CONFIG__S3_CONFIG__ENDPOINT_URL=https://<account-id>.r2.cloudflarestorage.com
QDRANT__STORAGE__SNAPSHOTS_CONFIG__S3_CONFIG__ACCESS_KEY=<access-key>
QDRANT__STORAGE__SNAPSHOTS_CONFIG__S3_CONFIG__SECRET_KEY=<secret-key>
```

Add a script to the client application that creates, lists and restores snapshots of the `products` collection:

```python snapshot.py theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
import os
import sys

from qdrant_client import QdrantClient, models

COLLECTION = "products"

client = QdrantClient(url=os.environ["QDRANT_URL"], api_key=os.environ["QDRANT_API_KEY"])

if sys.argv[1] == "create":
    print(client.create_snapshot(COLLECTION).name)
elif sys.argv[1] == "list":
    for snapshot in client.list_snapshots(COLLECTION):
        print(snapshot.name, snapshot.size)
elif sys.argv[1] == "restore":
    # Qdrant reads the snapshot through the collection, so recreate it first if it's gone,
    # with the same vector settings as app/main.py.
    if not client.collection_exists(COLLECTION):
        client.create_collection(
            COLLECTION,
            vectors_config=models.VectorParams(size=4, distance=models.Distance.COSINE),
        )
    client.recover_snapshot(
        COLLECTION,
        location=f"{os.environ['QDRANT_URL']}/collections/{COLLECTION}/snapshots/{sys.argv[2]}",
        api_key=os.environ["QDRANT_API_KEY"],
        priority="snapshot",
    )
    print(client.count(COLLECTION).count, "points restored")
```

Commit and push it. Once it's deployed, open the client application's **Web terminal** tab and run `python snapshot.py create`. It prints the snapshot's name, for example `products-4433101156359780-2026-10-30-04-13-11-0.snapshot`, and the bucket's **Objects** tab shows the file under `snapshots/products/`. Without the dashboard, run it with [Run a command in a process](https://api-docs.sevalla.com/v3/applications/run-a-command-in-a-process) (`execProcessCommandV3`) on the client's web process:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
CATALOG_PROCESS_ID=$(curl -s https://api.sevalla.com/v3/applications/$CATALOG_APP_ID/processes \
  -H "Authorization: Bearer $SEVALLA_API_KEY" | jq -r '.data[] | select(.type == "web") | .id')
curl -s -X POST https://api.sevalla.com/v3/applications/$CATALOG_APP_ID/processes/$CATALOG_PROCESS_ID/exec \
  -H "Authorization: Bearer $SEVALLA_API_KEY" -H "Content-Type: application/json" \
  -d '{"command": ["python", "snapshot.py", "create"], "timeout": 60}' | jq -r .stdout
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
products-2160410467018281-2026-10-30-05-05-59-1.snapshot
```

To take one every day, create a cron job in the client application: on its **Processes** tab, click **Create process**, pick **Cron job**, enter `Snapshot` as the **Name**, `python snapshot.py create` as the **Custom start command** and `0 3 * * *` as the **Schedule** (03:00 UTC every day), keep the **Resource** at **H1**, click **Create process**, and deploy. To check it, click **⋯** on the cron job and **Run now**: a new file appears in the bucket. Qdrant keeps every snapshot until you delete it, on the bucket's **Objects** tab or with `client.delete_snapshot`.

### Restore a snapshot

In the client application's web terminal, run `restore` with the snapshot's name:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
python snapshot.py restore products-4433101156359780-2026-10-30-04-13-11-0.snapshot
```

It prints `5 points restored`. Without the dashboard, send the same command through [Run a command in a process](https://api-docs.sevalla.com/v3/applications/run-a-command-in-a-process) (`execProcessCommandV3`):

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s -X POST https://api.sevalla.com/v3/applications/$CATALOG_APP_ID/processes/$CATALOG_PROCESS_ID/exec \
  -H "Authorization: Bearer $SEVALLA_API_KEY" -H "Content-Type: application/json" \
  -d '{"command": ["python", "snapshot.py", "restore", "products-2160410467018281-2026-10-30-05-05-59-1.snapshot"], "timeout": 60}' \
  | jq -r .stdout
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
5 points restored
```

The restore replaces the whole collection with the snapshot, including points added since. Qdrant downloads the snapshot through its own REST API, which reads it from the bucket. If you deleted the collection, `list` can't read its snapshots: find the name on the bucket's **Objects** tab, under `snapshots/products/`. The collection that `restore` recreates must have the same vector settings as the snapshot, or Qdrant answers `Snapshot is not compatible with existing collection`. A snapshot restores only into the same or the next minor version of Qdrant.

## Size Qdrant

Qdrant keeps every collection on the disk and uses memory to search it fast. For dense vectors, the `memory` setting of the collection decides how:

| Tier | What Qdrant does | Set it with |
| - | - | - |
| `cached` (default) | Loads the vectors into the page cache when it starts | `models.VectorParams(size=768, distance=..., memory=models.Memory.CACHED)` |
| `cold` | Reads vectors from the disk the first time a search needs them | `memory=models.Memory.COLD`, which replaces the deprecated `on_disk=True` |

These numbers come from 100,000 random vectors of 768 dimensions, each with a small payload, in Qdrant 1.20 on **S1**, searched with 200 random queries (10 results each) over gRPC from another application:

| | `cached` | `cold` |
| - | - | - |
| On the disk | 372 MB | 372 MB |
| Qdrant's own memory | 90 to 130 MB | 90 to 125 MB |
| Upload, in batches of 250 over gRPC | 131 s | 136 s |
| First searches after a restart (median, 95th percentile) | 8 ms, 75 ms | 15 ms, 389 ms, slowest 3 s |
| Searches once the data is in memory | 4 ms, 87 ms | 3 ms, 87 ms |

To plan the memory for the `cached` tier, add Qdrant's own memory, the vectors (points × dimensions × 4 bytes, here 307 MB) and the vector index. **S1** ran both 100,000-vector collections, 744 MB on the disk, without running out of memory: the operating system keeps in memory what fits, and searches read the rest from the disk, as in the `cold` column. Choose a **Memory optimized** size (**M1** has 2 GB, **M2** 4 GB) for larger collections, or keep the vectors `cold` with [quantization](https://qdrant.tech/documentation/manage-data/quantization/), which keeps a compressed copy in memory. Qdrant's [capacity planning](https://qdrant.tech/documentation/capacity-planning/) has the full formulas. Grow the disk on the **Disks** tab before it fills; its **Usage** shows there.

## Make Qdrant public

To reach Qdrant from outside Sevalla, for example from a notebook on your computer, turn HTTP traffic back on: on the `qdrant` application's **Networking** tab, in **Expose HTTP traffic**, click **Enable**, then **Expose HTTP**. Sevalla applies it without a deployment.

The domain then serves Qdrant's REST API over HTTPS, and every request needs the key in the `api-key` header; without it, Qdrant answers `401`:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -H "api-key: $QDRANT_API_KEY" https://qdrant-x7k2m.sevalla.app/collections
```

In `qdrant-client`, put the port in the URL: `QdrantClient(url="https://qdrant-x7k2m.sevalla.app:443", api_key=...)`. Without `:443`, the client adds Qdrant's default port `6333` to the address and the request times out. Qdrant's web interface is at `/dashboard`. The gRPC port stays private.

A public Qdrant is one leaked key away from someone reading or deleting your data, so keep it private unless a client outside Sevalla needs it. When one does, give the application a long random key, rotate it if it's exposed, and allow only known addresses with [IP restrictions](https://docs.sevalla.com/applications/networking.md#ip-restrictions).

## Qdrant or pgvector

If your application already uses Sevalla PostgreSQL, [pgvector](https://docs.sevalla.com/guides/ai/rag-pgvector.md) adds vector search to the same database: one service to run, managed backups, and SQL joins between vectors and the rest of your data, with no disk or single instance to plan around. Qdrant is a separate service, but it's built for vectors alone: it filters on payloads inside the search, keeps compressed vectors in memory with quantization, and lets you place each collection's vectors in memory or on the disk. Start with pgvector for a few hundred thousand vectors next to relational data, and choose Qdrant when vector search is the main workload or the collections outgrow the database.

## Update Qdrant

Qdrant upgrades one minor version at a time: from 1.20 to 1.22, upgrade to the latest 1.21 patch release first. Keep `qdrant-client` within one minor version of the server, and update the server first.

**1. Take a snapshot**

Run `python snapshot.py create` in the client application's web terminal, for each collection you need.

**2. Change the image**

On the `qdrant` application's **Settings** tab, in the **Source** section, click **Update source**. Change the **Docker image** to the new version, for example `qdrant/qdrant:v1.21.0-unprivileged`, and click **Update source**.

**3. Deploy**

Deploy the application. Qdrant stops, starts on the new version with the same disk, and the readiness probe holds the deployment until it's ready.

## Troubleshoot

| Symptom | Cause | Fix |
| - | - | - |
| `qdrant-client` times out on the public domain | The URL has no port, so the client uses `6333` | Use `https://qdrant-x7k2m.sevalla.app:443` |
| `Unexpected Response: 401 (Unauthorized)` with `Invalid API key or JWT` | The request has no API key or the wrong one | Check `QDRANT_API_KEY` against the `qdrant` application's `QDRANT__SERVICE__API_KEY` |
| A restore fails with `Failed to download tar from http://...` | The collection doesn't exist, so Qdrant can't read the snapshot through it | Create the collection with the snapshot's vector settings first, as `snapshot.py` does |
| A restore fails with `Snapshot is not compatible with existing collection` | The collection's vector settings differ from the snapshot's | Delete the collection and let `restore` create it with the right settings |
| gRPC requests fail with `UNAVAILABLE` right after a deployment of Qdrant | Qdrant was still starting | Add the [readiness probe](#deploy-qdrant) on `/readyz` |

## Deploy with the API

This sequence deploys Qdrant, the snapshot bucket and the `catalog` client 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 and object storage, in `SEVALLA_API_KEY`.
* The client's repository on a Git provider connected to your Sevalla account. Connecting one happens in the dashboard, under **My account**; see [Git providers](https://docs.sevalla.com/git/overview.md). If the account can't read the repository, creating the application fails with `400` and `Cannot access repository <url>: Repository not found or you don't have access. Make sure a Git account with access to this repository is connected at https://app.sevalla.com/my-account.` For GitHub, the message adds that Sevalla's GitHub App also needs access to the repository.
* `curl`, `jq` and `openssl`, in a bash shell that keeps variables between commands.

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 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`) and [List disk resource types](https://api-docs.sevalla.com/v3/resources/list-disk-resource-types) (`listDiskResourceTypesV3`):

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
H1=$(curl -s https://api.sevalla.com/v3/resources/process-resource-types \
  -H "Authorization: Bearer $SEVALLA_API_KEY" | jq -r '.[] | select(.name == "h1") | .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')
QDRANT_API_KEY=$(openssl rand -hex 32)
```

The last line generates Qdrant's API key. CLI: `sevalla resources process-types`, `sevalla resources disk-types`. MCP: `get_platform_options`.

**3. Create the snapshot 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": "qdrant-snapshots"}')
BUCKET_ID=$(echo "$BUCKET" | jq -r .id)
```

CLI: `sevalla object-storage create --name qdrant-snapshots`. MCP: `sevalla_api_create` with `createObjectStorageV3`.

**4. Create the Qdrant 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. The API creates the web process at **S1**, the size this guide uses.

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
QDRANT_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: "qdrant", cluster_id: $cluster, source: "dockerImage", docker_image: "qdrant/qdrant:v1.20.0-unprivileged"}')" \
  | jq -r .id)
QDRANT_PROCESS=$(curl -s https://api.sevalla.com/v3/applications/$QDRANT_APP_ID/processes \
  -H "Authorization: Bearer $SEVALLA_API_KEY" | jq '.data[] | select(.type == "web")')
QDRANT_PROCESS_ID=$(echo "$QDRANT_PROCESS" | jq -r .id)
QDRANT_HOST=$(echo "$QDRANT_PROCESS" | jq -r .internal_hostname)
```

CLI: `sevalla apps create --name qdrant --cluster "$CLUSTER_ID" --source dockerImage --docker-image qdrant/qdrant:v1.20.0-unprivileged`, then `sevalla apps processes list --app-id "$QDRANT_APP_ID"`. MCP: `create_application` with `deploy: false`, then `get_application`.

**5. Set the port, turn HTTP off and add the readiness probe**

[Update process](https://api-docs.sevalla.com/v3/applications/update-process) (`updateProcessV3`). `is_ingress_enabled: false` is the dashboard's **Disable HTTP**. 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/$QDRANT_APP_ID/processes/$QDRANT_PROCESS_ID \
  -H "Authorization: Bearer $SEVALLA_API_KEY" -H "Content-Type: application/json" \
  -d '{"port": 6333, "is_ingress_enabled": false, "readiness_probe": {"httpGet": {"path": "/readyz", "port": 6333},
    "initialDelaySeconds": 0, "periodSeconds": 10, "timeoutSeconds": 1, "successThreshold": 1, "failureThreshold": 3}}' \
  | jq -c '{port, is_ingress_enabled, path: .readiness_probe.httpGet.path, timeout: .readiness_probe.timeoutSeconds}'
```

```json theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
{"port":6333,"is_ingress_enabled":false,"path":"/readyz","timeout":1}
```

CLI: not available. MCP: `sevalla_api_update` with `updateProcessV3` and the same body.

**6. Expose the gRPC 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/$QDRANT_APP_ID/private-ports \
  -H "Authorization: Bearer $SEVALLA_API_KEY" -H "Content-Type: application/json" \
  -d "$(jq -n --arg process "$QDRANT_PROCESS_ID" '{process_id: $process, port: 6334}')" | jq -r .port
```

CLI: `sevalla apps private-ports create "$QDRANT_APP_ID" --process-id "$QDRANT_PROCESS_ID" --port 6334`. MCP: `sevalla_api_create` with `createApplicationPrivatePortV3`.

**7. 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/$QDRANT_APP_ID/processes/$QDRANT_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: "/qdrant/storage"}')" \
  | jq -c '{size_gib, mount_path}'
```

```json theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
{"size_gib":10,"mount_path":"/qdrant/storage"}
```

CLI: `sevalla apps processes disk update "$QDRANT_PROCESS_ID" --app-id "$QDRANT_APP_ID" --resource-type "$DISK_10GB" --mount-path /qdrant/storage`. MCP: `sevalla_api_update` with `updateProcessDiskV3`.

**8. Add Qdrant'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 \
  "QDRANT__SERVICE__API_KEY=$QDRANT_API_KEY" \
  "QDRANT__TELEMETRY_DISABLED=true" \
  "QDRANT__STORAGE__SNAPSHOTS_CONFIG__SNAPSHOTS_STORAGE=s3" \
  "QDRANT__STORAGE__SNAPSHOTS_CONFIG__S3_CONFIG__BUCKET=$(echo "$BUCKET" | jq -r .bucket_name)" \
  "QDRANT__STORAGE__SNAPSHOTS_CONFIG__S3_CONFIG__REGION=auto" \
  "QDRANT__STORAGE__SNAPSHOTS_CONFIG__S3_CONFIG__ENDPOINT_URL=$(echo "$BUCKET" | jq -r .endpoint)" \
  "QDRANT__STORAGE__SNAPSHOTS_CONFIG__S3_CONFIG__ACCESS_KEY=$(echo "$BUCKET" | jq -r .access_key)" \
  "QDRANT__STORAGE__SNAPSHOTS_CONFIG__S3_CONFIG__SECRET_KEY=$(echo "$BUCKET" | jq -r .secret_key)"; do
  curl -s -X POST https://api.sevalla.com/v3/applications/$QDRANT_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 "$QDRANT_APP_ID" --key QDRANT__TELEMETRY_DISABLED --value true`, once per variable. MCP: `set_environment_variables`.

**9. Deploy Qdrant**

[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"}}
QDRANT_DEPLOYMENT_ID=$(curl -s -X POST https://api.sevalla.com/v3/applications/$QDRANT_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/$QDRANT_APP_ID/deployments/$QDRANT_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 "$QDRANT_APP_ID"`, `sevalla apps deployments get "$QDRANT_DEPLOYMENT_ID" --app-id "$QDRANT_APP_ID"`. MCP: `trigger_deployment`, `get_deployment`.

**10. Create the client application**

[Create application](https://api-docs.sevalla.com/v3/applications/create-application) (`createApplicationV3`) from the repository, then [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"}}
CATALOG_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: "catalog", cluster_id: $cluster, source: "privateGit", git_type: "github",
      repo_url: "https://github.com/acme/catalog", default_branch: "main"}')" \
  | jq -r .id)
for i in $(seq 1 30); do CATALOG_HOST=$(curl -s https://api.sevalla.com/v3/applications/$CATALOG_APP_ID/domains \
    -H "Authorization: Bearer $SEVALLA_API_KEY" | jq -r '.data[] | select(.is_primary) | .name')
  [ -n "$CATALOG_HOST" ] && break; sleep 2; done
```

CLI: `sevalla apps create --name catalog --cluster "$CLUSTER_ID" --source privateGit --git-type github --repository https://github.com/acme/catalog --branch main`. MCP: `create_application` with `deploy: false`.

**11. Connect the client to Qdrant**

[Create internal connection](https://api-docs.sevalla.com/v3/applications/create-internal-connection) (`createApplicationInternalConnectionV3`). Through the API, a connection adds no variables; the next step adds them.

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s -X POST https://api.sevalla.com/v3/applications/$CATALOG_APP_ID/internal-connections \
  -H "Authorization: Bearer $SEVALLA_API_KEY" -H "Content-Type: application/json" \
  -d "$(jq -n --arg id "$QDRANT_APP_ID" '{target_id: $id, target_type: "app"}')" | jq -r .id
```

CLI: `sevalla apps internal-connections create "$CATALOG_APP_ID" --target-id "$QDRANT_APP_ID" --target-type app`. MCP: `sevalla_api_create` with `createApplicationInternalConnectionV3`.

**12. Add the client's variables**

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

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
for pair in "QDRANT_HOST=$QDRANT_HOST" "QDRANT_URL=http://$QDRANT_HOST:6333" "QDRANT_API_KEY=$QDRANT_API_KEY"; do
  curl -s -X POST https://api.sevalla.com/v3/applications/$CATALOG_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, is_runtime: true, is_buildtime: false}')" \
    | jq -r .key
done
```

CLI: `sevalla apps env-vars create` has no availability option, and its variables are available during the build too. MCP: `set_environment_variables` with `runtime: true, buildtime: false`.

**13. Create the snapshot cron job**

[Create process](https://api-docs.sevalla.com/v3/applications/create-process) (`createProcessV3`). Every process type needs `scaling_strategy`:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
CRON_ID=$(curl -s -X POST https://api.sevalla.com/v3/applications/$CATALOG_APP_ID/processes \
  -H "Authorization: Bearer $SEVALLA_API_KEY" -H "Content-Type: application/json" \
  -d "$(jq -n --arg size "$H1" '{display_name: "Snapshot", type: "cron", entrypoint: "python snapshot.py create",
    resource_type_id: $size, schedule: "0 3 * * *", scaling_strategy: {type: "manual", config: {instanceCount: 1}}}')" \
  | jq -r .id)
```

CLI: `sevalla apps processes create "$CATALOG_APP_ID" --name Snapshot --type cron --resource-type "$H1" --command "python snapshot.py create" --schedule "0 3 * * *"`. MCP: `sevalla_api_create` with `createProcessV3`.

**14. Deploy the client**

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

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
CATALOG_DEPLOYMENT_ID=$(curl -s -X POST https://api.sevalla.com/v3/applications/$CATALOG_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/$CATALOG_APP_ID/deployments/$CATALOG_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 build log with [Get application deployment logs](https://api-docs.sevalla.com/v3/applications/get-application-deployment-logs) (`getApplicationDeploymentLogsV3`). CLI: `sevalla apps deployments trigger "$CATALOG_APP_ID"`. MCP: `trigger_deployment`, `get_deployment`, `get_deployment_logs`.

**15. Check the deployment**

The client reaches Qdrant over the private network:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s https://$CATALOG_HOST/
curl -s https://$CATALOG_HOST/products/1/similar
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
{"products":5}
[{"name":"Wool sweater","score":0.966},{"name":"Hiking boots","score":0.488}]
```

Start one run of the cron job with [Trigger cron job](https://api-docs.sevalla.com/v3/applications/trigger-cron-job) (`triggerProcessCronV3`), and read its status with [List cron runs](https://api-docs.sevalla.com/v3/applications/list-cron-runs) (`listProcessCronRunsV3`) after a minute:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s -X POST https://api.sevalla.com/v3/applications/$CATALOG_APP_ID/processes/$CRON_ID/trigger \
  -H "Authorization: Bearer $SEVALLA_API_KEY" -H "Content-Type: application/json" -d '{}' | jq -r .message
sleep 60
curl -s https://api.sevalla.com/v3/applications/$CATALOG_APP_ID/processes/$CRON_ID/runs \
  -H "Authorization: Bearer $SEVALLA_API_KEY" | jq -r '.data[0].status'
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
Cron job triggered
succeeded
```

CLI: `sevalla apps processes trigger "$CRON_ID" --app-id "$CATALOG_APP_ID"`, `sevalla apps processes runs --app-id "$CATALOG_APP_ID" --process-id "$CRON_ID"`. MCP: `sevalla_api_create` with `triggerProcessCronV3`, `sevalla_api_read` with `listProcessCronRunsV3`.


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