1. 为什么在 Ubuntu 上配 Docker 代理比想象中更“痛”——从一次失败的 docker pull 说起

上周帮团队新来的实习生搭开发环境,Ubuntu 22.04 虚拟机里装完 Docker,执行 sudo docker pull mysql:5.7 卡在 “trying to pull repository” 超过 8 分钟,最后报错: Get https://registry-1.docker.io/v2/: net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers) 。他反复确认宿主机能正常访问外网、 curl -v https://docker.io 返回 200,甚至把 /etc/docker/daemon.json 里加了 "http-proxy" 字段重启服务,依然失败。直到我注意到终端里那行被忽略的提示: system has not been booted with systemd as init system (pid 1). can't operate ——这台 Vagrant Box 用的是 sysvinit 启动,根本没跑 systemd 。而 Docker 官方文档里所有关于代理配置的示例,都默认你用的是 systemd 管理服务。这不是配置错了,是整个操作路径从根上就断了。

这就是本篇要解决的真实问题: 在 Ubuntu 环境下,Docker 的网络代理配置不是简单改个 JSON 文件就能生效的“开关”,而是一套必须与系统初始化机制、用户权限模型、容器运行时生命周期深度耦合的链式配置体系。 它涉及三个关键层级:系统级代理(影响 dockerd 进程启动)、守护进程级代理(影响 docker pull/push 等 CLI 操作)、容器级代理(影响容器内应用联网)。三者缺一不可,且配置方式因 Ubuntu 版本、安装方式(deb 包 vs snap vs 二进制)、启动器(systemd vs sysvinit)而完全不同。关键词 Ubuntu Docker 网络代理配置 systemd docker pull 不是孤立标签,而是这个链条上的五个咬合齿——漏掉任何一个, docker pull 就会静默失败,连错误日志都藏得极深。

这篇指南不讲“下载安装包→解压→运行”的流水线,而是聚焦于 Ubuntu 下 Docker 代理配置中最常踩的三类坑:第一类是 systemd 未启用却硬套官方教程导致 dockerd 根本不读代理配置;第二类是 daemon.json 语法正确但 dockerd 进程实际未加载该文件(常见于 snap 安装或非标准路径);第三类是代理配置生效了,但容器内 apt update pip install 仍超时——因为容器网络栈完全隔离,宿主机代理对它无效。我会用真实终端日志还原排查过程,给出每种 Ubuntu 场景(桌面版、Server 版、WSL2、Vagrant、Docker Desktop WSL 后端)下的可验证配置方案,并附上一键检测脚本。如果你正对着 docker pull 的光标发呆,或者 journalctl -u docker 里只看到 Started Docker Application Container Engine 却找不到任何代理相关日志,那么接下来的内容,就是你真正需要的“保姆级”答案。

2. 系统启动器决定一切:先确认你的 Ubuntu 是用 systemd 还是 sysvinit 启动的

很多教程直接跳到 sudo systemctl edit docker ,却忘了问一个最基础的问题:你的 Ubuntu 真的在用 systemd 吗?这个问题的答案,直接决定了后续所有代理配置的路径是否成立。Ubuntu 自 15.04 起默认使用 systemd ,但大量生产环境仍在用旧版 Ubuntu Server(如 14.04),或通过 Vagrant、某些嵌入式镜像、精简版发行版(如 Ubuntu Core)部署的系统,它们可能仍使用 sysvinit upstart 。一旦启动器不匹配,所有基于 systemctl 的配置都会变成“对空气说话”。

验证方法极其简单,只需一条命令:

ps -p 1 -o comm=

如果输出是 systemd ,恭喜,你走的是主流路径;如果输出是 init ,那基本可以确定是 sysvinit ;如果是 upstart ,则属于 Ubuntu 14.04 及更早版本。注意: ps -p 1 查看的是 PID 1 进程,它是整个 Linux 系统的“祖先进程”,其类型决定了整个系统的初始化框架。别信 which systemctl systemctl --version ,这些命令即使存在,也可能只是兼容层,不代表系统真正在用它管理服务。

