Skip to content

使用Dockerfile创建镜像

Dockerfile 是一个文本格式的配置文件,用户可以使用 Dockerfile 来快速创建自定义的镜像

基本结构

Dockerfile 由一行行命令语句组成,并且支持以#开头的注释行

一般而言,Dockerfile 主题内容分成四部分:基础镜像信息、维护者信息、镜像操作指令和容器启动时执行命令

下面给出一个简单的示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# escape=\ (backslash)
# This dockerfile uses the ubuntu:xeniel image
# VERSION 2- EDITION 1
# Author: docker_user
# Command format: Instruction [arguments/command]..
# Base image to use, this must be set as the first line
FROM ubuntu:xeniel
# Maintainer: docker_user <docker_user at email.com>(@docker_user)
LABEL maintainer docker_user <docker_user@email.com>
# Commands to update the image
RUN echo "deb http://archive.ubuntu.com/ubuntu/xeniel main universe" >> /etc/apt/sources.list
RUN apt-get update && apt-get install -y nginx
RUN echo "\ndaemon off; " >> /etc/nginx/nginx.conf
# Commands when creating a new container
CMD /usr/sbin/nginx

首行可以通过注释来指定解析器命令,后续通过注释说明镜像的相关信息
主体部分首先使用FROM指令指明所基于的镜像名称,接下来一般是使用LABEL指令说明维护者信息。
后面则是镜像操作指令,例如RUN指令将对镜像执行跟随的命令。
每运行一条RUN指令,镜像添加新的一层,并提交。
最后是CMD指令,类指定运行容器时的操作命令

下面是 Docker Hub 上两个热门镜像 nginx 和 Go 的 Dockerfile 的例子,通过这两个例子,可以对 Dockerfile 结构有个基本的感知

