解决 WSL2 环境 Docker Pull 超时:基于 systemd 的代理动态注入方案

每次重启 Windows,旧版 WSL2(NAT 模式)的宿主机网关 IP 都会重新分配。这就导致了一个典型的进程隔离问题:终端里的 curl 还能靠 .profile 动态拿到新 IP 正常请求,但后台 systemd 托管的 dockerd 根本吃不到这个环境变量,一执行 docker pull 就直接超时。
分享一套我目前在用的自动化注入方案,核心就是利用 systemd 依赖,让 Docker 守护进程每次启动前动态感知 Windows 新 IP,免去手动改配置的麻烦。
前置环境确认
在动手前,确认下你的环境是否匹配这套方案:
- 未使用有些臃肿的 Docker Desktop,而是在 WSL2 内部署了原生的 Docker Engine。
- WSL 已启用 systemd(执行
ps -p 1 -o comm=确认输出为systemd)。 - 宿主机的代理软件已开启“允许局域网连接”(监听地址包含
0.0.0.0)。
核心步骤 1:抓取宿主机动态 IP
首当其冲的是,我们需要计算 Windows 当前的网关 IP。
新建 /etc/profile.d/wsl-proxy.sh(假设你的代理端口是 7897,按需修改),这类似于为整个 WSL 系统提供一个基准配置:
sudo tee /etc/profile.d/wsl-proxy.sh >/dev/null <<'EOF'
# Configure WSL2 NAT to use the Windows-hosted proxy.
if [ -n "${WSL_INTEROP:-}" ] || grep -qi microsoft /proc/version 2>/dev/null; then
_wsl_host_ip="$(ip -4 route show default 2>/dev/null | awk '{print $3; exit}')"
if [ -n "$_wsl_host_ip" ]; then
export http_proxy="http://${_wsl_host_ip}:7897"
export https_proxy="$http_proxy"
export all_proxy="$http_proxy"
export HTTP_PROXY="$http_proxy"
export HTTPS_PROXY="$http_proxy"
export ALL_PROXY="$http_proxy"
export no_proxy="localhost,127.0.0.1,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16"
export NO_PROXY="$no_proxy"
fi
unset _wsl_host_ip
fi
EOF
核心步骤 2:Systemd 自动化桥接机制
WSL 刚启动时,网卡和路由表初始化存在毫秒级延迟。在 Docker 里挂载代理,核心思路是写一个带重试机制的脚本,配合 systemd 在 Docker 服务拉起前强制拦截并注入环境变量。
新建 /usr/local/sbin/update-docker-proxy-env 并执行 sudo chmod 0755 /usr/local/sbin/update-docker-proxy-env 赋予执行权限:
sudo tee /usr/local/sbin/update-docker-proxy-env >/dev/null <<'EOF'
#!/bin/sh
set -eu
PROFILE=/etc/profile.d/wsl-proxy.sh
RUNTIME_DIR=/run/docker-proxy
ENV_FILE="$RUNTIME_DIR/environment"
if [ ! -r "$PROFILE" ]; then
echo "Cannot read $PROFILE" >&2
exit 0
fi
# 隔离执行环境
unset HTTP_PROXY HTTPS_PROXY NO_PROXY || true
# 轮询等待 IP 注入
i=0
while [ "$i" -lt 30 ]; do
. "$PROFILE"
if [ -n "${HTTP_PROXY:-}" ] && [ -n "${HTTPS_PROXY:-}" ]; then
break
fi
i=$((i + 1))
sleep 1
done
install -d -m 0755 "$RUNTIME_DIR"
tmp_file="$(mktemp "$RUNTIME_DIR/environment.XXXXXX")"
trap 'rm -f "$tmp_file"' EXIT HUP INT TERM
if [ -n "${HTTP_PROXY:-}" ]; then
{
printf 'HTTP_PROXY=%s\n' "$HTTP_PROXY"
printf 'HTTPS_PROXY=%s\n' "$HTTPS_PROXY"
printf 'NO_PROXY=%s\n' "${NO_PROXY:-}"
printf 'http_proxy=%s\n' "$HTTP_PROXY"
printf 'https_proxy=%s\n' "$HTTPS_PROXY"
printf 'no_proxy=%s\n' "${NO_PROXY:-}"
} > "$tmp_file"
printf 'Docker proxy environment generated: %s\n' "$HTTP_PROXY"
else
# 生成空文件,避免无代理时破坏 Docker 启动
printf '' > "$tmp_file"
echo "Warning: No proxy was generated by $PROFILE. Docker will start without proxy." >&2
fi
chmod 0600 "$tmp_file"
mv -f "$tmp_file" "$ENV_FILE"
trap - EXIT HUP INT TERM
EOF
接着,用 systemd 建立启动顺序,新建 /etc/systemd/system/docker-proxy-env.service:
sudo tee /etc/systemd/system/docker-proxy-env.service >/dev/null <<'EOF'
[Unit]
Description=Generate Docker proxy environment from WSL proxy profile
Wants=network-online.target
After=network-online.target
Before=docker.service
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/update-docker-proxy-env
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
EOF
最后,通过 drop-in 文件挂载代理环境。
注意 EnvironmentFile=-/... 里的减号绝对不能漏,它的作用是即使环境配置文件缺失,系统也不会报错中断启动,保障 Docker 基础服务能继续跑:
sudo mkdir -p /etc/systemd/system/docker.service.d
sudo tee /etc/systemd/system/docker.service.d/proxy.conf >/dev/null <<'EOF'
[Unit]
Wants=docker-proxy-env.service
After=docker-proxy-env.service
[Service]
EnvironmentFile=-/run/docker-proxy/environment
EOF
启动与避坑指北
全搞定后,重载 systemd 并启动服务:
sudo systemctl daemon-reload
sudo systemctl enable docker-proxy-env.service
sudo systemctl restart docker-proxy-env.service
sudo systemctl restart docker.service
执行 docker info | grep -i proxy。如果看到宿主机的新 IP 和你设置的端口号,直接 docker pull 即可。
顺带一提几个日常维护的踩坑点和操作细节:
- 排查 Connection refused:这通常是宿主机网络策略背锅。重点检查两处:Windows 代理客户端是否开启了“局域网共享”;Windows 防火墙是否拦截了 WSL vEthernet 虚拟网卡的入站请求(需在 PowerShell 补加放行规则)。
- 更换代理或端口:这套配置的好处是解耦了源头。后续如果换了代理软件,只需修改
/etc/profile.d/wsl-proxy.sh里的端口号,接着执行sudo systemctl restart docker-proxy-env docker即可热更新,不需要去动 Docker 的核心配置。 - 无损回退卸载:如果哪天想剥离这套方案,直接停用服务并删掉前文创建的 4 个配置文件及
/run/docker-proxy目录,最后执行sudo systemctl daemon-reload && sudo systemctl restart docker就能完全恢复原生状态。