Docker高级——DockerFile详解

DockerFile

Dockerfile是用来构建Docker镜像的文本文件,是由一条条构建镜像所需的指令和参数构成的脚本

Docker高级——DockerFile详解_第1张图片

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

构建三步骤

  • 编写Dockerfile文件
  • docker build命令构建镜像
  • docker run依镜像运行容器实例

Dockerfile内容基础知识

1:每条保留字指令都必须为大写字母且后面要跟随至少一个参数

2:指令按照从上到下,顺序执行

3:#表示注释

4:每条指令都会创建一个新的镜像层并对镜像进行提交

Docker执行Dockerfile的大致流程

(1)docker从基础镜像运行一个容器

(2)执行一条指令并对容器作出修改

(3)执行类似docker commit的操作提交一个新的镜像层

(4)docker再基于刚提交的镜像运行一个新容器

(5)执行dockerfile中的下一条指令直到所有指令都执行完成

Dockerfile基础命令

Docker高级——DockerFile详解_第2张图片

FROM 指定基础镜像

必须是第一条指令,从Docker Hub上拉取基础镜像,如 nginxredismongomysqlhttpdphptomcat 等;也有一些方便开发、构建、运行各种语言应用的镜像,如 nodeopenjdkpythonrubygolang

FROM 
或者
FROM :

MAINTAINER

# 指定维护者信息
MAINTAINER 

RUN 运行命令

RUN 
或者
RUN ["executable", "param1", "param2"]

前者将在 shell 终端中运行命令,即 /bin/sh -c;后者则使用 exec 执行。指定使用其它终端可以通过第二种方式实现,例如 RUN ["/bin/bash", "-c", "echo hello"]

每条 RUN 指令将在当前镜像基础上执行指定命令,并提交为新的镜像。当命令较长时可以使用 \ 来换行

CMD 命令行

支持三种格式

  • CMD ["executable","param1","param2"] 使用 exec 执行,推荐方式;
  • CMD command param1 param2/bin/sh 中执行,提供给需要交互的应用;
  • CMD ["param1","param2"] 提供给 ENTRYPOINT 的默认参数;

Docker 不是虚拟机,容器就是进程。既然是进程,那么在启动容器的时候,需要指定所运行的程序及参数。CMD 指令就是用于指定默认的容器主进程的启动命令的。

在运行时可以指定新的命令来替代镜像设置中的这个默认命令,比如,ubuntu 镜像默认的 CMD/bin/bash

如果我们直接 docker run -it ubuntu 的话,会直接进入 bash

docker run -it ubuntu

也可以在运行时指定运行别的命令

docker run -it ubuntu cat /etc/os-release

命令替换了默认的/bin/bash

cat /etc/os-release

输出了系统版本信息

在指令格式上,一般推荐使用 exec 格式,这类格式在解析时会被解析为 JSON 数组,因此一定要使用双引号 ",而不要使用单引号。

如果使用 shell 格式的话,实际的命令会被包装为 sh -c 的参数的形式进行执行。比如:

CMD echo $HOME

在实际执行中,会将其变更为

CMD ["sh","-c","echo","$HOME"]

这就是为什么我们可以使用环境变量的原因,因为这些环境变量会被 shell 进行解析处理。

提到 CMD 就不得不提容器中应用在前台执行和后台执行的问题。这是初学者常出现的一个混淆。

Docker 不是虚拟机,容器中的应用都应该以前台执行,而不是像虚拟机、物理机里面那样,用 systemd 去启动后台服务,容器内没有后台服务的概念。

注意

执行下面的语句,然后发现容器执行后就立即退出了。甚至在容器内去使用 systemctl 命令结果却发现根本执行不了。这就是因为没有搞明白前台、后台的概念,没有区分容器和虚拟机的差异,依旧在以传统虚拟机的角度去理解容器。

CMD service nginx start

对于容器而言,其启动程序就是容器应用进程,容器就是为了主进程而存在的,主进程退出,容器就失去了存在的意义,从而退出,其它辅助进程不是它需要关心的东西。

正确的做法是直接执行 nginx 可执行文件,并且要求以前台形式运行。比如:

CMD ["nginx", "-g", "daemon off;"]

ENTRYPOINT 入口点

