1. 项目概述:为什么企业内网里的容器总“连不上网”?

“Docker Proxy: The Complete Guide to Make Containers Work Behind Corporate Firewalls”——这个标题一上来就戳中了成千上万运维工程师、DevOps 工程师和开发人员的痛点。不是容器不会跑,是它一启动就报错: Failed to fetch https://index.docker.io/v1/... network is unreachable ;不是镜像拉不下来,是 docker pull nginx 卡在 Waiting 状态十分钟不动;不是应用部署失败,是 pip install requests 在构建阶段直接超时退出。这些现象背后,几乎都指向同一个现实:你的开发机、CI/CD 构建节点、甚至测试服务器,正运行在一套严格管控的 企业级网络策略之下 ——有统一出口网关、强制 HTTPS 解密、白名单域名控制、HTTP/HTTPS 代理认证、DNS 转发限制,以及对非标准端口(比如 Docker daemon 的 2375/2376)的深度封禁。

我亲身经历过三类典型场景:第一类是金融客户,所有出向流量必须经由带 NTLM 认证的 Squid 代理,且只允许访问 *.corp.example.com pypi.org registry.npmjs.org 等极少数白名单域名;第二类是大型制造集团,内部 DNS 不解析公网域名,所有外部请求需走自研的透明代理网关,但该网关不支持 CONNECT 方法,导致 TLS 握手直接失败;第三类是跨国药企,本地研发环境需对接美国镜像仓库,但防火墙策略禁止直连境外 IP 段,且要求所有 HTTP 头部必须携带 X-Corpo-Auth 自定义令牌。这三类场景,没有一个能靠 export http_proxy=http://proxy:3128 一行命令解决。它们共同揭示了一个被长期低估的事实: Docker 的网络模型不是“单点代理配置”就能打通的,而是一套需要分层治理、多点协同、协议适配的系统工程 。你配置的不只是 http_proxy ,而是要同时协调宿主机系统代理、Docker Daemon 代理、容器运行时代理、构建上下文代理、镜像仓库认证代理,甚至容器内应用自身的代理逻辑。本文不讲概念,不堆术语,只讲我在过去五年里,在 17 家不同行业客户现场踩过的坑、验证过的路径、压测过的效果、写进 SOP 的配置模板。如果你正在为“容器在内网跑不通”焦头烂额,这篇文章就是为你写的实操手册。

2. 核心设计思路:为什么不能只设一个环境变量?

2.1 Docker 网络栈的四层代理需求

很多人以为,只要在 shell 里执行 export http_proxy=http://10.1.2.3:8080 ,再 docker build . ,一切就该顺理成章。但现实是,Docker 的网络调用链远比想象中复杂,它天然存在四个独立的代理作用域,彼此隔离、互不继承:

  1. 宿主机 CLI 层 :这是你敲 docker pull 时,Docker CLI 进程自身发起的 HTTP 请求。它会读取 shell 环境变量 http_proxy https_proxy no_proxy ,用于拉取远程镜像元数据(如 manifest)、校验 registry token、上传 layer digest。这一层失败,表现为 Error response from daemon: Get "https://registry-1.docker.io/v2/..."

  2. Docker Daemon 层 :CLI 只是客户端,真正干活的是后台的 dockerd 进程。它负责与 registry 建立连接、下载 layer blob、解压镜像、管理存储驱动。 dockerd 默认 完全不读取用户 shell 的环境变量 ,它有自己的配置体系。若未显式配置,它会尝试直连,而企业防火墙往往直接丢弃这类无代理标记的请求。这一层失败,日志里会出现 failed to dial ... connection refused timeout ,且 docker info 显示 HTTP Proxy: <none>

  3. 容器运行时层(Build 阶段) :当你执行 docker build ,Dockerfile 中的 RUN apt-get update RUN pip install 是在临时容器内执行的。此时,容器内的进程 默认继承的是构建时传入的 --build-arg 或 Dockerfile 中 ARG 定义的代理变量,而非宿主机环境变量 。更关键的是,很多基础镜像(如 debian:slim python:3.9-slim )根本不预装 curl wget ,也不设置 apt 的 proxy 配置,导致包管理器完全无法联网。

  4. 容器应用层(Runtime 阶段) :容器启动后,其内部应用(如 Python Flask、Node.js Express)若需调用外部 API(比如调用公司内部的 Auth Service 或第三方支付网关),则必须由应用自身处理代理逻辑。Docker 不会自动把宿主机的 http_proxy 注入到每个进程的环境里——除非你显式用 -e http_proxy=... 启动,或在 Dockerfile 中用 ENV 固化。而很多微服务框架(如 Spring Boot)对代理的支持是可选的,需要额外配置 spring.http.proxy.host 等参数。

