docker - 基于DockerFile的镜像制作

制作自己的Docker镜像主要有如下两种方式:

  • 运行一个已有的镜像,启动镜像后,进入容器,进行相关的操作(安装、配置等)然后将容器保存为一个新的镜像
  • 构建一个DockerFile(记录了镜像创建步骤),然后使用docker build创建一个新的镜像。

本文主要介绍通过DockerFile进行镜像的创建,commit的创建可以参考另外一篇文章。

Dockerfile是一个文本文档,其中包含用户可以在命令行上运行调用以生成镜像的所有命令。获得 Dockerfile 文件后,我们可以使用 docker build 来完成镜像的创建。
要创建一个 Dockerfile 文件,我们首先介绍一下 DockerFile 文件支持的指令和文件格式。

获得一个DokcerFile

通过已知的镜像获取DockerFile

dfimage 是一款第三方工具,可用来从镜像中提取 Dockerfile

1
2
alias dfimage="docker run -v /var/run/docker.sock:/var/run/docker.sock --rm alpine/dfimage"
dfimage -sV=1.36 test:v1.0

如果我们有一个基本满足我们需求的镜像,但是可能这个镜像相对我们有一些冗余的功能,我们可以通过已知的镜像生成一个DockerFile进行微调。当然也可以通过这种方式了解一些镜像的构建过程,相比于 docker history 使用dfimage得到的镜像构建信息会更全面。

从头写一个 DockerFile

要自己写 DockerFile, 我们先来简单的了解一下DockerFile的语法。

Dockerfile 中使用#来进行注释,同时Dockerfile支持多种不同的命令分别用以指定初始镜像(FROM)、工作目录(WORKDIR)、运行数据挂载和网络设置(RUN)、命令行执行(CMD)、设置环境变量(ENV)、添加文件到镜像文件系统中(ADD)、添加文件到容器文件系统中(COPY)、创建挂载点(VOLUME)、设置用户(USER)、变量(ARG)、镜像添加元数据(LABEL)、监听网络(EXPOSE)、入口点(ENTRYPOINT)等等。
现在为大家介绍一些镜像构建过程中会用到的基本命令,一些指令在现在的个人进行镜像构建的过程中并未用到,暂时不进行扩展,大家有需要的时候可以参考官方文档DockerFile reference

DockerFile 指令说明

DockerFile指令 作用 示例
FROM 初始基础镜像,后续所有操作都是以基础镜像为初始环境 FROM ubuntu:14.04
RUN 用于执行后面跟着的命令行命令。有以下俩种格式。 RUN <命令行命令>
COPY 复制指令,从上下文目录中复制文件或者目录到容器里指定路径。 COPY [--chown=<user>:<group>] <源路径1>... <目标路径>
ADD 添加本地或远程文件和目录。
ARG 使用构建时变量。
CMD 指定默认命令。
ENTRYPOINT 指定默认可执行文件。
ENV 设置环境变量。
EXPOSE 描述您的应用程序正在侦听哪些端口。
HEALTHCHECK 在启动时检查容器的运行状况。
LABEL 将元数据添加到图像。
MAINTAINER 指定图像的作者。
ONBUILD 指定在构建中使用映像时的说明。
SHELL 设置图像的默认外壳。
STOPSIGNAL 指定退出容器的系统调用信号。
USER 设置用户和组 ID。
VOLUME 创建卷挂载。
WORKDIR 更改工作目录。

解析器指令

顾名思义,解析器指令和之前列的这些镜像操作指令不一样,解析器指令是用来配置DockerFile解释器如何解析文件的,
会影响 Dockerfile 中后续行的处理方式。解释器指令不是必须的,只有需要对解析逻辑进行调整时,需要配置。解析器指令不会向构建添加层,也不会显示为构建步骤。解析器指令以 # directive=value 形式编写为特殊类型的注释。单个指令只能使用一次。
解析器指令位于 Dockerfile 的顶部,否则都会视为注释。

1
2
3
4
5
#  指定DockerFile的版本
# syntax=docker/dockerfile:1

