本文へ移動
購読
GUIDE#ai-ml更新 2026.09.24

Open WebUIで社内ChatGPT環境を作る:Ollama連携とDocker Composeでの構築・運用

Ollama同梱のcompose.yaml、GPU割り当て、管理者作成と登録制御、RAG設定、nginx/CaddyでのHTTPS化とSSO、バックアップと更新までを実際のエラー例つきで解説します。

level
中級
time
約25分
stack
Open WebUI / Ollama / Docker Compose / Ubuntu 24.04
author
@ryoma_t
// 前提条件
DockerとDocker Composeの基本操作 / Linuxサーバーへのssh接続 / 社内DNSと証明書の準備(HTTPS化する場合)
Open WebUIの管理画面でOllama接続を設定したところ
画像:nagoya.dev(実際の画面・実行結果をもとに作成)

Open WebUIとは、Ollamaや各種LLM APIをブラウザから使うためのセルフホスト型チャットUIです。ユーザー管理、会話履歴、文書アップロード(RAG)、モデル切り替えが最初から入っており、Dockerコンテナ1つで動きます。ライセンスは執筆時点で独自の「Open WebUI License」で、直近30日間のエンドユーザーが50人を超える環境では「Open WebUI」のブランド表示を変更・削除できない条項があります(エンタープライズライセンスで解除可)。自社内で使う分には支障になりにくい条件ですが、導入前にLICENSEファイルの原文を読んでください。

ここでは、Ubuntu 24.04上でOllamaを同梱したDocker Compose構成を作り、管理者作成と登録制御、モデルの追加、RAG機能の設定と限界、nginx・CaddyでのHTTPS化と社内認証、バックアップと更新までを、実際に出るエラーと原因を添えて進めます。

この記事でわかること

  • ブラウザ→Open WebUI→Ollamaの通信経路と、プロキシでWebSocket転送とバッファ無効化が必要な理由
  • GPUあり・なし両対応の compose.yaml と、Ollamaをコンテナに入れるかホストに置くかの判断基準
  • 初回管理者の決まり方と、環境変数が2回目以降に効かなくなる「PersistentConfig」の仕組み
  • 文書アップロード(RAG)で最初に変える設定4つと、権限管理の限界
  • 更新前にバックアップが必須な理由(データベース移行は一方向)

全体構成と通信の流れ

構成は3層です。ブラウザはHTTPS(443)でリバースプロキシへ、プロキシはHTTP(8080)でOpen WebUIコンテナへ、Open WebUIはHTTP(11434)でOllamaへ接続します。Open WebUIの実体はPython(FastAPI)のバックエンドとSvelteKitのフロントエンドで、ユーザー・会話・設定は /app/backend/data/webui.db(SQLite)に、アップロード文書は uploads/、RAG用のベクトルは vector_db/(既定はChroma)に保存されます。

構成図
[ブラウザ] --HTTPS 443--> [nginx / Caddy] --HTTP 8080--> [open-webui コンテナ]
                                                              |
                                                     HTTP 11434(compose 内ネットワーク)
                                                              v
                                                       [ollama コンテナ] --> GPU

応答の逐次表示にはSSEとWebSocket(Socket.IO)を使うため、間に入るプロキシには Upgrade ヘッダの転送とバッファリングの無効化が要ります。この2つを落とすと「モデル一覧は出るのにチャットが固まる」「Markdownが崩れて一気に表示される」という症状になり、原因がプロキシ側にあると気づくまで時間を溶かします。

compose.yaml(Ollama同梱、GPUなし)

compose.yaml
services:
  ollama:
    image: ollama/ollama:latest
    container_name: ollama
    volumes:
      - ollama:/root/.ollama
    environment:
      OLLAMA_KEEP_ALIVE: 30m
      OLLAMA_CONTEXT_LENGTH: 8192
    restart: unless-stopped
    # ports を書かない: 11434 は compose 内ネットワークからしか到達できない

  open-webui:
    image: ghcr.io/open-webui/open-webui:main
    container_name: open-webui
    depends_on:
      - ollama
    ports:
      - "127.0.0.1:3000:8080"        # 同一ホストのリバースプロキシからのみ
    volumes:
      - open-webui:/app/backend/data
    environment:
      OLLAMA_BASE_URL: http://ollama:11434
      WEBUI_SECRET_KEY: ${WEBUI_SECRET_KEY}   # .env に置く(openssl rand -hex 32)
      WEBUI_URL: https://chat.corp.example
      ENABLE_SIGNUP: "true"                   # 初回起動時のみ読まれる。管理者作成後は UI で制御
      DEFAULT_USER_ROLE: pending              # 新規登録者は管理者の承認待ち
      ENABLE_COMMUNITY_SHARING: "false"
    restart: unless-stopped

