Руководство по сборке локального AI-стека. Часть 5. Open WebUI поверх llama.cpp

—

от автора

Руководство по сборке локального AI-стека

Часть 5. Open WebUI поверх llama.cpp

На этом шаге мы добавляем Open WebUI — веб-интерфейс с аутентификацией, историей чатов и настройкой параметров моделей. Это финальный слой: llama-server продолжает работать как обычно (только на 127.0.0.1), а Open WebUI становится «парадной дверью» для пользователей из LAN.

Зачем нужен Open WebUI

Родной UI llama.cpp (Svelte) хорош для отладки, но у него нет:

  • Управления моделями — нельзя задать per-model системные промпты, параметры сэмплинга, отображение.

  • Многопользовательского режима — один пользователь = один контекст.

Open WebUI добавляет всё это, оставаясь тонкой надстройкой над llama-server. Он ходит к нему по OpenAI-совместимому API (/v1/chat/completions), так что llama.cpp даже не знает, что его использует другой клиент.

Архитектура

LAN 0.0.0.0:8080 (Open WebUI) -> Local 127.0.0.1:10000 llama-server

Ключевые моменты:

  • llama-server слушает только 127.0.0.1:10000. Снаружи к нему не подключиться.

  • Open WebUI слушает 0.0.0.0:8080, доступен из LAN, требует логин.

  • Секреты (API-ключ llama + сессионный ключ WebUI) хранятся в .secrets (chmod 600), генерируются один раз.

  • Оркестратор run_webui.sh поднимает оба процесса, ждёт готовности llama-server, запускает WebUI, и корректно останавливает оба по Ctrl+C.

Секреты: .secrets и init_secrets.sh

Перед первым запуском нужно создать .secrets — файл с двумя ключами:

Ключ

Для чего

Кто использует

LLAMA_API_KEY

Аутентификация запросов к llama-server

Open WebUI (передаёт в заголовке Authorization: Bearer ...), внешние клиенты

WEBUI_SECRET_KEY

Подпись JWT-токенов сессий Open WebUI

Сам Open WebUI (без него все пользователи разлогиниваются при перезапуске)

Важно про WEBUI_SECRET_KEY. Если генерировать его при каждом запуске (secrets.token_hex(32) в Python), то при каждом перезапуске WebUI все сессии сбрасываются — пользователи вынуждены логиниться заново. Поэтому ключ хранится в файле и переиспользуется. init_secrets.sh создаёт файл один раз, при повторном запуске ничего не меняет.

Формат .secrets — простые KEY=VALUE, читаются через source в bash и через open() в Python:

# Секреты проекта. Не коммитить в git. chmod 600.LLAMA_API_KEY=<48 hex-символов>WEBUI_SECRET_KEY=<64 hex-символа>

Права 600 — только владелец может читать. Это критично: ключи дают полный доступ к серверу.

Установка Open WebUI

Open WebUI ставится в тот же venv, что и всё остальное (venv/). Это упрощает wheelhouse (один список пакетов) и не создаёт второй Python-интерпретатор.

./venv/bin/pip install --upgrade pip./venv/bin/pip install torch --index-url https://download.pytorch.org/whl/cpu./venv/bin/pip install open-webui

CPU-версия torch ставится до open-webui, иначе pip притянет CUDA-вариант с nvidia-* пакетами (более 2 ГБ лишнего).

Настройка Open WebUI: start_webui.py

Скрипт читает .secrets, выставляет env-переменные и запускает open-webui serve. Ключевые настройки:

Подключение к llama-server

os.environ["OPENAI_API_BASE_URL"] = "http://127.0.0.1:10000/v1"os.environ["OPENAI_API_KEY"] = llama_api_keyos.environ["BYPASS_MODEL_ACCESS_CONTROL"] = "True"
  • OPENAI_API_BASE_URL — адрес llama-server. Порт 10000 — фиксированный (задан в run_webui.sh через LLAMA_PORT), чтобы не конфликтовать с родным UI llama.cpp на 8080.

  • OPENAI_API_KEY — ключ из .secrets. Open WebUI передаёт его в каждом запросе, llama-server проверяет.

  • BYPASS_MODEL_ACCESS_CONTROL — все модели из роутера видны всем пользователям WebUI, без ручного «назначения прав на модель».

