LibreChat を Cloud Run と Firestore MongoDB 互換で構築する
- Shogo Umeda
- 9 時間前
- 読了時間: 12分
1. はじめに
LibreChat は、OpenAI・Google Gemini・Anthropic など複数の LLM に対応したオープンソースのチャット UI です。セルフホストすることで、組織や個人の用途に合わせた AI チャット基盤を構築できます。
LibreChat のデータベースには MongoDB が必要です。Google Cloud で MongoDB を使うには、GCE や GKE 上に Docker で構築するか、Google Cloud マーケットプレイスから MongoDB Atlas を契約する方法が一般的でした。前者はインスタンスの管理が必要になり、後者はフルマネージドですが別サービスとの契約・連携が必要です。
そこで注目したのが、Firestore の MongoDB 互換モードです。Firestore の MongoDB 互換モードを利用すると、MongoDB を別途構築・運用することなく、フルマネージドなサーバーレス構成で LibreChat を動かせます。本記事では、この構成の検証結果と構築手順を紹介します。
先に検証結果をお伝えします。
結論
結論としては、基本的なチャット機能は正常に動作します。ただし、エージェントやプロンプトの一覧がリロード後に表示されなくなる、管理者ダッシュボードが使えないといった問題があります。これは Firestore MongoDB 互換が LibreChat の権限管理で内部的に利用する MongoDB ビット演算子クエリをサポートしておらず、権限チェックが失敗するためです。
2. アーキテクチャ概要
今回構築した環境の全体像です。Cloud Run をアプリケーション基盤とし、データベースに Firestore MongoDB 互換を採用したサーバーレス構成になっています。
構成図

