Getting Started with Qdrant Vector Database on Ubuntu

Written by: Bagus Facsi Aginsa
Published at: 24 May 2026


If you have been reading about vector databases and wondering where to actually start, Qdrant is a solid first choice. It is fast, ships as a single Docker container, and has a well-designed REST API you can explore with nothing more than curl. No Python SDK, no config files, just HTTP calls.

This tutorial covers everything you need to go from zero to a working Qdrant instance on Ubuntu. You will install Qdrant using Docker, learn what collections and points are, create your first collection, insert some vectors with metadata, and run similarity searches and filtered queries using curl. By the end you will have enough hands-on experience to understand how a vector database actually works before you start wiring it into application code.


What is Qdrant and Why Should You Care

A traditional database finds records by matching values exactly or within a range. You search for status = 'active' or age BETWEEN 25 AND 40. That works great for structured data.

A vector database takes a different approach. Instead of storing values, it stores vectors, arrays of floating-point numbers that represent the meaning or characteristics of something. A sentence, an image, a product, a log entry can all be converted into a vector by a model. Once stored, you can find the most similar items by calculating which stored vectors are mathematically closest to your query vector.

This is what powers semantic search (where “how do I restart the service” finds the document titled “service restart procedure”), recommendation engines, duplicate detection, and anomaly detection.

Qdrant specifically is a vector database written in Rust. It is known for:

  • Being memory-efficient and fast under load
  • Supporting payload filtering, combining vector similarity with structured conditions in one query
  • Providing a clean REST API with a built-in dashboard
  • Persisting data to disk out of the box
  • Running comfortably on a single machine alongside other services

Prerequisites

Before starting, make sure you have:

  • Ubuntu 20.04, 22.04, or 24.04
  • Docker installed, check with docker --version
  • curl and jq installed for making and reading API calls
  • A user with sudo privileges
  • At least 1 GB of free RAM

Install jq if you do not have it:

sudo apt update && sudo apt install -y jq

If Docker is not installed yet, the quickest way on Ubuntu:

sudo apt install -y ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
  | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update && sudo apt install -y docker-ce docker-ce-cli containerd.io
sudo usermod -aG docker $USER
newgrp docker

Step 1: Run Qdrant with Docker

Create a local directory to hold Qdrant’s data so your collections and vectors survive container restarts:

mkdir -p ~/qdrant/storage

Start the Qdrant container:

docker run -d \
  --name qdrant \
  --restart unless-stopped \
  -p 6333:6333 \
  -p 6334:6334 \
  -v ~/qdrant/storage:/qdrant/storage \
  qdrant/qdrant:latest

What each flag does:

  • -d,runs the container in the background
  • --restart unless-stopped, Qdrant restarts automatically when Docker starts (e.g., after a server reboot), unless you manually stop it
  • -p 6333:6333,the REST API port
  • -p 6334:6334,the gRPC port (not used in this tutorial)
  • -v ~/qdrant/storage:/qdrant/storage, mounts your local directory into the container so data is persisted on the host

Check that the container started:

docker ps

You should see the qdrant container in the output with status Up.

Now confirm the REST API is responding:

curl -s http://localhost:6333/healthz

Expected output:

healthz check passed

You can also open the Qdrant dashboard in a browser at http://localhost:6333/dashboard. It is a built-in UI for browsing your collections and running queries visually, useful for debugging.


Step 2: Understand the Core Concepts

Before touching the API, take two minutes to understand Qdrant’s data model.

Collection, the top-level container, similar to a table in a relational database. When you create a collection you declare:

  • The vector size (dimension), must match the output size of the embeddings model that you used
  • The distance metric, how similarity is measured. Cosine is standard for text and most ML models. Dot is common for recommendation systems. Euclid is used for geometric distances.

Point is a single record inside a collection. Each point has three parts:

  • id, a unique identifier, either an integer or a UUID string
  • vector, the actual array of floats representing the item
  • payload, an arbitrary JSON object you attach to the point. This is where you store the original text, a category, a URL, a timestamp, or whatever metadata you need.

Search. you provide a query vector and Qdrant returns the N points whose stored vectors are closest to it, ranked by similarity score.

The payload is what separates a vector database from a plain k-NN index. You can filter search results by payload fields, for example, return only the 5 closest vectors where category == "runbook". Qdrant applies the filter efficiently without post-processing.


Step 3: Create a Collection

In this tutorial you will work with simple 4-dimensional vectors. Real embedding models produce 768 or 1536 dimensions, but 4 is enough to understand the mechanics without generating actual embeddings.

Create a collection called articles:

curl -s -X PUT http://localhost:6333/collections/articles \
  -H "Content-Type: application/json" \
  -d '{
    "vectors": {
      "size": 4,
      "distance": "Cosine"
    }
  }' | jq

Expected response:

{
  "result": true,
  "status": "ok",
  "time": 0.012
}

Verify the collection was created:

curl -s http://localhost:6333/collections/articles | jq

You will see information about the collection including its vector configuration, point count (currently 0), and status.