Оффлайн-режим

os.environ["OFFLINE_MODE"] = "true"os.environ["HF_HUB_OFFLINE"] = "1"os.environ["RAG_EMBEDDING_MODEL_AUTO_UPDATE"] = "false"os.environ["RAG_RERANKING_MODEL_AUTO_UPDATE"] = "false"os.environ["RAG_EMBEDDING_ENGINE"] = ""os.environ["RAG_EMBEDDING_MODEL"] = ""os.environ["WHISPER_MODEL_AUTO_UPDATE"] = "false"os.environ["AUDIO_STT_ENGINE"] = ""
  • OFFLINE_MODE=true — отключает проверку обновлений и автоматические загрузки с Hugging Face.

  • RAG_* — отключает RAG-эмбеддинги (иначе при первом запросе WebUI попытается скачать модель эмбеддингов ~500 МБ).

  • AUDIO_STT_ENGINE="" — отключает Whisper (распознавание речи), чтобы не тянуть модель.

Ollama — отключаем

os.environ["ENABLE_OLLAMA_API"] = "false"os.environ["OLLAMA_BASE_URL"] = ""

Зачем. Open WebUI по умолчанию пытается обнаружить Ollama на localhost:11434 и «подружиться» с ним. Если Ollama не установлена, в логах появляются ошибки подключения, а иногда WebUI «залипает» на попытках достучаться и не выдает список моделей из llama-cpp сервера. Отключение через ENABLE_OLLAMA_API=false убирает эту головную боль (мы уже обожглись на этом в старом проекте).

Веб-поиск — оставляем

os.environ["ENABLE_RAG_WEB_SEARCH"] = "true"os.environ["ENABLE_WEB_SEARCH"] = "true"os.environ["WEB_SEARCH_ENGINE"] = "duckduckgo"
  • DuckDuckGo — без API-ключа, работает «из коробки», если есть интернет. Модель может использовать результаты поиска для ответа (RAG-поиск в интернете). Если сети нет — просто не работает, но не ломает чат.

Прочие мелочи

os.environ["ENABLE_IMAGE_GENERATION"] = "false"    # мы не подключаем sd-server к WebUIos.environ["ENABLE_COMMUNITY_FUNCTIONS"] = "false" # не грузим сторонние плагиныos.environ["USER_AGENT"] = "OpenWebUI-Offline/1.0" # убирает предупреждение LangChainos.environ["DATA_DIR"] = DATA_DIR                  # данные внутри проекта

Регистрация

# os.environ["ENABLE_SIGNUP"] = "false"  # по умолчанию НЕ задаём

Логика по умолчанию:

  • ENABLE_SIGNUP не задаём -> регистрация открыта.

  • Первый зарегистрировавшийся пользователь автоматически становится админом.

  • После создания первого админа Open WebUI автоматически закрывает регистрацию.

  • Чтобы добавить пользователя: админ заходит в Admin -> Users -> включает регистрацию на время -> создаёт аккаунт -> закрывает обратно.

Если жёстко поставить ENABLE_SIGNUP=false до первого запуска — даже админ не сможет зарегистрироваться. Поэтому оставляем пустым.

Оркестратор: run_webui.sh

Скрипт делает пять вещей:

  1. Проверяет окружение — venv, open-webui, .secrets, run_llama_mmap.sh, start_webui.py. Если чего-то нет — создаёт/устанавливает (первый запуск).

  2. Проверяет порты 10000 и 8080 — если заняты, выходит с ошибкой.

  3. Опционально настраивает статический IP (через nmcli), если пользователь ответит 1. Полезно, чтобы адрес сервера не менялся после перезагрузки роутера.

  4. Запускает llama-server в фоне на 127.0.0.1:10000 через PORT=10000 HOST=127.0.0.1 ./run_llama_mmap.sh. Ждёт /health до 5 минут.

  5. Запускает Open WebUI на 0.0.0.0:8080 через start_webui.py. Уходит в wait.

