Docker 走代理的 3 种姿势:daemon / Compose / host 网络

目录

Docker 走代理是个看似简单实则坑很深的话题,因为它有三个完全独立的层级:

  1. docker pull 拉镜像走代理(Docker daemon 的代理)
  2. 容器内程序自己走代理(容器进程的环境变量)
  3. Compose 跨服务怎么访问宿主机代理

每个层级的配置位置都不一样,搞混了排查能查一天。这篇按这三个层级分别讲。

一、整体关系

流程图流程图
层级 配置位置 影响
Daemon /etc/systemd/system/docker.service.d/proxy.conf 拉镜像
容器构建(build) ~/.docker/config.json 或 --build-arg docker build 时容器内的网络请求
容器运行(run) -e HTTP_PROXY=... 或 environment: 容器进程的 HTTP 客户端

二、场景一:让 docker pull 走代理

国内服务器拉 Docker Hub 经常超时,最干净的方案不是改镜像源,而是让 daemon 走你的代理。

2.1 创建 systemd drop-in

sudo mkdir -p /etc/systemd/system/docker.service.d
sudo vim /etc/systemd/system/docker.service.d/proxy.conf

内容:

[Service]
Environment="HTTP_PROXY=http://127.0.0.1:7890"
Environment="HTTPS_PROXY=http://127.0.0.1:7890"
Environment="NO_PROXY=localhost,127.0.0.1,::1,*.aliyun.com,*.tencentyun.com"

💡 NO_PROXY 把国内常用的镜像域名加进去,避免走代理反而慢。

2.2 重启 Docker

sudo systemctl daemon-reload
sudo systemctl restart docker

# 验证
sudo systemctl show docker | grep -i proxy

2.3 取消代理

删掉 proxy.conf 再重启:

sudo rm /etc/systemd/system/docker.service.d/proxy.conf
sudo systemctl daemon-reload
sudo systemctl restart docker

🪤 这个配置只影响 docker pull / docker push,不影响容器内部进程。容器要走代理需要单独配置(场景二)。

三、场景二:让容器内程序走代理

最常见的需求:容器跑了个程序要访问 GitHub / OpenAI,但你的代理在宿主机上。

3.1 简单粗暴:-e 传环境变量

docker run -d \
  -e HTTP_PROXY=http://172.17.0.1:7890 \
  -e HTTPS_PROXY=http://172.17.0.1:7890 \
  -e NO_PROXY=localhost,127.0.0.1,::1 \
  myimage

注意:用宿主机 IP 172.17.0.1(默认 bridge 网关),不要写 127.0.0.1(那是容器自己)。

3.2 Compose 写法

services:
  myapp:
    image: myimage
    environment:
      HTTP_PROXY: http://172.17.0.1:7890
      HTTPS_PROXY: http://172.17.0.1:7890
      NO_PROXY: localhost,127.0.0.1,::1

3.3 ~/.docker/config.json(构建时也生效)

{
  "proxies": {
    "default": {
      "httpProxy": "http://172.17.0.1:7890",
      "httpsProxy": "http://172.17.0.1:7890",
      "noProxy": "localhost,127.0.0.1,::1"
    }
  }
}

这种方式对所有 docker build 和 docker run 都自动生效,省去每次写 -e。

⚠️ 程序必须遵守 HTTP_PROXY 环境变量才会走代理。比如 curl / wget / Python 的 requests / Java 都遵守;但有些程序不读这个变量(如 Go 自己实现的 HTTP 客户端、部分 nodejs 库),那就只能在程序代码里改。

四、场景三:宿主机代理在 Compose 网络中“找不到”

这是 Compose 用户最高频的坑:

用 host.docker.internal 在 Compose 容器里根本访问不到宿主机代理。

4.1 为什么 host.docker.internal 不行?

# 这种写法在 Compose 里基本不工作
services:
  app:
    image: myimage
    extra_hosts:
      - "host.docker.internal:host-gateway"

原理:

  • host-gateway 是 Docker 的魔法关键字,会被解析成默认 bridge 网桥的网关 IP 172.17.0.1。
  • 但 Compose 会为每个项目创建独立网络,网关 IP 是 172.18.0.1 / 172.19.0.1 等,和 172.17.0.1 不在同一段。
  • 结果:容器把 host.docker.internal 解析成 172.17.0.1,但自己网络段是 172.18.0.0/16,根本到不了 172.17.0.1。

4.2 查看你 Docker 上所有网关 IP

docker network inspect $(docker network ls -q) \
  --format '{{.Name}}: Subnet={{(index .IPAM.Config 0).Subnet}} Gateway={{(index .IPAM.Config 0).Gateway}}'

输出示例:

bridge:               Subnet=172.17.0.0/16 Gateway=172.17.0.1
myapp_default:        Subnet=172.18.0.0/16 Gateway=172.18.0.1
otherproject_default: Subnet=172.19.0.0/16 Gateway=172.19.0.1

bridge 才是 Docker 默认的;其它都是 Compose 自动创建的。

4.3 正确做法:手动指定 Compose 网络段 + 网关

