自动代理软件仓库镜像源,容器免替换镜像源

代理镜像

我一直有个痛点,在中国大陆环境启动 debian / ubuntu / alpine 容器拉取 apt / apk 包信息的时候,总是因为网络缓慢要等好久。

正常就会去 Ubuntu 软件仓库镜像使用帮助 - MirrorZ Help 改包管理器的镜像源,之前也调研过方便的手段 使用 mirror 协议为 Debian 选择 APT 镜像站,也有 RubyMetric/chsrc: chsrc 全平台通用换源工具与框架. Change Source everywhere for every software 这种工具可以一键换源。

能不能自动地拦截发往包管理平台的流量,跳转到镜像站呢。

今日调研结论是,可以,但只能 HTTP。

先在路由器的 DNS 把流量劫持到本地 Nginx 服务器,对 HTTP 流量 return 302,对 HTTPS 流量就只能原样转发到上游了。
实现也不难,主要是要避免 DNS 死循环。

我最终选择在主路由的 [MerlinClash](简介 | 魔法师云撸猫) 上完成这一套,毕竟大部分流量都经过它,而且主 DNS 也是它。

用 Docker 部署主要的服务

这次还学到 可以用模板文件来让 nginx 容器支持 stream block,只要 templates 目录有个带 stream {} 的块

services:
  alpine-redirect:
    image: nginx:alpine
    container_name: alpine-redirect
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./templates:/etc/nginx/templates

一个是 80 端口的重定向

/etc/nginx/templates/alpine.conf.template
server {
    listen 80;
    server_name dl-cdn.alpinelinux.org;
 
    location / {
        return 302 http://mirrors.cernet.edu.cn$request_uri;
    }
}

另一个是 443 端口的流式转发。
这里给 proxy_pass 设置变量的特殊技巧,用来避免 nginx 启动时就记住了错误的域名解析结果,强制每次解析。

/etc/nginx/templates/default.conf.stream-template
server {
    listen 443;
    resolver 1.1.1.1 ipv6=off;
    # Using a variable in proxy_pass forces re-resolution of the DNS names
    # https://serverfault.com/a/593003/48752
    set $backend dl-cdn.alpinelinux.org:443;
    proxy_pass $backend;
}

还没完,在 MerlinClash 上还有设置。重点是两处

  1. hosts 用来把域名解析指向本地服务器,也可以用 /etc/hosts 来实现。但在路由器上要持久化 /etc/hosts 的变更得写脚本,干脆让 clash DNS 来解析
  2. 如果启用了域名嗅探 sniffer 功能,一定要把相关域名加到忽略列表中,否则解析会死循环
hosts:
  dl-cdn.alpinelinux.org: 192.168.2.62
sniffer:
...
  skip-domain:
    - 'dl-cdn.alpinelinux.org'

然后就能验证效果了。容器如果默认就是 HTTP 源就不再需要修改软件仓库源了

$ dig dl-cdn.alpinelinux.org
NAME                     TYPE  CLASS  TTL  ADDRESS              NAMESERVER      TIME TAKEN
dl-cdn.alpinelinux.org.  A     IN     9s   192.168.2.99         192.168.9.1:53  2ms
dl-cdn.alpinelinux.org.  AAAA  IN     9s   ::ffff:192.168.2.99  192.168.9.1:53  2ms
 
$ curl -I http://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/APKINDEX.tar.gz
HTTP/1.1 302 Moved Temporarily
Server: nginx/1.29.4
Date: Wed, 10 Dec 2025 14:56:12 GMT
Content-Type: text/html
Content-Length: 145
Connection: keep-alive
Location: http://mirrors.cernet.edu.cn/alpine/v3.22/main/x86_64/APKINDEX.tar.gz
 
$ curl -I https://dl-cdn.alpinelinux.org/alpine/v3.22/main/x86_64/APKINDEX.tar.gz
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 500103
Content-Security-Policy: script-src 'self'
Content-Type: application/octet-stream
Etag: "69391eaf-7a187"
Last-Modified: Wed, 10 Dec 2025 07:18:07 GMT
Referrer-Policy: origin-when-cross-origin
Server: nginx/1.27.5
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Via: 1.1 varnish, 1.1 varnish
Accept-Ranges: bytes
Date: Wed, 10 Dec 2025 14:56:07 GMT
Age: 12
X-Served-By: cache-ams21061-AMS, cache-sin-wsss1830023-SIN
X-Cache: HIT, HIT
X-Cache-Hits: 117, 2
X-Timer: S1765378567.125863,VS0,VE0
Vary: Origin

Nginx Docker 容器日志输出到 stdout

我说怎么 nginx.conf 配置文件里明明是 access_log /var/log/nginx/access.log,可日志内容还是输出到容器终端上了。原来是 /var/log/nginx/access.log -> /dev/stdout 文件有链接。

# forward request and error logs to docker log collector
ln -sf /dev/stdout /var/log/nginx/access.log
ln -sf /dev/stderr /var/log/nginx/error.log