Firebase को छोड़कर $5 प्रति माह के सेल्फ-होस्टिंग पर स्विच करने के तकनीकी चरण
Firebase शुरुआत में बहुत अच्छा लगता है। आपको बुनियादी ढांचे (infrastructure) की चिंता किए बिना केवल फीचर्स लागू करने होते हैं। लेकिन जैसे-जैसे आपका सर्विस थोड़ा भी बढ़ता है, बिल की राशि बढ़ने लगती है। फिर भी, सेल्फ-होस्टिंग पर स्विच करने का विचार ही डरावना लगता है। अगर डेटा चला गया तो? अगर हर डिप्लॉयमेंट के दौरान सर्विस रुक गई तो?
ये चिंताएं पूरी तरह से जायज हैं। लेकिन इसका समाधान है। मैंने Firestore डेटा को SQLite-आधारित ऑल-इन-वन बैकएंड, पॉकेटबेस (PocketBase) पर सुरक्षित रूप से माइग्रेट करने, और GitHub Actions व Nginx का उपयोग करके जीरो-डाउनटाइम डिप्लॉयमेंट एनवायरनमेंट बनाने के विस्तृत तरीके संकलित किए हैं।
Firestore डेटा को PocketBase में माइग्रेट करने का पाइपलाइन
जब आप Firestore के अनस्ट्रक्चर्ड डेटा को रिलेशनल मॉडल-आधारित PocketBase स्कीमा में माइग्रेट करते हैं, तो सबसे पहली बाधा आइडेंटिफ़ायर की लंबाई होती है। Firestore 20-अक्षर की डॉक्यूमेंट ID का उपयोग करता है, जबकि PocketBase डिफ़ॉल्ट रूप से 15-अक्षर की अल्फ़ान्यूमेरिक सीमा लगाता है। यदि आप इस लंबाई को अनदेखा करके डेटा डालते हैं, तो आपको validation_length_invalid एरर देखने को मिलेगा।
इस समस्या से बचने के लिए, आपको विशिष्टता (uniqueness) बनाए रखते हुए लंबाई कम करनी होगी। सबसे निश्चित तरीका Firestore ID को SHA256 के साथ हैश करना और फिर पहले 15 अक्षरों को लेना है। टकराव (collision) की चिंता करने की आवश्यकता नहीं है। बड़े डेटासेट में भी, 15-अक्षर के हैश वैल्यू के टकराने की संभावना नगण्य है।
इसे संभालने वाले Node.js माइग्रेशन स्क्रिप्ट की संरचना नीचे दी गई है। यह firebase-admin और axios पैकेज का उपयोग करके 100-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-अक्षर की अल्फ़ान्यूमेरिक ID में बदलें
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 सर्वर सेटअप
पॉकेटबेस एक हल्का बैकएंड है जो सिंगल बाइनरी के रूप में चलता है। हालांकि, यदि आप इसे लिनक्स सर्वर पर बिना किसी प्रबंधन के छोड़ देते हैं, तो अप्रत्याशित आउट-ऑफ-मेमोरी (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 सेटिंग महत्वपूर्ण है। पॉकेटबेस की रियल-टाइम सब्सक्रिप्शन (Websocket) क्षमता का उपयोग करते समय, यह एक साथ आने वाले ट्रैफ़िक के कारण लिनक्स के डिफ़ॉल्ट फ़ाइल डिस्क्रिप्टर लिमिट को पार करने और कनेक्शन कटने की समस्या को रोकता है।
फ्रंट में SSL प्रमाणपत्र और प्रॉक्सी को संभालने के लिए Nginx रखें। /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;
# वेबसॉकेट और 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) को सीधे कॉपी करके बैकअप लेते हैं। इस पद्धति में बैकअप के दौरान डेटा के भ्रष्ट (corrupt) होने का अधिक जोखिम होता है।
हम 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. पॉकेटबेस को अस्थायी रूप से रोकें
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 का मुफ्त स्टोरेज देता है। इसके अलावा, यह 1 मिलियन राइट रिक्वेस्ट और 10 मिलियन रीड रिक्वेस्ट के लिए मुफ्त क्रेडिट प्रदान करता है, इसलिए एकल-व्यक्ति सर्विस के लिए, बैकअप की लागत लगभग शून्य हो जाती है।
GitHub Actions और Nginx का उपयोग करके जीरो-डाउनटाइम डिप्लॉयमेंट
डॉकर (Docker) के साथ डिप्लॉय करना सुविधाजनक है, लेकिन 512MB या 1GB RAM वाले छोटे VPS पर, डॉकर डेमन द्वारा ली जाने वाली मेमोरी भी बहुत बड़ी लगती है। आइए बिना डॉकर के, Linux systemd के पोर्ट्स को वैकल्पिक रूप से बदलकर (ब्लू-ग्रीन डिप्लॉयमेंट) जीरो-डाउनटाइम डिप्लॉयमेंट लागू करें।
हम SQLite की इस विशेषता का उपयोग करते हैं कि कई प्रक्रियाएं एक ही SQLite फ़ाइल को पढ़ और लिख सकती हैं। हम systemd सर्विस टेम्पलेट्स को पंजीकृत करते हैं ताकि पॉकेटबेस बाइनरी को क्रमशः 9011 पोर्ट (ब्लू) और 9012 पोर्ट (ग्रीन) पर चलाया जा सके।
सबसे पहले, 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: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 रिलोड होने के उस क्षण (मिलीसेकंड) के दौरान अनुरोधों को प्रतीक्षा पर रखता है और फिर सुरक्षित रूप से नए पोर्ट पर भेज देता है।
सेल्फ-होस्टिंग में शुरुआत में एक बार सेटअप करने में मेहनत लगती है, लेकिन एक बार कॉन्फ़िगरेशन पूरा हो जाने के बाद, यह एक शानदार आधार बनता है जहाँ आप हर महीने बिलों की चिंता किए बिना केवल अपने उत्पाद पर ध्यान केंद्रित कर सकते हैं।