我们来拆解两种典型场景的差异:

2.1 systemd 启动的 Ubuntu(主流情况)

这是绝大多数现代 Ubuntu(16.04+ 桌面版/Server 版、WSL2 默认、Docker Desktop 的 WSL 后端)的情况。 dockerd 作为 systemd 管理的服务,其配置遵循 systemd 的服务单元(unit)机制。代理配置必须通过 systemd EnvironmentFile Environment 指令注入到 dockerd 进程的环境变量中,而不是靠 daemon.json 。原因在于: dockerd 在启动时会读取环境变量 HTTP_PROXY HTTPS_PROXY NO_PROXY ,并将其作为默认代理设置应用于所有出站请求(包括 docker pull )。而 daemon.json 中的 proxies 字段,仅用于为容器内的 dockerd 子进程(如构建时的 docker build )提供默认代理,它不控制 dockerd 自身的联网行为。这是一个被官方文档严重弱化的关键点。

所以,在 systemd 环境下,正确的代理配置顺序是:

  1. 设置 dockerd 进程的环境变量 (核心,决定 docker pull 是否成功)
  2. 可选:配置 daemon.json proxies 字段 (影响容器内构建等子任务)
  3. 确保 docker CLI 工具自身也有代理 (影响 docker login 等 CLI 操作,但不影响 pull

提示: dockerd 进程的环境变量优先级高于 daemon.json 。如果两者都配置了代理, dockerd 会以环境变量为准。这也是为什么很多人改了 daemon.json 却没效果——他们真正需要配置的是 dockerd 的启动环境。

2.2 sysvinit/upstart 启动的 Ubuntu(老旧或特殊环境)

这类系统没有 systemd ,自然也没有 systemctl dockerd 通常由 /etc/init.d/docker 脚本启动,或通过 /etc/default/docker 配置文件控制。此时, daemon.json proxies 字段反而成了唯一有效途径,因为 dockerd 会直接读取该文件。但这里有个致命陷阱: /etc/default/docker 文件本身并不被 dockerd 直接读取,它只是 init.d 脚本的配置源。你需要手动修改 /etc/init.d/docker ,在 start() 函数中添加 export HTTP_PROXY=... 等语句,或者更稳妥地,在 /etc/default/docker 中添加 export HTTP_PROXY=... 并确保 init.d 脚本在 source /etc/default/docker 后才启动 dockerd

我曾在一个基于 Ubuntu 14.04 的工业网关设备上遇到此问题。客户提供的镜像禁用了 systemd systemctl 命令不存在。执行 sudo service docker restart 后, docker info | grep -i proxy 显示为空。检查 /etc/init.d/docker ,发现它根本没有 source /etc/default/docker 这一行。最终解决方案是直接编辑 /etc/init.d/docker ,在 start) 分支的 dockerd 命令前插入:

start)
    export HTTP_PROXY="http://your-proxy:8080"
    export HTTPS_PROXY="http://your-proxy:8080"
    export NO_PROXY="localhost,127.0.0.1,.internal-domain.com"
    # ... 原有的启动命令

然后 sudo service docker restart 。这才是 sysvinit 下的“正确姿势”。

2.3 一个快速自检清单:你的代理配置是否真的被 dockerd 加载?

无论你用哪种启动器,最终都要验证 dockerd 进程是否真的拥有了代理环境变量。不要依赖 echo $HTTP_PROXY ,那是当前 shell 的变量,不是 dockerd 的。正确方法是:

# 获取 dockerd 进程的 PID
sudo ps aux | grep dockerd | grep -v grep | awk '{print $2}'

# 查看该进程的环境变量(需要 root 权限)
sudo cat /proc/<PID>/environ | tr '\0' '\n' | grep -i proxy

如果输出包含 HTTP_PROXY=http://... HTTPS_PROXY=http://... ,说明环境变量已成功注入。如果为空,则配置未生效,需回溯检查启动器类型和配置位置。

注意: /proc/<PID>/environ 文件中的变量是以 \0 (空字符)分隔的,必须用 tr '\0' '\n' 转换为换行才能 grep 。这是 Linux 系统级调试的必备技巧,比 systemctl show docker 更底层、更可靠。