第一个是在 debian:jessie 基础镜像基础上安装 Nginx 环境,从而创建一个新的 nginx 镜像

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
FROM debian:jessie
LABEL maintainer docker_use<docker_user@email.com>
ENV NGINX_VERSION 1.10.1-1-jessie
RUN apt-key adv --kerserver hkp://pgp.mit.edu:80 --recv-keys 573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62 \
    && echo "deb http://nginx.org/packages/debian/ jessie nginx" >> /etc/apt/sources.list \
    && apt-get update \
    && apt-get install --no-install-recommends --no-install-suggests -y \
    ca-certificates \
    nginx=${NGINX_VERSION} \
    nginx-module-xslt \
    nginx-module-image-filter \
    nginx-module-perl \
    nginx-module-njs \
    gettext-base \
    && rm -rf /var/lib/apt/lists/*
# forward request and error logs to docker log collector
RUN ln -sf /dev/stdout /var/log/nginx/access.log \
    && ln -sf /dev/stderr /var/log/nginx/error.log
EXPOSE 80443
CMD ["nginx", "-g", "daemon off; "]

第二个是基于buildpack-deps:jessie-scm基础镜像,安装 Golang 相关环境,制作一个 Go 语言的运行环境镜像:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
FROM buildpack-deps:jessie-scm
# gcc for cgo
RUN apt-get update && apt-get install -y --no-install-recommends \
    g++ \
    gcc \
    libc6-dev \
    make \
    && rm -rf /var/lib/apt/lists/*
ENV GOLANG_VERSION 1.6.3
ENV GOLANG_DOWNLOAD_URL https://golang.org/dl/go$GOLANG_VERSION.linux-adm64.tar.gz
ENV GOLANG_DOWNLOAD_SHA256 cdde5e08530c0579255d6153b08fdb3b8e47caabbe717bc7bcd7561275a87aeb
RUN curl -fsSL "$GOLANG_DOWNLOAD_SHA256 golang.tar.gz" | sha256sum -c - \
    && tar -C /usr/local -xzf golang.tar.gz \
    && rm golang.tar.gz
ENV GOPATH /go
ENV PATH $GOPATH/bin:/usr/local/go/bin$PATH
RUN mkdir -p "$GOPATH/src" "$GOPATH/bin" && chmod -R 777 "$GOPATH"
WORKDIR $GOPATH
COPY go-wrapper /usr/local/bin/

指令说明

Dockerfile 中指令的一般格式为 INSTRUCTION arguments,包括“配置指令”(配置镜像信息)和“操作指令”(具体执行操作)

Dockerfile 中的指令及说明:

Dockerfile中的指令及说明

配置指令

ARG

定义创建镜像过程中使用的变量
格式为ARG <name>[=<default value>]

在执行 docker build 时,可以通过 -build-arg[=] 来为变量赋值。
当镜像编译成功后,ARG 指定的变量将不再存在(ENV 指定的变量将在镜像中保留)

Docker 内置了一些镜像创建变量,用户可以直接使用而无须声明,包括(不区分大小写)HTTP_PROXY、HTTPS_PROXY、FTP_PROXY、NO_PROXY。

FROM

指定所创建镜像的基础镜像。
格式为FROM <image> [AS <name>]FROM <image>:<tag> [AS <nmae>]FROM <image>@<digest> [AS <name>]

任何 Dockerfile 中第一条指令必须为 FROM 指令。
并且,如果在同一个 Dockerfile 中创建多个镜像时,可以使用多个 FROM 指令(每个镜像一次)

为了保证镜像精简,可以选用体积较小的镜像如 Alpine 或 Debian 作为基础镜像。例如

1
2
ARG VERSION=9.3
FROM debian:${VERSION}

LABEL

LABEL 指令可以为生成的镜像添加元数据标签信息。这些信息可以用来辅助过滤出特定镜像

格式为LABEL <key>=<value> <key>=<value> <key>=<value>...

例如:

1
2
3
4
LABEL version="1.0.0-rc3"
LABEL author="zyz@github" date="2020-01-01"
LABEL description="This text illustrates \
    that label-values can span multiple lines."

EXPOSE

声明镜像内服务监听的端口
格式为EXPOSE <port> [<port>/<protocol>...]

例如:

1
EXPOSE 22 808443

注意该指令只是起到声明作用,并不会自动完成端口映射

如果要映射端口出来,在启动容器时可以使用-P参数(Docker 主机会自动分配一个宿主机的临时端口)或-p HOST_PORT:CONTAINER_PORT参数(具体指定所映射的本地端口)

ENV

指定环境变量,在镜像生成过程中会被后续 RUN 指令使用,在镜像启动的容器中也会存在

格式为ENV <key> <value>ENV <key>=<value>...

例如:

1
2
3
ENV APP_VERSION=1.0.0
ENV APP_HOME=/usr/local/app
ENV PATH $PATH:/usr/local/bin

指令指定的环境变量在运行时可以被覆盖掉,如docker run --env <key>=<value> build_image

注意当一条 ENV 指令中同时为多个环境变量赋值并且值也是从环境变量读取时,会为变量都赋值后再更新。
如下面的指令,最终结果为key1=value1 key2=value2

1
2
ENV key1=value2
ENV key1=value1 key2=${key1}

ENTRYPOINT

指定镜像的默认入口命令,该入口命令会在启动容器时作为根命令执行,所有传入值作为该命令的参数

支持两种格式:
ENTRYPOINT ["executable", "param1", "param2"]:exec调用执行
ENTRYPOINT command param1 param2:shell中执行

此时,CMD 指令指定值将作为根命令的参数

每个 Dockerfile 中只能有一个 ENTRYPOINT,当指定多个时,只有最后一个起效

在运行时,可以被--entrypoint参数覆盖掉,如docker run --entrypoint

VOLUME

创建一个数据挂载点
格式为 VOLUME ["/data"]
运行容器时可以从本地主机或其他容器挂载数据卷,一般用来存放数据和需要保持的数据等

USER

指定运行容器时的用户名或 UID,后续的 RUN 等指令也会使用指定的用户身份

格式为 USER daemon
当服务不需要管理员权限时,可以通过该命令指定运行用户,并且可以在 Dockerfile 中创建所需要的用户。例如:

1
RUN groupadd -r postgres && useradd --no-log-init -r -g postgres postgres

要临时获取管理员权限可以使用gosu命令

WORKDIR

为后续的 RUN、CMD、ENTRYPOINT 指令配置工作目录

格式为WORKDIR /path/to/workdir

可以使用多个WORKDIR指令,后续命令如果参数时相对路径,则会基于之前命令指定的路径。例如:

1
2
3
4
WORKDIR /a
WORKDIR b
WORKDIR c
RUN pwd

则最终路径为/a/b/c

因此,为了避免出错,推荐WORKDIR指令中只使用绝对路径

ONBUILD

指定基于所生成镜像创建子镜像时,自动执行的操作指令

格式为ONBUILD [INSTRUCTION]

例如,使用如下的 Dockerfile 创建父镜像 ParentImage,指定 ONBUILD 指令:

1
2
3
4
5
# Dockerfile for ParentImage
[...]
ONBUILD ADD . /app/src
ONBUILD RUN /usr/local/bin/python-build --dir /app/src
[...]

使用 docker build 命令创建子镜像 ChildImage 时(FROM ParentImage),会首先执行 ParentImage 中配置的 ONBUILD 指令:

1
2
# Dockerfile for ChildImage
FROM ParentImage

等价于在 ChildImage 的 Dockerfile 中添加了如下指令:

1
2
3
4
# Automatically run the following when building ChildImage
ADD . /app/src
RUN /usr/lcoal/bin/python-build --dir app/src
...

由于 ONBUILD 指令时隐式执行的,推荐在使用它的镜像标签中进行标注,例如 ruby:2.1-onbuild

ONBUILD 指令在创建专门用于自动编译、检查等操作的基础镜像时,十分有用

STOPSIGNAL

指定所创建镜像启动的容器接收退出的信号值

1
STOPSIGNAL signal

HEALTHCHECK

配置所启动容器如何进行健康检查(如何判断健康与否),自 Docker 1.12 开始支持

格式有两种:

HEALTHCHECK [OPTIONS] CMD command: 根据所执行命令返回值是否为 0 来判断
HEALTHCHECK NONE: 禁止基础镜像中的健康检查

OPTION 支持如下参数:
-interval=DURATION(default: 30s): 过多久检查一次
-timeout=DURATION(default: 30s): 每次检查等待结果的超时
-retries=N(default: 3): 如果失败了,重试几次才最终确定失败

SHELL

指定其他命令使用 shell 时的默认 shell 类型

1
SHELL [ "executable", "parameters" ]

默认值为["/bin/sh", "-c"]

注意:
对于 Windows 系统,Shell 路径中使用了"\"作为分隔符,建议在 Dockerfile 开头添加# escape='来指定转义符

操作指令

RUN

运行指定命令。
格式为RUN <command>RUN ["executable", "param1", "param2"]
注意后者指令会被解析为 JSON 数组,因此必须用双引号。
前者默认将在 shell 终端中运行命令,即/bin/sh -c;后者则使用 exec 执行,不会启动 shell 环境。

指定使用其他终端类型可以通过第二种方式实现,例如RUN ["/bin/bash", "-c", "echo hello"]

每条 RUN 指令将在当前镜像基础上执行指定命令,并提交为新的镜像层。
当命令较长时可以使用\来换行。例如:

1
2
3
4
RUN apt-get update \
    && apt-get install -y libsnappy-dev zlib1gdev libbz2-ver \
    && rm -rf /var/cache/apt \
    && rm -rf /var/lib/apt/lists/*

CMD

CMD 指令用来指定启动容器时默认执行的命令。
支持三种格式:
CMD ["executable", "param1", "param2"]: 相当于执行 executable param1 param2,推荐方式;
CMD command param1 param2: 在默认的 Shell 中执行,提供给需要交互的应用
CMD ["param1", "param2"]: 提供给ENTRYPOINT的默认参数

每个 Dockerfile 只能有一条 CMD 命令。
如果指定了多条命令,只有最后一条会被执行

如果用户启动容器时候手动指定了运行的命令(作为 run 命令的参数),则会覆盖掉 CMD 指定的命令

ADD

添加内容到镜像
格式为ADD <src> <dest>
该命令将复制指定的<src>路径下内容到容器中的<dest>路径下。
其中<src>可以是 Dockerfile 所在目录的一个相对路径(文件或目录);也可以是一个 URL;还可以是一个 tar 文件(自动解压为目录)<dest>可以是镜像内绝对路径,或者相对于工作目录(WORKDIR)的相对路径

路径支持正则格式,例如:

1
ADD *.c /code/

COPY

复制内容到镜像
格式为COPY <src> <dest>
复制本地主机的<src>(为 Dockerfile 所在目录的相对路径,文件或目录)下内容到镜像中的<dest>
目标路径不存在时,会自动创建

路径同样支持正则格式。

COPY 与 ADD 指令功能类似,当使用本地目录为源目录时,推荐使用 COPY。

创建镜像

编写完成 Dockerfile 之后,可以通过docker [image] build命令来创建镜像。

基本的格式为docker build [OPTIONS] PATH|URL|-

该命令将读取指定路径下(包括子目录)的 Dockerfile,并将该路径下所有数据作为上下文(Context)发送给 Docker 服务端。
Docker 服务端在校验 Dockerfile 格式通过后,逐条执行其中定义的指令,碰到 ADD、COPY 和 RUN 指令会生成一层新的镜像。
最终如果创建镜像成功,会返回最终镜像的 ID

如果上下文过大,会导致发送大量数据给服务端,延缓创建过程。
因此除非是生成镜像所必需的文件,不然不要放到上下文路径下。
如果使用非上下文路径下的 Dockerfile,可以通过 -f 选项来指定其路径

要指定生成镜像的标签信息,可以通过 -t 选项。
该选项可以重复使用多次为镜像一次添加多个名称

例如,上下文路径为/tmp/docker_builder/,并且希望生成镜像标签为builder/birst_image:1.0.0,可以使用下面的命令:

1
docker build -t builder/first_image:1.0.0 /tmp/docker_builder/

命令选项

docker [image] build命令支持一系列的选项,可以调整创建镜像过程的行为,参见下表:

创建镜像的命令选项及说明

创建镜像的命令选项及说明

选择父镜像

大部分情况下,生成新的镜像都需要通过 FROM 指令来指定父镜像。
父镜像是生成镜像的基础,会直接影响到所生成镜像的大小和功能。

用户可以选择两种镜像作为父镜像,一种是所谓的基础镜像,另外一种是普通的镜像(往往由第三方创建,基于基础镜像)。

基础镜像比较特殊,其 Dockerfile 中往往不存在 FROM 指令,或者基于 scratch 镜像(FROM scratch),这意味着其在整个镜像树中处于根的位置。

下面的 Dockerfile 定义了一个简单的基础镜像,将用户提前编译好的二进制可执行文件 binary 复制到镜像中,运行容器时执行 binary 命令:

1
2
3
FROM scratch
ADD binary /
CMD ["/binary"]

普通镜像也可以作为父镜像来使用,包括常见的busyboxdebianubuntu

Docker 不同类型镜像之间的继承关系如图所示:

镜像的继承关系

使用 .dockerignore 文件

可以通过.dockerignore文件(每一行添加一条匹配模式)来让 Docker 忽略匹配路径或文件,在创建镜像时候不将无关数据发送到服务端

例如下面的例子中包括了 6 行忽略的模式(第一行为注释):

1
2
3
4
5
6
# .dockerignore 文件中可以定义忽略模式 */temp/*
*/*/tmp*
tmp?
~*
Dockerfile
!README.md

