Docker进阶篇之利用dockerfile构建属于自己的镜像,此篇需要Docker基础,可以查看Docker入门篇

What

Dockerfile是一个文本文档,其中包含用户可以在命令行上调用以组合图像的所有命令。使用docker build 用户可以创建自动执行的构建,该构建可以连续执行多个命令行指令。
摘自官方文档
简单来说,Dockerfile是一个文本文件,也可以理解为配置文件,自己写命令,然后以build命令运行脚本,执行所有命令已达到构建自己镜像的目的。

How

基本规范

  • 所有Dockerfile必须以FROM指令开头,以指定基础镜像,如果不需要基础镜像可以指定镜像为scratchscratch镜像是虚拟的概念,并不实际存在,它表示一个空白的镜像。
  • 所有指令不区分大小写,但是约定必须大写,以便将指令与参数区分开来。
  • 约定每两个不同指令间以空行隔开。
  • 一个指令只能写在一行内,不允许换行。

指令详解

FROM指定基础镜像

简介:用于指定本Dockerfile的基础镜像。
FROM nginx 表示指定基础镜像为nginx

RUN运行命令

简介:用于在容器中执行命令,由于命令行是很强大的,所有这个也是最常用的命令
RUN echo 'Hello Docker!' 表示在容器中输出Hello Docker!

COPY复制文件

格式: COPY <源路径>… <目标路径>
简介:用于复制构建上下文路径的文件到容器中的目标路径,源路径可以是多个,甚至可以是通配符,其通配符规则要满足 Go 的 filepath.Match 规则
COPY package.json /usr/src/app/ 复制上下文路径下的package.json 到 容器中/usr/src/app/路径下。
备注: 这个指令涉及到上下文概念,详情查看 上下文

ADD 更高级的复制文件

简介:ADD 指令和 COPY 的格式和性质基本一致。但是在 COPY 基础上增加了一些功能。
使用格式与COPY 相同,ADD命令源路径可以是URL Docker引擎会自动下载文件并将文件权限设置为600,如果需要修改权限还需要使用RUN 命令修改权限;
如果源文件是一个压缩包,ADD命令还会自动解压缩,如果不需要解压缩,则不可以使用ADD命令。所以这个命令并不合理,如果需要下载或者解压缩,使用RUN 命令操作更加方便。
官方也不推荐使用此命令。

CMD 容器启动命令

简介:CMD 指令的格式和 RUN 相似,也是两种格式:

  • shell 格式: CMD <命令>
  • exec 格式:CMD [“可执行文件”, “参数1”, “参数2”…]
  • 参数列表格式:CMD [“参数1”, “参数2”…]。在指定了 ENTRYPOINT 指令后,用 CMD 指定具体的参数。
    注意:Docker是进程,而不是容器,CMD也就是指定容器在启动进程时执行的命令(覆盖原命令)。
    在指令格式上,一般推荐使用 exec 格式,这类格式在解析时会被解析为 JSON 数组,因此一定要使用双引号 ",而不要使用单引号。
    如果使用 shell 格式的话,实际的命令会被包装为 sh -c 的参数的形式进行执行。比如:CMD echo $HOME 在实际执行时会转换为CMD [ "sh", "-c", "echo $HOME" ]这就是为什么我们可以使用环境变量的原因,因为这些环境变量会被 shell 进行解析处理。

ENTRYPOINT 入口点

简介:ENTRYPOINT 的格式和 RUN 指令格式一样。
一般有两种使用情景

为CMD添加额外可变参数:

1
ENTRYPOINT ["/bin/echo", "Hello"] # 当容器通过 docker run -it [image] 启动时,输出为:Hello ; 而如果通过 docker run -it [image] Ttt 启动,则输出为:Hello Ttt

将Dockerfile修改为:

1
2
ENTRYPOINT ["/bin/echo", "Hello"]  
CMD ["world"]

当容器通过 docker run -it [image] 启动时,输出为:Hello world
而如果通过 docker run -it [image] Ttt 启动,输出依旧为:Hello Ttt

ENTRYPOINT 中的参数始终会被使用,而 CMD 的额外参数可以在容器启动时动态替换掉。

应用运行前的准备工作

启动容器就是启动主进程,但有些时候,启动主进程前,需要一些准备工作。
比如 mysql 类的数据库,可能需要一些数据库配置、初始化的工作,这些工作要在最终的 mysql 服务器运行之前解决。
此外,可能希望避免使用 root 用户去启动服务,从而提高安全性,而在启动服务前还需要以 root 身份执行一些必要的准备工作,最后切换到服务用户身份启动服务。或者除了服务外,其它命令依旧可以使用 root 身份执行,方便调试等。
这些准备工作是和容器 CMD 无关的,无论 CMD 为什么,都需要事先进行一个预处理的工作。这种情况下,可以写一个脚本,然后放入 ENTRYPOINT 中去执行,而这个脚本会将接到的参数(也就是 )作为命令,在脚本最后执行。比如官方镜像 redis 中就是这么做的:

1
2
3
4
5
6
7
8
FROM alpine:3.4
...
RUN addgroup -S redis && adduser -S -G redis redis
...
ENTRYPOINT ["docker-entrypoint.sh"]

EXPOSE 6379
CMD [ "redis-server" ]

ENV 设置环境变量