volumes:
  ollama:
  open-webui:

要点は3つです。ollamaサービスに ports を書かないので、11434番はComposeの内部ネットワークからしか到達できず、無認証のAPIがLANに露出しません。open-webuiは 127.0.0.1:3000 にだけ束縛し、同一ホストのプロキシ経由以外では開きません。WEBUI_SECRET_KEY はセッショントークン(JWT)の署名鍵で、執筆時点の実装では認証有効時に未設定だと「WEBUI_SECRET_KEY is not set. It is a hard requirement when authentication is enabled.」で起動が止まります。値を変えると全員がログアウトされるので、.envで固定してバックアップにも含めます。

terminal
sudo mkdir -p /srv/open-webui && cd /srv/open-webui
echo "WEBUI_SECRET_KEY=$(openssl rand -hex 32)" | sudo tee .env >/dev/null
sudo chmod 600 .env
docker compose up -d
docker compose exec ollama ollama pull gemma4:12b    # チャット用(7.6GB、執筆時点)
docker compose exec ollama ollama pull bge-m3        # RAG の埋め込み用(多言語)

GPUあり構成(NVIDIA)とOllamaの置き場所

NVIDIA GPUをコンテナに渡すには、ホストにドライバとNVIDIA Container Toolkitを入れ、Dockerランタイムに登録します。GPUの割り当てはCompose標準の deploy.resources.reservations.devices で書き、別ファイルにしておくとGPUのない検証機でも同じ構成一式が動きます。

terminal
# NVIDIA Container Toolkit(公式 apt リポジトリを追加した後)
sudo apt-get install -y nvidia-container-toolkit
sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker
docker run --rm --gpus all ubuntu nvidia-smi        # GPU の表が出れば OK
compose.override.yaml
services:
  ollama:
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: all
              capabilities: [gpu]

compose.override.yaml は docker compose が自動で読み込んでマージするファイルです。GPUを積んだ本番機にだけ置けば、compose.yaml本体は検証機と共通のまま保てます。

Ollamaをコンテナに同梱するか、ホストのsystemdサービスとして動かすかは、用途で決めます。

観点Ollamaをコンテナに同梱Ollamaをホストで常駐
GPUドライバの扱いContainer Toolkitが必要。CUDAライブラリはイメージ側ホストのドライバだけで動く
他ツール(CLI、Dify、スクリプト)との共用portsを開けない限り不可127.0.0.1:11434 をそのまま共用できる
LANへの露出portsを書かなければゼロ0.0.0.0で待ち受ける必要があり、ufwが必須
更新docker compose pull で一括インストールスクリプトの再実行
向くケース用途がOpen WebUIだけ、検証環境複数ツールでモデルを共用、すでにOllama運用中

ホスト側に置く場合、open-webuiに extra_hosts: ["host.docker.internal:host-gateway"] を追加して OLLAMA_BASE_URL=http://host.docker.internal:11434 とし、Ollama側は OLLAMA_HOST=0.0.0.0 にしてufwでDockerのサブネット以外からの11434番を拒否します。手順はOllama入門の社内公開の節と同じです。

初回の管理者作成とユーザー登録の制御

最初にサインアップしたアカウントが自動的に管理者になります。裏を返せば、公開した直後に他人が先にサインアップすると、その人が管理者です。docker compose up -d の直後、プロキシを開ける前に、SSHポートフォワード(ssh -L 3000:127.0.0.1:3000 サーバー)などで http://127.0.0.1:3000 に入って管理者を作ってください。

管理者作成後は「管理者パネル → 設定 → 一般」で新規登録の扱いを決めます。推奨は「新規登録を許可、既定ロールは pending」で、登録は自由だが管理者が承認するまで使えない状態です。招待制にしたいなら新規登録を無効化し、管理者がユーザーを追加します。WEBUI_AUTH=False は認証そのものを無効化する設定なので、1人で使う検証機以外では使わないでください。

