← 返回博客

Linux、Docker 和 CI 中的 HTTP_PROXY、HTTPS_PROXY、NO_PROXY 变量:7 个常见错误

为什么代理环境变量在一个容器中有效而在另一个容器中失效——分析7个真实案例及其解决方案和代码。

📅2026年11月7日

环境变量 HTTP_PROXY, HTTPS_PROXY 和 NO_PROXY 看起来非常简单:设置变量后,所有流量都通过代理。在实践中,正是在这一点上,开发人员花费了数小时进行调试:代理在终端中工作,但在Docker容器中却不工作;curl通过代理工作,而Python应用程序却不工作;CI运行器突然禁止内部注册表,因为NO_PROXY的语法不正确。我们将分析7个真实的陷阱,并展示如何通过具体代码来规避它们。

应用程序如何读取代理变量

重要的是要理解:HTTP_PROXY、HTTPS_PROXY和NO_PROXY并不是HTTP协议的标准,而是每个库以自己的方式实现的约定。Linux内核对这些变量一无所知——它们在网络客户端初始化阶段被特定的应用程序或库读取。

例如,curl和基于libcurl的工具在每次进程启动时读取变量。Python库requests通过模块urllib.request.getproxies()读取它们,该模块在创建会话时扫描进程的环境。而Node.js内置的fetch()在18版本之前根本不考虑这些变量,除非使用额外的包。

因此,第一个规则是:代理变量仅在特定库明确支持的地方有效。通过curl进行的检查并不能保证您的Go、Java或Node.js应用程序会表现得一样。

export HTTP_PROXY="http://user:[email protected]:8080"
export HTTPS_PROXY="http://user:[email protected]:8080"
export NO_PROXY="localhost,127.0.0.1,.internal.corp"

curl -v https://api.example.com/ping
# curl将在详细日志中显示:"Trying proxy.example.com..."

陷阱1:变量的大小写和httpoxy漏洞

大多数Linux控制台工具以小写读取变量:http_proxy、https_proxy、no_proxy。许多现代SDK则依赖于大写。这个差异看似微不足道,但正是因为它,"在本地工作,但在服务器上不工作"的错误在设置的第一天就会发生。

另一个故事是httpoxy漏洞(CVE-2016-5385及相关)。其本质在于,CGI环境(PHP、Python WSGI、某些Ruby框架)会自动将传入请求的HTTP头Proxy转换为环境变量HTTP_PROXY。攻击者发送头Proxy: http://attacker.com,您的服务器应用程序开始通过外部代理服务器发送出站请求,从而打开了MITM攻击和数据泄露的途径。

解决方案是始终同时在两个大小写中设置变量,并在Web服务器级别明确将HTTP_PROXY置为零,以处理传入请求:

# 为了兼容性同时设置两个大小写
export http_proxy="http://proxy.example.com:8080"
export HTTP_PROXY="http://proxy.example.com:8080"
export https_proxy="http://proxy.example.com:8080"
export HTTPS_PROXY="http://proxy.example.com:8080"

# 在nginx中——阻止来自客户端的Proxy头
proxy_set_header Proxy "";

陷阱2:NO_PROXY的语法在不同工具之间有所不同

NO_PROXY是一个例外列表,但域名和掩码的记录格式并没有标准化。一些工具将.example.com理解为"任何子域",其他工具则要求*.example.com,还有一些工具只理解完全匹配的主机,而根本不支持通配符。

例如,Go库net/http支持NO_PROXY中的CIDR表示法(10.0.0.0/8),而经典的curl在某些版本之前则不支持。这导致同一个NO_PROXY在一个CI步骤中正确排除了内部注册表,而在另一个用不同语言编写的步骤中却没有排除。

工具 子域的通配符 CIDR掩码
curl / libcurl 通过.example.com 不支持
Go net/http 通过.example.com 支持
Python requests 通过.example.com 不支持
npm / Node.js 需要*.example.com 不支持

实用建议:同时列出NO_PROXY中的域名和通配符选项,以覆盖不同的解析器:

export NO_PROXY="localhost,127.0.0.1,.internal.corp,*.internal.corp,10.0.0.0/8"

陷阱3:Docker build与Docker run——不同的环境变量

