fix(deploy): 蓝绿端口反转——nginx 接管对外 8765,实例挪 8766/8767(bump 0.41.1)

对外契约(URL:8765)保持不变:客户端 / platform 嵌入 / EMBED 对接方零改动;
blue=8766、green=8767 只绑 127.0.0.1。bootstrap 顺序同步调整(停旧服务释放
8765 后 nginx 才能 reload 接管);已知边缘补文档:切换窗口内对旧实例 run
点「停止」送达不到(cancel 与 event 同为进程内 broker,根因一致)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
caoqianming 2026-07-06 14:16:46 +08:00
parent 1d62670366
commit 242548ea84
8 changed files with 31 additions and 22 deletions

View File

@ -2,7 +2,7 @@
> 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查 `git log` / `git diff` / `DESIGN §7.9` > 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查 `git log` / `git diff` / `DESIGN §7.9`
最后更新:2026-07-06(蓝绿无感部署 B 档落地,bump 0.41.0) 最后更新:2026-07-06(蓝绿端口方案改 nginx 接管 8765,bump 0.41.1)
--- ---
@ -23,6 +23,7 @@
### 2026-07 ### 2026-07
- **07-06 / 0.41.1 蓝绿端口方案反转**:nginx 接管原对外端口 8765(客户端 / platform 嵌入零改动),实例挪 8766(blue)/ 8767(green) 只绑本机——对外契约(URL:8765)不动优先于"实例沿用旧端口"的省事;顺带把「切换窗口内对旧实例 run 点停止送达不到」补进已知边缘文档。
- **07-06 / 0.41.0 蓝绿无感部署(RUN.md B 档落地)**:生产部署零 503 窗口 + 在跑对话 SSE 直播不断。`zcbot@blue/green` 双 systemd 实例(模板 unit + `deploy/instances/*.env`)+ nginx upstream 单行文件切流,编排在 `deploy/update_bluegreen.sh`(复用 `update.sh --prepare-only`;切流后轮询 DB 等旧色 run 自然清空再 stop——直接 stop 会被 uvicorn `timeout_graceful_shutdown=5` 掐 SSE)。双实例并存互踩三修:`tasks.run_owner`(0020)reaper 只收自己色、sandbox 容器名/label 带实例色各清各的、微信 ClawBot 长轮询 PG advisory lock 选主(detach 连接持锁,断连即让位);定时任务 claim 本就 SKIP LOCKED 免改。关键判断:broker 刻意不出进程(不上 Redis),接受"切换瞬间刷新的用户看不到旧实例直播"这一边缘。单实例部署 `ZCBOT_INSTANCE` 未设全部退回原行为,`update.sh` 保留不废。 - **07-06 / 0.41.0 蓝绿无感部署(RUN.md B 档落地)**:生产部署零 503 窗口 + 在跑对话 SSE 直播不断。`zcbot@blue/green` 双 systemd 实例(模板 unit + `deploy/instances/*.env`)+ nginx upstream 单行文件切流,编排在 `deploy/update_bluegreen.sh`(复用 `update.sh --prepare-only`;切流后轮询 DB 等旧色 run 自然清空再 stop——直接 stop 会被 uvicorn `timeout_graceful_shutdown=5` 掐 SSE)。双实例并存互踩三修:`tasks.run_owner`(0020)reaper 只收自己色、sandbox 容器名/label 带实例色各清各的、微信 ClawBot 长轮询 PG advisory lock 选主(detach 连接持锁,断连即让位);定时任务 claim 本就 SKIP LOCKED 免改。关键判断:broker 刻意不出进程(不上 Redis),接受"切换瞬间刷新的用户看不到旧实例直播"这一边缘。单实例部署 `ZCBOT_INSTANCE` 未设全部退回原行为,`update.sh` 保留不废。
- **07-06 / 0.40.0 ppt 伪导出根治(三层)**:0.35.1 复发(模型照禁令里的"配方"装 cairosvg 手搓整页贴图导出)复盘——①平台层机检门落地(`core/pptx_guard.py` + loop 钩子:shell/run_python 新产出 .pptx 命中"≥80% 页整版位图 + 全 deck 零原生文本"即注入 `[产物机检 ERROR]` 当场逼返工;判定刻意保守,`tests/test_pptx_guard.py` 8 例);②禁令去菜谱:ppt/brief SKILL.md 否定式指令改"正面窄门"(唯一入口 + 报错就修/上报),删掉可照做的违规配方;③官方脚本 5 处 `pip install` hint 改环境说明(pptx_media/svg_preview/crop_images/fix_image_aspect/pptx_cli)——打印给 agent 的安装指令就是执行指令。详 DESIGN §8.9。 - **07-06 / 0.40.0 ppt 伪导出根治(三层)**:0.35.1 复发(模型照禁令里的"配方"装 cairosvg 手搓整页贴图导出)复盘——①平台层机检门落地(`core/pptx_guard.py` + loop 钩子:shell/run_python 新产出 .pptx 命中"≥80% 页整版位图 + 全 deck 零原生文本"即注入 `[产物机检 ERROR]` 当场逼返工;判定刻意保守,`tests/test_pptx_guard.py` 8 例);②禁令去菜谱:ppt/brief SKILL.md 否定式指令改"正面窄门"(唯一入口 + 报错就修/上报),删掉可照做的违规配方;③官方脚本 5 处 `pip install` hint 改环境说明(pptx_media/svg_preview/crop_images/fix_image_aspect/pptx_cli)——打印给 agent 的安装指令就是执行指令。详 DESIGN §8.9。
- **07-06 / 0.39.2 research 对接 paper_server API Key**:paper_server GET 接口收紧要 Key 后,`skills/research/paper.py` 全部请求带 `?api_key=`(env `PAPER_SERVER_API_KEY`),认证失败(paper_server 异常处理器压成 HTTP 400 + err_code)转成带指引的 RuntimeError;这把低价值只读 key 是唯一刻意放进 sandbox 的凭证——`run_python` env 过滤器加 allowlist 放行、docker 模式 `_sandbox_env()` 透传进容器(顺带修掉 `PAPER_SERVER_URL` 覆盖不进 docker 的缺口)。 - **07-06 / 0.39.2 research 对接 paper_server API Key**:paper_server GET 接口收紧要 Key 后,`skills/research/paper.py` 全部请求带 `?api_key=`(env `PAPER_SERVER_API_KEY`),认证失败(paper_server 异常处理器压成 HTTP 400 + err_code)转成带指引的 RuntimeError;这把低价值只读 key 是唯一刻意放进 sandbox 的凭证——`run_python` env 过滤器加 allowlist 放行、docker 模式 `_sandbox_env()` 透传进容器(顺带修掉 `PAPER_SERVER_URL` 覆盖不进 docker 的缺口)。

