2026年9月6日日曜日

Docker desktopで Open WebUI を立てたら「無限リロード」にハマった記録

 はじめに

Docker desktop を使って Open WebUI をローカルに立ち上げました。


以下の流れで行いました。

・Docker desktop 起動

・環境構築用のフォルダをデスクトップに作成

・このフォルダに、docker-compose.yml を保存

・ターミナルを立ち上げ、このフォルダに移動、docker-compose up -d を実行

・http://localhost:3100で、Open WebUI が立ち上がれば成功


「コンテナは Up になっている。ログにも致命的なエラーはない。なのにブラウザを開くとページが無限にリロードを繰り返して、何も表示されない。」

そしてある瞬間、なぜか突然、まともに動いた。


この記事では、あの「突然動いた」の正体を、ログを見ながら紐解いていきます。同じ現象で詰まっている方の参考になれば。


現象:起きていたこと

ブラウザで http://localhost:3100 を開くと、リロードが止まらない

サーバーログに大量の 404 Not Found が流れ続ける

そのあと 500 Internal Server Error が混ざる

Docker のコンテナ状態は Up(緑)

ログが教えてくれたこと

流れるログの、いちばん怪しいのはここです。


"GET /_app/version.json HTTP/1.1" 200

"GET /_app/immutable/chunks/DI6U-d8h.js HTTP/1.1" 404

"GET /_app/immutable/chunks/D4rfk4Lu.js HTTP/1.1" 404

"GET /_app/version.json HTTP/1.1" 200

ポイントは、version.json は 200(成功)なのに、特定の .js ファイルだけが 404(存在しない) ということ。


つまりサーバーは生きているのに、「その JS ファイルがまだコンテナの中に存在していない」 ということです。


正体:「ファイル展開」と「モデルダウンロード」が完了していなかっただけ

Open WebUI は起動時に、フロントエンドの静的ファイルを配置し、さらに Hugging Face から embedding モデル(all-MiniLM-L6-v2)をダウンロードします。


この準備が完了する前にブラウザがリクエストしてくると、


ファイルがまだ展開済みでない → 404

DB や初期化が途中で飛ぶ → 500

ブラウザは「エラー!」と判定して再度読み込む → 無限リロード

という悪循環に陥ります。


さらに、このモデルのダウンロードは未認証リクエストだと Hugging Face にレート制限されて速度が激減します。私のログでは


Fetching 30 files: 23% | 7/30 [00:00<00:02, 16.44it/s]

Fetching 30 files: 30% | 9/30 [15:20<46:40, 133.38s/it]   ← ここから速度崩壊

と、いきなり速度が 2桁秒/ファイル になっていました。


つまり「突然動いた」の正体は、この背景のダウンロード・展開が、ようやく完了してファイルが揃った、ということでした。 設定が“奇跡的に正しかった”のではなく、時間とリソースをくれたことが効いたんです。


じゃあどうすればいいか(推奨設定)

最小構成だと初回が非常に不安定なので、実際には以下を強化することを強くおすすめします。


最小構成(動くが、初回が不安定)

yamlの内容

services:

  openwebui:

    image: ghcr.io/open-webui/open-webui:main

    container_name: open-webui

    ports:

      - "3100:8080"

    volumes:

      - ./openwebui-data:/app/backend/data

    environment:

      - WEBUI_AUTH=False

    restart: always

※末尾に誰も使わない volumes: open-webui: があると、意味のない named volume が作られるので削除しています。


強化構成(ここが本題:HF_TOKEN + キャッシュ永続化)

yamlの内容

services:

  openwebui:

    image: ghcr.io/open-webui/open-webui:main

    container_name: open-webui

    ports:

      - "3100:8080"

    volumes:

      - ./openwebui-data:/app/backend/data

      - ./huggingface-cache:/root/.cache/huggingface   # モデルのキャッシュをホストに保持

    environment:

      - WEBUI_AUTH=False

      - HF_TOKEN=hf_xxxxxxxxxxxxxxxxxxxxxxxx          # Hugging Face のトークン

      - HF_HOME=/root/.cache/huggingface

    restart: always

この2つの追加が何を変えるか:


追加 効果

HF_TOKEN 認証付きリクエストになるためレート制限が緩み、ダウンロードが速くなる

キャッシュ用 volume 初回だけダウンロードし、以降は再ダウンロードしない(起動が飛躍的に速くなる)

HF_TOKEN は Hugging Face の Access Tokens で Read 権限で発行できます。


チェックリスト(また詰まった時のために)

docker logs -f openwebui を見て、.js の 404 が流れていないか確認

