先に結論
- 自分のPCで動かす Gemma 4(Ollama) に FastAPIの小さな窓口を付けて、ターミナルもVS Codeも無しに、ブラウザから使えるようにした
- 使う人は VS Codeもコマンドも不要。ブラウザでURLを開くだけ
- 入力内容はPCの外に出ないので、クラウドAIに貼りづらい社内の話も安心して使える
- 最小構成から始めて、チャット形式のUI・Markdown整形・ファイル添付・会話履歴のフォルダ保存まで育てた過程も紹介します
- 「ローカルLLMって外部からアクセスされないの?」というセキュリティの疑問にも、仕組みから答えます
- 想定は 各自が自分のPCに入れて使う 形。1台を大勢で共有するのは無理(後述:1回の生成でCPUを半分ほど使うため)なので、そこも正直に書きます
きっかけ:VS Codeで使えた。でも周りは?
前回の記事で、VS Code + Ollama を使って Gemma 4 をローカルで動かし、Apexコードを書かせて検証しました。ローカル実行なので入力したコードが外部に送信されないのが大きな利点です。
ただ、この方法には壁があります。使うのにVS Codeが必要なこと。エンジニアはいいですが、営業や企画の人に「まずVS Codeを入れて、拡張機能を設定して……」とは言えません。
そこで思いつくのがこれです。
ブラウザからアクセスできる形にすれば、誰でも使えるのでは?
やってみたら、意外とあっさりできました。
仕組み:Ollamaの前に「窓口」を1枚立てるだけ
VS Code拡張がやっていたことは、突き詰めると 「Ollamaに質問を投げて、答えを受け取る」 だけです。であれば、その窓口をブラウザ向けに作ればいい。今回は FastAPI(Pythonの軽量Webフレームワーク)で作ります。
┌──────────────┐
│ Ollama │ 127.0.0.1:11434(同じPC内)
│ (常駐) │
└──────┬───────┘
│ HTTP API(すべて自分のPC内で完結)
┌──────┴───────┐
│ FastAPI │ 127.0.0.1:8000 ← ブラウザ向けの窓口
│ (app.py) │ Markdown整形・履歴保存などを担う
└──────┬───────┘
│ ブラウザ
┌──────┴───────┐
│ 自分のブラウザ │ ターミナル/VS Code 不要
└──────────────┘
全部が1台のPCの中で完結します。これを各自のPCに入れて、それぞれ自分専用のローカルAIとして使う、というのが基本の形です。
Ollama そのものは信頼していいのか
土台に使う Ollama は、Google製ではなく サードパーティ(Ollama Inc.) のオープンソースソフト(MITライセンス)です。ただし広く使われている定番ツールで、Googleの公式ドキュメントにも Ollama 連携ページがあるくらいなので、素性としては問題ありません。注意点は入手先だけで、必ず 公式サイト ollama.com から入れること(「ollama 無料ダウンロード」等で出てくる非公式サイトは踏まない)。
なぜ Ollama を直接ブラウザに出さないのか
FastAPIを挟まず「Ollamaを直接ブラウザから叩けばいいのでは?」と思うかもしれませんが、これは避けたほうが無難です。Ollamaはそのままネットワークに出すと、無認証・無制限で叩ける状態になります。間にFastAPIを一枚挟んでおけば、認証を足す・ログを取る・整形するといった余地を常に自分の手元に残せます。Ollama自体は 127.0.0.1 のまま内側に隠しておくのが安全です。
作り方
ファイルは2つだけ
app.py(FastAPI本体。HTMLフォーム + Ollamaへの中継が1ファイルに収まっています)
"""社内向け Ollama チャットフォーム(FastAPI)
ブラウザ ──> FastAPI(:8000) ──> Ollama(127.0.0.1:11434)
起動: uvicorn app:app --host 0.0.0.0 --port 8000
"""
import json
import os
import httpx
from fastapi import FastAPI, Form
from fastapi.responses import HTMLResponse, StreamingResponse
OLLAMA = os.environ.get("OLLAMA_URL", "http://127.0.0.1:11434")
MODEL = os.environ.get("OLLAMA_MODEL", "gemma4:12b")
app = FastAPI(title="社内AIチャット")
PAGE = """
<!doctype html><meta charset="utf-8"><title>社内AIチャット</title>
<style>body{font-family:sans-serif;max-width:760px;margin:40px auto;padding:0 16px}
textarea{width:100%;height:120px}#out{white-space:pre-wrap;border:1px solid #ccc;
padding:16px;margin-top:16px;min-height:120px;border-radius:8px}</style>
<h2>社内AIチャット(Gemma 4 / ローカル実行)</h2>
<form id="f"><textarea name="prompt" placeholder="質問を入力"></textarea>
<button>送信</button></form><div id="out"></div>
<script>
f.onsubmit = async e => {
e.preventDefault(); out.textContent = "";
const r = await fetch("/chat", {method:"POST", body:new FormData(f)});
const rd = r.body.getReader(), dec = new TextDecoder();
for(;;){ const {value,done} = await rd.read(); if(done) break;
out.textContent += dec.decode(value); }
};
</script>
"""
@app.get("/", response_class=HTMLResponse)
def index():
return PAGE
@app.post("/chat")
async def chat(prompt: str = Form(...)):
async def gen():
payload = {
"model": MODEL,
"messages": [{"role": "user", "content": prompt}],
"stream": True,
}
async with httpx.AsyncClient(timeout=None) as c:
async with c.stream("POST", f"{OLLAMA}/api/chat", json=payload) as r:
async for line in r.aiter_lines():
if not line:
continue
d = json.loads(line)
if tok := d.get("message", {}).get("content"):
yield tok
return StreamingResponse(gen(), media_type="text/plain; charset=utf-8")
やっていることはシンプルで、ブラウザの入力を受け取り → 同じPC内のOllamaに中継し → 返ってきた答えを1文字ずつブラウザに流す(ストリーミング)、それだけです。使うモデルは環境変数 OLLAMA_MODEL で切り替えられます(デフォルトは gemma4:12b)。
requirements.txt
fastapi
uvicorn[standard]
httpx
python-multipart
手順(Windows)
前提として、前回の導入手順で ollama run gemma4:12b が動く状態になっていることとします。
1. Python環境を用意
作業フォルダで(※OneDriveの外に置くのがおすすめ。OneDrive内に .venv を作ると同期が走って重くなることがあります):
python -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install -r requirements.txt
⚠️ 使っているシェルに注意。上記は PowerShell 用です。コマンドプロンプト(cmd) を使っている場合は、有効化コマンドが違います:
.venv\Scripts\activate.bat
Activate.ps1はPowerShell専用なので、cmdで打つと「認識されていません」と怒られます。プロンプトの先頭に(.venv)が付けば成功です。
2. まず自分だけで動作確認
Ollamaが起動している(タスクトレイにラマのアイコン)ことを確認してから:
uvicorn app:app --host 127.0.0.1 --port 8000
ブラウザで http://127.0.0.1:8000 を開き、質問を入力して文字が流れてくれば成功です。127.0.0.1 は自分のPCからのみアクセス可能なので、この段階では他の人からは見えません。
3.(おまけ)自分のスマホなど、同じLANの別端末から使う
「自分のPCで動かしているものを、手元のスマホからも使いたい」という場合は公開モードで起動します。ただし後述の通り複数人で同時に使うのは無理なので、これは"自分の別端末から使う"程度の用途と考えてください。
自分のPCのIPアドレスを調べ(ipconfig の「IPv4 アドレス」、例 192.168.1.23)、公開モードで起動します:
uvicorn app:app --host 0.0.0.0 --port 8000
初回起動時に Windowsファイアウォールの確認ダイアログが出たら、「プライベート ネットワーク」だけにチェックして許可します(パブリックにはチェックを入れない)。
あとは別端末のブラウザで http://192.168.1.23:8000(控えたIPに読み替え)を開けば使えます。
社内に配るなら、最低限の「合言葉」を
今回の app.py には認証がありません。URLを知っている人は誰でも使える状態です。「うっかり誰かが開いてしまう」のを防ぐくらいの、簡単な合言葉を付けておくと安心です。chat() に数行足すだけです。
from fastapi import HTTPException
PASSWORD = os.environ.get("APP_PASSWORD", "")
@app.post("/chat")
async def chat(prompt: str = Form(...), password: str = Form("")):
if PASSWORD and password != PASSWORD:
raise HTTPException(status_code=401, detail="合言葉が違います")
...
HTML側のフォームに <input type="password" name="password" placeholder="合言葉"> を足し、起動時に合言葉を設定します:
$env:APP_PASSWORD = "好きな合言葉"
uvicorn app:app --host 0.0.0.0 --port 8000
※これはあくまで簡易的なもの。本格的な認証が必要なら、ユーザー管理付きの仕組みを別途検討してください。
使いにくかったので直していったら、Claude Code みたいになった
最小構成でもちゃんと動くのですが、自分で実際に使ってみると細かいストレスが溜まります。送信したのか分からない、コードがベタ書きで読みにくい、前の会話が消える、長文を貼ると入力欄が窮屈……。気になるところを潰していったら、結果的に Claude Code や ChatGPT で見慣れたUI に近づいていって、一気に使いやすくなりました。
最終的に入った主なもの:
- チャット形式+Markdown整形:見出し・箇条書き・コードブロック(コピーボタン付き) を整形表示。回答はストリーミングで1文字ずつ流れる
- 「考え中…」表示:送信直後にすぐ出す。初回のモデル読み込み(数十秒)で固まったように見えても安心
- Claude風の入力欄:角丸のカード。左の「+」でファイル添付、右に丸い送信ボタン
-
ファイル添付:テキストファイル(.cls / .txt / .csv など)は中身を読んで質問に添える。「このApexのバグ見て」と
.clsを添付、という使い方ができる - 会話履歴をローカルフォルダに保存:過去の会話をサイドバーで切り替え(後述)
見た目を見慣れたものに寄せた副産物として、操作に迷わなくなったのが大きいです。「+は添付」「新しい会話は左上」といった配置は、みんな普段から触っているので、説明しなくても伝わりました。
会話履歴を「PCのフォルダ」に貯める
履歴の保存先には2択あります。
- ブラウザに保存(localStorage):手軽だが容量が小さく(5MB程度)、ブラウザごとにバラバラ
- サーバー(このPC)のフォルダに保存:1会話=1個のJSONファイル。何本でも貯まり、どのブラウザからでも同じ履歴が見える ← 今回はこちら
サーバー側に保存用のエンドポイントを足すだけです。
from pathlib import Path
import time
CONV_DIR = Path(os.environ.get("CONV_DIR", "conversations"))
CONV_DIR.mkdir(exist_ok=True)
def conv_path(cid: str) -> Path:
# パストラバーサル対策:英数字・ハイフンのみ許可
safe = "".join(ch for ch in cid if ch.isalnum() or ch in "-_")
return CONV_DIR / (safe + ".json")
@app.get("/api/conversations") # 一覧
def api_list():
items = []
for p in CONV_DIR.glob("*.json"):
d = json.loads(p.read_text(encoding="utf-8"))
items.append({"id": d["id"], "title": d.get("title"), "updated": d.get("updated", 0)})
return sorted(items, key=lambda x: x["updated"], reverse=True)
@app.put("/api/conversations/{cid}") # 保存
async def api_put(cid: str, req: Request):
data = await req.json()
obj = {"id": cid, "title": data.get("title", "(無題)"),
"updated": int(time.time()), "messages": data.get("messages", [])}
conv_path(cid).write_text(json.dumps(obj, ensure_ascii=False), encoding="utf-8")
return {"ok": True}
これで conversations/ フォルダに会話が貯まり、サーバーを再起動しても・別のブラウザから開いても履歴が残るようになりました。
依存ライブラリを増やさない工夫:Markdown整形やコードの見た目は、marked.js などのCDNライブラリを使うと手軽ですが、それだとネットが無いとUIが崩れます。「オフラインで動くローカルAI」という利点を守るため、整形処理はすべてHTML内に自前で書き、ネットを切っても完全に動くようにしました。
正直な注意点(ここが大事)
動くと嬉しくてつい「社内に配ろう!」となりますが、今回の構成は"みんなで本格運用"には向いていません。隠さず書きます。
1. 1台を大勢で共有するのは無理(=各自のPCで動かす)
ここが今回の一番大事な前提です。1回の生成だけでもCPUを半分ほど使い切るため、1台のPCを窓口にして複数人が同時に使う、という運用は現実的に成り立ちません。2人目のリクエストは激遅になるか、待たされます。
なので本記事は 「各自が自分のPCに Gemma 4 + このアプリを入れて、自分専用で使う」 ことを前提にしています。共有サーバーを立てるのではなく、アプリを各自に配って、それぞれのPCで動かすイメージです。
2. 動くのは自分のPCが起きている間だけ
OllamaもFastAPIも自分のPCの上で動くので、電源オフ・スリープ・Ctrl+C で止まります。常時起動のサービスではなく、自分が使いたいときに立ち上げるツール、という位置づけです。とはいえ各自のPCで完結するので、誰かのPCに依存して全員が止まる、といった心配はありません。
3. 認証は「合言葉」程度
上で付けた合言葉は、あくまで気休めレベル。インターネットに公開するのは論外で、ルーターのポート開放は絶対にしないでください。使うのは社内LANの中だけに留めます。
4. 会話履歴は自分のPC内に貯まる
履歴は conversations/ フォルダに保存されます。各自のPCで動かす前提なので、そこに貯まるのは自分の会話だけ。外部にも他人にも渡りません。プライバシー面はむしろ安心ですが、裏を返すとバックアップは自分でとる必要があります(フォルダごとコピーしておけばOK)。履歴を残したくないなら、保存処理を外せば1問1答に戻せます。
5. プライバシーはローカルならではの強み
入力内容は自分のPC内だけで処理され、外部サーバーには一切送信されません。クラウドAIに貼りづらい社内コードの下書きなどと相性が良いのは、この点です。(例外:後述のおまけ機能で他の端末に開放した場合は、その入力が自分のPCを通ります。そのときだけ扱いに注意)
6. 出力は鵜呑みにしない
ローカルモデルは誤りが普通に出ます。社内に広く配ると「AIが言ったんだから正しい」と受け取る人が必ず出ます。フォームに一言、注意書きを添えておくのがおすすめです。
「ローカルLLMって、外部からアクセスされないの?」
社内に配る話をすると必ず聞かれるのがこれです。結論から言うと 設定次第。どういう状態だと誰が届くのか、仕組みから整理します。
起動オプションで「誰が届くか」が変わる
| 起動コマンド | 届く範囲 |
|---|---|
--host 127.0.0.1 |
自分のPCだけ。他のどの機器からも見えない(一番安全) |
--host 0.0.0.0 |
同じLAN上の機器すべてから届きうる(社内公開はこれ) |
0.0.0.0 は「すべてのネットワーク接続口で待ち受ける」という意味で、これが"外部に開く"の実体です。
では「インターネットの誰か」に届く? → 通常は届かない
ここが一番誤解されるところ。0.0.0.0 にしても、そのままではインターネット上の他人はアクセスできません。理由は、家庭や社内のルーターが NAT で守っているから。あなたのPCのアドレス(例 192.168.1.23)はLAN内部だけの住所で、外のインターネットからは直接指定できません。
つまり、インターネットに晒されるのは「自分で意図的に穴を開けたとき」だけです。具体的には:
- ルーターのポート開放(ポートフォワーディング)をした → これは絶対にやらない
- ngrok / Cloudflare Tunnel などのトンネルを通した → 便利だが、認証無しで通すと世界中に公開される
-
Ollama自体を
OLLAMA_HOST=0.0.0.0で公開した → FastAPIを飛ばしてOllamaが直接叩ける状態に。認証が一切ないので特に危険
現実的に気をつけるのは、この3つ
-
信頼できないネットワークで
0.0.0.0にしない:カフェやコワーキングのWi-Fiで公開モードにすると、同じネットワークにいる他人から届きます。ファイアウォールは必ず**「プライベート」だけ許可**(前述)にして、自宅・社内の閉じたLANに限定する - ルーターのポート開放はしない:これをやった瞬間、インターネット全体に開きます
- 本当に外から使いたいなら、VPN か 認証付きトンネル:素で公開せず、社内VPN経由にするか、トンネル+認証を必ずセットにする
この2層構成が効いてくる
今回の「Ollamaはlocalhostのまま、前段のFastAPIだけ公開」という構成は、ここで活きます。仮にFastAPIをLANに公開しても、Ollama本体は外から直接触れないので、認証やログを差し込む余地を常に自分の手元(FastAPI層)に残せます。「万一のときに守りを足せる場所がある」というのが、この一手間の価値です。
それでも、価値がある
この構成の価値は**「各自が、無料で、安全な"自分専用のAIチャット"を持てる」**ことに尽きます。
- サーバー契約も、API課金も、面倒な申請も要らない
- 入力内容が自分のPCの外に出ないので、クラウドAIに貼りづらい話も気兼ねなく使える
- ターミナルもVS Codeも触らず、ブラウザの使い慣れたUIで使える
大勢で1台を共有する用途には向きませんが、**「アプリを各自に配って、それぞれのPCで動かす」**なら、今日から始められます。まずは自分の手元で、ローカルLLMの実力を体感する入口としてぜひ。
まとめ
- Ollama + FastAPI(
app.py)で、Gemma 4を**ブラウザから使える"自分専用のAIチャット"**にできた - 使う側はVS Codeもコマンドも不要、ブラウザでURLを開くだけ
- 使いながらUIを育て、チャット形式・Markdown整形・ファイル添付・会話履歴のフォルダ保存まで実装した
- 外部アクセスは設定次第。
127.0.0.1なら自分だけ、0.0.0.0でも通常はLAN内まで(インターネットはルーターのNATが遮断)。ポート開放だけは絶対にしない - 入力は外に出ない安全性はそのまま、非エンジニアでもブラウザで使える
- ただし 1台を大勢で共有するのは無理(1回の生成でCPUを半分ほど使う)。アプリを各自に配って、それぞれのPCで動かすのが現実解
※検証環境: Windows 11 / Python 3.12 / Ollama + gemma4:12b。モデルの出力は実行ごとに変動します。