17
RUN.md
View File

@ -310,7 +310,7 @@ ExecStart=/opt/zcbot/.venv/bin/python main.py web --host 0.0.0.0 --port 8765 --r
### 蓝绿部署(B 档,已实现,生产推荐) ### 蓝绿部署(B 档,已实现,生产推荐)
两个 systemd 实例 `zcbot@blue`(127.0.0.1:8765)/ `zcbot@green`(127.0.0.1:8766),nginx upstream(`/etc/nginx/zcbot_upstream.conf`,单行 `server` 指令)在中间切流量。日常部署一条命令: 两个 systemd 实例 `zcbot@blue`(127.0.0.1:8766)/ `zcbot@green`(127.0.0.1:8767),**nginx 对外监听原单实例端口 8765** —— 客户端 / platform 嵌入 URL 一个字不用改;upstream(`/etc/nginx/zcbot_upstream.conf`,单行 `server` 指令)在两实例间切流量。日常部署一条命令:
```bash ```bash
sudo bash /opt/zcbot/deploy/update_bluegreen.sh sudo bash /opt/zcbot/deploy/update_bluegreen.sh
@ -321,7 +321,7 @@ sudo bash /opt/zcbot/deploy/update_bluegreen.sh
脚本流程:`update.sh --prepare-only`(拉码 / pip / migration / build,全复用单实例脚本)→ 起空闲色实例 → healthz 验活(核对 `instance` 字段)→ 改写 upstream + nginx reload(**新请求全走新实例,零 503**)→ **轮询 DB 等旧色在跑 run 自然清空**(上限 `DRAIN_WAIT_SECONDS`,期间旧 SSE 继续直播不断流 —— 直接 stop 的话 uvicorn `timeout_graceful_shutdown=5` 会 5s 掐长连接)→ stop 旧实例(残留 run 走 SIGTERM drain / 协作式 cancel)→ 旧色 SIGKILL 残留的 running 行标 error → enable 新色 / disable 旧色(重启机器只拉起 live 色)。 脚本流程:`update.sh --prepare-only`(拉码 / pip / migration / build,全复用单实例脚本)→ 起空闲色实例 → healthz 验活(核对 `instance` 字段)→ 改写 upstream + nginx reload(**新请求全走新实例,零 503**)→ **轮询 DB 等旧色在跑 run 自然清空**(上限 `DRAIN_WAIT_SECONDS`,期间旧 SSE 继续直播不断流 —— 直接 stop 的话 uvicorn `timeout_graceful_shutdown=5` 会 5s 掐长连接)→ stop 旧实例(残留 run 走 SIGTERM drain / 协作式 cancel)→ 旧色 SIGKILL 残留的 running 行标 error → enable 新色 / disable 旧色(重启机器只拉起 live 色)。
效果 vs 单实例 `update.sh`:**没有 503 窗口,在跑对话的 SSE 直播也不断**。已知边缘(接受):用户恰在切换后刷新页面、而他的 run 还在旧实例上跑 → 新实例 broker 没有该 run 的事件,直播看不到(run 照常在旧实例跑完落库,刷新看历史即可)。broker 是进程内(`web/broker.py`)不跨实例 —— 刻意取舍,当前规模不值得上 Redis pub/sub。 效果 vs 单实例 `update.sh`:**没有 503 窗口,在跑对话的 SSE 直播也不断**。已知边缘(接受,仅存在于切换后旧色 drain 的几分钟窗口内、且 run 恰好还在旧实例上):① 用户刷新页面 → SSE 连到新实例,broker 没有该 run 的事件,直播看不到(run 照常在旧实例跑完落库,完了刷新看历史);② 点「停止」→ cancel 信号进的是新实例的进程内 broker,旧实例收不到,等 run 自然收尾即可。两者根因同一个:event/cancel broker 是进程内(`web/broker.py`)不跨实例 —— 刻意取舍,当前规模不值得上 Redis pub/sub(取舍详见 DESIGN §7.0)
蓝绿并存期的互踩问题已在代码层解决(单实例部署 `ZCBOT_INSTANCE` 未设,以下全部退回原行为): 蓝绿并存期的互踩问题已在代码层解决(单实例部署 `ZCBOT_INSTANCE` 未设,以下全部退回原行为):
- **启动 reaper**:`tasks.run_owner`(migration 0020)记 run 归属实例,实例启动只收自己色(+ NULL 遗留)的孤儿 run,不误杀另一实例在跑的 - **启动 reaper**:`tasks.run_owner`(migration 0020)记 run 归属实例,实例启动只收自己色(+ NULL 遗留)的孤儿 run,不误杀另一实例在跑的
@ -340,24 +340,27 @@ sudo bash deploy/update.sh
sudo cp deploy/zcbot@.service /etc/systemd/system/ sudo cp deploy/zcbot@.service /etc/systemd/system/
sudo systemctl daemon-reload sudo systemctl daemon-reload
# 3) 装 nginx + 反代配置 + 初始 upstream 指 blue(conf 里按需改 server_name / TLS / 对外端口) # 3) 装 nginx + 反代配置 + 初始 upstream 指 blue(conf 里按需改 server_name / TLS)
# 此时先别 reload —— nginx 要监听 8765,旧单实例服务还占着
sudo apt-get install -y nginx sudo apt-get install -y nginx
sudo cp deploy/nginx/zcbot.conf.example /etc/nginx/conf.d/zcbot.conf sudo cp deploy/nginx/zcbot.conf.example /etc/nginx/conf.d/zcbot.conf
echo 'server 127.0.0.1:8765;' | sudo tee /etc/nginx/zcbot_upstream.conf echo 'server 127.0.0.1:8766;' | sudo tee /etc/nginx/zcbot_upstream.conf
# 4) 停旧单实例服务(优雅 drain)→ 清无实例 label 的旧 sandbox 容器 → 起 blue → nginx 生效 # 4) 停旧单实例服务(优雅 drain,释放 8765)→ 清无实例 label 的旧 sandbox 容器
# → 起 blue(8766)→ nginx 接管 8765。停旧到 nginx 起来之间是本次唯一的中断窗口
sudo systemctl disable --now zcbot sudo systemctl disable --now zcbot
sudo docker rm -f $(sudo docker ps -aq -f label=zcbot.product=sandbox) 2>/dev/null || true sudo docker rm -f $(sudo docker ps -aq -f label=zcbot.product=sandbox) 2>/dev/null || true
sudo systemctl enable --now zcbot@blue sudo systemctl enable --now zcbot@blue
curl -fsS http://127.0.0.1:8765/healthz # 应含 "instance":"blue" curl -fsS http://127.0.0.1:8766/healthz # 应含 "instance":"blue"
sudo nginx -t && sudo systemctl reload nginx sudo nginx -t && sudo systemctl reload nginx
curl -fsS http://127.0.0.1:8765/healthz # 经 nginx,应同样返回 ok
# 5) 之后日常部署一律: # 5) 之后日常部署一律:
sudo bash deploy/update_bluegreen.sh sudo bash deploy/update_bluegreen.sh
``` ```
注意: 注意:
- **对外端口变了**:客户端从直连 8765 改走 nginx(80/443)。platform 嵌入 / EMBED.md 对接方同步换 URL;实例只绑 127.0.0.1 不再直接对外 - **对外端口不变**:nginx 接管 8765,客户端 / platform 嵌入 / EMBED.md 对接方零改动;实例(8766/8767)只绑 127.0.0.1 不对外。原来若用 uvicorn 原生 TLS(`--ssl-certfile`)跑 8765,证书挪到 nginx 层配
- **回滚 ≠ 切回旧色**:代码目录两实例共享,旧实例重启后跑的也是新代码。回滚 = `git revert` 后重跑 `update_bluegreen.sh`。`--switch` 只用于半途恢复(如新实例切完流量后挂了,切回还活着的那个色)。 - **回滚 ≠ 切回旧色**:代码目录两实例共享,旧实例重启后跑的也是新代码。回滚 = `git revert` 后重跑 `update_bluegreen.sh`。`--switch` 只用于半途恢复(如新实例切完流量后挂了,切回还活着的那个色)。
- `deploy/update.sh`(单实例版)保留不废:蓝绿脚本内部复用它 `--prepare-only`;单实例部署机(host backend / 内网机)继续用它,行为不变。 - `deploy/update.sh`(单实例版)保留不废:蓝绿脚本内部复用它 `--prepare-only`;单实例部署机(host backend / 内网机)继续用它,行为不变。
- unit 的 `TimeoutStopSec=90``config/agent.yaml` shutdown 段(drain 30s + cancel 15s)的约束同单实例:改 yaml 数值时模板 unit 跟着调。 - unit 的 `TimeoutStopSec=90``config/agent.yaml` shutdown 段(drain 30s + cancel 15s)的约束同单实例:改 yaml 数值时模板 unit 跟着调。