Обработка Ctrl+C. trap cleanup EXIT INT TERM ловит сигнал и:

  • останавливает WebUI (SIGTERM, ждёт 10 с, SIGKILL);

  • останавливает llama-server (шлёт SIGTERM в bash-обёртку run_llama_mmap.sh, её trap делает systemctl --user stop, сервер умирает);

  • при ошибке показывает хвосты логов обоих процессов.

Про модель. run_webui.sh принимает опциональный первый аргумент — имя модели. Если передать ./run_webui.sh Qwen3.5-9B-Q4_K_M, оно уйдёт в run_llama_mmap.sh -m ..., и сервер стартует в режиме одной модели. Если не передавать — run_llama_mmap.sh запустится в router mode (список моделей в дропдауне WebUI, загрузка по требованию).

Почему router mode по умолчанию: WebUI позволяет переключать модели без перезапуска сервера. Первая загрузка модели при выборе — долгая, но дальше — комфортно. Если нужна конкретная модель сразу — передайте её аргументом, загрузится только она.

Запуск и первый вход

./run_webui.sh

Вывод:

[INFO]  Запуск llama-server на 127.0.0.1:10000...[INFO]  llama-server запущен (PID 12345)[INFO]  Жду готовности API (до 5 мин, на HDD — долго)...[INFO]  llama-server готов.[INFO]  Запуск Open WebUI на 0.0.0.0:8080...[INFO]  Открыть: http://127.0.0.1:8080[INFO]     или:  http://192.168.1.100:8080

Первый вход:

  1. Откройте http://<IP>:8080 в браузере.

  2. Нажмите «Sign up» — создайте аккаунт. Первый пользователь = админ.

  3. После создания аккаунта регистрация закрывается автоматически.

  4. В дропдауне моделей — список всех .gguf из models/. Выберите нужную.

  5. Пишите в чат. История сохраняется автоматически.

Для добавления пользователя: зайдите как админ -> Admin Panel -> Users -> кнопка «Enable Signup» -> создайте аккаунт вручную или дайте человеку зарегистрироваться -> «Disable Signup».

Управление параметрами моделей в WebUI

Одно из главных преимуществ Open WebUI — per-model настройки, которых нет в родном UI llama.cpp:

  • Admin Panel -> Models — для каждой модели можно задать:

    • System Prompt — «Ты — ассистент, который отвечает кратко» и т.п.

    • Temperature, top_p, top_k, repeat_penalty — параметры сэмплинга.

    • Function calling, vision, file upload — включить/выключить.

    • Default model — какая модель выбирается при открытии нового чата.

Это позволяет «настроить и забыть»: например, для OxCoder-9B — temperature=0.2 (код точнее), для Gemma-4-E4B — temperature=0.9 (РП живее). В llama.cpp такие настройки пришлось бы передавать в каждом запросе.

Код init_secrets.sh

#!/usr/bin/env bash# Создаёт .secrets с LLAMA_API_KEY и WEBUI_SECRET_KEY, если файла нет.# Идемпотентен: повторный запуск ничего не меняет.set -euo pipefailSCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"SECRETS_FILE="$SCRIPT_DIR/.secrets"if [ -f "$SECRETS_FILE" ]; then    echo "[*] $SECRETS_FILE уже существует, ничего не меняю."    exit 0ficommand -v openssl >/dev/null || { echo "[!] нужен openssl"; exit 1; }LLAMA_API_KEY="$(openssl rand -hex 24)"WEBUI_SECRET_KEY="$(openssl rand -hex 32)"umask 077cat > "$SECRETS_FILE" <<EOF# Секреты проекта. Не коммитить в git. chmod 600.LLAMA_API_KEY=$LLAMA_API_KEYWEBUI_SECRET_KEY=$WEBUI_SECRET_KEYEOFchmod 600 "$SECRETS_FILE"echo "[*] Создан $SECRETS_FILE"echo "    LLAMA_API_KEY=${LLAMA_API_KEY:0:8}... (первые 8 символов)"echo "    WEBUI_SECRET_KEY=${WEBUI_SECRET_KEY:0:8}..."

