Firebaseを捨てて月額5ドルのセルフホスティングへ移行するための技術的ステップ
TuBrief 편집팀
2026년 7월 14일
0
Computing/Software원본 영상을 바탕으로 AI의 도움을 받아 작성했습니다. 원본 영상이 기준입니다.
커뮤니티의 다른 글
댓글 (0)
Log in to leave a comment
아직 작성된 글이 없습니다
원본 영상을 바탕으로 AI의 도움을 받아 작성했습니다. 원본 영상이 기준입니다.
Log in to leave a comment
아직 작성된 글이 없습니다
Firebaseは最初は魅力的です。インフラを気にせず機能の実装だけに集中できるからです。しかし、サービスが少しでも成長すると、請求書の数字が膨れ上がり始めます。かといって、セルフホスティングへの移行は不安がつきまといます。「データが消えたらどうしよう?」「デプロイするたびにサービスが止まったらどうしよう?」
そうした不安はもっともです。しかし、解決策はあります。FirestoreのデータをSQLiteベースのオールインワンバックエンドである「PocketBase」へ安全に移行し、GitHub ActionsとNginxを使用して無停止デプロイ環境を構築する具体的な方法をまとめました。
Firestoreの非定型データをリレーショナルモデルベースのPocketBaseスキーマへ移行する際に最初にぶつかる壁は、識別子の長さです。Firestoreは20文字のドキュメントIDを使用しますが、PocketBaseは基本的に15文字の英数字という制約があります。この長さを無視してそのまま投入すると、validation_length_invalidエラーが発生します。
この問題を回避するには、一意性を維持しつつ長さを短縮する必要があります。最も確実な方法は、Firestore IDをSHA256でハッシュ化した後、先頭の15文字を切り取って使用することです。衝突については心配無用です。大規模なデータセットであっても、15文字のハッシュ値が重複する確率は無視できるほど低いからです。
これを処理するNode.js移行スクリプトの構造は以下の通りです。firebase-adminとaxiosパッケージを使用し、データを100件ずつ取得する方式です。
`javascript
// migration.js
const admin = require('firebase-admin');
const axios = require('axios');
const crypto = require('crypto');
admin.initializeApp({
credential: admin.credential.applicationDefault()
});
const db = admin.firestore();
const PB_URL = 'http://127.0.0.1:8090/api/collections/posts/records';
async function migrate() {
let lastDoc = null;
let hasMore = true;
while (hasMore) {
let query = db.collection('posts').orderBy('name').limit(100);
if (lastDoc) {
query = query.startAfter(lastDoc);
}
const snapshot = await query.get();
if (snapshot.empty) {
hasMore = false;
break;
}
for (const doc of snapshot.docs) {
const data = doc.data();
// 20文字のFirestore IDを15文字の英数字に変換
const newId = crypto.createHash('sha256').update(doc.id).digest('hex').substring(0, 15);
try {
await axios.post(PB_URL, {
id: newId,
firestore_id: doc.id, // 万が一の検証用に元IDを記録
title: data.title,
content: data.content
});
} catch (err) {
console.error(`移行失敗: ${doc.id}`, err.response?.data);
}
}
lastDoc = snapshot.docs[snapshot.docs.length - 1];
}
}
migrate();
`
この作業を無停止で行うには、準備期間を設ける必要があります。まず、このスクリプトで既存データの90%以上をバックグラウンドで事前に移行しておきます。その後、クライアントコード側で書き込みを行う際、FirebaseとPocketBaseの両方に同時に記録する「デュアルライト(二重書き込み)」コードをデプロイします。両方のデータが一致することを確認した後に読み取りエンドポイントをPocketBaseへ切り替え、Firebaseのコードを取り除けば完了です。ユーザーから見れば、サーバーは1秒も停止しません。
PocketBaseは単一バイナリで実行される軽量なバックエンドです。しかし、Linuxサーバーでただ実行しておくだけでは、予期せぬメモリ不足(OOM)によりプロセスが終了した際、サービスを復旧させる手段がありません。
systemdを設定し、プロセスが停止しても自動的に再起動するようにする必要があります。/etc/systemd/system/pocketbase.serviceファイルを作成し、以下の内容を登録します。
`ini
[Unit]
Description=PocketBase Service
After=network.target
[Service]
Type=simple
User=pocketbase
Group=pocketbase
LimitNOFILE=65535
ExecStart=/opt/pocketbase/pocketbase serve --http="127.0.0.1:8090"
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
`
ここでLimitNOFILE=65535の設定が重要です。PocketBaseの強みであるリアルタイムサブスクリプション(WebSocket)機能を使う際、同時接続が集中してもLinuxのデフォルトのファイルディスクリプタ制限に引っかかって接続が切れる現象を防ぎます。
フロントエンドにはNginxを配置してSSL証明書とプロキシを処理します。/etc/nginx/sites-available/pocketbase設定には、リアルタイムストリーム接続が遅延しないようにオプションを追加する必要があります。
`nginx
upstream pocketbase {
server 127.0.0.1:8090;
}
server {
server_name api.yourdomain.com;
location / {
proxy_pass http://pocketbase;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocketおよびSSEリアルタイムストリーミングのサポート
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_buffering off;
}
}
`
あとはcertbot --nginxコマンドでHTTPS証明書を設定すれば、基本的なインフラ構成は完了です。
経験上、RAM 512MBの最も安価なVPSクラスでも、SQLiteの設定を少し調整すれば毎秒1,000件以上の軽量なリクエストを容易に処理できます。PocketBaseでSQLiteのパフォーマンスを最大化するには、WAL(Write-Ahead Log)モードを有効にする必要があります。PocketBaseはデフォルトでWALモードを有効にしますが、直接SQLiteを扱う場合はPRAGMA journal_mode=WAL;とPRAGMA synchronous=NORMAL;の設定を確認してください。ディスクへの書き込み待ちによるボトルネックがほとんど解消されます。
インディーハッカーが最も犯しやすいミスは、稼働中のサーバー上でSQLiteデータベースファイル(data.db)をそのままコピーしてバックアップすることです。この方式は、バックアップ中にデータが破損するリスクが高いです。
そこで「Litestream」を使用します。これはSQLiteのWALフレームをリアルタイムでキャプチャし、変更された部分のみをクラウドストレージへ送信するツールです。バックアップ先にはCloudflare R2を推奨します。アウトバウンド転送料金が無料なので、バックアップを頻繁に行ってもコストがほとんどかからないためです。
/etc/litestream.yml設定ファイルを以下のように作成します。
`yaml
dbs:
`
このように設定しておけば、元のDBファイルが消えても、以下のコマンド1行で直前1秒以内の状態までそのまま復元できます。
`bash
litestream restore -if-replica-exists /opt/pocketbase/pb_data/data.db
`
もし開発中に誤ってデータを消してしまい、特定の時点に復旧させたい場合は、タイムスタンプを指定して復元することも可能です。
`bash
sudo systemctl stop pocketbase.service
litestream restore -timestamp "2026-07-14T15:00:00Z" -o /tmp/recovered.db /opt/pocketbase/pb_data/data.db
sqlite3 /tmp/recovered.db "PRAGMA integrity_check;"
mv /tmp/recovered.db /opt/pocketbase/pb_data/data.db
chown -R pocketbase:pocketbase /opt/pocketbase/pb_data/
sudo systemctl start pocketbase.service
`
Cloudflare R2は毎月10GBの無料ストレージ容量を提供しています。さらに書き込みリクエスト100万回、読み取りリクエスト1,000万回まで無料枠があるため、個人サービス規模であればバックアップ費用は実質0円になります。
Dockerを立ち上げてデプロイするのは便利ですが、RAM 512MB〜1GBの小型VPSでは、Dockerデーモン自体が消費するメモリも貴重です。DockerなしでLinuxのsystemdポートを交互に切り替える方式(ブルーグリーンデプロイ)で無停止デプロイを実現します。
1つのSQLiteファイルを複数のプロセスが読み書きできるというSQLiteの特性を利用します。PocketBaseバイナリを9011ポート(ブルー)と9012ポート(グリーン)でそれぞれ立ち上げられるよう、systemdサービステンプレートを登録しておくのです。
まず、GitHub Actionsでビルドされた軽量バイナリをサーバーへ送信するワークフローの例です。
`yaml
name: Deploy
on:
push:
branches: [ main ]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.22'
- name: Build
run: |
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o pocketbase main.go
- name: Transfer Binary to VPS
uses: appleboy/scp-action@master
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.VPS_KEY }}
source: "pocketbase"
target: "/srv/pocketbase/next_release"
`
サーバーにバイナリが到着するとデプロイスクリプトが実行されます。現在アクティブなポートが9011か9012かを確認し、休止しているポートで新しいバージョンのバイナリを起動します。
`bash
#!/bin/bash
CURRENT_PORT=$(curl -s http://127.0.0.1:8090/api/health | jq -r '.port' 2>/dev/null || echo "9011")
if [ "$CURRENT_PORT" = "9011" ]; then
TARGET_PORT="9012"
else
TARGET_PORT="9011"
fi
sudo systemctl start pocketbase@$TARGET_PORT.service
sleep 3
HEALTH_CHECK=TARGET_PORT/api/health | jq -r '.status')
if [ "HEALTH_CHECK" = "OK" ]; then
# Nginxアップストリーム設定を新ポートへ修正してリロード
echo "upstream pocketbase { server 127.0.0.1:TARGET_PORT; }" | sudo tee /etc/nginx/conf.d/upstream.conf
sudo systemctl reload nginx
# 旧バージョンプロセスを終了
sudo systemctl stop pocketbase@$CURRENT_PORT.service
echo "デプロイ完了。ポート $TARGET_PORT への切り替え成功。"
else
echo "デプロイ失敗。新バージョンのヘルスチェックが通りません。"
sudo systemctl stop pocketbase@$TARGET_PORT.service
exit 1
fi
`
この方式を使えば、高価なコンテナオーケストレーションツールがなくても無停止デプロイが完璧に動作します。デプロイ中にユーザーからリクエストが来ても、Nginxがリロードされる瞬間の(ミリ秒単位の)間だけリクエストを待機させ、その後安全に新ポートへ流してくれるからです。
セルフホスティングは最初の一回だけ手間がかかりますが、一度構築してしまえば毎月のインフラコストを心配することなく、製品開発にのみ集中できる素晴らしい土台となります。