To list all collections on this Qdrant instance:

curl -s http://localhost:6333/collections | jq

Step 4: Insert Points (Upsert)

Now insert some points into the articles collection. Each point has an ID, a 4-dimensional vector, and a payload with the original text and a category.

Insert five points with a single request:

curl -s -X PUT http://localhost:6333/collections/articles/points \
  -H "Content-Type: application/json" \
  -d '{
    "points": [
      {
        "id": 1,
        "vector": [0.1, 0.9, 0.2, 0.8],
        "payload": {
          "text": "How to restart the nginx web server on Ubuntu",
          "category": "runbook"
        }
      },
      {
        "id": 2,
        "vector": [0.15, 0.85, 0.25, 0.75],
        "payload": {
          "text": "Restarting systemd services and checking service status",
          "category": "runbook"
        }
      },
      {
        "id": 3,
        "vector": [0.8, 0.1, 0.7, 0.15],
        "payload": {
          "text": "Introduction to Kubernetes deployments and pods",
          "category": "tutorial"
        }
      },
      {
        "id": 4,
        "vector": [0.82, 0.12, 0.68, 0.18],
        "payload": {
          "text": "Kubernetes scaling with HorizontalPodAutoscaler",
          "category": "tutorial"
        }
      },
      {
        "id": 5,
        "vector": [0.05, 0.92, 0.1, 0.88],
        "payload": {
          "text": "Checking nginx error logs and access logs",
          "category": "runbook"
        }
      }
    ]
  }' | jq

Expected response:

{
  "result": {
    "operation_id": 0,
    "status": "completed"
  },
  "status": "ok",
  "time": 0.005
}

The operation is called upsert, if a point with the same ID already exists, it is overwritten. If it does not exist, it is inserted. This makes it safe to re-index updated content without deduplication logic.

Confirm the points were stored:

curl -s "http://localhost:6333/collections/articles/points/count" | jq

You should see "count": 5.


Step 5: Retrieve a Single Point

You can retrieve any point by its ID:

curl -s http://localhost:6333/collections/articles/points/1 | jq

Expected output:

{
  "result": {
    "id": 1,
    "version": 0,
    "score": null,
    "payload": {
      "text": "How to restart the nginx web server on Ubuntu",
      "category": "runbook"
    },
    "vector": [0.1, 0.9, 0.2, 0.8]
  },
  "status": "ok",
  "time": 0.001
}

You can also retrieve multiple points by ID in one call:

curl -s -X POST http://localhost:6333/collections/articles/points \
  -H "Content-Type: application/json" \
  -d '{"ids": [1, 3, 5]}' | jq

This is the core operation. You provide a query vector and Qdrant returns the closest stored vectors.

Search for the 3 most similar points to a query vector that is close to the “runbook” cluster:

curl -s -X POST http://localhost:6333/collections/articles/points/search \
  -H "Content-Type: application/json" \
  -d '{
    "vector": [0.12, 0.88, 0.22, 0.78],
    "limit": 3,
    "with_payload": true
  }' | jq

Expected output:

{
  "result": [
    {
      "id": 2,
      "version": 0,
      "score": 0.9998,
      "payload": {
        "text": "Restarting systemd services and checking service status",
        "category": "runbook"
      }
    },
    {
      "id": 1,
      "version": 0,
      "score": 0.9993,
      "payload": {
        "text": "How to restart the nginx web server on Ubuntu",
        "category": "runbook"
      }
    },
    {
      "id": 5,
      "version": 0,
      "score": 0.9985,
      "payload": {
        "text": "Checking nginx error logs and access logs",
        "category": "runbook"
      }
    }
  ],
  "status": "ok",
  "time": 0.001
}

The score field is the cosine similarity between the query vector and each stored vector. Scores close to 1.0 mean nearly identical direction (very similar). Scores near 0 mean unrelated. Scores near -1.0 mean opposite.

Notice the Kubernetes points (IDs 3 and 4) did not appear in the top 3, their vectors are in a very different direction from the query vector.


Step 7: Filter Search Results by Payload

Payload filtering lets you combine vector similarity with structured conditions. You can limit results to specific categories, date ranges, authors, or any other field in the payload.

Search for similar documents but only within the tutorial category:

curl -s -X POST http://localhost:6333/collections/articles/points/search \
  -H "Content-Type: application/json" \
  -d '{
    "vector": [0.12, 0.88, 0.22, 0.78],
    "limit": 3,
    "with_payload": true,
    "filter": {
      "must": [
        {
          "key": "category",
          "match": {
            "value": "tutorial"
          }
        }
      ]
    }
  }' | jq

Now Qdrant only returns points where category == "tutorial", even though those vectors are farther from the query:

{
  "result": [
    {
      "id": 3,
      "score": 0.6243,
      "payload": {
        "text": "Introduction to Kubernetes deployments and pods",
        "category": "tutorial"
      }
    },
    {
      "id": 4,
      "score": 0.6018,
      "payload": {
        "text": "Kubernetes scaling with HorizontalPodAutoscaler",
        "category": "tutorial"
      }
    }
  ],
  "status": "ok",
  "time": 0.001
}