Код run_webui.sh

#!/usr/bin/env bash# Запуск llama-server (порт 10000) + Open WebUI (порт 8080).# При первом запуске: создаёт venv, ставит open-webui, генерирует .secrets.## Usage: ./run_webui.sh [model_name]#   model_name — опционально; если не задан, run_llama_mmap.sh спросит интерактивно.## Env:#   LLAMA_PORT=10000   порт llama-server (только localhost)#   WEBUI_PORT=8080    порт Open WebUI (0.0.0.0, доступ из LAN)#   ASK_FIX_IP=1       спрашивать про статический IP (0 — не спрашивать)set -euo pipefailSCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"VENV_DIR="$SCRIPT_DIR/venv"SECRETS_FILE="$SCRIPT_DIR/.secrets"OFFLINE_PACKAGES_DIR="$SCRIPT_DIR/offline_packages"LLAMA_SCRIPT="$SCRIPT_DIR/run_llama_mmap.sh"WEBUI_SCRIPT="$SCRIPT_DIR/start_webui.py"INIT_SECRETS="$SCRIPT_DIR/init_secrets.sh"LLAMA_HOST="${LLAMA_HOST:-127.0.0.1}"LLAMA_PORT="${LLAMA_PORT:-10000}"WEBUI_PORT="${WEBUI_PORT:-8080}"ASK_FIX_IP="${ASK_FIX_IP:-1}"RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'info()  { echo -e "${GREEN}[INFO]${NC}  $*"; }warn()  { echo -e "${YELLOW}[WARN]${NC}  $*"; }error() { echo -e "${RED}[ERROR]${NC} $*" >&2; exit 1; }# ==================================================================# 1. Установка / проверка окружения# ==================================================================if ! command -v python3 &>/dev/null; then    error "python3 не найден. Установите: sudo apt install python3 python3-venv python3-pip"fiif [ ! -d "$VENV_DIR" ]; then    info "venv не найден, создаю $VENV_DIR..."    python3 -m venv "$VENV_DIR"    "$VENV_DIR/bin/pip" install --quiet --upgrade pipfi# open-webui: сначала оффлайн, потом онлайнif [ ! -x "$VENV_DIR/bin/open-webui" ]; then    if [ -d "$OFFLINE_PACKAGES_DIR" ] && [ -n "$(ls -A "$OFFLINE_PACKAGES_DIR" 2>/dev/null)" ]; then        info "Ставлю open-webui из offline_packages..."        if ! "$VENV_DIR/bin/pip" install --no-index \                --find-links="$OFFLINE_PACKAGES_DIR" open-webui; then            warn "Оффлайн-установка не удалась, пробую из PyPI..."            if ! "$VENV_DIR/bin/pip" show torch &>/dev/null; then                "$VENV_DIR/bin/pip" install torch --index-url https://download.pytorch.org/whl/cpu            fi            "$VENV_DIR/bin/pip" install open-webui        fi    else        info "Ставлю open-webui из PyPI..."        "$VENV_DIR/bin/pip" install open-webui    fifi[ -x "$VENV_DIR/bin/open-webui" ] || error "open-webui не установлен в venv"# секреты[ -x "$INIT_SECRETS" ] || [ -f "$INIT_SECRETS" ] || error "Нет $INIT_SECRETS"if [ ! -f "$SECRETS_FILE" ]; then    info "Секреты не найдены, генерирую..."    bash "$INIT_SECRETS"fi[ -f "$LLAMA_SCRIPT" ] || error "Не найден $LLAMA_SCRIPT"[ -f "$WEBUI_SCRIPT" ] || error "Не найден $WEBUI_SCRIPT"# ==================================================================# 2. Проверка занятости портов# ==================================================================for p in "$LLAMA_PORT" "$WEBUI_PORT"; do    if (echo >/dev/tcp/127.0.0.1/"$p") 2>/dev/null; then        error "Порт $p уже занят. Освободите или задайте LLAMA_PORT/WEBUI_PORT."    fidone# ==================================================================# 3. Опциональная настройка статического IP# ==================================================================if [ "$ASK_FIX_IP" = "1" ] && command -v nmcli &>/dev/null; then    read -n1 -r -p "Сделать фиксированный IP для этого ПК? [0 - нет, 1 - да]: " fix_ip    echo ''    if [ "$fix_ip" = "1" ]; then        echo "Настройка статического IP..."        iface="$(ip route | grep default | cut -d' ' -f5 | head -1)"        [ -n "$iface" ] || error "Не удалось определить сетевой интерфейс"        echo "Интерфейс: $iface"        read -r -p "IP-адрес (например, 192.168.1.100/24): " ipaddr        read -r -p "Шлюз (Enter — авто): " gateway        read -r -p "DNS (Enter — авто): " dns        [ -n "$gateway" ] || gateway="$(ip route | grep default | cut -d' ' -f3 | head -1)"        [ -n "$dns" ]     || dns="$(nmcli dev show | grep 'IP4.DNS' | tr -s ' ' | cut -d' ' -f2 | head -1)"        sudo nmcli con mod "$iface" ipv4.addresses "$ipaddr"        sudo nmcli con mod "$iface" ipv4.gateway "$gateway"        sudo nmcli con mod "$iface" ipv4.dns "$dns"        sudo nmcli con mod "$iface" ipv4.method manual        sudo nmcli con up "$iface"        info "Статический IP настроен. Текущий адрес: $(hostname -I)"    fifi# ==================================================================# 4. Логи и trap# ==================================================================LLAMA_LOG="$(mktemp -t llama-orch.XXXXXX.log)"WEBUI_LOG="$(mktemp -t webui-orch.XXXXXX.log)"LLAMA_PID=""WEBUI_PID=""cleanup() {    local ec=$?    trap - EXIT INT TERM    set +e    echo ""    info "Завершение работы..."    # WebUI    if [ -n "$WEBUI_PID" ] && kill -0 "$WEBUI_PID" 2>/dev/null; then        info "Останавливаю Open WebUI (PID $WEBUI_PID)..."        kill "$WEBUI_PID" 2>/dev/null        for _ in $(seq 1 10); do            kill -0 "$WEBUI_PID" 2>/dev/null || break            sleep 1        done        kill -9 "$WEBUI_PID" 2>/dev/null        wait "$WEBUI_PID" 2>/dev/null    fi    # llama (bash-обёртка). Внутри её trap сделает systemctl stop.    if [ -n "$LLAMA_PID" ] && kill -0 "$LLAMA_PID" 2>/dev/null; then        info "Останавливаю llama-server (PID $LLAMA_PID)..."        kill "$LLAMA_PID" 2>/dev/null        for _ in $(seq 1 15); do            kill -0 "$LLAMA_PID" 2>/dev/null || break            sleep 1        done        kill -9 "$LLAMA_PID" 2>/dev/null        wait "$LLAMA_PID" 2>/dev/null    fi    if [ "$ec" -ne 0 ] && [ "$ec" -ne 130 ]; then        echo ""; warn "Хвост лога llama:"; tail -n 20 "$LLAMA_LOG" 2>/dev/null        echo ""; warn "Хвост лога webui:"; tail -n 20 "$WEBUI_LOG" 2>/dev/null    fi    rm -f "$LLAMA_LOG" "$WEBUI_LOG"    info "Все сервисы остановлены."    exit "$ec"}trap cleanup EXIT INT TERM# ==================================================================# 5. Запуск llama-server в фоне# ==================================================================LLAMA_ARGS=()[ $# -ge 1 ] && LLAMA_ARGS=(-m "$1")info "Запуск llama-server на $LLAMA_HOST:$LLAMA_PORT..."info "Лог: $LLAMA_LOG"PORT="$LLAMA_PORT" HOST="$LLAMA_HOST" \    "$LLAMA_SCRIPT" "${LLAMA_ARGS[@]}" >"$LLAMA_LOG" 2>&1 &LLAMA_PID=$!info "llama-server запущен (PID $LLAMA_PID)"# ==================================================================# 6. Ожидание готовности API# ==================================================================info "Жду готовности API (до 5 мин, на HDD — долго)..."READY=0for i in $(seq 1 300); do    kill -0 "$LLAMA_PID" 2>/dev/null || {        warn "llama-server умер. Лог:"; tail -n 40 "$LLAMA_LOG"        exit 1    }    if curl -sf "http://$LLAMA_HOST:$LLAMA_PORT/health" >/dev/null 2>&1; then        READY=1; break    fi    [ $((i % 30)) -eq 0 ] && info "  ... ждём ($i с)"    sleep 1done[ "$READY" -eq 1 ] || { warn "llama-server не поднялся за 5 мин. Лог:"; tail -n 40 "$LLAMA_LOG"; exit 1; }info "llama-server готов."# ==================================================================# 7. Запуск Open WebUI (foreground по сути; wait ниже)# ==================================================================LAN_IP="$(hostname -I 2>/dev/null | awk '{print $1}')"info "Запуск Open WebUI на 0.0.0.0:$WEBUI_PORT..."info "Открыть: http://127.0.0.1:$WEBUI_PORT"[ -n "$LAN_IP" ] && info "   или:  http://$LAN_IP:$WEBUI_PORT"WEBUI_HOST=0.0.0.0 WEBUI_PORT="$WEBUI_PORT" \    "$VENV_DIR/bin/python" "$WEBUI_SCRIPT" 2>&1 | tee -a "$WEBUI_LOG" &WEBUI_PID=$!# Ждём завершения WebUI (Ctrl+C -> trap)wait "$WEBUI_PID"