ENTRYPOINT 的目的和 CMD 一样,都是在指定容器启动程序及参数。ENTRYPOINT 在运行时也可以替代,不过比 CMD 要略显繁琐,需要通过 docker run 的参数 --entrypoint 来指定。

当指定了 ENTRYPOINT 后,CMD 的含义就发生了改变,不再是直接的运行其命令,而是将 CMD 的内容作为参数传给 ENTRYPOINT 指令,换句话说实际执行时,将变为:

 ""

ENV 设置环境变量

ENV  

或者

ENV = =...

这个指令很简单,就是设置环境变量而已,无论是后面的其它指令,如 RUN,还是运行时的应用,都可以直接使用这里定义的环境变量。

定义了环境变量,那么在后续的指令中,就可以使用这个环境变量。比如在官方 node 镜像 Dockerfile 中,就有类似这样的代码:

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" \
  && gpg --batch --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc \
  && grep " node-v$NODE_VERSION-linux-x64.tar.xz\$" SHASUMS256.txt | sha256sum -c - \
  && tar -xJf "node-v$NODE_VERSION-linux-x64.tar.xz" -C /usr/local --strip-components=1 \
  && rm "node-v$NODE_VERSION-linux-x64.tar.xz" SHASUMS256.txt.asc SHASUMS256.txt \
  && ln -s /usr/local/bin/node /usr/local/bin/nodejs

在这里先定义了环境变量 NODE_VERSION,其后的 RUN 这层里,多次使用 $NODE_VERSION 来进行操作定制。可以看到,将来升级镜像构建版本的时候,只需要更新 7.2.0 即可,Dockerfile 构建维护变得更轻松了。

ARG 构建参数

ARG <参数名>[=<默认值>]

构建参数和 ENV 的效果一样,都是设置环境变量。所不同的是,ARG 所设置的构建环境的环境变量,在将来容器运行时是不会存在这些环境变量的。但是不要因此就使用 ARG 保存密码之类的信息,因为 docker history 还是可以看到所有值的。

Dockerfile 中的 ARG 指令是定义参数名称,以及定义其默认值。该默认值可以在构建命令 docker build 中用 --build-arg <参数名>=<值> 来覆盖。

灵活的使用 ARG 指令,能够在不修改 Dockerfile 的情况下,构建出不同的镜像。

ARG 指令有生效范围,如果在 FROM 指令之前指定,那么只能用于 FROM 指令中。

ARG DOCKER_USERNAME=library

FROM ${DOCKER_USERNAME}/alpine

RUN set -x ; echo ${DOCKER_USERNAME}

使用上述 Dockerfile 会发现无法输出 ${DOCKER_USERNAME} 变量的值,要想正常输出,你必须在 FROM 之后再次指定 ARG

# 只在 FROM 中生效
ARG DOCKER_USERNAME=library

FROM ${DOCKER_USERNAME}/alpine

# 要想在 FROM 之后使用,必须再次指定
ARG DOCKER_USERNAME=library

RUN set -x ; echo ${DOCKER_USERNAME}

对于多阶段构建

# 这个变量在每个 FROM 中都生效
ARG DOCKER_USERNAME=library

FROM ${DOCKER_USERNAME}/alpine

RUN set -x ; echo 1

FROM ${DOCKER_USERNAME}/alpine

RUN set -x ; echo 2

对于上述 Dockerfile 两个 FROM 指令都可以使用 ${DOCKER_USERNAME},对于在各个阶段中使用的变量都必须在每个阶段分别指定:

ARG DOCKER_USERNAME=library

FROM ${DOCKER_USERNAME}/alpine

# 在FROM 之后使用变量,必须在每个阶段分别指定
ARG DOCKER_USERNAME=library

RUN set -x ; echo ${DOCKER_USERNAME}

FROM ${DOCKER_USERNAME}/alpine

# 在FROM 之后使用变量,必须在每个阶段分别指定
ARG DOCKER_USERNAME=library

RUN set -x ; echo ${DOCKER_USERNAME}

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 更高级的复制文件

比如 <源路径> 可以是一个 URL,这种情况下,Docker 引擎会试图去下载这个链接的文件放到 <目标路径> 去。下载后的文件权限自动设置为 600,如果这并不是想要的权限,那么还需要增加额外的一层 RUN 进行权限调整,另外,如果下载的是个压缩包,需要解压缩,也一样还需要额外的一层 RUN 指令进行解压缩。所以不如直接使用 RUN 指令,然后使用 wget 或者 curl 工具下载,处理权限、解压缩、然后清理无用文件更合理。因此,这个功能其实并不实用,而且不推荐使用。

如果 <源路径> 为一个 tar 压缩文件的话,压缩格式为 gzip, bzip2 以及 xz 的情况下,ADD 指令将会自动解压缩这个压缩文件到 <目标路径> 去。

VLOUME 定义数据卷

VOLUME ["<路径1>", "<路径2>"...]

或者

VOLUME <路径>

对于数据库类需要保存动态数据的应用,其数据库文件应该保存于卷(volume)中,后面的章节我们会进一步介绍 Docker 卷的概念。为了防止运行时用户忘记将动态文件所保存目录挂载为卷,在 Dockerfile 中,我们可以事先指定某些目录挂载为匿名卷,这样在运行时如果用户不指定挂载,其应用也可以正常运行,不会向容器存储层写入大量数据。

VOLUME /data

这里的 /data 目录就会在容器运行时自动挂载为匿名卷,任何向 /data 中写入的信息都不会记录进容器存储层,从而保证了容器存储层的无状态化。当然,运行容器时可以覆盖这个挂载设置。比如:

$ docker run -d -v mydata:/data xxxx

在这行命令中,就使用了 mydata 这个命名卷挂载到了 /data 这个位置,替代了 Dockerfile 中定义的匿名卷的挂载配置。

EXPOSE 暴露端口

EXPOSE <端口1> [<端口2>...]

WORKDIR 指定工作目录

WORKDIR <工作目录路径>

使用 WORKDIR 指令可以来指定工作目录(或者称为当前目录),以后各层的当前目录就被改为指定的目录,如该目录不存在,WORKDIR 会帮你建立目录

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

如果将这个 Dockerfile 进行构建镜像运行后,会发现找不到 /app/world.txt 文件,或者其内容不是 hello。原因其实很简单,在 Shell 中,连续两行是同一个进程执行环境,因此前一个命令修改的内存状态,会直接影响后一个命令;而在 Dockerfile 中,这两行 RUN 命令的执行环境根本不同,是两个完全不同的容器。这就是对 Dockerfile 构建分层存储的概念不了解所导致的错误。

之前说过每一个 RUN 都是启动一个容器、执行命令、然后提交存储层文件变更。第一层 RUN cd /app 的执行仅仅是当前进程的工作目录变更,一个内存上的变化而已,其结果不会造成任何文件变更。而到第二层的时候,启动的是一个全新的容器,跟第一层的容器更完全没关系,自然不可能继承前一层构建过程中的内存变化。

因此如果需要改变以后各层的工作目录的位置,那么应该使用 WORKDIR 指令。

WORKDIR /app

RUN echo "hello" > world.txt

如果你的 WORKDIR 指令使用的相对路径,那么所切换的路径与之前的 WORKDIR 有关:

WORKDIR /a
WORKDIR b
WORKDIR c

RUN pwd

RUN pwd的工作目录为/a/b/c

USER 指定当前用户

USER <用户名>[:<用户组>]

USER 指令和 WORKDIR 相似,都是改变环境状态并影响以后的层。WORKDIR 是改变工作目录,USER 则是改变之后层的执行 RUN, CMD 以及 ENTRYPOINT 这类命令的身份。

注意,USER 只是帮助你切换到指定用户而已,这个用户必须是事先建立好的,否则无法切换。

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

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

# 建立 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.12/gosu-amd64" \
    && chmod +x /usr/local/bin/gosu \
    && gosu nobody true
# 设置 CMD,并以另外的用户执行
CMD [ "exec", "gosu", "redis", "redis-server" ]

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

这里我们设置了每 5 秒检查一次(这里为了试验所以间隔非常短,实际应该相对较长),如果健康检查命令超过 3 秒没响应就视为失败,并且使用 curl -fs http://localhost/ || exit 1 作为健康检查命令。

使用 docker build 来构建这个镜像:

$ docker build -t myweb:v1 .

构建好了后,我们启动一个容器

