top of page

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


構成図


LibreChat構成図

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


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


  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. 参考リンク


コメント


bottom of page