Код start_webui.py

#!/usr/bin/env python3"""Запуск Open WebUI с настройками для локального llama-server."""import osimport sysimport subprocessSCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))SECRETS_FILE = os.path.join(SCRIPT_DIR, ".secrets")DATA_DIR = os.path.join(SCRIPT_DIR, "open-webui-data")# --- Чтение секретов из .secrets ---def load_secrets(path):    result = {}    if not os.path.isfile(path):        return result    with open(path) as f:        for line in f:            line = line.strip()            if not line or line.startswith("#") or "=" not in line:                continue            key, _, val = line.partition("=")            result[key.strip()] = val.strip()    return resultsecrets = load_secrets(SECRETS_FILE)# WEBUI_SECRET_KEY — обязателен для стабильных сессийif "WEBUI_SECRET_KEY" not in secrets:    print(f"Ошибка: в {SECRETS_FILE} нет WEBUI_SECRET_KEY. "          f"Запустите init_secrets.sh", file=sys.stderr)    sys.exit(1)os.environ["WEBUI_SECRET_KEY"] = secrets["WEBUI_SECRET_KEY"]# LLAMA_API_KEY — для доступа к llama-serverllama_api_key = secrets.get("LLAMA_API_KEY", "sk-no-key-required")os.environ["OPENAI_API_KEY"] = llama_api_key# --- Оффлайн-режим и отключение лишнего ---os.environ["OFFLINE_MODE"] = "true"os.environ["HF_HUB_OFFLINE"] = "1"os.environ["RAG_EMBEDDING_MODEL_AUTO_UPDATE"] = "false"os.environ["RAG_RERANKING_MODEL_AUTO_UPDATE"] = "false"os.environ["RAG_EMBEDDING_ENGINE"] = ""os.environ["RAG_EMBEDDING_MODEL"] = ""os.environ["WHISPER_MODEL_AUTO_UPDATE"] = "false"os.environ["AUDIO_STT_ENGINE"] = ""os.environ["ENABLE_IMAGE_GENERATION"] = "false"os.environ["ENABLE_COMMUNITY_FUNCTIONS"] = "false"os.environ["USER_AGENT"] = "OpenWebUI-Offline/1.0"# --- Ollama off (обожглись) ---os.environ["ENABLE_OLLAMA_API"] = "false"os.environ["OLLAMA_BASE_URL"] = ""# --- Веб-поиск: оставляем (DuckDuckGo без ключа) ---os.environ["ENABLE_RAG_WEB_SEARCH"] = "true"os.environ["ENABLE_WEB_SEARCH"] = "true"os.environ["WEB_SEARCH_ENGINE"] = "duckduckgo"# --- Подключение к llama-server ---os.environ["OPENAI_API_BASE_URL"] = os.environ.get(    "OPENAI_API_BASE_URL", "http://127.0.0.1:10000/v1")os.environ["BYPASS_MODEL_ACCESS_CONTROL"] = "True"# --- Директория данных ---os.environ["DATA_DIR"] = DATA_DIR# --- Запуск ---venv_bin = os.path.dirname(sys.executable)open_webui_cmd = os.path.join(venv_bin, "open-webui")if not os.path.exists(open_webui_cmd):    print(f"Ошибка: не найден {open_webui_cmd}. Установлен ли open-webui в venv?",          file=sys.stderr)    sys.exit(1)webui_port = os.environ.get("WEBUI_PORT", "8080")webui_host = os.environ.get("WEBUI_HOST", "0.0.0.0")cmd = [open_webui_cmd, "serve", "--host", webui_host, "--port", webui_port]print(f"[*] DATA_DIR:      {DATA_DIR}")print(f"[*] OPENAI_BASE:   {os.environ['OPENAI_API_BASE_URL']}")print(f"[*] Ollama:        отключён")print(f"[*] Web search:    DuckDuckGo")print(f"[*] Команда:       {' '.join(cmd)}\n")try:    subprocess.run(cmd, check=True)except KeyboardInterrupt:    print("\n[*] Open WebUI остановлен.")except subprocess.CalledProcessError as e:    print(f"[!] Ошибка запуска Open WebUI: {e}", file=sys.stderr)    sys.exit(1)    