dockerignore 文件中模式语法支持 Golang 风格的路径正则格式:
"*"表示任意多个字符
"?"代表单个字符
"!"表示不匹配(即不忽略指定的路径或文件)

多步骤创建

自 17.05 版本开始,Docker 支持多步骤镜像创建特性,可以精简最终生成的镜像大小。

对于需要编译的应用(如 C、Go、Java 或 Rust 语言等)来说,通常情况下至少需要准备两个环境的 Docker 镜像:

编译环境镜像:包括完整的编译引擎、依赖库等,往往比较庞大。作用是编译应用为二进制文件;
运行环境镜像:利用编译好的二进制文件,运用应用,由于不需要编译环境,体积比较小。

使用多步创建,可以在保证最终生成的运行环境镜像保持精简的情况下,使用单一的 Dockerfile,降低维护复杂度。

以 Go 语言应用为例。创建干净目录,进入到目录中,创建main.go文件,内容为:

1
2
3
4
5
6
7
8
// main.go will output "Hello, Docker"
package main
import (
    "fmt"
)
func main() {
    fmt.Println("Hello, Docker")
}

创建 Dockerfile,使用golang:1.9镜像编译应用二进制文件为app,使用精简的镜像alpine:latest作为运行环境。
Dockerfile 完整内容为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# define stage name as builder
FROM golang:1.9 as builder
RUN mkdir -p /go/src/test
WORKDIR /go/src/test
COPY main.go .
RUN GGO_ENABLED=0 GOOS=linux go build -o app .
FROM alpine:latest
WORKDIR /root/
# copy file from the builder stage
COPY --from=builder /go/src/test/app .
CMD ["./app"]
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
docker build -t nocilantro/test-multistage:latest .
Sending build context to Docker daemon  3.072kB
Step 1/9 : FROM golang:1.9 as builder
 ---> ef89ef5c42a9