3. systemd 环境下的代理配置:三步法确保 dockerd 进程加载代理

确认你的 Ubuntu 使用 systemd 后,就可以进入核心配置环节。这里强调“三步法”,是因为网上 90% 的失败案例,都源于只做了其中一步或两步,而忽略了 systemd 服务管理的完整生命周期。

3.1 第一步:创建独立的环境变量配置文件(推荐)

systemd 推荐将服务的环境变量分离到独立文件中,而非硬编码在 service unit 里。这样便于维护和复用。创建文件 /etc/systemd/system/docker.service.d/http-proxy.conf

[Service]
Environment="HTTP_PROXY=http://your-proxy-ip:8080"
Environment="HTTPS_PROXY=http://your-proxy-ip:8080"
Environment="NO_PROXY=localhost,127.0.0.1,docker-registry.internal.com"

注意:

  • 文件名必须以 .conf 结尾,且放在 /etc/systemd/system/docker.service.d/ 目录下, systemd 会自动合并。
  • Environment 指令支持多次出现,每个变量一行,清晰易读。
  • NO_PROXY 的值必须是逗号分隔的域名/IP 列表, 不能有空格 localhost,127.0.0.1 是必须项,否则 dockerd 会尝试用代理连接本地 socket。
  • 如果你的代理需要认证,格式为 http://username:password@proxy-ip:port 。但强烈建议避免在明文配置中写密码,应使用 http://username@proxy-ip:port 并在代理服务器上配置免密认证。

为什么推荐此法而非 systemctl edit docker ?因为 systemctl edit 创建的覆盖文件,其内容会被 systemd 解析为完整的 unit 文件片段,容易因格式错误(如多了一个空格)导致 systemd 加载失败。而独立的 .conf 文件结构简单,容错性更高。

3.2 第二步:重载 systemd 配置并重启 docker 服务

配置文件创建后, systemd 不会自动感知。必须显式通知:

# 重新加载所有 unit 文件,让 systemd 读取新配置
sudo systemctl daemon-reload

# 重启 docker 服务,使新环境变量生效
sudo systemctl restart docker

# 验证服务状态,确保没有报错
sudo systemctl status docker

关键点在于 daemon-reload 。很多用户跳过这一步,直接 restart ,结果配置依旧不生效。 daemon-reload systemd 的“刷新缓存”操作,它会重新解析 /etc/systemd/system/ /usr/lib/systemd/system/ 下的所有 unit 文件。没有它, systemd 根本不知道你新建了 http-proxy.conf

3.3 第三步:验证 dockerd 进程是否加载了环境变量

这是最关键的验证步骤,也是区分“配置完成”和“配置生效”的分水岭:

# 获取 dockerd 进程 PID
DOCKER_PID=$(sudo pgrep -f "dockerd.*--host" | head -n1)

# 查看其环境变量
if [ -n "$DOCKER_PID" ]; then
    echo "dockerd PID: $DOCKER_PID"
    sudo cat /proc/$DOCKER_PID/environ | tr '\0' '\n' | grep -i proxy
else
    echo "dockerd 进程未找到,请检查 docker 服务是否运行"
fi

如果输出类似:

HTTP_PROXY=http://your-proxy-ip:8080
HTTPS_PROXY=http://your-proxy-ip:8080
NO_PROXY=localhost,127.0.0.1,docker-registry.internal.com

恭喜,第一步成功。此时 docker pull 应该可以工作了。

实操心得:我在测试时发现, pgrep -f "dockerd" 有时会匹配到 dockerd --debug 这样的调试进程,导致 PID 错误。因此我加了 --host 作为更精确的匹配条件,因为生产环境的 dockerd 必定会监听 host socket。这是从无数次 ps aux | grep dockerd 的混乱输出中总结出的精准定位技巧。

4. daemon.json 的正确用法:它不控制 docker pull ,但控制容器内构建