View File

@ -1,3 +1,3 @@
# zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。 # zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。
# 改版本只动这一行。 # 改版本只动这一行。
__version__ = "0.41.0" __version__ = "0.41.1"

View File

@ -1,4 +1,4 @@
# zcbot@blue 实例配置(systemd 模板 unit EnvironmentFile,RUN.md B 档) # zcbot@blue 实例配置(systemd 模板 unit EnvironmentFile,RUN.md B 档)
# 端口/实例名是蓝绿脚本与 nginx upstream 的契约,别改;业务配置进 /opt/zcbot/.env # 端口/实例名是蓝绿脚本与 nginx upstream 的契约,别改;业务配置进 /opt/zcbot/.env
ZCBOT_PORT=8765 ZCBOT_PORT=8766
ZCBOT_INSTANCE=blue ZCBOT_INSTANCE=blue

View File

@ -1,4 +1,4 @@
# zcbot@green 实例配置(systemd 模板 unit EnvironmentFile,RUN.md B 档) # zcbot@green 实例配置(systemd 模板 unit EnvironmentFile,RUN.md B 档)
# 端口/实例名是蓝绿脚本与 nginx upstream 的契约,别改;业务配置进 /opt/zcbot/.env # 端口/实例名是蓝绿脚本与 nginx upstream 的契约,别改;业务配置进 /opt/zcbot/.env
ZCBOT_PORT=8766 ZCBOT_PORT=8767
ZCBOT_INSTANCE=green ZCBOT_INSTANCE=green

