AnythingLLM на VPS: веб-интерфейс и RAG поверх вашего Ollama

Ollama даёт API и больше ничего: ни истории чатов, ни загрузки документов, ни логина. AnythingLLM закрывает ровно эту дырку - веб-интерфейс, workspace'ы и встроенная векторная база для RAG поверх ваших файлов. Разбираем развёртывание через docker compose: как контейнер дотягивается до Ollama на хосте, где живут данные, почему авторизацию надо включить до того, как вы выдадите ссылку коллеге, и почему привычное

Ollama на сервере - это движок и HTTP API, больше ничего. Модель отвечает, curl работает, а дальше вы упираетесь в то, что у этого нет ни окна для переписки, ни памяти между запросами, ни способа скормить модели папку с документами, ни логина. Развернуть AnythingLLM на VPS - самый короткий способ закрыть все четыре пункта разом: это веб-интерфейс с рабочими пространствами, загрузкой файлов и встроенной векторной базой, который использует ваш Ollama как исполнителя. Ниже - развёртывание через docker compose, подключение к уже поднятому движку, персистентность данных, обязательное включение авторизации и периметр, за которым это должно стоять.

Коротко. AnythingLLM ставится одним compose-файлом из образа mintplexlabs/anythingllm, слушает порт 3001 и хранит всё - документы, векторную базу LanceDB, чаты, настройки - в каталоге /app/server/storage, который обязательно выносится томом на хост. К Ollama, запущенной нативно на том же сервере, контейнер ходит через host.docker.internal, и для этого нужны две вещи сразу: запись host-gateway в extra_hosts и Ollama, слушающая не только loopback. По ресурсам сам AnythingLLM просит 2 ГБ памяти, два ядра с AVX2 и 5 ГБ диска - модель считается сверху и стоит дороже. Сразу после первого входа включите защиту паролем или multi-user режим: по умолчанию инстанс открыт любому, кто знает адрес. Наружу порт не публикуйте: 127.0.0.1:3001:3001 в compose, nginx сверху, ufw на 80 и 443 - привычное ufw deny 3001 для Docker не работает, и это разбирается отдельно ниже.

Чем AnythingLLM отличается от голого Ollama API

Это не конкуренты и не замена друг другу, а два разных слоя. Ollama - движок инференса: держит веса модели в памяти и отвечает на HTTP-запросы. AnythingLLM - приложение поверх такого движка: оно решает, что именно отправить модели, и хранит всё остальное.

Самая содержательная часть разницы - RAG (retrieval-augmented generation, «генерация с подтягиванием источников»). Вы загружаете в рабочее пространство документы, AnythingLLM режет их на фрагменты, прогоняет через эмбеддер - модель, которая превращает текст в вектор чисел, - и складывает в векторную базу. Когда вы задаёте вопрос, он сначала ищет в этой базе куски, похожие на вопрос, и подкладывает их в запрос к модели. Модель при этом ничего не «дообучает» и не запоминает: она просто получает нужный отрывок вместе с вопросом.

Что нужно

Ollama сам по себе

AnythingLLM поверх него

Интерфейс для переписки

Нет, только API и ollama run в терминале

Веб-интерфейс, доступен из браузера

История чатов

Нет, каждый запрос независим

Сохраняется в базе на диске

Ответы по вашим документам

Нет, только то, что вы вручную засунули в запрос

Загрузка файлов, индексация, поиск по ним

Векторная база

Отдельный сервис, ставить самому

LanceDB встроена, отдельный сервер не нужен

Учётные записи и роли

Нет вообще

Пароль на инстанс или multi-user с ролями

Разделение контекста по задачам

Нет

Рабочие пространства с собственным набором документов

Обратное тоже верно: AnythingLLM не считает. Всю генерацию делает провайдер, который вы ему указали, и если модель отвечает медленно, менять надо движок или тариф, а не интерфейс.

Развилка на старте: есть Ollama или нет

Статья написана как продолжение разбора про Ollama на VPS и исходит из того, что движок у вас уже поднят, модель скачана и curl к 127.0.0.1:11434 отвечает. Если это не так - идите туда, ставьте Ollama и возвращайтесь: дальше по тексту будет нужен рабочий адрес движка.

Второй путь: Ollama нет и пока не надо. AnythingLLM умеет работать с внешними провайдерами - при первом запуске он спрашивает, кого использовать, и можно выбрать облачный API вместо локальной модели. Тогда сервер несёт только интерфейс, векторную базу и эмбеддер, память под модель не нужна, а вот приватность уезжает вместе с текстами документов на чужие серверы. Ради приватности всё это обычно и разворачивается, так что вариант с внешним API разумен скорее как временный, пока вы не решили, какую модель крутить у себя.

