前段时间在一台 VMware 虚拟机上构建一个 Go + React 前端项目的 Docker 镜像,本以为几分钟搞定,结果硬生生卡了一个多小时。记录下完整的排查过程,希望对遇到类似问题的朋友有帮助。

## 问题现象

执行 `docker build` 后,构建在 Step 5(npm install)和 Step 7(apk add gcc)长时间卡住不动,一两个小时不结束。

构建前端阶段:

“`

Step 5/21 : RUN npm install …

 —> Running in 4926b5358868

“`

然后就是漫长的等待。

## 排查过程

### 第一层:DNS 和网络连通性

首先验证基础网络是否通。用 `nslookup` 检查域名解析:

“`bash

# 检查 DNS 解析

nslookup registry.npmmirror.com

# 检查 TCP 连接

curl -v telnet://registry.npmmirror.com:443 2>&1 | grep -E “connected|refused|timeout”

# 用 curl 测试 HTTPS(跳过证书验证)

curl -sk https://registry.npmmirror.com | head -3

“`

✅ 正确响应示例:

“`

$ nslookup registry.npmmirror.com

Name:   registry.npmmirror.com

Address: 111.32.130.65

$ curl -v telnet://registry.npmmirror.com:443 2>&1

* Connected to registry.npmmirror.com (111.32.130.65) port 443

“`

DNS 和 TCP 都是正常的,网络本身没问题。

### 第二层:Docker Hub 镜像加速

服务器无法直连 Docker Hub,表现为 `failed to resolve reference` 超时。需要在 Docker 守护进程配置镜像加速器:

“`json

{

  “registry-mirrors”: [

    “https://docker.m.daocloud.io”,

    “https://docker.1ms.run”,

    “https://docker.1panel.live”

  ]

}

“`

重启 Docker 后问题解决。

### 第三层:npm 镜像源问题

构建卡在 `npm install`,但通过之前测试确认了网络是通的。问题在哪里?

在宿主机和容器内分别测试:

“`bash

# 宿主机上测试 SSL(不加 -k)

curl -v –connect-timeout 10 https://registry.npmmirror.com 2>&1 | grep -E “(SSL|certificate|error|HTTP/|Connected)”

“`

✅ 正确响应示例:

“`

* Connected to registry.npmmirror.com (2409:8c30:1000:107:3::5) port 443

*  CAfile: /etc/ssl/certs/ca-certificates.crt

* SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256

*  SSL certificate verify ok.

< HTTP/2 200

“`

SSL 验证通过,网络通畅。那 npm install 卡住的原因很可能是**源不稳定**——一开始用的 `registry.npm.taobao.org` 已经废弃,很多包下载超时。

解决方法:切换到 npmmirror.com:

“`dockerfile

RUN npm config set registry https://registry.npmmirror.com && \

    npm install –legacy-peer-deps –prefix /web/default

“`

### 第四层:Alpine 的 apk 安装工具链 — 真正的元凶

这是最坑的一步。npm install 完成后,构建进入后端阶段(Golang 编译),卡在了安装编译工具链:

“`

Step 7/20 : RUN apk add –no-cache gcc musl-dev sqlite-dev build-base

 —> Running in 367eadbab22d

“`

看似在装 gcc,但过了 16 分钟毫无进展。排查:

“`bash

# 查看容器内进程状态

sudo docker exec 367eadbab22d ps -ef

# 输出:

# PID   USER     TIME  COMMAND

#   1 root      0:01 apk add –no-cache gcc musl-dev sqlite-dev build-base

#  28 root      0:00 [ssl_client]    ← 僵尸进程!

# 查看 apk 日志

sudo docker exec 367eadbab22d cat /var/log/apk.log

# (15/29) Installing gcc (15.2.0-r5)   ← 卡在这里

# 查看进程等待状态

sudo docker exec 367eadbab22d cat /proc/1/wchan

# wait_woken   ← 等待子进程,说明子进程(ssl_client)挂了

“`

**`ssl_client` 是 Alpine 自带的 HTTPS 客户端,它在下载 gcc 包时连接 `dl-cdn.alpinelinux.org` 的 CDN 节点时卡死了。** 原因是该 CDN 节点在当时不稳定,SSL 连接挂起。

同时也可以用以下命令测试 alpine 容器内各源的连通性:

“`bash

# apk 测试(在 alpine 容器内)

sudo docker run –rm alpine:latest sh -c ‘wget -qO- –timeout=10 https://dl-cdn.alpinelinux.org/alpine/v3.24/main/x86_64/APKINDEX.tar.gz 2>/dev/null && echo “alpine OK” || echo “alpine FAIL”‘

“`

✅ 正常时应输出 `alpine OK`(返回 APKINDEX 数据)。

解决方法:在 Dockerfile 中将 Alpine 源切换到国内镜像:

“`dockerfile

RUN sed -i ‘s|dl-cdn.alpinelinux.org|mirrors.tuna.tsinghua.edu.cn|g’ /etc/apk/repositories

RUN apk add –no-cache gcc musl-dev sqlite-dev build-base

“`