dockerd 进程的环境变量配置成功后, docker pull 就能顺利拉取镜像了。但你会发现,如果在容器内执行 apt update npm install ,依然会超时。这是因为 dockerd 的代理只影响 dockerd 自身的出站请求(如连接 registry),而容器内部是一个完全独立的网络命名空间,它默认不继承宿主机的任何网络设置,包括代理。

这时, /etc/docker/daemon.json proxies 字段才真正派上用场。它的作用是: 为所有新创建的容器,设置默认的 HTTP_PROXY HTTPS_PROXY NO_PROXY 环境变量。 这些变量会自动注入到容器的 env 中,容器内的应用(如 apt curl pip )就能识别并使用它们。

4.1 daemon.json 的标准结构与代理配置

/etc/docker/daemon.json 是一个 JSON 文件,必须严格符合语法。一个典型的、包含代理配置的 daemon.json 如下:

{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  },
  "proxies": {
    "default": {
      "httpProxy": "http://your-proxy-ip:8080",
      "httpsProxy": "http://your-proxy-ip:8080",
      "noProxy": "localhost,127.0.0.1,.internal-domain.com"
    }
  }
}

关键细节:

  • proxies 是顶级键, default 是其子键,表示这是对所有容器的默认设置。
  • httpProxy httpsProxy 的值必须是 完整的 URL ,即 http://... https://... ,不能省略协议头。
  • noProxy 的值与 systemd 配置中的 NO_PROXY 格式相同,逗号分隔,无空格。
  • 文件必须是有效的 JSON:末尾不能有多余的逗号,字符串必须用双引号,布尔值不能用单引号。

4.2 为什么 daemon.json 不能替代 systemd 环境变量?

这是一个根本性误解。我们用一个实验来证明:

  1. 清空 systemd http-proxy.conf ,只保留 daemon.json 中的 proxies
  2. sudo systemctl daemon-reload && sudo systemctl restart docker
  3. 执行 sudo docker pull nginx:alpine —— 失败 ,卡在 trying to pull repository
  4. 执行 sudo docker run --rm -it alpine sh -c 'echo $HTTP_PROXY' —— 输出 http://your-proxy-ip:8080 ,证明 daemon.json 生效了。

结论清晰: daemon.json proxies 只影响容器内环境变量,不影响 dockerd 自身的联网能力。 docker pull dockerd 发起的请求,它不经过容器环境,所以 daemon.json 对它完全无效。只有 dockerd 进程自身的环境变量(通过 systemd 注入)才能控制 pull 行为。

4.3 一个实用技巧:为特定容器覆盖默认代理

daemon.json 设置的是全局默认值,但有时你需要为某个容器禁用代理(比如访问内网 registry),或使用不同的代理。这时可以在 docker run 命令中用 -e 参数覆盖:

# 启动一个容器,禁用代理(覆盖 daemon.json 的 default)
sudo docker run -e HTTP_PROXY="" -e HTTPS_PROXY="" -e NO_PROXY="" --rm -it ubuntu:20.04 apt update

# 启动一个容器,使用不同的代理
sudo docker run -e HTTP_PROXY="http://other-proxy:8080" --rm -it ubuntu:20.04 curl -v http://example.com

-e 参数的优先级最高,会覆盖 daemon.json dockerd 进程环境变量。这是调试和临时需求的利器。

5. 全链路排错:从 docker pull 失败到定位 systemd 启动问题的完整过程

现在,让我们把前面所有知识点串联起来,模拟一次真实的、从零开始的排错之旅。假设你刚在一台全新的 Ubuntu 20.04 Server(通过 VMware 安装)上完成了 Docker 安装,执行 sudo docker pull hello-world 却失败了。以下是完整的、可复现的排查链路。

5.1 第一步:观察现象,收集原始错误信息

不要急于百度,先看终端输出:

$ sudo docker pull hello-world
Using default tag: latest
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)

这个错误非常经典,它明确指向 dockerd 无法连接到 registry-1.docker.io 。但“无法连接”有无数种可能:DNS 解析失败、防火墙拦截、代理配置错误、网络不通。我们需要一层层剥开。

