VWork バイブコーディングフレームワーク

Appsmithの公式イメージは初回起動を20秒中断すると永久に壊れる——画面は200を返すのにAPIが全部502になる罠を突き止め、データを消さずに直した

社内の管理画面を画面組み立てだけで作れるオープンソース Appsmith を検証環境に立てたところ、公式のとおりに起動したのにログインできない状態になりました。

ブラウザで開くとログイン画面は出ます。コンテナ一覧にも名前があります。それでもメールアドレスとパスワードを入れると、何も起きません。

原因を最後まで追ったところ、初回起動の最初の20秒を中断すると環境が恒久的に壊れるという作りの問題でした。しかも壊れた状態は「動いているように見える」ため、気づくのが非常に難しい。

公式の案内はデータを消して作り直すことですが、消さずに直せました。手順と、その診断の道筋を残します。

症状

三つの事実が同時に成り立ちます。これが揃うのが特徴です。

画面は正常に返る。

$ curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/
200

APIは全滅している。

$ curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/api/v1/users/me
502

コンテナは永久に unhealthy。

$ docker ps --filter name=appsmith --format ''
Up 25 minutes (unhealthy)

外形監視をトップページに向けていると、この障害は永久に検知できません。200が返り続けるからです。

追い方

1. 内部プロセスが再起動を繰り返している

Appsmith は単一のコンテナの中で複数のプロセスを動かしています。状態を見ると、あるプロセスの稼働時間だけが毎回0分台に戻っていました。

$ docker exec appsmith supervisorctl status
backend    RUNNING   pid 1508, uptime 0:21:14
editor     RUNNING   pid 1509, uptime 0:21:14
mongodb    RUNNING   pid 1505, uptime 0:21:14
mcp        RUNNING   pid 3761, uptime 0:00:11     ← ここ

ログには同じ行が延々と並んでいました。

Appsmith MCP failed to start: Server selection timed out after 30000 ms
Appsmith MCP failed to start: Server selection timed out after 30000 ms

30秒のタイムアウトで落ちて、また起動する。それを無限に繰り返しています。

2. なぜ諦めないのか

プロセスの管理設定を見ると、再試行の上限は3回でした。それなのに止まりません。

[program:mcp]
autorestart=true
startretries=3
# Require the process to stay up 5s to count as a successful start

コメントに答えが書いてありました。「5秒持てば起動成功と数える」。ところがこの不具合では、接続の待ち時間が30秒あるため、プロセスは30秒生きてから落ちます。5秒を超えているので毎回「起動成功」と判定され、再試行の回数が加算されません。永久に繰り返します。

3. 本当の原因はデータベース

サーバー側のログを見ると、より直接的な理由が出ていました。

com.mongodb.MongoQueryException: Command failed with error 13436
(NotPrimaryOrSecondary): 'node is not in primary or recovering state'

同梱のMongoDBに接続はできるものの、読み書きを受け付けない状態でした。確認すると理由がわかりました。

$ mongosh ... --eval "rs.status()"
NotYetInitialized

MongoDBは複製構成(レプリカセット)として起動していますが、その初期化が一度も行われていません。初期化されていない複製構成のノードは、主系でも副系でもない宙ぶらりんの状態になります。どのドライバも接続先を選べず、30秒待って諦めます。

サーバー側の起動時の移行処理もここで死んでいたため、APIが全部502になっていました。画面配信は別プロセスなので、200を返し続けます。

なぜ初期化されなかったのか

起動スクリプトを読むと、初期化は確かに実装されていました。

if [[ $shouldPerformInitdb -gt 0 && $isUriLocal -eq 0 ]]; then
  mongod --fork --port 27017 --dbpath "$MONGO_DB_PATH" ...
  sleep 10
  # 利用者を作る
  mongod --shutdown
  mongod --fork ... --replSet mr1 --keyFile ...
  sleep 10
  mongosh "$APPSMITH_DB_URL" --eval 'rs.initiate()'    # ← 仕上げ
fi

問題は shouldPerformInitdb の決め方でした。

for path in "$MONGO_DB_PATH/local.0" "$MONGO_DB_PATH/storage.bson"; do
  if [ -e "$path" ]; then
    shouldPerformInitdb=0
    break
  fi
done

データファイルが存在するかどうかだけで判断しています。

そのデータファイルは、上の手順の1行目でMongoDBが起動した瞬間に作られます。仕上げの rs.initiate() まで、sleep 10 が2回に加えて起動と停止があり、20秒以上あります。

この20秒の間にコンテナが止まると、次回以降は「データファイルがある=初期化済み」と判断され、仕上げが二度と実行されません

判定すべきは「初期化が完了したか」であって「データファイルがあるか」ではない、という典型的な取り違えです。

中断は簡単に起きる

意図的に止めなくても起きます。私の場合は、ポートを変えるために起動直後に docker compose down したのが原因でした。

