theboyaply
theboyaply
发布于 2020-03-09 / 630 阅读
0
0

Dockerfile

参考:

https://vuepress.mirror.docker-practice.com/image/dockerfile/

https://www.runoob.com/docker/docker-dockerfile.html

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

docker-build-demo

上面我们已经成功的构建了一个名为 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 为基础镜像的话,意味着你不以任何镜像为基础,接下来所写的指令将作为镜像第一层开始存在。不以任何系统为基础,直接将可执行文件复制进镜像的做法并不罕见,比如 swarmetcd。对于 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 指令将会自动解压缩这个压缩文件到 <目标路径> 去。

因此我们可以明确区分 ADDCOPY 的使用场景,即:仅复制文件使用 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"]
  1. 不传参运行

    docker run  nginx:test
    

    容器内会默认运行以下命令,启动主进程:

    nginx -c /etc/nginx/nginx.conf
    
  2. 传参运行

    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 来帮助判断,其 DockerfileHEALTHCHECK 可以这么写:

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--


评论