# Dockerfile 定制镜像

Dockerfile 是一个用来构建镜像的文本文件,文本内容包含了一条条构建镜像所需的指令和说明。

每⼀条指令构建⼀层,因此每⼀条指令的内容,就是描述该层应当如何构建。

Dockerfile

# Dockerfile 优点

相对于提交容器修改,再进行镜像迁移的方式,使用 Dockerfile 存在很多优势。

  • Dockerfile 的体积远小于镜像包,更容易进行快速迁移和部署。
  • 环境构建流程记录了 Dockerfile 中,能够直观的看到镜像构建的顺序和逻辑。
  • 使用 Dockerfile 来构建镜像能够更轻松的实现自动部署等自动化流程。
  • 在修改环境搭建细节时,修改 Dockerfile 文件要比从新提交镜像来的轻松、简单。

# Dockerfile 基本格式

Dockerfile 的内容很简单,主要以两种形式呈现,一种是注释行,另一种是指令行。

一般情况下,以 # 开头的内容是 注释,其他内容以 指令 开头,后面跟着指令所使用的参数。

# 注释
INSTRUCTION arguments

指令实际不区分大小写,但是 约定使用大写

注意:一般情况下,以 # 开头的内容是 注释。这是因为存在特殊特殊情况。分别是:

  • # escape= 格式开头的 Dockerfile
  • # syntax= 格式开头的 Dockerfile

使用 escape 主要的需求是转义 Windows 镜像的特殊字符;而使用 syntax 的场景目前比较少,主要是使用构建的高级特性。

# Dockerfile 常用指令

# FROM

指定构建镜像所用的基础镜像,通常情况下我们会使用 Docker 官方镜像,可以在 Docker Hub 上找到。后续的操作都是基于基础镜像。

注意每个 Dockerfile 文件都必须包含 FROM 指令,如果没有指定 FROM,则 Docker 在解析 Dockerfile 时会报错。

如果不想使用任何基础镜像,则需要使用 FROM scratch,通常构建 基础系统镜像,或者 独立的纯二进制文件 的镜像时会使用这种方式。

# LABEL

LABEL指令可以为生成的镜像添加 元数据标签 信息。

这些信息可以用来辅助过滤出特定的镜像。

镜像会继承基础镜像的标签,LABEL指令会覆盖基础镜像中的同名标签。

每一条LABLE指令都会生成一个镜像层,Docker规定会给一个镜像最多生成127个层,如果超过127层,Docker Deamon 在构建进会出错。

所以我们应该尽量的减少镜像层的数量,即将多个LABEL合并成一个LABEL。

LABEL version="v1.0" \
atuhor="www.atuhor.com"  \
description="hello description"

# RUN

指定 构建过程中 需要执行的操作。

Docker 镜像是层级结构的,每个 RUN 指令都会在一个新层中执行,并将其结果提交为一个新的层,并用于后续的 Dockerfile 的操作。

所以我们应该将多个RUN合并成一个RUN,尽量的减少镜像层的数量。

RUN yum install -y vim

# EXPOSE

通过 EXPOSE 指令就可以为镜像指定要暴露的端口。

表示容器运行时要监听的端口,例如 EXPOSE 6379 则表示要暴露 6379 端口。

暴露端口除了在 Dockerfile 中直接声明以外,还可以通过 docker run -p 进行指定。

EXPOSE 6379

# WORKDIR

指定工作目录。用 WORKDIR 指定的工作目录,会在构建镜像的每一层中都存在。(WORKDIR 指定的工作目录,必须是提前创建好的)。

WORKDIR test # 目录不存在,会自动创建
WORKDIR demo
run pwd # 输出结果为 /test/demo

切换目录,要使用 WORKDIR,不要使用 RUN cd

尽量使用 绝对路径,表达更清晰。

# COPY vs ADD

两者均可在镜像构建时,为镜像添加内容。将 本地文件 添加到 Docker 镜像中。

ADD 指令和 COPY 的使用格式一致(同样需求下,官方推荐使用 COPY)。功能也类似,不同之处如下:

  • ADD 除可用于正常拷贝文件外,还可添加 URL 形式的远程内容。
  • ADD 可添加本地的 tar 归档文件或压缩文件(支持的格式为 gzip、bzip2 或 xz 等),并且会被解压。如果资源是来自远程的内容,则 不会进行解压
  • COPY 还可用于多阶段构建中,通过传递 --from= 的参数,可以从之前的阶段中拷贝内容到新的构建阶段中

添加远程文件或目录请使用 curlwget

COPY test /

ADD test.tar.gz / # 添加到根目录并解压

# ARG vs ENV

ARG 和 ENV 均可用于在构建镜像过程中 预定义变量

但两者的主要区别如下:

  • 生命周期不同:ARG 定义的变量只影响镜像构建阶段,但是 ENV 定义的变量会存在于镜像的整个声明周期,包括使用镜像创建容器,该变量仍然可用。
  • ARG 在构建时,可通过 --build-arg 进行修改和指定,但是 ENV 指定的变量在构建时 不可修改
  • 优先级不同:如果 ARG 和 ENV 定义的变量相同,且 ARG 在 ENV 之前,则 ENV 所定义的变量会覆盖 ARG 所定义的变量。
  • 使用范围不同:ARG 可先于 FROM 使用,但 ENV 不可以。

