メインコンテンツへスキップ

LibreChat を Cloud Run と Firestore MongoDB 互換で構築する

·10 分
著者
Shogo Umeda

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 互換を採用したサーバーレス構成になっています。

構成図
#

LibreChat構成図
LibreChat構成図

GCP リソース一覧
#

リソース用途
Cloud Run (LibreChat)メインアプリケーション
Firestore EnterpriseMongoDB 互換モードのデータベース
Vertex AIGemini API によるチャット
Secret ManagerJWT シークレット、SA キー等の管理
Cloud Storagelibrechat.yaml の配信(GCS FUSE マウント)
Artifact Registryghcr.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_databaseuid 属性から動的に組み立てています。

各パラメータの意味は以下の通りです。

パラメータ理由
loadBalancedtrue必須。Firestore はマネージドサービスのため、ユーザーからのリクエストは Google Cloud 内部のロードバランサーを経由してバックエンドに分散されます。一方、MongoDB ドライバーは通常、接続先に対して hello コマンドを送信し、レプリカセットやスタンドアロン等のサーバー構成を自動検出(トポロジー検出)します。ロードバランサーの背後ではこの検出が正しく動作しないため、loadBalanced=true を指定してトポロジー検出をスキップさせます。
retryWritesfalse必須。MongoDB ドライバーはデフォルトで書き込み失敗時に自動リトライしますが、Firestore 互換はこの機能に対応していません。デフォルト(true)のままだとエラーになります。
authMechanismMONGODB-OIDCCloud Run のサービスアカウントの ID トークンで自動認証されます。DB のユーザー名・パスワード管理は不要です。
tlstrueFirestore への接続には 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」と明記されています。

演算子サポート
$bitsAllSetNo
$bitsAnySetNo
$bitsAllClearNo
$bitsAnyClearNo

この結果、以下の流れでエージェント一覧が表示されなくなります。

  1. ユーザーがエージェント一覧をリクエスト
  2. LibreChat の PermissionService が $bitsAllSet クエリを実行
  3. Firestore が unknown operator $bitsAllSet エラーを返す
  4. PermissionService がエラーをキャッチし、アクセス可能リソース = 空配列で返す
  5. 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 等)がサポートされた際には、追加検証を実施し別途記事を更新したいと思います。

7. 参考リンク
#