这可能是Docker中最常见的陷阱。通过docker run -e HTTP_PROXY=...设置的变量仅在容器运行期间有效。它们不会自动传递到镜像构建阶段。如果您的Dockerfile内部调用apt-get install或pip install,并且构建在企业代理后进行,构建将会因为超时而挂起。

要进行构建,需要传递build-args,Docker从1.9版本开始"开箱即用"地理解这些参数,而无需在Dockerfile中声明:

docker build \
  --build-arg HTTP_PROXY=http://proxy.example.com:8080 \
  --build-arg HTTPS_PROXY=http://proxy.example.com:8080 \
  --build-arg NO_PROXY=localhost,127.0.0.1 \
  -t my-app:latest .

如果您希望变量在已启动的容器内仍然可用,则需要通过Dockerfile中的ENV明确复制它——ARG和ENV在Docker中是根本不同的机制,ARG在构建层后立即消失:

ARG HTTP_PROXY
ARG HTTPS_PROXY
ARG NO_PROXY

ENV HTTP_PROXY=${HTTP_PROXY}
ENV HTTPS_PROXY=${HTTPS_PROXY}
ENV NO_PROXY=${NO_PROXY}

RUN apt-get update && apt-get install -y curl

另一个细节:Docker守护进程本身可能会使用代理从注册表下载镜像——这不是在Dockerfile中配置的,而是在systemd单元的配置中/etc/systemd/system/docker.service.d/http-proxy.conf中设置的,这常常与容器内的变量混淆。

陷阱4:docker-compose和服务之间的变量继承

在docker-compose中,主机的环境变量不会自动传递到容器中,除非您在environment部分中明确列出它们,或者使用.env文件。一个常见的错误是在运行docker-compose up之前在终端中设置代理,并期望所有服务都能接收到它。

version: "3.9"
services:
  scraper:
    build:
      context: .
      args:
        HTTP_PROXY: "${HTTP_PROXY}"
        HTTPS_PROXY: "${HTTPS_PROXY}"
    environment:
      - HTTP_PROXY=${HTTP_PROXY}
      - HTTPS_PROXY=${HTTPS_PROXY}
      - NO_PROXY=localhost,127.0.0.1,db,redis
    depends_on:
      - db
      - redis

请注意NO_PROXY:在docker-compose网络中,容器通过服务名称(db、redis)而不是localhost相互访问。如果忘记将服务名称添加到排除列表中,容器之间的所有内部流量也将通过外部代理,这将增加延迟,并可能在代理不允许内部协议(如PostgreSQL或Redis)时导致连接失败。

陷阱5:CI/CD运行器和代理秘密泄露

在GitHub Actions、GitLab CI和Jenkins中,代理变量通常在运行器或整个组织级别设置,这会产生两个问题。首先,如果代理的URL包含明文的用户名和密码,它将在任何打印环境的命令(env、printenv、脚本中的调试输出)中出现在构建日志中。其次,自托管的运行器有时会继承主机的系统代理,而该代理并不适合CI作业的流量。

# .github/workflows/build.yml
jobs:
  build:
    runs-on: ubuntu-latest
    env:
      HTTP_PROXY: ${{ secrets.PROXY_URL }}
      HTTPS_PROXY: ${{ secrets.PROXY_URL }}
      NO_PROXY: "localhost,127.0.0.1,github.com"
    steps:
      - uses: actions/checkout@v4
      - name: 测试连接
        run: curl -sS https://api.example.com/health

安全规则:仅在CI的秘密中存储代理的URL(GitHub Secrets、GitLab CI/CD Variables带有"masked"标志),永远不要在调试日志中完整输出环境变量,并限制NO_PROXY,以确保对CI平台本身(例如github.com、gitlab.com)的请求不通过代理——否则,如果在高峰时段代理不可用,构建可能会挂起。

陷阱6:代理的用户名和密码中的特殊字符

如果代理的密码包含字符@、:、/或#,URL解析器会在错误的位置将字符串拆分成多个部分。经典错误:密码P@ssw0rd在字符串http://user:P@[email protected]:8080中,解析器会将主机读取为"[email protected]",而不是"proxy.com"。

解决方案是始终在将用户名和密码插入变量之前进行URL编码:

# Python:在构建代理字符串之前编码密码
import urllib.parse

user = "login"
password = "P@ss:w0rd/1"
encoded_password = urllib.parse.quote(password, safe="")
proxy_url = f"http://{user}:{encoded_password}@proxy.example.com:8080"