ENABLE_SIGNUP や DEFAULT_USER_ROLE のような設定は「PersistentConfig」と呼ばれ、初回起動時に環境変数から読まれた値がデータベースに保存されます。2回目以降の起動では環境変数を変えても反映されず、管理画面の値が優先されます。「composeを直したのに効かない」の正体はこれです。UIで変えるか、ENABLE_PERSISTENT_CONFIG=False で環境変数を常に優先させるか(この場合UIでの変更は再起動で消える)のどちらかに決めます。

モデルの追加とデフォルト設定

モデルは docker compose exec ollama ollama pull で入れるか、管理者パネル → 設定 → モデルからタグ名を指定して取得します。取得後は自動的に一覧へ出ます。全員に同じモデルを最初から選ばせたい場合は、DEFAULT_MODELS(初回起動時のみ有効)か管理者パネルの既定モデル設定で指定します。

忘れがちなのがコンテキスト長です。Ollamaの既定は4096トークンで、文書を添付した質問はすぐに溢れ、溢れた分は黙って切り捨てられます。ワークスペース → モデル → 対象モデルの編集 → 詳細パラメータで num_ctx を8192以上にするか、compose側の OLLAMA_CONTEXT_LENGTH を上げます。Web検索を併用するなら16384以上が公式ドキュメントの目安です。モデルごとの公開範囲はワークスペースのモデル設定でグループ単位に制限でき、「経理部だけが経費規程付きのモデルを使う」といった分け方はここで行います。

文書アップロード(RAG)の使い方と限界

使い方は2通りです。チャット入力欄にファイルをドラッグすると、その会話専用に分割・埋め込みされ、質問時に関連箇所だけが取り出されます。継続的に使う文書は「ワークスペース → ナレッジ」にナレッジベースとしてまとめ、入力欄で # を打って選ぶと会話に添付できます。ファイル単位で「文書全体を使う」に切り替えると検索を介さず全文を毎回渡すので、10ページ以下の規程ならこちらのほうが取りこぼしがありません。

既定値は日本語向きではありません。最初に変える4項目を挙げます。

設定(管理者パネル → 設定 → ドキュメント)既定値(執筆時点)推奨理由
埋め込みモデルsentence-transformers/all-MiniLM-L6-v2(英語中心、コンテナ内で実行)Ollamaの bge-m3日本語の意味検索精度。多言語対応で8192トークンまで入力可
チャンクサイズ / オーバーラップ1000 / 100800〜1500 / 150〜200規程の条項が途中で切れない範囲に合わせる
Top K38〜15候補を多めに取ってから再ランクで絞る
ハイブリッド検索無効有効(BM25 + CrossEncoder再ランク)製品名・型番・部署名はキーワード一致のほうが効く

埋め込みモデルを変えたら、既存ナレッジは必ず「再インデックス」を実行します。モデルが違えばベクトル空間も違うため、旧ベクトルとの比較は意味を持たず、検索結果が急に悪くなります。PDFの表や2段組みは既定の抽出器で崩れやすいので、Apache TikaかDoclingを抽出エンジンに設定してください。

限界も把握しておきます。ナレッジベースの権限は「ユーザーやグループに共有する」単位で、元のファイルサーバーやSharePointのACLは引き継がれません。文書の更新も自動同期ではなく再アップロードです。部署ごとにナレッジベースを分ければ社内チャットとしては十分ですが、文書単位の権限や自動同期が要件ならpgvectorで権限フィルタ付きRAGを自作するほうが結果的に早く着きます。既定のベクトルDBであるChromaはSQLiteベースで、複数ワーカー構成のアップロード時に「Child process [pid] died」が出る場合は VECTOR_DB=pgvector への切り替えが公式の対処です。

リバースプロキシとHTTPS、社内認証

社内公開時はプロキシでTLSを終端します。nginxの要点は公式リファレンスと同じで、WebSocketの Upgrade 転送、バッファリングの無効化、アップロード上限、長時間応答のタイムアウトの4点です。