5.2 第二步:验证基础网络连通性

先排除最底层的网络问题:

# 测试 DNS 解析
nslookup registry-1.docker.io

# 测试 TCP 连通性(不走 HTTP,绕过代理和 TLS)
nc -zv registry-1.docker.io 443

# 测试 HTTPS 连通性(模拟 dockerd 的请求)
curl -vI https://registry-1.docker.io/v2/

如果 nslookup 失败,说明 DNS 配置有问题,需检查 /etc/resolv.conf ;如果 nc 失败,说明网络路由或防火墙问题;如果 curl 成功返回 HTTP/2 200 ,则证明网络本身是通的,问题一定出在 dockerd 的配置上。

5.3 第三步:检查 dockerd 日志,寻找线索

dockerd 的详细日志是黄金线索:

# 查看最近 50 行日志
sudo journalctl -u docker -n 50 --no-pager

# 或者实时跟踪日志
sudo journalctl -u docker -f

在日志中搜索关键词 proxy http registry 。你可能会看到:

level=info msg="Starting up"
level=info msg="parsed scheme: \"unix\"" module=grpc
...
level=error msg="failed to dial registry-1.docker.io:443: context deadline exceeded"

但更关键的是,你可能看到一行被忽略的警告:

system has not been booted with systemd as init system (pid 1). can't operate

这行日志通常出现在 journalctl 输出的开头,很容易被滚动刷掉。它意味着你正在用 systemctl 命令操作一个非 systemd 系统。这是整个排查链的转折点。

5.4 第四步:确认启动器类型,修正操作路径

执行 ps -p 1 -o comm= 。如果输出是 init ,那么你立刻知道:所有 systemctl 命令都是无效的。此时,你应该:

  1. 放弃 systemctl edit docker ,转而检查 /etc/default/docker
  2. 如果该文件不存在,创建它,并添加:
export HTTP_PROXY="http://your-proxy:8080"
export HTTPS_PROXY="http://your-proxy:8080"
export NO_PROXY="localhost,127.0.0.1"
  1. 检查 /etc/init.d/docker 脚本,确认它在启动 dockerd source /etc/default/docker 。如果没有,按前文所述手动添加 export 语句。
  2. 重启服务: sudo service docker restart

5.5 第五步:终极验证与自动化脚本

为了不再重复这个痛苦的过程,我写了一个一键检测脚本 docker-proxy-check.sh

#!/bin/bash
echo "=== Docker 代理配置健康检查 ==="

# 1. 检查启动器
echo -n "1. 系统启动器: "
INIT_TYPE=$(ps -p 1 -o comm= 2>/dev/null)
echo "$INIT_TYPE"

# 2. 检查 dockerd 进程环境变量
echo -n "2. dockerd 环境变量 (HTTP_PROXY): "
DOCKER_PID=$(sudo pgrep -f "dockerd.*--host" | head -n1)
if [ -n "$DOCKER_PID" ]; then
    sudo cat /proc/$DOCKER_PID/environ 2>/dev/null | tr '\0' '\n' | grep -i "^HTTP_PROXY=" | cut -d= -f2
else
    echo "未运行"
fi

# 3. 检查 daemon.json
echo -n "3. daemon.json proxies: "
if [ -f /etc/docker/daemon.json ]; then
    jq -r '.proxies.default.httpProxy // "未配置"' /etc/docker/daemon.json 2>/dev/null
else
    echo "文件不存在"
fi

# 4. 尝试一次最小化 pull
echo "4. 尝试拉取 hello-world (5秒超时): "
timeout 5 sudo docker pull hello-world:latest >/dev/null 2>&1 && echo "✅ 成功" || echo "❌ 失败"

echo "=== 检查结束 ==="

将它保存为 docker-proxy-check.sh chmod +x 后运行,即可获得一份清晰的诊断报告。这是我给团队新人的入职必装工具,每次环境搭建后运行一次,5 秒内就知道代理配置是否到位。

