前言

我经常在内网部署一些服务,通过 Traefik 反向代理,并且使用自签名的 TLS 证书,如果只是本机浏览器访问是没有任何问题的,一切都很丝滑。但是如果部署在容器内的服务之间相互访问,则蛮烦的多,接下来介绍这其中可能遇到的坑!

域名解析

我本地部署的服务主要是我本机访问,很少涉及内网访问,而我使用 Dnsmasq 作为 DNS 服务,将 .test 的 TLD 都解析为 127.0.0.1。

这在本机浏览器上访问时是没有什么问题的,但是如果容期间访问就会存在问题,例如 Outline 要访问 MinIO 服务,而 MinIO 是通过 Traefik 反向代理,域名是 minio.test,其流量如下所示:

`Outline` ---> `Traefik` ---> `MinIO`

这就导致一个问题 Outline 在请求 minio.test 这个域名是,从 Dnsmasq 哪里获取到的解析结果是 127.0.0.1,在容器内就等于是请求自己,无法将流量发送到 Traefik 容器。

为了解决这个问题可行的方案有两种:

  • 将 Dnasmasq 的解析记录设置为 Traefik 的 IP;
  • 在 Docker Compose 的配置文件中设置 extra_hosts,等同于硬编码到容器的 /etc/hosts 文件中。

无论那种方式都需要为 Traefik 的容器设置一个固定 IP!

例如我本地使用 Valet 作为 PHP 的开发环境,在 ~/.config/valet/dnsmasq.d 目录中的配置如下:

tree
.
├── tld-infra.conf # 基础设施服务解析
├── tld-svc.dev.conf # Valet 相关项目解析
└── tld-test.conf # 其他测试服务解析

1 directory, 3 files
docker network inspect traefik
[
    {
        "Name": "traefik",
        "Id": "1bab78892e73451c7ed7f73a9ac4415da15d690885d293edb6aeb287e35e156b",
        "Created": "2023-10-22T18:07:03.173509102+08:00",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": {},
            "Config": [
                {
                    "Subnet": "10.8.10.0/24",
                    "Gateway": "10.8.10.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "b34f15b5a78146fbe5265e4653e843cb7201b8642747aa03dd4a11c9fc8122be": {
                "Name": "traefik",
                "EndpointID": "466ea1148e4ad1fe84757ee3c489e44b5c75b037d5988f43daaeb80b99f82012",
                "MacAddress": "02:42:0a:08:0a:fe",
                "IPv4Address": "10.8.10.254/24",
                "IPv6Address": ""
            }
        },
        "Options": {},
        "Labels": {}
    }
]

例如我这里 *.test 的所有流量都走 traefik,Dnsmasq 的配置如下:

address=/.test/10.8.10.254
listen-address=127.0.0.1

这样无论是容器内还是宿主机都是直接请求的 Traefik 这个容器,就不存在容器内和宿主机因解析地址是 loopback 而产生未知问题了!

证书

当 Outline 容器要请求 MinIO 的时候,走的是 HTTPS,因为使用了自签名证书,所以会导致建立 TLS 握手的时候失败,就是 X.509 证书不被信任!解决的方法也很简单,但如果基于不同的镜像可能存在不同的差异!

例如 Linux 系的都是将 CA 证书放到容器内的 /usr/local/share/ca-certificate 目录中,然后进入容器执行如下命令:

update-ca-certificates

如果容器内没有安装 update-ca-certificates 命令的话,那么可以直接将 CA 证书的内容追加到 /etc/ssl/certs/ca-certificates.crt 文件中:

cat /usr/local/share/ca-certificates/ca.crt >> /etc/ssl/certs/ca-certificates.crt

这种方法比较普适,但是存在的问题也很明显,你要么在创建容器收手动执行,要么重写 Dockerfile,将上述步骤在构建阶段就执行。这样不用每次 Recreate Container 的时候手动去执行上面的操作了!

切记更新完证书以后,需要重启容器(不是 Recreate 哦),否则不生效!

除了上述的方案以为,某些特定的服务也支持通过 ENV 来设置:

  • Node.JS 的镜像,可以使用 NODE_EXTRA_CA_CERTS 环境变量来指定 CA 所在位置,这样容器内的 Node.JS 进程发起的 Request 就可以自动加载 CA;
  • Gitlab Runner 可以通过 CA_CERTIFICATES_PATHCI_SERVER_TLS_CA_FILE 环境变量来信任 CA 证书。
  • Python 的镜像,可以使用 REQUESTS_CA_BUNDLE 环境变量来指定 CA 所在位置,这样容器内的 Python 进程发起的 Request 就可以自动加载 CA;

总结

在本地开发,要模拟和生产一致的体验,坑还是挺多的,需要你有一定的耐心和 Debug 能力。但是只要坚持下来,你发现没有一根头发是白掉的……

I hope this is helpful, Happy hacking…