他にも次のどれでも同じ状態になります。

つまり、小さなサーバーに初めて入れる人ほど踏みやすい罠です。

直す

データを消さずに直せます。未完了だった仕上げを、手で実行するだけです。

手順1. 状態を確認する

docker exec appsmith sh -c '. /appsmith-stacks/configuration/docker.env; \
  mongosh "$APPSMITH_DB_URL?authSource=appsmith&directConnection=true" --quiet \
  --eval "try{print(rs.status().myState)}catch(e){print(e.codeName)}"'

NotYetInitialized と出れば対象です。1 と出たら原因は別にあります。

手順2. 初期化する

docker exec appsmith sh -c '. /appsmith-stacks/configuration/docker.env; \
  mongosh "$APPSMITH_DB_URL?authSource=appsmith&directConnection=true" --quiet \
  --eval "rs.initiate({_id:\"mr1\",members:[{_id:0,host:\"localhost:27017\"}]})"'
{ ok: 1 }

手順3. 主系になるのを待つ

30秒ほどで 1 になります。1が主系を意味します。

手順4. サーバー側を起動し直す

初期化前に落ちているので、起動し直す必要があります。

docker exec appsmith supervisorctl restart backend

結果

$ curl -s -o /dev/null -w 'API %{http_code}\n' http://127.0.0.1:8080/api/v1/users/me
API 200

$ docker ps --filter name=appsmith --format ''
Up 5 minutes (healthy)

APIが復旧し、再起動を繰り返していたプロセスも安定しました。管理者アカウントを作ってログインでき、日本語の表示名もそのまま通りました。

効かなかった対処

再起動を繰り返すプロセスを止めようとして、無効化の変数を見つけました。

APPSMITH_MCP_ENABLED: "false"

効きませんでした。 プロセスの環境変数には確かに渡っています。

$ tr "\0" "\n" < /proc/<pid>/environ | grep MCP_ENABLED
APPSMITH_MCP_ENABLED=false

それでもプロセスは起動し、同じように失敗し続けます。起動する側がこの変数を見ていません。

そもそも、再起動を繰り返すプロセスは症状であって原因ではありません。直すべきはデータベースの側でした。

学び

画面が200を返すことは、動いている証拠になりません。

Appsmith に限らず、画面配信とサーバー処理が別プロセスになっている構成では、片方だけ生きている状態が普通に起こります。監視するなら、認証が要るAPIまで叩いてください。

curl -s -o /dev/null -w '%{http_code}\n' http://<host>/api/v1/users/me

200 か 401 なら正常。502 なら壊れています。 401でよいのです。認証を要求できているのは、サーバーが生きている証拠だからです。

もうひとつ。「一度きりの初期化」を、痕跡の有無で判断してはいけません。 途中でできる痕跡と、完了の印は別物です。完了したときにだけ書く印を用意すべきでした。自分で似た仕組みを書くときの教訓にします。

日本語での利用について

ついでに測ったことも書いておきます。

Appsmith には日本語表示がありません。設定で切り替えられないだけでなく、翻訳の仕組み自体が存在しません。画面のHTMLは lang="en" に固定され、言語ファイルは1本もなく、文言1,397件が一つのプログラムファイルに直接書かれています。

一方で、扱うデータが日本語であることには何の支障もありません。日本語のテーブル名・列名はむしろ扱いやすく、引用符なしでそのまま使えます。

SELECT 会社名, 契約金額 FROM 取引先 WHERE 都道府県 = '愛知県'

対して英字で大文字が混ざる名前は引用符が要ります。PostgreSQLは引用符のない識別子を小文字に変換しますが、日本語には大文字小文字の区別がないためです。

SELECT CustomerName FROM MixedCase
-- ERROR: relation "mixedcase" does not exist

唯一の実務的な穴はCSVの書き出しでした。配信されている画面用プログラム216本を調べましたが、CSVにExcel向けの目印(BOM)を付けている箇所はありません。日本語版のExcelで開くと文字化けします。表の標準ボタンを隠して、自分でボタンを置くのが確実です。

download("\uFEFF" + body, "取引先.csv", "text/csv");

まとめ

症状 画面200・API502・永久にunhealthy
原因 内部MongoDBの複製構成が未初期化
引き金 初回起動の最初の20秒での中断
根本 初期化済みかを「データファイルの有無」で判定している
復旧 rs.initiate() 一回 + backend再起動。データは消えない
予防 初回起動は healthy になるまで中断しない

Appsmith 自体は、社内の管理画面を短時間で作るには良い道具です。ただし最初の1分だけは、何もせず待ってください。


導入から復旧、日本語での運用までをまとめた手順書をKurage App Storeで配布しています。英語画面の読み替え表、Excelで化けないCSV書き出し、復旧を自動判定するスクリプトを同梱しています。