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

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

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

MAATRIX

Chroma ставится одной строкой pip install chromadb — и ровно поэтому её чаще всего запускают неправильно. Сначала она живёт библиотекой внутри приложения, потом её переносят на сервер, и там она слушает 0.0.0.0:8000 без пароля, отвечает старым клиентам кодом 410 и падает на первом же батче в десять тысяч записей. Разберём установку Chroma на VPS целиком: режимы работы, Docker Compose и systemd, токен с TLS, размерность коллекции, реальные лимиты и бэкап.

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

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

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

Chroma в двух режимах: библиотека и сервер

Chroma — открытая векторная база на Python-обвязке с ядром на Rust (переписано в релизе 1.0). Она хранит документы, эмбеддинги и метаданные рядом, ищет по косинусу или евклиду с фильтрами вида «только там, где lang = ru». Тексты и метаданные лежат в SQLite, векторный индекс — в бинарных файлах HNSW.

Работать с ней можно тремя способами:

  • chromadb.EphemeralClient() — всё в оперативной памяти, после перезапуска пусто. Только для тестов.
  • chromadb.PersistentClient(path="/var/lib/chroma") — встроенный режим: файлы на диске, но внутри вашего процесса, отдельного сервиса нет.
  • chromadb.HttpClient(host=..., port=8000) — клиент-серверный режим: отдельный процесс chroma run и HTTP API.

Встроенный режим подкупает простотой, но каталог базы открывается на запись только одним процессом: два воркера Gunicorn плюс cron-скрипт индексации дадут sqlite3.OperationalError: database is locked или разъехавшийся индекс.

Порт по умолчанию один — 8000, HTTP.

Второе, что ломает интеграции, — смена версии API: с релиза 1.0 (апрель 2025) пути /api/v1/... убраны. Старый клиент получит от нового сервера HTTP 410 и такое тело:

{"error":"Unimplemented","message":"The v1 API is deprecated. Please use /v2 apis"}

Живой сервер проверяется без всякого SDK:

curl -s http://127.0.0.1:8000/api/v2/heartbeat
curl -s http://127.0.0.1:8000/api/v2/version
{"nanosecond heartbeat":1756370411123456789}
"1.5.9"

В v2 появились арендаторы и базы: по умолчанию default_tenant и default_database, и они же лягут в URL.

И третья граница: эмбеддинги Chroma не считает — это отдельный разговор, ниже.

Установка на VPS: Docker Compose или venv под systemd

Ставим на чистую Ubuntu 24.04, подходит и Debian 12. Контейнер экономит нервы: образ уже собран с нужной версией SQLite и chroma-hnswlib.

Вариант с Docker Compose. Каталог /opt/chroma, файл compose.yml:

services:
  chroma:
    image: chromadb/chroma:latest
    container_name: chroma
    restart: unless-stopped
    ports:
      - "127.0.0.1:8000:8000"
    volumes:
      - /var/lib/chroma:/data
    environment:
      - ANONYMIZED_TELEMETRY=FALSE
      - IS_PERSISTENT=TRUE

Три вещи здесь важнее прочих.

  • 127.0.0.1:8000:8000, а не 8000:8000. Docker правит iptables в обход UFW: на всех интерфейсах база окажется в интернете, даже если ufw status пишет deny 8000.
  • Точка монтирования /data. В образах 1.x каталог данных именно такой, в 0.4–0.5 он назывался /chroma/chroma. Старый том по привычному пути после обновления не подхватится — сервер поднимется с пустой базой молча, без единой ошибки в логе.
  • Тег latest в проде — плохая идея. Узнайте фактический номер через /api/v2/version и пропишите его явно, например chromadb/chroma:1.5.9. Клиент и сервер держите одной серии, иначе вернётся та самая ошибка Unimplemented.
cd /opt/chroma && docker compose up -d
docker compose logs -f chroma | head -20

Вариант без Docker — venv под systemd. Пригодится, если контейнеров на машине больше не планируется.

apt update && apt install -y python3-venv python3-dev build-essential
useradd -r -s /usr/sbin/nologin -d /var/lib/chroma chroma
mkdir -p /var/lib/chroma /opt/chroma && chown -R chroma:chroma /var/lib/chroma
python3 -m venv /opt/chroma/venv
/opt/chroma/venv/bin/pip install -U pip "chromadb==1.5.*"

Здесь ждут три типичных отказа. Первый — попытка поставить пакет системным pip на Ubuntu 24.04:

error: externally-managed-environment
× This environment is externally managed

