构建镜像 与 `Dockerfile` 语法

3,407 阅读11分钟

Docker 使用 docker build 命令来构建镜像,而构建镜像的具体指令则保存在 Dockerfile 中。

当执行 docker build 命令后,无论 Dockerfile 文件处于何处,当前目录都是构建上下文,是 build 命令的工作目录,也是 COPY 等指令的起始目录

理解镜像生成 ( commit 命令 )

  • 从镜像到容器

镜像是多层存储,每一层都是在前一层的基础上进行的修改,而容器同样也是多层存储,它是在镜像的基础上添加一层作为容器运行存储层。容器的所有修改数据都被保存在该层。一旦容器被删除,那也只是它在镜像上添加的那层运行存储层被删除(除非使用数据卷等数据持久化操作)

  • 从容器到镜像

当我们在容器中进行了一系列操作,想把它作为镜像保存下来时,可以使用 docker commit 命令,将当前容器的运行存储层保存成一个新的镜像,这个镜像运行后的初始环境与当前容器一致

  • docker commit 命令
# 从容器创建一个新的镜像
docker commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]]

-a --author string :   提交的镜像作者
-c --change list :     使用Dockerfile指令来创建镜像
-m --message string :  提交时的说明文字
-p --pause :           在commit时,将容器暂停

CONTAINER: 容器ID
REPOSITORY[:TAG] REPOSITORY: 镜像名称 TAG:镜像标签

docker commit -a "runoob.com" -m "my apache" a404c6c174a2  mymysql:v1
docker images mymysql:v1
# REPOSITORY   TAG      IMAGE ID            CREATED             SIZE
# mymysql      v1       37af1236adef        15 seconds ago      329 MB
  • 尽量不要使用 commit 命令

    • 容器在运行时可能会生成一些后续镜像用不到的文件,导致镜像极为臃肿
    • 黑箱镜像极难维护
      • 使用 commit 从容器中生成镜像的操作对其他人而且是不可知的(即黑箱操作),而且就算是开发者也不一定会记住所有操作(无法保证每次操作容器内容是一致的),这会导致镜像的后续修改与维护十分困难
      • 如果是对容器生成后的镜像再次运行容器修改,再 commit 生成镜像,则会导致一层又一层的臃肿代码

构建镜像命令 build

创建镜像可以使用 docker build 命令根据 Dockerfile 创建一个新的镜像

docker build [OPTIONS] [NAME]   PATH | URL | -
NAME: 镜像名称(有时需要加上tag等镜像信息)
PATH | URL | -:Dockerfile文件的地址以及工作目录设置,默认是同一个目录(可以是远端git repo或本地文件)

--build-arg=[] :设置镜像创建时的变量
--cpu-shares :设置 cpu 使用权重
--cpu-period :限制 CPU CFS周期
--cpu-quota :限制 CPU CFS配额
--cpuset-cpus :指定使用的CPU id
--cpuset-mems :指定使用的内存 id
--disable-content-trust :忽略校验,默认开启
-f :指定要使用的Dockerfile路径
--force-rm :设置镜像过程中删除中间容器
--isolation :使用容器隔离技术
--label=[] :设置镜像使用的元数据
-m :设置内存最大值
--memory-swap :设置Swap的最大值为内存+swap,"-1"表示不限swap
--no-cache :创建镜像的过程不使用缓存
--pull :尝试去更新镜像的新版本
--quiet, -q :安静模式,成功后只输出镜像 ID
--rm :设置镜像成功后删除中间容器
--shm-size :设置/dev/shm的大小,默认值是64M
--ulimit :Ulimit配置
--tag, -t: 镜像的名字及标签,通常 name:tag 或者 name 格式;可以在一次构建中为一个镜像设置多个标签
--network: 默认 default。在构建期间设置RUN指令的网络模式
构建镜像,设置镜像标签等信息

. 当前目录下的Dockerfile文件 此时上下文为当前文件夹

docker build -t runoob/ubuntu:v1 .
根据 GitHub 上文件创建镜像