$ docker run -d --name web -p 80:80 myweb:v1

当运行该镜像后,可以通过 docker container ls 看到最初的状态为 (health: starting)

$ docker container ls
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                            PORTS               NAMES
03e28eb00bd0        myweb:v1            "nginx -g 'daemon off"   3 seconds ago       Up 2 seconds (health: starting)   80/tcp, 443/tcp     web

在等待几秒钟后,再次 docker container ls,就会看到健康状态变化为了 (healthy)

$ docker container ls
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                    PORTS               NAMES
03e28eb00bd0        myweb:v1            "nginx -g 'daemon off"   18 seconds ago      Up 16 seconds (healthy)   80/tcp, 443/tcp     web

如果健康检查连续失败超过了重试次数,状态就会变为 (unhealthy)

为了帮助排障,健康检查命令的输出(包括 stdout 以及 stderr)都会被存储于健康状态里,可以用 docker inspect 来查看。

$ docker inspect --format '{{json .State.Health}}' web | python -m json.tool
{
    "FailingStreak": 0,
    "Log": [
        {
            "End": "2016-11-25T14:35:37.940957051Z",
            "ExitCode": 0,
            "Output": "\n\n\nWelcome to nginx!\n\n\n\n

Welcome to nginx!

\n

If you see this page, the nginx web server is successfully installed and\nworking. Further configuration is required.

\n\n

For online documentation and support please refer to\n\"http://nginx.org/\">nginx.org.
\nCommercial support is available at\n\"http://nginx.com/\">nginx.com.

\n\n

Thank you for using nginx.

\n\n\n"
, "Start": "2016-11-25T14:35:37.780192565Z" } ], "Status": "healthy" }

ONBUILD 其他指令

格式:ONBUILD <其它指令>

ONBUILD 是一个特殊的指令,它后面跟的是其它指令,比如 RUN, COPY 等,而这些指令,在当前镜像构建时并不会被执行。只有当以当前镜像为基础镜像,去构建下一级镜像的时候才会被执行。

Dockerfile 中的其它指令都是为了定制当前镜像而准备的,唯有 ONBUILD 是为了帮助别人定制自己而准备的。

假设我们要制作 Node.js 所写的应用的镜像。我们都知道 Node.js 使用 npm 进行包管理,所有依赖、配置、启动信息等会放到 package.json 文件里。在拿到程序代码后,需要先进行 npm install 才可以获得所有需要的依赖。然后就可以通过 npm start 来启动应用。因此,一般来说会这样写 Dockerfile

FROM node:slim
RUN mkdir /app
WORKDIR /app
COPY ./package.json /app
RUN [ "npm", "install" ]
COPY . /app/
CMD [ "npm", "start" ]

把这个 Dockerfile 放到 Node.js 项目的根目录,构建好镜像后,就可以直接拿来启动容器运行。但是如果我们还有第二个 Node.js 项目也差不多呢?好吧,那就再把这个 Dockerfile 复制到第二个项目里。那如果有第三个项目呢?再复制么?文件的副本越多,版本控制就越困难,让我们继续看这样的场景维护的问题。

如果第一个 Node.js 项目在开发过程中,发现这个 Dockerfile 里存在问题,比如敲错字了、或者需要安装额外的包,然后开发人员修复了这个 Dockerfile,再次构建,问题解决。第一个项目没问题了,但是第二个项目呢?虽然最初 Dockerfile 是复制、粘贴自第一个项目的,但是并不会因为第一个项目修复了他们的 Dockerfile,而第二个项目的 Dockerfile 就会被自动修复。

那么我们可不可以做一个基础镜像,然后各个项目使用这个基础镜像呢?这样基础镜像更新,各个项目不用同步 Dockerfile 的变化,重新构建后就继承了基础镜像的更新?好吧,可以,让我们看看这样的结果。那么上面的这个 Dockerfile 就会变为:

FROM node:slim
RUN mkdir /app
WORKDIR /app
CMD [ "npm", "start" ]

这里我们把项目相关的构建指令拿出来,放到子项目里去。假设这个基础镜像的名字为 my-node 的话,各个项目内的自己的 Dockerfile 就变为:

FROM my-node
COPY ./package.json /app
RUN [ "npm", "install" ]
COPY . /app/