Проверка после запуска

1. llama-server отвечает:

curl -sf http://127.0.0.1:10000/health && echo OK

2. Модели видны в API (и, значит, в WebUI):

curl -s http://127.0.0.1:10000/v1/models | jq '.data[].id'

Должен вывести список всех .gguf из models/.

3. WebUI отвечает:

curl -sf http://127.0.0.1:8080/ > /dev/null && echo OK

4. Память обоих процессов:

ps -o pid,comm,rss,%mem -C llama-server,python3

llama-server ~2–3 ГБ реально (RSS может быть больше из-за mmap), Python (WebUI) ~300–500 МБ.

5. Логи при ошибке:

# Если run_webui.sh упал, он покажет хвосты автоматически.# Ручная диагностика:journalctl --user -u llama-server-* --since "5 min ago" 2>/dev/null

Подводные камни

  1. WEBUI_SECRET_KEY без файла — все пользователи разлогиниваются при перезапуске. Решение: .secrets + init_secrets.sh (уже сделано).

  2. Ollama «стучится» по умолчанию — ошибки в логах, иногда «залипание». Решение: ENABLE_OLLAMA_API=false (уже сделано).

  3. Регистрация — если случайно поставить ENABLE_SIGNUP=false до первого пользователя, войти будет невозможно. Решение: не задавать ENABLE_SIGNUP вообще.

  4. Порт 8080 vs 8888 — run_webui.sh использует 8080, run_sd_media.sh — 8888. Если запустить оба одновременно, конфликта не будет. Но run_llama_mmap.sh сам по себе (без run_webui.sh) использует 8080 — если вы запускаете его отдельно и одновременно с WebUI, будет конфликт. Решение: run_llama_mmap.sh отдельно — на 8080, а с WebUI — на 10000 (это уже настроено в run_webui.sh). Если ранее запускали и тестировали run_llama_mmap.sh на родном UI, а потом запустили WebUI — может не отобразиться интерфейс (нужно очистить кэш браузера и локальные бд браузера на этой странице).

  5. mmap и memory.pressure — при работе WebUI + llama под нагрузкой следите за memory.current через cgroup (как в части 2). Если MEM_MAX=10G мало для выбранной модели — уменьшите CTX_SIZE или поднимите лимит.

ссылка на оригинал статьи https://habr.com/ru/articles/1099532/