# 抓取与有状态端部署 # # 默认起抓取服务 scraping(:31107)与交易服务 trading(:31108): # docker compose up -d # 生产环境仍可按部署位置分别启动:服务器侧 `docker compose up -d scraping`, # 本地机侧 `docker compose up -d trading`。抓取服务匿名无状态,可独立多开。 # 下单任务网关按设计部署在**服务器侧**(与抓取服务同机),是另一个部署单元, # 所以放在 gateway profile 里,不会被默认启动。真要在这台机上一起跑(自包含 # 联调 / 单机部署)时: # docker compose --profile gateway up -d # # 两个服务都**只能单实例**:登录态 cookie 全局唯一、订单监控是常驻轮询、SQLite # 单连接 + 全局并发度 1。不要 `--scale`,不要在前面挂多副本。 # # 部署前置(缺一样服务就跑不通,不是可选项): # 1. cp .env.example .env,至少填 RAKUTEN_BEARER_TOKEN / RAKUTEN_ORDER_GATEWAY_URL / # RAKUTEN_SCRAPER_BASE_URL(后两个留空则 worker 不启动,只剩购物车与登录态接口)。 # 2. cp account.yaml.example account.yaml 并填真实凭据(session upgrade 复核密码、 # 手机号补录、支付方式核对都要读它)。**必须先建文件再 up**,否则 Docker 会 # 把这个挂载点当目录创建,容器里读到的是个空目录。 # 3. 登录态 cookie(.auth/rakuten_state.json):**不必**先在宿主机准备—— # RAKUTEN_AUTO_LOGIN_ON_START 默认开,容器起来就按 account.yaml 自己登一次。 # 撞 reCAPTCHA / 设备验证会停在浏览器里等人工,这时把 RAKUTEN_VNC_ENABLED 设成 # true(配 RAKUTEN_VNC_PASSWORD)连 127.0.0.1:5900 接管,完成后: # curl -X POST -H "Authorization: Bearer $TOKEN" http://127.0.0.1:31108/api/auth/login # 也可以照旧在宿主机跑 scripts/login.py 再挂载进来,两条路都行。 # # 敏感面:.auth/(可直接冒充账号的 cookie)、account.yaml(明文密码+卡号)、 # data/(真实姓名地址等 PII 与证据快照)全在宿主机目录里,注意宿主机权限与备份加密。 services: scraping: # 商品获取 API:乐天市场与ラクマ抓取,匿名无状态,可独立扩容。 image: ${RAKUTEN_SCRAPING_IMAGE:-git.jerryyan.net/jp/rakuten-api}:${RAKUTEN_SCRAPING_TAG:-latest} build: context: . dockerfile: Dockerfile container_name: rakuten-api restart: unless-stopped env_file: - .env environment: RAKUTEN_APP_HOST: 0.0.0.0 RAKUTEN_APP_PORT: 31107 RAKUTEN_APP_ENV: prod ports: # 默认仅供本机反向代理访问;需要直连时显式设 RAKUTEN_SCRAPING_BIND。 - "${RAKUTEN_SCRAPING_BIND:-127.0.0.1}:31107:31107" volumes: - ./logs:/app/logs logging: driver: json-file options: max-size: "50m" max-file: "5" trading: # 交易服务:加购 → 下单 → 付款 → 订单监控,以及 /api/auth/* /api/cart/* image: ${RAKUTEN_TRADING_IMAGE:-git.jerryyan.net/jp/rakuten-trading}:${RAKUTEN_TRADING_TAG:-latest} build: context: . dockerfile: Dockerfile.trading container_name: rakuten-trading restart: unless-stopped # PID 1 用 docker-init:Chromium 会派生一堆子进程,崩溃时需要有人收尸 init: true # Chromium 默认 /dev/shm 只有 64MB,渲染稍重的结算页会直接 crash shm_size: 1gb # 停容器时给正在执行的下单任务留出收尾时间(uvicorn 关 lifespan → worker 停轮询 # → 取消订单监控 → 关浏览器)。**停容器前最好先确认没有在途任务**:进程被硬杀 # 时站点侧可能已经提交成功,本地没记录,恢复要走 verify_on_site 人工核对。 stop_grace_period: 120s env_file: - .env environment: # 容器内必须监听 0.0.0.0,对外只从 127.0.0.1 映射(见 ports) RAKUTEN_TRADING_HOST: 0.0.0.0 RAKUTEN_TRADING_PORT: 31108 RAKUTEN_HEALTH_PORT: 31108 RAKUTEN_APP_ENV: prod # worker 标识:默认取主机名,容器主机名是随机 ID,重建就变——显式固定, # 便于在网关侧对上是哪台机在领任务 RAKUTEN_WORKER_ID: ${RAKUTEN_WORKER_ID:-rakuten-trading-docker} # 有头 Chromium 需要虚拟显示(镜像内 Xvfb),别关 RAKUTEN_XVFB_ENABLED: "true" # 启动即按 account.yaml 自动登录(后台跑,不阻塞端口)。撞 reCAPTCHA / 设备验证 # 时会停在浏览器里等人工——那时开 VNC 接管,或事后调 POST /api/auth/login 重试。 RAKUTEN_AUTO_LOGIN_ON_START: ${RAKUTEN_AUTO_LOGIN_ON_START:-true} RAKUTEN_RELOGIN_ENABLED: ${RAKUTEN_RELOGIN_ENABLED:-true} # 需要远程盯着浏览器(登录验证码、3DS、needs_human 人工接管)时改成 true, # 并设 RAKUTEN_VNC_PASSWORD;端口只映射到 127.0.0.1,走 SSH 隧道访问 RAKUTEN_VNC_ENABLED: ${RAKUTEN_VNC_ENABLED:-false} RAKUTEN_VNC_PASSWORD: ${RAKUTEN_VNC_PASSWORD:-} ports: # 这些接口能操作真实账号,只对本机开放;要跨机访问请走内网地址或 SSH 隧道 - "127.0.0.1:31108:31108" - "127.0.0.1:5900:5900" volumes: # 登录态 cookie:容器要能写(自动重登会重写 storage_state) - ./.auth:/app/.auth # 账号凭据:只读挂载。文件必须先存在,见文件头「部署前置」第 2 条 - ./account.yaml:/app/account.yaml:ro # login.py 的持久化浏览器目录(保住设备指纹,减少重登触发风控核验) - ./.browser-data:/app/.browser-data # 订单 SQLite(执行事实的权威记录)+ 证据快照,丢了没法追溯下过什么单 - ./data:/app/data - ./logs:/app/logs logging: driver: json-file options: max-size: "50m" max-file: "5" # Chromium 的 namespace sandbox 在容器里可能被 seccomp/AppArmor 挡住(表现为 # 浏览器起不来、Playwright 报 Target closed)。宿主机内核不允许非特权 user # namespace 时,按下面两条择一放行;两条都不想放,则需要在 # site_interact.py 的 launch(args=...) 里加 --no-sandbox(改生产代码,需另行决定)。 # security_opt: # - seccomp:unconfined # cap_add: # - SYS_ADMIN gateway: # 下单任务网关:任务队列 + 状态镜像,上游业务系统在这里提交下单任务与查任务状态。 # 正式拓扑里它在服务器侧、trading 在本地,两者靠出站长轮询联通(docs/order-gateway.md); # 这里只是为了单机自包含跑起来,默认不启动。 profiles: ["gateway"] image: ${RAKUTEN_TRADING_IMAGE:-git.jerryyan.net/jp/rakuten-trading}:${RAKUTEN_TRADING_TAG:-latest} build: context: . dockerfile: Dockerfile.trading container_name: rakuten-gateway restart: unless-stopped init: true command: ["python", "-m", "app.gateway.main"] env_file: - .env environment: RAKUTEN_GATEWAY_HOST: 0.0.0.0 RAKUTEN_GATEWAY_PORT: 31109 RAKUTEN_HEALTH_PORT: 31109 RAKUTEN_APP_ENV: prod # 网关不碰浏览器,跳过 Xvfb RAKUTEN_XVFB_ENABLED: "false" ports: # 上游业务系统要能调到;对外暴露时请在反代上收紧来源,鉴权只有一个 Bearer token - "${RAKUTEN_GATEWAY_BIND:-127.0.0.1}:31109:31109" volumes: # 任务队列 SQLite(data/gateway.db)必须持久化,丢了等于丢一批下单任务 - ./data:/app/data - ./logs:/app/logs logging: driver: json-file options: max-size: "50m" max-file: "5"