Технические шаги по отказу от 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 в PocketBase — универсальный бэкенд на базе SQLite, а также как настроить среду с бесшовным развертыванием (zero-downtime deployment) с помощью GitHub Actions и Nginx.
Главная проблема при переносе неструктурированных данных из Firestore в схему PocketBase, основанную на реляционной модели, — это длина идентификаторов. Firestore использует 20-символьные ID документов, тогда как PocketBase по умолчанию ограничивает их 15 буквенно-цифровыми символами. Если игнорировать это ограничение, вы получите ошибку validation_length_invalid.
Чтобы избежать этой проблемы, нужно сократить длину, сохранив при этом уникальность. Самый надежный способ — хешировать ID из Firestore с помощью 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-символьного ID Firestore в 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. Для пользователей сервер не будет недоступен ни на секунду.
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. Она предотвращает отключение пользователей при пиковых нагрузках, когда количество одновременных подключений достигает лимита файловых дескрипторов Linux, что важно при использовании функции подписки в реальном времени (WebSockets).
Перед PocketBase установите 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;
# Поддержка WebSockets и SSE (реальное время)
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_buffering off;
}
}
`
Теперь достаточно команды certbot --nginx, чтобы установить HTTPS-сертификат, и базовая инфраструктура готова.
По моему опыту, даже самый дешевый VPS с 512 МБ оперативной памяти легко обрабатывает более 1000 легких запросов в секунду при небольшой настройке SQLite. Чтобы максимизировать производительность SQLite при работе с PocketBase, нужно включить режим WAL (Write-Ahead Log). PocketBase по умолчанию включает WAL, но если вы работаете с SQLite напрямую, проверьте настройки PRAGMA journal_mode=WAL; и PRAGMA synchronous=NORMAL;. Это практически устраняет узкие места, когда запросы ждут записи на диск.
Самая распространенная ошибка инди-разработчиков — копирование файла базы данных SQLite (data.db) напрямую с работающего сервера. Этот метод несет высокий риск повреждения данных во время резервного копирования.
Мы используем Litestream. Это инструмент, который перехватывает WAL-фреймы SQLite в реальном времени и отправляет только изменения в облачное хранилище. В качестве хранилища для бэкапов рекомендуется Cloudflare R2. Поскольку плата за исходящий трафик отсутствует, стоимость бэкапов будет практически нулевой, сколько бы вы их ни делали.
Создайте файл конфигурации /etc/litestream.yml следующим образом:
`yaml
dbs:
`
При такой настройке, даже если оригинальный файл базы данных будет удален, вы сможете восстановить его до состояния, актуального на последнюю секунду, одной командой:
`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 предоставляет 10 ГБ бесплатного места ежемесячно. С учетом того, что также предоставляются бесплатные лимиты на 1 миллион операций записи и 10 миллионов операций чтения, для сервиса одного автора расходы на бэкапы будут стремиться к нулю.
Развертывание через Docker — это удобно, но на маленьких VPS с 512 МБ или 1 ГБ RAM жалко памяти, которую потребляет сам демон Docker. Давайте реализуем бесшовное развертывание без Docker, переключая порты службы Linux systemd (Blue-Green).
Мы воспользуемся особенностью SQLite, позволяющей нескольким процессам читать и писать в один файл базы данных. Мы создадим шаблон службы systemd, который позволит запускать бинарные файлы PocketBase на портах 9011 (Blue) и 9012 (Green) отдельно.
Сначала пример рабочего процесса (workflow) для отправки скомпилированного легкого бинарного файла на сервер через 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
# Обновление настроек upstream 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 в момент перезагрузки (измеряемый миллисекундами) автоматически придержит запрос и безопасно перенаправит его на новый порт.
Собственный хостинг требует усилий только при первой настройке. После завершения настройки он становится отличной основой, позволяющей сосредоточиться исключительно на продукте, не беспокоясь о ежемесячных расходах на инфраструктуру.