前段时间在一台 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. **不要过早下结论** — 多收集证据再归因