Supabase データベースを GitHub Actions で定期バックアップする手順(備忘録)

目次

はじめに

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のダッシュボードから接続情報を取得します。

  1. Supabaseダッシュボードにログイン
  2. 対象のプロジェクトを選択
  3. ヘッダーから Connect を開く
  4. MethodTransaction 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に保存します。

設定手順

  1. GitHubでバックアップ用のリポジトリを開く
  2. Settings タブをクリック
  3. 左メニューから Secrets and variablesActions を選択
  4. New repository secret をクリック
  5. 以下の内容で登録:
  • 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回の保存手順

  1. GitHubリポジトリの Actions タブを開く
  2. 最新のworkflow実行結果を開く
  3. Artifacts セクションから supabase-db-backup をダウンロード
  4. ZIPファイルを展開
  5. .dump ファイルを外付けSSDの該当フォルダにコピー
  6. 完了

実際に復元できるか試してみた

念のため、本当にバックアップから復元できるかを試した。
以下は、そのときに実際にやった手順の記録。


① ターミナルでバックアップファイルを配置

GitHub Actions で取得した .dump ファイルを
Mac の Documents フォルダ(自分がわかる場所)に移動。

cd ~/Documents
ls(エル・エス)

ここで supabase_YYYYMMDDTHHMMSSZ.dump が見える状態にしておく。


② PostgreSQL クライアントをインストール(初回のみ)

※ Homebrew が入っている前提

brew install postgresql@17

Supabase の PostgreSQL は 17系 のため、
pg_restore17系で揃える必要がある


③ 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_restorePostgreSQLのバックアップ復元コマンド
USERDBユーザー名(通常 postgres
PASSWORDプロジェクトのDBパスワード
HOSTxxx.pooler.supabase.com
PORT6543(Transaction Pooler)
postgres接続するDB名
–no-owner所有者情報を無視
–no-privileges権限設定を無視
.dumpGitHub 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 := ...  -- ここで詰まった
    );
  $$
);

何が起きたか:

  1. pg_net拡張がない → 有効化
  2. service_role_keyをどう渡す? → SQLに直書きは危険
  3. current_setting('app.settings.service_role_key') → そもそも存在しない設定
  4. Supabase Vaultを使う? → 実装が複雑化
  5. エラーログを何度も確認 → 毎日失敗し続ける

実行ログの一部:

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が起動していないと実行されない)
  • ❌ バックアップの思想(自動化・安全性)と合わない

最終的な判断:「後回し」にした理由

ここで一度立ち止まって、優先順位を整理しました。

現実的な判断:

  1. Storage容量は今すぐ逼迫していない
    月額コストへの影響は軽微
  2. 自動削除の実装が想定以上に複雑
    認証・スケジューリング・エラーハンドリングなど
  3. 手動削除でも対応可能
    必要に応じてSupabase Dashboardから削除できる

時間対効果を考えた結果:

  • 今は「確実に動くバックアップ」に注力
  • Storage削除は、容量が問題になった時点で再検討

目次