简介:这个指令很简单,就是设置环境变量而已,无论是后面的其它指令,如 RUN,还是运行时的应用,都可以直接使用这里定义的环境变量。
格式:ENV <key> <value> 或者 ENV <key1>=<value1> <key2>=<value2>...
使用格式: $key

ARG 构建参数

格式:ARG <参数名>[=<默认值>]
简介:构建参数和 ENV 的效果一样,都是设置环境变量。所不同的是,ARG 所设置的构建环境的环境变量,在将来容器运行时是不会存在这些环境变量的。

VOLUME 定义匿名卷

格式为:VOLUME ["<路径1>", "<路径2>"...] 或者 VOLUME <路径>
简介:定义成匿名卷后,数据会存储在宿主机上,多个容器可以共用,且不会在容器存储层写入大量数据。
备注:容器启动时可以使用docker run -d -v mydata:/data xxxx 覆盖原配置

EXPOSE 暴露端口

格式为 EXPOSE <端口1> [<端口2>...]
简介:EXPOSE 指令是声明运行时容器提供服务端口,注意,这只是声明,并不会暴露端口,有两个作用(帮助使用者理解容器守护端口、-P 命令是随机映射端口)

WORKDIR 指定工作目录

简介:使用 WORKDIR 指令可以来指定工作目录(或者称为当前目录),以后各层的当前目录就被改为指定的目录,如该目录不存在,WORKDIR 会帮你建立目录。
格式:WORKDIR <工作目录路径>
备注:

1
2
RUN cd /app
RUN echo "hello" > world.txt

如果在dockerfile中这样写的话。第二行的工作目录依旧是默认目录,而不是上一个命令执行的/app目录,因为dockerfile是每一个指令由一个进程执行,两行命令的执行环境都是不同的。

USER 指定当前用户

格式: USER 用户名
简介:USER 指令和 WORKDIR 相似,都是改变环境状态并影响以后的层,以后的所有指令都是在这个用户下操作。
当然,和 WORKDIR 一样,USER 只是帮助你切换到指定用户而已,这个用户必须是事先建立好的,否则无法切换。

1
2
3
RUN groupadd -r redis && useradd -r -g redis redis
USER redis
RUN [ "redis-server" ]

备注:如果以 root 执行的脚本,在执行期间希望改变身份,比如希望以某个已经建立好的用户来运行某个服务进程,不要使用 su 或者 sudo,这些都需要比较麻烦的配置,而且在 TTY 缺失的环境下经常出错。建议使用 gosu。

1
2
3
4
5
6
7
8
# 建立 redis 用户,并使用 gosu 换另一个用户执行命令
RUN groupadd -r redis && useradd -r -g redis redis
# 下载 gosu
RUN wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/1.7/gosu-amd64" \
&& chmod +x /usr/local/bin/gosu \
&& gosu nobody true
# 设置 CMD,并以另外的用户执行
CMD [ "exec", "gosu", "redis", "redis-server" ]

HEALTHCHECK 健康检查

格式:

  • HEALTHCHECK [选项] CMD <命令>:设置检查容器健康状况的命令
  • HEALTHCHECK NONE 如果基础镜像有健康检查指令,使用这行可以屏蔽掉其健康检查指令
    简介: 防止容器进入死锁状态或者死循环状态后,容器仍在运行的问题。
    HEALTHCHECK 支持下列选项:
  • --interval=<间隔>:两次健康检查的间隔,默认为 30 秒;
  • --timeout=<时长>:健康检查命令运行超时时间,如果超过这个时间,本次健康检查就被视为失败,默认 30 秒;
  • --retries=<次数>:当连续失败指定次数后,则将容器状态视为 unhealthy,默认 3 次。
    注意:和 CMD, ENTRYPOINT 一样,HEALTHCHECK 只可以出现一次,如果写了多个,只有最后一个生效。

ONBUILD 作为基础镜像构建时

格式: ONBUILD <其它指令>
简介: 当镜像被作为基础镜像使用时,执行相关指令。
ONBUILD RUN [ "npm", "install" ] 表示作为基础镜像时执行npm install 命令

上下文

在docker build 命令最后有一个.这个.也就是当前目录,也就是在指定上下文目录。什么是上下文呢?
这个要设计到docker 的工作原理,docker分为服务端和客户端,执行命令的就是客户端,客户端会根据命令发送相应的请求到服务端,由服务端去执行。所以docker build 其实是在服务端进行构建的,那docker又是怎么访问到我们的文件的呢,很简单,docker客户端会将指定的上下文路径直接打包发送给服务端。而上下文路径,也就是打包的文件夹路径

注意:如果在Dockerfile中使用COPY ../demo.txt 这个指令是不能执行成功的,因为…是上级目录,已经超出上下文目录了。相同的,使用COPY /demo.txt也是不行的。

Why

我们为什么要使用Dockerfile?
比如我现在的java项目要部署到docker上,那么我现在需要一个Dockerfile来构建我的镜像。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
FROM anapsix/alpine-java:8_server-jre_unlimited

MAINTAINER **@163.com

RUN mkdir -p /app

WORKDIR /app

EXPOSE 8080

ADD ./target/app.jar ./app.jar

ENTRYPOINT ["java", "-jar", "app.jar"]

CMD ["--spring.profiles.active=test"]

现在只需要build之后,将镜像push到docker私服上,然后再任何地方运行docker run imagesname 就可以启动我的项目了
或者不push,在本地build之后也可以直接运行了。

参考文档