Step 2/9 : RUN mkdir -p /go/src/test
 ---> Using cache
 ---> c3952c70de25
Step 3/9 : WORKDIR /go/src/test
 ---> Using cache
 ---> c949a537ca43
Step 4/9 : COPY main.go .
 ---> Using cache
 ---> 9c7c54e7c70b
Step 5/9 : RUN GGO_ENABLED=0 GOOS=linux go build -o app .
 ---> Using cache
 ---> 4f572e1dbdce
Step 6/9 : FROM alpine:latest
 ---> a24bb4013296
Step 7/9 : WORKDIR /root/
 ---> Running in 4a208b274ddb
Removing intermediate container 4a208b274ddb
 ---> a4febfbd25c4
Step 8/9 : COPY --from=builder /go/src/test/app .
 ---> 2a3af37b46fe
Step 9/9 : CMD ["./app"]
 ---> Running in ccd354d56903
Removing intermediate container ccd354d56903
 ---> 2a4097957ec4
Successfully built 2a4097957ec4
Successfully tagged nocilantro/test-multistage:latest

docker run --rm nocilantro/test-multistage
Hello, Docker

查看生成的最终镜像,大小只有 7.43MB

1
2
docker images | grep test-multistage
nocilantro/test-multistage                         latest               2a4097957ec4        2 minutes ago       7.43MB

