此博客除介绍Dockerfile的基本概念外,还会介绍如何将一个go语言编写的代码,通过Dockerfile构建成镜像,上传到docker hub仓库,启动构建的镜像,并在本机上完成对应用的访问。
为了学习如何编写Dockerfile,首先需要理清2个概念。
镜像构建上下文:
为了透彻理解镜像构建上下文,先介绍下docker build工作原理。Docker 在运行时分为 Docker 引擎(也就是服务端守护进程)和客户端工具。Docker 的引擎提供了一组 REST API,被称为 Docker Remote API,而如 docker 命令这样的客户端工具,则是通过这组 API 与 Docker 引擎交互,从而完成各种功能。因此,虽然表面上我们好像是在本机执行各种 docker 功能,但实际上,一切都是使用的远程调用形式在服务端(Docker 引擎)完成。也因为这种 C/S 设计,让我们操作远程服务器的 Docker 引擎变得轻而易举。那么在这种客户端/服务端的架构中,如何才能让服务端获得本地文件呢?
这就引入了上下文的概念。当构建的时候,用户会指定构建镜像上下文的路径,docker build 命令得知这个路径后,会将路径下的所有内容打包,然后上传给 Docker 引擎。这样 Docker 引擎收到这个上下文包后,展开就会获得构建镜像所需的一切文件。
例如命令:COPY ./package.json /app/,是指复制上下文目录下的package.json文件到镜像的/app/目录下。
那么docker build是如何指定构建镜像上下文路径的呢?
如果docker build命令的目录为.即执行的命令例如:“docker build -t test:0.1 . ”,镜像上下文路径就是当前目录,即执行docker build命令时的目录;如果构建镜像名是"docker build -f Dockerfile -t test:0.1 src/app",那么构建镜像上下文路径是执行命令所在目录+/src/app组合的目录。
WORKDIR:
shell 格式:RUN <命令>
exec 格式:RUN ["可执行文件", "参数1", "参数2"]
例如:RUN apt-get update
之前说过,Dockerfile 中每一个指令都会建立一层,RUN 也不例外。每一个 RUN 的行为,就和刚才我们手工建立镜像的过程一样:新建立一层,在其上执行这些命令,执行结束后,commit 这一层的修改,构成新的镜像。为了减少镜像的层数,如果是多个RUN命令,用&&符号进行连接
- shell 格式:CMD <命令>
- exec 格式:CMD ["可执行文件", "参数1", "参数2"...]
- CMD echo test
- CMD ["nginx", "-g", "daemon off;"]
- 注意:每个容器只能执行一条CMD命令,多个CMD命令时,只最后一条被执行
- ENV命令:
- ENV设置环境变量,有两种格式
- ENV <key> <value>
ENV <key1>=<value1> <key2>=<value2>... - EXPOSE命令:
- expose暴露容器端口,格式为:EXPOSE <端口1> [<端口2>...]
- ENTRYPOINT命令:
- ENTRYPOINT 的格式和 RUN 指令格式一样,分为 exec 格式和 shell 格式
- 例如exec格式:CMD [ "curl", "-s", "http://www.baidu.com" ]
注意:ENTRYPOINT 配置容器启动后执行的命令,并且不可被 docker run 提供的参数覆盖 - WORKDIR命令:
- 指定容器的工作目录,格式为 WORKDIR <工作目录路径>
- USER命令:
- 指定后续层执行dockerfile中命令的用户,格式:USER <用户名>[:<用户组>]
- 前面主要介绍了一些DockerFile的常用命令,接下来通过例子实际看看如果通过DockerFile构建镜像,如下图所示,这里有一个Dockerfile
- Dockerfile中基础镜像使用busybox,busybox 是一个集成了多个linux命令的镜像。
- 在执行docker build命令时,会cd到demo1目录下,具体的构建命令是:“docker build -t hello:0.1 .”
- 因为构建命令中的目录是.,.表示当前目录,所以镜像上下文路径即demo1目录。所以在Dockerfile中COPY hello.txt .命令不会报错,因为COPY命令是从构建镜像上下文路径中复制文件,而hello.txt文件就在demo1目录下;紧接着设置镜像的工作目录是/html,然后是"COPY /html/index.html .",构建镜像上下文路径是demo1目录,所以如果要复制index.html文件,前面需要添加/html目录,而因为已经设置了WORKDIR是/html,所以,index.html文件会复制到容器镜像的/html目录下。
- 构建镜像后,执行docker run命令启动容器,启动后,可以看到,默认进入容器的/html目录,该目录下有index.html文件,所以Dockerfile中的复制命令生效。
紧接着我们再来看另外一个Dockerfile例子,这个例子里面演示构建go语言编写的一个简单程序,然后推送到dockerhub这个公共的镜像仓库中。为了能将镜像推送到docker hub上,首先需要在docker hub上注册账号,然后创建repository,如下图所示:
basic.go文件中用go语言编写了很简单的一个http服务,暴露的端口是9099
下面是Dockerfile文件以及目录层级结构
CD到demo2目录,执行构建命令:docker build -f Dockerfile -t tlqiao/http-basic:0.1 src/http
因为构建镜的registry是docker hub中的registry,故需要通过docker login命令登陆docker hub,这里输入docker hub注册时的用户名和密码。登陆成功后,再执行docker build命令。
docker build命令指定的构建镜像上下文是src/http,上面的dockerfile中先是指定WOKRKDIR为 /build,然后COPY . .,即将构建上下文目录中的所有文件拷贝到当前目录,也就是将demo2/src/http中的basic.go文件拷贝到/build目录下,接着执行 RUN go build -o httpServer命令,因为basic.go文件就是main文件,所以go build可正常构建出编写的简单http服务。
接下来又是FROM命令,这里就涉及到Docker的多阶段构建,为什么会出现多阶段构建呢?试想一下,我们在构建镜像的时候有一种方式是将所有的构建过程编包含在一个 Dockerfile 中,包括项目及其依赖库的编译、测试、打包等流程。但是,这样可能会带来的一些问题:
- 镜像层次多,镜像体积较大,部署时间变长
- 源代码存在泄露的风险
为了避免这个问题,出现了多阶段构建,即构建某一阶段的镜像,可以使用as作为某一阶段命名。如上面的Dockerfile,第一阶段只是将go语言编写的代码构建成可执行的二进制文件,第二阶段将上一阶段构建的可执行文件COPY过来,因为第一个阶段的名称是builder,所以命令是“COPY --from=builder /build/httpServer /”,在上一段构建中设置了WORKDIR后才执行的构建命令,所以httpserver的二进制文件在/build目录下,另外,第二阶段构建是FROM scratch,scratch是一个特殊的docker镜像,这个镜像是虚拟的概念,并不实际存在,它表示一个空白的镜像。
镜像构建完成后,查看镜像docker images,可以看到http-basic镜像已经构建出来了。
执行命令:docker run -d -p 8090:9099 tlqiao/http-basic:0.1将镜像启动起来。-p设置端口映射,第一个端口是宿主机端口,第二个端口是容器进程端口,因为容器的端口在前面的程序和dockerfile中已经设置为9099,所以容器端口一定是9099,前面的宿主机端口可任意设置,这里设置成8090,后面在宿主机上访问,就通过8090进行访问
启动成功后,在宿主机器上访问“http://localhost:8090/hello”,可以看到访问成功。
最后,如果想保证镜像可在任意地方获取,那么可以通过docker push“docker push tlqiao/http-basic:0.1”命令将本地构建出来的镜像推送到docker hub仓库,这样在任何地方都可以通过docker pull命令获取到镜像了。
通过dockerfile构建镜像过程中,镜像越小越好,有如下策略尽可能的保证镜像比较小
一:使用尽可能小的base image,例如可以使用alpine/busybox/static等作为base image。
二:采用多段构建策略
三:删除无用的包,如下所示,下载安装完成后,把无用的包文件删除
RUN apt-get update && apt-get install -y \
bzr \
cvs \
git \
mercurial \
subversion \
# Don't remmeber remove apt cache
&& rm -rf /var/lib/apt/lists/*
四:使用尽量少的容器层,RUN等命令会创建新的镜像层,可以用&&串联多个RUN命令,另外,下载tar文件和解压tar文件一定要用&&进行串联。