/etc/nginx/conf.d/chat.conf
server {
    listen 443 ssl;
    http2 on;
    server_name chat.corp.example;
    ssl_certificate     /etc/nginx/certs/chat.crt;
    ssl_certificate_key /etc/nginx/certs/chat.key;
    client_max_body_size 50M;                    # 413 が出たらここ

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;  # WebSocket
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_buffering off;                     # 逐次表示に必須
        proxy_cache off;
        proxy_read_timeout 1800;
        proxy_send_timeout 1800;
    }
}

Caddyなら設定は3行です。社内CAの証明書を使うなら tls にファイルを指定し、検証機なら tls internal で自己署名証明書を自動生成させます。CaddyはSSEを既定で正しく扱うため、バッファ設定は不要です。

Caddyfile
chat.corp.example {
    tls /etc/caddy/certs/chat.crt /etc/caddy/certs/chat.key
    reverse_proxy 127.0.0.1:3000
}

WEBUI_URL は公開URLに合わせてください。OAuthのリダイレクト先やCORSの判定に使われ、ここがずれるとSSOのコールバックだけが失敗します。認証方式は次の3段階で考えます。

方式設定の要点向く規模・条件注意点
ローカルアカウント + pending承認既定のまま。管理者が承認〜30人、IdPがない退職者の無効化を手作業で忘れない運用が要る
OIDCでSSO(Entra ID、Google、Keycloakなど)OAUTH_CLIENT_ID、OAUTH_CLIENT_SECRET、OPENID_PROVIDER_URL(.well-known のURL)、ENABLE_OAUTH_SIGNUP=true、OAUTH_MERGE_ACCOUNTS_BY_EMAIL=trueMicrosoft 365やGoogle Workspaceを使う組織全般コールバックURLは WEBUI_URL/oauth/oidc/callback。IdP側で無効化すれば即ログイン不可
信頼ヘッダ認証(oauth2-proxy、Autheliaなど)WEBUI_AUTH_TRUSTED_EMAIL_HEADER=X-Forwarded-Emailすでに認証プロキシを全社で運用しているヘッダを付けられる経路が1つでも残ると誰にでもなりすませる。コンテナのポートはプロキシ以外に絶対開けない

Microsoft 365を使っていて利用者が30人を超えるならOIDC一択です。アカウント棚卸しが自動化され、ガイドライン上の「誰が使ったか」の記録もIdP側のサインインログで追えます。

バックアップと更新手順

永続データは open-webui ボリューム(/app/backend/data)に全部あります。webui.db(SQLite)、uploads/、vector_db/、cache/ です。Ollamaのモデルは再取得できるので、バックアップ対象は open-webui ボリューム、.env、compose.yaml の3つで足ります。

terminal
cd /srv/open-webui
docker compose stop open-webui
docker run --rm -v open-webui:/data -v "$PWD/backup":/backup alpine \
  tar czf /backup/open-webui-$(date +%F).tgz -C /data .
docker compose start open-webui

# 停止できない場合は SQLite のオンラインバックアップで DB だけ整合を取る
docker compose exec open-webui sqlite3 /app/backend/data/webui.db ".backup /app/backend/data/webui-backup.db"

更新は docker compose pull と docker compose up -d の2コマンドですが、その前に必ずバックアップを取ります。公式ドキュメントが明記するとおり、データベースのマイグレーションは一方向で、更新後に古いイメージへ戻してもスキーマは戻りません。リリースノートの破壊的変更も確認します。自動更新にWatchtowerを使う場合、執筆時点の公式ドキュメントは「元の containrrr/watchtower はメンテナンスが止まりDocker 29以降で動かない」としてフォーク版を案内しています。

terminal
docker compose pull
docker compose up -d
docker compose logs -f open-webui   # "Uvicorn running on http://0.0.0.0:8080" が出るまで待つ

動作確認

terminal
$ docker compose ps
NAME         IMAGE                                 STATUS
ollama       ollama/ollama:latest                  Up 2 minutes
open-webui   ghcr.io/open-webui/open-webui:main    Up 2 minutes (healthy)

$ curl -s http://127.0.0.1:3000/health
{"status":true}

$ docker compose exec ollama ollama ps
NAME          ID              SIZE      PROCESSOR    UNTIL
gemma4:12b    3f2c1a9b8d7e    9.1 GB    100% GPU     29 minutes from now

