非rootなdocker環境にllama.cpp+open-webui環境を構築する方法
小型モデルにお勧め.WEB外部検索と日本語グラフ生成付き
概要
本記事ではローカルPC上に以下のAI環境を構築する方法をまとめます.
- rootless Docker: ~/ 以下で運用出来る.
- lazydocker: docker管理ソフト(ncurses版).
- llama.cpp: LLM APIサーバー.
- jupyter: グラフ生成用 python実行環境.
- Open WebUI: フロントエンド.
- DuckDuckGo外部検索.
- Pythonグラフ生成 (Jupyter連携)
- DuckDuckGo外部検索.
systemdを使用せず,すべて手動スクリプトで管理する構成です.
小型モデルでも実用的なローカルAI環境を構築することを目的としています.
最終構成は以下の通りです.
| component | port |
|---|---|
| llama-server | 8080 |
| jupyter | 8888 |
| open-webui | 3000 |
構成
Open WebUI → rootless docker → llama.cp
rootless Docker インストール
すでに出来上がったものが用意されてるのでこれは簡単です.
rootless の注意点としては,システム予約ポート(1024)以下は使えない事です.
まず必要ツールをインストール.
sudo apt install uidmap dbus-user-session
確認.
which newuidmap which newgidmap
公式スクリプトで本体をインストール.
~/.local/share 以下にインストールされます.
curl -fsSL https://get.docker.com/rootless | sh
PATH 追加
.bashrc などに追加しておきます.
export PATH=$HOME/bin:$HOME/.local/bin:$PATH
systemdを使用せずDockerを起動するスクリプトを作成します.
~/bin/start-stop-docker.sh
#!/usr/bin/env bash
DOCKERD_ROOTLESS="$HOME/bin/dockerd-rootless.sh"
PIDFILE="$HOME/.docker-rootless.pid"
LOGFILE="$HOME/.docker-rootless.log"
start() {
if [ -f "$PIDFILE" ]; then
echo "docker already running"
exit 0
fi
echo "starting docker..."
nohup $DOCKERD_ROOTLESS \
> "$LOGFILE" 2>&1 &
echo $! > "$PIDFILE"
}
stop() {
if [ ! -f "$PIDFILE" ]; then
echo "docker not running"
exit 0
fi
kill $(cat "$PIDFILE")
rm -f "$PIDFILE"
}
status() {
if [ -f "$PIDFILE" ]; then
echo "docker running pid $(cat $PIDFILE)"
else
echo "docker stopped"
fi
}
restart() {
stop
sleep 2
start
}
case "$1" in
start) start ;;
stop) stop ;;
restart) restart ;;
status) status ;;
*) echo "usage: $0 {start|stop|restart|status}" ;;
esac
rootless Docker トラブル
以下のエラーが発生する場合があります.
failed to lock /run/user/1000/dockerd-rootless/lock another RootlessKit is running
これは rootlesskit のプロセスが残っていることが原因です.
確認
ps aux | grep rootless
停止
pkill rootlesskit pkill dockerd
lazydocker の導入
Docker管理を簡単にするため lazydocker を導入します.
ネットワークごしに docker.desktopの様な感覚で扱える ncurses 動作のものを探したところ,ほぼこれが定番の様です.
特徴
- Go製の高速TUI
- コンテナ / イメージ / ボリューム / ネットワーク管理
- ログ閲覧
- execシェル
- docker-compose対応
- rootless dockerでも問題なく動く
インストール
~/.local/bin/lazydocker にインストールされます.
curl https://raw.githubusercontent.com/jesseduffield/lazydocker/master/scripts/install_update_linux.sh | bash
起動
lazydocker
以下が確認できます.
- コンテナ
- ログ
- イメージ
llama.cpp APIサーバー
これに関しては,以前に詳しく記事にしてあります.
ローカルLLMサーバーを起動します.
注意点として,127.0.0.1 では Docker からアクセス出来ない事があるので,その場合は 0.0.0.0 にします.
ポートは 8080 にします.
以下の起動スクリプトを作ってます.
(モデル番号を省略するとメニューを表示)
~/bin/start-stop-llama.sh start|stop|status model_number
#! /bin/bash
# --- LLM Config ---
NAME="llama.cpp-server"
LM_MODEL_DIR="$HOME/AI/llama_models"
## for CPU
#LM_HOME_DIR="$HOME/AI/llama.cpp/build/bin"
#LM_NGL=0 # CPU
## for GPU
LM_HOME_DIR="$HOME/AI/llama.cpp/build-cuda/bin"
LM_NGL=99 # GPU
#LM_CTX=2048 # コンテキスト
#LM_CTX=3072 # コンテキスト
LM_CTX=4096 # コンテキスト
LM_N=1024 # 打ち止め
LM_TEMP=0.1 # 正確さ
OTHER_OPT=''
PIDFILE="$HOME/.${NAME}.pid"
LOGFILE="$HOME/${NAME}.log"
# モデル指定用
ARGS2="$2"
start() {
if [ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then
echo "$NAME already running (PID $(cat $PIDFILE))"
return 0
fi
echo "Starting $NAME..."
# 1. モデルファイルを自動スキャン (.ggufのみ)
local FILES=($(ls "$LM_MODEL_DIR"/*.gguf 2>/dev/null | xargs -n 1 basename))
if [ ${#FILES[@]} -eq 0 ]; then
echo "Error: No .gguf files found in $LM_MODEL_DIR"
return 1
fi
# 2. モデルがない場合メニュー表示
if [ -z "$ARGS2" ]; then
echo "--- Found Models in $LM_MODEL_DIR ---"
for i in "${!FILES[@]}"; do
echo "$((i+1))) ${FILES[$i]}"
done
read -p "Select number: " MODEL_CH
else
MODEL_CH="$ARGS2"
fi
# 3. 選択したモデルの設定
local SELECTED_FILE="${FILES[$((MODEL_CH-1))]}"
if [ -z "$SELECTED_FILE" ]; then echo "Invalid selection"; return 1; fi
LM_MODEL="$SELECTED_FILE"
"$LM_HOME_DIR/llama-server" -m "$LM_MODEL_DIR/$LM_MODEL" --host 0.0.0.0 --port 8080 -ngl $LM_NGL -c $LM_CTX $OTHER_OPT >> "$LOGFILE" 2>&1 &
echo $! > "$PIDFILE"
echo "$NAME started (PID $(cat $PIDFILE)) MODEL: $LM_MODEL"
}
stop() {
if [ ! -f "$PIDFILE" ]; then
echo "$NAME not running (no PID file)"
return 1
fi
PID=$(cat "$PIDFILE")
if kill -0 "$PID" 2>/dev/null; then
echo "Stopping $NAME (PID $PID)..."
kill "$PID"
sleep 2
fi
if kill -0 "$PID" 2>/dev/null; then
echo "Force killing $NAME..."
kill -9 "$PID"
fi
rm -f "$PIDFILE"
echo "$NAME stopped"
}
status() {
if [ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then
echo "$NAME running (PID $(cat $PIDFILE))"
return 0
else
echo "$NAME not running"
return 1
fi
}
case "$1" in
start) start ;;
stop) stop ;;
restart) stop; start ;;
status) status ;;
*)
echo "Usage: $0 {start|stop|restart|status} [model_no]"
exit 1
;;
esac
Jupyter サーバー
これも以前に記事にしてあります.標準ではブラウザ内の仮想python環境で実行されますが,あれは不完全です.
Pythonコード実行用のJupyterを起動します.
ポート:8888
jupyter notebook --ip 0.0.0.0 --port 8888
トークン確認
jupyter server list
起動スクリプトの仕様は同様です.
Open WebUI 起動
open webuiをインストール,起動します.
-v でコピーしたコンテナ外のローカルディレクトリをバインドしてます.
–restart unless-stopped で,必要な時は明示的に再起動する様に指示してます.
- 3000 open-webui
- 8080 llama.cpp
docker run -d \ -p 3000:8080 \ -v ~/ai/open-webui-data:/app/backend/data \ --name open-webui \ --restart unless-stopped \ ghcr.io/open-webui/open-webui:main
ブラウザから接続.
http://localhost:3000
Open WebUI データ保存ディレクトリ
Docker更新時に設定が消えないように,データはホスト側に移動します.
mkdir -p ~/ai/open-webui-data
ボリューム名を確認.open-webui ですね.
docker volume ls
コピー.
docker run --rm \ -v open-webui:/source \ -v ~/ai/open-webui-data:/dest \ alpine sh -c "cp -a /source/* /dest/"
コンテナ停止.
docker rm -f open-webui
Open WebUI設定
管理者パネル → 設定 → 接続
Connections → OpenAI Compatible
ここは,逆に 127.0.0.1 や 0.0.0.0 はダメで,192.168. 形式である必要があります.
http://192.168.x.x:8080/v1
認証は使われていないので dummy でOK.
モデル一覧が表示されれば成功です.
DuckDuckGo 外部検索
Open WebUIにはDuckDuckGo検索エンジンが標準搭載されています.
設定
Settings → Tools → DuckDuckGo
小型モデルではツール使用能力が重要です.
Qwen3はツール利用が比較的安定しています.
Python グラフ生成
code extentions で設定します.
実行エンジン,インタプリタどちらも Jupyterに.
http://192.168.x.x:8888
トークンは以下で確認.
jupyter server list
403エラーが出る場合はトークン未指定です.
Markdownコードブロック問題
モデルは以下の形式でコードを出力する場合があります.
```python
print("hello")
しかしこの形式のままでは,Open WebUI の Python 実行機能から
Jupyter にコードを送る際に正しく実行できない場合があります.
そのため,モデルが Python コードを出力する際には
Markdown コードブロックを使用しないように設定します.
Open WebUI のシステムプロンプトに以下を追加します.
Do not wrap python code in markdown. Output raw python code only.
これにより,モデルは以下のように直接 Python コードを出力するようになります.
import matplotlib.pyplot as plt plt.plot([1,2,3],[4,5,6]) plt.show()
この形式であれば,Open WebUI から Jupyter にコードが正しく送信され,
グラフ生成などが正常に実行されます.
ちなみに,コード生成も Qwen 系が強いです.
あと,日本語が豆腐に化ける場合は,python 環境に日本語フォントが入っていない事が問題です.
この問題もあって,python 環境は現時点では jupyter 一択です.
Linux 側に Noto Sans CJK を用意して下さい.
そしておもむろにシステムプロンプトに以下の様にします.
かなりオーバーキル気味なので,単にこんにちは,とか打っただけだとグラフがどうちゃらと返事する事もありますが.
あなたは日本人です.日本語で答えて下さい. 可能な限り推測は控え,正確な情報を提供して下さい. グラフの描画が必要な場合 - plt.show() を必ず実行してください. - この実行環境は Linux (Ubuntu系) です. - matplotlibの日本語フォントは Noto Sans CJK JP を使用してください.
Open WebUI データクリーナー
Open WebUI を長期間使用していると,以下のデータが徐々に蓄積されます.
- Python 実行時に生成された画像
- ナレッジベースのベクトルデータ
- キャッシュ
- ログ
特に vector DB や uploads ディレクトリはサイズが大きくなりやすいため,
不要になったデータを削除するクリーナースクリプトを作成します.
スクリプト
~/bin/open-webui-clean.sh
#!/usr/bin/env bash DATA="$HOME/ai/open-webui-data" echo "Open WebUI Cleaner" echo "Data directory: $DATA" echo if [ ! -d "$DATA" ]; then echo "Directory not found" exit 1 fi echo "Current usage:" du -sh "$DATA" echo echo "Detail:" du -sh "$DATA"/* 2>/dev/null echo BEFORE=$(du -sb "$DATA" | cut -f1) read -p "Clean generated data? (y/N): " ans [ "$ans" != "y" ] && exit 0 echo echo "Cleaning..." rm -rf "$DATA/uploads/"* rm -rf "$DATA/cache/"* rm -rf "$DATA/vector_db/"* rm -rf "$DATA/documents/"* rm -rf "$DATA/logs/"* echo AFTER=$(du -sb "$DATA" | cut -f1) echo "After cleaning:" du -sh "$DATA" echo echo "Detail:" du -sh "$DATA"/* 2>/dev/null echo DIFF=$((BEFORE-AFTER)) echo "Freed space:" numfmt --to=iec $DIFF
このスクリプトを実行すると,
- 実行前サイズ
- 削除対象
- 実行後サイズ
- 削減容量
が表示されます.
起動手順
最終的な起動手順は以下の通りです.
まず LLM サーバーを起動します.
start-stop-llama.sh start 1
次に Jupyter を起動します.
jupyter notebook --ip 0.0.0.0 --port 8888
最後に Docker を起動します.
start-stop-docker.sh start
さらに,これら全てを管理するラッパースクリプトも書いてます.
ai start|stop|restart|status|clean|gpu|force-kill n
とか出来ます.gpu は nvidia-smi 実行,force-kill は,docker のゾンビ退治です.
n はモデル番号ですね.
このスクリプトにより,以下の環境がすべて起動します.
- llama.cpp API サーバー
- Python 実行環境 (Jupyter)
- Open WebUI
ブラウザから以下にアクセスすると Open WebUI を利用できます.
http://localhost:3000
ai コマンドサンプル
~/bin/ai
#!/usr/bin/env bash
LLAMA="$HOME/bin/Start-stop-llama-server.sh"
JUPYTER="$HOME/bin/Start-stop-jupyter.sh"
DOCKER="$HOME/bin/Start-stop-docker.sh"
MODEL_DIR="$HOME/AI/llama_models"
MODEL=${2:-1}
start() {
echo "Starting AI stack"
$LLAMA start "$MODEL" || exit 1
sleep 2
$JUPYTER start || exit 1
sleep 2
$DOCKER start || exit 1
echo "AI stack ready"
}
stop() {
echo "Stopping AI stack"
$DOCKER stop
$JUPYTER stop
$LLAMA stop
echo "Stopped"
}
restart() {
stop
sleep 2
start "$MODEL"
}
status() {
echo "=== LLAMA ==="
$LLAMA status
echo
echo "=== JUPYTER ==="
$JUPYTER status
echo
echo "=== DOCKER ==="
$DOCKER status
echo
}
logs() {
echo "=== llama-server log ==="
tail -n 20 ~/.llama.log 2>/dev/null
echo
echo "=== jupyter log ==="
tail -n 20 ~/.jupyter.log 2>/dev/null
echo
echo "=== docker containers ==="
docker ps
}
models() {
echo "Available models:"
ls -1 "$MODEL_DIR"
}
gpu() {
if command -v nvidia-smi >/dev/null; then
nvidia-smi
else
echo "GPU info unavailable"
fi
}
clean() {
~/bin/open-webui-cleaner.sh
}
case "$1" in
start) start "$@" ;;
stop) stop ;;
restart) restart "$@" ;;
status) status ;;
logs) logs ;;
models) models ;;
gpu) gpu ;;
clean) celan ;;
*)
echo "Usage:"
echo " ai start [model]"
echo " ai stop"
echo " ai restart [model]"
echo " ai status"
echo " ai logs"
echo " ai models"
echo " ai gpu"
echo " ai clean"
exit 1
;;
esac
使用方法
AI環境起動
ai start 1
AI環境停止
ai stop
状態確認
ai status
まとめ
本記事では,ローカルPC上に以上の AI 環境を構築しました.
systemd に依存せず,すべてをスクリプトで管理する構成にし,docker 管理に lazydocker を使用した事で,ネットワークごしにも管理しやすくなりました.
llama.cpp がデフォルトで提供するWEBサーバーもシンプルで良いのですが,この構成により,小型モデルでも
- Web 検索
- Python グラフ生成
- ナレッジベース
などを利用できる実用的なローカル AI 環境を構築できます.
もちろん,open-webui は他にもエンドポイントを追加し,各種の LLM サービスに接続する事が出来るので,今後の拡張性も期待できます.