モチベーション
できるだけ低コストで持続的に高性能な壁打ち用AIを使いたい
導入構成
どちらかというと備忘録的なものに近いです。 細かいところはいろいろな人がいろいろな媒体で記載していると思います。・ラズベリーパイ5 RAM 4GB ⇒ 初期費用約2.5万円
(だいたいAI課金$20/月なので8か月分くらい)
・Docker上にOllama + OpenWebUI
・cloudモデル:gemini-3-flash-preview:cloud
⇒Ollamaで無料アカウント登録必要。Ollamaのサイトで確認できる好きなモデルをダウンロードする。
・Cloudflare Tunnel + ドメイン取得 1000円弱/年
最終的な構成はおおよそこんな感じ
家族に使ってもらうことを想定して使用者が複数いるのでDocker上に同じ構成を2つ構築しています。Ollamaのユーザアカウントを複数使用するため、ローカルでOllamaを1つ動かすのではなくDocker上に導入しています。また、応答速度は普段使えるレベルが必要だったためcloudのLLM利用です。
無料のレンタルサーバでも良いかもしれませんが、ローカルLLMなど応用が利くのとラズパイ5が自宅で動いているとかわいい(※最重要)
ラズパイなので消費電力低め
RAM 2GBでも足りそうだったのですが、各部品を買い揃えるのと同等の値段で4GBスターターキットを買えると思うので、RAM 4GBになっています。
ローカルLLMにするにはスペック不足なので、cloud版利用かつ自宅外からでもアクセスできるようにします。要は入力したデータの学習に関して考慮しません。
Ollamaでpullするときに:cloudを外せばローカル版になります。
ラズパイ5での環境構築(Docker + Ollama + OpenWebUI)
別途Windows機器上でRaspberry Pi Imagerを使用して作成したSDカードからPiOS 64bitをセットアップします。中華製インストール済みSDカードを信用していない。(省略)1)Dockerインストール
公式スクリプトでDockerをインストール
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
dockerのバージョン確認
docker --version
>Docker version xx.x.x, build xxxxxxx
Dockerアクセス権限が必要なラズパイ端末のユーザーをdockerグループに追加する
sudo usermod -a -G docker ユーザ名
2)docker-composeをインストール
最新バージョンを確認
【参考】
curl -s https://api.github.com/repos/docker/compose/releases/latest | grep "tag_name"
>"tag_name": "v5.1.0",
docker-compose をダウンロード
v5.1.0の部分は表示された最新バージョンに適宜変更する。
sudo curl -L "https://github.com/docker/compose/releases/download/v5.1.0/docker-compose-linux-aarch64" -o /usr/local/bin/docker-compose
実行権限を付与
sudo chmod +x /usr/local/bin/docker-compose
docker-composeのバージョン確認
docker-compose --version
> Docker Compose version v5.1.0
docker-composeファイルを作成する場所
専用機ならディレクトリ作らずhome直下が楽
※例 ディレクトリ作成と移動
mkdir LLM
cd LLM
docker-compose.ymlを新規作成する。今はローカル構成です。2つも構築する必要はなく、1つでいい場合は -a のみ。-aも区別しているだけなので無くて良い。
この後、トークン消費を抑え、自宅外からアクセスできるようにするため変更していきます。
restart: unless-stoppedは本番移行時にalwaysへ変更しても良い。
docker-compose.yml
services:
ollama-a: # ユーザーA用
image: ollama/ollama:latest
container_name: ollama-a
ports:
- "127.0.0.1:11434:11434"
volumes:
- ollama_a_data:/root/.ollama
restart: unless-stopped
networks:
- net-a
ollama-b: # ユーザーB用
image: ollama/ollama:latest
container_name: ollama-b
ports:
- "127.0.0.1:11435:11434"
volumes:
- ollama_b_data:/root/.ollama
restart: unless-stopped
networks:
- net-b
open-webui-a: # ユーザーA用
image: ghcr.io/open-webui/open-webui:main
container_name: open-webui-a
ports:
- "3000:8080"
networks:
- net-a
volumes:
- open-webui-a:/app/backend/data
restart: unless-stopped
environment:
- OLLAMA_BASE_URLS=http://ollama-a:11434
open-webui-b: # ユーザーB用
image: ghcr.io/open-webui/open-webui:main
container_name: open-webui-b
ports:
- "3001:8080"
networks:
- net-b
volumes:
- open-webui-b:/app/backend/data
restart: unless-stopped
environment:
- OLLAMA_BASE_URLS=http://ollama-b:11434
# こんな風に書いておくとName固定してiptables書けるらしい
# 固定化に伴って開発・本番環境などがあってname被ると面倒のよう
networks:
net-a:
driver: bridge
name: bridge-llm-a
driver_opts:
com.docker.network.bridge.name: bridge-llm-a
net-b:
driver: bridge
name: bridge-llm-b
driver_opts:
com.docker.network.bridge.name: bridge-llm-b
volumes:
open-webui-a: # ユーザーA用
open-webui-b: # ユーザーB用
ollama_a_data: # ユーザーA用
ollama_b_data: # ユーザーB用
事前にcdでdocker-composeファイルがあるディレクトリに移動する。Docker起動に少し時間がかかる
docker compose up -d
必要なcloudのLLMをダウンロードする。
cloudモデルの場合、runするとURLが表示されるのでアクセスして各Ollamaアカウントでログインする。( :cloudを外すとログイン不要のローカル版。ただし圧倒的に性能が足りない )
# ollama-a にモデルをダウンロードする
docker exec -it ollama-a ollama pull gemini-3-flash-preview:cloud
# ollama-b にモデルをダウンロードする
docker exec -it ollama-b ollama pull gemini-3-flash-preview:cloud
# 確認
docker exec -it ollama-a ollama list
docker exec -it ollama-b ollama list
# モデルを実行
docker exec -it ollama-a ollama run gemini-3-flash-preview:cloud
docker exec -it ollama-b ollama run gemini-3-flash-preview:cloud
>https://ollama.com/connect?name=xxxxxxxxxxxxxxx
>上記URLにアクセスする。
>ollama.com のアカウントでログインし、「Connect」ボタンをクリックします。
# サインアウトは下記のようなコマンド
docker exec -it ollama-a ollama signout
docker exec -it ollama-b ollama signout
ネットワーク設定
この辺りは人に教えられるほど詳しくないので、適宜実施して欲しい。LAN内からの通信には少なくとも接続元IPアドレスに関する通信許可と3000(3001)ポート解放は必要。
ここまでくると内部ネットワークから http://IPアドレス:3000 または3001でOpenWebUIにアクセスして使えるはず。
Cloud Usageの使用量調査と使用量減
上記の設定で1つ質問を投げるとOllama上のCloud Usageではhours 1~2% Weekly1~1.5%で使用量が増加していく。消費を抑えるためにdocker-compose.ymlの設定を見直す。一部はOpenWebUIの管理者パネルの設定からも可能なよう。
とりあえずclaude codeに頼る。コメントもつけてもらった。
RAGに関してはスペック不足でcloudに頼らざるを得ない。
バージョン確認も本来は定期的に確認して自動でアップデートするように処理を入れた方がいいかもしれないが、変更に伴い動かなくなる可能性があるので今回は未導入。
docker-compose.yml内にあるenvironment:に以下の記述を追加する。すると上記の設定で質問を投げるとhours 0.5~1% Weekly0.3~0.5%くらいに減少した。
weeklyで200~300程度問合せできるようになったためヘビーな使い方をしない限りは大丈夫そう。
RAMに余裕があったりAI Hat+2なんかでローカル版を動かしているとcloud版が使えなくなった時そっちのモデルが使える。その場合SearXNGでWeb検索はできるようにしたほうが精度はよくなる。
environment:配下に追記
environment:
- DEFAULT_PROMPT_SUGGESTIONS=false # 初期画面の提案を無効化(起動時の英語の提案が消えます)
- ENABLE_CHAT_SUGGESTIONS=false # 返答後の提案(プロンプトサジェスト)を無効化
# ================================================================
# テレメトリ・使用状況追跡の無効化
# これらはモデルとは無関係にバックグラウンドで外部送信されている
# ================================================================
# Open WebUIの匿名使用統計をScarfサービスへ送信しない
- SCARF_NO_ANALYTICS=true
# Open WebUI側へのトラッキングを拒否するフラグ
- DO_NOT_TRACK=true
# ================================================================
# 不要な外部通信の無効化
# ================================================================
# GitHubへのバージョン確認通信を無効化
# (起動時・定期的にGitHubにアクセスしている)
- WEBUI_VERSION_CHECK=false
# 外部Web検索API(SearXNG等)への通信を無効化
- ENABLE_RAG_WEB_SEARCH=false
# DALL-E等の外部画像生成APIへの通信を無効化
- ENABLE_IMAGE_GENERATION=false
# ================================================================
# トークン消費削減
# 以下は1回の質問ごとに「追加でAPIを呼び出す」機能の無効化
# ================================================================
# 入力中のオートコンプリート候補生成を無効化
# (キー入力のたびにAPIを呼ぶため消費が大きい)
- ENABLE_AUTOCOMPLETE_GENERATION=false
# 回答後に表示される「関連質問」の自動生成を無効化
# (1回の質問につき追加でAPI呼び出しが発生する)
- ENABLE_FOLLOW_UP_GENERATION=false
# 会話内容からタグを自動生成する機能を無効化
# (会話ごとに追加でAPI呼び出しが発生する)
- ENABLE_TAGS_GENERATION=false
# 会話タイトルの自動生成を無効化
# (新規会話開始時に追加でAPI呼び出しが発生する)
- ENABLE_TITLE_GENERATION=false
# ユーザーの会話内容からMemory(記憶)を自動生成する機能を無効化
# (バックグラウンドでAPI呼び出しが発生する)
- ENABLE_MEMORY_FEATURE=false
# ユーザーイベント時のWebhook送信を無効化
- ENABLE_USER_WEBHOOKS=false
# コンテキスト長を制限 会話が長くなると前の内容を忘れます。
- DEFAULT_CONTEXT_WINDOW_SIZE=2048
# モデル評価機能無効
- ENABLE_EVALUATION_ARENA_MODEL=false
# コミュニティ共有無効
- ENABLE_COMMUNITY_SHARING=false
# モデルが1回の返答で生成する最大トークン数を512に制限
# (デフォルトはモデルの上限まで使用するため消費が大きい)
# ※ 長い回答が必要な場合は1024〜2048に調整してください
- DEFAULT_MAX_TOKENS=512
docker-compose再起動で再ログイン不要にするなら追記
OpenWebUIのアップデート頻度はそこそこ多い
ログインセッションを固定する
environment:
- WEBUI_SECRET_KEY=${SECRET_KEY}
ランダムなキーを生成
openssl rand -hex 32
.envに追記
パーティション変更は chmod 600 .env
SECRET_KEY=zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz
外部からのWebアクセス
家族にできたから使えるよ!というと、学習に関して考慮しないならなんで外で使えないの…と外で使えないなら使わないくらいの勢いで言われたので外から使えるように改修することになりました。 最もなご指摘である..........。今回はドメイン取得にCloudflareを使用したので約1000円/年の課金が発生します。
Cloudflareには便利なTunnel機能があったのでそれも使いました。
そのうち無料ドメインに切り替えるかも?
CloudflareのTunnel機能を使うと簡単に外からでも接続できるようになり、Zero Trust無料版でも国内アクセスのみとE-mailのワンタイムパスワード認証を通せばセキュリティ的にはかなり防げるように思いました。(スマホにこのためだけのアプリなどを入れたくない、複雑な操作したくないという要求もあった)
1)Cloudflareにアカウント登録する
2)左メニュー「ドメイン」⇒「ドメインを購入する」⇒好きなドメイン1つ購入する。
例:llmhogehoge.hoge サブドメインを設定するので購入は1つでok
(省略)
cloudflaredのインストール
curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-arm64.deb -o cloudflared.deb
sudo dpkg -i cloudflared.deb
Cloudflareにログイン & トンネル作成
# ログイン(ブラウザが開く)
cloudflared tunnel login
# トンネルを2つ作成
cloudflared tunnel create webui-a
cloudflared tunnel create webui-b
# 例: ドメインが llmhogehoge.hoge の場合
cloudflared tunnel route dns webui-a usera.llmhogehoge.hoge
cloudflared tunnel route dns webui-b userb.llmhogehoge.hoge
Cloudflareのネットワーク→Tunnelsに表示されるのでDockerを選択
コマンド部分をコピーして、記載されているトークンを覚えておく。
この段階ではまだステータスは非アクティブ
compose.ymlの修正
services:配下に追加して、docker compose down とdocker compose up -dで再起動
services:
cloudflared-a:
image: cloudflare/cloudflared:latest
container_name: cloudflared-a
restart: unless-stopped
command: tunnel --no-autoupdate run --token ${TUNNEL_TOKEN_A}
networks:
- net-a
cloudflared-b:
image: cloudflare/cloudflared:latest
container_name: cloudflared-b
restart: unless-stopped
command: tunnel --no-autoupdate run --token ${TUNNEL_TOKEN_B}
networks:
- net-b
トークンの変数を.envファイルに記述する
vi .env
TUNNEL_TOKEN_A=xxxxxxxxxxxx
TUNNEL_TOKEN_B=yyyyyyyyyyyy
.envのパーミッション変更
chmod 600 .env
ls -la .env
>-rw------- ならok
これだけでは国外からもアクセスされるので、CloudflareのZero Trust無料版で国内アクセスのみ、かつ利用しているメールアドレスのみにワンタイムパスワードが送信されるようにする。実際、1日放置しただけでCloudflare上でアメリカと台湾からアクセスしてるよという形跡があった。
Cloudflare>Zero Trust>ポリシー
例 セッション時間は適宜
アプリケーション>アプリケーションを追加する>セルフホスト
・基本情報設定
・パブリック ホスト名を追加
・既存のポリシーを選択
おまけ
ネットワーク設定iptablesを試行錯誤しながら設定した。ping、LAN、WANなどから接続確認するとある程度期待通り設定はできていそうだった。CloudflareのTunnelからアクセスすることを想定しているので、プライベートIPアドレスからの通信は許可していない。SSHも許可していない。DHCPではなく静的にIPアドレスを決定しているので塞いでいる。
Docker内部ネットワークのブリッジ名をIDではなく、名前で設定している。
名前はdocker-compose.ymlで設定している。
Cloudflare TunnelはQUIC(443/UDP)を使っているのとport:7844/UDPも使っているようだった
# netfilter-persistentをインストールすると、ufwはインストール中に削除された
sudo apt install netfilter-persistent iptables-persistent -y
# いくつかのACCEPTルールでそもそもパケットが通過していないことを観測したが、
# しばらく放置後に確認コマンドをうち適宜廃止して最適化すればよい
# 確立済み通信
sudo iptables -A DOCKER-USER -m state --state ESTABLISHED,RELATED -j ACCEPT
# DNS
sudo iptables -A DOCKER-USER -p udp --dport 53 -j ACCEPT
# Cloudflare Tunnel
sudo iptables -A DOCKER-USER -p udp --dport 443 -j ACCEPT
sudo iptables -A DOCKER-USER -p tcp --dport 443 -j ACCEPT
sudo iptables -A DOCKER-USER -p udp --dport 7844 -j ACCEPT
sudo iptables -A DOCKER-USER -p tcp --dport 7844 -j ACCEPT
# Docker内部ネットワーク間
sudo iptables -A DOCKER-USER -i bridge-llm-a -j ACCEPT
sudo iptables -A DOCKER-USER -i bridge-llm-b -j ACCEPT
# それ以外DROP
sudo iptables -A DOCKER-USER -j DROP
# 確認
sudo iptables -L DOCKER-USER -n -v
======================================
# ループバック
sudo iptables -A INPUT -i lo -j ACCEPT
# 確立済み接続
sudo iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
# Docker内部ネットワーク
sudo iptables -A INPUT -i bridge-llm-a -j ACCEPT
sudo iptables -A INPUT -i bridge-llm-b -j ACCEPT
# それ以外DROP
sudo iptables -P INPUT DROP
# 確認
sudo iptables -L INPUT -n -v
======================================
# ループバック
sudo iptables -A OUTPUT -o lo -j ACCEPT
# 確立済み接続
sudo iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
# NTP
# 自宅ルーターからルーターに記載しているNTPサーバへ転送しているのでルーター宛
sudo iptables -A OUTPUT -d [ルーターIPアドレス] -p udp --dport 123 -j ACCEPT
# DNS apt installなどで必要
# 自宅ルーターからルーターに記載しているDNSサーバへ転送しているのでルーター宛
sudo iptables -A OUTPUT -d [ルーターIPアドレス] -p udp --dport 53 -j ACCEPT
sudo iptables -A OUTPUT -d [ルーターIPアドレス] -p tcp --dport 53 -j ACCEPT
# ラズパイ自身のWeb検索(HTTP/HTTPS/QUIC) apt installなどで必要
sudo iptables -A OUTPUT -p tcp --dport 80 -j ACCEPT
sudo iptables -A OUTPUT -p udp --dport 443 -j ACCEPT
sudo iptables -A OUTPUT -p tcp --dport 443 -j ACCEPT
# Docker内部ネットワーク ラズパイからDockerへの管理コマンドなどで必要
sudo iptables -A OUTPUT -o bridge-llm-a -j ACCEPT
sudo iptables -A OUTPUT -o bridge-llm-b -j ACCEPT
# それ以外DROP
sudo iptables -P OUTPUT DROP
# 確認
sudo iptables -L OUTPUT -n -v
======================================
# 確認
sudo iptables -L -n -v
# 永続化
sudo netfilter-persistent save
cloudflare Tunnel接続時に接続できるものの一箇所BufferSizeエラーが出るので直した。結果的には/etc/sysctl.d/に専用ファイルを作って対応した。
docker compose logs cloudflared-a --tail=30
# エラー内容
failed to sufficiently increase receive buffer size (was: 208 kiB, wanted: 7168 kiB, got: 416 kiB). See https://github.com/quic-go/quic-go/wiki/UDP-Buffer-Sizes for details.
設定変更
# 即時適用
sudo sysctl -w net.core.rmem_max=7340032
sudo sysctl -w net.core.rmem_default=7340032
sudo sysctl -w net.core.wmem_max=7340032
sudo sysctl -w net.core.wmem_default=7340032
# 永続化
echo "net.core.rmem_max=7340032" | sudo tee -a /etc/sysctl.conf
echo "net.core.rmem_default=7340032" | sudo tee -a /etc/sysctl.conf
echo "net.core.wmem_max=7340032" | sudo tee -a /etc/sysctl.conf
echo "net.core.wmem_default=7340032" | sudo tee -a /etc/sysctl.conf
# 確認
sysctl net.core.rmem_max
sysctl net.core.wmem_max
################################################
#上記の設定を行ったが、再起動後に設定がもとに戻ったので
#/etc/sysctl.d/ に専用ファイルを作って対応した
#(上記sysctl.confの内容は削除せずそのまま適用している)
################################################
sudo tee /etc/sysctl.d/99-cloudflared.conf << EOF
net.core.rmem_max=7340032
net.core.rmem_default=7340032
net.core.wmem_max=7340032
net.core.wmem_default=7340032
EOF
# 確認
sysctl net.core.rmem_max
sysctl net.core.wmem_max
あとがき
無事に使えるようになった。ラズパイ・Dockerの導入として分かりやすく成果物ができ初めて触るには丁度良かった。
備忘録 誰かの参考になれば