Сколько ресурсов просит AnythingLLM на VPS: память, диск, AVX2

Документация AnythingLLM называет минимум: 2 ГБ оперативной памяти, двухъядерный процессор с поддержкой AVX2 и 5 ГБ диска. Это про сам AnythingLLM вместе с LanceDB и встроенным эмбеддером - языковая модель в эти цифры не входит и почти всегда стоит дороже, чем всё остальное вместе взятое.

Отдельный расход, который легко пропустить: встроенный эмбеддер (EMBEDDING_ENGINE=native) - это своя небольшая нейросеть, которая грузится в память того же контейнера. Она работает не тогда, когда вы переписываетесь, а тогда, когда индексируете документы, и на маленьком тарифе индексация толстого PDF может попасть по памяти в тот же момент, когда модель отвечает на вопрос.

Сценарий

Ориентир по памяти на весь сервер

AnythingLLM, генерация - внешним API

2-4 ГБ

AnythingLLM + Ollama с моделью на 1-3 млрд параметров

4-6 ГБ

AnythingLLM + Ollama с моделью на 7-8 млрд параметров

8 ГБ и выше

Это прикидка из требований проекта плюс правило «память под размер файла модели и сверху запас» из статьи про Ollama, а не замер на живом боксе - на своей нагрузке сверьтесь через docker stats. Диск считайте так же честно: 5 ГБ под приложение, плюс веса моделей, плюс ваши документы вместе с их векторами.

А вот что проверяется за одну команду и экономит вечер. LanceDB собрана с расчётом на набор инструкций AVX2, и на процессоре без него контейнер падает с Illegal instruction - без внятных подсказок, что произошло. На KVM-VPS это зависит от того, какую модель процессора вам отдал гипервизор, так что проверьте заранее:

grep -o -m1 avx2 /proc/cpuinfo

Вывод avx2 - всё в порядке. Пустой вывод означает, что стандартная сборка LanceDB на этой машине не заведётся, и придётся либо брать другую локацию или тариф, либо переключать VECTOR_DB на внешнюю базу, которую надо поднимать отдельным сервисом.

Шаг 1. Docker и каталог для данных

Ставим Docker из репозитория самого Docker, а не из репозитория Ubuntu: пакет docker.io из дистрибутива отстаёт по версиям, и docker compose в нём отдельная история. Команды ниже - официальный способ, они одинаковы для Ubuntu 22.04, 24.04 и 26.04.

sudo apt-get update
sudo apt-get install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] 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-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin

  • $(. /etc/os-release && echo "$VERSION_CODENAME") подставляет кодовое имя вашей версии Ubuntu, так что строку не надо править руками.
  • docker-compose-plugin - это docker compose в два слова. Старый docker-compose через дефис - другой, устаревший инструмент, команды из статьи на нём могут вести себя иначе.

Проверка, что всё встало: docker compose version должен вывести строку вида Docker Compose version v2.x.x. Если вместо этого docker: 'compose' is not a docker command - плагин не установился, вернитесь к последней команде.

Теперь каталог, в котором будет жить сервис. Данные AnythingLLM - документы, векторы, чаты - должны остаться на хосте, иначе любой docker compose down сотрёт вашу работу вместе с контейнером.

sudo mkdir -p /opt/anythingllm/storage
cd /opt/anythingllm
sudo touch .env
sudo chown -R 1000:1000 /opt/anythingllm

  • storage - сюда примонтируется /app/server/storage из контейнера, вся персистентность именно здесь.
  • Пустой .env создаётся заранее специально: если его нет, Docker при монтировании создаст на его месте каталог, и контейнер не запустится.
  • chown 1000:1000 - процесс внутри образа работает не от root, и без этого он не сможет писать в свой же том.

Шаг 2. compose-файл и переменные

Кладём /opt/anythingllm/docker-compose.yml:

services:
anythingllm:
image: mintplexlabs/anythingllm:latest
container_name: anythingllm
restart: unless-stopped
cap_add:
- SYS_ADMIN
ports:
- "127.0.0.1:3001:3001"
volumes:
- ./storage:/app/server/storage
- ./.env:/app/server/.env
extra_hosts:
- "host.docker.internal:host-gateway"

  • 127.0.0.1:3001:3001 - порт публикуется только на loopback. Снаружи его не существует; наружу будет смотреть nginx. Это ключевая строка, к ней вернёмся в разделе про периметр.
  • ./.env:/app/server/.env - тот же файл, что вы правите руками, приложение использует и для записи: настройки, выбранные в интерфейсе, дописываются туда же. Без этого монтирования выбор провайдера и модели потеряется при первом же пересоздании контейнера.
  • extra_hosts с host-gateway - чтобы имя host.docker.internal внутри контейнера указывало на хост. На Linux это не работает само по себе, в отличие от Docker Desktop.
  • cap_add: SYS_ADMIN нужен сборщику документов: он поднимает встроенный браузер, когда вы скармливаете ссылку на сайт. Если вы грузите только локальные файлы, попробуйте сначала без этой строки - лишние привилегии контейнеру ни к чему.
  • restart: unless-stopped - сервис поднимется после перезагрузки сервера. Про то, почему политика рестарта вообще нужна и чем она отличается от ручного запуска, подробно в статье про развёртывание сервиса на сервере.

Теперь /opt/anythingllm/.env. Минимальный рабочий набор под связку с Ollama:

SERVER_PORT=3001
STORAGE_DIR="/app/server/storage"
JWT_SECRET="замените-на-случайную-строку-от-32-символов"
SIG_KEY="замените-на-случайную-строку-от-32-символов"
SIG_SALT="замените-на-случайную-строку-от-32-символов"
DISABLE_TELEMETRY="true"

LLM_PROVIDER="ollama"
OLLAMA_BASE_PATH="http://host.docker.internal:11434"
OLLAMA_MODEL_PREF="llama3.1:8b"
OLLAMA_MODEL_TOKEN_LIMIT=4096

EMBEDDING_ENGINE="native"
VECTOR_DB="lancedb"

  • Три секрета генерируются командой openssl rand -hex 32, каждый своей. Они подписывают сессии: если оставить примеры из документации, любой, кто их знает, сможет подделать вход.
  • OLLAMA_MODEL_PREF - тег модели ровно в том виде, в каком его показывает ollama list на вашем сервере. Подставьте свой, модели из примера у вас может не быть.
  • OLLAMA_MODEL_TOKEN_LIMIT - размер контекстного окна, на который AnythingLLM рассчитывает, собирая запрос из вашего вопроса и найденных фрагментов документов. Поставите больше, чем тянет модель, - получите обрезанные ответы.
  • VECTOR_DB="lancedb" - тот самый встроенный вариант, который требует AVX2 и не требует отдельного сервера.

Шаг 3. Дать контейнеру дотянуться до Ollama

Здесь спотыкаются чаще всего. По умолчанию Ollama на Linux слушает 127.0.0.1:11434 - только loopback самого хоста. У контейнера свой сетевой стек, его 127.0.0.1 - это он сам, и запрос к хостовому loopback до Ollama не доходит. В интерфейсе это выглядит как ошибка подключения к провайдеру при полностью живой Ollama, и полчаса уходит на проверку того, что работает.

Лечится тем, что Ollama начинает слушать все интерфейсы. Через штатное переопределение systemd:

sudo systemctl edit ollama

В открывшемся редакторе, в блок для пользовательских строк:

[Service]
Environment="OLLAMA_HOST=0.0.0.0:11434"

Применяем и проверяем:

sudo systemctl daemon-reload
sudo systemctl restart ollama
ss -ltnp | grep 11434

Что вы должны увидеть: в выводе ss адрес 0.0.0.0:11434 или *:11434. Если там всё ещё 127.0.0.1:11434 - переопределение не подхватилось, проверьте, что файл сохранился, и повторите daemon-reload.

Важное следствие. Теперь Ollama формально слушает и внешний интерфейс сервера, а собственной авторизации у неё нет. Это безопасно ровно до тех пор, пока порт закрыт фаерволом. Убедитесь, что 11434 наружу не разрешён:

sudo ufw status verbose

В списке правил не должно быть ничего про 11434. В отличие от Docker-портов, здесь ufw действительно работает: Ollama запущена нативно, а не контейнером, и её трафик проходит обычные цепочки.

Когда проще иначе. Если Ollama вы ещё не ставили и вас не смущает контейнер, запустите её вторым сервисом в том же compose-файле - тогда хостовая сеть вообще не участвует, и ничего никуда открывать не надо:

ollama:
image: ollama/ollama:latest
container_name: ollama
restart: unless-stopped
volumes:
- ./ollama:/root/.ollama

Порт наружу у него не публикуется вовсе, а в .env вместо host.docker.internal пишется имя сервиса: OLLAMA_BASE_PATH="http://ollama:11434". Compose кладёт оба контейнера в общую сеть, и имена в ней резолвятся сами. Модель после первого запуска качается командой docker compose exec ollama ollama pull llama3.1:8b.

Поднимаем всё:

