一个基础的 Dockerfile

Dockerfile 是用于构建 Docker 镜像的一个文本文件,在 Dockerfile 中用户可以定义容器的基础镜像是什么,如何将外部二进制放入镜像中,容器里的程序如何启动等等。

Dockerfile 的语法比较简单易读,拿一个最简单的 Dockerfile 举例:

FROM ubuntu:latest
RUN apt-get update && apt-get install nginx -y
CMD [“sh”, “-c”, “nginx -g ‘daemon off;’”]

以上三行的解释如下:

  • 此镜像会使用 Ubuntu 最新版本作为基础镜像构建;

  • 运行 apt-get update && apt-get install nginx -y 命令来安装 nginx 软件包;

  • 通过 sh 启用 nginx,并使得 nginx 在前台运行(在 Docker 下,PID 为 1 的进程必须在前台运行,否则容器会退出)。

参数解释如下:

  • FROM :设置基础镜像名称,此选项为必选的;

  • RUN:在构建镜像时运行一些命令,这些命令可以是软件更新,软件安装,从其他路径获取镜像需要的文件等;

  • CMD:设置容器默认的启动命令。

通过 Dockerfile 构建镜像

如果要通过 Dockerfile 构建镜像文件,找一台安装好 Docker 的机器,新建一个目录,将 Dockerfile 放在该目录下:

🥚5Ax03 Tanzu 3 基础的 Dockerfile - 图1

运行下列命令即可进行镜像构建:

docker build . -t nginx:test

🥚5Ax03 Tanzu 3 基础的 Dockerfile - 图2

🥚5Ax03 Tanzu 3 基础的 Dockerfile - 图3

当进行构建完成后,最后一行会提示 Successfully tagged nginx:test,此时通过 docker images 命令可以查看到新建的 nginx:test 镜像。

🥚5Ax03 Tanzu 3 基础的 Dockerfile - 图4

在本地运行镜像

通过下列命令运行容器:

docker run -d -p 8899:80 nginx:test

🥚5Ax03 Tanzu 3 基础的 Dockerfile - 图5

通过浏览器访问 Linux host 的 8899 端口,可以正常访问到,页面为 Nginx 的默认页面:

🥚5Ax03 Tanzu 3 基础的 Dockerfile - 图6

如果换个启动命令会如何?

上个章节我们在 run 容器时未配置任何的启动参数(即 image 名称后未包含任何命令),那如果添加一个启动命令会怎样?

通过下列命令运行容器:

docker run -it -p 8899:80 nginx:test sh

🥚5Ax03 Tanzu 3 基础的 Dockerfile - 图7

再次通过浏览器访问 Linux Host 的 8899 端口,发现页面无法打开。

🥚5Ax03 Tanzu 3 基础的 Dockerfile - 图8

这是预期的现象,一开始我们在 Dockerfile 中设置的 CMD 只是一个默认的启动命令,docker run 可以覆盖掉,上述实验中的 sh 即是覆盖 CMD 的参数,进入到容器后通过 ps 查看进程,会发现没有 Nginx 相关的进程,证明 CMD 确实未被执行:

🥚5Ax03 Tanzu 3 基础的 Dockerfile - 图9

ENTRYPOINT

在 Docker 的官方手册中,有两种办法设置容器的默认启动命令,CMD 和 ENTRYPOINT,一般在 Dockerfile 中必须设置 CMD 或者 ENTRYPOINT,两者也可以同时使用。

简单来说,ENTRYPOINT 可以用于设置容器的默认启动命令,并且不能轻易被覆写。

比如将一开始的示例 Dockerfile 中的 CMD 换为 ENTRYPOINT 后,再次通过下列命令启动容器,会发现网页能正常访问:

🥚5Ax03 Tanzu 3 基础的 Dockerfile - 图10

如果进入到容器中通过 ps 查看进程,会发现我们输入的 sh 被作为参数传递到了 ENTRYPOINT 设置的命令之后。

🥚5Ax03 Tanzu 3 基础的 Dockerfile - 图11

ENTRYPOINT 与 CMD 混合使用

当把 ENTRYPOINT 和 CMD 混合使用时,会出现和上个实验类似的现象,即 CMD 设置的值会作为参数传递给 ENTRYPOINT。

示例 Dockerfile:

FROM ubuntu:latest
RUN apt-get update && apt-get install nginx -y
ENTRYPOINT [“sh”, “-c”]
CMD [“nginx -g ‘daemon off;’”]

再次构建镜像并运行镜像,网页可以正常访问。进入到容器后,查看启动命令,和最早单行 CMD 的执行结果是一致的:

🥚5Ax03 Tanzu 3 基础的 Dockerfile - 图12

在 Docker 的官方文档中,建议通过 ENTRYPOINT 作为容器的主启动命令,CMD 可以辅助地作为启动参数。在实际应用中,可以视需求只使用 CMD 或者 ENTRYPOINT,或者两者同时使用。

