MAATRIX / Блог / Как установить и настроить Qdrant на VPS

Как установить и настроить Qdrant на VPS

Как установить и настроить Qdrant на VPS

MAATRIX

Поднять Qdrant — это одна строка docker run, и почти все инструкции на ней заканчиваются. Дальше выясняется, что база слушает 0.0.0.0:6333 вообще без пароля, дашборд открыт всему интернету, а размерность коллекции выбрана наугад и поменять её уже нельзя. Разбираем установку Qdrant на VPS целиком: развёртывание, production.yaml, ключи и TLS, первая коллекция, снапшоты и обслуживание.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →

Что такое Qdrant и какие порты он открывает

Qdrant — база данных для векторного поиска на Rust: один бинарник, ни JVM, ни внешних зависимостей. Хранит он не строки таблиц, а точки (points): идентификатор, один или несколько векторов и произвольный JSON-payload рядом.

Граница, из-за которой чаще всего разочаровываются: Qdrant не считает эмбеддинги, внутри сервера нет ни одной модели. Векторы вы приносите готовыми — из sentence-transformers, из API OpenAI, из FastEmbed. Как считать их без видеокарты — в материале про эмбеддинги на CPU.

ПортПротоколЗачем нужен
6333HTTP RESTAPI, дашборд /dashboard, /metrics, /healthz
6334gRPCтот же API, быстрее на больших батчах
6335внутреннийконсенсус Raft в кластере

Порт 6335 нужен только в кластере, наружу его не выставляют; 6334 пригодится при массовой заливке. Начинать проще с 6333 — там дашборд и проверка версии:

curl -s http://127.0.0.1:6333/ | jq
{
  "title": "qdrant - vector search engine",
  "version": "1.15.1",
  "commit": "b7a2c1f0d3e9c4a5..."
}

При старте Qdrant печатает ASCII-логотип, версию и строку Access web UI at http://localhost:6333/dashboard. Строки нет, а процесс вроде бы запущен — значит, упал раньше: на конфиге или на правах к хранилищу.

Хранилище лежит в /qdrant/storage, снапшоты — в /qdrant/snapshots. Обе директории обязаны переживать пересоздание контейнера, иначе docker compose down -v унесёт базу вместе с индексом.

Развёртывание: Docker Compose или бинарник под systemd

Заказали сервер в MAATRIX с приложением Qdrant из каталога apps.maatrix.io — разворачивать нечего: база поднята, адрес и ключ ждут в кабинете, в разделе «Доступ». Дальше — для тех, кто ставит сам.

Docker Compose предпочтителен: образ собирают разработчики, обновление сводится к смене тега.

services:
  qdrant:
    image: qdrant/qdrant:v1.15.1
    container_name: qdrant
    restart: unless-stopped
    ports:
      - "127.0.0.1:6333:6333"
      - "127.0.0.1:6334:6334"
    volumes:
      - ./storage:/qdrant/storage
      - ./snapshots:/qdrant/snapshots
      - ./production.yaml:/qdrant/config/production.yaml:ro
    environment:
      QDRANT__SERVICE__API_KEY: ${QDRANT_API_KEY}
      QDRANT__TELEMETRY_DISABLED: "true"
    ulimits:
      nofile:
        soft: 65535
        hard: 65535

Три вещи здесь неслучайны. Тег зафиксирован: latest подтянет мажорное обновление не вовремя, а откатиться Qdrant не даст. Порты привязаны к 127.0.0.1, наружу база смотрит через Nginx. ulimits.nofile поднят: движок держит открытыми файлы сегментов и mmap-области и упирается в лимит 1024 с os error 24.

На портах обжигаются постоянно. Запись -p 6333:6333 без адреса публикует порт на всех интерфейсах, и Docker при этом обходит UFW: правило DNAT попадает в цепочку DOCKER таблицы nat раньше ваших ufw deny.

sudo iptables -t nat -L DOCKER -n | grep 6333
# DNAT tcp -- 0.0.0.0/0  0.0.0.0/0  tcp dpt:6333 to:172.18.0.2:6333

Строка без префикса 127.0.0.1 значит, что база открыта интернету, что бы ни показывал ufw status.

Бинарник под systemd оправдан, когда Docker нежелателен. Его берут из релизов на GitHub, кладут в /opt/qdrant, конфиг — в /etc/qdrant.

[Unit]
Description=Qdrant vector database
After=network-online.target

[Service]
Type=simple
User=qdrant
Group=qdrant
WorkingDirectory=/opt/qdrant
EnvironmentFile=/etc/qdrant/qdrant.env
ExecStart=/opt/qdrant/qdrant --config-path /etc/qdrant/production.yaml
LimitNOFILE=65535
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

