工具大全
开发者工具2026年8月5日6 次阅读约 3 分钟阅读

GitHub Actions 秒挂 connection reset:境外 runner 拉阿里云 ACR 的跨境陷阱

一、问题描述

一次很平常的 push 触发了 Docker 镜像构建,GitHub Actions 启动约 2 秒后直接失败:

ERROR: failed to authorize: failed to fetch oauth token:
Post "https://dockerauth.cn-hangzhou.aliyuncs.com/auth": read: connection reset by peer

日志里报错位置指向 Dockerfile 的这一行:

FROM registry.cn-hangzhou.aliyuncs.com/<namespace>/node:22-pm2

第一反应很容易跑偏:是不是基础镜像 tag 写错了?是不是 ACR 密码过期了?是不是刚改的 Dockerfile 有问题?

都不是。判据只有一条:同一份 Dockerfile、同一套凭证,前一次构建成功,后一次失败,中间没有任何相关改动。 代码没变而结果变了,问题就不在代码。

二、环境

项目 详情
CI GitHub Actions,GitHub 托管 runner(境外机房)
构建 docker/build-push-action,multi-platform(amd64 + arm64)
镜像仓库 阿里云 ACR(杭州 region)
基础镜像 自定义 node:22-pm2,也存放在同一 ACR

三、根因

链路拆开看:

  1. GitHub 托管 runner 在境外
  2. FROM 一个 ACR 私有镜像,buildx 在 "load metadata" 阶段要先向 dockerauth.cn-hangzhou.aliyuncs.com 拿 OAuth token
  3. 这条境外 → 阿里云杭州的跨境 TCP 链路质量不稳定,偶发被中间设备直接 RST——表现就是 connection reset by peer

所以这是网络层的偶发故障,不是鉴权配置问题(鉴权错误会返回 401/403 的 HTTP 响应,而不是 TCP 连接被重置),更不是 Dockerfile 问题(报错指向 FROM 行只是因为失败发生在解析该行的元数据阶段)。

一条值得记住的诊断口诀

先看失败发生在第几秒。秒级失败 = 网络 / 鉴权链路;分钟级失败才可能是编译 / 依赖问题。

2 秒就挂,连依赖都还没开始装,把时间花在检查 npm 包或代码上是南辕北辙。

四、修复:workflow 里加一次自动重试

跨境链路的偶发 RST 无法从我们这端消除,但它的特点是重试大概率成功。改造 build 步骤:

- name: Build and push
  id: build
  continue-on-error: true        # 第一次失败不终止 job
  uses: docker/build-push-action@v6
  with:
    context: .
    platforms: linux/amd64,linux/arm64
    push: true
    tags: ${{ env.IMAGE_TAGS }}

- name: Wait before retry
  if: steps.build.outcome == 'failure'
  run: sleep 30                  # 给链路一点恢复时间

- name: Build and push (retry)
  if: steps.build.outcome == 'failure'
  uses: docker/build-push-action@v6
  with:                          # ⚠️ 参数必须与上面逐字一致
    context: .
    platforms: linux/amd64,linux/arm64
    push: true
    tags: ${{ env.IMAGE_TAGS }}

两个实操注意点:

  • GitHub Actions 的 YAML 不支持锚点&anchor / *ref),两个步骤的 with 块没法复用,只能手工保持逐字同步——以后改参数时两处都要改,最好在文件里留注释提醒。
  • continue-on-error: true 会让第一步失败时步骤状态显示绿色勾(外层 job 不失败),要靠 steps.build.outcome 判断真实结果,别被界面颜色骗了。

上线这套重试后,同样的偶发 reset 都被第二次构建接住了,没有再出现整条流水线红掉的情况。

五、结构性根治(可选)

重试只是止血。想让"境外 runner 拉境内镜像"这个跨境依赖彻底消失,思路是把基础镜像镜像一份到境外可稳定访问的 registry

  1. 把自定义基础镜像同步推一份到 GHCR(ghcr.io
  2. Dockerfile 的 FROM 改走 GHCR——拉取侧不再跨境
  3. 构建产物照旧推 ACR(推送侧的跨境写入实测比鉴权拉取稳定,且失败会被上面的重试兜住)

代价是多维护一份镜像同步。如果偶发频率不高,重试方案已经够用;等失败频率影响到发布节奏再上这套。

六、排查清单(遇到同类报错时按序过一遍)

  1. 失败发生在第几秒?秒级 → 网络/鉴权方向,分钟级 → 编译/依赖方向
  2. 报错是 TCP 层(connection reset / timeout)还是 HTTP 层(401/403/404)?前者查链路,后者才查凭证
  3. 同样的配置之前成功过吗?成功过且无相关改动 → 偶发环境问题,先重试再说
  4. 报错指向的代码行(比如 FROM)只是失败时正在处理的位置,不等于那一行有问题

相关文章

Node.js 内置 fetch 不走 HTTPS_PROXY:Google API 连不上的第二个坑

curl 带代理能通,Node 脚本里的 fetch 却一直超时?Node.js 内置 fetch(undici)设计上就不读 HTTPS_PROXY 等环境变量。本文用 Google Search Console API 的实际排查过程讲清这个行为,并给出三种修复路径:google-auth-library 的 client.request()、undici ProxyAgent、以及新版 Node 的实验性开关。

troubleshootingproxy+4
developer2026年8月5日3 min
6

Next.js 全站 hydration mismatch:根因是 GTM 载入的 AdSense,不是你的代码

Next.js 站点控制台随机出现 A tree hydrated but some attributes didn't match,且冷缓存首个页面干净、之后页面必报?根因不在你的组件,而是 GTM 拉起的 AdSense 自动广告在 React 水合前向 head 注入脚本,位置撞上了内联 GTM script。本文讲清这个时序谜题、一行修复方案,以及用 Playwright 抓完整报错 diff 的诊断技巧。

troubleshootingnextjs+4
developer2026年8月5日3 min
8

GA4 Data API 代理环境超时 DEADLINE_EXCEEDED:gRPC 不认大写 HTTPS_PROXY

GA4 Data API 在代理环境下 60 秒超时报 DEADLINE_EXCEEDED,而 curl 走同一个代理完全正常。根因是 SDK 底层走 gRPC,而 grpc-js 只读小写的 grpc_proxy / https_proxy,大写 HTTPS_PROXY 对它不可见。本文从报错现象到源码逐层排查,给出一次配对的修复方案。

troubleshootingga4+3
developer2026年8月5日3 min
4

私有仓库的文章如何自动同步到公开镜像站:GitHub Actions 跨仓库推送方案

如果你有一个私有 GitHub 仓库存放代码和文章,但又希望文章内容公开可访问,可以通过公开镜像仓库 + GitHub Actions 自动同步来实现。本文记录了一次完整的实施过程,包括跨仓库 Token 配置、同步工作流设计和常见注意事项。

cicdgithub-actions+4
developer2026年7月23日4 min
157