Lan Tian @ Blog

x86 下制作 ARM Docker 镜像,Docker Hub、Travis 自动构建

一般情况下,Docker 的镜像都是在一个已有的镜像内,一步步运行给定的命令,从而生成一个新的镜像。这样的步骤在大多数人使用的 x86 架构计算机上都不是问题,由于架构互相兼容,一台计算机上生成的镜像往往可以被直接复制到其它计算机上运行,除非镜像中的程序使用了 AVX 等较新的指令集。

但是,还有一批基于 ARM 架构的主机也可以运行 Docker,并运行专门编译的 ARM 架构的镜像。这些主机包括树莓派系列,和其它类似树莓派的主机,例如 Cubieboard,Orange Pi,Asus Tinker Board 等等。另外,Scaleway 等主机商也提供基于 ARM 架构的独立服务器。

由于 ARM 架构的系统无法在 x86 架构计算机上运行,因此无法在 x86 计算机上直接通过 Dockerfile 生成 ARM 架构的镜像,一般采用的方法是直接找一台 ARM 主机来 docker build。

但是我在为我的树莓派制作 nginx 的 Docker 镜像时发现这并不是一个很好的方法。由于树莓派的内存只有 1GB,如果开启多线程编译(make -j4 或者 make -j2),内存会不足,gcc 会被杀掉;如果单线程编译(直接 make),编译时间又非常长(几个小时)。

经过查找,另有一种方案可以解决这个问题。这个方案是在 x86 架构计算机上模拟 ARM 环境,即“虚拟机”的方式来编译镜像。虽然 x86 模拟 ARM 没有硬件加速(VT-x,AMD-V 等)支持,效率极低,但是得益于 x86 CPU 的高性能,总体效率还是高于直接在树莓派上编译。

qemu-user-static

第一步是要模拟出一个 ARM 环境。当然,我们可以用 QEMU 直接开一个 ARM 架构的完整 Linux 虚拟机,然后在里面运行 Docker 构建镜像。但是这样做的问题是,需要额外管理一个 Docker,难以将系统资源在主系统和虚拟机之间灵活分配,并且难以使用脚本自动化,即难以整合到 CI、CD 中。

更好的方案是 qemu-user-static,是 QEMU 虚拟机的用户态实现。它可以直接在 amd64 系统上运行 ARM、MIPS 等架构的 Linux 程序,将指令动态翻译成 x86 指令。这样 ARM 系统环境中的进程与主系统的进程一一对应,资源分配灵活,并且易于脚本自动化。

但是还有一个问题:当 ARM 进程尝试运行其它进程时,qemu-user-static 并不会接管新建的进程。如果新的进程仍然是 ARM 架构,那么 Linux 内核就无法运行它。因此,需要开启 Linux 内核的 binfmt 功能,该功能可以让 Linux 内核在检测到 ARM、MIPS 等架构的程序时,自动调用 qemu-user-static。开启该功能,并且注册 qemu-user-static 虚拟机后,运行 ARM 程序就和运行 x86 程序一样,对用户来说毫无差别。

在 x86 Docker 中运行 ARM 镜像

要在 Docker 中运行 ARM 镜像,我们要先在计算机上注册 qemu-user-static 虚拟机:

docker run --rm --privileged multiarch/qemu-user-static:register --reset

另外,Docker 镜像内必须也含有对应的 qemu-user-static 虚拟机。不过,Docker Hub 上已经有了添加 qemu-user-static 的系统镜像,可以在 https://hub.docker.com/u/multiarch/ 获取:

插图

例如,multiarch/alpine 镜像就在不同 tag 下提供了 aarch64(armv8)、armhf、amd64、i386 的镜像:

插图

如果你之前已经注册了虚拟机,那么就可以直接运行了:

docker run -it --rm multiarch/alpine:armhf-edge /bin/sh
docker run -it --rm multiarch/alpine:aarch64-edge /bin/sh

插图

修改 Dockerfile