Подводный камень: без --config-path бинарник ищет каталог config/ относительно рабочего каталога процесса. Из домашней директории всё работает, а systemd без WorkingDirectory стартует из /, конфига не находит и берёт умолчания — включая отсутствие ключа.

Развернуть за пару минут

Готовый образ на VPS MAATRIX: NVMe, AMD EPYC, root-доступ. Локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Развернуть Qdrant

Конфигурация: production.yaml и переменные QDRANT__

Настройки читаются слоями: встроенный config/config.yaml с умолчаниями, затем config/{RUN_MODE}.yaml — в официальном Docker-образе RUN_MODE=production, поэтому подхватывается config/production.yaml. Дальше config/local.yaml и переменные окружения, перебивающие всё.

Именование механическое: префикс QDRANT__, уровни вложенности разделяются двойным подчёркиванием, и service.api_key превращается в QDRANT__SERVICE__API_KEY. Здесь чаще всего ошибаются: QDRANT_SERVICE_API_KEY с одиночными подчёркиваниями игнорируется, и база стартует без авторизации.

log_level: INFO
telemetry_disabled: true

service:
  host: 127.0.0.1
  http_port: 6333
  grpc_port: 6334
  max_request_size_mb: 64

storage:
  storage_path: /qdrant/storage
  snapshots_path: /qdrant/snapshots
  on_disk_payload: true
  performance:
    max_search_threads: 0
    max_optimization_threads: 1
  optimizers:
    indexing_threshold_kb: 20000
    memmap_threshold_kb: 200000
  wal:
    wal_capacity_mb: 32

Что здесь стоит понимать:

  • on_disk_payload: true — payload не держится в памяти, из RAM читаются только векторы и граф: на длинных текстах это экономит гигабайты ценой обращения к диску при with_payload: true.
  • memmap_threshold_kb — с какого размера сегмент переезжает в memory-mapped файл. Порог около 200 МБ разумен для скромной по памяти машины: горячее остаётся в page cache.
  • indexing_threshold_kb: 20000 — до 20 МБ сегмент ищется перебором, HNSW не строится: на маленькой коллекции это быстрее графа.
  • max_search_threads: 0 значит «по числу ядер»; на двух ядрах, где рядом живёт веб-приложение, ставьте 1.
  • max_request_size_mb по умолчанию 32: батч из тысячи точек с длинными payload получит 413 вместо вставки.

Что применилось на самом деле, показывает телеметрия:

curl -s -H "api-key: $QDRANT_API_KEY" \
  'http://127.0.0.1:6333/telemetry?details_level=3' | jq '.result.app'

Не совпало — конфиг не подхвачен: проверьте монтирование через docker compose exec qdrant cat /qdrant/config/production.yaml.

Безопасность: API-ключ, TLS и фаервол

Аутентификации в Qdrant по умолчанию нет вообще. Ключ не задан — любой, кто дотянется до порта 6333, читает ваши коллекции и удаляет их одним DELETE. Открытые векторные базы находят сканерами и вычищают, а бэкапа у большинства нет.

Ключ генерируют и кладут в конфиг:

openssl rand -hex 32
service:
  api_key: ${QDRANT_API_KEY}
  read_only_api_key: ${QDRANT_RO_KEY}

Второй ключ, read_only_api_key, недооценён: сервис, который только ищет, должен ходить с ним. Передаётся ключ заголовком api-key, отказы выглядят так:

$ curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:6333/collections
401
$ curl -s http://127.0.0.1:6333/collections
Must provide an API key or an Authorization bearer token

401 значит, что ключ не передан вовсе; передан, но не тот — придёт 403. Различие бесценно при отладке: 401 — «заголовок потерялся по дороге», 403 — «дошёл, значение неверное».

С версии 1.11 есть JWT-токены с правами только на чтение и ограничением по коллекциям (service.jwt_rbac: true при заданном api_key); подписаны они мастер-ключом, и его смена инвалидирует все разом. В 1.13 добавили строгий режим strict_mode_config.

TLS проще отдать Nginx: свои сертификаты движок умеет (service.enable_tls: true плюс секция tls), но обновлять их придётся отдельно, с перезапуском после каждого продления Let's Encrypt.

