見出し画像

Supabase をやめて、ローカルに全部持ってきた話(AI漫画制作の裏側)


こんにちは、グイグイです⚡

今回は「Supabase をやめて、ローカルに全部持ってきた話」を書こうと思います。
※厳密にはやめてないです。Supabaseをローカルに構築した感じです。💦

今、僕は AI 漫画を制作していて、そのために自作のアプリを作って運用しています。

アプリは Next.js で作っていて、完全に「自分だけが使う用」のローカルアプリです。

ただ、制作していると生成画像がどんどん増えていくので、

「これ、どこかにちゃんと永続化して保存しておきたいな」

と思って、AIに相談したところ、

「それなら Supabase がいいですよ。無料枠でも結構使えますよ」

と言われて、じゃあそれでいいか、という感じで Supabase を使い始めました。

Supabaseの画面

最初は無料プランで十分だった

最初のうちは、Supabase の無料プランで特に困ることはありませんでした。
画像もそこそこ保存できるし、動作も安定しているし、

「あ、これ普通に使えるじゃん」

という感じでした。


AI漫画制作が本格化して、状況が変わる

ところが、AI漫画の制作が本格化してくると、

  • 生成する画像の枚数が増える(第5話完成時点で2800枚の画像が💦)

  • Supabase に保存している画像の数も増える

  • ストレージ容量がどんどん食われる

という状態になってきました。

そして、つい昨日。

Supabase からこんなメールが来ました。

「容量オーバーしてます。プロプランにアップグレードするか、容量を減らしてください」

……あー、来たか、という感じです。


有料プラン、思ったより普通に高い

じゃあ Supabase の有料プランっていくらなんだろう、と思って見てみたら、

月額 25 ドル〜

まあまあ、普通にします。

正直、

「この漫画、15話まで作ったら一旦区切る予定なんだよな……」

という状態なので、このために毎月課金するのも、ちょっと微妙だなと。


そもそも無料プランは「止まる」

もう一つ問題があって、Supabase の無料プランって、

7日間くらい何もアクセスがないと「プロジェクト止めるぞ」って言われる

んですよね。

実際、

  • しばらく触ってないと警告が来る

  • 使ってないプロジェクトは止めてリソースを他に回す、という思想

なので、完全に

「小規模な個人のお試し用途」

向けなんですよね。

今の使い方だと、ちょっと合わなくなってきたな、という感覚がありました。


じゃあ、ローカルに全部持ってくるか

そこで、

「もうこれ、ローカルに持ってきたほうがいいんじゃない?」

と思って、AIに相談。

結果、

  • Supabase をローカルにコンテナとして構築

  • クラウド上のデータを全部ローカルに移行

  • Dockerでコンテナとして構築すればアプリ側の実装は変えずに済む(接続先のURLとkeyだけ変更すればいい)

ということをやりました。

当然ですが、そこそこハマりました。

「あー、そこそうなるのね……」

みたいなのを、何回か踏みました。

せっかくなので、このときの実作業ログを、ほぼそのままの形で残しておこうと思います。
同じことをやる人がいたら、たぶん確実に役に立つはずなので。


今回、どこが一番しんどかったか

結論から言うと、一番ハマったのは、

「2. データベースのダンプ(クラウドからの救出)」の工程

です。

Supabase のクラウドって、接続方式がいくつか用意されていて、

  • Direct Connection(直接接続)

  • Connection Pooler

  • もう1種類(リージョンや構成による)

みたいな感じで選べるんですが、このうち Direct Connection は IPv6 専用なんですよね。

正直、最初はそんなこと全然意識してなくて、

「普通に pg_dump 叩けば取れるでしょ」

くらいのノリでやってました。

ところが、自宅の固定回線(IPv4環境)からだと、どうやら Supabase のクラウドに直接接続できないっぽい、というところでハマり始めました。

  • コマンドは合ってそうなのに接続できない

  • 何をやっても Network unreachable

  • Pooler 経由も試すけど認証周りで不安定

みたいな感じで、しばらく完全に迷子でした。

最終的に、

「これ、IPv6でつながるネットワークから実行するしかないな」

という結論になって、スマホのテザリングで接続するという力技に行き着きました。

これはもう、エラーを AI に投げ続けていたら、

「それ、回線が IPv4 だからじゃないですか? テザリング試してみては?」

と提案されて、

「あー……なるほど……」

ってなったやつです。
結果的に、これで一発でつながりました。

今回の作業の中で、ここが一番「環境依存でハマったポイント」でした。


次にハマったのが、文字コード地獄

