feat(deploy): openclaw-01 生产部署脚本、systemd 单元与部署文档
This commit is contained in:
@@ -0,0 +1,118 @@
|
|||||||
|
# 部署(生产 · openclaw-01)
|
||||||
|
|
||||||
|
> 适用于当前唯一的线上环境:`openclaw-01`(192.168.8.188),访问入口 `http://192.168.8.188:8200`。
|
||||||
|
> 本地开发见 [`DEVELOPMENT.md`](DEVELOPMENT.md)。部署铁律见仓库根 [`AGENTS.md`](../AGENTS.md) 第 2 节。
|
||||||
|
|
||||||
|
## 1. 生产拓扑
|
||||||
|
|
||||||
|
```
|
||||||
|
浏览器 ──HTTP──▶ openclaw-01:8200
|
||||||
|
└ gunicorn(2 × UvicornWorker) ── app.main:app
|
||||||
|
├ /api/v1/** 业务 API
|
||||||
|
└ / 前端产物 web/dist(单页应用,404 回退 index.html)
|
||||||
|
│
|
||||||
|
┌────────────────────────┼────────────────────────┐
|
||||||
|
▼ ▼ ▼
|
||||||
|
fnsvr-01:8432 PG 17.6 fnsvr-01:6379 Redis 本机 backend/var/uploads
|
||||||
|
库 dadaotask_prod db 6(独立库号) 附件文件
|
||||||
|
角色 dadao_prod_app(RLS)
|
||||||
|
角色 dadao_prod_owner(迁移)
|
||||||
|
```
|
||||||
|
|
||||||
|
为什么是这个形状:
|
||||||
|
|
||||||
|
- **生产库和开发库在同一个 PG 实例**(fnsvr-01),所以生产用**独立的库 + 独立的角色**。复用 `dadao_app` 等于拿到开发口令就能连生产。迁移里的 `GRANT`/`DEFAULT PRIVILEGES` 按 `APP_DB_ROLE` / `OWNER_DB_ROLE` 生成,靠 `backend/app/roles.py` 注入。
|
||||||
|
- **Redis 用 6 号库**隔离:共享实例没有密码,靠库号避免和别的项目撞键(验证码、幂等键)。
|
||||||
|
- **没有 nginx**:openclaw 账号无 sudo 密码,改不了系统 nginx,所以后端单端口同时托管 API 与前端产物(同源,免 CORS)。root 愿意配合时按 `deploy/prod/nginx.dadaotask.conf.example` 加反代,不用改应用。
|
||||||
|
- **用户级 systemd**:openclaw-01 上所有服务(easymemo、portal-site…)都是 `systemctl --user` 起的,本方案沿用,全程不需要 root。
|
||||||
|
|
||||||
|
## 2. 首次部署(一次性)
|
||||||
|
|
||||||
|
前置:能 SSH 到 openclaw-01;fnsvr-01 的 root 权限(只在建库这一步需要)。
|
||||||
|
|
||||||
|
### 2.1 建生产库与角色(在 fnsvr-01 上,root)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 口令自己生成,别用示例里的
|
||||||
|
OWNER_PW='<dadao_prod_owner 的口令>' APP_PW='<dadao_prod_app 的口令>' \
|
||||||
|
bash deploy/prod/provision-db.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
会创建:库 `dadaotask_prod`(owner `dadao_prod_owner`)、角色 `dadao_prod_owner` / `dadao_prod_app`(`NOSUPERUSER NOBYPASSRLS`,否则 RLS 会被跳过)、`public` schema 归 owner、`REVOKE CREATE ... FROM PUBLIC`。建表与 RLS 策略由 alembic 负责(2.4)。
|
||||||
|
|
||||||
|
### 2.2 代码落地(在 openclaw-01 上,openclaw 身份)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone --branch main --single-branch http://gitea.tatta.cn/Tatta/DadaoTask.git ~/app/dadaotask
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 配置(不进仓库)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/.config/dadaotask && chmod 700 ~/.config/dadaotask
|
||||||
|
cp ~/app/dadaotask/deploy/prod/env.production.example ~/.config/dadaotask/env.production
|
||||||
|
chmod 600 ~/.config/dadaotask/env.production
|
||||||
|
vim ~/.config/dadaotask/env.production # 替换全部 __CHANGE_ME__
|
||||||
|
```
|
||||||
|
|
||||||
|
必填项:`DATABASE_URL` / `MIGRATION_DATABASE_URL`(2.1 的口令)、`JWT_SECRET`(≥32 位随机串)、`REDIS_URL`、`WEB_DIST_DIR`、`UPLOAD_DIR`。
|
||||||
|
|
||||||
|
**当前状态是内测模式**:短信通道还没接(非 dev provider 直接 501),所以 `SMS_PROVIDER=dev` + `INTERNAL_TEST_LOGIN=true`,固定验证码 `123456`。启动日志会打 WARNING。**只允许内网访问**;短信通道接好后删掉 `INTERNAL_TEST_LOGIN` 并把 `SMS_PROVIDER` 换成真实供应商。
|
||||||
|
|
||||||
|
### 2.4 装服务单元
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/.config/systemd/user
|
||||||
|
cp ~/app/dadaotask/deploy/prod/dadaotask-api.service ~/.config/systemd/user/
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.5 首次上线
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash ~/app/dadaotask/deploy/prod/deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 日常发布(每次上线)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 本地:提交并 push 到 main(生产只跑仓库 HEAD)
|
||||||
|
# 服务器:
|
||||||
|
bash ~/app/dadaotask/deploy/prod/deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
脚本做六件事,任何一步失败即中止:代码对齐 `origin/main`(`git reset --hard`,禁止服务器手工改)→ 装 Python 依赖 → `alembic upgrade head`(用迁移角色)→ **在服务器上** `npm ci && npm run build` → 写 `env.build` 并重启 → 校验 `/health` 与 `/version` 的 commit 等于 HEAD。
|
||||||
|
|
||||||
|
前端产物只在服务器上构建,**不允许**本地上传 dist(AGENTS.md 铁律 3)。
|
||||||
|
|
||||||
|
## 4. 部署后校验
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s http://192.168.8.188:8200/health # {"status":"ok","db":true,...}
|
||||||
|
curl -s http://192.168.8.188:8200/version # commit 必须等于 git rev-parse --short HEAD
|
||||||
|
curl -sI http://192.168.8.188:8200/ | head -1 # 200,text/html(前端产物)
|
||||||
|
```
|
||||||
|
|
||||||
|
浏览器验收:打开 `http://192.168.8.188:8200` → 手机号任意 + 验证码 `123456` 登录 → 任务/工单/知识各点一遍。
|
||||||
|
|
||||||
|
## 5. 运维
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl --user status dadaotask-api # 状态
|
||||||
|
systemctl --user restart dadaotask-api # 重启
|
||||||
|
journalctl --user -u dadaotask-api -f # 日志(gunicorn 输出到 stdout)
|
||||||
|
```
|
||||||
|
|
||||||
|
- 附件落盘:`backend/var/uploads`(按工作区分子目录),删库不删文件,备份时一起打包
|
||||||
|
- 回滚:`cd ~/app/dadaotask && git reset --hard <旧 commit> && bash deploy/prod/deploy.sh`;**数据库迁移不回滚**,回滚代码前确认旧代码兼容新表结构
|
||||||
|
- 端口:`8200`(systemd 单元与 `deploy.sh` 的 `PORT` 默认值,两处要一致)
|
||||||
|
|
||||||
|
## 6. 已知限制(当前阶段)
|
||||||
|
|
||||||
|
| 限制 | 影响 | 解除方式 |
|
||||||
|
|---|---|---|
|
||||||
|
| 没有 HTTPS / 域名 | 只能内网 IP 访问,令牌明文传输 | 域名 + nginx + 证书(需 root) |
|
||||||
|
| 内测固定验证码 | 谁都能用任意手机号登录 | 接短信通道,关 `INTERNAL_TEST_LOGIN` |
|
||||||
|
| 无 pgvector | 语义检索走 `real[]` 全表扫描,数据量大了会慢 | 上独立 pgvector 实例(`deploy/docker-compose.vector.yml`) |
|
||||||
|
| 上传目录在本机磁盘 | 机器坏了附件就没了 | 备份 `backend/var/uploads`,或将来到对象存储 |
|
||||||
|
| 共享 PG / Redis | 别人占满连接或内存会波及本站 | 独立实例 |
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
- M0–M1 实施清单(含真实进度):[`2.Design/ROADMAP-M0-M1.md`](2.Design/ROADMAP-M0-M1.md)
|
- M0–M1 实施清单(含真实进度):[`2.Design/ROADMAP-M0-M1.md`](2.Design/ROADMAP-M0-M1.md)
|
||||||
- 接口约定(已实现部分):[`2.Design/API.md`](2.Design/API.md)
|
- 接口约定(已实现部分):[`2.Design/API.md`](2.Design/API.md)
|
||||||
- 本地开发:[`2.Design/DEVELOPMENT.md`](2.Design/DEVELOPMENT.md)
|
- 本地开发:[`2.Design/DEVELOPMENT.md`](2.Design/DEVELOPMENT.md)
|
||||||
|
- 生产部署(openclaw-01):[`2.Design/DEPLOY.md`](2.Design/DEPLOY.md)
|
||||||
- 中文命名方案:[`2.Design/NAMING.md`](2.Design/NAMING.md)
|
- 中文命名方案:[`2.Design/NAMING.md`](2.Design/NAMING.md)
|
||||||
|
|
||||||
## 技术栈
|
## 技术栈
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
fastapi==0.115.6
|
fastapi==0.115.6
|
||||||
uvicorn[standard]==0.34.0
|
uvicorn[standard]==0.34.0
|
||||||
|
# 生产进程管理:gunicorn + UvicornWorker(开发用 `uvicorn --reload` 即可)
|
||||||
|
gunicorn==23.0.0
|
||||||
sqlalchemy[asyncio]==2.0.36
|
sqlalchemy[asyncio]==2.0.36
|
||||||
asyncpg==0.30.0
|
asyncpg==0.30.0
|
||||||
alembic==1.14.0
|
alembic==1.14.0
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# 达道Task 生产服务(用户级 systemd,openclaw-01 上不需要 root)
|
||||||
|
#
|
||||||
|
# 安装:
|
||||||
|
# cp deploy/prod/dadaotask-api.service ~/.config/systemd/user/
|
||||||
|
# systemctl --user daemon-reload
|
||||||
|
# systemctl --user enable --now dadaotask-api
|
||||||
|
#
|
||||||
|
# 两份 EnvironmentFile:
|
||||||
|
# env.production —— 真实配置与口令(chmod 600,不进仓库)
|
||||||
|
# env.build —— 由 deploy.sh 每次部署重写(BUILD_COMMIT / BUILD_TIME),供 /version 校验
|
||||||
|
[Unit]
|
||||||
|
Description=DadaoTask API (prod, gunicorn + uvicorn workers)
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
WorkingDirectory=/home/openclaw/app/dadaotask/backend
|
||||||
|
EnvironmentFile=/home/openclaw/.config/dadaotask/env.production
|
||||||
|
EnvironmentFile=/home/openclaw/.config/dadaotask/env.build
|
||||||
|
Environment=PYTHONUNBUFFERED=1
|
||||||
|
ExecStart=/home/openclaw/app/dadaotask/.venv/bin/gunicorn app.main:app \
|
||||||
|
-k uvicorn.workers.UvicornWorker -w 2 \
|
||||||
|
-b 0.0.0.0:8200 \
|
||||||
|
--timeout 60 --graceful-timeout 30 --keep-alive 5 \
|
||||||
|
--access-logfile - --error-logfile -
|
||||||
|
Restart=always
|
||||||
|
RestartSec=3
|
||||||
|
KillSignal=SIGTERM
|
||||||
|
TimeoutStopSec=40
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
Executable
+77
@@ -0,0 +1,77 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# 生产部署脚本 —— openclaw-01(用户级 systemd,全程不需要 root)
|
||||||
|
#
|
||||||
|
# 遵守 AGENTS.md 的部署铁律:
|
||||||
|
# 1. 生产只跑仓库 HEAD(git reset --hard origin/main,禁止服务器手工改)
|
||||||
|
# 2. 前端产物只在服务器上由 HEAD 构建(npm ci && npm run build)
|
||||||
|
# 3. 部署后校验 /version 的 commit == git rev-parse --short HEAD
|
||||||
|
#
|
||||||
|
# 用法(在 openclaw-01 上,openclaw 身份):
|
||||||
|
# bash ~/app/dadaotask/deploy/prod/deploy.sh
|
||||||
|
# 首次部署前准备好 ~/.config/dadaotask/env.production(见 env.production.example)。
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
APP_DIR=${APP_DIR:-/home/openclaw/app/dadaotask}
|
||||||
|
CONF_DIR=${CONF_DIR:-$HOME/.config/dadaotask}
|
||||||
|
ENV_FILE=$CONF_DIR/env.production
|
||||||
|
SERVICE=${SERVICE:-dadaotask-api}
|
||||||
|
PORT=${PORT:-8200}
|
||||||
|
REPO_URL=${REPO_URL:-https://gitea.tatta.cn/Tatta/DadaoTask.git}
|
||||||
|
|
||||||
|
step() { printf '\n== %s ==\n' "$*"; }
|
||||||
|
die() { printf '!! %s\n' "$*" >&2; exit 1; }
|
||||||
|
|
||||||
|
step "0. 前置检查"
|
||||||
|
test -f "$ENV_FILE" || die "缺少 $ENV_FILE(参考 deploy/prod/env.production.example)"
|
||||||
|
|
||||||
|
if ! command -v node >/dev/null; then die "node 未安装(前端必须在服务器上构建)"; fi
|
||||||
|
node_major=$(node -v | sed 's/^v\([0-9]*\).*/\1/')
|
||||||
|
[ "$node_major" -ge 20 ] || die "node 版本过低:$(node -v),需要 20+"
|
||||||
|
|
||||||
|
step "1. 代码对齐仓库 HEAD"
|
||||||
|
if [ ! -d "$APP_DIR/.git" ]; then
|
||||||
|
git clone --branch main --single-branch "$REPO_URL" "$APP_DIR"
|
||||||
|
fi
|
||||||
|
cd "$APP_DIR"
|
||||||
|
git fetch --prune origin
|
||||||
|
git checkout -q main
|
||||||
|
git reset --hard -q origin/main # 生产不允许领先/落后仓库,也不允许脏改动
|
||||||
|
COMMIT=$(git rev-parse HEAD)
|
||||||
|
SHORT=$(git rev-parse --short HEAD)
|
||||||
|
echo " HEAD=$COMMIT ($SHORT)"
|
||||||
|
|
||||||
|
step "2. Python 依赖"
|
||||||
|
[ -x .venv/bin/python ] || python3 -m venv .venv
|
||||||
|
.venv/bin/pip install -q --upgrade pip
|
||||||
|
.venv/bin/pip install -q -r backend/requirements.txt
|
||||||
|
|
||||||
|
step "3. 数据库迁移(迁移角色,非应用角色)"
|
||||||
|
set -a; . "$ENV_FILE"; set +a
|
||||||
|
( cd backend && ../.venv/bin/alembic upgrade head )
|
||||||
|
|
||||||
|
step "4. 前端构建(只在服务器上、只用 HEAD 的源码)"
|
||||||
|
( cd web && npm ci --no-audit --no-fund && npm run build )
|
||||||
|
test -f web/dist/index.html || die "前端构建产物缺失:web/dist/index.html"
|
||||||
|
|
||||||
|
step "5. 写入构建标记并重启服务"
|
||||||
|
mkdir -p "$CONF_DIR" backend/var/uploads
|
||||||
|
chmod 700 "$CONF_DIR"
|
||||||
|
printf 'BUILD_COMMIT=%s\nBUILD_TIME=%s\n' "$SHORT" "$(date -Iseconds)" >"$CONF_DIR/env.build"
|
||||||
|
chmod 600 "$CONF_DIR/env.build" "$ENV_FILE"
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
systemctl --user enable "$SERVICE" >/dev/null 2>&1 || true
|
||||||
|
systemctl --user restart "$SERVICE"
|
||||||
|
|
||||||
|
step "6. 校验(/health + /version 的 commit)"
|
||||||
|
for _ in $(seq 1 30); do
|
||||||
|
curl -fsS "http://127.0.0.1:$PORT/health" >/dev/null 2>&1 && break
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
HEALTH=$(curl -fsS "http://127.0.0.1:$PORT/health") || die "健康检查失败:服务没起来(journalctl --user -u $SERVICE -n 100)"
|
||||||
|
COMMIT_HTTP=$(curl -fsS "http://127.0.0.1:$PORT/version" | .venv/bin/python -c 'import json,sys; print(json.load(sys.stdin)["commit"])') \
|
||||||
|
|| die "/version 读取失败"
|
||||||
|
echo " health=$HEALTH"
|
||||||
|
echo " version.commit=$COMMIT_HTTP"
|
||||||
|
[ "$COMMIT_HTTP" = "$SHORT" ] || die "/version 的 commit($COMMIT_HTTP) 与 HEAD($SHORT) 不一致"
|
||||||
|
|
||||||
|
printf '\n部署完成:http://%s:%s (commit %s)\n' "$(hostname -I | awk '{print $1}')" "$PORT" "$SHORT"
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# 生产环境变量模板(部署到 openclaw-01)
|
||||||
|
# 用法:复制到 ~/.config/dadaotask/env.production(chmod 600),把 __CHANGE_ME__ 换成真实口令。
|
||||||
|
# 真实口令不进仓库;本文件只描述字段。
|
||||||
|
ENV=prod
|
||||||
|
APP_NAME=达道Task
|
||||||
|
|
||||||
|
# ── 数据库:fnsvr-01 共享 PG 上**独立的库 + 独立的角色** ──────────────
|
||||||
|
# 生产不复用 dev 的 dadao_app/dadao_owner 角色:同集群同角色 = 拿到开发口令即可连生产。
|
||||||
|
# 角色名必须在迁移前注入(迁移里的 GRANT 按它生成)。
|
||||||
|
OWNER_DB_ROLE=dadao_prod_owner
|
||||||
|
APP_DB_ROLE=dadao_prod_app
|
||||||
|
DATABASE_URL=postgresql+asyncpg://dadao_prod_app:__APP_PW__@192.168.8.160:8432/dadaotask_prod
|
||||||
|
MIGRATION_DATABASE_URL=postgresql+asyncpg://dadao_prod_owner:__OWNER_PW__@192.168.8.160:8432/dadaotask_prod
|
||||||
|
|
||||||
|
# ── Redis:fnsvr-01 共享实例,独立 db 号隔离(不使用默认 0 号库)──────
|
||||||
|
REDIS_URL=redis://192.168.8.160:6379/6
|
||||||
|
|
||||||
|
# ── 认证 ─────────────────────────────────────────────────────────────
|
||||||
|
# 生产校验:长度 ≥ 32 且不是开发默认值
|
||||||
|
JWT_SECRET=__CHANGE_ME_32_CHARS_OR_MORE__
|
||||||
|
ACCESS_TOKEN_MINUTES=30
|
||||||
|
REFRESH_TOKEN_DAYS=14
|
||||||
|
|
||||||
|
# 内测登录:短信通道还没接(非 dev provider 直接 501),先把固定验证码显式打开。
|
||||||
|
# 只允许内网访问;短信通道接好后把这两行改成 SMS_PROVIDER=<供应商> 并删掉开关。
|
||||||
|
INTERNAL_TEST_LOGIN=true
|
||||||
|
SMS_PROVIDER=dev
|
||||||
|
SMS_DEV_CODE=123456
|
||||||
|
SMS_THROTTLE_SECONDS=60
|
||||||
|
|
||||||
|
# 同源部署(后端直接托管 web/dist),CORS 只留本机调试用
|
||||||
|
CORS_ORIGINS=http://127.0.0.1:8200,http://192.168.8.188:8200
|
||||||
|
|
||||||
|
# ── 前端产物与上传目录(绝对路径)─────────────────────────────────────
|
||||||
|
WEB_DIST_DIR=/home/openclaw/app/dadaotask/web/dist
|
||||||
|
UPLOAD_DIR=/home/openclaw/app/dadaotask/backend/var/uploads
|
||||||
|
MAX_UPLOAD_MB=20
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# 给 root 用的 nginx 反代(当前没启用:openclaw 账号无 sudo,先跑 8200 单端口)
|
||||||
|
#
|
||||||
|
# 装法(root):
|
||||||
|
# cp deploy/prod/nginx.dadaotask.conf.example /etc/nginx/sites-available/dadaotask
|
||||||
|
# ln -s /etc/nginx/sites-available/dadaotask /etc/nginx/sites-enabled/dadaotask
|
||||||
|
# nginx -t && systemctl reload nginx
|
||||||
|
#
|
||||||
|
# 说明:后端已直接在 8200 上同时托管 API 和前端产物(web/dist),
|
||||||
|
# 所以这里只做一层透传,不需要单独的静态站点 root。
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name _; # 有域名后改成真实域名
|
||||||
|
|
||||||
|
client_max_body_size 25m; # 附件上限 20MB,留余量
|
||||||
|
gzip on;
|
||||||
|
gzip_types text/css application/javascript application/json image/svg+xml;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:8200;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
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;
|
||||||
|
proxy_read_timeout 120s; # 知识检索/上传偶发慢请求
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+50
@@ -0,0 +1,50 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# 在 fnsvr-01 的共享 PostgreSQL 上给「生产」建独立的库与角色。
|
||||||
|
#
|
||||||
|
# 为什么独立角色:生产库和开发库在同一个实例(fnsvr-01:8432)。复用 dadao_app
|
||||||
|
# 等于拿到开发口令就能连生产;所以生产用 dadao_prod_owner / dadao_prod_app,
|
||||||
|
# 迁移里的 GRANT 与 DEFAULT PRIVILEGES 会按 APP_DB_ROLE / OWNER_DB_ROLE 生成。
|
||||||
|
#
|
||||||
|
# 在 fnsvr-01 上以 root 执行(需要 docker 权限):
|
||||||
|
# OWNER_PW='...' APP_PW='...' bash deploy/prod/provision-db.sh
|
||||||
|
#
|
||||||
|
# 幂等:角色或库已存在时跳过创建(不会重置已有口令)。
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
CONTAINER=${CONTAINER:-1Panel-postgresql-wIKz}
|
||||||
|
DB=${DB:-dadaotask_prod}
|
||||||
|
OWNER_ROLE=${OWNER_ROLE:-dadao_prod_owner}
|
||||||
|
APP_ROLE=${APP_ROLE:-dadao_prod_app}
|
||||||
|
: "${OWNER_PW:?需要 OWNER_PW 环境变量(迁移角色口令)}"
|
||||||
|
: "${APP_PW:?需要 APP_PW 环境变量(应用角色口令)}"
|
||||||
|
|
||||||
|
echo "== 建角色与库:$DB(owner=$OWNER_ROLE app=$APP_ROLE)=="
|
||||||
|
docker exec -i "$CONTAINER" psql -U dbadmin -d postgres -v ON_ERROR_STOP=1 -q \
|
||||||
|
-v owner_role="$OWNER_ROLE" -v owner_pw="$OWNER_PW" \
|
||||||
|
-v app_role="$APP_ROLE" -v app_pw="$APP_PW" -v db="$DB" <<'SQL'
|
||||||
|
SELECT format('CREATE ROLE %I LOGIN PASSWORD %L', :'owner_role', :'owner_pw')
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = :'owner_role')\gexec
|
||||||
|
SELECT format('CREATE ROLE %I LOGIN PASSWORD %L', :'app_role', :'app_pw')
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = :'app_role')\gexec
|
||||||
|
-- 应用角色绝不能是 superuser / bypassrls,否则 RLS 直接被跳过
|
||||||
|
SELECT format('ALTER ROLE %I NOSUPERUSER NOBYPASSRLS NOCREATEDB NOCREATEROLE', :'app_role')\gexec
|
||||||
|
SELECT format('ALTER ROLE %I NOSUPERUSER NOBYPASSRLS', :'owner_role')\gexec
|
||||||
|
SELECT format('CREATE DATABASE %I OWNER %I', :'db', :'owner_role')
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM pg_database WHERE datname = :'db')\gexec
|
||||||
|
SQL
|
||||||
|
|
||||||
|
echo "== 库内权限(schema 归 owner,app 只能读写数据)=="
|
||||||
|
docker exec -i "$CONTAINER" psql -U dbadmin -d "$DB" -v ON_ERROR_STOP=1 -q \
|
||||||
|
-v owner_role="$OWNER_ROLE" -v app_role="$APP_ROLE" <<'SQL'
|
||||||
|
REVOKE CREATE ON SCHEMA public FROM PUBLIC;
|
||||||
|
SELECT format('ALTER SCHEMA public OWNER TO %I', :'owner_role')\gexec
|
||||||
|
SELECT format('GRANT CONNECT ON DATABASE %I TO %I', current_database(), :'app_role')\gexec
|
||||||
|
SELECT format('GRANT USAGE ON SCHEMA public TO %I', :'app_role')\gexec
|
||||||
|
SQL
|
||||||
|
|
||||||
|
echo "== 自检 =="
|
||||||
|
docker exec -i "$CONTAINER" psql -U dbadmin -d "$DB" -tAc \
|
||||||
|
"select current_database(), (select rolname from pg_roles where rolname='$APP_ROLE'),
|
||||||
|
(select rolsuper from pg_roles where rolname='$APP_ROLE'),
|
||||||
|
(select rolbypassrls from pg_roles where rolname='$APP_ROLE')"
|
||||||
|
echo "完成。注意:建表与 RLS 由 alembic(deploy.sh 第 3 步)负责。"
|
||||||
Reference in New Issue
Block a user