接下来我们要在 Dockerfile 中调用 ARM 架构的镜像。如果你的 ARM 主机是 armv7l(armhf)架构(树莓派(默认),Tinker Board 等),那么把 Dockerfile 中的第一行修改成 https://hub.docker.com/u/multiarch/ 下对应的 armhf 架构镜像即可。对应关系如下:

  • alpine -> multiarch/alpine:armhf-edge
  • ubuntu:bionic -> multiarch/ubuntu-debootstrap:armhf-bionic
  • debian:stretch -> multiarch/debian-debootstrap:armhf-stretch

如果你的 ARM 主机是 aarch64(armv8)架构(树莓派 3 开始支持,但是需要特殊系统才是这个架构),那么对应关系如下:

  • alpine -> multiarch/alpine:aarch64-edge
  • ubuntu:bionic -> multiarch/ubuntu-debootstrap:arm64-bionic
  • debian:stretch -> multiarch/debian-debootstrap:arm64-stretch

改完后直接重新构建镜像,你就可以在本地生成 ARM 架构的镜像了。

Docker Hub 自动构建

Docker Hub 不仅提供镜像的存储共享服务,也提供简单的镜像自动构建服务。自动构建服务给每个用户分配了一台 2GB 内存、1 核心 CPU、30GB 硬盘的完整虚拟机运行 2 小时(来自 Docker 官方论坛),并且用户具有 root 权限。

默认的自动构建相当于是我们构建镜像时运行的 docker build 那一步,但是我们需要在这之前注册 qemu-user-static 虚拟机。我们可以用 Docker 官方提供的 hook 在构建开始前运行自定义的命令(来自 Docker Cloud 文档)。因为我们分配到的是完整的虚拟机,有 root 权限,所以我们也可以在 hook 中注册虚拟机。

如何创建这样一个 hook?在 Dockerfile 的文件夹下创建 hooks 文件夹,再在 hooks 文件夹下创建 pre_build 文件,内容如下:

#!/bin/sh
docker run --rm --privileged multiarch/qemu-user-static:register --reset

可以在我的这个 commit 中看到 hook 的示例。

Docker Hub 的自动构建服务会先运行这个脚本注册 qemu-user-static,然后再开始构建。构建完成时 push 上来的就是 ARM 架构

如果你的镜像构建时没有编译操作,构建速度应该相当快,不会比 x86 的镜像慢多少;但是如果有大量的编译操作,例如我的 nginx 镜像,很有可能就超出了 2 小时的时间限制而构建失败。在这种情况下,我们就要换其它不限制时间的自动构建服务,例如 Travis CI。

Travis CI 自动构建

Travis CI 是对开源社区免费的一款自动构建工具。只要你的 Dockerfile 传到了 GitHub 上的 Public Repository(公开代码仓库)里,就可以直接使用它。

对于构建 Docker 镜像来说,Travis 提供的配置是 7.5GB 内存、2 核心 CPU、18GB 硬盘,不限制运行时间。因此编译时可以开启 make -j4 四线程编译来提高速度。

首先到 https://travis-ci.org/ 用 GitHub 账号登录,然后开启你放 Dockerfile 仓库的自动构建功能。

插图

然后在 Settings 页面添加你的 Docker Hub 账户的用户名密码到环境变量,这样后续你就不用在自动构建配置中明文保存密码了。

插图

然后创建一个名为 .travis.yml 的文件到 git 仓库的根目录,这就是 Travis 的 “Dockerfile”,保存你的自动构建指令。

.travis.yml 的语法较复杂,你可以在我的 .travis.yml 的基础上作修改。我的 .travis.yml 可以在 https://github.com/xddxdd/dockerfiles/blob/master/.travis.yml 看到。

如果需要更复杂的修改,可以阅读 Travis 的官方文档自行学习。

编辑 .travis.yml 完成后,把它提交到 GitHub 上,Travis 就会自动开始构建你的镜像,把它们 push 到 Docker Hub 上,并且发邮件告诉你自动构建的情况。

Docker 镜像的精简

自从放弃 OpenVZ 架构的 VPS 并购买 KVM 架构的 VPS 以来,我一直使用 Docker 部署 nginx、MariaDB、PHP 等网站需要的程序,不仅方便平时单个服务的重启和配置管理(把配置目录全部用 volume 映射到一起管理),而且方便了服务的升级。

例如,我现在博客所在的 VPS 因为配置不高,内存占用最近一直在 80% 左右。我要更新 nginx 或者向 nginx 里加模块时,如果直接在这台 VPS 上编译 nginx,不仅速度慢,而且有可能因为内存不足而把网站也崩掉。使用 Docker 后,我就可以在其它的空闲资源较多的 VPS 或者在我自己的电脑上构建镜像,push 到 Docker Hub,再在 VPS 上 pull 下来运行。

不过,一直以来,我的 nginx 镜像大小都在 200 MB 左右(Docker Hub 显示大小,不包含基础镜像大小,比一般 docker image 显示的要小),明显大于它应该有的容量,不过因为 VPS 的硬盘暂时足够,而且前段时间我也没什么时间,我就没有管这个问题。现在有了空,我就仔细研究并解决了这个问题,修改了 Dockerfile,把镜像大小降到了 17 MB。

合并 RUN 命令

刚打开 Dockerfile,我就发现了一个低级错误:

FROM debian:stretch
MAINTAINER Lan Tian "lantian@lantian.pub"
ENV NGINX_VERSION=1.15.1 OPENSSL_VERSION=OpenSSL_1_1_1-pre8
RUN apt-get update -q \
    && apt-get -y upgrade \
    && apt-get -y install build-essential git autoconf automake libtool wget tar zlib1g-dev libpcre3 libpcre3-dev unzip libjemalloc-dev patch
RUN cd /tmp \
    && wget -q http://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz \
    && tar xf nginx-${NGINX_VERSION}.tar.gz \
    && # 接下来是编译过程

Docker 在构建镜像时,会为 Dockerfile 中的每一条指令生成一“层”镜像,将一层层镜像叠加起来成为一“个”镜像。

在上述第一条 RUN 指令中,Docker 生成了一“层”包含了 nginx 所需的依赖包,以及 apt-get 下载下来的临时文件的镜像。后续的编译、安装过程在第二条 RUN 指令中,是在含依赖包那一层之上的一“层”操作的,完全不能影响到依赖包那一层。虽然我后续执行了 apt-get clean,但是这只是在最顶层中标记了删除这些文件,这些文件仍然在第二层含依赖包的一层留存着。

修改方法就是把两个 RUN 命令通过 && 和反斜杠连接起来,共同构成一层就可以了。

当时我这样写 Dockerfile,是因为后续的 nginx 编译过程反复出现问题,我在测试时为了防止反复下载依赖包就分离了这些指令,来重复利用下载好的依赖包。但是在测试完成后,我忘了把这里改回去!

这里修改后,Docker 镜像的大小直接小了 25 MB。虽然 175 MB 的镜像比之前已经小了一些,但是还是很大,还有精简的空间

使用 Alpine 作为基础镜像

我之前一直使用我比较熟悉的 Debian 作为基础镜像。Debian 的好处是可以使用常用的 apt-get 等工具,但是它基础镜像的体积大概在 125 MB 左右,很大。而 Alpine Linux 是一个体积只有 5 MB 左右,被广泛运用在 Docker 镜像中的 Linux 发行版。为了精简体积,我准备使用它。

首先是把 Dockerfile 开头的 FROM debian:stretch 换成 FROM alpine。我直接用了 Alpine 的最新版本,如果软件对系统版本有要求记得指定。

然后,把 apt-get 安装的依赖包在 Alpine 的包管理器 apk 中找到对应的包,并安装上去。我在 Debian 中安装依赖的命令如下:

apt-get update -q
apt-get -y install build-essential git autoconf automake libtool wget tar zlib1g-dev libpcre3 libpcre3-dev unzip libjemalloc-dev patch

把这些包一一在 apk 中找到对应的包:

apk --no-cache add build-base git autoconf automake libtool wget tar zlib-dev pcre-dev unzip jemalloc-dev patch linux-headers

其中 --no-cache 参数代表 apk 不使用本地缓存,直接去镜像源拉取软件包列表和下载,并且会自动删除下载缓存,相当于自带了 apt-get update 和 apt-get clean 两行。

同理,把最后删除编译依赖包的部分也改掉。原先在 Debian 下我是这样删的:

apt-get purge -y unzip -q git autoconf libtool automake build-essential
apt-get autoremove -y --purge

在 Alpine 下我这样删:

apk del build-base git autoconf automake wget tar unzip patch linux-headers

这样操作后,镜像的体积被直接减到了 17 MB!在 docker images 中包含 Alpine 本身显示为 39.6 MB。相比原先已经非常小了。这里应该不只是 Alpine 本身的差距,还可能和 Alpine 自带的轻量级 musl C 库文件有关。

同样操作精简其它镜像

nginx 的镜像精简完了,用类似的操作也可以精简其它的镜像。首先拿 PHP-FPM 开刀,这里因为我需要一大堆 PHP 模块所以不能直接用官方的镜像。

原先的安装命令是这样,我的 PHP-FPM 不是编译的,是直接从软件源安装的:

apt-get install -y php7.2-fpm php7.2-bz2 php7.2-curl php7.2-gd php7.2-json php7.2-mbstring php7.2-memcached php7.2-mysql php7.2-redis php7.2-sqlite3 php7.2-xml php7.2-xmlrpc php7.2-zip php7.2-intl

Alpine 的软件源也有 PHP-FPM,可以直接安装:

apk --no-cache add php7-fpm php7-bz2 php7-curl php7-gd php7-json php7-mbstring php7-memcached php7-mysqli php7-pdo_mysql php7-redis php7-sqlite3 php7-pdo_sqlite php7-xml php7-xmlrpc php7-zip php7-intl php7-ctype php7-tokenizer

其中注意 Alpine 软件源的 PHP 有点迷,默认不包含一些在其它发行版上有的包,例如 php7-ctype、php7-tokenizer,如果不装,Typecho 等程序就会报错。

换成 Alpine 后 Docker Hub 上显示的大小从 90 MB 降到了 25 MB,docker image 中的大小降到了原先的 1/4。

MariaDB 我也换成了基于 Alpine 的,不过这个镜像因为有现成的,我就没自己做,直接用了 bianjp/mariadb-alpine。它和官方基于 Debian 的镜像几乎完全兼容,我在 docker-compose.yml 里只改了 image 名就成功了,什么设置都不用动。

总结

这样操作后我的 VPS 里省了大概 1 GB 的空间,对于我的小硬盘 VPS 来说已经省出了很多的空间了。

而且以后更新程序时 push、pull 镜像的速度也大大加快了,尤其是香港 VPS 从 Docker Hub pull 镜像的速度一直不怎么样,更小的镜像意味着在更短的时间内可以 pull 下更多个版本进行测试,出现问题 pull 原来版本镜像恢复运行的速度也会更快。

nginx:TLS 1.3 多版本草案和 HPACK

距离我之前给 nginx 启用 TLS 1.3 已经过了 11 个月了。快一年过后,许多与 nginx 相关的程序、补丁都有了很大的变化:

  1. OpenSSL 已经在发布 1.1.1 的测试版,写本文时最新版本是 1.1.1-pre8(也就是 Beta 6)。
  2. nginx 已经更新到 1.15.1。
  3. nginx 的 HPACK 补丁(HTTP 头压缩补丁)的 bug 已经有另外的补丁的补丁修复,使用原先的 HPACK 补丁会导致网站访问不正常,体现为每个网站只能打开一个页面,第二个页面开始就出现协议错误。
  4. 有大佬发布了 OpenSSL 的补丁,可以让最新版 OpenSSL 同时支持 TLS 1.3 的 draft 23,26,28 三个版本。
  5. Lets Encrypt 证书已经自带 Certificate Transparency 信息了,不需要 nginx-ct 了。
  6. 2018 年 7 月 1 日起,TLS 1.0 不再被建议使用。

因此我重新调整了 nginx 的编译和运行配置,以适应 8102 年的需要。

Dockerfile

我依然使用 Docker 部署 nginx。与之前的 Dockerfile 相比,新的 Dockerfile 只是改了下版本号,添加了几个补丁,整体并没有大的变化。

为了节省篇幅,我将 Dockerfile 上传到了 https://github.com/xddxdd/dockerfiles/blob/master/nginx/Dockerfile.amd64。你也可以直接 docker pull xddxdd/nginx 来使用。

这个 Dockerfile 包含了如下内容:

  • nginx 1.15.1
  • OpenSSL 1.1.1-pre8
  • kn007 大佬的 SPDY、HPACK、Dynamic TLS Record 三合一补丁,他的项目地址在此访问
  • kn007 大佬的 HPACK 补丁的修复补丁
  • Brotli 压缩算法
  • hakasenyang 大佬的 TLS 1.3 三版本草案补丁,他的项目地址在此访问
  • nginx headers-more 模块

配置改变

首先禁用 TLS 1.0。

#ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;

然后,因为 OpenSSL 修改了一大堆加密算法的名称,因此如果直接沿用之前的 ssl_ciphers 会出现 ERR_SSL_VERSION_OR_CIPHER_MISMATCH 错误,意思是没有服务器和客户端同时支持的加密算法。因此修改 ssl_ciphers:

#ssl_ciphers 'TLS13-AES-256-GCM-SHA384:TLS13-CHACHA20-POLY1305-SHA256:TLS13-AES-128-GCM-SHA256:TLS13-AES-128-CCM-8-SHA256:TLS13-AES-128-CCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:!DSS';
ssl_ciphers '[TLS13+AESGCM+AES128|TLS13+CHACHA20]:TLS13+AESGCM+AES256:[EECDH+ECDSA+AESGCM+AES128|EECDH+ECDSA+CHACHA20]:EECDH+ECDSA+AESGCM+AES256:EECDH+ECDSA+AES128+SHA:EECDH+ECDSA+AES256+SHA:[EECDH+aRSA+AESGCM+AES128|EECDH+aRSA+CHACHA20]:EECDH+aRSA+AESGCM+AES256:EECDH+aRSA+AES128+SHA:EECDH+aRSA+AES256+SHA:RSA+AES128+SHA:RSA+AES256+SHA';

然后?

重新启动 nginx,用最新稳定版的 Chrome 和 Firefox 就可以以 TLS 1.3 draft 23 访问了,用最新开发版的 Chrome 和 Firefox 就可以以 TLS 1.3 draft 28 访问了。

使用 ZeroTier One 在多台 Docker 服务器间建立双栈互通网络

前言

多台 Docker 服务器上的容器互通是一个不好解决的问题。如果自建一个 Overlay 网络,就需要在一台服务器上建立 etcd 之类的服务。但如果 etcd 所在的服务器挂了,整个网络就 GG 了。我用的便宜 VPS 有偶尔网络中断的情况,我自己搞崩也服务器是常有的事,所以我不能采取这种方式。

Docker 也有其它的基于 Overlay 的商业化组网方案,例如 Weave,但是对于个人用户来说这些方案的价格太高了(我只是搞来玩玩),所以也不考虑。

在这些网络结构上,etcd 或者 Weave 之类的中心服务器记录了每个容器所在的服务器和内部 IP,所以在任何容器上都可以直接 DNS 解析到其它容器。也就是说,假如我设置了 lantian-nginx 和 lantian-phpfpm 两个容器,在 nginx 的配置文件里我可以直接把 php-fpm 的地址填成 lantian-phpfpm:[端口号],方便配置。

但我好像可以放弃这个功能啊?我的容器数量并不多,而且只有几个 MariaDB 需要跨服务器连接,做数据库主从备份,手动指定一下 IP 并不麻烦。