cd /opt/anythingllm
sudo docker compose up -d
sudo docker compose ps

В выводе ps статус должен быть running, а в колонке портов - 127.0.0.1:3001->3001/tcp. Если статус restarting - смотрите sudo docker compose logs -n 50 anythingllm: на этом этапе это почти всегда либо права на том, либо AVX2.

Шаг 4. Включить авторизацию до того, как откроете доступ

Свежий AnythingLLM не спрашивает пароль. Кто открыл адрес - тот администратор: может читать все рабочие пространства, скачивать загруженные документы и менять настройки. Пока порт висит на loopback, вас это не касается, но включить защиту нужно раньше, чем появится nginx с доменом.

Добираемся до интерфейса, не открывая ничего наружу, - через SSH-туннель с вашей машины:

ssh -L 3001:127.0.0.1:3001 root@ВАШ_IP

Пока сессия открыта, http://127.0.0.1:3001 в вашем браузере - это порт на сервере. Пройдите первичную настройку, выберите провайдера, дождитесь, пока интерфейс подтвердит, что видит модель. После этого - в настройки инстанса, в раздел безопасности. Там два варианта:

  • Пароль на экземпляр (Password Protect Instance) - один общий пароль на всё. Годится, когда пользователь вы один. Отключается и меняется потом в любой момент.
  • Multi-user режим (Enable multi-user mode) - при включении вы задаёте логин и пароль первой администраторской учётки, дальше заводите пользователей с ролями: admin - полный доступ, manager - все рабочие пространства, но без настроек модели, эмбеддера и векторной базы, default - только то, что ему выдали.

Одна деталь, которую стоит знать до нажатия: по документации проекта, включив multi-user режим, вернуться к одиночному уже нельзя. Если инстансом будет пользоваться кто-то кроме вас - включайте multi-user сразу, потому что перевести потом одну общую учётку в нормальные роли без переноса данных не выйдет.

Шаг 5. nginx и ufw перед сервисом

Теперь про то, почему в compose стоит 127.0.0.1:3001:3001, а не просто 3001:3001. Docker, публикуя порт, сам пишет правила в iptables - в цепочки, которые обрабатываются раньше тех, куда пишет ufw. Практический итог: ufw default deny incoming стоит, порт всё равно открыт всему интернету, и ufw status выглядит при этом абсолютно спокойно. Это не баг, а следствие порядка цепочек, но ловятся на нём регулярно.

Надёжный обход - не давать Docker публиковать порт наружу вообще. Привязка к 127.0.0.1 делает ровно это: контейнер доступен с самого сервера и невидим снаружи, независимо от состояния фаервола. Дальше наружу смотрит один nginx, и вот его как раз ufw фильтрует нормально.

Серверный блок - обычный обратный прокси с тремя дополнениями, без которых интерфейс ведёт себя странно:

server {
listen 80;
server_name llm.example.com;

client_max_body_size 128M;

location / {
proxy_pass http://127.0.0.1:3001;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";

proxy_buffering off;
proxy_read_timeout 600s;
}
}

  • proxy_buffering off - иначе nginx копит ответ модели в буфере и отдаёт куском в конце. Текст перестаёт «печататься» на глазах, и медленная модель выглядит зависшей.
  • proxy_read_timeout 600s - модель на процессоре думает долго. С умолчанием в 60 секунд длинные ответы будут обрываться на 504.
  • client_max_body_size 128M - лимит на размер загружаемого документа. Умолчание nginx - 1 МБ, то есть почти любой ваш PDF упрётся в 413.
  • Upgrade и Connection - для websocket-соединений, на которых работают агентские сценарии.

Дальше по накатанному: sudo nginx -t, и только если проверка прошла - sudo systemctl reload nginx. Правила ufw нужны ровно на SSH, 80 и 443. Всё это - включая почему reload, а не restart, и почему сертификат ставится отдельным шагом после того, как HTTP заработал, - подробно разобрано в статье про nginx и ufw перед вашим приложением; повторять её здесь незачем.

Если не заработало

  • Контейнер перезапускается по кругу. Первым делом sudo docker compose logs -n 50 anythingllm. Падение с упоминанием Illegal instruction - это AVX2, проверьте процессор командой из раздела про ресурсы. Ошибки доступа к файлам - права на том, повторите chown.
  • Интерфейс не видит Ollama. Проверьте по порядку: ss -ltnp | grep 11434 показывает 0.0.0.0, а не 127.0.0.1; в compose есть extra_hosts; в .env адрес http://host.docker.internal:11434 без опечаток и без /api на конце.
  • Модель в списке есть, но ответ не приходит. Скорее всего, модель просто медленно грузится в память при первом обращении. Посмотрите ollama ps на хосте: если модель загружена, а ответа нет - проверьте, не режет ли запрос таймаут nginx.
  • Настройки слетели после обновления контейнера. Значит, .env не примонтирован - проверьте, что на хосте это файл, а не случайно созданный Docker каталог.
  • Индексация документа кладёт сервер. Эмбеддер и модель делят одну память. Смотрите docker stats во время загрузки файла и либо индексируйте, когда никто не переписывается, либо берите тариф с запасом.