最佳实践

所谓最佳实践,就是从需求出发,来定制适合自己、高效方便的镜像

首先,要尽量吃透每个指令的含义和执行效果,多编写一些简单的例子进行测试,弄清楚了再撰写正式的 Dockerfile。
此外,Docker Hub 官方仓库中提供了大量的优秀镜像和对应的 Dockerfile,可以通过阅读它们来学习如何撰写高效的 Dockerfile。

建议在生成镜像过程中,尝试从如下角度进行思考,完善所生成镜像:

精简镜像用途
尽量让每个镜像的用途都比较集中单一,避免构造大而复杂、多功能的镜像

选用合适的基础镜像
容器的核心是应用。选择过大的父镜像(如 Ubuntu 系统镜像)会造成最终生成应用镜像的臃肿,推荐选用瘦身过的应用镜像(如node:slim),或者较为小巧的系统镜像(如alpine、busyboxdebian)

提供注释和维护者信息
Dockerfile 也是一种代码,需要考虑方便后续的扩展和他人的使用

正确使用版本号
使用明确的版本号信息,如 1.0,2.0,而非依赖于默认的latest。通过版本号可以避免环境不一致导致的问题;

减少镜像层数
如果希望所生成镜像的层数尽量少,则要尽量合并 RUN、ADD 和 COPY 指令。
通常情况下,多个 RUN 指令可以合并为一条 RUn 指令

恰当使用多步骤创建(17.05+版本支持):
通过多步骤创建,可以将编译和运行等过程分开,保证最终生成的镜像只包括运行应用所需要的最小化环境。
当然,用户也可以通过分别构造编译镜像和运行镜像来达到类似的结果,但这种方式需要维护多个 Dockerfile。

使用.dockerignore文件
使用它可以标记在执行 docker build 时忽略的路径和文件,避免发送不必要的数据内容,从而加快整个镜像创建过程。

及时删除临时文件和缓存文件
特别是在执行apt-get指令后,/var/cache/apt下面会缓存了一些安装包

提高生成速度
如合理使用 cache,减少内容目录下的文件,或使用.dockerignore文件指定等

调整合理的指令顺序
在开启 cache 的情况下,内容不变的指令尽量放在前面,这样可以尽量复用;

减少外部源的干扰
如果确实要从外部引入数据,需要指定持久的地址,并带版本信息等,让他人可以复用而不出错