非rootなdocker環境にllama.cpp+open-webui環境を構築する方法
小型モデルにお勧め.WEB外部検索と日本語グラフ生成付き

ozy's labo.

概要

本記事ではローカルPC上に以下のAI環境を構築する方法をまとめます.

  • rootless Docker: ~/ 以下で運用出来る.
  • lazydocker: docker管理ソフト(ncurses版).
  • llama.cpp: LLM APIサーバー.
  • jupyter: グラフ生成用 python実行環境.
  • Open WebUI: フロントエンド.
    • DuckDuckGo外部検索.
    • Pythonグラフ生成 (Jupyter連携)

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 サービスに接続する事が出来るので,今後の拡張性も期待できます.


Date: 2026-03-08

Author: ozyukiwo

Created: 2026-03-11 水 08:08

Emacs 26.3 (Org mode 9.1.9)

Validate