参考:
https://vuepress.mirror.docker-practice.com/image/dockerfile/
Dockerfile 制作镜像示例
我们先制作一个简单的镜像来演示下制作过程。
开发一个可执行 jar 包
我们以 springboot
为例,开发一个端口为 8080
的应用服务,提供一个 /hello?name=tom
接口,并返回 name
的值。
访问 spring.io ,输入相关信息(全部使用默认值即可),添加 web 依赖,然后下载源码并解压。
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication
@RestController
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@GetMapping("/hello")
public String hello(String name) {
return "name is: " + name;
}
}
为了简化开发,这里直接在启动类上添加 hello
接口(请自行测试代码正确性)。将开发好的代码打包为可执行 jar 包,使用相关工具将 jar 包上传到服务器,更名为 demo.jar
。
编写 Dockerfile
在服务器上创建一个文件夹 /dockerDir/
(路径及文件夹名称随意,jar 包也放入这个文件夹下)
在 /dockerDir/
文件夹中创建一个名为 Dockerfile
的文件,其内容如下:
# 基础镜像(如没有会自动从远程仓库拉取)
FROM openjdk:8
# 将宿主机中的 demo.jar 包添加到容器 app.jar 中
ADD demo.jar app.jar
# 暴漏容器 8080 端口
EXPOSE 8080
# 容器启动时需要运行的命令
ENTRYPOINT exec java -Djava.security.egd=file:/dev/./urandom -jar /app.jar
制作镜像
使用下面的命令制作镜像 :
docker build -t my-server:v1 .
- docker build:运行Dockerfile 制作镜像的命令
- -t my-server:v1:镜像的名称及标签,需要注意的是镜像名称不能出现大写
- 最后的一个 '.':表示 Dockerfile 的上下文路径
使用 docker build
时,默认情况下会寻找当前目录下的 Dockerfile
文件,实际上我们可以使用 -f
参数指定文件所在路径以及文件名称,例如:-f /myfolder/dockerfile.txt
上面我们已经成功的构建了一个名为 my-server:v1
的镜像了,下面我们试着运行它:
# 将容器的 8080 端口映射到宿主机的 80 端口,容器名称为 demoServer
[root@localhost dockerDir]# docker run -d -p 80:8080 --name demoServer my-server:v1
f54b8c2f08b21b288b493877c8b29504a722afa4184cac54f52df06cff383b53
# 查看容器
[root@localhost dockerDir]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
f54b8c2f08b2 my-server:v1 "/bin/sh -c 'exec ja…" 13 seconds ago Up 11 seconds 0.0.0.0:80->8080/tcp demoServer
至此,制作镜像以及运行镜像的流程已经走完了。
Dockerfile 介绍
Dockerfile 是一个文本文件,其内包含了一条条的 指令(Instruction),每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。
示例中的 Dockerfile 使用到了 FROM
ADD
EXPOSE
ENTRYPOINT
等指令,实际上 Dcokerfile 还有很多其它指令,但是在介绍这些指令之前,我们先来介绍下 构建镜像上下文
。
构建镜像上下文
示例中的命令 docker build -t my-server:v1 .
,其最后一个 .
表示当前目录,也就是 /dockerDir
目录(即上下文路径)。
示例中的 Dockerfile 里存在这么一条指令 ADD demo.jar app.jar
,其中的 demo.jar
就是上下文中的 demo.jar
,也就是 /dockerDir
目录下的 demo.jar
。
通过上面的解释我们知道了 .
的作用之后,那么就能理解 docker build -t my-server:v1 .
这条命令是可以替换为 docker build -t my-server:v1 /dockerDir
的。
那么为什么要引入上下文这个概念呢?这里引用 链接 中的描述:
首先我们要理解 docker build
的工作原理。Docker 在运行时分为 Docker 引擎(也就是服务端守护进程)和客户端工具。Docker 的引擎提供了一组 REST API,被称为 Docker Remote API,而如 docker
命令这样的客户端工具,则是通过这组 API 与 Docker 引擎交互,从而完成各种功能。因此,虽然表面上我们好像是在本机执行各种 docker
功能,但实际上,一切都是使用的远程调用形式在服务端(Docker 引擎)完成。也因为这种 C/S 设计,让我们操作远程服务器的 Docker 引擎变得轻而易举。
当我们进行镜像构建的时候,并非所有定制都会通过 RUN
指令完成,经常会需要将一些本地文件复制进镜像,比如通过 COPY
指令、ADD
指令等。而 docker build
命令构建镜像,其实并非在本地构建,而是在服务端,也就是 Docker 引擎中构建的。那么在这种客户端/服务端的架构中,如何才能让服务端获得本地文件呢?
这就引入了上下文的概念。当构建的时候,用户会指定构建镜像上下文的路径,docker build
命令得知这个路径后,会将路径下的所有内容打包,然后上传给 Docker 引擎。这样 Docker 引擎收到这个上下文包后,展开就会获得构建镜像所需的一切文件。
一般来说,应该会将 Dockerfile
置于一个空目录下,或者项目根目录下。如果该目录下没有所需文件,那么应该把所需文件复制一份过来。如果目录下有些东西确实不希望构建时传给 Docker 引擎,那么可以用 .gitignore
一样的语法写一个 .dockerignore
,该文件是用于剔除不需要作为上下文传递给 Docker 引擎的。
那么为什么会有人误以为 .
是指定 Dockerfile
所在目录呢?这是因为在默认情况下,如果不额外指定 Dockerfile
的话,会将上下文目录下的名为 Dockerfile
的文件作为 Dockerfile。
这只是默认行为,实际上 Dockerfile
的文件名并不要求必须为 Dockerfile
,而且并不要求必须位于上下文目录中,比如可以用 -f ../Dockerfile.php
参数指定某个文件作为 Dockerfile
。
当然,一般大家习惯性的会使用默认的文件名 Dockerfile
,以及会将其置于镜像构建上下文目录中。
FROM 指定基础镜像
FROM
指定了一个基础镜像,我们构建的所有镜像,都需要建立在一个基础镜像之上,因此 FROM
必须是第一条指令。示例中我们的基础镜像就是 openjdk ,在制作的过程中,如果本地没有事先拉取基础镜像,在制作的过程中会自动拉取。
除了 openjdk
之外,Docker Hub 上还提供很多现有的镜像,可自行查看。
除了选择现有镜像为基础镜像外,Docker 还存在一个特殊的镜像,名为 scratch
。这个镜像是虚拟的概念,并不实际存在,它表示一个空白的镜像。
如果你以 scratch
为基础镜像的话,意味着你不以任何镜像为基础,接下来所写的指令将作为镜像第一层开始存在。不以任何系统为基础,直接将可执行文件复制进镜像的做法并不罕见,比如 swarm
、etcd
。对于 Linux 下静态编译的程序来说,并不需要有操作系统提供运行时支持,所需的一切库都已经在可执行文件里了,因此直接 FROM scratch
会让镜像体积更加小巧。使用 Go 语言 开发的应用很多会使用这种方式来制作镜像,这也是为什么有人认为 Go 是特别适合容器微服务架构的语言的原因之一。
RUN 执行命令
RUN
指令是用来执行命令行命令的。由于命令行的强大能力,RUN
指令在定制镜像时是最常用的指令之一。其格式有两种:
-
shell格式:
RUN <命令>
<命令> 等同于在终端操作的 shell 命令。
-
exec格式:
RUN ["可执行文件", "参数1", "参数2"]
例如:
RUN ["./test.php", "dev", "offline"]
等价于RUN ./test.php dev offline
注意:
Dockerfile 的指令每执行一次都会在 docker 上新建一层。所以过多无意义的层,会造成镜像膨胀过大。
例如:
FROM centos
RUN yum install wget
RUN wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz"
RUN tar -xvf redis.tar.gz
# 以上执行会创建 3 层镜像。可简化为以下格式:
FROM centos
RUN yum install wget \
&& wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz" \
&& tar -xvf redis.tar.gz
如上,以 && 符号连接命令,这样执行后,只会创建 1 层镜像。
COPY 复制文件
格式:
COPY [--chown=:] <源路径>... <目标路径>
COPY [--chown=:] ["<源路径1>",... "<目标路径>"]
和 RUN
指令一样,也有两种格式,一种类似于命令行,一种类似于函数调用。
COPY
指令将从构建上下文目录中 <源路径>
的文件/目录复制到新的一层的镜像内的 <目标路径>
位置。比如:
COPY package.json /usr/src/app/
<源路径>
可以是多个,甚至可以是通配符,其通配符规则要满足 Go 的 filepath.Match
规则,如:
COPY hom* /mydir/
COPY hom?.txt /mydir/
<目标路径>
可以是容器内的绝对路径,也可以是相对于工作目录的相对路径(工作目录可以用 WORKDIR
指令来指定)。目标路径不需要事先创建,如果目录不存在会在复制文件前先行创建缺失目录。
此外,还需要注意一点,使用 COPY
指令,源文件的各种元数据都会保留。比如读、写、执行权限、文件变更时间等。这个特性对于镜像定制很有用。特别是构建相关文件都在使用 Git 进行管理的时候。
在使用该指令的时候还可以加上 --chown=:
选项来改变文件的所属用户及所属组。
COPY --chown=55:mygroup files* /mydir/
COPY --chown=bin files* /mydir/
COPY --chown=1 files* /mydir/
COPY --chown=10:11 files* /mydir/
ADD 复制文件
ADD
指令和 COPY
的格式和性质基本一致。但是在 COPY
基础上增加了一些功能。
比如 ADD
的源文件可以是 URL
,这种情况下,Docker 引擎会试图去下载这个链接的文件放到 <目标路径>
去。下载后的文件权限自动设置为 600
,如果这并不是想要的权限,那么还需要增加额外的一层 RUN
进行权限调整,另外,如果下载的是个压缩包,需要解压缩,也一样还需要额外的一层 RUN
指令进行解压缩。所以不如直接使用 RUN
指令,然后使用 wget
或者 curl
工具下载,处理权限、解压缩、然后清理无用文件更合理。因此,这个功能其实并不实用,而且不推荐使用。
另外,如果 <源路径>
为一个 tar
压缩文件的话,压缩格式为 gzip
, bzip2
以及 xz
的情况下,ADD
指令将会自动解压缩这个压缩文件到 <目标路径>
去。
因此我们可以明确区分 ADD
和 COPY
的使用场景,即:仅复制文件使用 COPY
,需要解压使用 ADD
。
同时 ADD
也支持使用--chown=:
选项来改变文件的所属用户及所属组。
CMD 容器启动命令
CMD
指令的格式和 RUN
相似,也是两种格式:
shell
格式:CMD <命令>
exec
格式:CMD ["可执行文件", "参数1", "参数2"...]
- 参数列表格式:
CMD ["参数1", "参数2"...]
。在指定了ENTRYPOINT
指令后,用CMD
指定具体的参数。
在指令格式上,一般推荐使用 exec
格式,这类格式在解析时会被解析为 JSON 数组,因此一定要使用双引号 "
,而不要使用单引号。
二者运行的时间点不同:
- RUN 是在 docker build 时执行
- CMD 在 docker run 时运行
CMD
的作用是为启动的容器指定默认要运行的程序,程序运行结束,容器也就结束。CMD
指令指定的程序可被docker run 命令行参数中指定要运行的程序所覆盖。
注意:如果 Dockerfile 中如果存在多个 CMD
指令,仅最后一个生效。
ENTRYPOINT 入口点
ENTRYPOINT
的格式和 RUN
指令格式一样,分为 exec
格式和 shell
格式。
ENTRYPOINT
的目的和 CMD
一样,都是在指定容器启动程序及参数。
但是, 如果运行 docker run 时使用了 --entrypoint
选项,此选项的参数可当作要运行的程序覆盖 ENTRYPOINT
指令指定的程序。
优点:在执行 docker run 的时候可以指定 ENTRYPOINT
运行所需的参数。
注意:如果 Dockerfile 中如果存在多个 ENTRYPOINT
指令,仅最后一个生效。
可以搭配 CMD
命令使用:一般是变参才会使用 CMD
,这里的 CMD
等于是在给 ENTRYPOINT
传参,以下示例会提到。
示例:
假设已通过 Dockerfile 构建了 nginx:test 镜像:
FROM nginx
# # 定参
ENTRYPOINT ["nginx", "-c"]
# 变参
CMD ["/etc/nginx/nginx.conf"]
-
不传参运行
docker run nginx:test
容器内会默认运行以下命令,启动主进程:
nginx -c /etc/nginx/nginx.conf
-
传参运行
docker run nginx:test -c /etc/nginx/new.conf
容器内会默认运行以下命令,启动主进程( /etc/nginx/new.conf :假设容器内已有此文件):
nginx -c /etc/nginx/new.conf
ENV 环境变量
格式有两种:
ENV <key> <value>
ENV <key1>=<value1> <key2>=<value2>...
设置环境变量,定义了环境变量,那么在后续的指令中,就可以使用这个环境变量。
例如:
ENV NODE_VERSION 7.2.0
RUN curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.xz" \
&& curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc"
下面的例子演示了如何换行,以及对含有空格的值用双引号括起来的办法,这和 Shell 下的行为是一致的
ENV VERSION=1.0 DEBUG=on \
NAME="Happy Feet"
ARG 构建参数
格式:ARG <参数名>[=<默认值>]
构建参数和 ENV
的效果一样,都是设置环境变量。所不同的是,ARG
所设置的构建环境的环境变量,在将来容器运行时是不会存在这些环境变量的。但是不要因此就使用 ARG
保存密码之类的信息,因为 docker history
还是可以看到所有值的。
Dockerfile
中的 ARG
指令是定义参数名称,以及定义其默认值。该默认值可以在构建命令 docker build
中用 --build-arg <参数名>=<值>
来覆盖。
VOLUME 定义匿名卷
格式为:
VOLUME ["<路径1>", "<路径2>"...]
VOLUME <路径>
因为容器运行时产生的各种数据变化,仅在容器内部,一旦容器丢失数据也会随之丢失。对于数据库类需要保存动态数据的应用,其数据库文件应该保存于卷(volume)中。
为了防止运行时用户忘记将动态文件所保存目录挂载为卷,在 Dockerfile
中,我们可以事先指定某些目录挂载为匿名卷,这样在运行时如果用户不指定挂载,其应用也可以正常运行,不会向容器存储层写入大量数据。
VOLUME /data
这里的 /data
目录就会在运行时自动挂载为匿名卷,任何向 /data
中写入的信息都不会记录进容器存储层,从而保证了容器存储层的无状态化。当然,运行时可以覆盖这个挂载设置。比如:
docker run -d -v /myData:/data xxx
在这行命令中,就使用了 mydata
这个命名卷挂载到了 /data
这个位置,替代了 Dockerfile
中定义的匿名卷的挂载配置。
EXPOSE 声明端口
格式为 EXPOSE <端口1> [<端口2>...]
仅仅只是声明端口。
作用:
- 帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射。
- 在运行时使用随机端口映射时,也就是 docker run -p 时,会自动随机映射
EXPOSE
的端口。
要将 EXPOSE
和在运行时使用 -p <宿主端口>:<容器端口>
区分开来。-p
,是映射宿主端口和容器端口,换句话说,就是将容器的对应端口服务公开给外界访问,而 EXPOSE
仅仅是声明容器打算使用什么端口而已,并不会自动在宿主进行端口映射。
WORKDIR 工作目录
格式为 WORKDIR <工作目录路径>
。
使用 WORKDIR
指令可以来指定工作目录(或者称为当前目录),以后各层的当前目录就被改为指定的目录,如该目录不存在,WORKDIR
会帮你建立目录。
简而言之就是,构建镜像时,所有构建步骤都位于 WORKDIR
指定的这个目录进行。
USER 指定后续步骤用户
格式:USER <用户名>[:<用户组>]
USER
指令和 WORKDIR
相似,都是改变环境状态并影响以后的层。WORKDIR
是改变工作目录,USER
则是改变之后层的执行 RUN
, CMD
以及 ENTRYPOINT
这类命令的身份。
当然,和 WORKDIR
一样,USER
只是帮助你切换到指定用户而已,这个用户必须是事先建立好的,否则无法切换。
HEALTHCHECK 健康检查
格式:
HEALTHCHECK [选项] CMD <命令>
:设置检查容器健康状况的命令HEALTHCHECK NONE
:如果基础镜像有健康检查指令,使用这行可以屏蔽掉其健康检查指令
当在一个镜像指定了 HEALTHCHECK
指令后,用其启动容器,初始状态会为 starting
,在 HEALTHCHECK
指令检查成功后变为 healthy
,如果连续一定次数失败,则会变为 unhealthy
。
HEALTHCHECK
支持下列选项:
--interval=<间隔>
:两次健康检查的间隔,默认为 30 秒;--timeout=<时长>
:健康检查命令运行超时时间,如果超过这个时间,本次健康检查就被视为失败,默认 30 秒;--retries=<次数>
:当连续失败指定次数后,则将容器状态视为unhealthy
,默认 3 次。
和 CMD
, ENTRYPOINT
一样,HEALTHCHECK
只可以出现一次,如果写了多个,只有最后一个生效。
在 HEALTHCHECK [选项] CMD
后面的命令,格式和 ENTRYPOINT
一样,分为 shell
格式,和 exec
格式。命令的返回值决定了该次健康检查的成功与否:0
:成功;1
:失败;2
:保留,不要使用这个值。
假设我们有个镜像是个最简单的 Web 服务,我们希望增加健康检查来判断其 Web 服务是否在正常工作,我们可以用 curl
来帮助判断,其 Dockerfile
的 HEALTHCHECK
可以这么写:
FROM nginx
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
HEALTHCHECK --interval=5s --timeout=3s \
CMD curl -fs http://localhost/ || exit 1
ONBUILD 延迟构建
格式:ONBUILD <其它指令>
。
ONBUILD
是一个特殊的指令,它后面跟的是其它指令,比如 RUN
, COPY
等,而这些指令,在当前镜像构建时并不会被执行。只有当以当前镜像为基础镜像,去构建下一级镜像的时候才会被执行。
--end--