自定义镜像

在第一章中我们已经做好了一个镜像,也可以正常运行,我们可以在此基础上,稍微调整下默认的访问界面:

比如,将默认页面调整为下列内容:

This is a Test Page

要实现此功能,可以使用下列 Dockerfile:

FROM ubuntu:latest
RUN apt-get update && apt-get install nginx -y && echo “

This is a Test Page

“>/var/www/html/index.nginx-debian.html
CMD [“sh”, “-c”, “nginx -g ‘daemon off;’”]

简单来说就是在 RUN 中通过命令将 HTML 内容写入到了默认的 Nginx 页面中。

构建镜像并运行容器后,测试效果如下:

🥚5Ax03 Tanzu 3 基础的 Dockerfile - 图13

如果是复杂的页面替换呢?

通过 RUN 可以替换简单的页面,如果是复杂的页面,则可以预先写好 HTML 文件,然后拷贝到容器中使用。

在实验中我们在 Dockerfile 的根目录下创建了 src 目录,然后创建了名为 index.html 的文件:

🥚5Ax03 Tanzu 3 基础的 Dockerfile - 图14

使用下列 Dockerfile 构建镜像:

FROM ubuntu:latest
RUN apt-get update && apt-get install nginx -y
COPY src/index.html /var/www/html/
CMD [“sh”, “-c”, “nginx -g ‘daemon off;’”]

运行容器后访问效果如下:

🥚5Ax03 Tanzu 3 基础的 Dockerfile - 图15

成功替换了默认的访问页面。

在新的 Dockerfile 中我们使用了另一个很重要的命令:COPY,如其名称一样,意思是将指定的文件拷贝到容器镜像的指定目录中。

参数传递

前面的示例中,我们只是做了一个简单的静态页面,现实中容器几乎不会是静态不变的,这时候就涉及到如何将外部参数传递给容器,且容器要能够读取并使用这个参数。

这时候就用到了第一篇文章中提到的环境变量。

环境变量的传递在第二篇文章中简单提过,通过 -e 参数来指定。本文着重讲讲在容器中如何调用。

其中最简单的方法就是:shell 脚本。

还是以上述的 nginx 网页为例,我们希望在每次容器启动时,都能够从环境变量中获取主机名等信息,然后将这些信息展示在 web 页面中,为了实现这个需求,需要做以下修改:

  • 预先编写好 index.html 模板,在模板中设置“关键词”,这些关键词可以通过 sed 等工具查找替换成其他内容;
  • 编写 shell 脚本,这个脚本在每次容器启动时会运行,脚本会通过 sed 等工具将 index.html 中的关键词替换为环境变量中的值;shell 脚本的末尾设置容器启动参数;
  • 修改 Dockerfile,将必要的文件通过 COPY 复制到容器中;修改 CMD,将默认启动命令修改为 shell 脚本。

具体的步骤如下:

1、修改一下上个章节中的 index.html 文件,新增几行,每一行都有一些“默认关键词”,这些关键词未来会通过 shell 脚本替换掉。

🥚5Ax03 Tanzu 3 基础的 Dockerfile - 图16

🥚5Ax03 Tanzu 3 基础的 Dockerfile - 图17

2、用于替换关键词的脚本如下,工作机制是通过 sed 来进行查找替换,比如将 demoapp 替换成环境变量 HOSTNAME:

🥚5Ax03 Tanzu 3 基础的 Dockerfile - 图18

3、相应的 Dockerfile 内容如下:

FROM ubuntu:latest
WORKDIR /var/www/html/
RUN apt-get update && apt-get install nginx -y
COPY src /var/www/html/
COPY start.sh /usr/bin/start.sh
CMD [“/usr/bin/start.sh”]

完整的目录结构如下:

🥚5Ax03 Tanzu 3 基础的 Dockerfile - 图19

构建并通过下列命令运行容器:

docker run -e hostinfo=$HOSTNAME -p 8899:80 nginx:test

访问到的页面内容如下,已经成功地显示 hostname,Private IP,宿主机信息等内容:

🥚5Ax03 Tanzu 3 基础的 Dockerfile - 图20

默认工作路径

在上面的示例 Dockerfile 中,我们新引入了一个参数 WORKDIR,此参数用于定义 RUN、COPY、CMD、ENTRYPOINT 等的默认路径。

因此,上述 Dockerfile 可以进行微调,将 COPY 中的绝对路径 “/var/www/html” 替换为相对路径 “.”,表示将文件复制到 WORKDIR 设置的 /var/www/html 下:

FROM ubuntu:latest
WORKDIR /var/www/html/
RUN apt-get update && apt-get install nginx -y
COPY src .
COPY start.sh /usr/bin/start.sh
CMD [“/usr/bin/start.sh”]
You have mail in /var/spool/mail/root