那么我就可以直接使用传统的 VPN 方案来做互通了。

问题又来了:由于自己的服务器不稳定,我不希望某台服务器挂掉导致网络互通挂掉,所以 Open^_^VPN 之类需要架设中心 VPN 服务器的也算了。Tinc 这类 P2P VPN 符合我的要求,但是我的服务器常有增减,难道我每次都要一台台上去改配置吗?

有中央管理面板的 P2P VPN,我或许可以用 LogMeIn Hamachi。这款免费软件通过和 LogMeIn 公司的中心服务器连接,获取网络内其它计算机的实际 IP,并分别建立 P2P 连接。我的服务器上倒是有 Hamachi,但是它只给每台计算机一个 IPv4 和一个 IPv6,对于 Docker 组网来说不够用啊。而且它每个网络只能让 5 台计算机互联,否则就要加钱。

到现在为止,我的需求如下:

  1. 任何一台服务器 GG 不能影响其它服务器
  2. 需要一个统一的管理面板可以快速增减服务器
  3. 不需要 Docker 的 DNS 解析之类功能
  4. 每台服务器可以获得多于一个的 IPv4 和 IPv6(可以是内网 ULA 网段),最好有一个内网网段

经过一番搜索,我选定了 ZeroTier One 这款软件。它类似于 Hamachi,但是相比之下它有如下优点:

  1. 我可以指定哪些 IPv4 网段被路由到 ZeroTier One,并且可以任意设置分配地址池。
  2. 我可以让每台计算机分配到一个 IPv6 ULA 下的 /80 网段,足够 Docker 使用。
  3. 每个用户可以免费添加 100 台计算机。

第一点尤其重要,例如,我可以设置给每台计算机在 172.27.0.0/16 下自动分配 IP,但是我又可以统一指定把 172.28.0.0/24 路由到某台 Docker 服务器,172.28.1.0/24 路由到另一台,以此类推。

安装与配置

ZeroTier One 官方提供一键安装脚本。在你的服务器上运行如下命令即可。

curl -s 'https://pgp.mit.edu/pks/lookup?op=get&search=0x1657198823E52A61' | gpg --import && \ if z=$(curl -s 'https://install.zerotier.com/' | gpg); then echo "$z" | sudo bash; fi

然后在 ZeroTier One 官方注册一个账号。这里需要你的手机号。我测试中国电信手机号可以收到验证短信,但是隔了 7 个小时……最后我用的是 Google Voice 的号码。

注册登录后你就能看到如下界面:

插图

点击上方的 Network(网络),再点击 Create New Network(创建新网络),你会看到列表上多了一个网络。

插图

如图,第二个是我已有的网络,第一个是新创建的。点击进去。

插图

左上角是 Network ID,稍后你要输入到你的 ZeroTier 客户端去。

Short Name 是网络名称,你可以自定义以使得它更好辨别。

右上角 Managed Routes 是路由表设置,稍后再改。

IPv4 Auto-Assign 是 IPv4 地址的自动分配,我们要把它打开。

插图

打开后,在列出的网段中选一个看得顺眼的。这个网段不能与你服务器实际在用的网段有重合。例如,如果有一台 NAT VPS,其 IPv4 所在网段是 172.17.0.0/16,那么你就不能选它,否则它有可能会断网。选择后,会自动给你添加好路由表设置,如图:

插图

IPv6 Auto-Assign 是 IPv6 的自动分配,下有两个选项。

插图

RFC4193 选项会为你随机生成一个 ULA 网段(在 fd00::/8 下),每台服务器获得一个 IPv6(/128);6PLANE 在另一个 ULA 网段(fc00::/8 下),每台服务器获得一个 /80 段。为了 Docker 组网,我们需要的就是 6PLANE 网段。

接下来,我们把服务器加进这个网络。在服务器上输入命令:

zerotier-cli join [网络ID]

然后在设置页面的下方,勾上对应服务器的 Auth 选项:

插图

页面上会显示服务器对应的 IPv4 和 IPv6,各台服务器也能互相 ping 通了。