View File

@ -1,22 +1,25 @@
# zcbot nginx 反代示例(蓝绿双实例,RUN.md §无感更新 B 档)。 # zcbot nginx 反代示例(蓝绿双实例,RUN.md §无感更新 B 档)。
# #
# 端口约定:nginx 接管原单实例的对外端口 8765(客户端 / platform 嵌入 URL 不用改),
# 实例挪到本机 8766(blue)/ 8767(green)。装 nginx 前先停旧单实例服务释放 8765。
#
# 安装(一次性): # 安装(一次性):
# sudo cp deploy/nginx/zcbot.conf.example /etc/nginx/conf.d/zcbot.conf # 按需改 server_name/端口/TLS # sudo cp deploy/nginx/zcbot.conf.example /etc/nginx/conf.d/zcbot.conf # 按需改 server_name/TLS
# echo 'server 127.0.0.1:8765;' | sudo tee /etc/nginx/zcbot_upstream.conf # 初始指 blue # echo 'server 127.0.0.1:8766;' | sudo tee /etc/nginx/zcbot_upstream.conf # 初始指 blue
# sudo nginx -t && sudo systemctl reload nginx # sudo nginx -t && sudo systemctl reload nginx
# #
# 之后 zcbot_upstream.conf 由 deploy/update_bluegreen.sh 改写 + reload nginx 切流量, # 之后 zcbot_upstream.conf 由 deploy/update_bluegreen.sh 改写 + reload nginx 切流量,
# 本文件不再动。nginx reload 对已建立连接零影响:旧 SSE 留在旧实例自然收尾,新请求走新实例。 # 本文件不再动。nginx reload 对已建立连接零影响:旧 SSE 留在旧实例自然收尾,新请求走新实例。
upstream zcbot_backend { upstream zcbot_backend {
# 只含一行 server 127.0.0.1:8765|8766;由 update_bluegreen.sh 维护 # 只含一行 server 127.0.0.1:8766|8767;由 update_bluegreen.sh 维护
include /etc/nginx/zcbot_upstream.conf; include /etc/nginx/zcbot_upstream.conf;
keepalive 32; keepalive 32;
} }
server { server {
listen 80; # 对外沿用原单实例端口,客户端无感;上 TLS 时换 listen 443 ssl + 证书(certbot 等)
# TODO 按部署域名改;上 TLS 时换 listen 443 ssl + 证书(certbot 等) listen 8765;
server_name _; server_name _;
# 上传接口(/v1/files/upload)会传大文件;与后端无硬限制,给足余量 # 上传接口(/v1/files/upload)会传大文件;与后端无硬限制,给足余量

View File

@ -1,8 +1,9 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# zcbot 蓝绿无感部署(RUN.md §无感更新 B 档)。 # zcbot 蓝绿无感部署(RUN.md §无感更新 B 档)。
# #
# 拓扑:zcbot@blue(127.0.0.1:8765)/ zcbot@green(127.0.0.1:8766)双 systemd 实例, # 拓扑:zcbot@blue(127.0.0.1:8766)/ zcbot@green(127.0.0.1:8767)双 systemd 实例,
# nginx upstream(/etc/nginx/zcbot_upstream.conf,单行 server 指令)在中间切流量。 # nginx 对外监听原单实例端口 8765(客户端 / platform 嵌入无感),upstream
# (/etc/nginx/zcbot_upstream.conf,单行 server 指令)在两实例间切流量。
# #
# 干什么(顺序写死): # 干什么(顺序写死):
# 1) update.sh --prepare-only 拉码 / pip / db migration / docker build(复用单实例脚本) # 1) update.sh --prepare-only 拉码 / pip / db migration / docker build(复用单实例脚本)
@ -50,7 +51,7 @@ UPSTREAM_FILE="${UPSTREAM_FILE:-/etc/nginx/zcbot_upstream.conf}"
# 切流量后等旧实例在跑 run 自然收尾的上限(超了就 stop,残留 run 走协作式 cancel) # 切流量后等旧实例在跑 run 自然收尾的上限(超了就 stop,残留 run 走协作式 cancel)
DRAIN_WAIT_SECONDS="${DRAIN_WAIT_SECONDS:-600}" DRAIN_WAIT_SECONDS="${DRAIN_WAIT_SECONDS:-600}"
declare -A PORT=([blue]=8765 [green]=8766) declare -A PORT=([blue]=8766 [green]=8767)
log() { echo "[deploy] $*"; } log() { echo "[deploy] $*"; }
fail() { echo "[deploy] ERROR: $*" >&2; exit 1; } fail() { echo "[deploy] ERROR: $*" >&2; exit 1; }
@ -65,7 +66,7 @@ command -v nginx >/dev/null || fail "nginx 未安装(蓝绿依赖 nginx 反代,
[ -f "$UPSTREAM_FILE" ] || fail "$UPSTREAM_FILE(nginx upstream 未初始化,见 RUN.md §蓝绿部署 bootstrap)" [ -f "$UPSTREAM_FILE" ] || fail "$UPSTREAM_FILE(nginx upstream 未初始化,见 RUN.md §蓝绿部署 bootstrap)"
systemctl cat zcbot@blue >/dev/null 2>&1 || fail "模板 unit 未安装(sudo cp deploy/zcbot@.service /etc/systemd/system/ && daemon-reload)" systemctl cat zcbot@blue >/dev/null 2>&1 || fail "模板 unit 未安装(sudo cp deploy/zcbot@.service /etc/systemd/system/ && daemon-reload)"
if systemctl is-active --quiet zcbot 2>/dev/null; then if systemctl is-active --quiet zcbot 2>/dev/null; then
fail "旧单实例服务 zcbot.service 还在跑(占 8765 与 zcbot@blue 冲突)—— 先走 RUN.md §蓝绿部署 bootstrap 停掉它" fail "旧单实例服务 zcbot.service 还在跑(占 8765,与 nginx 对外监听冲突)—— 先走 RUN.md §蓝绿部署 bootstrap 停掉它"
fi fi
DB_URL="$(grep -E '^[[:space:]]*ZCBOT_DB_URL=' "$APP_DIR/.env" | tail -n1 | cut -d= -f2-)" DB_URL="$(grep -E '^[[:space:]]*ZCBOT_DB_URL=' "$APP_DIR/.env" | tail -n1 | cut -d= -f2-)"
[ -n "$DB_URL" ] || fail ".env 里没读到 ZCBOT_DB_URL" [ -n "$DB_URL" ] || fail ".env 里没读到 ZCBOT_DB_URL"

View File

@ -1,8 +1,9 @@
# zcbot 蓝绿双实例模板 unit(RUN.md §无感更新 B 档)。 # zcbot 蓝绿双实例模板 unit(RUN.md §无感更新 B 档)。
# 安装:sudo cp deploy/zcbot@.service /etc/systemd/system/ && sudo systemctl daemon-reload # 安装:sudo cp deploy/zcbot@.service /etc/systemd/system/ && sudo systemctl daemon-reload
# 实例:zcbot@blue(port 8765)/ zcbot@green(port 8766),端口与实例名由 # 实例:zcbot@blue(port 8766)/ zcbot@green(port 8767),端口与实例名由
# deploy/instances/<实例>.env 注入(ZCBOT_PORT / ZCBOT_INSTANCE)。 # deploy/instances/<实例>.env 注入(ZCBOT_PORT / ZCBOT_INSTANCE)。
# nginx upstream 在两实例间切流量,部署走 deploy/update_bluegreen.sh。 # 对外端口仍是 8765 —— 由 nginx 接管(客户端 / platform 嵌入无感),upstream 在
# 两实例间切流量,部署走 deploy/update_bluegreen.sh。
[Unit] [Unit]
Description=zcbot web (FastAPI/uvicorn) - instance %i Description=zcbot web (FastAPI/uvicorn) - instance %i
After=network-online.target postgresql.service After=network-online.target postgresql.service