はじめに
SupabaseのPostgreSQLデータベースを、GitHub Actionsを使って定期的にバックアップする方法をまとめました。
当初はSupabase Storageに保存された画像ファイルの自動削除も検討しましたが、「まずは大切なデータを守る仕組みを作る」ことを優先し、今回はデータベースのバックアップに集中しました。削除機能は、バックアップが安定稼働してから検討する予定です。
ここで実現すること
- ✅ Supabase の PostgreSQL データベースを安全にバックアップ
- ✅
service_role_keyを外部に出さないセキュアな設計 - ✅ GitHub Actions による週1回の自動バックアップ
- ✅ 約4世代分のバックアップを保持
- ✅ 月1回、外付け SSD に手動で退避できる形式で保存
- ✅ 他のPostgreSQLサービス(AWS RDS、GCP Cloud SQLなど)への移行も可能
全体の流れ
GitHub Actions(週1回自動実行)
│
├─ PostgreSQL 17 の pg_dump をインストール
│
├─ Supabaseデータベースに接続してバックアップ取得
│
└─ GitHub Artifacts に .dump ファイルを保存(35日間保持)
│
└─ 月1回、手動でダウンロード → 外付けSSDに保存
バックアップファイルは .dump 形式(PostgreSQL custom format)で保存されます。この形式は圧縮されているためサイズが小さく、復元時の柔軟性も高いです。
前提条件
この手順を進めるには、以下が必要です:
- ✅ Supabase プロジェクトが作成済み
- ✅ GitHub アカウントとリポジトリ(バックアップ専用でOK)
- ✅ Supabase プロジェクトのデータベース接続情報にアクセスできる権限
特別な知識は必要ありませんが、GitHubの基本的な操作(リポジトリの作成、Secretsの設定など)ができると進めやすいです。
手順1:Supabase側の準備
データベース接続情報の確認
まず、Supabaseのダッシュボードから接続情報を取得します。