Это PEP 668 — обходить --break-system-packages не надо, venv решает вопрос сам. Второй отказ — нет компилятора: error: command 'gcc' failed: No such file or directory на chroma-hnswlib, лечится build-essential с python3-dev. На старых дистрибутивах (Debian 11 и его SQLite 3.34) добавится третий: RuntimeError: Your system has an unsupported version of sqlite3. Chroma requires sqlite3 >= 3.35.0. — лечится pip install pysqlite3-binary с подменой модуля или переходом на контейнер.

Юнит /etc/systemd/system/chroma.service:

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

[Service]
User=chroma
Group=chroma
WorkingDirectory=/var/lib/chroma
Environment=ANONYMIZED_TELEMETRY=FALSE
ExecStart=/opt/chroma/venv/bin/chroma run --path /var/lib/chroma --host 127.0.0.1 --port 8000
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

Дальше systemctl daemon-reload && systemctl enable --now chroma. На стороне приложения тяжёлый пакет не нужен: pip install chromadb-client ставит тонкий HTTP-клиент без onnxruntime и tokenizers.

Нужен сервер под эту задачу?

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

Арендовать сервер

Закрываем доступ: токен, htpasswd и Nginx с TLS

По умолчанию аутентификации в Chroma нет вообще: любой, кто дотянулся до порта 8000, читает все коллекции и вызывает delete_collection(). Пока сервер привязан к 127.0.0.1, это терпимо; как только к нему пойдёт другой хост, нужен токен.

    environment:
      - CHROMA_SERVER_AUTHN_PROVIDER=chromadb.auth.token_authn.TokenAuthenticationServerProvider
      - CHROMA_SERVER_AUTHN_CREDENTIALS=chr_7f2b9d4e1a6c8035bfa1
      - CHROMA_AUTH_TOKEN_TRANSPORT_HEADER=X_CHROMA_TOKEN

Ловушка, на которой теряют вечера: в 0.5.0 переменные переименовали. Было CHROMA_SERVER_AUTH_PROVIDER и chromadb.auth.token.TokenAuthServerProvider, стало CHROMA_SERVER_AUTHN_* с буквой N и классом из token_authn. Старые имена новый сервер молча игнорирует: стартует, отвечает и пускает всех.

Второй нюанс: часть путей проверку обходит, и heartbeat без токена отвечает даже на защищённом сервере — проверка нужна на коллекциях:

curl -s -o /dev/null -w '%{http_code}\n' \
  http://127.0.0.1:8000/api/v2/tenants/default_tenant/databases/default_database/collections
# 401

curl -s -H 'X-Chroma-Token: chr_7f2b9d4e1a6c8035bfa1' \
  http://127.0.0.1:8000/api/v2/tenants/default_tenant/databases/default_database/collections | jq length

Есть и вариант с логином и паролем — chromadb.auth.basic_authn.BasicAuthenticationServerProvider с файлом server.htpasswd. Клиент на Python:

import chromadb
from chromadb.config import Settings

client = chromadb.HttpClient(
    host="chroma.example.com", port=443, ssl=True,
    settings=Settings(
        chroma_client_auth_provider="chromadb.auth.token_authn.TokenAuthClientProvider",
        chroma_client_auth_credentials="chr_7f2b9d4e1a6c8035bfa1",
        chroma_auth_token_transport_header="X_CHROMA_TOKEN",
    ),
)

TLS Chroma не умеет — наружу её выставляют через Nginx как реверс-прокси с сертификатом Let's Encrypt:

server {
    listen 443 ssl http2;
    server_name chroma.example.com;
    ssl_certificate     /etc/letsencrypt/live/chroma.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/chroma.example.com/privkey.pem;

    client_max_body_size 128m;
    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_read_timeout 300s;
        proxy_buffering off;
    }
}

client_max_body_size здесь не украшение: батч из нескольких тысяч векторов по 1536 измерений — это десятки мегабайт JSON, а дефолтный лимит 1 МБ отдаст 413 Request Entity Too Large раньше, чем Chroma увидит запрос. И не вешайте сюда же auth_basic: заголовок Authorization достанется Nginx, а X_CHROMA_TOKEN до клиента не доедет. Фаервол закрывает вопрос:

ufw allow 22/tcp && ufw allow 443/tcp && ufw deny 8000/tcp && ufw enable

Первая коллекция: размерность, метрика и кто считает эмбеддинги

Коллекция в Chroma — индекс плюс схема, которую задают один раз. Навсегда фиксируются два параметра: размерность вектора и метрика расстояния.

col = client.get_or_create_collection(
    name="docs",
    configuration={"hnsw": {"space": "cosine", "ef_construction": 200,
                            "max_neighbors": 32, "ef_search": 100}},
)
col.add(ids=["d1", "d2"],
        documents=["первый абзац", "второй абзац"],
        metadatas=[{"lang": "ru"}, {"lang": "ru"}])
