Expo自托管OTA更新与可观测性方案:Xprem部署实践
这次看一个针对 Expo 应用打造的开发者基础设施项目Xprem。它的定位在标题里写得很明确——Self-hosted OTA updates and observability for Expo apps。一句话说清楚帮助 React Native 团队把 Expo 应用的 OTA 更新分发和服务端可观测能力整体迁移到自己可控的服务器上。如果你已经在用 Expo 生态做 App大概率用过 EAS Update。托管服务体验顺滑但更新包、设备上报、发布记录全部落在第三方服务器。遇到合规要求、内网部署、离线环境或者只是想让发布流程和公司内部系统打通时就需要一个自托管方案。Xprem 解决问题的思路就是这套自建 OTA 更新服务 可观测大盘。这篇文章会先给一个快速判断表再展开讲部署方式、Expo 应用怎么接入、更新 API 和批量任务怎么设计、可观测性要看哪些指标最后给出常见问题排查和工程化建议。适合正在评估 Expo 自托管 OTA 方案的技术负责人、移动端开发以及维护内部 CI/CD 基础设施的工程师。1. 核心能力速览能力项说明项目类型自托管 OTA 更新服务 可观测性平台服务对象基于 Expo / React Native 构建的 Android、iOS 应用核心功能更新包上传与分发、更新发布记录、更新状态追踪、设备运行观测部署形态服务端应用可通过 Docker 或 Node.js 进程部署数据存储需要数据库存储发布记录和元数据常见方案是 PostgreSQL轻量场景可用 SQLite文件存储更新包资源需要落盘或接入对象存储如 MinIO / S3客户端接入基于expo-updates配置自定义更新服务器 URLAPI 能力支持上传、查询、发布、回滚等操作具体接口路径以项目 README 为准批量任务支持按 channel 分批发布、灰度比例控制、定时发布任务可观测性关注更新安装成功率、回滚率、版本分布、活跃设备等指标适用读者有 Expo 应用生产经验、需要自建更新基础设施的团队需要特别说明该项目是社区发布的自托管项目具体内置功能、版本号、接口字段可能会随迭代变化。本文以通用 Expo OTA 自托管方案为骨架展开实际部署时以仓库 README 为准。2. 适用场景与使用边界2.1 适合谁用第一类是有数据合规要求的团队。App 的更新包内容、设备版本分布、更新是否成功这些数据如果跑在自己的服务器上审计和合规都更容易讲清楚。第二类是更新频率高、需要精细控制发布节奏的团队。托管服务能覆盖大多数发布场景但自托管方案更容易和公司已有的发布平台、CI/CD、权限系统打通。第三类是网络或基础设施环境受限的团队。比如需要在内网部署、没有公网出口、或者对第三方服务的可用性有顾虑。2.2 解决什么问题更新包不再依赖第三方云服务发布和存储都在自己控制范围内。可以拿到更完整的更新链路数据谁在用什么版本、更新包是否下发成功、启动后有没有崩溃回退。发布策略更灵活可以按环境、平台、渠道、用户比例分批放量。可以对接内部的告警系统更新失败能第一时间发现。2.3 不适合什么场景如果团队还没有稳定使用 Expo 的更新机制不建议一上来就自建 OTA 服务。先把 Expo 官方托管流程跑通理解 manifest、channel、runtimeVersion 这些概念之后再迁移到自托管会更稳妥。如果只是几个人内部测试的小应用自托管服务的运维成本可能会高于收益。托管服务Expo 开发者工具前期效率更高。2.4 使用边界与合规提醒OTA 更新能力必须用来做正常的应用版本修复和功能迭代不能用来绕过应用商店审核也不能通过更新通道推送违反公序良俗、侵犯第三方权益的内容。涉及用户设备数据采集时要遵循隐私合规要求对设备标识做匿名化处理。本文所有操作建议仅限在具备合法授权的环境和技术测试环境中执行。3. 为什么 Expo 应用需要自托管 OTA 更新3.1 expo-updates 的工作原理Expo 应用的 OTA 更新核心是expo-updates这个客户端库。App 启动时会向配置好的更新服务请求一份 manifest里面包含 bundle 的下载地址、版本信息、runtimeVersion 等信息。客户端拿到 manifest 后和本地已有的 bundle 做比对如果发现新版本且匹配当前 runtimeVersion就会下载新 bundle在下次启动时加载新内容。这个机制让 App 不需要重新走应用商店审核就可以完成 JS 层面的更新。对 React Native Expo 团队来说这是很大的效率提升。3.2 官方托管服务的局限Expo 官方 EAS Update 默认提供的是托管更新服务。托管服务的好处是开箱即用上传、发布、渠道管理都在 Expo 平台完成。但长期使用会碰到几个实际问题更新包和发布记录存在 Expo 云端数据不落在自己手里。发布管理和公司内部权限体系有割裂团队成员需要额外申请 Expo 账号权限。若遇到网络出口限制客户端拉取更新包可能不稳定。自定义数据结构、告警规则、审计日志这些需求托管平台很难完全覆盖。自托管方案的价值正是解决这些边界问题。数据落地、发布可控、可观测性指标可以按自己需要扩展。3.3 自托管不是只跑一个静态服务有人以为自托管 OTA 只是把更新包放到一个静态目录里让客户端去下载。实际上没那么简单。一个完整可用的自托管更新服务至少要处理这些事保存每个更新包的内容和元数据生成符合expo-updates期望的 manifest。按 channel 分发不同版本的更新支持灰度发布。在客户端设备上记录更新结果包括成功、失败、回滚。提供查询 API让内部系统能看到当前发布状态。考虑更新包签名、鉴权、HTTPS 等安全问题。Xprem 这类项目要解决的就是把上面这些能力整体打包让团队可以自己部署、自己运维、自己看数据。4. 环境准备与前置条件4.1 服务器基本配置自托管 OTA 服务的负载不高更新资源下载会比较吃网络带宽但 CPU 和内存压力不大。测试环境可以用 1C2G 起步生产环境建议 2C4G 或更高。如果要对大量设备同时下发更新包需要重点关注出网带宽、对象存储的带宽和 CDN 配置。4.2 软件依赖部署前先确认以下组件操作系统推荐 Ubuntu 22.04 / Debian 12 这类 LTS 系统。Docker 和 Docker Compose如果使用容器化部署建议 Docker 20.10。Node.js如果选择非 Docker 部署推荐 Node.js 18 以上。数据库PostgreSQL 14 以上轻量测试也可以用 SQLite。对象存储MinIO、AWS S3 或本地磁盘目录用于保存更新包压缩包。反向代理Nginx 或 Caddy用于 HTTPS 终结和转发。4.3 域名与 HTTPSOTA 更新服务必须使用 HTTPS。expo-updates在非开发模式下默认会拒绝明文 HTTP 的 manifest 请求。iOS 的 ATS 策略也会限制 HTTP 访问。所以建议在部署前先准备好一个域名并申请好 TLS 证书。开发调试阶段如果确实没有公网域名可以用内网域名自签名证书做验证但要注意在客户端配置里做对应的信任处理。4.4 端口规划常见端口规划端口用途80HTTP 重定向到 HTTPS443HTTPS 入口8080应用服务内部端口由 Nginx 反向代理到域名避免使用默认的 PostgreSQL 端口和对象存储端口冲突数据库和管理界面不要暴露到公网。5. 安装部署与启动服务5.1 Docker Compose 部署模板如果项目提供了 Docker 镜像推荐用 Compose 一键编排。下面是一个通用模板实际镜像名、环境变量名需要按 Xprem 的 README 替换version: 3.9 services: db: image: postgres:16-alpine environment: POSTGRES_USER: xprem POSTGRES_PASSWORD: change-me POSTGRES_DB: xprem volumes: - db_data:/var/lib/postgresql/data healthcheck: test: [CMD-SHELL, pg_isready -U xprem] interval: 5s timeout: 5s retries: 5 xprem: image: your-registry/xprem:latest depends_on: db: condition: service_healthy ports: - 8080:8080 env_file: - .env volumes: - ./data/uploads:/app/uploads - ./config:/app/config volumes: db_data:5.2 环境变量示例以下.env是常见自托管服务的配置模板DATABASE_URLpostgres://xprem:change-medb:5432/xprem JWT_SECRETreplace-with-a-long-random-string UPLOAD_DIR/app/uploads PUBLIC_BASE_URLhttps://ota.example.com PORT8080 STORAGE_BACKENDlocal MINIO_ENDPOINT MINIO_ACCESS_KEY MINIO_SECRET_KEY字段说明PUBLIC_BASE_URL是客户端实际请求的地址一定要写成正式的 HTTPS 地址。STORAGE_BACKEND用于切换本地磁盘和对象存储。具体变量名以项目文档为准。5.3 验证服务启动启动后先做几个判断检查容器日志看数据库迁移是否执行成功。访问https://ota.example.com/health之类的健康检查接口。用 curl 确认服务能正常返回 JSON 响应。curl https://ota.example.com/health如果返回 200并且能看到服务名和版本信息说明服务主体已经跑起来了。5.4 反向代理配置生产环境建议用 Nginx 统一收口server { listen 443 ssl; server_name ota.example.com; ssl_certificate /etc/ssl/certs/example.crt; ssl_certificate_key /etc/ssl/private/example.key; location / { proxy_pass http://127.0.0.1:8080; 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 300s; client_max_body_size 200m; } }client_max_body_size要适当调大否则上传较大的更新包时会被 Nginx 拦截。6. Expo 应用接入与测试更新6.1 安装 expo-updates在 Expo 项目里安装客户端依赖npx expo install expo-updates安装完成后需要在app.json或app.config.js中配置更新服务器地址。核心字段如下{ expo: { updates: { url: https://ota.example.com/api/manifest, enabled: true, fallbackToCacheTimeout: 30000 } } }url是客户端拉取 manifest 的地址必须和自托管服务提供的接口路径一致。fallbackToCacheTimeout表示启动时等待更新响应的超时时间不想阻塞启动可以调小。6.2 使用 development build 测试这里有个重要建议测试自定义 OTA 服务时不要用 Expo Go建议打一个 development build 来验证。Expo Go 的更新加载逻辑和正式安装包有差异无法完全模拟生产环境下的行为。npx expo run:android # 或 npx expo run:ios打出的 development build 会读取app.json中的更新地址启动时向自托管服务请求 manifest。6.3 构建并上传更新包Expo 项目可以通过以下命令导出更新 bundlenpx expo export --platform android --output-dir distdist目录里就是一次更新需要的完整静态资源。需要把dist目录打包成 zip 或按协议上传到 Xprem由它生成对应的 manifest 记录。上传流程一般有两种直接在 Xprem 的 Web 控制台里传 zip 包填写版本号、channel、描述信息。调用 API 上传写入自己的 CI/CD 发布脚本。6.4 客户端更新流程正常情况下客户端下次启动时会触发更新检查。如果 manifest 中的 runtimeVersion 和 App 原生代码匹配客户端就会下载新 bundle然后在下下次启动时加载新版本。可以在 App 里加一个版本号显示方便验证更新是否生效。如果服务端更新了 bundle 但客户端始终没有拉取到优先检查manifest 接口是否返回正常。runtimeVersion 是否匹配。channel 是否一致。7. 接口 API 与批量更新任务7.1 接口设计思路自托管 OTA 服务面向内部系统和 CI/CD通常会提供这些类型的接口上传更新包。创建发布任务。查询更新状态。回滚到指定版本。拉取统计数据。下面的 curl 示例是一个通用模板实际接口路径和字段名需要按 Xprem 的 API 文档调整curl -X POST https://ota.example.com/api/v1/deployments \ -H Authorization: Bearer $XPREM_TOKEN \ -H Content-Type: application/json \ -d { project: com.example.app, platform: android, release_channel: production, bundle_id: 20250401123000, rollout_percentage: 10 }7.2 批量发布任务批量更新任务在移动端 OTA 场景下通常不等于一次性把所有人全量推完而是一批一批放量。常见做法先在 staging channel 发布给测试人员验证。然后在 production channel 发布灰度比例从 5% 开始。观察一段时间升级率和崩溃率确认稳定后逐渐升到 25%、50%、100%。Python 脚本可以轮询发布任务状态import time import requests base_url https://ota.example.com token your-api-token headers {Authorization: fBearer {token}} payload { project: com.example.app, platform: ios, release_channel: production, bundle_id: 20250401150000, rollout_percentage: 5, } r requests.post( f{base_url}/api/v1/deployments, jsonpayload, headersheaders, timeout30, ) r.raise_for_status() deployment_id r.json()[deployment_id] for _ in range(30): resp requests.get( f{base_url}/api/v1/deployments/{deployment_id}, headersheaders, timeout30, ) data resp.json() print(status:, data[status], progress:, data.get(progress)) if data[status] completed: break time.sleep(10)7.3 回滚策略OTA 更新最怕的是新版本引入严重崩溃。自托管服务要有一个高优先级的回滚接口遇到问题能快速让设备回到上一个可用版本。回滚操作在工作原理上是一次新的 manifest 更新把客户端指向旧的 bundle。所以回滚动作本身也必须走发布流程保留发布记录和操作人信息。要避免直接删掉旧 bundle。一旦旧版本被删设备就无法通过 manifest 找到可用更新只能回退到内置 bundle体验会更差。8. 可观测性指标与查询8.1 应该关注哪些指标OTA 更新的可观测性重点不是看服务器 CPU 和内存而是看更新链路是否健康。建议关注以下指标指标说明manifest 请求量有多少设备在启动时请求了更新更新包下载量有多少设备实际下载了新 bundle安装成功率下载后新 bundle 是否能正常加载回滚率新 bundle 启动后崩溃客户端自动回退到旧版本版本分布当前活跃设备都运行在哪些版本上渠道分布production / staging / 各渠道的更新覆盖情况发布任务耗时从上传到全量生效花费了多少时间8.2 通过 API 查询如果项目提供了统计查询接口可以按 project、channel、时间范围去拉数据curl -G https://ota.example.com/api/v1/analytics/installations \ -H Authorization: Bearer $XPREM_TOKEN \ --data-urlencode projectcom.example.app \ --data-urlencode channelproduction \ --data-urlencode from2025-04-01T00:00:00Z \ --data-urlencode to2025-04-02T00:00:00Z8.3 可观测大盘的工程化建议自托管服务的数据是一次性基础设施建议把它接入现有的监控体系将回滚率超过阈值作为告警项接企业微信、钉钉或 Slack。将发布任务长时间未完成作为 SLO 指标每周复盘。和崩溃监控平台联动例如 Sentry查看新版本 bundle 是不是引入了明显崩溃。这样Xprem 不再是孤立的工具而是整个移动端发布链路的一部分。9. 安全与合规注意事项9.1 HTTPS 必须配好前面反复强调 HTTPS这里再给一个检查清单域名证书没有过期。证书链完整。服务内部端口不直接暴露公网。反向代理只开放 443。9.2 访问鉴权上传、发布、回滚、删除这些接口必须做鉴权。内部使用的 token 要有独立的生命周期管理不要用同一个密钥处理所有环境。尽量做到开发环境的 token 和生产环境的 token 隔离。token 只具备必要权限不用给全部接口权限。定期轮换。9.3 更新包完整性OTA 场景下客户端下载的 bundle 必须能校验完整性和来源。更新包在服务端存储时要计算 hash客户端加载前可以对 bundle 做完整性校验。如果项目支持更新包签名应该开启签名校验防止中间人篡改。9.4 设备数据隐私可观测性需要收集设备信息但不能无限采集。建议只收集匿名化的设备标识、平台、系统版本、App 版本、channel、更新结果。不要收集定位、通讯录、行为轨迹等与更新本身无关的信息。数据存储和日志策略要符合相关隐私法规。10. 常见问题与排查方法问题现象可能原因排查方式解决方案客户端一直提示更新失败HTTPS 配置不正确或证书不受信任用 curl 模拟客户端请求 manifest停止用 HTTP更换为受信任的 HTTPS 域名和证书manifest 返回 404项目名、channel、platform 参数不一致查看服务端日志确认客户端请求路径使用与上传时完全一致的 channel 和 platform更新包下载失败更新包过大或带宽不足抓包看下载是否中途断开优化更新包体积接入对象存储或 CDN新版本启动后崩溃并回滚runtimeVersion 不匹配或 JS 代码异常查看崩溃日志和回滚记录回滚到上一个稳定版本修复后再重新发布上传更新包超时Nginx 或应用服务限制上传大小检查 Nginxclient_max_body_size和代理超时调大上传限制改用对象存储直传数据库连接失败Docker Compose 服务启动顺序问题检查依赖服务和环境变量在 Compose 中配置 healthcheck 和 depends_on设备始终使用旧版本客户端缓存或 channel 不一致检查客户端配置和服务端发布记录确认 channel 一致必要时清理客户端缓存回滚操作没有生效旧 bundle 已被删除检查存储目录和发布记录保留至少一个历史版本回滚通过发布流程完成11. 最佳实践与使用建议11.1 先小范围跑通再全量第一次接入自托管 OTA不要直接全量覆盖生产用户。先用一个内部测试应用把上传更新包 - 发布到 staging - 客户端拉取 - 验证成功 - 观察回滚率这个链路全部打通再逐步向 production 推进。11.2 保存一套最小可用配置服务器、数据库、对象存储、反向代理、客户端配置建议整理成一套可复现的模板。新环境部署时直接套模板减少人工操作带来的配置漂移。11.3 目录和发布记录管理更新包文件、日志、配置文件、数据库元数据要分开存放定期备份。发布记录保留操作人、时间、变更描述、渠道、灰度比例方便做追溯。11.4 灰度发布是底线任何一次生产 OTA 发布都建议从 5% 灰度开始。给发布设置观察窗口看安装成功率和回滚率。11.5 回滚预案要提前写不要等线上出问题再想回滚。发布前就确认好回滚接口是否可用、操作人是谁、回滚目标版本是哪个、回滚后是否需要重新发布。可以定期做一次回滚演练。11.6 合规检查发布内容要确保不包含侵犯版权的内容不绕过应用商店政策不采集无关隐私数据。涉及人脸、声音、版权素材的应用必须在合法授权前提下再走 OTA 更新。12. 总结与下一步Xprem 这类项目最值得尝试的点是把 Expo 应用的 OTA 更新基础设施拉到自托管边界内。第一个要验证的功能不是控制台界面好不好看而是完整跑通一次上传更新包 - 配置 channel - 客户端拉取 - 设备回传结果的链路。最容易踩的坑是 HTTPS 没配好和 channel/runtimeVersion 不一致这两个问题承担了绝大多数更新失败排查。如果验证顺利下一步可以考虑把发布 API 接入自己的 CI/CD结合内部告警把回滚率指标监控起来。如果只是抱着尝鲜的心态也可以先用 Docker 部署一套测试环境用一个小项目验证 manifest 拉取和更新分发是否正常。对正在评估自托管 Expo OTA 方案的团队来说这篇文章可以作为一张搭建和排查踩坑地图建议收藏备用。动手之前有一个建议是始终有效的第一次部署先小流量、小范围、小步骤验证不要直接打全量。