经验教训:在 Vagrant 环境中, vagrant box add 下载的 Ubuntu 镜像,其 systemd 支持是“阉割版”的。即使 ps -p 1 显示 systemd systemctl 也可能无法管理服务。此时, service docker restart 才是真正可靠的重启命令。这是我在为 12 个不同 Vagrant Box 编写 CI 脚本时,踩了 7 次坑后总结出的血泪经验。

6. 特殊 Ubuntu 场景的适配方案:WSL2、Docker Desktop、Snap 安装

Ubuntu 的部署场景千差万别,通用方案在特殊环境下往往失效。下面针对几个高频“坑点”场景,给出定制化解决方案。

6.1 WSL2 Ubuntu: systemd 默认禁用,需手动开启

WSL2 的 Ubuntu 默认不启用 systemd ps -p 1 显示 init 。这是微软的刻意设计,以减少资源占用。但 Docker Desktop 的 WSL 后端又依赖 systemd 。矛盾由此产生。

解决方案:

  1. 升级 WSL2 内核到最新版( wsl --update )。
  2. %USERPROFILE%\AppData\Local\Packages\...\wsl.conf 中添加:
[boot]
systemd=true
  1. 重启 WSL: wsl --shutdown ,然后重新打开 Ubuntu。
  2. 执行 ps -p 1 ,确认输出为 systemd
  3. 此后,即可按 systemd 方案配置代理。

注意: wsl.conf 的路径因 Windows 用户名和 WSL 发行版名称而异,需在 Windows 文件资源管理器中手动导航查找。这是 WSL2 用户最容易卡住的一步。

6.2 Docker Desktop for Windows/macOS:代理配置在宿主机,而非 WSL

如果你使用的是 Docker Desktop,那么 dockerd 运行在 Windows/macOS 的虚拟机中,而非你的 WSL Ubuntu。你在 WSL 里配置的 systemd daemon.json ,对 Docker Desktop 的 dockerd 完全无效。

正确做法:

  • Windows : 打开 Docker Desktop → Settings → Resources → Proxies → 勾选 Manual proxy configuration ,填入代理地址。
  • macOS : Docker Desktop → Preferences → Resources → Proxies → 同上。
  • 配置后,Docker Desktop 会自动将代理设置同步到其内部的 Linux VM 中。

此时,你在 WSL Ubuntu 里执行 docker pull ,请求会经由 Docker Desktop 的 VM 中转,代理已生效。无需在 WSL 里做任何配置。

6.3 Snap 安装的 Docker: daemon.json 被忽略,必须用 snap 的接口

Ubuntu 官方软件中心安装的 Docker,通常是 snap 包。 snap 应用有严格的沙盒限制,它不会读取 /etc/docker/daemon.json ,因为该路径不在其安全范围内。

解决方案:

  1. 查看 snap 的配置选项: snap get docker
  2. 使用 snap set 命令设置代理:
sudo snap set docker daemon='{"proxies":{"default":{"httpProxy":"http://your-proxy:8080","httpsProxy":"http://your-proxy:8080"}}}'
  1. 重启 snap 服务: sudo snap restart docker

snap 的配置是 JSON 字符串,必须用单引号包裹,且内部双引号需转义。这是 snap 机制的固有复杂性,无法绕过。

7. 最后的实战检验:用一个真实项目验证所有配置

理论终须实践。我们用一个最贴近开发日常的场景来收尾:在 Ubuntu 上,通过代理拉取 mysql:5.7 镜像,并启动一个容器,使其内部的 apt update 也能成功。