当使用git repo构建镜像时,docker会自己与git clone一个文件夹,然后进入其中开始构建
此时的上下文为git clone后的文件内部,Dockerfile 也在文件内

docker build github.com/creack/docker-firefox
-f Dockerfile 指定文件的位置

最后的 . 此时只作为工作目录定义Dockerfile的路径会被-f的参数覆盖

# 此时没有指定名称等标签,ps查看的镜像标签全为<none>,应尽量避免出现这种情况
docker build -f /path/to/a/Dockerfile .
从标准输入中读取 Dockerfile 进行构建

docker 会将标准输入的文本作为 Dockerfile,此时没有上下文,也不能执行COPY等操作上下文文件的指令

docker build - < Dockerfile
cat Dockerfile | docker build -
从标准输入中读取上下文压缩包进行构建

如果上一项中,输入的内容是压缩文件(只支持gzip、bzip、xz格式),docker会将其展开,将文件内容视作上下文,Dockerfile文件也从内部读取

docker build - < context.tar.gz

.dockerignore 文件

如果上下文中的文件过多,可能会导致构建的镜像非常庞大、臃肿,尤其是其中患有很多我们不需要的文件,那么这就需要用到 .dockerignore 文件了.

.dockerignore是一个类似 .gitignore 的文件,用于在构建上下文时忽略某些文件,它支持正则和通配符,它的规则定义如下:

# 语法

#	注释
*	匹配0或多个非/的字符
?	匹配1个非/的字符
**	0个或多个目录
!	除...外,需结合上下文语义

# 除README*.md外,所有其他md文件(包括README-secret.md)都被docker忽
*.md
!README*.md
README-secret.md

Dockerfile 语法

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

这个脚本就是 Dockerfile,它 是一个包含了一条条构建镜像所需的指令和说明的文本文件

# Dockerfile 常用指令集
FROM ubuntu:18.04
# 构建的镜像都是基于FROM指定的镜像
COPY . /app
# 复制当前工作目录的文件到容器(此时workdir尚未生效,执行后才生效)
# ADD . /app
# ADD 与 COPY功能一致,但ADD会将压缩文件解压
LABEL com.example.version="0.0.1-beta"
LABEL vendor1="ACME Incorporated"
LABEL com.example.version.is-production=""
# 设置镜像标签
ENV file=docker
# 设置环境变量
ARG app=test
# 声明仅在build生效的环境变量
VOLUME /foo
# 数据卷默认挂载目录(容器数据持久化使用)
RUN echo '构建ubuntu18.04镜像'
# 运行RUN后的命令行命令
CMD echo cmd
# 指定在容器中运行的命令
ENTRYPOINT echo 12121
# run 时默认启动命令
WORKDIR /mydir
# 指定工作目录,在后续指令生效
EXPOSE 5000
# 声明端口
USER patrick
# 指定执行后续命令的用户和用户组
ONBUILD RUN echo '当前基础镜像为ubuntu18.04'
# 使用当前文件构建的镜像为基础镜像时执行
FROM 指令

FROM 用于指定用于构建新镜像的基础镜像

CPOY 指令
  • 源路径:源文件或者源目录,这里可以是通配符表达式
  • 目标路径:容器内的指定路径,该路径不用事先建好,路径不存在的话,会自动创建
COPY [--chown=<user>:<group>] <源路径1>...  <目标路径>
COPY [--chown=<user>:<group>] ["<源路径1>",...  "<目标路径>"]
[--chown=<user>:<group>]:可选参数,用户改变复制到容器内文件的拥有者和属组。

# 示例
COPY hom* /mydir/
COPY hom?.txt /mydir/
ADD 指令

ADD 的使用方法与 COPY 一致,功能也类似。但 ADD 指令在源文件为压缩文件(gzip、bzip2、xz)时,会自动解压缩文件,对需解压缩的文件友好,但无法传输不需压缩的文件,因此在非特殊情况下,推荐使用 CPOY

RUN 指令

执行后面的命令行命令,支持两种写法

