Supabase をやめて、ローカルに全部持ってきた話(AI漫画制作の裏側)
こんにちは、グイグイです⚡
今回は「Supabase をやめて、ローカルに全部持ってきた話」を書こうと思います。
※厳密にはやめてないです。Supabaseをローカルに構築した感じです。💦
今、僕は AI 漫画を制作していて、そのために自作のアプリを作って運用しています。
アプリは Next.js で作っていて、完全に「自分だけが使う用」のローカルアプリです。
ただ、制作していると生成画像がどんどん増えていくので、
「これ、どこかにちゃんと永続化して保存しておきたいな」
と思って、AIに相談したところ、
「それなら 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移行だけでは画像の実体がないため、スクリプトで一括ダウンロード・アップロードを行う。
npm install @supabase/supabase-js を実行。
migrate-storage.js を作成(コードは以下に掲載、service_roleキーを使用)。
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
いいなと思ったら応援しよう!
この記事が少しでも役に立ったと思ったら、サポートいただけると励みになります!