7.1 步骤分解与预期结果

  1. 前提 :Ubuntu 22.04, systemd 启动,已按前文配置好 systemd 环境变量和 daemon.json

  2. 执行 docker pull

    time sudo docker pull mysql:5.7
    

    预期:在 2 分钟内完成,无超时错误。 time 命令会显示实际耗时,可用于对比优化效果。

  3. 启动容器并验证内部代理

    # 启动一个交互式容器
    sudo docker run -it --rm mysql:5.7 bash
    
    # 在容器内执行
    root@abc123:/# echo $HTTP_PROXY
    http://your-proxy-ip:8080
    root@abc123:/# apt update | head -n 5
    Get:1 http://archive.ubuntu.com/ubuntu focal InRelease [265 kB]
    ...
    Fetched 265 kB in 2s (132 kB/s)
    Reading package lists... Done
    

    预期: echo $HTTP_PROXY 输出你的代理地址; apt update 能成功获取包列表,且速度合理(非 0B/s)。

  4. 验证 NO_PROXY 的作用

    # 启动一个容器,尝试连接本地服务(假设你有本地 nginx)
    sudo docker run -it --rm --network host ubuntu:20.04 curl -v http://localhost:8080
    

    预期: curl 应直接连接 localhost ,而不经过代理。如果 NO_PROXY 配置正确,响应会很快;如果错误, curl 会尝试连接代理服务器,导致超时或 404。

7.2 一个不容忽视的细节:代理服务器的 NO_PROXY 白名单

NO_PROXY 不仅要配置在客户端,更要配置在代理服务器端。例如,如果你的代理是 Squid,其配置文件 /etc/squid/squid.conf 中,必须有类似:

acl localnet src 127.0.0.1/32
acl localnet src 10.0.0.0/8
acl localnet src 172.16.0.0/12
acl localnet src 192.168.0.0/16
...
http_access allow localnet

否则,即使客户端 NO_PROXY 设置了 localhost ,请求仍会发往代理服务器,而代理服务器若未放行 localhost ,就会拒绝该请求。这是一个典型的“两端配置不一致”导致的疑难杂症。

我在为客户排查时,曾花了 3 小时才发现问题根源在代理服务器的 ACL 规则上。 dockerd 发出的 localhost 请求,被 Squid 认为是非法的 src 地址而拒绝。最终解决方案是在 Squid 配置中添加 acl localhost src 127.0.0.1/32 http_access allow localhost 。这个教训提醒我们:网络代理是一个端到端的系统,客户端的配置只是其中一环。

8. 我的个人体会:代理配置不是终点,而是理解 Docker 架构的起点

写完这篇指南,我翻看了自己过去三年的运维笔记,发现关于 Docker 代理的记录有 17 次之多。每一次,都对应着一个不同的 Ubuntu 部署场景:从裸金属服务器到 OpenStack 虚拟机,从 AWS EC2 到阿里云 ECS,从物理机房的国产化 ARM 服务器到客户现场的离线内网环境。每一次的解决方案都不尽相同,但背后的核心逻辑始终如一: Docker 不是一个黑箱,而是一个由 dockerd 守护进程、 docker CLI 工具、容器运行时(containerd)、以及底层 Linux 内核特性(cgroups, namespaces)共同构成的精密系统。代理配置,只是这个系统与外部网络世界对话的一个接口。

因此,与其死记硬背“改哪个文件”,不如培养一种系统性的调试思维:

  • 分层 :区分 dockerd 进程、 docker CLI、容器内应用三个层级的网络行为。
  • 溯源 :遇到问题,第一反应不是“怎么配”,而是“这个请求是由谁发起的?它运行在哪个进程里?那个进程的环境是什么?”
  • 验证 :永远用 ps , cat /proc/PID/environ , journalctl 这些底层命令去验证,而不是相信配置文件的存在就等于配置生效。

最后分享一个小技巧:在团队内部,我要求所有成员在提交 Docker 相关的 CI/CD 脚本时,必须在脚本开头加入一段注释,明确写出该脚本所依赖的 Ubuntu 版本、Docker 安装方式(deb/snap/binary)、以及代理配置的生效路径(如 systemd env file: /etc/systemd/system/docker.service.d/proxy.conf )。这看似繁琐,却在无数次跨环境部署中,避免了“在我机器上是好的”这类经典甩锅。

Docker 的魅力,正在于它用简单的命令封装了复杂的系统。而真正的掌握,始于敢于掀开这层封装,直面其下每一个齿轮的咬合。当你能从容地在 ps -p 1 cat /proc/$(pgrep dockerd)/environ 之间自由切换时,你就已经超越了“保姆级教程”的范畴,进入了“自主掌控”的境界。

更多推荐