feat(0.2.1): M4-a 部署栈 —— compose(PG逻辑复制+powersync+app:13001), sync streams(任务域6表+User), initdb, DISABLE_CRON 开关, 健康检查端口跟随 PORT
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
# 复制为 .env (服务器上 chmod 600, 已 gitignore)。
|
||||
# POWERSYNC_JWT_KEY_B64 = base64url(POWERSYNC_JWT_SECRET 的 UTF-8 字节)
|
||||
# 生成: printf %s "$POWERSYNC_JWT_SECRET" | openssl base64 -A | tr '+/' '-_' | tr -d '='
|
||||
|
||||
PG_PASSWORD=change-me
|
||||
PS_STORAGE_PASSWORD=change-me
|
||||
JWT_SECRET_021=change-me
|
||||
POWERSYNC_JWT_SECRET=change-me
|
||||
POWERSYNC_JWT_KEY_B64=change-me
|
||||
POWERSYNC_URL=http://bh.vps.honor3.com:13002
|
||||
OTA_PUBLIC_BASE=http://bh.vps.honor3.com:13001
|
||||
@@ -0,0 +1,58 @@
|
||||
# 0.2.1 独立验证部署 (baihu /app/iboard-0.2.1)
|
||||
|
||||
与现网 `/app/iboard`(3001/宿主 MySQL) 完全隔离:本栈使用独立 PG 容器、端口 13001/13002,不写原库。
|
||||
|
||||
## 栈
|
||||
|
||||
| 服务 | 说明 |
|
||||
| --- | --- |
|
||||
| `pg` | postgres:16-alpine,`wal_level=logical`(供 PowerSync 逻辑复制),数据在 `./data/pg`,**不对外暴露端口** |
|
||||
| `app` | 本仓库 Dockerfile 构建,13001 对外;启动时先 `prisma migrate deploy`;`DISABLE_CRON=1` 关闭定时任务 |
|
||||
| `powersync` | journeyapps/powersync-service,13002 对外;源库=pg,bucket storage=同实例 PG 的 powersync schema |
|
||||
|
||||
## 首次部署
|
||||
|
||||
```bash
|
||||
# 服务器上 (源码已上传到 /app/iboard-0.2.1)
|
||||
cd /app/iboard-0.2.1/deploy/0.2.1
|
||||
|
||||
# 1. 生成 .env (参照 .env.example; POWERSYNC_JWT_KEY_B64 由 secret 派生)
|
||||
umask 077
|
||||
PG_PASSWORD=$(openssl rand -hex 16)
|
||||
PS_STORAGE_PASSWORD=$(openssl rand -hex 16)
|
||||
JWT_SECRET_021=$(openssl rand -hex 32)
|
||||
POWERSYNC_JWT_SECRET=$(openssl rand -hex 32)
|
||||
POWERSYNC_JWT_KEY_B64=$(printf %s "$POWERSYNC_JWT_SECRET" | openssl base64 -A | tr '+/' '-_' | tr -d '=')
|
||||
cat > .env <<EOF
|
||||
PG_PASSWORD=$PG_PASSWORD
|
||||
PS_STORAGE_PASSWORD=$PS_STORAGE_PASSWORD
|
||||
JWT_SECRET_021=$JWT_SECRET_021
|
||||
POWERSYNC_JWT_SECRET=$POWERSYNC_JWT_SECRET
|
||||
POWERSYNC_JWT_KEY_B64=$POWERSYNC_JWT_KEY_B64
|
||||
POWERSYNC_URL=http://bh.vps.honor3.com:13002
|
||||
OTA_PUBLIC_BASE=http://bh.vps.honor3.com:13001
|
||||
EOF
|
||||
|
||||
# 2. 构建并启动
|
||||
docker compose build app
|
||||
docker compose up -d
|
||||
|
||||
# 3. 验证
|
||||
docker compose ps
|
||||
curl -s http://127.0.0.1:13001/api/health
|
||||
docker compose logs powersync --tail 50 # 应看到连接源库/建 publication/sync config 部署成功
|
||||
```
|
||||
|
||||
## 日常更新
|
||||
|
||||
```bash
|
||||
cd /app/iboard-0.2.1
|
||||
git archive 方式重新上传源码 tar 后解包覆盖 (data/.env 不受影响)
|
||||
cd deploy/0.2.1 && docker compose build app && docker compose up -d app
|
||||
```
|
||||
|
||||
## 注意
|
||||
|
||||
- `./data/pg` 为 0.2.1 独立数据卷;`./.env` 为密钥,两者均已 gitignore,更新源码时不会覆盖 `.env`(tar 解包不产生它)。
|
||||
- PowerSync 的 publication / replication slot 由 service 自动管理(账号 iboard021 为容器内 superuser)。
|
||||
- 同步规则改动:编辑 `powersync/sync-config.yaml` 后 `docker compose restart powersync`。
|
||||
@@ -0,0 +1,70 @@
|
||||
name: iboard-021
|
||||
|
||||
# 0.2.1 独立验证栈: PG (逻辑复制) + PowerSync + App, 与现网 /app/iboard 完全隔离
|
||||
# 见 docs/V0.2/离线编辑模块/0.2.1实施方案.md; 端口 13001 (app) / 13002 (powersync), PG 不对外
|
||||
|
||||
services:
|
||||
pg:
|
||||
image: postgres:16-alpine
|
||||
restart: unless-stopped
|
||||
command: ["postgres", "-c", "wal_level=logical"]
|
||||
environment:
|
||||
POSTGRES_USER: iboard021
|
||||
POSTGRES_PASSWORD: ${PG_PASSWORD:?PG_PASSWORD missing}
|
||||
POSTGRES_DB: iboard021
|
||||
# initdb 脚本用: 创建 powersync bucket storage 用户
|
||||
PS_STORAGE_PASSWORD: ${PS_STORAGE_PASSWORD:?PS_STORAGE_PASSWORD missing}
|
||||
volumes:
|
||||
- ./data/pg:/var/lib/postgresql/data
|
||||
- ./initdb:/docker-entrypoint-initdb.d:ro
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U iboard021 -d iboard021"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 12
|
||||
|
||||
app:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: Dockerfile
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
pg:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
DATABASE_URL: postgresql://iboard021:${PG_PASSWORD}@pg:5432/iboard021
|
||||
PORT: "13001"
|
||||
JWT_SECRET: ${JWT_SECRET_021:?JWT_SECRET_021 missing}
|
||||
POWERSYNC_JWT_SECRET: ${POWERSYNC_JWT_SECRET:?POWERSYNC_JWT_SECRET missing}
|
||||
POWERSYNC_URL: ${POWERSYNC_URL:-http://bh.vps.honor3.com:13002}
|
||||
OTA_PUBLIC_BASE: ${OTA_PUBLIC_BASE:-http://bh.vps.honor3.com:13001}
|
||||
# 0.2.1 验证部署关闭定时任务 (新闻/油价/米家等), 避免重复外呼
|
||||
DISABLE_CRON: "1"
|
||||
command: ["sh", "-c", "npx prisma migrate deploy && node --experimental-strip-types --no-warnings server/index.js"]
|
||||
ports:
|
||||
- "13001:13001"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:13001/api/health || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 20s
|
||||
|
||||
powersync:
|
||||
image: journeyapps/powersync-service:latest
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
pg:
|
||||
condition: service_healthy
|
||||
command: ["start", "-r", "unified"]
|
||||
volumes:
|
||||
- ./powersync/service.yaml:/config/service.yaml:ro
|
||||
- ./powersync/sync-config.yaml:/config/sync-config.yaml:ro
|
||||
environment:
|
||||
POWERSYNC_CONFIG_PATH: /config/service.yaml
|
||||
# service.yaml 内 !env PS_* 引用
|
||||
PS_PG_PASSWORD: ${PG_PASSWORD}
|
||||
PS_STORAGE_PASSWORD: ${PS_STORAGE_PASSWORD}
|
||||
PS_JWT_KEY_B64: ${POWERSYNC_JWT_KEY_B64:?POWERSYNC_JWT_KEY_B64 missing}
|
||||
ports:
|
||||
- "13002:8080"
|
||||
@@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
# 一次性初始化 (仅首次建库时执行): 创建 powersync bucket storage 专用用户
|
||||
# PG >= 14 允许源库与 bucket storage 同实例 (powersync schema 由 service 自建)
|
||||
set -e
|
||||
: "${PS_STORAGE_PASSWORD:?PS_STORAGE_PASSWORD missing}"
|
||||
|
||||
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
|
||||
DO \$\$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'powersync_storage') THEN
|
||||
CREATE USER powersync_storage WITH PASSWORD '$PS_STORAGE_PASSWORD';
|
||||
END IF;
|
||||
END
|
||||
\$\$;
|
||||
GRANT CREATE ON DATABASE $POSTGRES_DB TO powersync_storage;
|
||||
EOSQL
|
||||
|
||||
echo "[initdb] powersync_storage user ready."
|
||||
@@ -0,0 +1,39 @@
|
||||
# PowerSync Service 主配置 (0.2.1 验证部署)
|
||||
# 参考: https://docs.powersync.com/configuration/powersync-service/self-hosted-instances
|
||||
# !env 仅支持 PS_ 前缀变量, 由 docker-compose environment 注入
|
||||
|
||||
replication:
|
||||
connections:
|
||||
- type: postgresql
|
||||
# 源库: 本栈 pg 容器 (0.2.1 独立实例), 复制走 iboard021 账号
|
||||
uri: postgresql://iboard021:!env PS_PG_PASSWORD@pg:5432/iboard021
|
||||
sslmode: disable
|
||||
|
||||
# bucket storage: 同实例 PG (16 >= 14 允许同库不同 schema, service 自建 powersync schema)
|
||||
storage:
|
||||
type: postgresql
|
||||
uri: postgresql://powersync_storage:!env PS_STORAGE_PASSWORD@pg:5432/iboard021
|
||||
|
||||
port: 8080
|
||||
|
||||
sync_config:
|
||||
path: /config/sync-config.yaml
|
||||
|
||||
# 客户端鉴权: app 后端 /api/powersync/token 签发 HS256 JWT (aud=powersync)
|
||||
# oct 密钥 k = base64url(POWERSYNC_JWT_SECRET)
|
||||
client_auth:
|
||||
jwks:
|
||||
keys:
|
||||
- kty: oct
|
||||
k: !env PS_JWT_KEY_B64
|
||||
alg: HS256
|
||||
audience:
|
||||
- powersync
|
||||
|
||||
telemetry:
|
||||
disable_telemetry_sharing: true
|
||||
|
||||
system:
|
||||
logging:
|
||||
level: info
|
||||
format: text
|
||||
@@ -0,0 +1,24 @@
|
||||
# Sync Streams (edition 3): 任务模块 6 表 + User (展示用)
|
||||
# 引号保留 PG PascalCase 标识; 主键要求: 同步表必须输出 text 型 id 列 => CAST("id" AS TEXT)
|
||||
# 可见性: 用户能看到自己参与的项目 (ProjectMember), 及其下全部任务域数据
|
||||
# 见 docs/V0.2/离线编辑模块/0.2.1实施方案.md
|
||||
|
||||
config:
|
||||
edition: 3
|
||||
|
||||
streams:
|
||||
iboard_users:
|
||||
auto_subscribe: true
|
||||
query: SELECT CAST("id" AS TEXT) AS "id", "name", "email" FROM "User"
|
||||
|
||||
iboard_task_data:
|
||||
auto_subscribe: true
|
||||
with:
|
||||
my_projects: SELECT "projectId" FROM "ProjectMember" WHERE CAST("userId" AS TEXT) = auth.user_id()
|
||||
queries:
|
||||
- SELECT CAST("id" AS TEXT) AS "id", "uuid", "code", "name", "ownerId", "isPersonal", "archived", "notes", "taskKinds", "icon", "createdAt", "updatedAt" FROM "Project" WHERE "id" IN my_projects
|
||||
- SELECT CAST("id" AS TEXT) AS "id", "uuid", "projectId", "userId", "permission", "createdAt" FROM "ProjectMember" WHERE "projectId" IN my_projects
|
||||
- SELECT CAST("id" AS TEXT) AS "id", "uuid", "projectId", "name", "sortOrder", "boundStatus", "retention", "createdAt", "updatedAt" FROM "BoardColumn" WHERE "projectId" IN my_projects
|
||||
- SELECT CAST("id" AS TEXT) AS "id", "uuid", "projectId", "columnId", "parentId", "title", "description", "kind", "markDate", "status", "doneAt", "assigneeId", "creatorId", "extras", "sortOrder", "number", "createdAt", "updatedAt" FROM "Task" WHERE "projectId" IN my_projects
|
||||
- SELECT CAST("id" AS TEXT) AS "id", "uuid", "taskId", "userId" FROM "TaskMember" WHERE "taskId" IN (SELECT "id" FROM "Task" WHERE "projectId" IN my_projects)
|
||||
- SELECT CAST("id" AS TEXT) AS "id", "uuid", "taskId", "name" FROM "TaskTag" WHERE "taskId" IN (SELECT "id" FROM "Task" WHERE "projectId" IN my_projects)
|
||||
Reference in New Issue
Block a user