server {
    listen 443 ssl;
    http2 on;
    server_name qdrant.example.com;

    ssl_certificate     /etc/letsencrypt/live/qdrant.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/qdrant.example.com/privkey.pem;

    client_max_body_size 128m;
    proxy_read_timeout 300s;

    location / {
        proxy_pass http://127.0.0.1:6333;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

client_max_body_size обязателен: умолчание Nginx — 1 МБ, и батч на несколько тысяч векторов отобьётся с 413 ещё до базы. gRPC через этот блок не проксируется — для 6334 нужен отдельный сервер с grpc_pass. Фаервол: ufw allow 22/tcp, ufw allow 443/tcp, ufw enable.

Дашборд на /dashboard — не панель администратора с ролями, а окно к API: он спрашивает ключ и кладёт его в хранилище браузера. Дать по нему доступ подрядчику — то же, что отдать мастер-ключ; ходите через SSH-туннель ssh -L 6333:127.0.0.1:6333 user@сервер.

Первая коллекция: размерность, метрика и индексы payload

Коллекция создаётся одним PUT, и два параметра в нём определяют судьбу проекта.

curl -s -X PUT http://127.0.0.1:6333/collections/docs \
  -H "api-key: $QDRANT_API_KEY" -H 'Content-Type: application/json' \
  -d '{
    "vectors": {"size": 1024, "distance": "Cosine"},
    "hnsw_config": {"m": 16, "ef_construct": 100},
    "optimizers_config": {"indexing_threshold": 20000}
  }'
# {"result":true,"status":"ok","time":0.031}

Размерность (size) обязана точно совпадать с моделью эмбеддингов: 768 для bge-base и e5-base, 1024 для bge-m3 и e5-large, 1536 для text-embedding-3-small, 3072 для text-embedding-3-large. Ошиблись — узнаете при первой вставке:

{"status":{"error":"Wrong input: Vector inserting error: expected dim: 768, got 1536"},"time":0.0}

Честное ограничение: изменить размерность существующей коллекции нельзя — только создать новую и залить заново, то есть пересчитать эмбеддинги. Поэтому модель выбирают до создания коллекции и записывают её имя в payload.

Метрика (distance) должна соответствовать модели. У нормализованных векторов Cosine и Dot дают одинаковый порядок, но Dot дешевле. Если векторы не нормализованы, Dot выдаст длинные вместо близких по смыслу: поиск не сломается с ошибкой, он тихо станет неправильным. Начинайте с Cosine.

Без индексов payload фильтр вроде «только документы этого клиента» превращается в перебор:

curl -s -X PUT http://127.0.0.1:6333/collections/docs/index \
  -H "api-key: $QDRANT_API_KEY" -H 'Content-Type: application/json' \
  -d '{"field_name":"tenant_id","field_schema":{"type":"keyword","is_tenant":true}}'

Флаг is_tenant: true — правильный способ жить с мультиарендностью: Qdrant раскладывает точки одного арендатора рядом, и поиск с фильтром не растекается по индексу. Плодить по коллекции на клиента — частая ошибка: каждая несёт накладные расходы.

Поиск с версии 1.10 делают через POST /collections/{name}/points/query; старый /points/search ещё отвечает, но помечен устаревшим.

curl -s -X POST http://127.0.0.1:6333/collections/docs/points/query \
  -H "api-key: $QDRANT_API_KEY" -H 'Content-Type: application/json' \
  -d '{"query":[0.02,-0.11,0.34],"limit":5,"with_payload":true,
       "filter":{"must":[{"key":"tenant_id","match":{"value":"acme"}}]}}'

При массовой первой заливке выключите построение графа — indexing_threshold: 0 перед загрузкой и возврат к 20000 после, иначе Qdrant будет перестраивать HNSW параллельно вставке. Состояние коллекции:

curl -s -H "api-key: $QDRANT_API_KEY" http://127.0.0.1:6333/collections/docs \
  | jq '.result | {status, points_count, indexed_vectors_count, optimizer_status}'

Поле status: green (готово), yellow (оптимизаторы работают, поиск отвечает), grey (оптимизации приостановлены), red (сегмент сломан). Долгое yellow при optimizer_status: "ok" нормально после большой заливки; red — повод разворачивать снапшот.

Эксплуатация: снапшоты, обновление и наблюдение

Бэкап делают снапшотами, а не копированием каталога. Копия storage/ с работающего процесса почти наверняка неконсистентна: сегменты и WAL живут своей жизнью.

curl -s -X POST -H "api-key: $QDRANT_API_KEY" \
  http://127.0.0.1:6333/collections/docs/snapshots | jq '.result'
{
  "name": "docs-3241755230098001-2026-08-28-09-14-22.snapshot",
  "creation_time": "2026-08-28T09:14:22",
  "size": 412839424
}

Файл ложится в snapshots_path; забирают его через GET /collections/docs/snapshots/{name} и увозят на другой хост — на том же диске снапшот не спасёт. Снимок всего хранилища — POST /snapshots. Восстановление:

curl -X POST -H "api-key: $QDRANT_API_KEY" \
  'http://127.0.0.1:6333/collections/docs/snapshots/upload?priority=snapshot' \
  -F 'snapshot=@docs-3241755230098001-2026-08-28-09-14-22.snapshot'

Снапшоты не чистятся сами: крон без ротации за месяц забьёт диск, а при нехватке места Qdrant падает.