次にハマったのが、DBインポート時の文字コード問題です。

最初、このあたりの手順を Jenova.ai と一緒に詰めてたんですが、

  • Windows のパイプ処理の罠

  • PowerShell のリダイレクトの挙動

  • UTF-8 になっているつもりが UTF-16 (BOM付き) になっている問題

このへんを全然ケアできてなくて、

「ダンプは取れてるのに、インポートするとエラーまみれ」

という状態に何回もなりました。

結果として、

  • ダンプを取り直す

  • 文字コードを疑う

  • また取り直す

  • それでも壊れる

みたいなことを何周かやってます。

ここも最終的には、

「PowerShell が勝手に文字コード変えてるのが原因」

というところにたどり着いて、
cmd /c 経由でダンプを取る、というやり方に落ち着きました。

このへんの話は、下の作業ログの「トラブルシューティング」に全部そのまま残っています。


というわけで、ここから完全作業ログ

ここから先は、

実際に AI と殴り合いながらトラブルを踏み抜いて復旧した記録を、整理してもらったものをそのまま貼っています。


Supabase クラウド → ローカル (Windows/Docker) 完全移行ログ

Supabase クラウド → ローカル (Windows/Docker) 完全移行ログ

📌 概要

  • 目的: 容量制限(500MB)を超過したSupabaseプロジェクト(約1.4GB)を、ローカルPC上のDocker環境へ完全移行する。

  • 環境: Windows 11, Docker Desktop (WSL2), PowerShell

  • 結果: DB構造・データ・画像ファイル(Storage)のすべてを移行完了。


✅ 【正解ルート】再現性のある完全移行手順

数々のエラーを経て確立された、「最短かつ失敗しない」手順。

1. ローカル環境の準備

まず、受け皿となるSupabaseをDockerで起動し、初期状態(クリーン)にする。

# プロジェクトフォルダへ移動
mkdir ai-comic-local
cd ai-comic-local

# 初期化と起動
npx supabase init
npx supabase start

# 【重要】もし過去のゴミデータが残っている場合はリセット
# npx supabase stop --no-backup
# npx supabase start

2. データベースのダンプ(クラウドからの救出)

最大の難関。 以下の条件を揃えることで突破する。

  • ネットワーク: IPv6対応回線(スマホのテザリング等)を使用し、Direct Connection を行う。

  • ツール: Docker経由で postgres:17 を使い、サーバーバージョンに合わせる。

  • 文字コード: PowerShellのエンコーディング問題を回避するため、cmd /c を噛ませる。

実行コマンド (PowerShell):

# パスワードには特殊文字が含まれるためシングルクォーテーションで囲む
$env:PGPASSWORD='[DBパスワード]'

# cmd経由で実行し、UTF-8のままファイルに保存する
cmd /c "docker run --rm --network host -e PGPASSWORD=$env:PGPASSWORD postgres:17 pg_dump -h [Direct接続のホスト名] -p 5432 -U postgres -d postgres --clean --if-exists > supabase_dump.sql"

確認: 生成された supabase_dump.sql が数百MB〜1GB超のサイズになっていること。(これは自分の場合はそのくらいデータがあっただけです)

3. ローカルDBへのインポート

Windows上でのパイプ処理(|)は文字化けの原因となるため、ファイルをコンテナ内に転送してから実行する。

# 1. ファイルをコンテナ内部へコピー
docker cp .\supabase_dump.sql supabase_db_ai-comic-local:/tmp/dump.sql

# 2. コンテナ内部から psql を実行(文字化けリスク・ゼロ)
docker exec -it supabase_db_ai-comic-local psql -U postgres -f /tmp/dump.sql