提示:这四层不是并列关系,而是嵌套调用。CLI 调用 Daemon,Daemon 启动 Build Container,Build Container 运行应用命令。任何一层断开,整个链路就中断。这也是为什么“只配一个环境变量”永远不够——它最多覆盖第一层,其余三层全靠猜。

2.2 企业防火墙的三大技术特征与应对策略

要让容器穿透企业防火墙,必须先理解防火墙本身的技术边界。根据我参与的 32 个企业网络审计项目,当前主流企业级防火墙(Palo Alto、FortiGate、Cisco Firepower)普遍具备以下三个核心特征,每一条都直接决定代理方案的设计方向:

特征一:TLS 流量深度检测(SSL Decryption)
企业为安全审计,常开启“SSL Inbound Inspection”,即所有出向 HTTPS 流量先被防火墙截获,用企业 CA 证书重新签发一个“中间证书”,再转发给目标网站。这对容器的影响是致命的:Docker CLI 和 dockerd 默认信任操作系统 CA 证书库( /etc/ssl/certs/ca-certificates.crt ),但企业 CA 往往未预装。结果就是 x509: certificate signed by unknown authority 错误。解决方案不是绕过证书检查(那等于放弃安全),而是将企业 CA 证书注入到 Docker 的信任链中——这需要修改 dockerd --tlscacert 参数或挂载证书到容器内。

