TuBrief
구독 채널
비디오
커뮤니티

Firebaseを捨てて月額5ドルのセルフホスティングへ移行するための技術的ステップ

TuBrief 편집팀
2026년 7월 14일
0
Computing/Software

원본 영상을 바탕으로 AI의 도움을 받아 작성했습니다. 원본 영상이 기준입니다.

日本語한국어EnglishEspañol中文العربيةहिन्दीDeutschFrançaisPortuguêsРусскийBahasa Indonesia

관련 영상

Firebaseの無料代替ツール、たった1つのファイルで完結する驚きの選択肢7:49

Firebaseの無料代替ツール、たった1つのファイルで完結する驚きの選択肢

Better Stack

커뮤니티의 다른 글

사내 시스템에 llm api 붙일 때 마주하는 현실적인 한계와 대응법

2026년 9월 13일

레거시 백엔드에 GPT-6 Astra 붙일 때 예산 승인과 보안 통과를 먼저 끝내는 법이 있습니다

2026년 9월 13일

에이전트끼리 대화하다 6천만 원 청구서가 나오는 이유

2026년 9월 13일

사내 RAG 벡터 검색에 Okta 권한 필터를 직접 거는 방법

2026년 9월 13일

브라우저 에이전트에게 내 구글 계정을 통째로 넘기면 안 되는 이유

2026년 9월 12일

Apple Won the AI Race

2026년 9월 12일

댓글 (0)

Log in to leave a comment

아직 작성된 글이 없습니다

© 2026 . All rights reserved.

TuBrief
구독 채널
비디오
커뮤니티
로그인

Firebaseを捨てて月額5ドルのセルフホスティングへ移行するための技術的ステップ

Firebaseは最初は魅力的です。インフラを気にせず機能の実装だけに集中できるからです。しかし、サービスが少しでも成長すると、請求書の数字が膨れ上がり始めます。かといって、セルフホスティングへの移行は不安がつきまといます。「データが消えたらどうしよう?」「デプロイするたびにサービスが止まったらどうしよう?」

そうした不安はもっともです。しかし、解決策はあります。FirestoreのデータをSQLiteベースのオールインワンバックエンドである「PocketBase」へ安全に移行し、GitHub ActionsとNginxを使用して無停止デプロイ環境を構築する具体的な方法をまとめました。


FirestoreデータをPocketBaseへ移行するパイプライン

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秒も停止しません。


24時間停止しないPocketBaseサーバーの設定

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;の設定を確認してください。ディスクへの書き込み待ちによるボトルネックがほとんど解消されます。


Cloudflare R2とLitestreamによるリアルタイムバックアップ

インディーハッカーが最も犯しやすいミスは、稼働中のサーバー上でSQLiteデータベースファイル(data.db)をそのままコピーしてバックアップすることです。この方式は、バックアップ中にデータが破損するリスクが高いです。

そこで「Litestream」を使用します。これはSQLiteのWALフレームをリアルタイムでキャプチャし、変更された部分のみをクラウドストレージへ送信するツールです。バックアップ先にはCloudflare R2を推奨します。アウトバウンド転送料金が無料なので、バックアップを頻繁に行ってもコストがほとんどかからないためです。

/etc/litestream.yml設定ファイルを以下のように作成します。

`yaml
dbs:

  • path: /opt/pocketbase/pb_data/data.db
    replicas:
    • type: s3
      bucket: your-r2-bucket-name
      endpoint: https://.r2.cloudflarestorage.com
      access-key-id:
      secret-access-key:

`

このように設定しておけば、元のDBファイルが消えても、以下のコマンド1行で直前1秒以内の状態までそのまま復元できます。

`bash
litestream restore -if-replica-exists /opt/pocketbase/pb_data/data.db

`

もし開発中に誤ってデータを消してしまい、特定の時点に復旧させたい場合は、タイムスタンプを指定して復元することも可能です。

`bash

1. PocketBaseを一時停止

sudo systemctl stop pocketbase.service

2. 特定時点へ復元するためのファイルを作成

litestream restore -timestamp "2026-07-14T15:00:00Z" -o /tmp/recovered.db /opt/pocketbase/pb_data/data.db

3. データ整合性チェック後にファイルを置き換え

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/

4. サービス再開

sudo systemctl start pocketbase.service

`

Cloudflare R2は毎月10GBの無料ストレージ容量を提供しています。さらに書き込みリクエスト100万回、読み取りリクエスト1,000万回まで無料枠があるため、個人サービス規模であればバックアップ費用は実質0円になります。


GitHub ActionsとNginxを活用した無停止デプロイ

Dockerを立ち上げてデプロイするのは便利ですが、RAM 512MB〜1GBの小型VPSでは、Dockerデーモン自体が消費するメモリも貴重です。DockerなしでLinuxのsystemdポートを交互に切り替える方式(ブルーグリーンデプロイ)で無停止デプロイを実現します。

1つのSQLiteファイルを複数のプロセスが読み書きできるというSQLiteの特性を利用します。PocketBaseバイナリを9011ポート(ブルー)と9012ポート(グリーン)でそれぞれ立ち上げられるよう、systemdサービステンプレートを登録しておくのです。

まず、GitHub Actionsでビルドされた軽量バイナリをサーバーへ送信するワークフローの例です。

`yaml

.github/workflows/deploy.yml

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

deploy_swap.sh

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

ヘルスチェック確認のため3秒待機

sleep 3
HEALTH_CHECK=(curl−shttp://127.0.0.1:(curl -s http://127.0.0.1:(curl−shttp://127.0.0.1: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がリロードされる瞬間の(ミリ秒単位の)間だけリクエストを待機させ、その後安全に新ポートへ流してくれるからです。

セルフホスティングは最初の一回だけ手間がかかりますが、一度構築してしまえば毎月のインフラコストを心配することなく、製品開発にのみ集中できる素晴らしい土台となります。