Dockerfile编写和镜像构建

一、 常用Dockerfile指令

  • FROM 指定当前镜像构建流程的基础镜像,一般是基于官方linux发行版或其他官方镜像源
    语法:
    FROM [–platform=<platform>] <image> [AS <name>]
    示例:
    1
    FROM alpine:latest

  • RUN 创建一个容器层,并在当前容器层执行命令
    语法:
    RUN <command>
    示例:
    1
    2
    RUN apk update \
    && apk add nginx

  • ENV 设置环境变量
    语法:
    ENV <key>=<value> …
    示例:
    1
    2
    3
    ENV SERVER_NAME=服务名称
    # 一次设置多个
    ENV TITLE=标题 TIMEZONE="Asia/SangHai"

  • LABEL 设置镜像元数据标签信息
    语法:
    LABEL <key>=<value> <key>=<value> <key>=<value> …
    示例:
    1
    2
    3
    4
    5
    6
    7
    LABEL SERVER_NAME=“服务名称“
    # 设置多个
    LABEL LABEL1="LABEL1" LABEL2="LABEL2"
    # 换行多个
    LABEL LABEL1="LABEL1" \
    LABEL2="LABEL2"
    LABEL3="LABEL3"

  • EXPOSE 声明服务内监听的端口,可以指定端口和协议,如果不指定协议默认使用TCP协议。实际上,并没有发布或开放对应端口,仅仅声明要开放的端口。一般写在Dockerfile末尾进行声明,类似一行注释文档。使用docker inspect 容器id可见
    语法:
    EXPOSE <port> [<port>/<protocol>…]
    示例:
    1
    2
    3
    4
    5
    6
    7
    # 声明53端口的dns服务
    EXPOSE 53/udp
    # 声明80端口的http服务
    EXPOSE 80/tcp
    # 同时开放80端口同时开放udp和tcp协议
    EXPOSE 80/tcp
    EXPOSE 80/udp

  • COPY 从构建的上下文路径中拷贝文件到指定目录,复制源基于构建上下文的相对路径,支持使用通配符匹配复制.来源如果是文件夹,表示的是复制文件夹下的全部内容。目标文件夹如果使用相对路径表示相对WORKDIR目录
    语法:
    COPY [–chown=<user>:<group>]\ [“<src>“,… “<dest>“]
    示例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # 复制上下文目录的所有文件到root目录
    COPY ./ /root/
    # 复制上下文目录下的tar包到root目录
    COPY *.tar /root/
    # 复制文件名test开头的文件到root目录
    COPY test* /root/
    # 复制start.sh到WORKDIR/shell目录
    COPY start.sh shell/
    # 复制文件指定复制文件后的属主和数组为www-data(很少用,一般启动容器时指定)
    COPY --chown=www-data:www-data ./code /home/www-data/

  • ENTRYPOINT 容器启动默认执行的指令
    语法:
    ENTRYPOINT [“executable”, “param1”, “param2”]
    示例:
    1
    2
    # 容器启动是启动nginx作为1号进程
    ENTRYPOINT ["top", "-b"]

  • WORKDIR 设置工作目录,如果目录不存在则创建。如果提供的是相对路径则是相对上次WORKDIR设置的路径
    语法:
    WORKDIR /path/to/workdir
    示例:
    1
    WORKDIR /home/www-data

  • CMD 指定启动容器时默认执行的命令
    语法:
    CMD [“executable”,”param1”,”param2”]
    示例:
    1
    2
    3
    4
    # 启动容器是打印一条信息
    CMD echo "容器启动啦"
    # 打印nginx帮助信息
    CMD ["nginx", "-h"]

二、构建命令

语法:

docker build [OPTIONS] PATH | URL | -