在构建镜像 docker build 时执行

  • shell 命令写法:RUN <命令行命令> 等同于,在终端操作的 shell 命令。
  • exec 格式: RUN [可执行文件, arg1, arg2, ...]
CMD 指令

CMD 是在docker运行镜像(docker run)时执行,是默认的容器启动命令, 指令与 RUN 类似,都是运行命令行命令,支持 shellexec 写法 ,但 CMD 多了一种

  • CMD [arg1, arg2, arg3 ...] 该写法主要是为 ENTRYPOINT 指令提供参数
  • 在运行镜像 docker run 时执行
  • CMD 会被 run 命令的容器启动后执行命令给覆盖
  • CMD 只能执行一次,如果有多个 CMD
ENTRYPOINT 指令

ENTRYPOINT 指令也是在镜像运行 docker run 时运行(也是默认容器启动命令的一种),但它不会被 run 指定的命令覆盖,run 的指令也将作为 ENTRYPOINT 命令的参数(CMD 指定的参数会被覆盖)。它也支持 shellexec 写法

run 命令设置 --entrypoint 时可以用命令行参数覆盖该指令

FROM nginx
ENTRYPOINT ["nginx", "-c"] # 定参
CMD ["/etc/nginx/nginx.conf"] # CMD传参
  • 使用 CMD 参数
docker run  nginx:test
容器运行:nginx -c /etc/nginx/nginx.conf
  • run 命令传参
docker run  nginx:test -c /config/new.conf
容器运行:nginx -c /config/new.conf
CMDENTRYPOINT 指令的优先级

当两个指令同时设置命令行命令时,ENTRYPOINT 如果是 shell 则直接覆盖 CMD,如果是 exec 则将 CMD 作为参数使用

No ENTRYPOINT ENTRYPOINT exec_entry p1_entry ENTRYPOINT [“exec_entry”, “p1_entry”]
No CMD error, not allowed /bin/sh -c exec_entry p1_entry exec_entry p1_entry
CMD [“exec_cmd”, “p1_cmd”] exec_cmd p1_cmd /bin/sh -c exec_entry p1_entry exec_entry p1_entry exec_cmd p1_cmd
CMD [“p1_cmd”, “p2_cmd”] p1_cmd p2_cmd /bin/sh -c exec_entry p1_entry exec_entry p1_entry p1_cmd p2_cmd
CMD exec_cmd p1_cmd /bin/sh -c exec_cmd p1_cmd /bin/sh -c exec_entry p1_entry exec_entry p1_entry /bin/sh -c exec_cmd p1_cmd
LABEL 指令

给镜像添加标签

LABEL <key>=<value> <key>=<value> <key>=<value> ...
EXPOSE 指令

仅仅只是声明端口,在运行时使用随机端口映射时,也就是 docker run -P 时,会自动随机映射 EXPOSE 的端口。

EXPOSE <端口 1> [<端口 2>...]
ENV 指令

定义环境变量,该指令定义的变量在后续的指令以及运行的容器中都可以获取


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

# 如:设置version=0.1.0
ENV version=0.1.0
RUN echo $version
容器中:echo $version
ARG 指令

ARG 指令也是用于设置环境变量,与 ENV 指令作用一致,但 ARG 指令设置的变量只能在 build 时(即 Dockerfile 文件中)使用,作用域不及 ENV 指令

VOLUME 指令

定义默认数据卷挂载目录,在没有设置挂载目录时,会默认挂载到该目录下,避免数据丢失

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

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

用于指定执行后续命令的用户和用户组,这边只是切换后续命令执行的用户(用户和用户组必须提前已经存在)。

WORKDIR 指令

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

docker build 构建镜像过程中的,每一个 RUN 命令都是新建的一层。只有通过 WORKDIR 创建的目录才会一直存在。

ONBUILD 指令