The must array works like SQL’s AND, all conditions must be satisfied. You can also use should (OR logic) and must_not (exclusion). For example, to exclude runbooks:

curl -s -X POST http://localhost:6333/collections/articles/points/search \
  -H "Content-Type: application/json" \
  -d '{
    "vector": [0.12, 0.88, 0.22, 0.78],
    "limit": 3,
    "with_payload": true,
    "filter": {
      "must_not": [
        {
          "key": "category",
          "match": {"value": "runbook"}
        }
      ]
    }
  }' | jq

Step 8: Update and Delete Points

Update a point’s payload without changing its vector:

curl -s -X POST http://localhost:6333/collections/articles/points/payload \
  -H "Content-Type: application/json" \
  -d '{
    "payload": {"reviewed": true},
    "points": [1, 2]
  }' | jq

This adds "reviewed": true to the payload of points 1 and 2 without touching their vectors. Useful when metadata changes but the content does not.

Delete a single point:

curl -s -X POST http://localhost:6333/collections/articles/points/delete \
  -H "Content-Type: application/json" \
  -d '{"points": [5]}' | jq

Delete an entire collection (destructive, deletes all points and the collection definition):

curl -s -X DELETE http://localhost:6333/collections/articles | jq

Step 9: Create a Snapshot (Backup)

Qdrant has a built-in snapshot API for backups. A snapshot is a self-contained file you can move to another server and restore.

Create a snapshot of the articles collection:

curl -s -X POST http://localhost:6333/collections/articles/snapshots | jq

The response includes a name field. The snapshot file is stored inside the container at /qdrant/storage/snapshots/. Because you mounted ~/qdrant/storage as the volume, the file is also accessible on the host:

ls ~/qdrant/storage/snapshots/

To restore a snapshot on another Qdrant instance:

curl -X POST "http://localhost:6333/collections/articles/snapshots/upload" \
  -H "Content-Type: multipart/form-data" \
  -F "snapshot=@/path/to/articles-snapshot-name.snapshot"

Common Mistakes and Troubleshooting

Container exits immediately, this is almost always a permissions problem on the storage volume. Fix it with:

sudo chown -R 1000:1000 ~/qdrant/storage
docker restart qdrant
docker logs qdrant

curl returns “collection not found”, collection names are case-sensitive. Articles and articles are different collections.

Dimension mismatch error on insert, you declared the collection with size: 4 but are inserting a vector with a different number of elements. The vector dimension on insert must exactly match what was set at collection creation. Drop and recreate the collection if you need to change dimensions.

Search returns no results, confirm you inserted points first with curl -s http://localhost:6333/collections/articles/points/count. Also make sure with_payload: true is set if you expect payload fields in the response.

Port 6333 already in use, another process is on that port. Find it with sudo ss -tlnp | grep 6333 and stop it, or change the host port mapping in the Docker run command, e.g., -p 6335:6333.


Best Practices

Create a payload index for fields you filter on frequently. Without an index, Qdrant scans every point’s payload to apply filters. With an index it is much faster on large collections:

curl -s -X PUT http://localhost:6333/collections/articles/index \
  -H "Content-Type: application/json" \
  -d '{
    "field_name": "category",
    "field_schema": "keyword"
  }' | jq

Use UUIDs instead of sequential integers for point IDs in production. Sequential IDs make it easy to accidentally overwrite a point when indexing from multiple sources. Generate a UUID per document and use it consistently.

Do not expose port 6333 to the public internet without authentication. Qdrant has no built-in authentication by default. If you need external access, put it behind Nginx with HTTP basic auth, or restrict access via ufw:

sudo ufw deny 6333
sudo ufw allow from 10.0.0.0/8 to any port 6333

Take regular snapshots. The mounted volume protects you from container-level loss, but does not protect against accidental collection deletion. Schedule a cron job to call the snapshot API nightly and copy the files to a separate location.

Match your vector size to your embedding model. When you move beyond manual vectors to a real embedding model like nomic-embed-text (768 dimensions) or OpenAI text-embedding-3-small (1536 dimensions), the size you declare at collection creation must match the model output exactly. A mismatch means no data can be inserted.


Conclusion

You now have Qdrant running on Ubuntu and know how to operate it entirely through the REST API. You created a collection, inserted points with vector and payload data, ran similarity searches, applied payload filters, updated and deleted points, and created a backup snapshot, all with curl.

The fundamentals you practiced here translate directly to any language SDK. The Qdrant Python client, Node.js client, and Go client are all thin wrappers around the same REST API you just used by hand.

From here, the natural next step is generating real embeddings instead of manually crafted vectors. If you have Ollama running locally, you can pull the nomic-embed-text model and call its embedding endpoint to convert actual text into vectors before inserting them into Qdrant. That is when the similarity scores stop being theoretical and start being genuinely useful and you already know exactly which API to call on the Qdrant side.