print(col.query(query_texts=["о чём первый"], n_results=2, where={"lang": "ru"}))

Метрика по умолчанию — l2, а не косинус. Для нормализованных эмбеддингов порядок результатов совпадёт, для ненормализованных — разъедется и сам результат. space у созданной коллекции не меняется — только пересоздать и залить заново. В 0.5.x параметры HNSW задавались через metadata={"hnsw:space": "cosine"}, в 1.x — через configuration; старые ключи ещё работают, но устарели.

Размерность берётся из первого вектора; попытка подмешать эмбеддинги другой модели даёт:

chromadb.errors.InvalidDimensionException: Embedding dimension 1536 does not match collection dimensionality 384

Это не баг, а смена модели на середине проекта.

Теперь главное. Эмбеддинги считает клиент, а не сервер. При вызове add(documents=[...]) embedding-функция считает в вашем процессе — на сервер уходят готовые числа. По умолчанию берётся all-MiniLM-L6-v2 в формате ONNX, 384 измерения; при первом вызове библиотека скачивает архив модели примерно на 79 МБ в ~/.cache/chroma/onnx_models/all-MiniLM-L6-v2/. На машине без исходящего доступа к AWS S3 первый add() повиснет с таймаутом загрузки, и слова «модель» в ошибке не будет.

Если эмбеддинги считает внешний API (text-embedding-3-small — 1536 измерений, text-embedding-3-large — 3072), нагрузки на Chroma нет, зато важен исходящий IP: с российского адреса OpenAI отвечает 403 unsupported_country_region_territory.

Если модель локальная — скажем, nomic-embed-text через Ollama на той же машине, — CPU становится общим дефицитным ресурсом. Наш замер на AMD EPYC 9554 (16 vCPU, это 8 ядер плюс HT, Ollama 0.33.1, qwen2.5:7b в Q4_K_M): при num_thread 2 / 4 / 8 / 16 генерация даёт 5,7 / 7,6 / 7,6 / 7,4 ток/с — полка уже на четырёх потоках, упор в память, а 32 потока роняют скорость до 0,35 ток/с, в двадцать раз. Потоками инференс не разгонишь, а Chroma на тех же ядрах задушить легко.

Грабли, о которых узнают на проде

Лимит батча. Chroma не принимает произвольно большой add(). Сервер честно сообщает свой предел:

curl -s http://127.0.0.1:8000/api/v2/pre-flight-checks
{"max_batch_size":5461}

Число не случайное — лимит переменных SQLite в одном запросе, делённый на число колонок. Превышение даёт ValueError: Batch size 20000 exceeds maximum batch size 5461. Лечение — резать заливку самому:

B = client.get_max_batch_size()
for i in range(0, len(ids), B):
    col.add(ids=ids[i:i+B], embeddings=vecs[i:i+B], metadatas=meta[i:i+B])

Запрос больше, чем коллекция. Просьба вернуть n_results=10 из коллекции с тремя элементами в ряде версий выдаёт не пустоту, а исключение из hnswlib — с фирменной опечаткой в тексте: RuntimeError: Cannot return the results in a contigious 2D array. Probably ef or M is too small. Причина не в ef, а в том, что n_results больше числа доступных точек, — ограничивайте его через col.count().

Один узел — и точка. Реплик, шардов и автофейловера в открытой сборке нет: две Chroma над одним каталогом не поднять, а сетевые ФС вроде NFS дают database is locked уже на средней нагрузке — каталог должен лежать на локальном диске. Кластер и раздельные снапшоты — это к Qdrant, и его лучше выбрать заранее, чем мигрировать миллион векторов потом.

Память, диск, вакуум и бэкап

Индекс HNSW держится целиком в оперативной памяти. Это арифметика, а не измерение: на каждый вектор уходит размерность × 4 байта (float32) плюс граф связей — при max_neighbors = 16 ещё около 140 байт на запись.

РазмерностьБайт на вектор100 тыс. векторов1 млн векторов
384 (MiniLM)~1,7 КБ~170 МБ~1,7 ГБ
768 (nomic-embed)~3,2 КБ~320 МБ~3,2 ГБ
1536 (OpenAI small)~6,3 КБ~630 МБ~6,3 ГБ

Это нижняя граница самого индекса: сверху лягут процесс Python, кеш SQLite и страничный кеш ФС — закладывайте плюс 30–50 %. Впритык к RAM базу не планируйте: индекс перестаёт помещаться — машина уходит в своп, поиск замедляется на порядки. При max_neighbors = 32 граф стоит вдвое дороже, около 270 байт на вектор.

На диске каталог выглядит так:

/var/lib/chroma/
├── chroma.sqlite3
└── 4f1c0a3e-....-9b2d/
    ├── data_level0.bin
    ├── header.bin
    ├── length.bin
    └── link_lists.bin

Отдельного внимания стоит chroma.sqlite3: каждая запись сначала попадает в журнал внутри него и лишь потом переносится в HNSW-индекс, а журнал не подчищается сам. Файл разрастается кратно полезным данным, и удаление коллекций места не возвращает. Лечение — остановить сервер и прогнать вакуум:

systemctl stop chroma          # или docker compose stop chroma
/opt/chroma/venv/bin/chroma utils vacuum --path /var/lib/chroma
systemctl start chroma

Операция долгая — на больших базах минуты и десятки минут, — зато потом журнал чистится сам.

Штатного API снапшотов у Chroma, в отличие от Qdrant, нет: chroma.sqlite3 и бинарники HNSW пишутся независимо, и горячая копия «на живую» почти наверняка развалится. Рабочий бэкап — только с остановкой:

docker compose stop chroma
tar -czf /backup/chroma-$(date +%F).tar.gz -C /var/lib chroma
docker compose start chroma

Пара секунд простоя раз в сутки по cron — приемлемая цена. Восстановление симметрично: остановить сервис, распаковать каталог, поднять — версия должна быть не ниже той, на которой снимали бэкап.

Какой сервер под Chroma брать в MAATRIX

Chroma — сервис памяти и диска, а не процессора: HNSW нагружает CPU короткими всплесками, а индекс обязан целиком помещаться в RAM.

Минимум: 1 vCPU, 2 ГБ RAM, 20 ГБ NVMe. Хватает на прототип и базу до ~300 тысяч векторов по 384 измерения, если эмбеддинги считает внешний API. Ограничение честное: на 1 ГБ не ставьте — процесс с зависимостями съедает сотни мегабайт ещё до первой коллекции, и первый крупный add() закончится OOM.

Комфортный вариант: 2–4 vCPU, 8 ГБ RAM, 80 ГБ NVMe. Это миллион векторов по 768 измерений (около 3,2 ГБ индекса) с запасом на журнал SQLite, вакуум и tar-архив рядом. Диск берите с трёхкратным запасом от размера индекса — там же живут журнал и бэкап.

Локальные эмбеддинги меняют картину. Модель и база делят одни ядра, а потоками, как показал замер выше, инференс не разгоняется. Для связки «Chroma + локальная embedding-модель + LLM» разумный старт — 8 vCPU и 16 ГБ RAM: это уже территория выделенного сервера.

Локация — Великобритания, Лондон. Причина прикладная: RAG-стек держат рядом с приложением, которое его дёргает, — лишние 40–50 мс на каждом обращении к базе за один ответ заметно накапливаются. С британского адреса штатно отвечают внешние embedding-API, тогда как 403 unsupported_country_region_territory с российского IP ломает связку целиком. Если в документах персональные данные россиян и вы работаете по 152-ФЗ — берите RU-локацию, но сразу планируйте локальную модель эмбеддингов.

Про установку без иллюзий. Chroma в каталоге приложений apps.maatrix.io нет: сервер приезжает чистым, с Ubuntu 24.04, и разворачивается по инструкции выше — минут за десять вместе с Nginx и сертификатом. Зато в каталоге есть готовые сборки соседей по AI-стеку — например, LiteLLM для маршрутизации между моделями: их отмечают при заказе, и они поднимаются сами. Удобно, когда Chroma нужна как часть RAG по своим документам.

Оплата — картами российских банков, по СБП, криптовалютой или токеном MAAT: иностранная карта не нужна, хотя сервер стоит в Лондоне. После выдачи доступов первым делом закройте порт 8000 фаерволом и включите токен — открытую базу с документами компании сканеры находят за минуты.

Нужен сервер под эту задачу?

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

Арендовать сервер

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

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

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

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

Chroma или Qdrant — что выбрать под RAG?

Chroma выигрывает на старте: контейнер, знакомый Python-API, документы и метаданные вместе. Qdrant — на дистанции: снапшоты по API, квантование, кластер. Водораздел практический — примерно миллион векторов и бэкап без остановки.

Можно ли обойтись PersistentClient и не поднимать сервер?

Да, если писатель — ровно один процесс: скрипт индексации, одиночный воркер. Появится второй воркер, cron рядом или вторая машина — упрётесь в database is locked.

Удалил коллекцию, а место на диске не освободилось. Почему?

Записи попадают в журнал chroma.sqlite3, который сам не подчищается: удаление лишь помечает данные, файл не сжимается. Остановите сервис и выполните chroma utils vacuum --path /var/lib/chroma.

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

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