Как установить и настроить Qdrant на VPS
Поднять Qdrant — это одна строка docker run, и почти все инструкции на ней заканчиваются. Дальше выясняется, что база слушает 0.0.0.0:6333 вообще без пароля, дашборд открыт всему интернету, а размерность коллекции выбрана наугад и поменять её уже нельзя. Разбираем установку Qdrant на VPS целиком: развёртывание, production.yaml, ключи и TLS, первая коллекция, снапшоты и обслуживание.
Содержание
- Что такое Qdrant и какие порты он открывает
- Развёртывание: Docker Compose или бинарник под systemd
- Конфигурация: production.yaml и переменные QDRANT__
- Безопасность: API-ключ, TLS и фаервол
- Первая коллекция: размерность, метрика и индексы payload
- Эксплуатация: снапшоты, обновление и наблюдение
- Какой сервер под Qdrant взять в MAATRIX
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →Что такое Qdrant и какие порты он открывает
Qdrant — база данных для векторного поиска на Rust: один бинарник, ни JVM, ни внешних зависимостей. Хранит он не строки таблиц, а точки (points): идентификатор, один или несколько векторов и произвольный JSON-payload рядом.
Граница, из-за которой чаще всего разочаровываются: Qdrant не считает эмбеддинги, внутри сервера нет ни одной модели. Векторы вы приносите готовыми — из sentence-transformers, из API OpenAI, из FastEmbed. Как считать их без видеокарты — в материале про эмбеддинги на CPU.
| Порт | Протокол | Зачем нужен |
|---|---|---|
| 6333 | HTTP REST | API, дашборд /dashboard, /metrics, /healthz |
| 6334 | gRPC | тот же 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 — десятки моделей в одном окне. Оплата картой РФ и по СБП.