# stacks/browser-server.yml — onesvm-browser-server Swarm stack(Contract §5-A5)。 # # 权威对齐:docs/deploy-prod-preset-20260901.md §2 服务清单/镜像/资源/端口(逐字段); # stack 必备字段对齐 onesvm-dev-md/base/deployment-rules.md §1.3 # (每服务 TZ / placement / restart_policy / resources.limits / healthcheck,backup 除外)。 # 测试期全钉 mgr1(placement node.hostname==swarm-mgr1);scheduler 因 SQLite 卷必须钉。 # # 密钥注入面(deploy.env 提供,禁默认值禁入 git): # BROWSER_SERVER_KEYS_SEED / BROWSER_SERVER_ADMIN_TOKEN / PROXY_SUB_URLS / SEARXNG_SECRET # 部署:docker stack deploy -c stacks/browser-server.yml --resolve-image never browser-server # 红线:本文件本轮仅交付,不执行部署(Contract §9)。 # # O13 镜像 tag 约定:自研镜像 onesvm/browser-server:dev / onesvm/trafilatura-http:dev # 日常迭代复用覆盖;第三方镜像 tag + digest 双锁(deployment-rules §2.3)。 version: "3.8" x-restart: &restart restart_policy: condition: on-failure delay: 5s max_attempts: 3 window: 60s x-placement-mgr1: &placement-mgr1 placement: constraints: - node.hostname==swarm-mgr1 services: # ---------- 控制层(自研三角色,单二进制 role 分流) ---------- gateway: image: onesvm/browser-server:dev command: ["-role=gateway"] environment: TZ: Asia/Shanghai BROWSER_SERVER_KEYS_SEED: ${BROWSER_SERVER_KEYS_SEED:?required} BROWSER_SERVER_ADMIN_TOKEN: ${BROWSER_SERVER_ADMIN_TOKEN:?required} BROWSER_SERVER_LISTEN_gateway: ":8640" BROWSER_SERVER_SCHEDULER_URL: http://scheduler:8641 # gateway 与 scheduler 共库(gateway 只读 consumers/api_keys,写 audit/quota)。 BROWSER_SERVER_DB_PATH: /data/browser-server.db volumes: - browser-server-data:/data # 与 scheduler 共库(gateway 只读写 consumers/api_keys/audit/quota; # SQLite 需目录可写以建 WAL 伴生文件,读写纪律由应用层单写者约定保证) ports: - "8640:8640" # host ingress(对外唯一入口;反代前缀 /bs-api) networks: # vlepontas-casa-net 附加(D10,对齐 WSG 先例);长格式显式声明 alias # browser-server(V3 arch-review D-2:compose 校验器不接受列表内映射, # 须用长格式;alias 防 vlepontas 消费方 DNS 解析失败) browser-server-net: aliases: [] casa-net: aliases: - browser-server deploy: replicas: 1 <<: *restart placement: constraints: - node.hostname==swarm-mgr1 # 测试期钉 mgr1(与共库卷同节点) resources: limits: memory: 64m cpus: "0.25" healthcheck: test: ["CMD", "/browser-server", "-healthz-probe"] # 见下方说明(distroless 无 curl/wget) interval: 30s timeout: 3s retries: 3 start_period: 10s scheduler: image: onesvm/browser-server:dev command: ["-role=scheduler"] environment: TZ: Asia/Shanghai BROWSER_SERVER_LISTEN_scheduler: ":8641" BROWSER_SERVER_DB_PATH: /data/browser-server.db BROWSER_SERVER_ADMIT_MAX: "60" BROWSER_SERVER_WORKERS: "8" BROWSER_SERVER_SEARXNG_CN_URL: http://searxng-cn:8080 BROWSER_SERVER_SEARXNG_GLOBAL_URL: http://searxng-global:8080 BROWSER_SERVER_TRAFILATURA_URL: http://trafilatura:8080 BROWSER_SERVER_LIGHTPANDA_CDP_HOST: lightpanda:9222 BROWSER_SERVER_SHELL_CDP_HOST: headless-shell:9222 BROWSER_SERVER_PROXYMANAGER_URL: http://proxymanager:8642 # MIHOMO 配置目录:proxymanager 写 provider 到挂载卷,mihomo 同卷读取。 BROWSER_SERVER_MIHOMO_CONFIG_DIR: /data/mihomo MIHOMO_CONTROLLER_URL: http://mihomo:19090 volumes: - browser-server-data:/data # named volume 钉 mgr1(SQLite 单写者,禁跨节点) networks: - browser-server-net deploy: replicas: 1 <<: *restart placement: constraints: - node.hostname==swarm-mgr1 resources: limits: memory: 64m cpus: "0.5" healthcheck: test: ["CMD", "/browser-server", "-role=scheduler", "-healthz-probe"] interval: 30s timeout: 3s retries: 3 start_period: 10s proxymanager: image: onesvm/browser-server:dev command: ["-role=proxymanager"] environment: TZ: Asia/Shanghai BROWSER_SERVER_LISTEN_proxymanager: ":8642" PROXY_SUB_URLS: ${PROXY_SUB_URLS:?required} # 订阅拉取专用 bootstrap(非密钥;缺省空=直连)。mgr1 直连机场被墙, # 复用 mgr3 central-proxy :7890(deploy-preset §1)。探活仍走自家 mihomo。 BROWSER_SERVER_SUB_FETCH_PROXY: ${BROWSER_SERVER_SUB_FETCH_PROXY:-} MIHOMO_CONTROLLER_URL: http://mihomo:19090 # provider 文件写到 mihomo 挂载卷(stack 把默认 /data/mihomo 指到 mihomo 同卷挂载点)。 BROWSER_SERVER_MIHOMO_CONFIG_DIR: /data/mihomo # 热载路径必须是 mihomo 容器内视角(同卷挂 /root/.config/mihomo)。 BROWSER_SERVER_MIHOMO_RELOAD_PATH: /root/.config/mihomo/config.yaml BROWSER_SERVER_DB_PATH: /data/browser-server.db volumes: - browser-server-data:/data # rules 表共享(gateway policy 联动) - browser-server-mihomo:/data/mihomo # mihomo provider 写入点(与 mihomo 服务同卷) networks: - browser-server-net deploy: replicas: 1 <<: *restart placement: constraints: - node.hostname==swarm-mgr1 resources: limits: memory: 32m cpus: "0.1" healthcheck: test: ["CMD", "/browser-server", "-role=proxymanager", "-healthz-probe"] interval: 30s timeout: 3s retries: 3 start_period: 10s # ---------- 数据面 ---------- mihomo: image: metacubex/mihomo:v1.19.24@sha256:9e37208fae8afa4c8b83d14ff2e9771b99178ebaf65a2c4fb388bc67ecefe4dc volumes: - browser-server-mihomo:/root/.config/mihomo # proxymanager 写 config.yaml/providers 到此(PM 侧 BROWSER_SERVER_MIHOMO_CONFIG_DIR=/data/mihomo 指同卷) networks: - browser-server-net deploy: replicas: 1 <<: *restart placement: constraints: - node.hostname==swarm-mgr1 resources: limits: # 69 节点 + 订阅自带 dns/geosite 会加载 geoip(64m 实测 OOM 137)。 # 档标仍按 L2 超配;瘦 dns 后可回 64m(O12 校准)。 memory: 128m healthcheck: # mihomo controller 19090 REST 探活(仅 overlay;busybox sh 无 /dev/tcp, # 用 busybox wget 打 /version——controller 无 secret 时返回 JSON)。 test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1:19090/version || exit 1"] interval: 30s timeout: 5s retries: 3 start_period: 15s # ---------- Worker 层(第三方/自构建引擎,全部 pin) ---------- searxng-cn: image: searxng/searxng:2026.8.29-d226b78bc@sha256:61ad36fcf71eb300f7d64216a6a04e010754755c7f5d33a526bd52e597e78c69 volumes: - ./searxng-cn-settings.yml:/etc/searxng/settings.yml:ro environment: TZ: Asia/Shanghai SEARXNG_BASE_URL: http://searxng-cn:8080/ networks: - browser-server-net deploy: replicas: 1 <<: *restart placement: constraints: - node.hostname==swarm-mgr1 resources: limits: memory: 192m healthcheck: test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:8080/healthz"] interval: 30s timeout: 5s retries: 3 start_period: 20s searxng-global: image: searxng/searxng:2026.8.29-d226b78bc@sha256:61ad36fcf71eb300f7d64216a6a04e010754755c7f5d33a526bd52e597e78c69 volumes: - ./searxng-global-settings.yml:/etc/searxng/settings.yml:ro environment: TZ: Asia/Shanghai SEARXNG_BASE_URL: http://searxng-global:8080/ networks: - browser-server-net deploy: replicas: 1 <<: *restart placement: constraints: - node.hostname==swarm-mgr1 resources: limits: memory: 192m healthcheck: test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:8080/healthz"] interval: 30s timeout: 5s retries: 3 start_period: 20s trafilatura: build: context: ./trafilatura image: onesvm/trafilatura-http:dev environment: TZ: Asia/Shanghai networks: - browser-server-net deploy: replicas: 1 <<: *restart placement: constraints: - node.hostname==swarm-mgr1 resources: limits: memory: 64m healthcheck: test: ["CMD", "python", "-c", "import urllib.request;urllib.request.urlopen('http://127.0.0.1:8080/health', timeout=3)"] interval: 30s timeout: 5s retries: 3 start_period: 15s lightpanda: image: lightpanda/browser:0.3.7@sha256:447598e4c103b70b0fb610813e72795da44341c564e1ec872c3a490c1bc6aebe command: ["/bin/lightpanda", "serve", "--host", "0.0.0.0", "--port", "9222", "--log_level", "info"] environment: TZ: Asia/Shanghai # 境外出口统一走 mihomo(对齐 bench compose-lightpanda-proxy.yml 的 proxy env; # 域名级直连/代理由 mihomo 规则裁决)。 HTTP_PROXY: http://mihomo:17890 HTTPS_PROXY: http://mihomo:17890 http_proxy: http://mihomo:17890 https_proxy: http://mihomo:17890 NO_PROXY: 127.0.0.1,localhost no_proxy: 127.0.0.1,localhost networks: - browser-server-net deploy: replicas: 1 <<: *restart placement: constraints: - node.hostname==swarm-mgr1 resources: limits: memory: 320m # lightpanda 镜像内无 curl/wget/sh 工具链(distroless 风格),CDP 探活由 # scheduler 适配器 Health 上报承担(healthcheck 缺省即 deployment-rules 例外,注明)。 headless-shell: image: chromedp/headless-shell:151.0.7922.109@sha256:5f877a2a559dea1a99fb750da695d28a020cdd49db660aead6c78a46e3c7dd50 command: - --disable-dev-shm-usage # 境外出口统一走 mihomo(对齐 bench compose:--proxy-server=http://mihomo:17890)。 - --proxy-server=http://mihomo:17890 # ENTRYPOINT /headless-shell/run.sh 已带 --no-sandbox、--remote-debugging-port=9223 # 与 socat 9222→9223 转发(bench compose 实测复用),不得覆盖端口。 shm_size: 128m networks: - browser-server-net deploy: replicas: 0 # 按需槽:平时 0,scheduler 经 Swarm API 拉起(空闲 10min 回收) <<: *restart placement: constraints: - node.hostname==swarm-mgr1 resources: limits: memory: 400m healthcheck: # headless-shell 基于 debian + socat(有 shell),探 CDP 调试端口。 test: ["CMD", "sh", "-c", "exec 3<>/dev/tcp/127.0.0.1/9222 || exit 1"] interval: 30s timeout: 5s retries: 3 start_period: 10s # ---------- 备份 sidecar(对齐组织 NAS sidecar 惯例) ---------- backup: # 简化实现:每日 01:00(Asia/Shanghai)sqlite3 .backup → tar 到 /backup 卷。 # 说明:alpine 基础镜像无 sqlite3 包,这里用 busybox sleep 循环 + cp 快照 #(WAL 模式下 cp 非原子;严格一致性备份轮换 sqlite3/amd64 镜像后升级 .backup)。 image: alpine:3.20@sha256:c64c687cbea9300178b30c95835354e34c4e4febc4badfe27102879de0483b5e command: - /bin/sh - -c - | # 每日 01:00 Asia/Shanghai 备份(TZ 已设 Asia/Shanghai): # 1) sleep 到次日 01:00;2) cp 数据库(WAL 模式 + checkpoint 后主库自洽); # 3) tar 打包进 /backup,保留最近 14 份。 while true; do now=$$(date +%s); target=$$(date -d "tomorrow 01:00" +%s 2>/dev/null || echo $$((now + 86400))); sleep $$((target - now)); ts=$$(date +%Y%m%d-%H%M%S); cp /data/browser-server.db /data/browser-server.db-wal /tmp/ 2>/dev/null || true; tar -czf /backup/browser-server-$$ts.tar.gz -C /data browser-server.db browser-server.db-wal browser-server.db-shm 2>/dev/null \ || tar -czf /backup/browser-server-$$ts.tar.gz -C /data browser-server.db; ls -1t /backup/browser-server-*.tar.gz | tail -n +15 | xargs -r rm -f; done environment: TZ: Asia/Shanghai volumes: - browser-server-data:/data:ro - browser-server-backup:/backup networks: - browser-server-net deploy: replicas: 1 <<: *restart placement: constraints: - node.hostname==swarm-mgr1 resources: limits: memory: 32m # 备份 sidecar 无对外服务,healthcheck 缺省(deployment-rules §1.3 例外,对齐 pg 备份 sidecar 惯例)。 # ---------- 网络 / 卷 ---------- networks: browser-server-net: driver: overlay name: browser-server-net casa-net: external: true name: vlepontas-casa-prod-net volumes: browser-server-data: {} # SQLite 库(scheduler 单写者;gateway 只读;钉 mgr1 由 placement 保证) browser-server-mihomo: {} # mihomo 配置/provider(PM 写、mihomo 读) browser-server-backup: {} # 备份产物(NAS 侧同步由部署轮另议)