在编写 start.sh 启动脚本时,我们额外添加了 pwd 命令,用于在执行脚本时显示当前的目录,运行容器时显示当前路径为 /var/www/html,和 WORKDIR 一致:

🥚5Ax03 Tanzu 3 基础的 Dockerfile - 图21

日志输出

在容器环境下,一般需要将程序的运行日志输出到控制台方便排错,像 Nginx 等传统应用在运行时并不会将日志输出到控制台,此时也可以通过 shell 脚本来简单实现。

修改后的 start.sh 如下(此时 nginx 就需要放在后台执行,这样才能保证脚本运行到结尾 tail 处):

🥚5Ax03 Tanzu 3 基础的 Dockerfile - 图22

构建并运行容器后,再次通过网页访问,即可在容器的控制台看到访问日志:

🥚5Ax03 Tanzu 3 基础的 Dockerfile - 图23

至此,一个基础的镜像制作就到此为止,虽然简单,但其实很多传统应用到容器的改造都会利用到上面介绍的手段,比如 MySQL,官方的镜像就是使用 docker-entrypoint.sh 脚本进行初始化,根据环境变量设置数据库密码等信息。

🥚5Ax03 Tanzu 3 基础的 Dockerfile - 图24

精简镜像

在上面的例子中,我们使用 Ubuntu 作为基础镜像,使用 nginx 作为 web 引擎,其中 Ubuntu 镜像自身有 72M,做好的 nginx 镜像有 162M,单个镜像看好像并不是很大,但当环境中容器越来越多时,精简镜像就成了一件很有意义的事。

🥚5Ax03 Tanzu 3 基础的 Dockerfile - 图25

精简镜像一般可以带来下列好处:

  • 更小的空间占用:无论是对于容器镜像仓库还是运行容器的机器来说,都会占用更少的磁盘空间;

  • 更快的镜像拉取时间:一般在生产环境中运行容器时,都是先从镜像仓库下载镜像,再在本地启动容器,镜像越小拉取镜像更快;

  • 更快的启动时间:容器较小时,也可能有较快的启动速度;

  • 更加安全:一般通过精简镜像,可以移除没必要的二进制文件,从而也可以避免这些二进制潜在的安全风险。

使用更精简的基础镜像

在开源社区中,有一些区别于主流 Ubuntu、CentOS 存在的极简基础镜像,比如 Alpine、Busybox 等,其大小是 Ubuntu 等的数十分之一:

🥚5Ax03 Tanzu 3 基础的 Dockerfile - 图26

使用这些镜像通常不会有什么问题,但因为底层不一样,可能会遇到一些坑,比如 Alpine 默认会在域名解析时加上 search domain,但其他镜像不会,这就有可能导致域名解析问题。

更进一步的话,也可以直接使用 docker 内置的 scratch 作为基础镜像,scratch 相当于一个空镜像。

减少命令行数

在 Dockerfile 中,RUN、COPY 和 ADD 三种语句每一行都会对应地创建一个新的层,同种操作将多行命令合并成单行,一定程度上可以减少镜像大小。比如在编写 RUN 时,多个操作可以合并成一行,像文始的示例一样:

RUN apt-get update && apt-get install nginx -y

另外,减少 RUN、COPY 和 ADD 的使用也可以减少层,比如通过 “RUN wget http:///xx.tar” 来替换 “COPY/ADD local-fils destination” 将文件拷贝到容器中,这样可以省掉 COPY/ADD 带来的层。

删除临时文件

在进行一些安装、拷贝操作时,可能会残留一些临时文件,删除这些临时文件可以进一步缩小镜像大小。比如在使用 apt 更新库并安装软件后,可以加上下列清理命令:

RUN apt-get update && apt-get install nginx -y && rm -rf /var/lib/apt/lists/*

多阶段构建

Docker 支持多阶段构建,比如第一阶段使用 golang 镜像进行程序编译,第二阶段通过 COPY 将第一阶段的成品包添加到容器,再设置启动命令等等。

Stage-1
FROM golang:1.16-alpine AS build
RUN apk add —no-cache git
RUN go get github.com/golang/dep/cmd/dep
COPY Gopkg.lock Gopkg.toml /go/src/project/
WORKDIR /go/src/project/
RUN dep ensure -vendor-only
COPY . /go/src/project/
RUN go build -o /bin/project

Stage-2
FROM scratch
COPY —from=build /bin/project /bin/project
ENTRYPOINT [“/bin/project”]
CMD [“—help”]

更多内容

以上就是本文全部内容,只是作为一个引子,Docker 的官方文档很多都有介绍到,下面是一些文档链接:

Dockerfile 参考:

https://docs.docker.com/engine/reference/builder/

编写 Dockerfile 的最佳实践:

https://docs.docker.com/develop/develop-images/dockerfile_best-practices/

创建一个基础镜像:

https://docs.docker.com/develop/develop-images/baseimages/