$ docker compose exec open-webui python3 -c "import urllib.request;print(urllib.request.urlopen('http://ollama:11434/api/tags').read()[:80])"
b'{"models":[{"name":"gemma4:12b","model":"gemma4:12b", ...'

最後の1行は「open-webuiコンテナからollamaコンテナに名前解決して到達できるか」の確認です。ブラウザで管理者としてログインし、モデル一覧に gemma4:12b が出ること、応答が逐次表示されること、PDFを添付した質問で引用元が表示されることの3点を見て完成です。

つまずきポイント

モデル一覧が空、または「Open WebUI: Server Connection Error」

OLLAMA_BASE_URL がコンテナから見て間違っています。localhost はコンテナ自身を指すので、同梱構成ならサービス名の http://ollama:11434、ホスト運用なら http://host.docker.internal:11434 です。管理者パネル → 設定 → 接続に保存済みのURLが残っていることもあるので(PersistentConfig)、そちらも確認します。

「WebSocket connection failed: 403」、送信後にチャットが固まる

プロキシがWebSocketの Upgrade と Connection "upgrade" を転送していません。上のnginx設定を見直します。ENABLE_WEBSOCKET_SUPPORT は既定でtrueなので、複数レプリカ構成でなければ変更不要です。

ファイルを添付すると 413 Request Entity Too Large

nginxの client_max_body_size が既定の1MBのままです。Open WebUI側の RAG_FILE_MAX_SIZE(既定は無制限)ではなく、まずプロキシを疑ってください。

起動直後に停止し、ログに「WEBUI_SECRET_KEY is not set」

.envが読まれていません。docker compose config | grep WEBUI_SECRET_KEY で展開後の値を確認します。.envはcompose.yamlと同じディレクトリに置き、docker compose をそのディレクトリで実行する必要があります。

モデル一覧の読み込みが極端に遅い、/api/models が500を返す

到達できないOpenAI互換URLやOllama URLが接続設定に残っていて、タイムアウトまで待っています。管理者パネル → 設定 → 接続で不要なURLを削除するか、AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST=3 で待ち時間を短くします。UIに入れないほど遅い場合は RESET_CONFIG_ON_START=true を付けて一度起動し、保存済み設定を環境変数で上書きします。

よくある質問

Open WebUIは無料ですか?

ソフトウェア自体は無料で利用できます。執筆時点のライセンスはBSD系の条件に「Open WebUI」ブランド表示の維持義務を加えたもので、直近30日のエンドユーザーが50人以下なら表示変更も許されます。それを超える規模でロゴを差し替えたい場合はエンタープライズライセンスが必要です。原文をLICENSEファイルで確認してください。

GPUがないと使えませんか?

使えます。Open WebUI自体はCPUで軽く動き、重いのはOllama側の推論だけです。GPUなしなら3〜4B級のモデルに絞り、既定の埋め込みモデル(コンテナ内で動き、ワーカーあたり500MB前後のRAMを使います)をOllamaの埋め込みモデルに切り替えると負荷を分離できます。

社内の複数人で使うときの注意点は?

3点あります。最初のサインアップが管理者になるのでプロキシ公開前に作ること、新規登録を pending 承認にすること、そして同時利用数です。Ollamaは既定で1モデルあたり1リクエストずつ処理し(OLLAMA_NUM_PARALLEL)、増やすと並列数分のKVキャッシュがVRAMに乗ります。10人が同時に投げる想定なら、モデルサイズに数GBの余裕を見込んでください。

まとめ

Open WebUIは、Ollamaのポートを閉じたままComposeの内部ネットワークで繋ぎ、127.0.0.1に束縛したUIをリバースプロキシで公開するのが基本形です。管理者はプロキシ公開前に作り、以後の登録は pending 承認にします。環境変数はPersistentConfigの仕組み上「初回だけ」効くものが多いので、2回目以降はUIで変えます。RAGは埋め込みモデルを bge-m3 に変えて再インデックスするだけで日本語の精度が変わり、更新前のバックアップはマイグレーションが一方向である以上、省略できません。

次に読む

$ subscribe --weekly
毎週月曜、企業ITとホームラボの要点を5分で。