ERROR: must be owner of... 等のエラーは無視してOK。
最後にプロンプトが戻れば成功。Studio (http://localhost:54323) でデータを確認。

4. Storage(画像データ)の移行

DB移行だけでは画像の実体がないため、スクリプトで一括ダウンロード・アップロードを行う。

  1. npm install @supabase/supabase-js を実行。

  2. migrate-storage.js を作成(コードは以下に掲載、service_roleキーを使用)。

  3. node migrate-storage.js を実行。


マイグレーションツールの使い方

1. 必要な準備

まず、必要なライブラリをインストールします。

npm install @supabase/supabase-js dotenv

次に、プロジェクトのルート(同じフォルダ)に .env という名前のファイルを作成し、キーを記述します。

.env ファイルの中身:

CLOUD_SERVICE_ROLE_KEY=eyJh......(クラウド側のservice_roleキー)
LOCAL_SERVICE_ROLE_KEY=eyJh......(ローカル側のservice_roleキー)

2. 移行スクリプト(migrate-storage.js)

設定ファイルの準備ができたら、以下のコードを migrate-storage.js として保存し、実行してください。

// .env ファイルから読み込む形にする
require('dotenv').config();

// ▼設定:ここだけ書き換えてください▼
// Supabase管理画面 > Project Settings > API > service_role (secret) から取得
const CLOUD_SERVICE_ROLE_KEY = process.env.CLOUD_SERVICE_ROLE_KEY;
const LOCAL_SERVICE_ROLE_KEY = process.env.LOCAL_SERVICE_ROLE_KEY;
const BUCKET_NAME = 'XXXXXXXXXXXX'; // ログにあったバケット名
// ▲設定ここまで▲

const cloudSupabase = createClient('https://xxxxxxxxxx.supabase.co', CLOUD_SERVICE_ROLE_KEY);
const localSupabase = createClient('http://127.0.0.1:54321', LOCAL_SERVICE_ROLE_KEY);

// クライアントを指定してファイル一覧を取得する関数に改良
async function getAllFiles(client, bucket, path = '') {
  let allFiles = [];
  const { data, error } = await client.storage.from(bucket).list(path, { limit: 100 });
  if (error) {
    // バケットが空、または存在しない場合は空配列を返す(ローカルの初回チェック用)
    return [];
  }

  for (const item of data) {
    if (item.name === '.emptyFolderPlaceholder') continue;
    
    if (item.id === null) {
      const subPath = path ? `${path}/${item.name}` : item.name;
      const subFiles = await getAllFiles(client, bucket, subPath);
      allFiles = allFiles.concat(subFiles);
    } else {
      allFiles.push(path ? `${path}/${item.name}` : item.name);
    }
  }
  return allFiles;
}

async function migrate() {
  console.log("---------------------------------------------------");
  console.log("🚀 移行プロセスを再開します(差分転送モード)");
  console.log("---------------------------------------------------");

  try {
    // 1. クラウド(コピー元)の全ファイルリストを取得
    process.stdout.write(`☁️  [Cloud] ファイル一覧を取得中... `);
    const cloudFiles = await getAllFiles(cloudSupabase, BUCKET_NAME);
    console.log(`✅ ${cloudFiles.length} ファイル`);

    // 2. ローカル(コピー先)の全ファイルリストを取得
    process.stdout.write(`💻 [Local] 既存ファイルを確認中... `);
    const localFiles = await getAllFiles(localSupabase, BUCKET_NAME);
    const localFileSet = new Set(localFiles); // 高速検索用にSet化
    console.log(`✅ ${localFiles.length} ファイル済み`);

    // 3. 差分(まだローカルにないファイル)だけを抽出
    const filesToMigrate = cloudFiles.filter(f => !localFileSet.has(f));
    
    if (filesToMigrate.length === 0) {
      console.log("\n🎉 全てのファイルは移行済みです!作業はありません。");
      return;
    }

    console.log(`\n📦 残り ${filesToMigrate.length} ファイルの転送を開始します...`);

    // 4. 転送実行
    for (const [index, file] of filesToMigrate.entries()) {
      // 進捗表示
      const percent = Math.round(((index + 1) / filesToMigrate.length) * 100);
      console.log(`[${index + 1}/${filesToMigrate.length}] ${percent}% ダウンロード中: ${file}`);
      
      const { data: blob, error: dlError } = await cloudSupabase.storage.from(BUCKET_NAME).download(file);
      if (dlError) {
        console.error(`  ❌ DL失敗: ${file}`, dlError.message);
        continue;
      }

      const arrayBuffer = await blob.arrayBuffer();
      const buffer = Buffer.from(arrayBuffer);

      const { error: upError } = await localSupabase.storage.from(BUCKET_NAME).upload(file, buffer, {
        upsert: true,
        contentType: blob.type
      });

      if (upError) console.error(`  ❌ UP失敗: ${file}`, upError.message);
    }
    console.log("\n🎉 全ての移行が完了しました!");

  } catch (err) {
    console.error("\n💥 重大なエラーが発生しました:", err);
  }
}

migrate();

💥 トラブルシューティング(起きた問題と解決策)

今回の作業で発生したエラーとその原因、最終的な解決策の記録。

1. ネットワーク接続エラー

  • 事象: pg_dump: error: connection to server ... failed: Network is unreachable

  • 原因: SupabaseのDirect Connectionは現在 IPv6のみ 提供されている。自宅の固定回線(IPv4)からは到達できない。

  • 試行錯誤:

    • Connection Pooler(ポート6543)の使用 → 認証エラーやタイムアウトで不安定。

    • aws-ipv4 アドレスの使用 → 有料アドオンが必要な場合がある。

  • ✅ 解決策: 「スマホのテザリング」を使用。キャリア回線(IPv6対応)を経由することで、物理的にネットワークを開通させた。

2. 認証・ユーザー名エラー

  • 事象: FATAL: Tenant or user not found / no such user

  • 原因: Connection Pooler(Supavisor)経由の場合、ユーザー名に .session を付けるか付けないかの挙動が不安定、かつDB名の指定も厳密さが求められるため。

  • ✅ 解決策: テザリングで「Direct Connection(ポート5432)」を使用したため、複雑なユーザー名修飾が不要になり、シンプルな postgres ユーザーで接続成功。

3. バージョン不一致エラー

  • 事象: pg_dump: server version: 17.6; pg_dump version: 15.4

  • 原因: ローカルPCに入っていたPostgreSQLのバージョンが古かった。

  • ✅ 解決策: ローカルにインストールせず、Dockerのイメージを使用。

    • docker run --rm ... postgres:17 pg_dump ...

    • これにより、即座に最新バージョンのツールを利用可能にした。

4. PowerShellの文字コード破壊(最重要)

  • 事象: インポート時に ERROR: invalid byte sequence for encoding "UTF8": 0xff や大量の syntax error が発生。

  • 原因: PowerShellで > filename.sql とリダイレクトすると、デフォルトで UTF-16 (BOM付き) に変換して保存してしまう。Postgresはこれを読めない。

  • ✅ 解決策: cmd /c "..." を使い、コマンドプロンプト経由で実行することで、リダイレクト処理時の勝手なエンコード変換を回避し、純粋なUTF-8(バイナリ)として保存させた。

5. パイプライン経由のインポート失敗

  • 事象: Get-Content ... | docker exec ... でインポートしようとするとエラーや激遅になる。

  • 原因: 1.4GBの巨大テキストをPowerShellが読み込んでパイプするのはメモリ効率が悪く、かつここでもエンコーディング問題が発生する。

  • ✅ 解決策: docker cp でファイルをコンテナ内に物理コピーし、Linux内部で完結させて実行した。


📝 今後の運用メモ

  • データの場所:
    画像ファイルの実体はWindowsフォルダ直下ではなく、Docker Volume(仮想ディスク)内にある。

  • バックアップ:
    今回作成した supabase_dump.sql は貴重なバックアップ資産。

  • 環境リセット⚠️:
    💥 npx supabase stop --no-backup💥 は全データを消すコマンドなので、今後は安易に使わないこと。


最後に(AI時代のスピード感について)

今回この Supabase のローカル移行、実は

日曜日の朝に Supabase から警告メールが来て、「じゃあ今日のうちに何とかするか」と思って着手して、その日のうちに完全移行が終わっています。

といっても、ずっと張り付いて作業していたわけではなくて、

  • 午前中に2時間くらい

  • 途中で休憩したり、子供のテニスの習い事に送っていったり

  • 買い物に行ったり

  • 夕方に1時間くらい

  • 夜ご飯のあとに1時間くらい

みたいな感じで、合間合間に進めただけです。

実作業時間としては、たぶん 4〜5時間くらい しかやっていません。

それで、

クラウド → ローカルへの完全移行が全部終わっている

という状態になっています。

これ、たぶん昔だったら絶対ありえないスピード感です。

昔ならまず、

  • 「どうやって移行するんだっけ?」を調べる

  • Supabase の公式ドキュメントを読む

  • 手順を理解する

  • 試す

  • 失敗する

  • また調べる

みたいなことを延々と繰り返して、

たぶん、土日を何回か潰しながら、下手したら数週間〜数ヶ月コース

になっていたと思います。

それが今は、

AIにエラーを投げ続けながら、その場で修正し、その場で次に進む

というやり方で、
「休日の隙間時間」だけで全部終わってしまう。

このスピード感は、正直ちょっと異常です。

いろいろ具体的な移行方法やハマりポイントも書きましたが、結局のところ、

「ああ、これが AI 時代なんだな」

と思った出来事でした。



#Supabase #クラウド #ローカル移行 #AI漫画 #AIマンガ #生成AI


いいなと思ったら応援しよう!

グイグイ ⚡ 圧倒的AI実務家 この記事が少しでも役に立ったと思ったら、サポートいただけると励みになります!