配置 Docker - 原生 Docker

在这个例子中,假定这台 Docker 服务器分配到了 10.147.17.233 和 fc23:3333:3333:3333:3333::1/80,我们希望添加一个 172.28.233.x/24 供它使用,系统环境是 Debian 8。

我们需要指定 Docker 使用一些启动命令参数。在 Debian 8 这类 systemd 发行版上,我们需要修改一下 systemd 的配置。输入以下命令:

cd /etc/systemd/system
mkdir docker.service.d
cd docker.service.d
nano docker.conf

向 docker.conf 输入如下内容:

[Service]
EnvironmentFile=-/etc/default/docker
ExecStart=
ExecStart=/usr/bin/dockerd -H fd:// $DOCKER_OPTS

Ctrl+X,Y,回车保存退出,再运行:

systemctl daemon-reload
cd /etc/default
nano docker

经过如上修改,此处显示的 DOCKER_OPTS 环境变量就能生效了。将对应的这一行修改成:

DOCKER_OPTS="--fixed-cidr=172.28.233.0/24 --ipv6 --fixed-cidr-v6=fc23:3333:3333:3333:3333::/80"

重启 Docker:

service docker restart

然后你的 Docker 容器就有了在这两个网段上的 IP。

问题又来了,由于容器获得的是 ULA 网段,无法访问 IPv6 公网,怎么办?我们需要借助 docker-ipv6nat。这个软件(容器)可以根据你的容器设置自动配置 IPv6 NAT,就像 Docker 在 IPv4 上做的一样。

等等,为什么要用 IPv6 NAT?我认为它的优点如下:

  1. 某些主机商只提供一个 IPv6 地址
  2. 更方便的防火墙配置(外网无法直接访问到容器)
  3. 它在 ULA 网段上运行,刚好符合这次的需求

启动这个容器:

docker run -d --restart=always -v /var/run/docker.sock:/var/run/docker.sock:ro --privileged --net=host robbertkl/ipv6nat

你可能需要重新启动一下其它容器以使配置生效。这样你的容器就能访问 IPv6 公网了。

如果你愿意,可以在 DOCKER_OPTS 里加一句 --userland-proxy=false,禁用 Docker 的应用层代理,可以节省内存。

配置 Docker - Compose

如果你用 Docker-Compose,那么事情就方便了许多,不需要修改 systemd 配置了。将你的 docker-compose.yml 里修改成如下内容:

version: "2.1"
services:
  docker-ipv6nat:
    image: robbertkl/ipv6nat
    container_name: docker-ipv6nat
    restart: always
    privileged: true
    network_mode: host
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock:ro"

  你原有的容器:
    [...]
    depends_on:
      - docker-ipv6nat

  [你原有的容器们...]

networks:
  default:
    driver: bridge
    enable_ipv6: true
    ipam:
      driver: default
      config:
      - subnet: 172.28.233.0/24
        gateway: 172.28.233.1
      - subnet: fc23:3333:3333:3333:3333::/80
        gateway: fc23:3333:3333:3333:3333::1

注意要给原有的每个容器添加依赖 docker-ipv6nat。然后重新 docker-compose up -d 即可。

配置系统转发

在 /etc/sysctl.conf 里添加以下内容:

net.ipv4.ip_forward = 1
net.ipv6.conf.default.forwarding=1
net.ipv6.conf.all.forwarding=1
net.ipv6.conf.all.proxy_ndp=1

执行 sysctl -p 开启转发。如果你有防火墙,记得给相应的网段放行。

配置 ZeroTier 路由表

在右上角 Managed Routes 里添加“172.28.233.1/24,10.147.17.233”,代表把 172.28.233.1/24 网段的请求全部交由 10.147.17.233 处理。

插图

然后,你在其它服务器就能 ping 通 10.147.17.233 上的 Docker 容器了。在其它服务器上如法炮制(记得 IP 段不能重合),就能在所有 Docker 服务器和容器之间建成一张双栈局域网了。