0%

Docker —— Docker 使用 Dockerfile 构建

如果有什么错误的地方,希望指出。

在之前学习 Spring Boot 项目的时候,使用过 Dockerfile 来构建生产 Docker 镜像 —— Spring Boot 在 Docker 上部署,在之前的文章中稍微介绍了两种构建 Docker 镜像的方法,一种是基于 Dockerfile,一种是基于 pom.xml 内的插件直接构建。同时在之前的博文中简单介绍了一点关于 Dockerfile 的内容,这篇文字主要是详细了解下关于 Dockerfile 中的一些命令。

使用 Dockerfile 构建镜像

镜像的定制实际上就是定制每一层所添加的配置、文件。如果我们可以把每一层修改、安装、构建、操作的命令都写入一个脚本,用这个脚本来构建、定制镜像,那么之前提及的无法重复的问题、镜像构建透明性的问题、体积的问题就都会解决。这个脚本就是 Dockerfile。

Dockerfile 是一个文本文件,其内包含了一条条的指令(Instruction),每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。

比如构架一个 nginx 镜像:

1
2
FROM nginx
RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html

FROM 指定基础镜像

所谓定制镜像,那一定是以一个镜像为基础,在其上进行定制。就像我们之前运行了一个 nginx 镜像的容器,再进行修改一样,基础镜像是必须指定的。而 FROM 就是指定基础镜像,因此一个 Dockerfile 中 FROM 是必备的指令,并且必须是第一条指令。

在 Docker Store 上有非常多的高质量的官方镜像,有可以直接拿来使用的服务类的镜像,如 nginx 、 redis 、 mongo 、 mysql 、 httpd 、 php 、 tomcat 等;也有一些方便开发、构建、运行各种语言应用的镜像,如 node 、 openjdk 、 python 、 ruby 、 golang 等。可以在其中寻找一个最符合我们最终目标的镜像为基础镜像进行定制。

RUN 执行命令

RUN 指令是用来执行命令行命令的。由于命令行的强大能力, RUN 指令在定制镜像时是最
常用的指令之一。其格式有两种。

shell 格式

RUN <命令> ,就像直接在命令行中输入的命令一样。刚才写的 Dockerfile 中的 RUN 指令就是这种格式。

exec 格式

RUN [“可执行文件”, “参数1”, “参数2”] ,这更像是函数调用中的格式。

镜像构建上下文(Context)

如果注意,会看到 docker build 命令最后有一个 .. 表示当前目录,而 Dockerfile 就在当前目录,因此不少初学者以为这个路径是在指定 Dockerfile 所在路径,这么理解其实是不准确的。如果对应上面的命令格式,你可能会发现,这是在指定上下文路径。

首先我们要理解 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 中这么写:

1
COPY ./package.json /app/ 

这并不是要复制执行 docker build 命令所在的目录下的 package.json ,也不是复制 Dockerfile 所在目录下的 package.json ,而是复制上下文(context) 目录下的 package.json 。

COPY 这类指令中的源文件的路径都是相对路径。这也是初学者经常会问的为什么 COPY ../package.json /app 或者 COPY /opt/xxxx /app 无法工作的原因,因为这些路径已经超出了上下文的范围,Docker 引擎无法获得这些位置的文件。如果真的需要那些文件,应该将它们复制到上下文目录中去。

其他 docker build 的用法

直接用 Git repo 构建,也可以直接给定的tar 压缩包构建。

1
2
3
docker build https://github.com/twang2218/gitlab-ce-zh.git#:8.14
//或者
docker build http://server/context.tar.gz

Dockerfile 指令详解

COPY 复制文件

格式:

  • COPY <原路径> … <目标路径>
  • COPY [“<源路径1>”,… “<目标路径>”]

和 RUN 指令一样,也有两种格式,一种类似于命令行,一种类似于函数调用。

ADD 更高级的复制文件

ADD 指令和 COPY 的格式和性质基本一致。但是在 COPY 基础上增加了一些功能。

在某些情况下,如果我们真的是希望复制个压缩文件进去,而不解压缩,这时就不可以使 用 ADD 命令了。

CMD 容器启动

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

  • shell 格式: CMD <命令>
  • exec 格式: CMD [“可执行文件”, “参数1”, “参数2”…]
  • 参数列表格式: CMD [“参数1”, “参数2”…] 。在指定了 ENTRYPOINT 指令后,用 CMD 指 定具体的参数。

ENTRYPOINT 入口点

ENTRYPOINT 的格式和 RUN 指令格式一样,分为 exec 格式和 shell 格式。

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

ENV 设置环境变量

格式有两种:

  • ENV <key> <value>
  • ENV <key1>=<value1> <key2>=<value2>...

ARG

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

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

VOLUME 定义匿名卷

格式为:

  • VOLUME [“<路径1>”, “<路径2>”…]
  • VOLUME <路径>

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

1
VOLUME /data 

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

1
docker run -d -v mydata:/data xxxx 在

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

EXPOSE 声明端口

格式为 EXPOSE <端口1> [<端口2>…] 。

EXPOSE 指令是声明运行时容器提供服务端口,这只是一个声明,在运行时并不会因为这个声 明应用就会开启这个端口的服务。在 Dockerfile 中写入这样的声明有两个好处,一个是帮助 镜像使用者理解这个镜像服务的守护端口,以方便配置映射;另一个用处则是在运行时使用 随机端口映射时,也就是 docker run -P 时,会自动随机映射 EXPOSE 的端口。

此外,在早期 Docker 版本中还有一个特殊的用处。以前所有容器都运行于默认桥接网络中, 因此所有容器互相之间都可以直接访问,这样存在一定的安全性问题。于是有了一个 Docker 引擎参数 –icc=false ,当指定该参数后,容器间将默认无法互访,除非互相间使用了 – links 参数的容器才可以互通,并且只有镜像中 EXPOSE 所声明的端口才可以被访问。这个 –icc=false 的用法,在引入了 docker network 后已经基本不用了,通过自定义网络可以很 轻松的实现容器间的互联与隔离。

要将 EXPOSE 和在运行时使用 -p <宿主端口>:<容器端口> 区分开来。 -p ,是映射宿主端口和 容器端口,换句话说,就是将容器的对应端口服务公开给外界访问,而 EXPOSE 仅仅是声明 容器打算使用什么端口而已,并不会自动在宿主进行端口映射。

WORKDIR 指定工作目录

格式为 WORKDIR <工作目录路径> 。

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

之前提到一些初学者常犯的错误是把 Dockerfile 等同于 Shell 脚本来书写,这种错误的理解 还可能会导致出现下面这样的错误:

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

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

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

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

Reference

详细的可以查看下面这篇,本片主要是阅读下面这本书籍的记录。

客官,赏一杯coffee嘛~~~~