docker通过build命令读取Dockerfile和上下文构建Docker镜像。构建的上下文可以使相对或绝对路径,也可以是一个URL。其他常用选项有

  • -f: 指定构建文件,默认是文件是当前目录的名称为Dockerfile的文件
  • -t: 指定镜像的名称和标签
  • –label: 为镜像打标签
  • –no-cache: 构建过程不使用层缓存
  • –rm: 构建成功后删除掉中间容器
  • –target: 指定最终构建阶段名称,针对分多阶段构建的Dockerfile

其他选项用的很少或者基本用不上,不再过多介绍,更多选线参考官方文档

构建示例如下:

1
2
3
4
5
6
7
8
9
10
# 当前环境作为上下文构建镜像
docker build -t foo:version_1.0 .
# 指定构建文件
docker build -t foo:version_1.0 -f ./Dockerfile.dev .
# 指定构建过程的上下文
docker build -t foo:version ./context_resource
# 分阶段构建的构建文件,构建TEST过程作为最终镜像
docker build -t foo:version_1.0 --target TEST .
# 分阶段构建的构建文件,删除中间镜像
docker build --rm -t foo:version_1.0 .

三、 Docker分层容器

一个Docker镜像是有多个层构成的,每一层代表Dockerfile的一条指令,最后一层是可写层,其它层都是只读层。前一层和后一层的仅仅保存差异。比如下层镜像删除了一个文件,而上层镜像依然可以读取,并且删除下层的文件不会让总体积更小。基于镜像创建一个容器,本质是在给镜像增加了一层作为可写层,容器内的任意文件读写都针对可写层。当容器被删除时,可写层被删除,镜像原有的镜像层保持不变。所以,每个容器堆文件的写入是在独立的可写层中。而多个容器共享底层镜像的只读层。这就是为什么当有基于镜像创建的容器时,无法删除镜像的原因。容器内对文件的读取和修改采用写时复制的(CoW)方式。拿修改系统/etc/hosts文件举例,当在可写层对hosts文件进行读取时,直接使用镜像上层文件进行读取。而当堆/etc/hosts文件进行修改,才会将hosts文件复制到可写层。总之,一个镜像分为多层,下层采用写时复制的方式使用上层文件以缩减当前层级的体积。

sharing-layers

四、分步构建

在v1.75之前,Dockerfile文件只能写一个镜像的构建脚本,这样就带来几个问题。

  1. 镜像层级和体积增多
    一个Docker镜像存在多层,每一层互相隔离。这就导致无法通过删除容器内文件来达到缩减层级和镜像体积的目的。一般来说,我们使用先构建一个镜像,将中间镜像的文件拷贝到Dockerfile的上下文,然后继续编译。这种方式为了达到自动化,需要写两个Dockerfile和一个shell脚本。一方面减少了层级和体积,另一方面能够加速构建的过程
  2. Dockerfile可读性不高
    Dockerfile中每个RUN指令都会创建一个新的镜像层。确切的说是对镜像本身环境有影响的Dockerfile指令都会创建新层。比如COPY, RUN, ENV等命令会创建新增,而LABEL,CMD等仅修改镜像元数据的命令则不会产生新层。这就意味着要想最终通过删除文件缩减容器体积,在最终删除构建文件前,只能使用一个RUN命令。如果分成多个RUN,最后删除文件操作也无法删除之前层级的文件,导致镜像体积增大。

好在,Docker从v17.05开始支持多阶段构建。一个Dockerfile可以分多个阶段构建。后面的构建阶段可以引用、复制前面构建完成的镜像内的文件。下面的例子,演示了通过分布构建设置alpine镜像的时区为Asia/shanghai

1
2
3
4
5
FROM alpine:latest AS BUILD_ONE
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories
RUN apk add --update-cache tzdata
FROM alpine:latest
COPY --from=BUILD_ONE /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

首先,通过第0层镜像安装tzdata,得到时区文件。之后将时区文件复制到最终镜像中。容器体积仅仅增加localtime文件大小。因此分层构建经常用于缩减镜像体积。