Docker Desktop の Resources → Memory が 8GB〜16GB あるか(少なすぎると展開に失敗する)

初回起動は数分〜十数分、放置する(ダウンロードの完了を待つ)

ブラウザはシークレット窓か**強制リロード(Cmd+Shift+R / Ctrl+Shift+R)**で確認

それでも 500 が出るなら docker-compose down → ./openwebui-data 削除 → docker-compose up -d で DB を再生成

おわりに

「コンテナが Up = アプリの準備完了」とは限らない、という今回の教訓。


ログの 404(ファイルがまだない) と 500(処理が途中で壊れる) を見分けるだけで、「故障した」のではなく「準備中」と判断できて、無駄な再インストールを避けられます。


私の“突然動いた”は、魔法ではなかった。

待てたこと、メモリを割けたこと、そして次回の自分を救うためにキャッシュを永続化したこと。


その3つが、今回の答えでした。

M5Stack PowerHubでGPS情報をCANロガーに送信

M5Stack PowerHubを購入したので、以下を試してみました。

構成

・目的

・使用ハードウェア

・進捗の記録(エラーだらけの旅路)

・完成コード

・.dbcファイル

・CAN通信デバッグのポイント

・まとめ

1. 目的

M5Stack PowerHub(ESP32-S3)でGPSによる現在位置情報を取得し、OLEDで表示するプログラムがありました。これに CAN通信で車両用のCANロガーに送信する機能 を追加した、記録です。


使用ハードウェア

部品 備考

M5Stack PowerHub ESP32-S3-WROOM-1U-N16R8

M5Unit OLED 128×64 ドットディスプレイ

GPSモジュール Serial2(115200bps)接続

CANトランシーバ SIT1044QTK/3(PowerHub内蔵)

CANロガー 2ch、500kbps、車載OBD対応

CANピンアサイン(PowerHub回路図より)


MCU CAN TXD → GPIO 39

MCU CAN RXD → GPIO 40

2. 旅路(エラーの記録)

2-1. ライブラリが見つからない

#include <ESP32CAN.h>

fatal error: ESP32CAN.h: No such file or directory

原因: 外部ライブラリがインストールされていなかった。


学び: ESP32にはCAN機能が標準搭載(TWIドライバー)されているので、ライブラリに頼らず使う手がある。


2-2. クラス名が合わない

ESP32-TWAI-CAN.hpp をインストールして使ってみたが:

error: 'ESP32CAN' does not name a type

原因: ライブラリによってクラス名が異なる(ESP32CAN / TWAI_CAN など)。


学び: 外部ライブラリは「バージョンによる互換性問題」に頭を痛めさせる。標準ドライバーを使うのが安定。


2-3. TWAIドライバーの地獄(ここからが本番)

#include "driver/twai.h" を使うことにしたが、M5Stack S3のボードマネージャー(v3.x = ESP-IDF v5系) では、古いチュートリアルやネット記事の記述が全部合わない状況に陥った。


エラー 原因 解決

TWAI_GENERAL_CONFIG_DEFAULT requires 3 arguments v5系では引数が必要 (tx, rx, mode) を渡す

no member named 'tx_gpio_num' 変数名が tx_io に変更 型キャスト + 変数名変更

TWAI_TIMING_CONFIG_DEFAULT not declared マクロ名が変更 TWAI_TIMING_CONFIG_500KBITS() を使う

TWAI_FILTER_CONFIG_DEFAULT not declared マクロが削除 構造体を {} で初期化

no member named 'ext' 構造体から削除 extd に変更

学び: ESP32の公式ドライバは「バージョンによって構造体が変わる」。ネットの情報を鵜呑みにすると、必ずどこかで引っかかる。


2-4. 通信エラー(Status: Failed!)

コンパイルは通ったが、送信すると Failed! と表示された。


調べたこと

検証 結果

CAN_H / CAN_L 電圧 各2.5V(差分0V)→ アイドル正常

GNDを共通化 してもダメ

CAN_H/L を入れ替え ダメ

ロガー同士の通信(1ch→2ch) 成功 ← 物理層・ボーレートはOK

M5Stackから送信 Failed ← ACKが返らない

結論

ロガー同士は通信しているのにM5Stackだけが失敗する = M5Stackの送信信号がバスに乗っていない(あるいはACKが返ってこない)。


2-5. 正解にたどり着く

M5Stack公式ドキュメント(PowerHub CANページ)のコードを確認:

twai_general_config_t g_config = TWAI_GENERAL_CONFIG_DEFAULT(MCU_CAN_TXD, MCU_CAN_RXD, TWAI_MODE_NORMAL);

twai_timing_config_t t_config = TWAI_TIMING_CONFIG_500KBITS();

twai_filter_config_t f_config = TWAI_FILTER_CONFIG_ACCEPT_ALL();


そして twai_message_t の拡張フラグは:

msg.extd = 0;  // ← .ext ではなく .extd

この公式コードをそのまま踏襲したところ、ACKが返って通信成功!


3. 完成コード

#include <M5Unified.h>

#include <M5UnitOLED.h>

#include <TinyGPS++.h>

#include "driver/twai.h"


M5UnitOLED oled;


// --- GPS 設定 ---

const int GPS_RX = 2;

const int GPS_TX = 1;

const long GPS_BAUD = 115200;


// --- CAN (TWAI) 設定 ---

const gpio_num_t MCU_CAN_TXD = GPIO_NUM_39;

const gpio_num_t MCU_CAN_RXD = GPIO_NUM_40;


TinyGPSPlus gps;

bool isSearching = true;


unsigned long lastCANSendTime = 0;

const unsigned long CAN_SEND_INTERVAL = 1000; // 1秒ごと


void setup() {

    M5.begin();

    oled.begin();

    oled.setRotation(1);

    oled.setTextColor(WHITE, BLACK);

    oled.setTextSize(1);

    oled.fillScreen(BLACK);


    Serial2.begin(GPS_BAUD, SERIAL_8N1, GPS_RX, GPS_TX);


    // CAN初期化(公式ドキュメント準拠)

    twai_general_config_t g_config = TWAI_GENERAL_CONFIG_DEFAULT(MCU_CAN_TXD, MCU_CAN_RXD, TWAI_MODE_NORMAL);

    twai_timing_config_t t_config = TWAI_TIMING_CONFIG_500KBITS();

    twai_filter_config_t f_config = TWAI_FILTER_CONFIG_ACCEPT_ALL();


    if (twai_driver_install(&g_config, &t_config, &f_config) == ESP_OK &&

        twai_start() == ESP_OK) {

        Serial.println("CAN ready.");

    } else {

        Serial.println("CAN init failed.");

        while (1) delay(1000);

    }

}


void loop() {

    M5.update();


    // GPS解析

    while (Serial2.available() > 0) {

        if (gps.encode(Serial2.read())) {

            displayGPSInfo();

        }

    }


    // 1秒ごとにCAN送信

    if (millis() - lastCANSendTime > CAN_SEND_INTERVAL) {

        sendGPSViaCAN();

        lastCANSendTime = millis();

    }

}


void sendGPSViaCAN() {

    if (!gps.location.isValid()) return;


    // --- 1. 座標 (ID: 0x123) ---

    int32_t lat = (int32_t)(gps.location.lat() * 1000000);

    int32_t lon = (int32_t)(gps.location.lng() * 1000000);


    twai_message_t msgLoc = {};

    msgLoc.extd = 0;

    msgLoc.identifier = 0x123;

    msgLoc.data_length_code = 8;


    // Big Endian で格納

    msgLoc.data[0] = (lat >> 24) & 0xFF;

    msgLoc.data[1] = (lat >> 16) & 0xFF;

    msgLoc.data[2] = (lat >> 8)  & 0xFF;

    msgLoc.data[3] =  lat        & 0xFF;

    msgLoc.data[4] = (lon >> 24) & 0xFF;

    msgLoc.data[5] = (lon >> 16) & 0xFF;

    msgLoc.data[6] = (lon >> 8)  & 0xFF;

    msgLoc.data[7] =  lon        & 0xFF;


    twai_transmit(&msgLoc, pdMS_TO_TICKS(100));


    // --- 2. 時刻 (ID: 0x124) ---

    int hour   = gps.time.hour()   + 9; // UTC → JST

    int minute = gps.time.minute();

    int second = gps.time.second();

    int day    = gps.date.day();

    int month  = gps.date.month();

    int year   = gps.date.year();

    if (hour >= 24) { hour -= 24; day++; }


    twai_message_t msgTime = {};

    msgTime.extd = 0;

    msgTime.identifier = 0x124;

    msgTime.data_length_code = 7;


    msgTime.data[0] = (year >> 8) & 0xFF;

    msgTime.data[1] =  year      & 0xFF;

    msgTime.data[2] = month;

    msgTime.data[3] = day;

    msgTime.data[4] = hour;

    msgTime.data[5] = minute;

    msgTime.data[6] = second;


    twai_transmit(&msgTime, pdMS_TO_TICKS(100));

}