services:
  myapp:
    image: myimage
    extra_hosts:
      - "host.docker.internal:172.30.0.1"      # 指向下面定义的网关
    environment:
      HTTP_PROXY: http://host.docker.internal:7890
      HTTPS_PROXY: http://host.docker.internal:7890

networks:
  default:
    ipam:
      config:
        - subnet: 172.30.0.0/16
          gateway: 172.30.0.1

关键点:

  1. 自定义网段和网关(172.30.0.0/16,挑一个没被占用的)
  2. extra_hosts 写死这个网关 IP,不要用 host-gateway 魔法值
  3. 容器里访问 host.docker.internal → 172.30.0.1 → 宿主机

4.4 必备:宿主机防火墙放开这个网段

⚠️ 重要:默认 ufw 会拦截除 127.0.0.1 外所有源(含 Docker 网段)。容器请求宿主机代理会被防火墙挡掉。

sudo ufw allow from 172.30.0.0/16
sudo ufw reload

或者代理软件层面允许 LAN(如 mihomo 的 allow-lan: true 配合 bind-address: "*")。

4.5 验证容器内能否解析 + 连通

docker exec -it myapp sh

# 解析
getent ahosts host.docker.internal
# 应该显示 172.30.0.1

# 连通
nc -zv host.docker.internal 7890
# 应该 succeeded

五、environment 代理 vs 应用配置文件代理

很多人发现:明明 environment 写了 HTTP_PROXY,应用的某些请求还是没走代理——这是因为两者作用层级不同:

层级 影响范围
environment 的 HTTP_PROXY 进程级,影响通用 HTTP 库(curl、Python requests、Java HttpClient 等)
应用自己的 config.yaml 里的 proxy 业务级,应用特定 API 走的代理

举例:某个 AI 网关程序

  • environment: HTTP_PROXY=... → 控制下载管理面板、访问 GitHub API 等“框架请求”
  • config.yaml 里的 proxy → 控制转发到 OpenAI 的“业务请求”

两个都要配,不能互相覆盖。

六、host 网络模式:最简单粗暴的方案

如果你不想折腾网络段,直接让容器和宿主机共享网络栈:

docker run -d --network host myimage
services:
  myapp:
    image: myimage
    network_mode: host
    environment:
      HTTP_PROXY: http://127.0.0.1:7890     # 直接写 127.0.0.1
      HTTPS_PROXY: http://127.0.0.1:7890

优点:

  • 127.0.0.1 就是宿主机的 127.0.0.1
  • 不用映射端口(-p 失效)
  • 性能最好(无 NAT)

缺点:

  • 失去网络隔离(容器能看到宿主机所有端口)
  • 多个容器端口冲突(不能再有两个监听 80 的容器)
  • 不能用 Compose 的容器名 DNS(host 模式没有 Compose 网络)

适合:单机部署、性能敏感、不需要多容器互联的场景。

七、Docker 镜像加速(拉镜像的另一条路)

如果不想配代理,国内可以用镜像加速器:

sudo vim /etc/docker/daemon.json
{
  "registry-mirrors": [
    "https://docker.1ms.run"
  ]
}
sudo systemctl restart docker
docker info | grep -A 10 "Registry Mirrors"

⚠️ 国内可用的镜像源经常被封 / 失效,建议配 3-5 个,失败自动 fallback。这条路适合只想拉镜像、不需要容器内程序走代理的场景。

八、调试代理是否生效

按“层级”排查:

8.1 daemon 代理生效了吗?

sudo systemctl show docker | grep -i proxy

8.2 镜像拉得动了吗?

docker pull hello-world

8.3 容器能访问宿主机吗?

docker run --rm -it alpine sh
# 进容器后
apk add curl
curl -v http://172.17.0.1:7890

8.4 容器内程序确实读到环境变量了吗?

docker exec myapp env | grep -i proxy

8.5 程序是否遵守环境变量?

docker exec myapp curl -v https://www.google.com
# 如果走代理,会看到 Proxy-Connection 之类的头

九、踩坑速查表

现象 原因 解决
docker pull 慢 daemon 没配代理 配 /etc/systemd/system/docker.service.d/proxy.conf
配了 daemon 代理,容器还是慢 容器不读 daemon 代理 容器需单独 -e HTTP_PROXY
容器内 curl 走代理失败 ufw 挡了 Docker 网段 ufw allow from <Docker 网段>
Compose 用 host.docker.internal 不通 跨网段问题 手动指定网段 + 网关
host-gateway 解析成 172.17.0.1 但不通 Compose 网络段不是 172.17 用上面的“指定网关”方案
环境变量配了程序还是没代理 程序不读 HTTP_PROXY 在程序代码里改

十、几条实用经验

  • 能用宿主机代理就别在容器里再装 mihomo——多套一层全是麻烦。
  • 172.17.0.1 只在默认 bridge 网络下可用,Compose 自定义网络要算自己的网关。
  • 代理 + 防火墙是组合拳:代理通了但 ufw 挡了网段,照样不通。
  • docker exec ... env 是排查“环境变量是否生效”的第一招。
  • 混用 daemon 代理 + 容器代理没关系,分别管不同事。

把这三个层级理清楚,后面再遇到任何“Docker 走不通”的问题,定位都是几分钟的事。