GCP リソース一覧
リソース | 用途 |
Cloud Run (LibreChat) | メインアプリケーション |
Firestore Enterprise | MongoDB 互換モードのデータベース |
Vertex AI | Gemini API によるチャット |
Secret Manager | JWT シークレット、SA キー等の管理 |
Cloud Storage | librechat.yaml の配信(GCS FUSE マウント) |
Artifact Registry | ghcr.io リモートリポジトリ |
リポジトリ構成
.
├── librechat.yaml # LibreChat 設定ファイル
└── terraform/
├── main.tf # 全リソース・変数定義
└── terraform.tfvars # 変数値(gitignore 推奨)3. 前提条件
必要な環境
GCP プロジェクト
Terraform >= 1.5
Google Provider >= 7.19
Firestore の `mongodb_compatible_data_access_mode` 属性は v7.19.0 で追加されました。それ以前のバージョンではこの属性が認識されず、MongoDB 互換モードを Terraform から有効にできません。
gcloud CLI
4. 構築手順
librechat.yaml の設定
LibreChat の設定ファイル `librechat.yaml` は、GCS バケットに配置し Cloud Run の GCS FUSE マウントで `/app/config/librechat.yaml` として読み込みます。
この例では、リモート MCP Server として Langfuse のドキュメント検索サーバーを `streamable-http` タイプで設定しています。
# librechat.yaml
version: 1.3.6
cache: true
mcpServers:
langfuse-docs:
type: streamable-http
url: "https://langfuse.com/api/mcp"
timeout: 30000
initTimeout: 10000
registration:
socialLogins: []
allowedDomains: []Terraform コード
全リソースを Terraform で IaC 管理しています。以下の内容を `main.tf` として保存します。各リソースの意図はコメントで説明しています。
main.tf
# =================================================================
# 変数定義
# ======================+==========================================
variable "project_id" {
description = "GCP プロジェクト ID"
type = string
}
variable "region" {
description = "GCP リージョン"
type = string
default = "asia-northeast1"
}
variable "environment" {
description = "環境名 (dev / stg / prod)"
type = string
default = "dev"
}
variable "firestore_database_name" {
description = "Firestore データベース名"
type = string
default = "librechat"
}
variable "firestore_location" {
description = "Firestore ロケーション"
type = string
default = "asia-northeast1"
}
variable "librechat_cpu" {
description = "Cloud Run の vCPU 数"
type = string
default = "2"
}
variable "librechat_memory" {
description = "Cloud Run のメモリ"
type = string
default = "1Gi"
}
# =================================================================
# プロバイダー設定
# =================================================================
terraform {
required_version = ">= 1.5"
required_providers {
google = {
source = "hashicorp/google"
version = ">= 7.19"
}
}
}
provider "google" {
project = var.project_id
region = var.region
}
data "google_project" "current" {
project_id = var.project_id
}
# =================================================================
# Firestore (MongoDB 互換モード)
# =================================================================
# Enterprise エディション + mongodb_compatible_data_access_mode を有効化することで、
# MongoDB プロトコルでの接続が可能になる。
# 接続文字列の詳細は後述の「Firestore MongoDB 互換モードの解説」を参照。
resource "google_firestore_database" "librechat" {
project = var.project_id
name = var.firestore_database_name
location_id = var.firestore_location
type = "FIRESTORE_NATIVE"
database_edition = "ENTERPRISE"
mongodb_compatible_data_access_mode = "DATA_ACCESS_MODE_ENABLED"
concurrency_mode = "PESSIMISTIC"
delete_protection_state = "DELETE_PROTECTION_DISABLED"
depends_on = [google_project_service.firestore]
}
resource "google_project_service" "firestore" {
project = var.project_id
service = "firestore.googleapis.com"
disable_on_destroy = false
}
# =================================================================
# Artifact Registry
# =================================================================
# Cloud Run は ghcr.io から直接 pull できないため、
# Artifact Registry にリモートリポジトリを作成して中継する。
resource "google_artifact_registry_repository" "ghcr_remote" {
project = var.project_id
location = var.region
repository_id = "ghcr-remote"
format = "DOCKER"
mode = "REMOTE_REPOSITORY"
# NOTE: custom_repository は deprecated。
# 最新の Provider では common_repository への移行が推奨されている。
remote_repository_config {
docker_repository {
custom_repository {
uri = "https://ghcr.io"
}
}
}
depends_on = [google_project_service.artifactregistry]
}
resource "google_project_service" "artifactregistry" {
project = var.project_id
service = "artifactregistry.googleapis.com"
disable_on_destroy = false
}
# =================================================================
# Secret Manager
# =================================================================
# LibreChat に必要なシークレット(JWT、暗号化キー等)を管理。
# Terraform ではシークレットの「箱」のみ作成する。値は apply 後に手動で設定する。
# 例: echo -n "your-secret-value" | gcloud secrets versions add dev-librechat-jwt-secret --data-file=-
# 各シークレットに対して上記コマンドを実行し、適切な値を設定すること。
locals {
secrets = {
jwt-secret = "LibreChat JWT シークレット"
jwt-refresh-secret = "LibreChat JWT リフレッシュシークレット"
creds-key = "LibreChat 暗号化キー"
creds-iv = "LibreChat 暗号化 IV"
}
}
resource "google_secret_manager_secret" "secrets" {
for_each = local.secrets
project = var.project_id
secret_id = "${var.environment}-librechat-${each.key}"
replication {
auto {}
}
depends_on = [google_project_service.secretmanager]
}
# Vertex AI 用 SA キーを自動生成し Secret Manager に保存
resource "google_service_account_key" "librechat_vertex" {
service_account_id = google_service_account.librechat.name
}
resource "google_secret_manager_secret" "vertex_sa_key" {
project = var.project_id
secret_id = "${var.environment}-librechat-vertex-sa-key"
replication {
auto {}
}
depends_on = [google_project_service.secretmanager]
}
resource "google_secret_manager_secret_version" "vertex_sa_key" {
secret = google_secret_manager_secret.vertex_sa_key.id
secret_data = google_service_account_key.librechat_vertex.private_key
}
resource "google_project_service" "secretmanager" {
project = var.project_id
service = "secretmanager.googleapis.com"
disable_on_destroy = false
}
# =================================================================
# Cloud Storage - librechat.yaml の配信
# =================================================================
# GCS バケットに配置し、Cloud Run の GCS FUSE マウントで読み込む。
# librechat.yaml の詳細は後述の「librechat.yaml の設定」を参照。
resource "google_storage_bucket" "librechat_config" {
project = var.project_id
name = "${var.project_id}-librechat-config"
location = var.region
force_destroy = true
uniform_bucket_level_access = true
}
resource "google_storage_bucket_object" "librechat_yaml" {
name = "librechat.yaml"
bucket = google_storage_bucket.librechat_config.name
source = "${path.module}/../librechat.yaml"
}
# =================================================================
# IAM - サービスアカウントと権限
# =================================================================
resource "google_service_account" "librechat" {
project = var.project_id
account_id = "librechat-${var.environment}"
display_name = "LibreChat Cloud Run SA (${var.environment})"
}
resource "google_project_iam_member" "librechat_firestore" {
project = var.project_id
role = "roles/datastore.user"
member = "serviceAccount:${google_service_account.librechat.email}"
}
resource "google_project_iam_member" "librechat_secretmanager" {
project = var.project_id
role = "roles/secretmanager.secretAccessor"
member = "serviceAccount:${google_service_account.librechat.email}"
}
resource "google_project_iam_member" "librechat_vertexai" {
project = var.project_id
role = "roles/aiplatform.user"
member = "serviceAccount:${google_service_account.librechat.email}"
}
resource "google_project_iam_member" "librechat_artifact_reader" {
project = var.project_id
role = "roles/artifactregistry.reader"
member = "serviceAccount:${google_service_account.librechat.email}"
}
resource "google_storage_bucket_iam_member" "librechat_config_reader" {
bucket = google_storage_bucket.librechat_config.name
role = "roles/storage.objectViewer"
member = "serviceAccount:${google_service_account.librechat.email}"
}
# Cloud Run サービスエージェントにも Secret Manager へのアクセス権が必要。
# コンテナ起動時にシークレットを注入するのはサービスエージェントであり、
# アプリケーションの SA とは異なる。
resource "google_project_iam_member" "cloudrun_agent_secretmanager" {
project = var.project_id
role = "roles/secretmanager.secretAccessor"
member = "serviceAccount:service-${data.google_project.current.number}@serverless-robot-prod.iam.gserviceaccount.com"
}
# =================================================================
# Cloud Run - LibreChat
# =================================================================
resource "google_cloud_run_v2_service" "librechat" {
project = var.project_id
name = "librechat-${var.environment}"
location = var.region
ingress = "INGRESS_TRAFFIC_ALL"
deletion_protection = false
template {
service_account = google_service_account.librechat.email
scaling {
min_instance_count = 0
max_instance_count = 2
}
containers {
# Artifact Registry のリモートリポジトリ経由で ghcr.io のイメージを pull
image = "${var.region}-docker.pkg.dev/${var.project_id}/ghcr-remote/danny-avila/librechat:v0.8.3"
name = "librechat"
# PORT は Cloud Run の予約済み環境変数のため設定不可。
# container_port を指定すると Cloud Run が自動的に PORT=3080 を設定する。
ports {
container_port = 3080
}
# GCS FUSE: バケットをディレクトリとしてマウント
volume_mounts {
name = "librechat-config"
mount_path = "/app/config"
}
resources {
limits = {
cpu = var.librechat_cpu
memory = var.librechat_memory
}
cpu_idle = true
startup_cpu_boost = true
}
# ---------- 環境変数 ----------
env {
name = "HOST"
value = "0.0.0.0"
}
env {
name = "NODE_ENV"
value = "production"
}
env {
name = "NO_INDEX"
value = "true"
}
env {
name = "CONFIG_PATH"
value = "/app/config/librechat.yaml"
}
# Firestore MongoDB 互換接続
# 接続文字列の詳細は後述の「Firestore MongoDB 互換モードの解説」を参照
env {
name = "MONGO_URI"
value = "mongodb://${google_firestore_database.librechat.uid}.${var.firestore_location}.firestore.goog:443/${var.firestore_database_name}?loadBalanced=true&tls=true&retryWrites=false&authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:gcp,TOKEN_RESOURCE:FIRESTORE"
}
# Firestore 互換との接続安定化:
# autoIndex を無効にし、接続直後のインデックス一斉作成を抑制
env {
name = "MONGO_AUTO_INDEX"
value = "false"
}
env {
name = "MONGO_AUTO_CREATE"
value = "false"
}
# mongoMeili 等の非同期エラーが uncaughtException になりクラッシュするのを防止
env {
name = "CONTINUE_ON_UNCAUGHT_EXCEPTION"
value = "true"
}
# Meilisearch 無効 (mongoMeili プラグインがクラッシュの原因のため)
env {
name = "SEARCH"
value = "false"
}
# Vertex AI (Gemini) - SA キーで認証
env {
name = "GOOGLE_SERVICE_KEY_FILE"
value_source {
secret_key_ref {
secret = google_secret_manager_secret.vertex_sa_key.secret_id
version = "latest"
}
}
}
# global を指定。リージョン指定だと一部モデルが利用不可
env {
name = "GOOGLE_LOC"
value = "global"
}
# Secret Manager からの参照
env {
name = "JWT_SECRET"
value_source {
secret_key_ref {
secret = google_secret_manager_secret.secrets["jwt-secret"].secret_id
version = "latest"
}
}
}
env {
name = "JWT_REFRESH_SECRET"
value_source {
secret_key_ref {
secret = google_secret_manager_secret.secrets["jwt-refresh-secret"].secret_id
version = "latest"
}
}
}
env {
name = "CREDS_KEY"
value_source {
secret_key_ref {
secret = google_secret_manager_secret.secrets["creds-key"].secret_id
version = "latest"
}
}
}
env {
name = "CREDS_IV"
value_source {
secret_key_ref {
secret = google_secret_manager_secret.secrets["creds-iv"].secret_id
version = "latest"
}
}
}
env {
name = "ALLOW_REGISTRATION"
value = "true"
}
env {
name = "SESSION_EXPIRY"
value = "900000" # 15 min
}
env {
name = "REFRESH_TOKEN_EXPIRY"
value = "604800000" # 7 days
}
startup_probe {
http_get {
path = "/health"
}
initial_delay_seconds = 10
period_seconds = 10
timeout_seconds = 5
failure_threshold = 30
}
liveness_probe {
http_get {
path = "/health"
}
period_seconds = 30
}
}
volumes {
name = "librechat-config"
gcs {
bucket = google_storage_bucket.librechat_config.name
read_only = true
}
}
}
depends_on = [
google_project_service.run,
google_project_service.aiplatform,
google_secret_manager_secret.secrets,
google_secret_manager_secret_version.vertex_sa_key,
google_storage_bucket_object.librechat_yaml,
]
}
resource "google_project_service" "run" {
project = var.project_id
service = "run.googleapis.com"
disable_on_destroy = false
}
resource "google_project_service" "aiplatform" {
project = var.project_id
service = "aiplatform.googleapis.com"
disable_on_destroy = false
}terraform.tfvars の設定例
project_id = "your-gcp-project-id"
region = "asia-northeast1"
environment = "dev"
firestore_database_name = "librechat"
firestore_location = "asia-northeast1"Firestore MongoDB 互換モードの解説
Firestore MongoDB 互換モードでは、通常の MongoDB と同じプロトコルで接続しますが、接続文字列のパラメータにいくつか注意点があります。
mongodb://<UID>.<LOCATION>.firestore.goog:443/<DATABASE>
?loadBalanced=true
&tls=true
&retryWrites=false
&authMechanism=MONGODB-OIDC
&authMechanismProperties=ENVIRONMENT:gcp,TOKEN_RESOURCE:FIRESTOREホスト名の `<UID>`は Firestore データベースごとに自動生成される一意の識別子です。Terraform では `google_firestore_database` の `uid` 属性から動的に組み立てています。
各パラメータの意味は以下の通りです。
パラメータ | 値 | 理由 |
loadBalanced | true | 必須。Firestore はマネージドサービスのため、ユーザーからのリクエストは Google Cloud 内部のロードバランサーを経由してバックエンドに分散されます。一方、MongoDB ドライバーは通常、接続先に対して `hello` コマンドを送信し、レプリカセットやスタンドアロン等のサーバー構成を自動検出(トポロジー検出)します。ロードバランサーの背後ではこの検出が正しく動作しないため、`loadBalanced=true` を指定してトポロジー検出をスキップさせます。 |
retryWrites | false | 必須。MongoDB ドライバーはデフォルトで書き込み失敗時に自動リトライしますが、Firestore 互換はこの機能に対応していません。デフォルト(true)のままだとエラーになります。 |
authMechanism | MONGODB-OIDC | Cloud Run のサービスアカウントの ID トークンで自動認証されます。DB のユーザー名・パスワード管理は不要です。 |
tls | true | Firestore への接続には TLS が必須です。 |
接続の安定化
LibreChat は内部で Mongoose(MongoDB の ODM ライブラリ)を使用しています。Mongoose には DB 接続直後にスキーマ定義に基づいてインデックスを自動作成する機能がありますが、Firestore MongoDB 互換はレート制限が厳しく、大量のインデックス作成要求で接続が切断されます。さらに Meilisearch 連携プラグイン(mongoMeili)が非同期エラーを投げ、Node.js の未捕捉例外としてプロセスが終了してしまいます。
Terraform コード内では以下の環境変数でこれらを抑制しています。
MONGO_AUTO_INDEX=false : インデックス自動作成を無効化
MONGO_AUTO_CREATE=false : コレクション自動作成を無効化
CONTINUE_ON_UNCAUGHT_EXCEPTION=true : 未捕捉例外でプロセスを終了させない
SEARCH=false : Meilisearch プラグインを無効化
5. 動作確認と制約
機能ごとの動作可否
機能 | 動作 | 備考 |
ユーザー登録・ログイン | OK | |
Gemini (Vertex AI) チャット | OK | |
マルチユーザー会話分離 | OK | ユーザー間で会話は見えない |
会話共有リンク | OK | |
MCP Server 利用 | OK | |
エージェント作成・利用 | OK | ただしリロードで一覧から消える(後述) |
管理者ダッシュボード | NG | `$bitsAllSet` 非サポート |
エージェント一覧(リロード後) | NG | 権限チェック失敗で空になる |
プロンプト一覧(リロード後) | NG | 同上 |
`$bitsAllSet` 問題
LibreChat v0.8では ACL(Access Control List)ベースのビットマスク権限を採用しています。`aclentries` コレクションに以下のようなドキュメントが保存されます。
{
principalType: "user",
principalId: "user-id-xxx",
resourceType: "agent",
resourceId: "agent-id-xxx",
permBits: 7 // 0b111 = read(1) + use(2) + edit(4)
}エージェントやプロンプトの一覧を取得する際、MongoDB の `$bitsAllSet` 演算子でビットマスクを照合します。
// 「このユーザーが閲覧権限を持つエージェントの一覧」
AclEntry.find({
principalId: userId,
resourceType: "agent",
permBits: { $bitsAllSet: 1 } // 閲覧ビットが立っているか
}).distinct('resourceId')Firestore MongoDB 互換は、ビット演算クエリ演算子を公式にサポートしていません。Supported features ドキュメントの Bitwise operators セクションで、以下が全て「No」と明記されています。
演算子 | サポート |
$bitsAllSet | No |
$bitsAnySet | No |
$bitsAllClear | No |
$bitsAnyClear | No |
この結果、以下の流れでエージェント一覧が表示されなくなります。
ユーザーがエージェント一覧をリクエスト
LibreChat の PermissionService が `$bitsAllSet` クエリを実行
Firestore が `unknown operator $bitsAllSet` エラーを返す
PermissionService がエラーをキャッチし、アクセス可能リソース = 空配列で返す
UI にはエージェントが0件として表示される
データ自体は Firestore に保存されています。リロード時の権限チェッククエリが失敗するため、作成したエージェントやプロンプトが「消えた」ように見えます。
なお、チャット(会話)の一覧取得は PermissionService を経由せず、単純に `userId` でフィルタするだけなので正常に動作します。
6. まとめ
問題なく動作する機能
以下の機能は Firestore MongoDB 互換 + Cloud Run の構成で問題なく利用できます。
ユーザー登録・ログイン
チャット
マルチユーザーの会話分離
会話共有リンク
MCP Server 連携
これらの機能のみを使う場合、Firestore MongoDB 互換 + Cloud Run の構成は十分に実用的です。MongoDB を別途構築・運用する必要がなく、フルマネージドかつサーバーレスで LibreChat を動かせます。
動作しない機能
ただし、以下の機能は Firestore MongoDB 互換の `$bitsAllSet` 演算子未サポートにより動作しません。
管理画面(`/d/admin`): 何も表示されない
エージェント一覧 : 作成できるがリロードすると消える
プロンプト一覧 : 同上
エージェントやプロンプトの活用、管理者による権限制御が必要な場合は、この構成では対応できません。その場合は Cloud Run + MongoDB Atlas の構成や、GCE / GKE 上に Docker で MongoDB を構築する構成を検討してください。
今後の展望
Firestore MongoDB 互換は現在も機能拡充が進んでおり、Supported features のページは定期的に更新されています。ビット演算クエリ演算子(`$bitsAllSet` 等)がサポートされた際には、追加検証を実施し別途記事を更新したいと思います。

コメント