void displayGPSInfo() {

    if (gps.location.isValid()) {

        if (isSearching) { oled.fillScreen(BLACK); isSearching = false; }

        int h = gps.time.hour() + 9;

        int m = gps.time.minute();

        int s = gps.time.second();

        int d = gps.date.day();

        int mo = gps.date.month();

        int y = gps.date.year();

        if (h >= 24) { h -= 24; d++; }


        oled.setTextColor(WHITE, BLACK);

        oled.setCursor(0, 0);  oled.printf("Date: %04d/%02d/%02d", y, mo, d);

        oled.setCursor(0, 12); oled.printf("Time: %02d:%02d:%02d", h, m, s);

        oled.setCursor(0, 24); oled.printf("Lat: %.6f", gps.location.lat());

        oled.setCursor(0, 36); oled.printf("Lon: %.6f", gps.location.lng());

        oled.setCursor(0, 48); oled.printf("Sats: %d", gps.satellites.value());

    } else {

        if (!isSearching) { oled.fillScreen(BLACK); isSearching = true; }

        oled.setCursor(0, 24);

        oled.setTextColor(WHITE, BLACK);

        oled.println("Status: Searching...");

    }

}

4. .dbcファイル

VERSION ""


NS_ :

BS_:

BU_: M5Stack_S3


BO_ 291 GPS_Location: 8 M5Stack_S3

 SG_ Latitude  :  0|32|-1@0 1e-6 (0 180)      "deg" M5Stack_S3

 SG_ Longitude : 32|32|-1@0 1e-6 (-180 180)  "deg" M5Stack_S3


BO_ 292 GPS_Time: 7 M5Stack_S3

 SG_ Year   :   0|16| 1@0 1 (0 2100)   "year"  M5Stack_S3

 SG_ Month  :  16| 8| 1@0 1 (1 12)     "month" M5Stack_S3

 SG_ Day    :  24| 8| 1@0 1 (1 31)     "day"   M5Stack_S3

 SG_ Hour   :  32| 8| 1@0 1 (0 23)     "hour"  M5Stack_S3

 SG_ Minute :  40| 8| 1@0 1 (0 59)     "min"   M5Stack_S3

 SG_ Second :  48| 8| 1@0 1 (0 59)     "sec"   M5Stack_S3

@0 = Little Endian(Intel)/ @1 = Big Endian(Motorola)

今回のコードは Big Endian で送信しているため、ロガー側で Motorola (Big Endian) に設定する。


5. デバッグで学んだこと

CAN通信デバッグのチェックリスト

# チェック項目 確認方法

1 ボーレート一致 送信側・受信側・ロガーで同一

2 GND共通 送信側・受信側のGNDを接続

3 終端抵抗 120Ω 電源OFFで CAN_H–CAN_L 間を測定(約60Ω)

4 CAN_H / CAN_L 電圧 アイドル時 各2.5V / 差分0V

5 配線(H↔H, L↔L) 入れ替えて再確認

6 トランシーバ電源 VCCピンで5V確認

7 STBピン GND接続(動作時)

8 ACK取得 送信側で ESP_OK になるか

ACKとは

CAN通信では、送信ノードがデータを送信した後、バスの上にある他のノードが「ACK(確認応答)」ビットを返す ことで、正常な送信が確定します。


ACKが返らない → 送信失敗(Failed)

原因:相手不在、ボーレート不一致、物理断線、電位差

Big Endian / Little Endian の違い

形式 別名 バイト順 例(0x1234)

Big Endian Motorola MSB → LSB [12, 34]

Little Endian Intel LSB → MSB [34, 12]

今回のコードは Big Endian(Motorola)で送信しているため、ロガー側も Big Endian (Motorola) に設定 すれば正しく数値が復元されます。


6. まとめ

課題 解決策

ライブラリが見つからない ESP32標準の driver/twai.h を使用

構造体のメンバー名が変わる 公式ドキュメントのコードをベースに

送信がFailedになる GND共通化 + ACK取得を確認

数値がずれる ロガー側で Big Endian (Motorola) に設定

8バイト制限 座標と時刻を2つのIDに分割

今回の「教訓」

最新のESP32-S3環境(ボードマネージャーv3.x / ESP-IDF v5系)では、旧バージョン向けのコードがそのまま通用しない。


ネットの情報が「正しい」のか「古い」のかを見極める目と、公式ドキュメントを最終リファレンスにする ことが大切です。


2026年8月、M5Stack PowerHub × CANロガー、ついに通信成功。