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

Firebase를 버리고 월 5달러 자체 호스팅으로 갈아타는 기술적 단계

TuBrief 편집팀
2026년 7월 14일
0
컴퓨터/소프트웨어

원본 영상을 바탕으로 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건씩 끊어서 가져오는 방식입니다.

// 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 양쪽에 동시에 기록하는 이중 기록(Dual-Write) 코드를 배포합니다. 양쪽 데이터가 일치하는 것을 확인한 뒤, 읽기 엔드포인트를 PocketBase로 바꾸고 Firebase 코드를 걷어내면 됩니다. 사용자 입장에서는 서버가 단 1초도 끊기지 않습니다.


24시간 죽지 않는 PocketBase 서버 세팅

포켓베이스는 단일 바이너리로 실행되는 가벼운 백엔드입니다. 하지만 리눅스 서버에 대충 실행만 해두면 예기치 못한 메모리 부족(OOM)으로 프로세스가 꺼졌을 때 서비스를 복구할 방법이 없습니다.

systemd를 설정해 프로세스가 죽어도 스스로 살아나게 만들어야 합니다. /etc/systemd/system/pocketbase.service 파일을 만들어 아래 내용을 등록합니다.

[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 설정이 중요합니다. 포켓베이스의 강점인 실시간 구독(웹소켓) 기능을 쓸 때, 동시 접속자가 몰리면 리눅스 기본 파일 디스크립터 한계에 걸려 접속이 끊기는 현상을 막아줍니다.

앞단에는 Nginx를 두어 SSL 인증서와 프록시를 처리합니다. /etc/nginx/sites-available/pocketbase 설정에 실시간 스트림 연결이 지연되지 않도록 옵션을 넣어야 합니다.

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;

        # 웹소켓 및 SSE 실시간 스트리밍 지원
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_buffering off;
    }
}

이제 certbot --nginx 명령어로 HTTPS 인증서만 입혀주면 기본적인 인프라 구성은 끝납니다.

경험상 512MB RAM을 가진 가장 저렴한 VPS 등급에서도 SQLite 설정을 조금만 손보면 초당 1,000건 이상의 가벼운 요청은 거뜬히 처리합니다. 포켓베이스를 구동할 때 SQLite의 성능을 극대화하려면 WAL(Write-Ahead Log) 모드를 켜야 합니다. 포켓베이스는 기본적으로 WAL 모드를 활성화하지만, 직접 SQLite를 다룰 일이 있다면 PRAGMA journal_mode=WAL;과 PRAGMA synchronous=NORMAL; 설정을 확인하세요. 디스크에 기록하느라 요청이 대기하는 병목 현상이 거의 사라집니다.


Cloudflare R2와 Litestream으로 실시간 백업하기

인디 해커들이 가장 많이 하는 실수가 작동 중인 서버에서 SQLite 데이터베이스 파일(data.db)을 그대로 복사해서 백업하는 것입니다. 이 방식은 백업 도중 데이터가 깨질 위험이 큽니다.

우리는 리트스트림(Litestream)을 사용합니다. SQLite의 WAL 프레임을 실시간으로 가로채서 변경된 부분만 클라우드 스토리지로 쏘아 보내는 도구입니다. 백업 저장소로는 Cloudflare R2를 권장합니다. 아웃바운드 전송 요금이 무료라 백업을 아무리 많이 받아도 돈이 거의 안 나오기 때문입니다.

/etc/litestream.yml 설정 파일을 아래와 같이 작성합니다.

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

이렇게 세팅해 두면 원본 db 파일이 날아가도 아래 커맨드 한 줄로 마지막 1초 이내의 상태까지 그대로 복원할 수 있습니다.

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

만약 개발 도중 실수로 데이터를 날려서 특정 시점으로 복구하고 싶다면 시간값을 지정해서 살려내는 것도 가능합니다.

# 1. 포켓베이스 일시 정지
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만 회까지 무료 크레딧을 채워주므로, 1인 서비스 규모에서는 백업 비용으로 나가는 돈이 0원에 수렴합니다.


GitHub Actions와 Nginx를 활용한 무중단 배포

도커(Docker)를 띄워 배포하면 편하지만, 512MB 혹은 1GB RAM을 쓰는 소형 VPS에서는 도커 데몬 자체가 먹는 메모리도 아쉽습니다. 도커 없이 Linux systemd의 포트를 교대로 전환하는 방식(블루-그린)으로 무중단 배포를 구현해 봅니다.

하나의 SQLite 파일을 여러 프로세스가 읽고 쓸 수 있는 SQLite의 특성을 이용합니다. 포켓베이스 바이너리를 9011 포트(블루)와 9012 포트(그린)로 각각 띄울 수 있도록 systemd 서비스 템플릿을 등록해 두는 것입니다.

우선 GitHub Actions로 빌드된 가벼운 바이너리를 서버로 전송하는 워크플로우 예시입니다.

# .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인지 확인하고, 쉬고 있는 포트에 새 버전의 바이너리를 띄웁니다.

#!/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 -s http://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가 리로드되는 찰나의 순간(밀리초 단위) 동안 알아서 요청을 대기시켰다가 새 포트로 안전하게 넘겨주기 때문입니다.

자체 호스팅은 처음 한 번 세팅할 때 손이 갈 뿐, 구성을 끝내고 나면 매달 결제되는 인프라 비용 걱정 없이 오롯이 프로덕트에만 집중할 수 있는 훌륭한 기반이 됩니다.