用于延迟构建命令的执行。简单的说,就是 Dockerfile 里用 ONBUILD 指定的命令,在本次构建镜像的过程中不会执行(假设镜像为 test-build)。当有新的 Dockerfile 使用了之前构建的镜像 FROM test-build ,这是执行新镜像的 Dockerfile 构建时候,会执行 test-buildDockerfile 里的 ONBUILD 指定的命令

STOPSIGNAL 指令

STOPSIGNAL 指令设置将被发送到容器退出的系统调用信号。该信号可以是与内核 syscall 表中的位置匹配的有效无符号数字(例如 9),也可以是格式为 SIGNAME 的信号名称 (例如 SIGKILL)

STOPSIGNAL signal
HEALTHCHECK 指令

用于指定某个程序或者指令来监控 docker 容器服务的运行状态

HEALTHCHECK [选项] CMD <命令>:设置检查容器健康状况的命令
HEALTHCHECK NONE:如果基础镜像有健康检查指令,使用这行可以屏蔽掉其健康检查指令

HEALTHCHECK [选项] CMD <命令> : 这边 CMD 后面跟随的命令使用,可以参考 CMD 的用法

--interval=DURATION(默认值:30s)
--timeout=DURATION(默认值:30s)
--start-period=DURATION(默认值:0s)
--retries=N(默认值:3)
SHELL 指令

设置容器运行的默认 shell

SHELL ["executable", "parameters"]

# 使用shell(linux默认)
SHELL ["/bin/sh", "-c"]

# 使用powershell
SHELL ["powershell", "-command"]

# 使用cmd(Windows默认)
SHELL ["cmd", "/S", "/C"]

Dockerfile 注意事项

多阶段构建

在构建镜像时如果全部内容都在一个 Dockerfile 内容,最后生成的镜像可能会非常庞大,但如果分散到多个 Dockerfile 文件,管理与部署又会变得非常麻烦,在 docker 17.05 之后可以使用多阶段构建来处理这种情况

多阶段构建是通过 FROM 指令实现的,您可以在 Dockerfile 中使用多个 FROM 语句。每个 FROM 指令可以使用不同的基础,并且每个指令都开始一个新的构建。您可以选择性地将工件从一个阶段复制到另一个阶段,从而在最终 image 中只留下您想要的内容

docker build --target builder # 可以使用--target 指定构建某一阶段的镜像

# 通过多次使用FROM,将镜像构建分为两个阶段
FROM golang:1.11-alpine AS build
RUN apk add --no-cache git
RUN go get github.com/golang/dep/cmd/dep
COPY Gopkg.lock Gopkg.toml /go/src/project/
WORKDIR /go/src/project/
RUN dep ensure -vendor-only
COPY . /go/src/project/
RUN go build -o /bin/project

# 通过 COPY --from=xxxx 指令将第一阶段的镜像复制到第二阶段,但只留下需要的部分
FROM scratch
COPY --from=build /bin/project /bin/project
ENTRYPOINT ["/bin/project"]
CMD ["--help"]
避免无意义的层数

Dockerfile 的指令每执行一次都会在 docker 上新建一层。所以过多无意义的层,会造成镜像膨胀过大

通过 RUN

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
其他优化设置
  • 设置 .dockerignore 文件,避免无用的大文件影响
  • 尽量使用 COPY 而非 ADD

尽管 ADDCOPY 在功能上相似,但 COPYADD 简单。COPY 仅支持将本地文件基本复制到容器中,而 ADD 具有一些并非必要的功能(如解压 tar 文件和支持 url 获取),因此,除非一些需要解压缩文件等特殊情况外,尽量使用 ADD

  • 缓存设置

docker 在按行执行 Dockerfile 指令时,如果存在缓存会直接使用缓存的结果,如果不需要缓存可以在 build 时设置 --no-cache=true,避免缓存过多减慢速度

  • 合理调整 COPYRUN 的顺序

一但 COPYADD 执行,那么之前的缓存就会失效,该指令之后构建的镜像也会因此不再使用缓存,因此,类似 COPY WORKDIR ENV LABEL 等命令可以往后移,将缓存变化较少的指令前移

  • 添加 HEALTHCHECK 健康检查

参考文档