import os
os.environ["HTTPS_PROXY"] = proxy_url
# Node.js:通过encodeURIComponent进行类似的构建
const user = "login";
const password = "P@ss:w0rd/1";
const encoded = encodeURIComponent(password);
const proxyUrl = `http://${user}:${encoded}@proxy.example.com:8080`;

process.env.HTTPS_PROXY = proxyUrl;

陷阱7:忽略env的应用程序

并非所有HTTP客户端都尊重系统代理变量。在Python中,库requests会通过requests.Session()自动读取它们,但如果您一次性明确传递了字典proxies={},这将覆盖环境变量,流量将直接通过,即使系统中设置了HTTP_PROXY。

import requests

# 空字典proxies会覆盖环境变量
response = requests.get("https://api.example.com", proxies={})
# 正确的做法是明确传递代理,或者根本不传递proxies
proxies = {
    "http": "http://user:[email protected]:8080",
    "https": "http://user:[email protected]:8080",
}
response = requests.get("https://api.example.com", proxies=proxies)

在Node.js中,内置的fetch在没有额外库的情况下根本不知道HTTP_PROXY。需要明确引入undici与ProxyAgent或经典的https-proxy-agent:

const { ProxyAgent, setGlobalDispatcher } = require("undici");

const proxyUrl = process.env.HTTPS_PROXY;
if (proxyUrl) {
  setGlobalDispatcher(new ProxyAgent(proxyUrl));
}

fetch("https://api.example.com/data")
  .then(res => res.json())
  .then(console.log);

Java也需要单独的系统属性,而不是环境变量:-Dhttp.proxyHost=proxy.example.com -Dhttp.proxyPort=8080 -Dhttps.proxyHost=proxy.example.com -Dhttps.proxyPort=8080。JVM默认不读取HTTP_PROXY环境变量。

如何正确测试代理变量

在花时间调试应用程序之前,请先独立检查变量和代理。这可以节省数小时。

# 1. 检查变量在当前会话中是否真正设置
env | grep -i proxy

# 2. 直接通过curl检查代理的有效性
curl -x "$HTTP_PROXY" -v https://api.ipify.org

# 3. 从容器内部检查变量是否到达
docker exec -it my_container env | grep -i proxy

# 4. 检查NO_PROXY是否真正排除了所需的主机
curl -v --noproxy "internal.corp" https://service.internal.corp/health

如果任务是对外部API进行大量请求、解析市场或测试CI管道中的广告地理定位,选择合适类型的代理以应对负载非常重要。对于CI中的稳定高负载场景,通常只需使用数据中心代理——在大量构建和测试流量中,它们比住宅代理更快且更便宜。如果您的管道检查来自特定区域的真实用户IP的站点行为,最好连接住宅代理——它们降低了因"数据中心"IP而被封锁的风险。

部署前检查清单

  • 变量同时在大写和小写中设置
  • 代理URL中的用户名和密码通过urllib.parse.quote或encodeURIComponent进行了URL编码
  • NO_PROXY同时包含域名和通配符格式,以便在工具之间兼容
  • 在docker-compose中,NO_PROXY包括来自Compose网络的服务名称,而不仅仅是localhost
  • 对于Docker build,通过Dockerfile中的ENV将build-args与runtime env分开传递
  • CI中的代理URL秘密被掩码,并且不会在调试日志中打印
  • 对于Java应用程序,代理通过系统属性-D设置,而不仅仅是通过env
  • 检查防止httpoxy的措施——在Web服务器级别将客户端的Proxy头置为零

结论

HTTP_PROXY、HTTPS_PROXY和NO_PROXY变量看似简单的约定,实际上却因为字符大小写、例外语法、Docker build与Docker run之间的界限、docker-compose中的环境继承、CI日志中的秘密泄露,以及并非每个库都读取这些变量而破裂。在寻找应用程序业务逻辑中的错误之前,请通过curl和docker exec独立检查每个变量。

如果您的基础设施依赖于解析外部网站、测试广告的地理定位或通过CI管道绕过IP封锁,正确传递环境变量并选择适合任务的代理类型至关重要。对于测试移动流量和检查移动运营商行为的应用程序,使用移动代理是合适的——它们提供真实移动网络的IP,并且更少受到反欺诈系统的过滤。