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

放弃 Firebase,转投月费 5 美元自托管的技术指南

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

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

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

관련 영상

这个免费且仅需单个文件的 Firebase Go 替代方案7:49

这个免费且仅需单个文件的 Firebase Go 替代方案

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% 以上的现有数据。之后,在客户端代码中部署“双写”(Dual-Write)逻辑,即在执行写入操作时同时记录到 Firebase 和 PocketBase。在确认两端数据一致后,将读取端点切换到 PocketBase,最后移除 Firebase 相关代码。对于用户而言,服务器不会有哪怕一秒的中断。


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 证书,基础架构就搭建完成了。

根据经验,即使在拥有 512MB RAM 的最便宜 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 秒之内的状态:

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 进行部署很方便,但在使用 512MB 或 1GB RAM 的小型 VPS 上,Docker 守护进程本身占用的内存也显得弥足珍贵。我们可以尝试在不使用 Docker 的情况下,通过交替切换 Linux systemd 端口的方式(蓝绿部署)来实现无停机部署。

利用 SQLite 允许多个进程同时读取和写入同一文件的特性,我们可以分别为 9011 端口(蓝)和 9012 端口(绿)注册 systemd 服务模板,从而分别运行 PocketBase 二进制文件。

首先是 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 "部署完成。端口 TARGETPORT切换成功。"elseecho"部署失败。新版本健康检查未通过。"sudosystemctlstoppocketbase@TARGET_PORT 切换成功。" else echo "部署失败。新版本健康检查未通过。" sudo systemctl stop pocketbase@TARGETP​ORT切换成功。"elseecho"部署失败。新版本健康检查未通过。"sudosystemctlstoppocketbase@TARGET_PORT.service
exit 1
fi
`

使用这种方式,无需昂贵的容器编排工具也能完美实现无停机部署。因为即使在部署过程中用户发送请求,Nginx 在重载的那一瞬间(毫秒级)也会自动等待,然后安全地将请求转发到新端口。

自托管在初始设置时稍微花点心思,但一旦配置完成,你就可以不再担心每月不断增长的基础设施费用,从而能够全身心地专注于产品本身。