我最初使用Docker的时候,每个人都在说它用起来有多简单方便,它内部的机制是有多么好,它为我们节省了多少时间。但是当我一使用它就发现,几乎所有镜像都是臃肿而且不安全的(没有使用包签名,盲目相信上游的镜像库以<code>curl | sh</code>的方式安装),而且也没有一个镜像能实现Docker的初衷:隔离,单进程,容易分发,简洁。
Docker镜像本来不是为了取代复杂的虚拟机而设计的,后者有完整的日志、监控、警报和资源管理模块。而Docker则倾向于利用内核的<code>cgroups</code>和<code>namespaces</code>特性进行封装组合,这就好像:
在物理机器环境下,一旦内核完成了初始化,<code>init</code>进程就起来了。
这也是为什么当你在Dockerfile的<code>CMD</code>指令启动的进程PID是1,这是与Unix中的进程机制类似的。
现在请查看一下你的进程列表,使用<code>top</code>或者<code>ps</code>,你会看到<code>init</code>进程占用的也是这个PID,这是每个类Unix系统的核心进程,所有进程的父进程,一旦你理解这个概念:在类Unix系统上每个进程都是init进程的子进程,你会理解Docker容器里不应该有无关的修饰文件,它应该是刚好满足进程运行需要。
现在的应用多数是大型复杂的系统,通常都需要很多依赖库,例如有调度,编译和很多其他相关工具类应用,它们的架构通常封装性良好,通过一层层的抽象和接口把底层细节隐藏了,从某种程度上说,这也算是一种容器,但是从系统架构视角看,我们需要一种比以往虚拟环境更简单的方案了。
以Java为例
从零开始,思考你要构建一个最通用的基础容器,想想你的应用本身,它运行需要什么?
可能性有很多,如果你要运行Java应用,它需要Java运行时;如果运行Rails应用,它需要Ruby解释器,对Python应用也一样。Go和其他一些编译型语言有些许不同,我以下会提到。
在Java例子中,下一步要想的是:JRE需要什么依赖才能运行?因为它是让应用能运行的最重要的组件,所以很自然的下一步就是要想清楚JRE运行依赖于什么。
而实际上JRE并没太多依赖,它本来就是作为操作系统的抽象层,使代码不依赖于宿主系统运行,因此安装好JRE就基本准备就绪了。
(实际上,对操作系统的独立性并不是理所当然的事,有非常多的系统特有API和专有的系统扩展,但是便于举例,我们把注意力放在简单的情况下)
在Linux上,JVM主要是调用系统的C语言库,Oracle的官方JRE,使用的是libc,也就是glibc,这意味着你要运行任何Java程序,都需要先装好glibc。另外你可能需要某种shell来管理环境,还有一个与外部通讯的接口,例如网络和资源的接口。
我们总结一下Java应用示例需要的最低配置是:
## 走进Alpine Linux ##
Alpine Linux最近得到很多关注,主要是因为它打包了一系列的经过验签的可信任的依赖,并且还保持体积在2MB!而在本文发布时,其他的一些镜像分发版如下:
** Busybox是最小的竞争者?**
从上边的对比中你可以看到,在体积上唯一能打败Alpine Linux的是Busybox,所以现在几乎所有嵌入式系统都是使用它,它被应用在路由器,交换机,ATM,或者你的吐司机上。它作为一个最最基础的环境,但是又提供了足够容易维护的shell接口。
在网上有很多文章解释了为什么人们会选择Alpine Linux而不是Busybox,我在这总结一下:
正如我刚解释的,Alpine Linux是一个构建自有镜像时不错的选择,因此,我们在此将使用它来构建简洁高效的Docker镜像,我们开始吧!
组合:Alpine + bash
每个Dockerfile第一个指令都是指定它的父级容器,通常是用于继承,在我们的例子中是<code>alpine:latest</code>:
sh FROM alpine:latest MAINTAINER cSphere <docker@csphere.cn>
我们同时声明了谁为这个镜像负责,这个信息对上传到Docker Hub的镜像是必要的。
就这样,你就有了往下操作的基础,接下来安装我们选好的shell,把下边的命令加上:
sh RUN apk add --no-cache --update-cache bash CMD ["/bin/bash"]
最终的Dockerfile是这样:
```sh
FROM alpine:latest
MAINTAINER cSphere < docker@csphere.cn >
RUN apk add --no-cache --update-cache bash
CMD ["/bin/bash"]
```
好了,现在我们构建容器:
sh $ docker build -t my-java-base-image . Sending build context to Docker daemon 2.048 kB Step 1 : FROM alpine:latest ---> 2314ad3eeb90 Step 2 : MAINTAINER cSphere <docker@csphere.cn> ---> Running in 63433312d77e ---> bfe94713797a Removing intermediate container 63433312d77e ... 省略若干行 Step 4 : CMD /bin/bash ---> Running in d2291684b797 ---> ecc443d68f27 Removing intermediate container d2291684b797 Successfully built ecc443d68f27
并且运行它:
sh $ docker run --rm -ti my-java-base-image bash-4.3#
成功了!我们有了一个运行着bash的Alpine Linux。
glibc and friends
前边提到,Oracle的JRE依赖于glibc,Alpine Linux上并没有glibc,它使用一个更小体积的替代版,叫musl libc。glibc发展了这么多年,几乎包含了所有C语言中需要的依赖包,显然这样会很不灵活,一个glibc库被编译进Alpine Linux,勉强能维持在5MB的体积,而它的替代者musl-libc是一个二进制文件,只有897KB,并且支持了所有Linux架构上的C依赖。
对Oracle的JRE,没有办法不把glibc加上,幸运的是,Andy Shinn已经做过了这些,他提供了一个预编译的glibc镜像给Alpine Linux,在Github上的alpine-pkg-glibc,最新版是2.23-r1。
这样把这相关依赖加到Dockerfile中:
```sh
ENV GLIBC_PKG_VERSION=2.23-r1
RUN apk add --no-cache --update-cache curl ca-certificates bash && /
curl -Lo /etc/apk/keys/andyshinn.rsa.pub " https://github.com/andyshinn/a ... load/ ${GLIBC_PKG_VERSION}/andyshinn.rsa.pub" && /
curl -Lo glibc-${GLIBC_PKG_VERSION}.apk " https://github.com/andyshinn/a ... load/ ${GLIBC_PKG_VERSION}/glibc-${GLIBC_PKG_VERSION}.apk" && /
curl -Lo glibc-bin-${GLIBC_PKG_VERSION}.apk " https://github.com/andyshinn/a ... load/ ${GLIBC_PKG_VERSION}/glibc-bin-${GLIBC_PKG_VERSION}.apk" && /
curl -Lo glibc-i18n-${GLIBC_PKG_VERSION}.apk " https://github.com/andyshinn/a ... load/ ${GLIBC_PKG_VERSION}/glibc-i18n-${GLIBC_PKG_VERSION}.apk" && /
apk add glibc-${GLIBC_PKG_VERSION}.apk glibc-bin-${GLIBC_PKG_VERSION}.apk glibc-i18n-${GLIBC_PKG_VERSION}.apk && /
```
现在我们的Dockerfile看起来是这样:
```sh
FROM alpine:latest
MAINTAINER cSphere < docker@csphere.cn >
ENV GLIBC_PKG_VERSION=2.23-r1
RUN apk add --no-cache --update-cache curl ca-certificates bash && /
curl -Lo /etc/apk/keys/andyshinn.rsa.pub " https://github.com/andyshinn/a ... load/ ${GLIBC_PKG_VERSION}/andyshinn.rsa.pub" && /
curl -Lo glibc-${GLIBC_PKG_VERSION}.apk " https://github.com/andyshinn/a ... load/ ${GLIBC_PKG_VERSION}/glibc-${GLIBC_PKG_VERSION}.apk" && /
curl -Lo glibc-bin-${GLIBC_PKG_VERSION}.apk " https://github.com/andyshinn/a ... load/ ${GLIBC_PKG_VERSION}/glibc-bin-${GLIBC_PKG_VERSION}.apk" && /
curl -Lo glibc-i18n-${GLIBC_PKG_VERSION}.apk " https://github.com/andyshinn/a ... load/ ${GLIBC_PKG_VERSION}/glibc-i18n-${GLIBC_PKG_VERSION}.apk" && /
apk add glibc-${GLIBC_PKG_VERSION}.apk glibc-bin-${GLIBC_PKG_VERSION}.apk glibc-i18n-${GLIBC_PKG_VERSION}.apk
CMD ["/bin/bash"]
```
我们一句句解释一下这些指令:
sh ENV GLIBC_PKG_VERSION=2.23-r1
我们通过变量指定GitHub上的glibc版本,所以每当一个新版本发布,都不需要更改URL,而直接更改这个变量即可。
sh RUN apk add --update-cache curl ca-certificates bash && /
这个指令会使用apk命令安装我们需要的包,包括curl和ca-certificates(以便使用TLS的页面),最后的bash是我们Dockerfile上个版本已经有的了。
sh curl -Lo /etc/apk/keys/andyshinn.rsa.pub "https://github.com/andyshinn/alpine-pkg-glibc/releases/download/${GLIBC_PKG_VERSION}/andyshinn.rsa.pub" && / curl -Lo glibc-${GLIBC_PKG_VERSION}.apk "https://github.com/andyshinn/alpine-pkg-glibc/releases/download/${GLIBC_PKG_VERSION}/glibc-${GLIBC_PKG_VERSION}.apk" && / curl -Lo glibc-bin-${GLIBC_PKG_VERSION}.apk "https://github.com/andyshinn/alpine-pkg-glibc/releases/download/${GLIBC_PKG_VERSION}/glibc-bin-${GLIBC_PKG_VERSION}.apk" && / curl -Lo glibc-i18n-${GLIBC_PKG_VERSION}.apk "https://github.com/andyshinn/alpine-pkg-glibc/releases/download/${GLIBC_PKG_VERSION}/glibc-i18n-${GLIBC_PKG_VERSION}.apk" && /
这些命令会接着刚刚的RUN指令,它们会从GitHub下载相关公钥和依赖包。
sh apk add glibc-${GLIBC_PKG_VERSION}.apk glibc-bin-${GLIBC_PKG_VERSION}.apk glibc-i18n-${GLIBC_PKG_VERSION}.apk
所有包下载完成后,我们会用这一行命令安装全部,由于我们之前添加了公钥,所以它们的签名会被验证。
好了!我们现在有了一个能运行几乎全部依赖于glibc包的环境。
Java运行环境
一般来说,Oracle不提供软件仓库的形式让人们下载,但是人们总是会找到一些方法绕过它,你可以使用以下命令把JRE添加到Docker镜像中:
```sh
ENV JAVA_VERSION_MAJOR=8 /
JAVA_VERSION_MINOR=73 /
JAVA_VERSION_BUILD=02 /
JAVA_PACKAGE=server-jre
WORKDIR /tmp
RUN curl -jksSLH "Cookie: oraclelicense=accept-securebackup-cookie" /
" http://download.oracle.com/otn-pub/java/jdk/ ${JAVA_VERSION_MAJOR}u${JAVA_VERSION_MINOR}-b${JAVA_VERSION_BUILD}/${JAVA_PACKAGE}-${JAVA_VERSION_MAJOR}u${JAVA_VERSION_MINOR}-linux-x64.tar.gz" | gunzip -c - | tar -xf - && /
apk del curl ca-certificates && /
mv jdk1.${JAVA_VERSION_MAJOR}.0_${JAVA_VERSION_MINOR}/jre /jre && /
rm /jre/bin/jjs && /
rm /jre/bin/keytool && /
rm /jre/bin/orbd && /
rm /jre/bin/pack200 && /
rm /jre/bin/policytool && /
rm /jre/bin/rmid && /
rm /jre/bin/rmiregistry && /
rm /jre/bin/servertool && /
rm /jre/bin/tnameserv && /
rm /jre/bin/unpack200 && /
rm /jre/lib/ext/nashorn.jar && /
rm /jre/lib/jfr.jar && /
rm -rf /jre/lib/jfr && /
rm -rf /jre/lib/oblique-fonts && /
rm -rf /tmp/* /var/cache/apk/* && /
echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf
ENV JAVA_HOME /jre
ENV PATH ${PATH}:${JAVA_HOME}/bin
```
这堆命令究竟做了什么,我们还是一句句来看一下吧:
```sh
ENV JAVA_VERSION_MAJOR=8 /
JAVA_VERSION_MINOR=73 /
JAVA_VERSION_BUILD=02
JAVA_PACKAGE=server-jre
WORKDIR /tmp
```
这句非常简单,它定义了我们要从Oracle服务器上要下载的软件版本,本文编写时,上边的版本号是最新的,以后可能会变化,你可以从Oracle官网上查看。它同时也指定了<code>WORKDIR</code>工作目录,我们需要从一个临时目录开始运行,所以这里设置了/tmp。
sh RUN curl -jksSLH "Cookie: oraclelicense=accept-securebackup-cookie" / "http://download.oracle.com/otn-pub/java/jdk/${JAVA_VERSION_MAJOR}u${JAVA_VERSION_MINOR}-b${JAVA_VERSION_BUILD}/${JAVA_PACKAGE}-${JAVA_VERSION_MAJOR}u${JAVA_VERSION_MINOR}-linux-x64.tar.gz" | gunzip -c - | tar -xf - && /
这句稍微有点复杂,它使用curl传了一个指定的头信息("Cookie: oraclelicense=accept-securebackup-cookie"),以从Oracle上获取真正的下载包,这是必须的,不然会返回一个错误页。然后它会把下载好的包通过管道传给gunzip和tar ,换言之,它并不会保存下载回来的tar包,而是直接解压出来到磁盘上。
sh apk del curl ca-certificates && /
这时curl和ca-certificates两个包都完成了它们的使命,可以删除了它们以节省空间。
sh rm /jre/bin/jjs && / rm /jre/bin/keytool && / rm /jre/bin/orbd && / rm /jre/bin/pack200 && / rm /jre/bin/policytool && / rm /jre/bin/rmid && / rm /jre/bin/rmiregistry && / rm /jre/bin/servertool && / rm /jre/bin/tnameserv && / rm /jre/bin/unpack200 && / rm /jre/lib/ext/nashorn.jar && / rm /jre/lib/jfr.jar && / rm -rf /jre/lib/jfr && / rm -rf /jre/lib/oblique-fonts && / rm -rf /tmp/* /var/cache/apk/* && /
JRE自带了一些工具包,可能永远都不会用到的,我们也将它们删掉。 最后一行,会把全部临时文件和apk的包缓存也清理了。
sh echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf
这一行中,我们修改了nsswitch.conf,以确保网络正常,这会被glibc等包所用到。
最后,我们的Dockerfile会是下边这样:
```sh
FROM alpine:latest
MAINTAINER cSphere < docker@csphere.cn >
ENV JAVA_VERSION_MAJOR=8 /
JAVA_VERSION_MINOR=73 /
JAVA_VERSION_BUILD=02 /
JAVA_PACKAGE=server-jre /
GLIBC_PKG_VERSION=2.23-r1 /
LANG=en_US.UTF8
WORKDIR /tmp
RUN apk add --no-cache --update-cache curl ca-certificates bash && /
curl -Lo /etc/apk/keys/andyshinn.rsa.pub " https://github.com/andyshinn/a ... load/ ${GLIBC_PKG_VERSION}/andyshinn.rsa.pub" && /
curl -Lo glibc-${GLIBC_PKG_VERSION}.apk " https://github.com/andyshinn/a ... load/ ${GLIBC_PKG_VERSION}/glibc-${GLIBC_PKG_VERSION}.apk" && /
curl -Lo glibc-bin-${GLIBC_PKG_VERSION}.apk " https://github.com/andyshinn/a ... load/ ${GLIBC_PKG_VERSION}/glibc-bin-${GLIBC_PKG_VERSION}.apk" && /
curl -Lo glibc-i18n-${GLIBC_PKG_VERSION}.apk " https://github.com/andyshinn/a ... load/ ${GLIBC_PKG_VERSION}/glibc-i18n-${GLIBC_PKG_VERSION}.apk" && /
apk add glibc-${GLIBC_PKG_VERSION}.apk glibc-bin-${GLIBC_PKG_VERSION}.apk glibc-i18n-${GLIBC_PKG_VERSION}.apk && /
curl -jksSLH "Cookie: oraclelicense=accept-securebackup-cookie" /
" http://download.oracle.com/otn-pub/java/jdk/ ${JAVA_VERSION_MAJOR}u${JAVA_VERSION_MINOR}-b${JAVA_VERSION_BUILD}/${JAVA_PACKAGE}-${JAVA_VERSION_MAJOR}u${JAVA_VERSION_MINOR}-linux-x64.tar.gz" | gunzip -c - | tar -xf - && /
apk del curl ca-certificates && /
mv jdk1.${JAVA_VERSION_MAJOR}.0_${JAVA_VERSION_MINOR}/jre /jre && /
rm /jre/bin/jjs && /
rm /jre/bin/keytool && /
rm /jre/bin/orbd && /
rm /jre/bin/pack200 && /
rm /jre/bin/policytool && /
rm /jre/bin/rmid && /
rm /jre/bin/rmiregistry && /
rm /jre/bin/servertool && /
rm /jre/bin/tnameserv && /
rm /jre/bin/unpack200 && /
rm /jre/lib/ext/nashorn.jar && /
rm /jre/lib/jfr.jar && /
rm -rf /jre/lib/jfr && /
rm -rf /jre/lib/oblique-fonts && /
rm -rf /tmp/* /var/cache/apk/* && /
echo 'hosts: files mdns4_minimal [NOTFOUND=return] dns mdns4' >> /etc/nsswitch.conf
ENV JAVA_HOME=/jre
ENV PATH=${PATH}:${JAVA_HOME}/bin
```
注意这里,我整合了两个ENV和RUN指令,因为最好是用更少的中间层,特别是这个容器是作为通用的构建单元。
简单来说,有一个规则:你需要更大的灵活性,那你需要更多的层;如果你需要减小体积和降低复杂度,你需要更少的层。这完全取决于你的需求。
在顶部我还加上了这句:
sh ENV LANG=en_US.UTF-8
这句是为了确保运行在这个系统环境的应用能指定语言。你可以根据需要设定这个LANG环境变量。
另外,JAVA_HOME和PATH也要设置好,以使用刚刚装好的JRE。
CMD指令会怎么运行?
我之前提到,我们这是在构建一个能提供给其他服务作为基础的镜像,它不需要带上CMD指令,因为它永远不会运行,但是一旦一个服务关联上它,就需要用到了。
不过你还是可以通过其他方式启动这个容器,例如<code>docker run</code>或<code>docker exec</code>指令:
sh $ docker run --rm -ti my-java-base-image /bin/bash
## 构建最终镜像 ##
最后,我们终于到了构建镜像这步了:
sh $ docker build -t my-java-base-image . Sending build context to Docker daemon 60.42 kB Step 1 : FROM alpine:latest ---> 2314ad3eeb90 Step 2 : MAINTAINER cSphere <docker@csphere.cn> ---> Using cache ---> 93cc2bc0bd60 Step 3 : ENV JAVA_VERSION_MAJOR 8 JAVA_VERSION_MINOR 73 JAVA_VERSION_BUILD 02 JAVA_PACKAGE server-jre GLIBC_PKG_VERSION 2.23-r1 LANG en_US.UTF8 ---> Running in 3f0ffeaeca78 ---> 1dcfd34b0f1a Removing intermediate container 3f0ffeaeca78 ... 省略若干行 Removing intermediate container 0a98b36a6e37 Step 7 : ENV PATH ${PATH}:${JAVA_HOME}/bin ---> Running in 54d0dfb04f98 ---> 493399ac9ca6 Removing intermediate container 54d0dfb04f98 Successfully built 493399ac9ca6
哈哈!它执行成功了。我们运行容器里的java来验证一下吧:
sh $ docker run --rm -ti my-java-base-image java -version java version "1.8.0_73" Java(TM) SE Runtime Environment (build 1.8.0_73-b02) Java HotSpot(TM) 64-Bit Server VM (build 25.73-b02, mixed mode)
太好了,这正是我们要看到的结果,我们已经有了一个独立的Oracle JRE环境,以后我们只需要基于这个镜像来构建应用镜像即可:
```sh
FROM my-java-base-image
[...]
```
最终镜像有多大?
我们来看看:
sh $ docker images | grep my-java-base-image | awk '{print $7,$8}' 130.4 MB
说实话,这还是挺大的,但是毕竟里边装的是Java嘛~
## 总结 ##
我们现在构建了一个安全、轻量的Docker镜像,基本上可以运行任何Java应用在上面,当然你也可以根据实际情况调整这个Dockerfile,但是主要的思想还是像上边说的那样,减小体积,使用安全的软件源。
一旦你明白Docker容器只是一个基础的单进程容器,只是一个应用运行的环境,它能让你专注于应用的构建而不是其他杂七杂八的依赖关系,你就会把Docker应用到得心应手。
以下是简单的几点指引:
docker login index.csphere.cn # 账号在 https://csphere.cn/hub 上获取 docker pull index.cspehre.cn/microimages/alpine:3.3 docker pull index.csphere.cn/microimages/alpine-glibc:3.3## 关于希云cSphere ##
希云cSphere是一个高度集成、功能强大的Docker私有云平台和类PaaS解决方案,其架构设计借鉴了VMWare vSphere的思想。系统健壮性比肩VMWare这样的商业产品,产品经过一年多十多个版本的迭代更新,在内部更是经历了1000次以上的破坏性测试,目前已经在金融、制造、游戏、安全、电商、教育等多个领域落地。
cSphere的亮点:
欢迎联系我们: