返回

Docker 代理 %% 之谜:为何 systemd 配置需双重转义?

Linux

解密 Docker 代理配置中的 %%:为何需要双重转义?

刚接触 Docker,想要配置让它通过公司内网的认证代理拉取镜像?这很常见。你可能翻看了 Docker 的官方文档 (Docker Proxy Configuration),然后被其中一个细节搞懵了:

Special characters in the proxy value, such as #?!()[]{}, must be double escaped using %%. For example:

[Service]
Environment="HTTP_PROXY=http://domain%%5Cuser:complex%%[email protected]:3128/"

等一下... 通常给 Linux 应用配代理,遇到特殊字符,比如用户名或密码里的 \ 或者 #,咱们会用标准的 URL 编码 (URL Encode) 处理,像 \ 变成 %5C# 变成 %23

可 Docker 这个文档里的 %%5C 是个什么操作?为啥多了个 %?这不是画蛇添足吗?别急,这背后还真有点说道。

一、 问题现象:令人困惑的 %%

我们通常设置环境变量,像这样就行了(假设密码里有个 #):

export HTTP_PROXY="http://myuser:[email protected]/

这里的 # 被编码成了 %23,这是标准的 URL 百分号编码。

但 Docker 推荐的 systemd 配置文件方式,却要求这样写:

# /etc/systemd/system/docker.service.d/http-proxy.conf
[Service]
Environment="HTTP_PROXY=http://domain%%5Cuser:complex%%[email protected]:3128/"

对比一下:

  • 标准 URL 编码:\ -> %5C
  • Docker 文档示例: \ -> %%5C
  • 标准 URL 编码:# -> %23
  • Docker 文档示例: # (这里假设 pass#word ) -> pass%%[email protected] (按照文档逻辑推断)

多出来的这个 %,到底是什么玄机?

二、 刨根问底:为什么是 %%

这个问题的关键在于,你配置 Docker 代理的地方——通常是 systemd 的 unit 文件(比如 /etc/systemd/system/docker.service.d/http-proxy.conf)。这个配置文件不是直接给 Docker 守护进程(dockerd)读取的 ,而是先由 systemd 这个系统和服务管理器来解析。

这里的配置过程其实涉及了 两层处理

2.1 第一层:Systemd 的变量解析

systemd 在读取 .service.conf 文件时,有自己的一套规则来处理文件内容,特别是 Environment= 这行。systemd 支持一种叫做“说明符(Specifiers)”的机制,允许你在单元文件中使用一些特殊占位符,它们会被 systemd 动态替换成实际的值。

这些说明符很多都是以 % 开头的,比如:

  • %h:用户的主目录
  • %u:运行服务的用户名
  • %H:主机名
  • 等等... (可以看 man systemd.unit 获取完整列表)

问题就出在这里:% 符号在 systemd 的单元文件中是个特殊字符!如果你想在 Environment= 的值里包含一个 字面量% 符号(而不是让 systemd 把它当作说明符的开始),你就必须对它进行 转义systemd 的转义规则很简单:%% 来表示一个普通的 % 字符。

所以,当 systemd 读取到 Environment="HTTP_PROXY=http://domain%%5Cuser:complex%%[email protected]:3128/" 这行时:

  • 它看到 %%,就知道:“哦,这俩连着的百分号代表一个真正的、字面意义上的百分号字符 %。”
  • 然后它继续处理,遇到 5Cuser...

经过 systemd 的第一层处理后,它实际设置到 Docker 守护进程运行环境里的环境变量 HTTP_PROXY 的值就变成了:

http://domain%5Cuser:complex%[email protected]:3128/

看!第一个 % 通过 %% 的转义保留下来了。

2.2 第二层:Docker 守护进程读取环境变量

systemd 处理完配置文件,启动 Docker 守护进程 (dockerd) 时,会将处理过的环境变量传递给 dockerd 进程。

这时候,dockerd 拿到的 HTTP_PROXY 环境变量值是 http://domain%5Cuser:complex%[email protected]:3128/

Docker 守护进程需要解析这个代理 URL。它遵循的是标准的网络代理 URL 格式,其中特殊字符必须 使用标准的百分号编码 (URL Encoding)。

  • %5C : Docker 解析为反斜杠 \
  • %23 : Docker 解析为井号 # (如果密码是 pass#word 的话)
  • %40 : Docker 解析为 @ 符号
  • 等等...

所以,Docker 看到 domain%5Cuser,就正确理解为用户名是 domain\user。它看到 complex%23word (假设密码是 complex#word),就理解为密码是 complex#word

2.3 双重处理的真相

搞明白了吗?总结一下这个过程:

  1. 你写入配置文件 (.conf) 的是 %%5C (代表 \) 或 %%23 (代表 #) 等。
  2. systemd 读取 配置文件,执行它自己的转义规则:%% 替换为 %。于是,它设置给 Docker 进程的环境变量变成了 %5C%23
  3. Docker 守护进程读取 这个环境变量,执行标准的 URL 解码:%5C 解码为 \%23 解码为 #

这就是为什么需要“双重转义” %%。第一个 % 是为了满足 systemd 的转义要求,确保一个字面量的 % 能够存活下来,传递给 Docker 进程。第二个 % 加上后面的十六进制代码(如 5C23)才是真正给 Docker 使用的标准 URL 编码部分。

如果你在 systemd 配置文件里只写了 %5Csystemd 在解析时可能会把它当作一个无效的说明符 %5 处理掉(或者产生警告),最终传递给 Docker 的环境变量可能是错误的,比如只剩下 Cuser,或者整个环境变量设置失败,导致代理不生效。

三、 实战演练:如何正确配置

知道了原理,操作起来就简单了。咱们一步步来配置 Docker 通过 systemd 使用带特殊字符的认证代理。

假设你的代理信息是:

  • 地址:proxy.example.com
  • 端口:3128
  • 用户名:domain\user#name (包含 \#)
  • 密码:P@$$w?rd! (包含 @, $, ?, !)

3.1 找到或创建配置文件

Docker 的 systemd 代理配置通常放在这个目录下:/etc/systemd/system/docker.service.d/。你可能需要创建这个目录。

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

然后创建一个配置文件,比如叫 http-proxy.conf

sudo nano /etc/systemd/system/docker.service.d/http-proxy.conf
# 或者使用你喜欢的编辑器,比如 vim
# sudo vi /etc/systemd/system/docker.service.d/http-proxy.conf

3.2 编辑配置文件

在文件里写入以下基本结构,注意替换成你自己的代理服务器地址和端口:

[Service]
Environment="HTTP_PROXY=http://<你的用户名>:<你的密码>@proxy.example.com:3128/"
Environment="HTTPS_PROXY=http://<你的用户名>:<你的密码>@proxy.example.com:3128/"
Environment="NO_PROXY=localhost,127.0.0.1,*.example.com,corp.internal" # 按需添加不走代理的地址

重点是处理用户名和密码里的特殊字符。

3.3 转义规则实操

遵循我们刚才分析的两步原则:

  1. 先对用户名和密码做标准的 URL 编码 (Percent Encoding)。

    • 用户名:domain\user#name
      • \ -> %5C
      • # -> %23
      • 编码后:domain%5Cuser%23name
    • 密码:P@$$w?rd!
      • @ -> %40 (虽然有些客户端能处理,但编码是更保险的做法)
      • $ -> %24
      • ? -> %3F
      • ! -> %21
      • 编码后:P%40%24%24w%3Frd%21

    现在,我们期望 Docker 最终拿到的 URL 是这样的:
    http://domain%5Cuser%23name:P%40%24%24w%3Frd%21@proxy.example.com:3128/

  2. 再对上一步结果中的 每一个 % 符号,用 %% 替换,以满足 systemd 的要求。

    • 用户名编码 domain%5Cuser%23name 中的 % 替换为 %% -> domain%%5Cuser%%23name
    • 密码编码 P%40%24%24w%3Frd%21 中的 % 替换为 %% -> P%%40%%24%%24w%%3Frd%%21

好了,把这些替换回 .conf 文件里的 Environment 行:

[Service]
Environment="HTTP_PROXY=http://domain%%5Cuser%%23name:P%%40%%24%%24w%%3Frd%%[email protected]:3128/"
Environment="HTTPS_PROXY=http://domain%%5Cuser%%23name:P%%40%%24%%24w%%3Frd%%[email protected]:3128/"
Environment="NO_PROXY=localhost,127.0.0.1,*.example.com,corp.internal"

保存文件并退出编辑器。

3.4 重载配置并重启 Docker

修改了 systemd 的单元文件后,需要让 systemd 重新加载配置,然后重启 Docker 服务使配置生效:

# 让 systemd 知道配置文件变了
sudo systemctl daemon-reload

# 重启 Docker 服务
sudo systemctl restart docker

3.5 验证配置

可以通过几种方式检查配置是否正确加载:

  1. 检查 Docker 服务的环境变量:

    systemctl show --property=Environment docker
    

    输出应该包含类似这样的行(注意这里已经是 systemd 处理后的结果,只有一个 %):
    Environment=HTTP_PROXY=http://domain%5Cuser%23name:P%40%24%24w%3Frd%[email protected]:3128/ ...

  2. 查看 Docker Info:

    docker info | grep -i proxy
    

    如果配置成功,应该能看到设置的 HTTP/HTTPS/No Proxy 信息。

  3. 尝试拉取镜像:

    docker pull hello-world
    

    如果能成功拉取,说明代理配置多半是没问题了。如果失败,检查 /var/log/syslogjournalctl -u docker.service 中的 Docker 日志,可能会有关于代理连接失败的线索。

四、 安全小贴士

  • 凭证敏感性: 代理的用户名密码直接写在配置文件里,虽然方便,但要注意文件的权限。systemd 的配置文件通常只有 root 可读写。可以确认下:sudo ls -l /etc/systemd/system/docker.service.d/http-proxy.conf。确保权限是 644 (owner=root read/write, group=root read, others=read) 或者更严格的 600 (owner=root read/write only)。如果文件被非授权用户读取,可能导致凭证泄露。
  • 专用代理用户: 如果可能,最好为 Docker 或服务器专门创建一个权限较低的代理账户,而不是使用你自己的个人账户。
  • 考虑Secrets管理: 对于更复杂的环境或有更高安全要求的场景,可以研究使用 Docker Swarm secrets 或 Kubernetes secrets,配合专门的代理管理方案,避免明文存储密码。不过对于单个 Docker host,systemd 文件通常是标准做法。

五、 进阶技巧:不仅仅是 systemd

需要强调的是,这种 %% 的双重转义,主要是因为我们通过 systemd 来设置 Docker 守护进程的环境变量。如果你用其他方式 给 Docker 守护进程设置环境变量,可能就不需要 %% 了。

比如,有些老的教程或者特定场景下,可能会修改 /etc/default/docker/etc/sysconfig/docker 文件(取决于你的 Linux 发行版和 Docker 安装方式,但这种方式逐渐被 systemd 取代)。如果这些文件是直接被 shell source (加载) 来设置环境变量,而不是被 systemd 解析,那么你可能只需要标准的 URL 编码 (单个 %)。

同理,如果你是在启动容器时 为容器内部的应用设置代理(比如通过 docker run -e HTTP_PROXY=...),这时环境变量是直接传递给容器内的 shell/进程的,也不需要 %% 双重转义,直接用标准 URL 编码的单个 % 就行了。

简单说就是:看环境变量是谁设置的 。如果是 systemd 读取单元文件设置的,它就要掺和一手,需要 %% 来表示 %。如果是其他机制(如 shell 直接导出、docker run -e)直接设置的,那通常就按标准 URL 编码来。

六、 核心要点回顾

Docker 代理配置中看似奇怪的 %% 要求,源于配置过程的两层处理:

  1. systemd 首先解析 .conf 文件,它把 % 视为特殊字符,需要用 %% 来表示一个字面量的 %
  2. 经过 systemd 处理后,包含单 % 的标准 URL 编码字符串(如 %5C, %23)被作为环境变量传递给 Docker 守护进程。
  3. Docker 守护进程按照标准,正确解析这些包含单个 % 的 URL 编码。

下次再配置需要转义的 Docker 代理时,记住这个 systemd 的“中间商”角色,就不会再对 %% 感到困惑了。