# 指定命令续航符(默认的"\"),比如windwos系统,默认续航符会和目录间隔符存在冲突,我们需要进行更改。
# escape=`

ENV

环境变量(使用 ENV 语句声明)也可以在某些指令中用作由 Dockerfile 解释的变量。还可以处理转义以将类似变量的语法按字面意思包含到语句中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

${variable:-word} #表示如果设置了 variable ,则结果将是该值。如果 variable 未设置,则结果为 word 。

${variable:+word} #表示如果设置了 variable ,则结果为 word ,否则结果为空字符串。

#${variable#pattern} 从 variable 中删除 pattern 的最短匹配,从字符串的开头查找。
str=foobarbaz echo ${str#f*b} # arbaz

#${variable##pattern} 从 variable 中删除 pattern 的最长匹配项,从字符串的开头查找。
str=foobarbaz echo ${str##f*b} # az

#${variable%pattern} 从 variable 中删除 pattern 的最短匹配,从字符串末尾向后查找。
string=foobarbaz echo ${string%b*} # foobar

#${variable%%pattern} 从 variable 中删除 pattern 的最长匹配项,从字符串末尾向后查找。
string=foobarbaz echo ${string%%b*} # foo

#${variable/pattern/replacement} 将 variable 中第一次出现的 pattern 替换为 replacement
string=foobarbaz echo ${string/ba/fo} # fooforbaz

#${variable//pattern/replacement} 将 variable 中出现的所有 pattern 替换为 replacement
string=foobarbaz echo ${string//ba/fo} # fooforfoz

#pattern 是一个全局模式,其中 ? 匹配任何单个字符, * 匹配任意数量的字符(包括零)。要匹配文字 ? 和 * ,请使用反斜杠转义: \? 和 \*

word 可以是任何字符串,包括其他环境变量

1
2
3
4
5
6
# 示例(解析后的表示显示在 # 之后):
FROM busybox
ENV FOO=/bar
WORKDIR ${FOO} # WORKDIR /bar
ADD . $FOO # ADD . /bar
COPY \$FOO /quux # COPY $FOO /quux

ADD、COPY、ENV、EXPOSE、FROM、LABEL、STOPSIGNAL、USER、VOLUME、WORKDIR中都支持使用环境变量。
** 环境变量替换在整个指令中对每个变量使用相同的值。更改变量的值仅在后续指令中生效。

From

1
2
3
4
# 三种语法示例
FROM [--platform=<platform>] <image> [AS <name>]
FROM [--platform=<platform>] <image>[:<tag>] [AS <name>]
FROM [--platform=<platform>] <image>[@<digest>] [AS <name>]

FROM 初始化一个新的构建阶段并为后续指令设置基础镜像。因此,有效的 Dockerfile 必须以 FROM 开头。该镜像可以是任何有效的镜像(尽可能选择满足需求的最小镜像)。

  • ARG是唯一可以位于FROM之前的指令
  • FROM 可以在单个 Dockerfile 中出现多次,以创建多个镜像或使用一个构建阶段作为另一个构建阶段的依赖项。前面的FROM及对应的构建阶段都不会保存,只有最后一个阶段构建的镜像会被直接保存。
  • tagdigest 值是可选的。如果省略其中任何一个,构建器默认采用 latest 标记。

RUN

RUN 指令将执行任何命令以在当前图像之上创建新层。添加的层将在 Dockerfile 的下一步中使用。 RUN 有两种形式:

1
2
3
4
5
6
7
#shell格式,<命令行命令> 等同于,在终端操作的 shell 命令。
RUN <命令行命令>

#exec 格式:
RUN ["可执行文件", "参数1", "参数2"]
# 例如:
# RUN ["./test.php", "dev", "offline"] 等价于 RUN ./test.php dev offline

shell 形式是最常用的,它允许您将较长的指令分成多行,可以使用换行符转义,也可以使用下属方法:

1
2
3
4
5
#shell格式,<命令行命令> 等同于,在终端操作的 shell 命令。相当于命令行续航符"\"
RUN <<EOF
apt-get update
apt-get install -y curl
EOF

前面的RUN构建过程相对容易,比较复杂的时相关文件系统的处理,因为可能在构建过程中,我们需要使用到一些特定的数据,这些数据我们需要在执行某些特定命令的时候,挂载到镜像上。 RUN –mount 允许您创建构建可以访问的文件系统挂载。这可以用于:

  • 创建到主机文件系统或其他构建阶段的绑定挂载
  • 访问构建机密或 ssh-agent 套接字
  • 使用持久的包管理缓存来加快构建速度
类型 说明
bind(default) 用于挂载一个上下文目录(不能挂载宿主机的目录)到容器中 ,默认时只读的
cache 主要用于挂载一个临时目录来缓存编译器和包管理器的目录。
tmpfs 主要用于挂载一个tmpfs
secret 允许构建容器访问诸如私钥之类的安全文件,并且此类文件不会出现在构建好的镜像中,避免密钥外泄。
ssh 允许构建容器通过SSH代理访问SSH密钥,并支持密码短语

这部分使用因为在自己业务场景中没有太大需求,这里介绍一些简单的示例。知道有这个功能就好,就不进行展开了。

1
2
3
4
5
RUN --mount=[type=TYPE][,option=<value>[,option=<value>]...]

# 挂载一个大小为100MB的临时文件系统到 /mnt 目录,并在其中执行命令
RUN --mount=type=tmpfs,size=100m,uid=1000,gid=1000,mode=0755 \
touch /mnt/test_file

CMD

CMD 指令设置从映像运行容器时执行的命令。
Dockerfile 中只能有一条 CMD 指令。如果列出多个 CMD ,则只有最后一个生效。
CMD 的目的是为执行容器(创建镜像阶段不会执行)提供默认值。这些默认值可以包含可执行文件,也可以省略可执行文件,在这种情况下,您还必须指定 ENTRYPOINT 指令。

COPY示例

1
2


SHELL

SHELL 指令用于更改执行命令的默认 shell。
Linux 上的默认 shell 是 [“/bin/sh”, “-c”] ,
Windows 上的默认 shell 是 [“cmd”, “/S”, “/C”] 。
SHELL 指令必须以JSON形式写入 Dockerfile 中。 SHELL 指令在 Windows 上特别有用,其中有两种常用且截然不同的本机 shell: cmdpowershell ,以及可用的备用其它 shell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
FROM microsoft/windowsservercore

# Executed as cmd /S /C echo default
RUN echo default

# Executed as cmd /S /C powershell -command Write-Host default
RUN powershell -command Write-Host default

# Executed as powershell -command Write-Host hello
SHELL ["powershell", "-command"]
RUN Write-Host hello

# Executed as cmd /S /C echo hello
SHELL ["cmd", "/S", "/C"]
RUN echo hello

当 Dockerfile 中使用 shell 形式时,以下指令可能会受到 SHELL 指令的影响: RUN 、 CMD 和 ENTRYPOINT .

ARG

ARG 指令定义了一个变量,用户可以在构建时使用 –build-arg = 标志通过 docker build 命令将其传递给构建器。
ARG 变量定义从 Dockerfile 中定义它的行开始生效,而不是从命令行或其他地方使用参数开始生效。所以尽可能紧跟FROM 完成需要传递变量的定义,从而避免引用变量时,还未进行定义。

1
2
3
4
5
6
FROM busybox
# 定义一个变量
ARG user1
# 定义一个变量并设置默认值
ARG buildno=1
# ...
  • 每个构建阶段单独声明
    ARG 指令在定义它的构建阶段结束时超出范围。要在多个阶段使用参数,每个阶段必须包含 ARG 指令。
    1
    2
    3
    4
    5
    6
    7
    FROM busybox
    ARG SETTINGS
    RUN ./run/setup $SETTINGS

    FROM busybox
    ARG SETTINGS
    RUN ./run/other $SETTINGS

撰写DockerFile

了解了DockerFile的语法之后,我们可以利用对应的指令编写我们需要环境对应的一个 DockerFile。
再编写DockerFile的时候,我们需要注意几个点:

规范要求

  1. 将以 # 开头的行视为注释,除非该行是有效的解析器指令。行中任何其他位置的 # 标记都被视为参数。这允许这样的语句:

    1
    2
    3
    RUN echo hello \
    # comment
    world
  2. 约定俗成

  3. 该指令不区分大小写。然而,惯例是它们是大写的,以便更容易地将它们与参数区分开来。

注意事项

  1. 编写Dockerfile,Dockerfile中每一条/行指令都创建镜像的一层,所以尽可能合并无意义的层,避免镜像过大,例如:
    这时候,也许会想,我们把所有环境安装都放到一层不就可以了么。
    创建镜像的时候还要考虑其他问题:
    1. 那就是镜像创建环境的网络稳定性。如果只有一层,而网络又不稳定的话,那么可能每次构建都会中途中断,然后需要从头重构。进行合理的分层,相当于设置一些构建过程中的一些重要节点(层),这样在进行build的时候,会记录保存中间层的构建,二次构建的时候,可以直接引用已有层的结果,即使中途中断,也可以从对应节点继续。
    2. 如果需要使用的镜像环境非常多,我们需要构建多个镜像,但是他们有一些相同的底层镜像依赖。那么我们可以通过镜像分层构建共同的底座,然后通过继承的方式来构建不同的镜像。
    
    不建议的写法(命令拆分产生无意义的多层镜像):
    1
    2
    3
    4
    5
    6
    7
    8
    # 这里是注释
    # 设置继承自哪个镜像
    FROM ubuntu:14.04
    # 下面是一些创建者的基本信息
    MAINTAINER liubo (liubo4@genomics.com)
    # 在终端需要执行的命令
    RUN apt-get install -y openssh-server # 创建第一层
    RUN mkdir -p /var/run/sshd # 创建第二层

建议的写法(如非必要只创建一层)

1
2
3
4
5
6
# 设置继承自哪个镜像
FROM ubuntu:14.04
# 下面是一些创建者的基本信息
MAINTAINER birdben (191654006@163.com)
# 在终端需要执行的命令
RUN apt-get install -y openssh-server && mkdir -p /var/run/sshd # 创建第一层

通过DockerFile生成镜像

编写完成 Dockerfile 后可以使用 docker build 来生成镜像。
首先创建一个新的目录,将 DockerFile 文件存档道目录中,执行 docker build 命令来生成镜像。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ sudo docker build -t="birdben/ubuntu:v1" .

# 下面是一堆构建日志信息
############
我是日志
############

# 参数:
# -t 标记来添加 tag,指定新的镜像的用户和镜像名称信息。
# “.” 是 Dockerfile 所在的路径(当前目录),也可以替换为一个具体的 Dockerfile 的路径。

# 以交互方式运行docker
$ docker run -it birdben/ubuntu:v1 /bin/bash

# 运行docker时指定配置
$ sudo docker run -d -p 10.211.55.4:9999:22 ubuntu:tools '/usr/sbin/sshd' -D

# 参数:
# -i:表示以“交互模式”运行容器,-i 则让容器的标准输入保持打开
# -t:表示容器启动后会进入其命令行,-t 选项让Docker分配一个伪终端(pseudo-tty)并绑定到容器的标准输入上
# -v:表示需要将本地哪个目录挂载到容器中,格式:-v <宿主机目录>:<容器目录>,-v 标记来创建一个数据卷并挂载到容器里。在一次 run 中多次使用可以挂载多个数据卷。
# -p:指定对外80端口
# 不一定要使用“镜像 ID”,也可以使用“仓库名:标签名”

在build阶段设置参数

  • Dockerfile 最后一行如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    [root@fangjike temp]# cat Dockerfile 
    FROM python:2.7-slim

    MAINTAINER yellowtail

    COPY startup.sh /opt

    RUN chmod +x /opt/startup.sh

    ARG envType=xxx
    ENV envType ${envType}

    CMD /opt/startup.sh ${envType}
  • build

    1
    docker build -t yellow:4.0 --build-arg envType=dev .
  • run

    1
    2
    [root@fangjike temp]# docker run -ti --rm=true yellow:4.0
    in startup, args: dev

参考资料

官方指南
知乎

-------------本文结束感谢您的阅读-------------