Data Platform — Bootstrap Playbook¶
Full rebuild instructions for the data-platform lakehouse stack. Use when: fresh deploy, server rebuild, or pipeline reset.
Migration status: Gravitino → Lakekeeper migration is COMPLETE (see ADR-0001). All Gravitino references below have been replaced with Lakekeeper.
Architecture overview¶
graph TD
TG[Telegram Telethon] --> AD
WC[WeChat future] --> AD
FB[Facebook future] --> AD
ZL[Zalo future] --> AD
MK[mikrotik-tools
ntopng scraper] --> AD
AD["Push API :8093 (50-adapters)
Sole entry point. HTTP /api/v1/{chat,events,metrics,logs}"]
AD --> RP["Redpanda (40-streaming)
tmpfs 2GB RAM disk
Topics: chat.messages, cdc.events, platform_*"]
RP --> RW["RisingWave v3.0.0 (40-streaming)
single_node persistent mode (8GB RAM)
Sources → MVs → Iceberg Sinks"]
RW --> RFS["RustFS (unraid)
s3://iceberg/"]
RW --> LK["Lakekeeper (15-lakekeeper) :8181
Iceberg REST catalog
warehouse=homelab"]
LK -->|REST catalog| RFS
LK -.->|auth backend| FGA["OpenFGA :8082"]
OPA["OPA (16-opa) :8282
policy enforcement"] -.->|allow/deny| TR
RFS --> TR["Trino (30-query) :8091
catalog=gravitino_iceberg (legacy name)"]
TR --> SS["Superset (BI dashboards)
http://superset.tail1137c..."]
HC["dp-health-check (60-monitoring)
60s loop"] -.->|apprise tag:infra| AP["Apprise gateway"]
Stacks (6 Komodo-managed)¶
| Stack | Folder | Containers | Purpose |
|---|---|---|---|
| data-platform-catalog | 15-lakekeeper/ | dp-lakekeeper, dp-lakekeeper-migrate, dp-openfga, dp-openfga-migrate | Lakekeeper Iceberg REST catalog + OpenFGA auth |
| data-platform-opa | 16-opa/ | dp-opa | OPA policy enforcement for Trino |
| data-platform-query | 30-query/ | dp-trino | SQL query engine |
| data-platform-streaming | 40-streaming/ | redpanda, redpanda-console, dp-risingwave | Kafka (tmpfs) + streaming DB (single_node persistent) |
| data-platform-adapters | 50-adapters/ | dp-chat-adapter (Push API + Telegram listener) | Sole data entry point + Telegram ingestion |
| data-platform-monitoring | 60-monitoring/ | dp-health-check | 60s health loop, Apprise infra tag alerts |
10-catalog/is legacy — only retainsconf/gravitino.conffor reference. NOT deployed.
Prerequisites¶
- Komodo Core + Periphery running on
development(100.126.172.96) - RustFS running on
unraid(192.168.100.59:9000) with ports 9000 + 19001 published - Pigsty PostgreSQL running on
unraid(192.168.100.59:5439) - Komodo Variables set:
RUSTFS_ACCESS_KEY,RUSTFS_SECRET_KEY(existing admin)RUSTFS_ICEBERG_ACCESS_KEY,RUSTFS_ICEBERG_SECRET_KEY(set to admin key values for dev)DB_PASSWORD(pigsty superuser)LAKEKEEPER_PG_PASSWORD(lakekeeper user in pigsty — currently hardcodedlakekeeperfor dev)OPENFGA_PG_PASSWORD(openfga user in pigsty — currently hardcodedopenfgafor dev)TELEGRAM_API_ID,TELEGRAM_API_HASH(from https://my.telegram.org)WHITELIST_THREAD_IDS(Telegram group IDs to ingest; DMs always included)
Phase 1: Deploy containers (GitOps)¶
# Sync TOMLs
echo "" | km execute run-sync data-platform
# Deploy stacks (order matters — Lakekeeper + OpenFGA must be up before Trino)
echo "" | km execute deploy-stack data-platform-catalog
echo "" | km execute deploy-stack data-platform-opa
echo "" | km execute deploy-stack data-platform-streaming
echo "" | km execute deploy-stack data-platform-query
echo "" | km execute deploy-stack data-platform-adapters
echo "" | km execute deploy-stack data-platform-monitoring
# Verify all running
km container -s development | grep -E "dp-|redpanda"
# Expected: dp-lakekeeper :8181, dp-openfga :8082, dp-opa :8282,
# dp-trino :8091, redpanda :9092, redpanda-console :8085,
# dp-risingwave :4566/:5691, dp-chat-adapter :8092/:8093,
# dp-health-check (no port — host network loop)
Phase 2: Initialize pigsty databases (one-time)¶
Lakekeeper and OpenFGA each need their own database in pigsty-postgres. Schema
migration is handled by the *-migrate init containers in 15-lakekeeper/compose.yaml
(lakekeeper-migrate and openfga-migrate). You only need to create the databases
- users.
# Run via Komodo API (RunStackService on pigsty):
python3 -c "
import os, urllib.request, json
url = 'http://100.126.172.96:9120/execute'
HEADERS = {
'Content-Type': 'application/json',
'x-api-key': os.environ['KOMODO_API_KEY'],
'x-api-secret': os.environ['KOMODO_API_SECRET']
}
cmd = (
f\"PGPASSWORD='{os.environ[\"DB_PASSWORD\"]}' psql -h pigsty-postgres -U postgres -d postgres -v ON_ERROR_STOP=1 \"
f\"-c \\\"DO \\$\\$ BEGIN IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'lakekeeper') THEN CREATE ROLE lakekeeper WITH LOGIN PASSWORD 'lakekeeper'; END IF; END \\$\\$;\\\" \"
f\"-c \\\"DO \\$\\$ BEGIN IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'openfga') THEN CREATE ROLE openfga WITH LOGIN PASSWORD 'openfga'; END IF; END \\$\\$;\\\" \"
f\"-c \\\"SELECT 'CREATE DATABASE lakekeeper OWNER lakekeeper' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'lakekeeper')\\\" | grep -q CREATE && psql -h pigsty-postgres -U postgres -c 'CREATE DATABASE lakekeeper OWNER lakekeeper' || true \"
f\"-c \\\"SELECT 'CREATE DATABASE openfga OWNER openfga' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'openfga')\\\" | grep -q CREATE && psql -h pigsty-postgres -U postgres -c 'CREATE DATABASE openfga OWNER openfga' || true \"
f\"-c \\\"GRANT ALL ON SCHEMA public TO lakekeeper\\\" \"
f\"-c \\\"GRANT ALL ON SCHEMA public TO openfga\\\"\"
)
data = {'type': 'RunStackService', 'params': {'stack': 'pigsty', 'service': 'postgres', 'no_deps': True, 'command': ['sh', '-c', cmd]}}
resp = urllib.request.urlopen(urllib.request.Request(url, data=json.dumps(data).encode(), headers=HEADERS), timeout=120)
print('DB init:', json.loads(resp.read()).get('_id', {}).get('\$oid'))
"
After this, restart data-platform-catalog so the migrate init containers re-run
against the freshly created databases:
echo "" | km execute destroy-stack data-platform-catalog
echo "" | km execute deploy-stack data-platform-catalog
Verify: dp-lakekeeper logs show listening on :8181 and migrate containers exit 0.
Phase 3: Create RustFS bucket¶
docker run --rm --network reverse-proxy --entrypoint sh minio/mc:latest -c \
"mc alias set rustfs http://192.168.100.59:9000 <RUSTFS_ACCESS_KEY> <RUSTFS_SECRET_KEY> --api S3v4 && \
mc mb rustfs/iceberg 2>&1; mc ls rustfs/"
Phase 4: Bootstrap Lakekeeper warehouse + namespace¶
Lakekeeper uses a warehouse concept (not Gravitino's metalake). The Trino
catalog config declares iceberg.rest-catalog.warehouse=homelab, so Lakekeeper
must have a warehouse named homelab with a storage profile pointing at RustFS.
sleep 30 # Wait for Lakekeeper startup
# Create warehouse 'homelab' + namespace via Lakekeeper REST API (no container exec needed)
curl -fsS -X POST http://192.168.100.31:8181/management/v1/warehouse \
-H 'Content-Type: application/json' \
-d '{
"warehouse-name": "homelab",
"project-id": "00000000-0000-0000-0000-000000000000",
"storage-profile": {
"type": "s3",
"bucket": "iceberg",
"key-prefix": "homelab",
"region": "us-east-1",
"path-style-access": true,
"endpoint": "http://192.168.100.59:9000",
"access-key-id": "<RUSTFS_ICEBERG_ACCESS_KEY>",
"secret-access-key": "<RUSTFS_ICEBERG_SECRET_KEY>"
},
"storage-credential": {
"type": "s3",
"credential-type": "access-key",
"access-key-id": "<RUSTFS_ICEBERG_ACCESS_KEY>",
"secret-access-key": "<RUSTFS_ICEBERG_SECRET_KEY>"
}
}'
# Create namespace dev_streaming under warehouse homelab
curl -fsS -X POST 'http://192.168.100.31:8181/catalog/v1/homelab/namespaces' \
-H 'Content-Type: application/json' \
-d '{"namespace": ["dev_streaming"]}'
Note: the
project-idof all-zeros is Lakekeeper's default project whenLAKEKEEPER__AUTHZ_BACKEND=allowallis set (dev mode).
Phase 5: Bootstrap Redpanda topics¶
Redpanda runs on tmpfs — topics are recreated automatically via
auto_create_topics_enabled=true. This step is only needed if you want explicit partition/replica config.
timeout 30 script -qec "km exec redpanda sh" /dev/null << 'EOF'
for topic in cdc.events chat.messages platform_events platform_metrics platform_logs; do
rpk topic describe "$topic" > /dev/null 2>&1 && echo "$topic exists" || \
rpk topic create "$topic" --partitions 1 --replicas 1
done
rpk topic list
exit
EOF
tmpfs trade-off note: the 2GB RAM disk at /var/lib/redpanda/data is
in-memory only. Topics + their data are lost on container restart. Acceptable
for dev because Telegram session can replay and RisingWave re-consumes from
the earliest available offset.
Phase 6: Bootstrap RisingWave pipeline¶
RisingWave runs in single_node persistent mode — sources, MVs, and sinks
survive container restarts. You only need to run this phase ONCE per
fresh volume, not after every restart.
The init script bootstrap/risingwave-init.sql declares all sources, MVs,
and Iceberg sinks. The Iceberg sink config must point at Lakekeeper's REST
endpoint (http://192.168.100.31:8181/catalog) and use warehouse=homelab.
# Step 1: Install psql in RisingWave container
timeout 180 script -qec "km exec dp-risingwave bash" /dev/null << 'EOF'
apt-get update -qq > /dev/null 2>&1 && apt-get install -y -qq postgresql-client > /dev/null 2>&1 && echo "psql ready"
exit
EOF
# Step 2: Create all pipelines (sources + MVs + Iceberg sinks via Lakekeeper)
# Source file: stacks/data-platform/compose/bootstrap/risingwave-init.sql
# Substitute ${S3_ACCESS_KEY} / ${S3_SECRET_KEY} with Komodo Variable values,
# and ensure catalog.uri points to Lakekeeper :8181 (NOT Gravitino :8090).
timeout 120 script -qec "km exec dp-risingwave bash" /dev/null << 'OUTER'
AK=$(curl -sS http://100.126.172.96:9120/read ... ) # substitute actual value
SK=$(curl -sS http://100.126.172.96:9120/read ... ) # substitute actual value
cat > /tmp/rw_init.sql << 'INNERSQL'
# Paste contents of risingwave-init.sql here with ${S3_ACCESS_KEY}/${S3_SECRET_KEY} substituted
# AND catalog.uri=http://192.168.100.31:8181/catalog AND warehouse=homelab
INNERSQL
psql -h localhost -p 4566 -U root -d dev -v ON_ERROR_STOP=1 -f /tmp/rw_init.sql 2>&1
echo "RC=$?"
psql -h localhost -p 4566 -U root -d dev -tAc "SELECT 'sources:' || count(*) FROM rw_catalog.rw_sources"
psql -h localhost -p 4566 -U root -d dev -tAc "SELECT 'mvs:' || count(*) FROM rw_catalog.rw_materialized_views"
psql -h localhost -p 4566 -U root -d dev -tAc "SELECT 'sinks:' || count(*) FROM rw_catalog.rw_sinks"
exit
OUTER
# Expected: sources:2, mvs:3+, sinks:2
Phase 7: Telegram login (one-time)¶
Chat adapter needs authenticated Telegram session.
Option A: Web login (recommended)¶
# Switch to login mode
# Edit compose.yaml: MODE=login
# Redeploy: km execute deploy-stack data-platform-adapters
# Open: http://100.126.172.96:8092
# Enter: phone → verification code → 2FA password
# Switch back: MODE=listen
# Redeploy: km execute deploy-stack data-platform-adapters
Option B: CLI login¶
timeout 60 script -qec "km exec dp-chat-adapter bash" /dev/null << 'EOF'
python -m adapters.telegram_login
exit
EOF
Phase 8: Verify end-to-end¶
8a. Send a test message in any Telegram group¶
Open Telegram → send a message in any group you're a member of.
8b. Check adapter received it¶
8c. Check Redpanda topic¶
timeout 30 script -qec "km exec redpanda sh" /dev/null << 'EOF'
rpk topic consume chat.messages --num 1
exit
EOF
# Should show JSON message with provider=telegram
8d. Check RisingWave¶
timeout 30 script -qec "km exec dp-risingwave bash" /dev/null << 'EOF'
psql -h localhost -p 4566 -U root -d dev -tAc \
"SELECT count(*) FROM chat_messages"
exit
EOF
# Should show >= 1
8e. Check RustFS (Iceberg sink via Lakekeeper)¶
docker run --rm --network reverse-proxy --entrypoint sh minio/mc:latest -c \
"mc alias set rustfs http://192.168.100.59:9000 <KEY> <SECRET> --api S3v4 && \
mc ls rustfs/iceberg/homelab/dev_streaming/chat_messages/data/"
# Should show .parquet files
8f. Check health-check¶
km-logs dp-health-check -s development -n 5
# Should show: "OK (6 checks)" every 60s
# Checks: push-api, risingwave, redpanda-console, lakekeeper, trino, risingwave-pipeline
Post-restart recovery¶
Persistence model after Lakekeeper migration:
| Component | What's lost on restart | Recovery needed |
|---|---|---|
| Lakekeeper catalog | Nothing (pigsty-backed) | Auto-recovers |
| OpenFGA | Nothing (pigsty-backed) | Auto-recovers |
| Redpanda (tmpfs) | Topics + data (in-memory) | Topics auto-recreate; RW re-consumes from start |
| RisingWave (single_node) | Nothing (persisted to disk) | Auto-recovers all sources/MVs/sinks |
| Chat adapter | Nothing (session file persisted) | Auto-reconnects |
Recovery sequence after full server restart:
# 1. Wait for stacks to auto-start (restart: unless-stopped)
sleep 120
# 2. Verify Lakekeeper + OpenFGA migrate containers completed:
km-logs dp-lakekeeper-migrate -s development -n 20
km-logs dp-openfga-migrate -s development -n 20
# 3. Verify Trino can see Iceberg tables via Lakekeeper:
curl -X POST http://100.126.172.96:8091/v1/statement \
-H "X-Trino-User: user" \
-d "SHOW TABLES IN gravitino_iceberg.dev_streaming"
# 4. Verify RisingWave pipeline still has sources:
timeout 30 script -qec "km exec dp-risingwave bash" /dev/null << 'EOF'
psql -h localhost -p 4566 -U root -d dev -tAc \
"SELECT count(*) FROM rw_catalog.rw_sources"
exit
EOF
# 5. Verify health-check (Phase 8f)
Troubleshooting¶
See README.md § Known limitations + komodo.md lessons.