基础镜像变化后,各个项目都用这个 Dockerfile 重新构建镜像,会继承基础镜像的更新。

那么,问题解决了么?没有。准确说,只解决了一半。如果这个 Dockerfile 里面有些东西需要调整呢?比如 npm install 都需要加一些参数,那怎么办?这一行 RUN 是不可能放入基础镜像的,因为涉及到了当前项目的 ./package.json,难道又要一个个修改么?所以说,这样制作基础镜像,只解决了原来的 Dockerfile 的前4条指令的变化问题,而后面三条指令的变化则完全没办法处理。

ONBUILD 可以解决这个问题。让我们用 ONBUILD 重新写一下基础镜像的 Dockerfile:

FROM node:slim
RUN mkdir /app
WORKDIR /app
ONBUILD COPY ./package.json /app
ONBUILD RUN [ "npm", "install" ]
ONBUILD COPY . /app/
CMD [ "npm", "start" ]

这次我们回到原始的 Dockerfile,但是这次将项目相关的指令加上 ONBUILD,这样在构建基础镜像的时候,这三行并不会被执行。然后各个项目的 Dockerfile 就变成了简单地:

FROM my-node

是的,只有这么一行。当在各个项目目录中,用这个只有一行的 Dockerfile 构建镜像时,之前基础镜像的那三行 ONBUILD 就会开始执行,成功的将当前项目的代码复制进镜像、并且针对本项目执行 npm install,生成应用镜像。

LABEL 为镜像添加元数据

LABEL 指令用来给镜像以键值对的形式添加一些元数据(metadata)

LABEL = = = ...

我们还可以用一些标签来申明镜像的作者、文档地址等:

LABEL org.opencontainers.image.authors="yeasy"

LABEL org.opencontainers.image.documentation="https://yeasy.gitbooks.io"

SHELL 指令

格式:SHELL ["executable", "parameters"]

SHELL指令可以指定RUN ENTRYPOINT CMD指令的 shell,Linux 中默认为[“/bin/sh”, “-c”]

SHELL ["/bin/sh", "-c"]

RUN lll ; ls

SHELL ["/bin/sh", "-cex"]

RUN lll ; ls

两个 RUN 运行同一命令,第二个 RUN 运行的命令会打印出每条命令并当遇到错误时退出。

ENTRYPOINT CMD 以 shell 格式指定时,SHELL 指令所指定的 shell 也会成为这两个指令的 shell

SHELL ["/bin/sh", "-cex"]

# /bin/sh -cex "nginx"
ENTRYPOINT nginx

SHELL ["/bin/sh", "-cex"]

# /bin/sh -cex "nginx"
CMD nginx

打包一个boot项目

Docker高级——DockerFile详解_第3张图片

@RestController
public class HelloController {

    @GetMapping("/hello")
    public String hello(){
        return "hello world";
    }

}
server.port=8888

打包放到服务器上

Docker高级——DockerFile详解_第4张图片

编写dockerfile

FROM openwhisk/java8action
MAINTAINER kangxiaozhaung <[email protected]>
ADD demo.jar app.jar
CMD java -jar app.jar

执行打包镜像

docker build -f ./demo-dockerfile -t app:0.0.1 .

运行打包的容器

[root@test opt]# docker run -id -p 8888:8888 app:0.0.1
23b7595758f66f3e11b767f93e4692a4869bd56f88745b1a5975a226c13ecddf
[root@test opt]# docker ps
CONTAINER ID   IMAGE       COMMAND                  CREATED         STATUS         PORTS                                       NAMES
23b7595758f6   app:0.0.1   "/bin/sh -c 'java -j…"   9 seconds ago   Up 8 seconds   0.0.0.0:8888->8888/tcp, :::8888->8888/tcp   pedantic_curran

访问本地8888端口

[root@test opt]# curl http://127.0.0.1:8888/hello
hello world

参考文档

Dockerfile 官方文档:https://docs.docker.com/engine/reference/builder/

Dockerfile 最佳实践文档:https://docs.docker.com/develop/develop-images/dockerfile_best-practices/

Docker 官方镜像 Dockerfile:https://github.com/docker-library/docs

你可能感兴趣的:(Docker基础到高级,docker,容器,运维,java,spring,boot,tomcat)