- Supabaseダッシュボードにログイン
- 対象のプロジェクトを選択
- ヘッダーから
Connectを開く Method→Transaction pooler
ここで重要なのは、Transaction pooler の接続文字列を使うことです。
- ポート番号:
6543 - 接続形式: URI形式(
postgresql://...)
接続文字列の例(実際の値はダミーです):
postgresql://postgres:your_password_here@db.xxxxx.supabase.co:6543/postgres?sslmode=require
重要な注意点
- ⚠️ ここで使うパスワードは、Supabaseプロジェクト作成時に設定したデータベースパスワードです
- ⚠️ Supabaseへのログインパスワードとは異なります
- ⚠️ パスワードを忘れた場合は、Database設定画面から再設定できます
この接続文字列は次のステップで使います。
手順2:GitHub Secretsの設定
接続情報をGitHub Actionsで安全に使うため、Secretsに保存します。
設定手順

- GitHubでバックアップ用のリポジトリを開く
Settingsタブをクリック- 左メニューから
Secrets and variables→Actionsを選択 New repository secretをクリック- 以下の内容で登録:
- Name:
SUPABASE_DB_URL - Secret: 先ほどコピーした接続文字列(URI全体)
これで、GitHub ActionsからSupabaseデータベースに安全に接続できるようになります。
手順3:GitHub Actions workflowの作成
ファイルの配置
リポジトリのルートに、以下のフォルダ構造でファイルを作成します:
your-repository/
└── .github/
└── workflows/
└── supabase-db-backup.yml
workflowファイルの内容
.github/workflows/supabase-db-backup.yml に以下の内容を記述する:
cron 設定(週1回)
on:
schedule:
# 毎週月曜 03:15 JST(= 日曜 18:15 UTC)
- cron: "15 18 * * 0"
workflow_dispatch: {}
pg_dump で Supabase DB をバックアップ
- name: Create backup
env:
SUPABASE_DB_URL: ${{ secrets['SUPABASE_DB_URL'] }}
run: |
TS=$(date -u +"%Y%m%dT%H%M%SZ")
mkdir -p backups
pg_dump "$SUPABASE_DB_URL" \
--format=custom \
--no-owner \
--no-privileges \
--file "backups/supabase_${TS}.dump"
format=custom:容量が小さく、復元時に柔軟- 権限・オーナー情報は除外(環境差異対策)
Artifact として保存
- uses: actions/upload-artifact@v4
with:
name: supabase-db-backup
path: backups/
retention-days: 35
- GitHub Actions 上に 約4世代分保持
workflowの内容説明
このworkflowは3つのステップで動作します:
ステップ1: PostgreSQL 17 クライアントのインストール
- Ubuntu標準のPostgreSQLクライアントはバージョン16ですが、Supabaseはバージョン17を使用しています
- PGDG(PostgreSQL Global Development Group)の公式リポジトリから、バージョン17のクライアントをインストールします
- バージョンが一致していないとバックアップが失敗します(後述)
ステップ2: バックアップの作成
pg_dumpコマンドでデータベースをダンプします- タイムスタンプ付きのファイル名(例:
supabase_20260114T123000Z.dump)で保存します --format=customオプションで圧縮形式にすることで、ファイルサイズを小さく保ちます
ステップ3: Artifactへのアップロード
- 作成したバックアップファイルをGitHub Artifactsに保存します
retention-days: 35で約5週間分(4〜5世代)を保持します
実行タイミング
- 自動実行: 毎週月曜日の深夜(日本時間03:15)
- 手動実行: GitHubのActionsタブから
Run workflowボタンでいつでも実行可能
ハマったポイント:pg_dumpのバージョン問題
実装中、最も苦労したのがPostgreSQLのバージョン不一致問題でした。
発生したエラー
pg_dump: error: aborting because of server version mismatch
server version: 17.4
pg_dump version: 16.11
原因
- Ubuntu標準の
postgresql-clientパッケージはバージョン16系 - Supabaseのデータベースサーバーはバージョン17.4
- pg_dumpはサーバーとクライアントのメジャーバージョンが一致していないと動作しません
解決方法
PGDG(PostgreSQL Global Development Group)の公式リポジトリから、PostgreSQL 17のクライアントをインストールすることで解決しました。
さらに、インストール後はフルパス指定で pg_dump を実行することで、確実にバージョン17のコマンドが使われるようにしています。
/usr/lib/postgresql/17/bin/pg_dump --version
.dumpファイルについて
バックアップファイルは .dump という拡張子で保存されます。
.dumpファイルとは
PostgreSQLの pg_dump コマンドで --format=custom オプションを使うと生成される、独自のバイナリ形式のバックアップファイルです。
1. ファイルサイズが小さい
- 自動的に圧縮されます
- SQL形式と比べて大幅に小さくなります
- データ量がまだ少ない場合、数百KB〜数MBになります
2. 復元時の柔軟性が高い
- 特定のテーブルだけ復元することができます
- 復元順序を自動で最適化してくれます
- エラーが発生しても続行できるオプションがあります
3. 他のPostgreSQLサービスへの移行が可能(・・・という情報)
- AWS RDS for PostgreSQL
- Google Cloud SQL for PostgreSQL
- Azure Database for PostgreSQL
- ローカルのPostgreSQLサーバー
これらのサービスにも、この.dumpファイルを使って復元できます。Supabaseに依存しない形式なので、将来的な選択肢が広がります。
ファイルサイズが小さい理由
バックアップファイルを見て「こんなに小さくて大丈夫?」と不安になるかもしれませんが、以下の理由で正常です:
- ✅ 自動的に圧縮されている
- ✅ WAL(Write-Ahead Log)や未使用領域は含まれない
- ✅ データ量がまだ少ない段階では数百KB〜数MBが普通
データが増えるにつれて、ファイルサイズも比例して大きくなっていきます。
外付けSSDへも保存
GitHub Artifactsは35日間で自動削除されるように設定したため、長期保存用として外付けSSDにもバックアップを保存します。
SSDに保存するもの
.dump ファイルのみでOKです。
このファイルがあれば、データベース全体を復元できます。
推奨フォルダ構成
ExternalSSD/
└── supabase-backup/
└── crop-tracker/ ← プロジェクト名
├── 2026-01/ ← 年月でフォルダ分け
│ └── supabase_20260106T120000Z.dump
├── 2026-02/
│ └── supabase_20260203T120000Z.dump
└── 2026-03/
└── supabase_20260310T120000Z.dump
年月でフォルダを分けることで、後から探しやすくなります。
月1回の保存手順
- GitHubリポジトリの
Actionsタブを開く - 最新のworkflow実行結果を開く
Artifactsセクションからsupabase-db-backupをダウンロード- ZIPファイルを展開
.dumpファイルを外付けSSDの該当フォルダにコピー- 完了
実際に復元できるか試してみた
念のため、本当にバックアップから復元できるかを試した。
以下は、そのときに実際にやった手順の記録。
① ターミナルでバックアップファイルを配置
GitHub Actions で取得した .dump ファイルを
Mac の Documents フォルダ(自分がわかる場所)に移動。
cd ~/Documents
ls(エル・エス)
ここで supabase_YYYYMMDDTHHMMSSZ.dump が見える状態にしておく。
② PostgreSQL クライアントをインストール(初回のみ)
※ Homebrew が入っている前提
brew install postgresql@17
Supabase の PostgreSQL は 17系 のため、pg_restore も 17系で揃える必要がある。
③ PostgreSQL 17 を明示的に使うようにする(初回のみ)
brew link --force --overwrite postgresql@17
このコマンドの意味
brew installしただけでは
どの PostgreSQL が使われるか確定しない- すでに 16 や 18 が入っていると
古いpg_restoreが使われることがある
このコマンドは、
- PostgreSQL 17 を
- システムのデフォルトとして強制的に使う
という指定。
実行後、以下で確認できる。
pg_restore --version
pg_restore (PostgreSQL) 17.x
これが出ていればOK。
④まずはdumpの中身を確認
いきなり復元せず、
dumpの中身を確認すると安全です。
pg_restore -l supabase_YYYYMMDDTHHMMSSZ.dump
ここで、
TABLE DATA public destinations
POLICY public destinations ...
ROW SECURITY public destinations
などが出ていれば、
そのテーブル(今回はdesitnationsというテーブル)は復元可能です。
⑤「一部だけ復元」を実行
実際には、
テーブル構造は残っていて、中身だけ消した
というケースが多いです。
その場合は data-only + table指定 を使います。
例:destinations(出荷先マスタ) だけ復元する
pg_restore \
--dbname="postgresql://USER:PASSWORD@HOST:PORT/postgres" \
--data-only \
--table=public.destinations \
supabase_YYYYMMDDTHHMMSSZ.dump
pg_restore: リストアのためデータベースに接続しています
pg_restore: テーブル”public.destinations”のデータを処理しています
と、メッセージが出れば成功です。
ポイント
public.は 必須- Supabaseは schema =
publicが基本
全部戻す場合の復元コマンド(未確認)
全部戻す場合 の基本形ですが、これは未実施です。
pg_restore \
--dbname="postgresql://USER:PASSWORD@HOST:PORT/postgres" \
--no-owner \
--no-privileges \
supabase_20260118T184427Z.dump
各項目の意味(初心者向け)
| 項目 | 意味 |
|---|---|
| pg_restore | PostgreSQLのバックアップ復元コマンド |
| USER | DBユーザー名(通常 postgres) |
| PASSWORD | プロジェクトのDBパスワード |
| HOST | xxx.pooler.supabase.com |
| PORT | 6543(Transaction Pooler) |
| postgres | 接続するDB名 |
| –no-owner | 所有者情報を無視 |
| –no-privileges | 権限設定を無視 |
| .dump | GitHub Actionsで作成したバックアップ |
復元の注意点
- ⚠️ このバックアップにはデータベースのデータのみが含まれます
- ⚠️ Supabase Auth(認証情報)は含まれません
- ⚠️ Supabase Storage(ファイル)は含まれません
- ⚠️ Supabase Functions(Edge Functions)は含まれません
Auth、Storage、Functionsについては、別途バックアップや移行作業が必要です。
ただし、データベースのバックアップとしては、これで十分な内容が保存されています。
Storage削除の自動化を見送った理由
当初は、データベースのバックアップと同時に、Supabase Storageに保存された古い画像ファイルの自動削除も検討していました。
検討した目的
- Storageの肥大化を防ぐ
- 一定期間(例:7日・30日)を過ぎた画像を自動削除
- 運用コストの削減
- 人手を介さないクリーンアップ
試行錯誤の記録
1. Supabase Edge Functionでの削除(第一案)
最初に考えたのは、Edge Functionから直接Storage APIを操作する方法でした。
// 実際に書いたコード(抜粋)
const { data: files } = await supabase.storage
.from('shipment-images')
.list('shipments', { limit: 5000 });
const oldFiles = files.filter((file) => {
const lastModified = new Date(file.updated_at).getTime();
return lastModified < threshold;
});
await supabase.storage.from('shipment-images').remove(paths);
手応え: 削除処理自体は動いた。Testボタンで実行すると、確かに古い画像が消える。
しかし壁にぶつかった:
- ❌ 定期実行の仕組みが複雑
「毎日自動で実行」をどう実現するか - ❌ service_role_keyの安全な管理
PostgreSQLのcronから呼び出す際、認証キーをどう渡すか - ❌ 誤削除のリスク
テスト中に本番データを消してしまう可能性
2. pg_cronでのスケジュール実行(第二案)
「Supabaseの中で完結できないか」と考え、PostgreSQLのpg_cron拡張を使う方法を試しました。
-- 試したSQL
SELECT cron.schedule(
'cleanup-images',
'0 18 * * *',
$$
SELECT net.http_post(
url := 'https://xxx.supabase.co/functions/v1/cleanup-old-shipment-images',
headers := ... -- ここで詰まった
);
$$
);
何が起きたか:
pg_net拡張がない → 有効化service_role_keyをどう渡す? → SQLに直書きは危険current_setting('app.settings.service_role_key')→ そもそも存在しない設定- Supabase Vaultを使う? → 実装が複雑化
- エラーログを何度も確認 → 毎日失敗し続ける
実行ログの一部:
ERROR: unrecognized configuration parameter "app.settings.service_role_key"
ERROR: schema "net" does not exist
ERROR: function extensions.http_post(...) does not exist
3. Supabase CLIでのスケジュール設定(第三案)
公式ドキュメントにsupabase functions scheduleというコマンドがあることを知り、試しました。
supabase functions schedule create cleanup-images \
--schedule "0 18 * * *" \
--function cleanup-old-shipment-images
結果:
unknown flag: --schedule
CLIのバージョンを最新(v2.65.5)にアップグレードしても、scheduleコマンドは存在しませんでした。
調査結果:
- 公式ドキュメントに記載はあるが、実装されていない可能性
- または一部のユーザーにのみ提供されているベータ機能
4. ローカルスクリプトでの手動実行(妥協案)
一時的に、ローカルでスクリプトを実行する方法も考えました。
# 定期的に手動実行
node scripts/cleanup-storage.js
却下した理由:
- ❌ 実行忘れのリスク(人的ミス)
- ❌ 再現性が低い(PCが起動していないと実行されない)
- ❌ バックアップの思想(自動化・安全性)と合わない
最終的な判断:「後回し」にした理由
ここで一度立ち止まって、優先順位を整理しました。
現実的な判断:
- Storage容量は今すぐ逼迫していない
月額コストへの影響は軽微 - 自動削除の実装が想定以上に複雑
認証・スケジューリング・エラーハンドリングなど - 手動削除でも対応可能
必要に応じてSupabase Dashboardから削除できる
時間対効果を考えた結果:
- 今は「確実に動くバックアップ」に注力
- Storage削除は、容量が問題になった時点で再検討
