Руководство по сборке локального AI-стека. Часть 7. Финал. Офлайн-развёртывание
Ссылки на все части:
Часть 1. Общий план
Часть 2. Запуск llama-cpp
Часть 3. Наполнение моделями
Часть 4. Запуск сервера генерации изображений
Часть 5. Open WebUI поверх llama.cpp
Часть 6. Конвертация моделей Hugging Face в GGUF
Часть 7. Финал. Офлайн-развёртывание
Офлайн-развёртывание: локальная репа и перенос
Для работы стека нужен интернет только на этапе сборки. После того как все Python-пакеты будут скачаны в offline_packages/, проект можно перенести на машину без сети и развернуть из локальной репы. Эта часть — про то, как собрать «снимок» зависимостей и восстановить стек на чистой Debian 12.
Зачем это нужно
Стандартный pip install open-webui требует интернета и разрешает зависимости заново — сегодня поставит одну версию, завтра другую. Для офлайн-машины это неприемлемо: pip либо упадёт с «no internet», либо (если кэш частичный) поставит несовместимые версии.
Локальная репа (offline_packages/) решает обе проблемы:
Фиксация версий —
requirements.lockсодержит точный список пакетов (transformers==5.5.4,torch==2.11.0+cpuи т.д.), а не «свободные» диапазоны.Работа без сети — все
.whlлежат локально,pip install --no-index --find-links=./offline_packagesне обращается к PyPI.Воспроизводимость — на любой машине с теми же ОС/Python получится идентичное окружение.
Что попадает в репу: только Python-пакеты. Модели, бинарники llama.cpp/stable-diffusion.cpp, скрипты, HTML — переносятся отдельно (через tar всей папки проекта). offline_packages/ — это дополнение, а не замена «папки проекта целиком».
Как работает make_offline_packages.sh
Скрипт делает снимок текущего venv и скачивает все пакеты в offline_packages/:
pip freeze > requirements.lock— фиксирует точные версии всего, что стоит вvenv.pip download -r requirements.lock— скачивает wheels для каждого пакета.--only-binary=:all:— гарантирует, что не скачаются.tar.gz(sdist). Если для какого-то пакета нет wheel — скрипт сообщит об этом сейчас, а не на офлайн-машине.--extra-index-urlhttps://download.pytorch.org/whl/cpu— ключевой момент:torchберётся с CPU-индекса (версия+cpu), а не с PyPI (CUDA-версия сnvidia-*).Fallback на sdist — если для какого-то пакета wheel нет, скрипт скачает
.tar.gzи предупредит, что на целевой машине понадобитсяgcc,python3-dev, а дляtokenizers—rustc/cargo.Проверка — сравнивает число файлов с числом пакетов в
requirements.lock. Если меньше — предупреждает.
Почему --extra-index-url, а не --index-url. С --index-url pip ищет только на PyTorch-индексе и не находит open-webui (его там нет). С --extra-index-url — ищет везде: open-webui на PyPI, torch на PyTorch-индексе. При этом torch==2.11.0+cpu (суффикс +cpu) однозначно указывает на CPU-версию, и CUDA не подтянется.
Про «no wheel» fallback. В 99% случаев для стека open-webui + torch на Debian 12 x86_64 все пакеты имеют готовые wheels. Fallback нужен как страховка: лучше узнать о sdist сейчас (когда есть интернет и можно доставить python3-dev), чем на офлайн-машине получить error: command 'gcc' failed.
Полный код make_offline_packages.sh
#!/usr/bin/env bash
# Скачивает все зависимости из текущего venv в offline_packages/.
# Требуется интернет. После этого папку можно перенести на офлайн-машину.
#
# Usage: ./make_offline_packages.sh
# Env:
# VENV_DIR=./venv путь к venv (по умолчанию)
# OUT_DIR=./offline_packages куда качать
# PIP_EXTRA_INDEX_URL для CPU-torch (по умолчанию PyTorch CPU)
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VENV_DIR="${VENV_DIR:-$SCRIPT_DIR/venv}"
OUT_DIR="${OUT_DIR:-$SCRIPT_DIR/offline_packages}"
REQ_FILE="$SCRIPT_DIR/requirements.lock"
TORCH_INDEX="${PIP_EXTRA_INDEX_URL:-https://download.pytorch.org/whl/cpu}"
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; }
[ -d "$VENV_DIR" ] || error "venv не найден: $VENV_DIR"
PIP="$VENV_DIR/bin/pip"
[ -x "$PIP" ] || error "pip не найден: $PIP"
mkdir -p "$OUT_DIR"
# --- 1. Фиксируем список пакетов ---
info "Генерирую $REQ_FILE из текущего окружения..."
"$PIP" freeze > "$REQ_FILE"
PKG_COUNT=$(wc -l < "$REQ_FILE")
info "Пакетов в списке: $PKG_COUNT"
# --- 2. Скачиваем всё, кроме torch (сначала только wheels) ---
info "Скачиваю пакеты в $OUT_DIR (wheels)..."
#grep -v -i '^torch' "$REQ_FILE" > "$REQ_FILE.tmp" || true
WHEEL_FAILED=()
if [ -s "$REQ_FILE" ]; then
if ! "$PIP" download \
--dest "$OUT_DIR" \
--only-binary=:all: \
--no-cache-dir \
--extra-index-url "$TORCH_INDEX" \
-r "$REQ_FILE"; then
warn "Не все пакеты имеют wheel. Определяю виновников..."
# Пытаемся по одному, чтобы найти те, у кого нет wheel
while IFS= read -r pkg; do
[ -z "$pkg" ] && continue
if ! "$PIP" download \
--dest "$OUT_DIR" \
--only-binary=:all: \
--no-cache-dir \
"$pkg" >/dev/null 2>&1; then
WHEEL_FAILED+=("$pkg")
fi
done < "$REQ_FILE"
fi
else
warn "Список без torch пуст, пропускаю"
fi
rm -f "$REQ_FILE.tmp"
# --- 2b. Fallback: sdist для пакетов без wheel ---
if [ ${#WHEEL_FAILED[@]} -gt 0 ]; then
warn "Пакеты без wheel: ${WHEEL_FAILED[*]}"
warn "Скачиваю их как sdist (сборка потребуется на целевой машине):"
warn " - нужен gcc (build-essential)"
warn " - нужен python3-dev (заголовки Python)"
warn " - для tokenizers нужен rustc/cargo"
for pkg in "${WHEEL_FAILED[@]}"; do
"$PIP" download --dest "$OUT_DIR" --no-cache-dir "$pkg" || \
warn " не удалось скачать $pkg"
done
fi
# --- 4. Проверка результата ---
WHL_COUNT=$(find "$OUT_DIR" -maxdepth 1 -type f \( -name '*.whl' -o -name '*.tar.gz' \) | wc -l)
SIZE=$(du -sh "$OUT_DIR" | cut -f1)
info "Скачано файлов: $WHL_COUNT"
info "Размер $OUT_DIR: $SIZE"
if [ "$WHL_COUNT" -lt "$PKG_COUNT" ]; then
warn "Файлов меньше, чем пакетов ($WHL_COUNT < $PKG_COUNT)."
warn "Возможно, часть пакетов не имеет wheel под вашу платформу."
warn "Проверьте: pip download может ругаться на отсутствие wheel."
fi
# --- 5. Инструкция по офлайн-установке ---
echo ""
info "Для установки на офлайн-машине:"
echo " python3 -m venv venv"
echo " ./venv/bin/pip install --no-index --find-links=$OUT_DIR -r $REQ_FILE"
echo ""
info "Готово."
Ключевые моменты
requirements.lock — фиксация версий
pip freeze > requirements.lock
Это снимок текущего venv. Если завтра вы обновите open-webui и снова запустите make_offline_packages.sh, requirements.lock перезапишется новыми версиями, а offline_packages/ — новыми wheels. Старые файлы останутся (не удаляются), но pip install --no-index возьмёт те, что указаны в requirements.lock.
Для воспроизводимости храните requirements.lock вместе с проектом (он в git/архиве) и не запускайте make_offline_packages.sh без необходимости.
Размер и состав
Для текущего стека (open-webui + torch + llama.cpp зависимости):
~244 файла, ~730 МБ.
torch-2.11.0+cpu— ~200 МБ (самый большой).nvidia-*отсутствуют — это главный индикатор, что CUDA не подтянулась.
Если размер больше 2 ГБ — почти наверняка попала CUDA-версия torch. Проверка:
ls offline_packages/ | grep -i nvidia # должно быть пусто
ls offline_packages/torch-*.whl # должно быть +cpu в имени
Проверка перед переносом
1. Тестовая установка в чистый venv:
rm -rf /tmp/test-offline
python3 -m venv /tmp/test-offline
/tmp/test-offline/bin/pip install \
--no-index \
--find-links=./offline_packages \
-r requirements.lock
/tmp/test-offline/bin/python -c "import torch; print(torch.__version__)"
rm -rf /tmp/test-offline
Должно вывести 2.11.0+cpu. Если упадёт с No matching distribution — какого-то пакета не хватает, pip download его не включил (редкий случай, но бывает для пакетов с platform-specific wheels).
2. pip check в тестовом окружении:
/tmp/test-offline/bin/pip check
Должно быть пусто. Если есть конфликты — requirements.lock содержит несовместимые версии (например, fla требует transformers>=5.6.2, а в lock — 5.5.4). Решение: убрать конфликтующий пакет из venv, pip freeze > requirements.lock заново, пересобрать репу.
Упаковка и перенос
1. Собрать репу (на машине с интернетом):
./make_offline_packages.sh
Появится offline_packages/ (~730 МБ) и requirements.lock.
2. Упаковать весь проект в архив:
cd ..
tar czf llama_server_offline.tar.gz \
--exclude='llama_server/venv' \
--exclude='llama_server/.secrets' \
--exclude='llama_server/open-webui-data' \
llama_server/
Что исключать и почему:
Исключение | Причина |
|---|---|
| Пересоздаётся на целевой машине из |
| Содержит ключи. Если переносите — сессии WebUI сохранятся, но ключи «уедут» на новую машину. Безопаснее сгенерировать заново через |
| Пользователи, история чатов. Если хотите «чистое развёртывание» — не переносите, пусть создастся с нуля. Если хотите сохранить историю — переносите, но тогда и |
Что НЕ исключать:
offline_packages/— это и есть локальная репа.requirements.lock— фиксация версий.models/— модели (тяжёлые, но нужны).llama.cpp/иstable-diffusion.cpp/— исходники +build/bin/(бинарники можно перенести, но лучше пересобрать на целевой машине, если архитектура CPU отличается).web/, все скрипты,README.md.
3. Перенести архив на целевую машину (флешка, scp, rsync — что удобно).
4. Развернуть на целевой машине (Debian 12 x86_64, без интернета):
tar xzf llama_server_offline.tar.gz
cd llama_server
# Системные пакеты (нужен локальный apt-репозиторий или они уже стоят)
sudo apt install -y cmake build-essential libopenblas-dev \
python3 python3-venv python3-pip git curl wget jq
# Python-окружение из локальной репы
python3 -m venv venv
./venv/bin/pip install --no-index --find-links=./offline_packages -r requirements.lock
# Секреты (генерируются заново)
./init_secrets.sh
# Запуск
./run_webui.sh
По шагам:
python3 -m venv venv— быстро, без сети.pip install --no-index— читает толькоoffline_packages/, не обращается к PyPI. Если видитеDownloading— что-то не так (например,--no-indexзабыт)../init_secrets.sh— создаст.secretsс новыми ключами../run_webui.sh— при первом запуске может спросить статический IP, поставитьllama.cpp, если бинарников нет.
Подводные камни
Разная версия Python.
offline_packages/содержит wheels с тегомcp311(Python 3.11, стандарт для Debian 12). Если на целевой машине Python 3.12 — часть wheels не подойдёт. Решение: та же ОС и версия Python (Debian 12 -> 3.11).Разная архитектура.
offline_packages/содержит wheels дляx86_64. На ARM (Raspberry Pi) они не подойдут. Решение: собирать репу на целевой архитектуре.systemd-runв LXC/контейнере.run_llama_mmap.shиспользуетsystemd-run --user --scopeдля лимита памяти. В LXC/Docker без user session это может не работать — скрипт откатится на запуск «без лимита», но модель может съесть всю RAM. Решение: либо запускать на хосте (не в контейнере), либо перенести лимиты вsystemdюниты (мы решили не делать).systemd-runтребуетlogind. На headless-сервере безlogind(или в SSH-сессии безpam_systemd)systemd-run --userупадёт. Решение:sudo loginctl enable-linger $USER, чтобы user-сессия жила после логаута, или запускатьllama-serverбез лимита вручную.open-webui-data/и.secrets. Если переносите оба — сессии WebUI продолжат работать на новой машине (ключ тот же). Если перенести толькоopen-webui-data/без.secrets— WebUI сгенерирует новыйWEBUI_SECRET_KEY, и все пользователи разлогинятся. Если переносить только.secretsбезopen-webui-data/— нет пользователей, но ключи валидны для новых.
Итог
Что даёт:
Полная воспроизводимость: те же версии пакетов на любой машине.
Никаких обращений к PyPI во время установки.
Нет риска, что завтра
open-webuiобновится и сломает совместимость.Размер архива: ~730 МБ (репа) + модели (несколько ГБ, у меня около 70 Гб) + исходники.
Что НЕ решает:
Системные пакеты (
apt) — их нужно доставить отдельно (локальный apt-репозиторий илиapt-offline). Для Debian 12 базовый набор (cmake,python3-venv,git) обычно уже стоит, но на чистой системе придётся доставить.Бинарники
llama.cpp/stable-diffusion.cpp— либо переносятся в архиве, либо пересобираются на целевой машине (еслиcmake/gccесть).
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.