FAQ

Чем AnythingLLM отличается от Ollama?

Ollama - движок инференса: загружает модель в память и отдаёт HTTP API, без интерфейса, истории переписки, работы с файлами и учётных записей. AnythingLLM - слой над движком: веб-интерфейс, рабочие пространства с раздельным набором документов, загрузка файлов и встроенная векторная база для поиска по ним, управление доступом. Генерирует текст по-прежнему Ollama, AnythingLLM решает, что именно ей отправить.

Как подключить AnythingLLM в Docker к Ollama на хосте?

Нужны две настройки одновременно. В compose-файле контейнеру добавляется extra_hosts с записью host.docker.internal:host-gateway, иначе это имя внутри контейнера просто не резолвится. И сама Ollama должна слушать не только loopback: через sudo systemctl edit ollama задаётся Environment="OLLAMA_HOST=0.0.0.0:11434". После этого в OLLAMA_BASE_PATH указывается http://host.docker.internal:11434, а порт 11434 закрывается фаерволом.

Сколько оперативной памяти нужно AnythingLLM?

Документация проекта называет 2 ГБ памяти, два ядра с поддержкой AVX2 и 5 ГБ диска. Это только сам AnythingLLM с векторной базой и встроенным эмбеддером; языковая модель считается отдельно и обычно занимает больше. Для связки с моделью на 7-8 миллиардов параметров разумный ориентир - сервер от 8 ГБ памяти.

Где AnythingLLM хранит документы и векторную базу?

В каталоге /app/server/storage внутри контейнера: туда складываются загруженные документы, файлы векторной базы LanceDB, история чатов и настройки. Этот каталог обязательно выносится томом на хост, иначе данные умрут вместе с контейнером при первом docker compose down. Отдельный сервер под векторную базу не нужен - LanceDB встроена и работает файлами на диске.

Почему ufw не блокирует порт контейнера Docker?

При публикации порта Docker пишет собственные правила в iptables, и они обрабатываются раньше правил ufw, поэтому запрет в ufw на такой порт не действует. Для сервиса за обратным прокси правильный ответ - не публиковать порт на все интерфейсы: в compose пишется 127.0.0.1:3001:3001 вместо 3001:3001. Тогда снаружи порта нет вообще, а весь внешний трафик приходит через nginx, который ufw фильтрует штатно.

Можно ли выключить multi-user режим обратно?

Нет. По документации проекта переход в multi-user режим необратим: вернуться к одиночному экземпляру с одним паролем после этого нельзя. Поэтому решайте на старте: если инстансом будет пользоваться кто-то кроме вас, включайте multi-user сразу, а не после того, как заведёте рабочие пространства.

Коротко

  • AnythingLLM - интерфейс и RAG-слой, а не движок. Считает по-прежнему Ollama; скорость ответов зависит от неё и от тарифа, а не от интерфейса.
  • Вся персистентность - в /app/server/storage плюс примонтированный .env. Не вынесли на хост - потеряли рабочие пространства и проиндексированные документы при первом пересоздании контейнера.
  • Связка с нативной Ollama требует двух вещей сразу: host.docker.internal:host-gateway в extra_hosts и OLLAMA_HOST=0.0.0.0:11434 на стороне движка. Открыв Ollama на все интерфейсы, сразу убедитесь, что 11434 закрыт фаерволом: своей авторизации у неё нет.
  • 2 ГБ памяти, два ядра с AVX2 и 5 ГБ диска - это требования самого AnythingLLM. Модель считается сверху, и без AVX2 встроенная LanceDB не запустится вообще.
  • Авторизацию включайте до того, как сервис станет доступен по домену. Пароль на экземпляр - для себя, multi-user - для команды, и второе необратимо.
  • Порт публикуйте на 127.0.0.1. Правило ufw на опубликованный Docker-порт не сработает, а loopback-привязка решает вопрос без борьбы с цепочками iptables.

Что дальше

Связка «движок плюс интерфейс» - это база, на которую дальше вешается остальная AI-инфраструктура на своём сервере. По порядку:

PUBLISHED
AUTHOR
HIP-HOSTING
LANGUAGES
EN · RU