204 lines
10 KiB
Markdown
204 lines
10 KiB
Markdown
---
|
|
name: pocketbase-deployment
|
|
description: "PocketBase als Backend auf Linux-VPS deployen: Installation, systemd, Collection-Schema anlegen (0.25 Quirks!), Filter-Syntax, API-Integration mit Flask. Vermeidet die typischen 400/404-Fehler."
|
|
version: 1.0.0
|
|
author: mehlzoerwer
|
|
platforms: [linux]
|
|
---
|
|
|
|
# PocketBase Deployment (Linux VPS)
|
|
|
|
PocketBase als leichtgewichtiges Backend für Web-Projekte (PersoEnt, Weltklasse-OS). Getestet mit v0.25.0 auf Ubuntu 24.04.
|
|
|
|
## Installation
|
|
|
|
```bash
|
|
cd /tmp && curl -sL https://github.com/pocketbase/pocketbase/releases/download/v0.25.0/pocketbase_0.25.0_linux_amd64.zip -o pb.zip
|
|
unzip -o pb.zip -d /opt/pocketbase
|
|
/opt/pocketbase/pocketbase --version
|
|
```
|
|
|
|
**systemd** (`/etc/systemd/system/pocketbase.service`):
|
|
```ini
|
|
[Unit]
|
|
Description=PocketBase
|
|
After=network.target
|
|
|
|
[Service]
|
|
User=www-data
|
|
Group=www-data
|
|
WorkingDirectory=/opt/pocketbase
|
|
ExecStart=/opt/pocketbase/pocketbase serve --http=127.0.0.1:8090
|
|
Restart=always
|
|
RestartSec=5
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
```
|
|
```bash
|
|
mkdir -p /opt/pocketbase/pb_data && chown -R www-data:www-data /opt/pocketbase
|
|
systemctl daemon-reload && systemctl enable pocketbase && systemctl start pocketbase
|
|
curl -s http://127.0.0.1:8090/api/health # {"message":"API is healthy."}
|
|
```
|
|
|
|
## Superuser + Auth-Token
|
|
|
|
```bash
|
|
# Superuser via CLI erstellen (API-Endpoint /_superusers existert NICHT zum Anlegen!)
|
|
/opt/pocketbase/pocketbase superuser upsert admin@projekt.local P4ssw0rd!
|
|
|
|
# Token holen:
|
|
TOKEN=$(curl -s -X POST http://127.0.0.1:8090/api/collections/_superusers/auth-with-password \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{"identity":"admin@projekt.local","password":"P4ssw0rd!"}' | python3 -c "import sys,json; print(json.load(sys.stdin)['token'])")
|
|
```
|
|
|
|
⚠️ **Token läuft lange** (exp ~1 Jahr bei `superuser upsert`), trotzdem bei API-Code hartcodieren nur für Prototypen. Produktion: OAuth/Middleware.
|
|
|
|
## 🔥 Collection-Schema anlegen (0.25 QUIRKS!)
|
|
|
|
**POST `/api/collections` mit `schema`-Array IGNORIERT das Schema komplett!** Die Collection wird OHNE Felder erstellt (nur `id`). Das führt zu 400 bei jedem Filter.
|
|
|
|
**Fix — Schema NACHTRÄGLICH via PATCH mit `id`-Feldern:**
|
|
```bash
|
|
curl -s -X PATCH http://127.0.0.1:8090/api/collections/health_records \
|
|
-H "Authorization: $TOKEN" \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{
|
|
"fields": [
|
|
{"name":"weight","type":"number","required":true,"min":0,"max":500,"id":"w1","presentable":false,"system":false},
|
|
{"name":"blood_pressure","type":"text","required":true,"min":3,"max":10,"id":"bp1","presentable":false,"system":false},
|
|
{"name":"pulse","type":"number","required":true,"min":0,"max":300,"id":"p1","presentable":false,"system":false},
|
|
{"name":"user_id","type":"text","required":true,"min":1,"max":100,"id":"u1","presentable":false,"system":false}
|
|
]
|
|
}'
|
|
```
|
|
→ Antwort zeigt `FIELDS: ['id', 'weight', 'blood_pressure', 'pulse', 'user_id']` ✅
|
|
|
|
**Jedes Feld BRAUCHT eine `id`!** Ohne `id` wird es abgelehnt.
|
|
|
|
## 🔥 Filter-Syntax (0.25)
|
|
|
|
| Falsch | Richtig | Warum |
|
|
|--------|---------|-------|
|
|
| `filter=user_id='x'` | `filter=user_id = "x"` | Leerzeichen + Doppelte Anführungszeichen |
|
|
| `sort=-created` | `sort=-id` | `created` ist System-Feld → 400 bei Sort. **ABER `sort=-id` funktioniert (200):** PocketBase-IDs sind chronologisch aufsteigend, `-id` = neueste zuerst. VERIFIZIERT (2026-08-06): `sort=created` und `sort=-created` geben 400, `sort=-id` und `sort=weight` geben 200. Für "neueste zuerst" immer `sort=-id` nutzen. |
|
|
|
|
**Beispiel (funktioniert):**
|
|
```
|
|
GET /api/collections/health_records/records?filter=user_id%20%3D%20%22x%22&page=1&perPage=10
|
|
```
|
|
|
|
**Flask-Integration (Backend):**
|
|
```python
|
|
import requests
|
|
POCKETBASE_URL = "http://127.0.0.1:8090"
|
|
PB_TOKEN = "<SUPERUSER_TOKEN>" # für Filter-Abfragen nötig (viewRule leer reicht nicht!)
|
|
|
|
# WRITE (POST):
|
|
r = requests.post(f"{POCKETBASE_URL}/api/collections/health_records/records",
|
|
json={"weight":72,"blood_pressure":"120/80","pulse":68,"user_id":"x"})
|
|
|
|
# READ (GET mit Filter):
|
|
r = requests.get(f"{POCKETBASE_URL}/api/collections/health_records/records",
|
|
params={"filter": 'user_id = "x"', "page":1, "perPage":10},
|
|
headers={"Authorization": PB_TOKEN})
|
|
```
|
|
|
|
⚠️ **`viewRule: ""` reicht NICHT für Filter-Queries** — PocketBase verlangt Auth-Header bei `filter=`. Admin-Token im Backend-Code löst das.
|
|
|
|
## 🔥 Write-Schutz: leerer `createRule`/`updateRule` blockt User-Writes (400)
|
|
|
|
**Symptom (2026-08-15):** PersoEnt-User mit `users/auth-with-password`-Token kann **lesen**, aber `POST /api/collections/health_records/records` → **`400 Failed to create record`**, selbst mit vollständigem Record, der exakt einem bestehenden entspricht.
|
|
|
|
**Ursache:** Die Collection hat **leere `createRule` UND `updateRule`** (= nur Superuser darf schreiben; normaler User darf nur bei gesetzter Regel). Das ist **gutes Sicherheits-Design**, aber man stolpert darüber, weil der User-Read funktioniert.
|
|
|
|
**Diagnose (definitiv, ohne Ratenein):**
|
|
```bash
|
|
# Collection-Regeln direkt aus der SQLite-DB lesen:
|
|
python3 -c "
|
|
import sqlite3
|
|
c=sqlite3.connect('/opt/pocketbase/pb_data/data.db')
|
|
for r in c.execute(\"SELECT name,createRule,updateRule FROM _collections WHERE name='health_records'\"):
|
|
print('create:', repr(r[1]), '| update:', repr(r[2]))
|
|
"
|
|
# leere Regel = '' oder NULL → nur Superuser darf schreiben
|
|
```
|
|
|
|
**Fix (Hauptweg):** Für Server-Integration mit **Superuser-Token** schreiben (nicht User-Token). Siehe `## Superuser + Auth-Token` oben — `pocketbase superuser upsert ...` anlegen + `collections/_superusers/auth-with-password`-Token verwenden. Behalte das restriktive Design bei (kein offener User-Write), statt die Regel zu lockern — sensible Gesundheitsdaten.
|
|
|
|
**Fallback (Option B, verifiziert 2026-08-15):** Wenn **KEIN Superuser-Passwort existiert** (Nutzer hat PB nicht aufgesetzt / kennt das Admin-PW nicht), kann der **Server-Integration-Poller direkt in die SQLite-DB schreiben** (root, läuft auf dem VPS):
|
|
```python
|
|
# direkter INSERT in /opt/pocketbase/pb_data/data.db — umgeht API-createRule komplett
|
|
import sqlite3, shutil, datetime, os
|
|
PB_DB = "/opt/pocketbase/pb_data/data.db"
|
|
os.makedirs("/opt/pocketbase/pb_data/backups", exist_ok=True)
|
|
# Rolling-Backup (max 3) VOR erstem Write — Backup-Regel
|
|
backups = sorted(b for b in os.listdir("/opt/pocketbase/pb_data/backups") if b.endswith(".db"))
|
|
if len(backups) >= 3: os.remove(os.path.join("/opt/pocketbase/pb_data/backups", backups[0]))
|
|
shutil.copy2(PB_DB, f"/opt/pocketbase/pb_data/backups/data_{datetime.datetime.now():%Y%m%d_%H%M%S}.db")
|
|
con = sqlite3.connect(PB_DB); cur = con.cursor()
|
|
cols = [r[1] for r in cur.execute("PRAGMA table_info('health_records')")] # vorhandene Spalten pruefen
|
|
vals = {"steps":981,"stress":22,"pulse":51,"sleep_hours":7.77,"user_id":"thorsten"}
|
|
sub = [c for c in vals if c in cols] # NUR vorhandene Spalten senden
|
|
cur.execute(f"INSERT INTO health_records ({','.join(sub)}) VALUES ({','.join('?'*len(sub))})",
|
|
[vals[c] for c in sub]); con.commit(); con.close()
|
|
```
|
|
**PocketBase-Leseseite:** direkte Writes sind via API lesbar (`sort=-id`), weil PB die SQLite-DB direkt liest — aber PB-Validierung/Trigger werden umgangen. **Nur nutzen für** einfache, single-User-Inert-Tables (Cron-Poller auf `health_records`); **nicht** für Beziehungen/Migrationen/parallele PB-Writes. Vor dem ersten Write DOM-typ trotzdem: Collection-Spalten via `PRAGMA table_info` holen (kein `datum`/`source`-Feld annehmen — es kann fehlen). NICHTS erfinden, echte COL-Check.
|
|
|
|
## 🔥 Schema-Cache-Gotcha: per ALTER TABLE ergänzte Spalte wird NICHT von der API geliefert
|
|
|
|
**Symptom (2026-08-15):** Du fügst per direktem `ALTER TABLE health_records ADD COLUMN datum TEXT` eine neue Spalte hinzu und schreibst sie per Direkt-Write — aber die API liefert `datum` nicht (Record-Keys ohne das Feld), obwohl es in der SQLite-Tabelle existiert.
|
|
|
|
**Ursache:** PocketBase hält sein Collection-Schema **in `_collections.fields` (separate JSON) + im RAM-Cache**. Ein rohes `ALTER TABLE` ändert die Tabelle, aber **nicht das PB-Schema** → API white-/blacklistet das Feld nicht.
|
|
|
|
**Fix (verifiziert 2026-08-15):** Feld NACHträglich in PB-Schema eintragen UND Service neu laden:
|
|
```bash
|
|
# 1. Backup (Backup-Regel) — cp data.db data.db.bak-<ts>
|
|
# 2. Feld in _collections.fields (JSON) eintragen:
|
|
python3 -c "
|
|
import sqlite3, json
|
|
c=sqlite3.connect('/opt/pocketbase/pb_data/data.db')
|
|
row=c.execute(\"SELECT fields FROM _collections WHERE name='health_records'\").fetchone()
|
|
f=json.loads(row[0])
|
|
if 'datum' not in [x.get('name') for x in f]:
|
|
f.append({'name':'datum','type':'text','required':False,'system':False,'max':10,'min':0,'pattern':'','autogeneratePattern':'','primaryKey':False})
|
|
c.execute(\"UPDATE _collections SET fields=? WHERE name='health_records'\",(json.dumps(f),)); c.commit()
|
|
# 3. PocketBase-Service neu laden (RAM-Schema-Cache!):
|
|
"
|
|
systemctl restart pocketbase # ODER systemctl restart pocketbase.service
|
|
# Verifikation: API-Response-keys enthalten jetzt das Feld
|
|
```
|
|
**Ohne den `systemctl restart pocketbase` liefert die API das neue Feld NICHT** — der RAM-Cache nimmt Schema-Änderungen erst nach Neustart an. Danach: `GET .../records?filter=...&sort=-id` → `datum` taucht in Record-Keys auf.
|
|
|
|
## ⌚ Wearable-/Geräte-Daten einspeisen (COROS-MCP-Beispiel)
|
|
|
|
## Flask + Nginx Reverse-Proxy
|
|
|
|
Backend unter `/var/www/<projekt>/backend/app.py` (systemd `persoent-api`, siehe `references/flask-deploy.md`). Nginx:
|
|
```nginx
|
|
location /api/ {
|
|
proxy_pass http://127.0.0.1:5000/api/;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
}
|
|
```
|
|
|
|
## Verification
|
|
|
|
```bash
|
|
# Write test:
|
|
curl -s -X POST https://<domain>/api/health/record -H 'Content-Type: application/json' \
|
|
-d '{"weight":72,"blood_pressure":"120/80","pulse":68,"user_id":"test"}'
|
|
# → {"id":"...","message":"Health record saved successfully"}
|
|
|
|
# Read test:
|
|
curl -s "https://<domain>/api/health/records?user_id=test"
|
|
# → {"records":[...],"total":1}
|
|
```
|
|
|
|
## Referenzen
|
|
- `references/flask-deploy.md` — Flask-App als systemd-Service + Nginx Proxy (PersoEnt-Pattern)
|
|
- `references/pocketbase-quirks-19-07-2026.md` — Session-Transcript der 400-Fehler + Fixes
|
|
- `references/coros-mcp-wearable-integration.md` — COROS-Sportuhr-Daten via offiziellem `coros-mcp` CLI in health_records (Login-Flow, korrekte `--tool`/`--arguments-json`-Syntax, Text-Block-Parsing) für Wearable-/Geräte-Einspeisung
|