特征二:CONNECT 方法受限
标准 HTTP 代理通过 CONNECT 方法建立隧道来传输 HTTPS 流量。但部分企业网关(尤其是老旧的 Blue Coat 或自研网关)为防恶意隧道,会禁用 CONNECT ,或仅允许 CONNECT 到特定端口(如 443)。当 dockerd 尝试用 CONNECT 连接 registry-1.docker.io:443 时,网关直接返回 405 Method Not Allowed 。此时,传统代理失效,必须切换到 HTTPS 代理模式(即 https_proxy 指向 https://proxy:port ,利用 TLS over TLS 的方式绕过限制。但这又带来新问题: https_proxy 要求代理服务器本身支持 TLS 终止,且客户端需信任代理的证书。

特征三:DNS 解析策略隔离
企业内网常部署 split-DNS:内部域名(如 git.corp.com )由内网 DNS 解析,公网域名(如 github.com )则被重定向到防火墙的 DNS 代理。但 Docker 默认使用宿主机的 /etc/resolv.conf ,而该文件可能只包含内网 DNS(如 10.0.0.1 ),导致 ping github.com 能通,但 nslookup github.com 返回 NXDOMAIN 。更隐蔽的问题是,某些镜像仓库(如 AWS ECR)使用私有 endpoint(如 123456789012.dkr.ecr.us-east-1.amazonaws.com ),其 DNS 解析依赖公网根域,一旦 DNS 被劫持,容器就永远找不到 registry。

注意:这三大特征不是理论假设,而是我在某银行客户现场连续三天抓包分析后确认的。他们用 FortiGate 做 SSL 解密,禁用 CONNECT ,且 DNS 强制走 10.200.1.10 (内网 DNS)。我们最初只配了 http_proxy ,结果 docker login 成功(因为走的是 HTTP Basic Auth),但 docker pull 失败(因为拉 layer 需要 CONNECT )。后来改用 https_proxy + 企业 CA 注入,才彻底解决。

2.3 方案选型逻辑:为什么推荐 systemd + config.json + build-args 三位一体?

面对上述复杂性,业界曾出现过多种“简化方案”,但我在真实生产环境中全部否决了:

  • 方案A:全局 export + alias docker
    有人建议在 ~/.bashrc 里写 alias docker='http_proxy=... https_proxy=... no_proxy=... docker' 。这看似一劳永逸,但它只覆盖 CLI 层,对 dockerd 、Build、Runtime 全无效,且 no_proxy 的逗号分隔符在不同 shell 下行为不一致(zsh 用空格,bash 用逗号),极易出错。

  • 方案B:修改 /etc/default/docker
    Ubuntu 系统下,有人习惯改 /etc/default/docker export http_proxy=... 。但该文件仅在 systemd 未接管时生效;现代 Docker(20.10+)已全面迁移到 systemd ,此文件被忽略。强行修改会导致 systemctl daemon-reload 后配置丢失。

  • 方案C:Docker Desktop 内置代理
    Mac/Windows 用户可能想用 Docker Desktop 的 GUI 代理设置。但它只影响 Desktop 自身的 dockerd ,对 WSL2 或远程 dockerd 无效,且无法控制 Build 阶段的 no_proxy ,在混合云环境下完全不可靠。

最终,我锁定 systemd + config.json + build-args 三位一体方案,理由非常务实:

  1. systemd 是事实标准 :所有主流 Linux 发行版(RHEL 8+, Ubuntu 20.04+, CentOS Stream)均以 systemd 管理 dockerd ,其 EnvironmentFile Environment 指令稳定、可审计、支持热重载。

  2. config.json 是容器层唯一权威 ~/.docker/config.json 是 Docker CLI 和 dockerd 共享的凭证与代理配置中心,支持 proxies 字段精确控制每个 registry 的代理策略(比如 registry.gitlab.com 走内网代理, quay.io 走另一套),避免 no_proxy 泛化带来的安全风险。

  3. build-args 是 Build 阶段的黄金标准 docker build --build-arg http_proxy=... 是 Docker 官方推荐的构建时变量传递方式,它确保变量只在构建上下文中生效,不污染宿主机环境,且可被 Dockerfile 中的 ARG ENV 精确捕获,兼容所有基础镜像。

这三者组合,覆盖了四层代理需求中的三层(Daemon、CLI、Build),剩下 Runtime 层则交由应用自身或 docker run -e 控制,职责清晰,无冗余,可版本化管理( systemd unit 文件可存 Git, config.json 可加密分发),这才是企业级落地的正确姿势。

3. 实操全流程:从零开始配置一个可投产的代理环境

3.1 第一步:获取并验证企业代理服务器信息

在动手前,必须拿到准确的企业代理参数。这不是简单问 IT 部门“代理地址是多少”,而是要拿到一份可验证的、带协议细节的配置清单。我通常会要求客户提供以下五项信息,并现场验证:

项目 示例值 验证方法 为什么重要
代理协议 http https curl -v -x http://proxy:3128 https://google.com vs curl -v -x https://proxy:3128 https://google.com http 代理不支持 TLS 终止, https 代理必须提供证书
认证方式 Basic NTLM Kerberos 无认证 curl -v -x http://user:pass@proxy:3128 https://google.com Basic 认证明文传输,NTLM 需 curl --ntlm ,Kerberos 需 kinit
白名单域名 *.corp.com,10.0.0.0/8,localhost curl -v -x http://proxy:3128 http://intranet.corp.com no_proxy 必须精确匹配,否则内网服务被错误代理
DNS 代理地址 10.200.1.10 dig @10.200.1.10 github.com +short 若 DNS 不通, docker pull 会卡在解析 registry 域名
企业 CA 证书 corp-ca.crt (PEM 格式) openssl x509 -in corp-ca.crt -text -noout | grep "Issuer" 用于注入 dockerd 和容器内,解决 x509 错误

实操心得:我曾在一个客户现场,IT 提供的“代理地址”是 http://proxy.corp.com:8080 ,但实际网络策略要求所有流量必须走 https://secure-proxy.corp.com:8443 ,且该地址只接受 https_proxy 。我们按 http 配置折腾两天,最后发现 curl -x https://proxy.corp.com:8080 直接返回 501 Not Implemented 。所以, 务必用 curl 命令逐项验证,而不是相信文档或口头描述

验证脚本我常放在 /opt/bin/proxy-test.sh ,内容如下(可直接复制使用):

#!/bin/bash
# 请替换为实际值
PROXY_HTTP="http://proxy.corp.com:8080"
PROXY_HTTPS="https://proxy.corp.com:8443"
CORP_CA="/path/to/corp-ca.crt"
NO_PROXY="*.corp.com,10.0.0.0/8,localhost"

echo "=== 测试 HTTP 代理 ==="
curl -v -x "$PROXY_HTTP" -k https://google.com 2>&1 | head -20

echo -e "\n=== 测试 HTTPS 代理 ==="
curl -v -x "$PROXY_HTTPS" --cacert "$CORP_CA" https://google.com 2>&1 | head -20

echo -e "\n=== 测试 DNS 解析 ==="
dig @10.200.1.10 registry-1.docker.io +short

echo -e "\n=== 测试 no_proxy 绕过 ==="
curl -v -x "$PROXY_HTTP" http://gitlab.corp.com 2>&1 | head -10

运行后,重点看是否返回 200 OK 、是否有 x509 错误、DNS 是否返回 IP。只有全部通过,才能进入下一步。

3.2 第二步:配置 Docker Daemon 层代理(systemd 方式)

这是最关键的一步,决定了 dockerd 本身能否联网。操作必须在 root 权限下进行,且需重启 dockerd

步骤1:创建 systemd 环境变量文件
不要直接改 /lib/systemd/system/docker.service (会被更新覆盖),而是创建 /etc/systemd/system/docker.service.d/http-proxy.conf

[Service]
Environment="HTTP_PROXY=http://proxy.corp.com:8080"
Environment="HTTPS_PROXY=https://proxy.corp.com:8443"
Environment="NO_PROXY=*.corp.com,10.0.0.0/8,localhost,127.0.0.1,docker-registry.corp.com"
Environment="DOCKER_CERT_PATH=/etc/docker/certs.d"

注意: HTTPS_PROXY 必须是 https:// 开头,表示这是一个 TLS 终止代理; NO_PROXY 用逗号分隔,支持域名通配符 * 和 CIDR 网段, 必须包含 localhost 127.0.0.1 ,否则 dockerd 无法与本地 socket 通信。

步骤2:注入企业 CA 证书
Docker 默认信任 /etc/ssl/certs/ca-certificates.crt ,但企业 CA 通常不在其中。我们需要为 dockerd 单独配置:

# 创建 certs.d 目录(若不存在)
sudo mkdir -p /etc/docker/certs.d/registry-1.docker.io:443
# 复制企业 CA 到该目录(文件名必须是 ca.crt)
sudo cp /path/to/corp-ca.crt /etc/docker/certs.d/registry-1.docker.io:443/ca.crt
# 重启 docker 服务
sudo systemctl daemon-reload
sudo systemctl restart docker

提示: /etc/docker/certs.d/<host>:<port>/ca.crt 是 Docker 的硬编码路径, <host>:<port> 必须与 registry 的实际访问地址完全一致。例如,若你用 docker login https://my-ecr.us-east-1.amazonaws.com ,则目录名应为 my-ecr.us-east-1.amazonaws.com:443

步骤3:验证 Daemon 代理是否生效
执行 docker info | grep -i proxy ,输出应为:

HTTP Proxy: http://proxy.corp.com:8080
HTTPS Proxy: https://proxy.corp.com:8443
No Proxy: *.corp.com,10.0.0.0/8,localhost,127.0.0.1,docker-registry.corp.com

再执行 docker pull hello-world ,观察日志( journalctl -u docker -f )。成功时,你会看到 Pulling fs layer Downloading 日志;失败时, journalctl 会明确报错 x509: certificate signed by unknown authority connection refused

3.3 第三步:配置 CLI 与 Build 层代理(config.json + build-args)

CLI 层和 Build 层的代理,我们用 Docker 官方推荐的 ~/.docker/config.json 统一管理,它比环境变量更精细、更安全。

步骤1:生成初始 config.json
如果文件不存在,先运行 docker login 创建骨架:

docker login -u dummy -p dummy https://registry-1.docker.io
# 然后编辑 ~/.docker/config.json

步骤2:编辑 config.json,添加 proxies 字段
完整配置如下(请按实际代理替换):

{
  "auths": {
    "https://index.docker.io/v1/": {
      "auth": "ZG...base64...=="
    }
  },
  "proxies": {
    "default": {
      "httpProxy": "http://proxy.corp.com:8080",
      "httpsProxy": "https://proxy.corp.com:8443",
      "noProxy": "*.corp.com,10.0.0.0/8,localhost,127.0.0.1"
    },
    "https://my-ecr.us-east-1.amazonaws.com": {
      "httpProxy": "http://proxy.corp.com:8080",
      "httpsProxy": "https://proxy.corp.com:8443",
      "noProxy": ""
    }
  }
}

关键点:

  • default 对象控制所有未显式声明的 registry;
  • 可为特定 registry(如 ECR、GitLab CI Registry)单独配置代理,实现策略差异化;
  • noProxy config.json 中同样生效,且优先级高于环境变量。

步骤3:编写 Dockerfile,支持 build-args 代理
这是 Build 阶段的核心。一个健壮的 Dockerfile 应该这样写:

# syntax=docker/dockerfile:1
FROM python:3.9-slim

# 声明构建参数,带默认值(空值表示不代理)
ARG http_proxy
ARG https_proxy
ARG no_proxy

# 将参数转为环境变量,供 RUN 命令使用
ENV http_proxy=${http_proxy} \
    https_proxy=${https_proxy} \
    no_proxy=${no_proxy}

# 为 apt 包管理器单独配置 proxy(Debian/Ubuntu 系)
RUN if [ -n "$http_proxy" ]; then \
      echo "Acquire::http::Proxy \"$http_proxy\";" > /etc/apt/apt.conf.d/90proxy && \
      echo "Acquire::https::Proxy \"$https_proxy\";" >> /etc/apt/apt.conf.d/90proxy; \
    fi

# 为 pip 单独配置 proxy(Python)
RUN if [ -n "$http_proxy" ]; then \
      pip config set global.proxy "$http_proxy" && \
      pip config set global.trusted-host "pypi.org" && \
      pip config set global.trusted-host "files.pythonhosted.org"; \
    fi

# 现在可以安全地执行联网操作
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
RUN pip install requests flask

COPY app.py .
CMD ["python", "app.py"]

这个 Dockerfile 的精妙之处在于:

  • 使用 ARG 声明,而非硬编码 ENV ,保证构建时灵活性;
  • if [ -n "$http_proxy" ] 判断,避免无代理时写入空配置导致 apt/pip 故障;
  • apt.conf.d/90proxy 是 Debian 系的标准配置路径,比 export 更可靠;
  • pip config 是 pip 20.1+ 的官方配置方式,比 pip install --proxy 更持久。

步骤4:执行构建,传入代理参数

docker build \
  --build-arg http_proxy="http://proxy.corp.com:8080" \
  --build-arg https_proxy="https://proxy.corp.com:8443" \
  --build-arg no_proxy="*.corp.com,10.0.0.0/8" \
  -t myapp:latest .

构建过程中, RUN apt-get update RUN pip install 将自动使用传入的代理,无需修改任何代码。

3.4 第四步:Runtime 层代理与应用集成

容器运行起来后,应用如何调用外部服务?这取决于应用类型,但原则只有一个: 代理配置必须由容器启动时注入,而非写死在镜像里

场景1:通用 HTTP 客户端(cURL、Wget)
直接用 docker run -e 注入:

docker run -d \
  -e http_proxy="http://proxy.corp.com:8080" \
  -e https_proxy="https://proxy.corp.com:8443" \
  -e no_proxy="*.corp.com,10.0.0.0/8" \
  --name myapp \
  myapp:latest

所有基于 libc 的程序(包括 curl wget git )都会自动读取这些环境变量。

场景2:Java 应用(Spring Boot)
Spring Boot 2.4+ 支持 spring.http.proxy.host 配置,但更推荐用 JVM 参数:

docker run -d \
  -e JAVA_TOOL_OPTIONS="-Dhttp.proxyHost=proxy.corp.com -Dhttp.proxyPort=8080 -Dhttps.proxyHost=proxy.corp.com -Dhttps.proxyPort=8443 -Dhttp.nonProxyHosts=\"*.corp.com|10.*\"" \
  myjavaapp:latest

注意: nonProxyHosts 的语法是 | 分隔,且需用反斜杠转义双引号。

场景3:Node.js 应用
Node.js 本身不读取 http_proxy ,需在代码中显式设置:

// app.js
const https = require('https');
const agent = new https.Agent({
  proxy: {
    host: process.env.HTTP_PROXY?.replace('http://', '').split(':')[0],
    port: process.env.HTTP_PROXY?.replace('http://', '').split(':')[1] || 8080,
    protocol: 'http:'
  }
});
// 然后在 axios/fetch 中使用 agent

或者,用 global-agent npm 包统一拦截:

npm install global-agent
# 启动时
GLOBAL_AGENT_HTTP_PROXY=http://proxy.corp.com:8080 node app.js

实操心得:我曾在一个 Node.js 项目中,因忘记在 fetch 中指定 agent ,导致 API 调用超时。后来我们统一在 Dockerfile 中加入 ENV NODE_OPTIONS="--loader global-agent/register" ,让所有 require() 自动加载代理,一劳永逸。这个技巧值得所有 Node.js 团队收藏。

4. 常见问题排查与独家避坑指南

4.1 问题速查表:从错误日志定位故障层

当容器代理失败时,不要盲目重试。请按以下流程,根据错误日志精准定位是哪一层出了问题:

错误日志关键词 可能故障层 排查命令 解决方案
Get "https://registry-1.docker.io/v2/...": dial tcp: lookup registry-1.docker.io on 127.0.0.11:53: no such host DNS 层 docker run --rm alpine nslookup registry-1.docker.io 检查 /etc/resolv.conf ,或 dockerd 启动参数 --dns 10.200.1.10
x509: certificate signed by unknown authority TLS 证书层 curl -v --cacert /etc/docker/certs.d/registry-1.docker.io:443/ca.crt https://registry-1.docker.io 确认 ca.crt 路径正确,且证书 PEM 格式无误
Error response from daemon: Get "https://registry-1.docker.io/v2/...": net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers) Daemon 代理层 docker info | grep -i proxy journalctl -u docker -n 50 检查 systemd 配置是否生效,代理地址是否可达
E: Failed to fetch http://deb.debian.org/... Connection failed Build 层(apt) docker build --progress=plain . 查看具体 RUN 步骤 检查 Dockerfile apt.conf.d/90proxy 是否写入, build-arg 是否传入
requests.exceptions.ConnectionError: Max retries exceeded with url: /api/v1/... Runtime 层(应用) docker exec -it myapp env | grep -i proxy 确认 docker run -e 是否注入,应用代码是否读取环境变量

