INFRA

Turso を本番DBにできず SQLite + Fly Volume に切り替えた

SaaS で「1社1DB」の物理分離をやろうとして Turso (libsql) を入れたら、SQLAlchemy 2.0 async + alembic で本番に通る経路が見つからず、SQLite + Fly Volume + Litestream に切り替えた記録。

「1社につき Fly.io app 1 つ + DB 1 つ」をテナントごとに丸ごと分ける構成にしようとしていた。

DB は最初 Turso(libsql ベースのマネージド SQLite)にする予定だった。Fly.io app と Turso DB を作って、fly secrets set DATABASE_URL=libsql://... TURSO_AUTH_TOKEN=... まで通った。

fly deploy のビルドも通った。詰まったのは release_command に書いた alembic upgrade head が起動するところ:

sqlalchemy.exc.NoSuchModuleError: Can't load plugin: sqlalchemy.dialects:libsql

調べた範囲

PyPI と GitHub を見て回って、こうなっていた(2026-06 時点):

  • sqlalchemy-libsql 0.2.0 は sync ダイアレクトのみ。アプリは FastAPI + SQLAlchemy 2.0 の async で書いてあるので、ここに sync を混ぜる構造になる
  • libsql-experimental は async 対応だが、SQLAlchemy に繋ぐパッケージは公式には無い
  • Turso の embedded replica(ローカルに SQLite ファイルを置いて、裏で Turso と同期する方式)は async から使えるが、アプリ側にレプリケーションの同期処理を組み込む必要がある

どれも、SQLAlchemy 2.0 async + alembic 構成の本番DBドライバとして「DATABASE_URL を差し替えるだけで動く」状態には今は無い。

切り替え

きちんと統合すれば数日かかるし、Tursoであることのメリットはそこまで大きくないかなと思い、下記に構成を変えた。

[Fly.io VM (1社につき1台)]
app (FastAPI)
└─ /data/logicky.db (SQLite。Fly Volume にマウント)
└─ Litestream → Cloudflare R2 (継続バックアップ)

fly.toml 側はこれだけ:

fly.toml
[env]
# 4スラッシュ = 絶対パス
DATABASE_URL = "sqlite+aiosqlite:////data/logicky.db"
[[mounts]]
source = "logicky_data"
destination = "/data"
initial_size = "1gb"

alembic upgrade headaiosqlite ドライバでそのまま通る。

物理分離の話は変わらない。VM が1社ごとに別、Volume も1社ごとに別、DB ファイルも別。テナント間でデータが混ざる経路は依然として無い。

切り替えで失ったもの

  • マルチリージョンの読み取りレプリカ:今は国内のお客様しか想定していないので、効きが薄い
  • Turso の DB を API で作れる仕組み:テナント数が一桁のうちは fly apps create + fly volumes create を手で叩ける範囲

いつ Turso に戻すか

例えば、数百DBが必要になったり、マルチリージョンが重要になったりする場合に、Tursoへの切り替えをまた検討すればいいかなと思った。 SQLite ファイルなので、移行は中身をエクスポートして Turso に流し込むだけで済む形にはなる。