個人開発でSupabaseを使っていたら、こんなメールが届きました。
To save on cloud resources I just did a scan of all our projects and identified those which have not seen sufficient activity for more than 7 days.
Your project ... is scheduled to be paused in the near future.
差出人はSupabaseの共同創業者。内容は「7日間活動がないプロジェクトを一時停止する」という予告です。
対象は本番のデータベースでした。
この記事では、そのとき考えたことと、最終的に組んだ構成を書きます。同じように無料プランで本番を回している人の参考になれば。
まず疑ったこと:本当にkeep-aliveすべきか
最初に思ったのは「定期的にpingを打てば済む」でした。実際、そういう記事はたくさんあります。
ただ、手を動かす前に立ち止まりました。このメールが言っているのは「7日間アクセスが無かった」ということです。
本番のDBなのに、です。
ダッシュボードのObservabilityを見ると、24時間でこうでした。
API Gateway 6 requests
Database 11 requests
PostgREST 14 requests
CPU 2%
Disk IO 1%
しかもグラフの右端に固まっているスパイクは、自分がダッシュボードを開いた分です。つまり実ユーザーからのアクセスはほぼゼロ。
ここで判断が2つに割れます。
- Proプラン(月$25)に上げる。自動停止の対象外になり、日次バックアップも付く
- 無料のまま、keep-aliveとバックアップを自前で組む
有料課金しているユーザーが1人でもいれば、迷わず1です。データを守る責任があります。
自分の場合は課金ユーザーがゼロでした。守るべきデータがまだほとんど存在しない状態で月$25を払い続けるのは、判断として弱い。なので2を選びました。
ここは各自の状況で変わる部分です。 「keep-aliveすれば無料で済む」という話ではなく、「今の段階ならどちらが妥当か」を先に決めるべきだと思います。
ただし、無料プランにはバックアップが存在しません。停止より、こちらの方が深刻でした。誤ってテーブルをDROPしたら復旧手段がありません。
構成
GitHub Actionsで2本のワークフローを組みました。
- keep-alive — 週2回、DBにINSERTを打つ
-
backup — 週1回、
pg_dumpをGPGで暗号化してArtifactに保存
keep-alive:ダッシュボードを開くだけでは足りない
「毎日ログインすればいいのでは」と思いましたが、これは危ういです。判定されるのは実際のデータベースアクセスであって、ダッシュボードの閲覧は含まれない可能性が高い。
確実なのはクエリを投げることです。専用テーブルを作れば既存テーブルを触らずに済みます。
create table if not exists public.keep_alive (
id bigint generated always as identity primary key,
pinged_at timestamptz not null default now()
);
name: Supabase Keep Alive
on:
schedule:
# 毎週 日曜・水曜 の 03:00 UTC(= 12:00 JST)
# cron は必ず UTC 基準であることに注意
- cron: "0 3 * * 0,3"
workflow_dispatch:
jobs:
ping:
runs-on: ubuntu-latest
steps:
- name: Ping database
env:
DB_URL: ${{ secrets.SUPABASE_DB_URL }}
run: |
set -euo pipefail
psql "$DB_URL" -v ON_ERROR_STOP=1 -c \
"insert into public.keep_alive default values;"
# 30日より古い行は削除してテーブルが太らないようにする
psql "$DB_URL" -v ON_ERROR_STOP=1 -c \
"delete from public.keep_alive where pinged_at < now() - interval '30 days';"
週2回にした理由
週1回だと閾値ぎりぎりです。実行がずれたり、一度失敗したりすると7日を超えます。日曜と水曜に分けておけば、片方が落ちても残りで間に合います。
接続文字列は Session pooler を使う
Supabaseの接続文字列には種類があります。GitHub ActionsのランナーはIPv4しか持たないため、IPv6前提の Direct connection では接続できません。
Session pooler(ポート5432) を選んでください。ホスト名に pooler.supabase.com が含まれます。Transaction pooler(6543)でも動くことは多いですが、prepared statementを使う処理で問題が出ることがあります。
GitHub Actionsは60日で止まる
見落としがちですが、リポジトリに60日間まったく動きがないと、スケジュール実行は自動的に無効化されます。
活発に更新しているうちは問題ありませんが、放置が続きそうなときは頭の片隅に置いてください。keep-aliveが止まれば、その先でDBも止まります。
backup:ここが本題
pg_dumpのバージョン不一致
最初の実行はこれで落ちました。
pg_dump: error: aborting because of server version mismatch
pg_dump: detail: server version: 17.6; pg_dump version: 16.15 (Ubuntu 16.15-1.pgdg24.04+2)
pg_dump はサーバーより古いバージョンだと実行を拒否します。Ubuntuランナーに標準で入っているのは16系なので、PGDGから17を入れる必要があります。
さらに落とし穴があって、インストールしてもPATHは古い方を指したままです。
- name: Install PostgreSQL client
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get install -y postgresql-common
sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y
sudo apt-get install -y postgresql-client-17
これだけだと /usr/bin/pg_dump が16を指しています。フルパスで呼んでください。
/usr/lib/postgresql/17/bin/pg_dump "$DB_URL" ...
サーバーのバージョンは Settings → Infrastructure で確認できます。
公開リポジトリのArtifactは誰でもダウンロードできる
これが一番危ないポイントです。
GitHubの公開リポジトリでは、Actionsが生成したArtifactを認証なしで誰でも取得できます。
つまりDBのダンプをそのまま置くと、ユーザーの個人情報を公開することになります。自分の場合、面接の回答内容という極めて個人的なデータを扱っているので、これは致命的でした。
対処として、GPGで対称暗号化してからアップロードしています。
- name: Dump database
env:
DB_URL: ${{ secrets.SUPABASE_DB_URL }}
run: |
set -euo pipefail
STAMP=$(date -u +"%Y%m%d")
echo "STAMP=$STAMP" >> "$GITHUB_ENV"
/usr/lib/postgresql/17/bin/pg_dump "$DB_URL" \
--no-owner \
--no-privileges \
--clean \
--if-exists \
--schema=public \
--schema=auth \
-f "backup_${STAMP}.sql"
- name: Encrypt dump
env:
PASSPHRASE: ${{ secrets.BACKUP_PASSPHRASE }}
run: |
set -euo pipefail
gpg --batch --yes \
--passphrase "$PASSPHRASE" \
--symmetric --cipher-algo AES256 \
-o "backup_${STAMP}.sql.gpg" \
"backup_${STAMP}.sql"
# 平文は必ず削除してから Artifact 化する
rm -f "backup_${STAMP}.sql"
- name: Upload encrypted backup
uses: actions/upload-artifact@v4
with:
name: backup-${{ env.STAMP }}
path: "*.sql.gpg"
retention-days: 90
if-no-files-found: error
rm -f で平文を消してからアップロードしている点が重要です。パスに *.sql.gpg を指定していますが、うっかり *.sql を含めると台無しになります。
パスフレーズを紛失すると復号できません。パスワードマネージャーなどに必ず別途保管してください。
--schema=auth を忘れない
最初は --schema=public だけで取っていました。復号して中身を見たら、こんな構成でした。
CREATE TABLE public.interview_history (
CREATE TABLE public.keep_alive (
CREATE TABLE public.user_usage (
COPY public.interview_history (...) FROM stdin;
一見問題なさそうですが、ユーザーのアカウント情報が入っていません。Supabaseの認証情報は auth スキーマに格納されるためです。
これでは復旧しても、誰もログインできないDBが出来上がります。
権限エラーを心配しましたが、実際には --schema=auth を追加しても問題なくダンプできました。
復号の確認までがバックアップ
取れているだけでは意味がありません。戻せることを確認して初めて成立します。
gpg --batch --yes --output restore.sql --decrypt backup_20260901.sql.gpg
ここでも落とし穴がありました。PowerShellで > を使うとこうなります。
gpg --decrypt backup.sql.gpg > restore.sql # 日本語が文字化けする
PowerShellのリダイレクトはShift-JISで書き出すため、UTF-8の日本語が壊れます。--output でgpgに直接書かせれば回避できます。
復号したファイルは確認後に必ず削除してください。特にリポジトリのフォルダ内に置いたまま git add . すると、公開リポジトリに個人情報が流出します。 ダウンロードフォルダなど、リポジトリの外で作業するのが安全です。
まとめ
| 項目 | 落とし穴 |
|---|---|
| 接続文字列 | GitHub ActionsはIPv4のみ。Session poolerを使う |
| cron | UTC基準。週1回だと閾値ぎりぎり |
| pg_dump | サーバーより古いと拒否される。PATHも古い方を向いている |
| Artifact | 公開リポジトリでは誰でもDL可能。暗号化必須 |
| スキーマ |
auth を含めないとアカウント情報が抜ける |
| 復号確認 | PowerShellの > は文字化けする。--output を使う |
| Actions | 60日間リポジトリに動きがないとスケジュールが無効化される |
そしてもう一度書いておきたいのは、この構成は「無料で済ませる裏技」ではないということです。
課金ユーザーがいるなら、月$25を払ってProにする方が正しい判断です。自動停止の対象外になり、日次バックアップが付き、何より自分の実装ミスでバックアップが取れていないリスクが消えます。
自分がこの構成を選んだのは、まだ守るべきデータがほとんど無い段階だったからです。ユーザーが増えたら、迷わず切り替えるつもりです。