注意: docker build --progress=plain 是调试神器,它会显示每一行 RUN 命令的实时输出,而不是默认的“跳过”模式。很多 apt 超时问题,都是靠它定位到具体哪一行 apt-get install 卡住的。

4.2 五个血泪教训:那些文档里不会写的坑

坑1: no_proxy 的大小写敏感性
NO_PROXY 环境变量在 Linux 下是 全大写且大小写敏感 的。如果你写成 no_proxy No_Proxy dockerd curl 都会忽略它。更坑的是,某些旧版 curl (<7.55)会把 NO_PROXY 当作 no_proxy 处理,导致行为不一致。 解决方案:永远用全大写 NO_PROXY ,并在所有地方(systemd、config.json、build-arg、docker run)保持一致

坑2: https_proxy 的协议歧义
https_proxy=http://proxy:3128 https_proxy=https://proxy:3128 是完全不同的东西。前者表示“用 HTTP 协议连接代理服务器”,后者表示“用 HTTPS 协议连接代理服务器”。如果你的代理服务器只监听 HTTP(如 Squid 默认),却配置了 https:// curl 会直接报 Failed to connect to proxy.corp.com port 3128: Connection refused 验证方法: curl -v -x https://proxy:3128 https://google.com ,若返回 Failed to establish a new connection ,说明代理不支持 HTTPS 协议