Обновление — смена тега и docker compose up -d, с двумя оговорками: снапшот до обновления и понимание, что даунгрейда нет. Новая версия может обновить формат хранилища, и старый бинарник эти файлы не прочитает.

Наблюдение. Эндпоинты /healthz, /livez и /readyz отвечают коротким текстом, метрики Prometheus отдаются на /metrics без экспортёра.

curl -s http://127.0.0.1:6333/healthz
# healthz check passed
curl -s -H "api-key: $QDRANT_API_KEY" http://127.0.0.1:6333/metrics | grep -E '^collections'

Следите за OOM: при нехватке памяти Qdrant не деградирует, ядро просто убивает процесс.

docker inspect qdrant --format '{{.State.OOMKilled}} {{.State.ExitCode}}'
# true 137

Код 137 и OOMKilled: true значат, что памяти не хватило: нужно больше RAM, квантование или memmap_threshold_kb пониже. Если база не стартует из-за битой коллекции, помогает QDRANT_ALLOW_RECOVERY_MODE=true — сервис поднимется в урезанном режиме, где её можно удалить. Остальные аварии — в разборе частых ошибок Qdrant, а просадки времени ответа — в медленном поиске по векторам.

Какой сервер под Qdrant взять в MAATRIX

Считаем по формуле из документации Qdrant: память ≈ число векторов × размерность × 4 байта × 1.5, где 4 байта — это float32, а 1.5 закрывает граф HNSW. База знаний на 200 000 фрагментов с моделью на 1024 измерения: 200 000 × 1024 × 4 × 1.5 ≈ 1,2 ГБ. Миллион фрагментов на text-embedding-3-small (1536) — уже ≈ 9,2 ГБ, а скалярное квантование в int8 уменьшает векторную часть вчетверо, до 2,3 ГБ, ценой небольшой потери точности. Сверху — ОС и page cache под payload; подробный расчёт в статье сколько RAM нужно для Qdrant.

Честный минимум: 2 vCPU, 4 ГБ RAM, 60 ГБ NVMe. Хватает на коллекцию до 300–400 тысяч точек умеренной размерности с on_disk_payload: true. Ограничение называю прямо: считать на этой же машине эмбеддинги локальной моделью не стоит — sentence-transformers съест два-три гигабайта, и OOM-killer выберет не в вашу пользу. На 2 ГБ первая серьёзная заливка закончится кодом 137.

Комфортный вариант: 4 vCPU, 8–16 ГБ RAM, 100–160 ГБ NVMe. Помещаются миллион векторов, локальный эмбеддер, Nginx с TLS и место под снапшоты — по объёму они сопоставимы с коллекцией, так что диск планируйте с двойным запасом. Дальше растут по памяти, а не по ядрам: поиск упирается в RAM и скорость диска задолго до процессора.

Локация — UK, Лондон. Причин три. Пинг до пользователей и серверов в ЕС — единицы и первые десятки миллисекунд, а в RAG-конвейере запрос ходит в базу не один раз за ответ. С британского адреса штатно работают API эмбеддингов OpenAI, Cohere и Voyage — российская локация для этого сценария не годится из-за региональных ограничений. И GDPR-соседство: вопрос «где физически лежат данные» европейские клиенты задают на первой встрече. Франция уместна по тем же причинам, США — если нагрузка идёт из Америки, Россия — при персональных данных по 152-ФЗ.

Заказ занимает несколько минут: выбираете локацию и конфигурацию, отмечаете в каталоге apps.maatrix.io приложение Qdrant — оно ставится автоматически при развёртывании сервера, на Ubuntu и на Debian. Адрес и ключи появятся в личном кабинете, в разделе «Доступ»: останется положить свой production.yaml и создать коллекцию. Оплата — картами российских банков, по СБП, криптой или токеном MAAT; иностранная карта не нужна.

Развернуть за пару минут

Готовый образ на VPS MAATRIX: NVMe, AMD EPYC, root-доступ. Локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Развернуть Qdrant

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →

Частые вопросы

Можно ли поменять размерность вектора у существующей коллекции?

Нет: она фиксируется при создании, и вставка вектора другой длины возвращает Wrong input: Vector inserting error: expected dim: 768, got 1536. Путь один — новая коллекция с нужным size, переиндексация и переключение алиаса через POST /collections/aliases.

Нужен ли Qdrant GPU?

Нет: поиск по HNSW — работа процессора, памяти и диска. GPU пригодится на соседнем шаге конвейера, при вычислении эмбеддингов локальной моделью.

Как понять, что база открыта наружу?

Постучитесь на публичный адрес с другой машины: curl -m 5 http://ВАШ_IP:6333/collections. Список коллекций вместо таймаута или 401 означает, что защиты нет: порт должен публиковаться как 127.0.0.1:6333:6333, а ключ задаваться переменной QDRANT__SERVICE__API_KEY — с двойным подчёркиванием.

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.