切换后工具链在 30 秒内装完,编译步骤也顺利通过。

## TLS 握手 SSL 证书问题排查命令清单

如果怀疑是 SSL/TLS 证书问题,按以下顺序排查,每个命令都附有**正确响应的判断标准**

### 1. 检查 DNS 解析

“`bash

nslookup registry.npmmirror.com

# 或

dig registry.npmmirror.com +short

“`

**正确响应**:返回 IP 地址(如 `111.32.130.65`),而非 `NXDOMAIN`  

❌ 异常:`** server can’t find …: NXDOMAIN` → DNS 解析失败

### 2. 检查 TCP 连通性

“`bash

curl -v telnet://registry.npmmirror.com:443 2>&1 | grep -E “connected|refused|timeout”

“`

**正确响应**:`Connected to registry.npmmirror.com (111.32.130.65) port 443`  

❌ 异常:`Connection refused` 或 `Connection timed out`

### 3. 测试 TLS 握手(跳过证书验证)

“`bash

curl -vk –connect-timeout 10 https://registry.npmmirror.com 2>&1 | head -20

“`

**正确响应**

“`

* SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256 / X25519 / RSASSA-PSS

* TLS handshake completed

“`

❌ 异常:`SSL connection timeout` 或无法建立连接

### 4. 测试 TLS 握手(完整证书验证)

“`bash

curl -v –connect-timeout 10 https://registry.npmmirror.com 2>&1 | grep -E “(SSL|certificate|error|HTTP/|Connected)”

“`

**正确响应**

“`

* SSL certificate verify ok.

< HTTP/2 200

“`

❌ 异常:`SSL certificate problem: unable to get local issuer certificate`

### 5. 查看证书链完整性

“`bash

openssl s_client -connect registry.npmmirror.com:443 -showcerts 2>&1

“`

**正确响应**(证书链完整):

“`

Certificate chain

 0 s:CN = *.npmmirror.com

 1 s:Encryption Everywhere DV TLS CA – G2

 2 s:DigiCert Global Root G2

Verification: OK

Verify return code: 0 (ok)

“`

关键判断点:

– 应该有 **3 级证书链**(叶子证书 → 中间 CA → 根 CA)

– `Verify return code: 0 (ok)` 表示验证通过

– 如果只有 1 级或 2 级,说明中间 CA 缺失

❌ 异常响应:

“`

Verify return code: 21 (unable to verify the first certificate)

“`

“`

Verify return code: 20 (unable to get local issuer certificate)

“`

### 6. 验证 Node.js 的 CA 问题(在容器内执行)

“`bash

sudo docker run –rm node:16 node -e “

  const https = require(‘https’);

  https.get(‘https://registry.npmmirror.com’, (res) => {

    console.log(‘OK:’, res.statusCode);

  }).on(‘error’, (e) => console.error(‘FAIL:’, e.message));

“`

**正确响应**

“`

OK: 200

“`

❌ 异常响应:

“`

FAIL: certificate error

“`

如果 `openssl s_client` 显示 `Verify return code: 0 (ok)` 但 Node.js 报 `certificate error`,说明是 **Node.js 内置 CA 列表过旧**(不包含新的根证书),可通过 `NODE_EXTRA_CA_CERTS` 环境变量或 `strict-ssl false` 绕过。

### 7. 如果卡在容器内,查看进程状态

“`bash

docker exec <container_id> ps -ef

docker exec <container_id> cat /proc/1/wchan

docker exec <container_id> cat /var/log/apk.log

“`

✅ `cat /proc/1/wchan` 正常应为 `do_sys_poll`(网络等待)或 `0`(空闲)  

❌ 异常:`wait_woken` 表示等待子进程,结合 `ps -ef` 看到的 `ssl_client` 僵尸进程说明 HTTPS 下载卡死

## 总结

这次排查最大的教训是:**不要轻易下结论。**

– 看到 npm install 卡住就以为是 SSL 证书问题 —— 错

– 看到 apk 的 ssl_client 进程就推测是 CA 证书缺失 —— 错

– 用 `openssl s_client` 对比 `curl` 行为就归因于 Node.js 内置 CA 过旧 —— 还是错

最终发现:npm 卡住是因为用了废弃的淘宝源,apk 卡死是因为 Alpine CDN 节点不稳定。两个都是**网络源的可用性问题**,跟 SSL 证书本身无关。

作为运维/开发者,面对这类问题时的正确姿势:

1. **先看进程状态** — `ps -ef`、`/proc/<pid>/wchan` 远比猜有用

2. **换源测试** — 官方源不行就换国内镜像,这是最快的验证手段

3. **对比测试** — 在宿主机和容器内部都跑一遍相同的测试,定位问题边界

4. **知道每个命令的**“正确响应”长什么样** — 这样才能快速判断异常

5. **不要过早下结论** — 多收集证据再归因