坑3:Docker Desktop 的 DNS 劫持
Mac 上的 Docker Desktop 会自动修改 macOS 的 /etc/resolver/docker-desktop ,将所有 .docker.internal 域名解析到虚拟机 IP。但如果你的企业 DNS 策略也劫持了 .internal ,就会冲突。表现是 docker run --rm alpine ping host.docker.internal 能通,但 ping gitlab.corp.com 超时。 解决方案:在 Docker Desktop 设置中关闭 Use the Docker Desktop VM's DNS resolver ,改用 DNS Server: 10.200.1.10

坑4: build-arg 的作用域陷阱
ARG 只在构建时有效, ENV 才在运行时有效。如果你在 Dockerfile 中写:

ARG http_proxy
RUN echo $http_proxy # ✅ 正确,构建时可用
CMD echo $http_proxy # ❌ 错误,运行时 $http_proxy 为空

很多开发者误以为 ARG 会自动变成 ENV ,结果 CMD ENTRYPOINT 里拿不到代理变量。 正确做法:显式 ENV http_proxy=$http_proxy ,或在 CMD 中用 sh -c "echo $http_proxy"

坑5:企业 CA 证书的格式陷阱
企业提供的 corp-ca.crt 有时是 DER 格式(二进制),而 Docker 只认 PEM(文本 Base64)。用 file corp-ca.crt 查看,若显示 data ,说明是 DER。 转换命令: openssl x509 -inform DER -in corp-ca.crt -outform PEM -out corp-ca.pem 。漏掉这一步, dockerd 启动会静默失败, journalctl 里只有一行 failed to load TLS config ,毫无提示。