总结来说,当你在使用时,如果需要在构建过程中修改变量的值,则使用 ARG 指令,如果是想要将值保留至镜像中,甚至是之后容器中使用的话,那使用 ENV 更为合适。另外,为了避免行为混淆,尽量避免 ARG 和 ENV 指定相同名称的变量。

ENV APP_VERSION=v1.0 APP_HOME=/usr/local/app

# VOLUME

定义匿名数据卷。在启动容器时忘记挂载数据卷,会自动挂载到匿名卷。

VOLUME ["/data"]

在启动容器 docker run 的时候,我们可以通过 -v 参数修改 挂载点

# ENTRYPOINT vs CMD

ENTRYPOINT 和 CMD 都定义了当 容器启动时,需要执行的命令。

基于镜像启动的容器,在容器启动时会根据镜像所定义的一条命令来启动容器中进程号为 1 的进程。而这个命令的定义,就是通过 Dockerfile 中的 ENTRYPOINT 和 CMD 实现的

它们的主要区别如下:

  • ENTRYPOINT 指令主要用于对容器进行一些 初始化,而 CMD 指令则用于真正定义容器中 主程序的启动命令
  • NTRYPOINT 定义的命令,在启动容器时,需要指定 --entrypoint 才能覆盖。而使用docker run [OPTIONS] IMAGE [COMMAND] [ARG...]时,如果指定了其他命令,CMD 命令会被忽略。
  • 当 ENTRYPOINT 与 CMD 同时定义了,如果 CMD 使用的是 exec 格式,即:CMD ["aa", "bb"] 形式的话,则其内容会直接作为 ENTRYPOINT 的参数。但如果 CMD 使用的是 shell 格式,即:CMD aa bb 的话,最终的连接形式为 ENTRYPOINT sh -c aa bb。最终执行容器启动的还是 ENTRYPOINT 中给出的命令。
  • 如果定义了多个 CMD 只有最后一个会执行。

如果是想要组合使用 ENTRYPOINT 和 CMD 时,没有特殊需求的情况下,建议 ENTRYPOINT 和 CMD 均使用 exec 格式。

# Shell格式

RUN yum install -y vim
ENV name docker
ENTRYPOINT echo "hello ${name}"

# Exec格式

RUN ["yum", "install", "-y", "vim"]
ENV name docker
ENTRYPOINT ["/bin/echo", "hello $name"]

# ENTRYPOINT vs CMD实例

我们以官方 Redis 镜像为例,下面是 Redis 镜像中对 ENTRYPOINTCMD 的定义。

COPY docker-entrypoint.sh /usr/local/bin/
ENTRYPOINT ["docker-entrypoint.sh"]

EXPOSE 6379
CMD ["redis-server"]

CMD 指令定义的是启动 Redis 的服务程序,而 ENTRYPOINT 使用的是一个外部引入的脚本文件。

事实上,使用 脚本文件 来作为 ENTRYPOINT 的内容是常见的做法,因为对容器运行初始化的命令比较多,全部直接放置在 ENTRYPOINT 后会导致 Dockerfile 文件过于复杂。

docker-entrypoint.sh 脚本文件内容如下:

#!/bin/sh
set -e

# first arg is `-f` or `--some-option`
# or first arg is `something.conf`
if [ "${1#-}" != "$1" ] || [ "${1%.conf}" != "$1" ]; then
	set -- redis-server "$@"
fi

# allow the container to be started with `--user`
if [ "$1" = 'redis-server' -a "$(id -u)" = '0' ]; then
	find . \! -user redis -exec chown redis '{}' +
	exec gosu redis "$0" "$@"
fi

exec "$@"

我们要关注脚本最后的一条命令,也就是 exec "$@" 。在很多镜像的 ENTRYPOINT 脚本里,都有这条命令,其作用其实很简单,就是运行一个程序,而运行命令就是 ENTRYPOINT 脚本的参数。

反过来,由于 ENTRYPOINT 脚本的参数就是 CMD 指令中的内容,所以实际执行的就是 CMD 里的命令

所以说,虽然当 ENTRYPOINT 与 CMD 同时定义了,CMD 会作为 ENTRYPOINT 的参数,但实际在我们使用中,我们还会在 ENTRYPOINT 的脚本里代理到 CMD 命令上。

# 构建镜像

在编写好 Dockerfile 之后,我们就可以通过 docker build 构建我们所定义的镜像了。

docker build -t runoob/ubuntu:v1 . 

当你执行 docker build 命令时,当前的⼯作⽬录被称为 构建上下⽂

默认情况下,Dockerfile 就位于该路径下,当然您也可以使⽤ -f 参数指定 Dockerfile 文件的位置。⽆论 Dockerfile 在什么地⽅,当前⽬录中 的所有⽂件内容都将作为 构建上下⽂发 送到 Docker 守护进程 中去。

# 参考

更新时间: 7/30/2020, 8:25:50 PM