4.3 性能优化:如何让代理不拖慢构建速度?

代理不是万能的,不当配置反而会让构建慢 3-5 倍。以下是我在高并发 CI 环境中验证过的优化技巧:

技巧1:启用 Docker BuildKit 缓存代理
BuildKit 默认会对 RUN 命令的网络请求做缓存。但若代理不稳定,缓存会失效。启用 --cache-from 并配合 --output=type=registry 可大幅提升复用率:

# 启用 BuildKit
export DOCKER_BUILDKIT=1
# 构建时指定缓存源
docker buildx build \
  --cache-from type=registry,ref=myregistry.com/cache:base \
  --cache-to type=registry,ref=myregistry.com/cache:base,mode=max \
  --output=type=registry,ref=myregistry.com/myapp:latest \
  --build-arg http_proxy=... \
  .

技巧2:为 apt/pip 配置超时与重试
Dockerfile apt.conf.d/90proxy 中,追加:

Acquire::Retries "3";
Acquire::http::Timeout "60";
Acquire::https::Timeout "60";

pip ,在 pip config 后加:

pip config set global.timeout "60"
pip config set global.retries "3"

技巧3:使用镜像代理(Registry Mirror)替代直连
如果企业允许,最高效的方式是部署一个内网镜像代理(如 Harbor、Nexus Repository),将 registry-1.docker.io 镜像同步到 harbor.corp.com/docker-hub 。然后在 daemon.json 中配置:

{
  "registry-mirrors": ["https://harbor.corp.com/docker-hub"]
}

这样 docker pull nginx 实际走的是内网 harbor.corp.com ,延迟从 2s 降到 200ms,且完全规避代理认证和 TLS 问题。

最后分享一个小技巧:我在所有客户的 CI 节点上,都部署了一个 proxy-check.sh 脚本,它会在每次 docker build 前自动运行,检查代理连通性、DNS 解析、CA 证书有效性。如果任一检查失败,立即 exit 1 并打印清晰错误。这让我们把 80% 的代理问题,拦截在构建开始之前,而不是等 20 分钟后构建失败才发现。脚本虽小,却是保障交付节奏的关键一环。

更多推荐