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

编写 Dockerfilehttps://smoothies.com.cn/docker-docs/Docker/Dockerfile/

1. 基本结构

Dockerfile 由一行行命令语句组成,并且支持以 # 开头的注释行。一般而言,Dockerfile,分为四部分:

  • 基础镜像信息;
  • 维护者信息;
  • 镜像操作指令;
  • 和容器启动时执行指令;

如下示例

# This Dockerfile uses the ubuntu 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
# Maintainer: docker_user <docker_user at email.com> (@docker_user)
MAINTAINER docker_user docker_user@email.com
# Commands to update the image
RUN echo "deb http://archive.ubuntu.com/ubuntu/ raring 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

其中,一开始必须指明所基于的镜像名称,接下来一般是说明维护者信息。后面则是镜像操作指令,例如 RUN 指令,RUN 指令将对镜像执行跟随的命令。每运行一条 RUN 指令,镜像就添加新的一层,并提交。最后是 CMD 指令,用来指定运行容器时的操作命令。

2. 指令说明

指令说明
FROM指定所创建镜像的基础镜像
MAINTAINER指定维护者信息
RUN运行命令
CMD指定容器启动时默认执行的命令
LABEL指定生成镜像的元数据标签信息
EXPOSE声明镜像内服务所监听的端口
ENV指定环境变量
ADD复制指定的 路径下的内容到容器中的 路径下, 可以为 URL;如果为 tar 文件,会自动解压到 路径下
COPY复制本地主机的 路径下的内容到镜像中的 路径下;一般情况下推荐使用 COPY 而不是 ADD
ENTRYPOINT指定镜像的默认入口
VOLUME创建数据卷挂载点
WORKDIR配置工作目录
ARG指定镜像内使用的参数 (例如版本号信息等)
ONBUILD配置当所创建的镜像作为其它镜像的基础镜像时,所执行的创建操作指令
STOPSIGNAL容器退出的信号值
HEALTHCHECK如何进行健康检查
SHELL指定使用 shell 时的默认 shell 类型

下面具体介绍各个指令

2.1 FROM

指定所创建镜像的基础镜像,如果本地不存在,则默认会去 Docker Hub 下载指定镜像。格式为

FROM <image> 
FROM <image>:<tag>
FROM <image>@<digest>

FROM <image> [AS <name>]
FROM <image>[:<tag>] [AS <name>]
FROM <image>[@<digest>] [AS <name>]

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

Dockerfile 中可以多次出现 FROM 指令,当 FROM 第二次或者之后出现时,表示在此刻构建时,要将当前指出镜像的内容合并到此刻构建镜像的内容里。这对于我们直接合并两个镜像的功能很有帮助。

2.2 MAINTAINER

指定维护者信息,格式为

MAINTAINER <name> <email>

例如:

MAINTAINER image_creator@docker.com

该信息会写入生成镜像的 Author 属性域中。

2.3 RUN

RUN 指令在新镜像内部执行的命令,如:执行某些动作、安装系统软件、配置系统信息之类。格式为

RUN <command> 
或 
RUN ["executable","param1","param2"]

注意,后一个指令会被解析为 Json 数组,因此必须用双引号。前者默认将在 shell 终端中运行命令,即 /bin/sh -c ;后者则使用 exec 执行,不会启动 shell 环境。

指定使用其他终端类型可以通过第二种方式实现,例如

RUN ["/bin/bash","-c","echo hello"]

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

RUN apt-get update \
 && apt-get install -y libsnappy-dev zlib1g-dev libbz2-dev \
 && rm -rf /var/cache/apt

注:多行命令不要写多个 RUN ,原因是 Dockerfile 中每一个指令都会建立一层,多少个 RUN 就构建了多少层镜像,会造成镜像的臃肿、多层,不仅仅增加了构件部署的时间,还容易出错。

2.4 CMD

CMD 指令用来指定启动容器时默认执行的命令。它支持三种格式:

  • CMD["executable","param1","param2"] 使用 exec 执行,是推荐使用的方式;
    * CMD command param1 param2/bin/sh 中执行,提供给需要交互的应用;
    * CMD["param1","param2"] 提供给 ENTRYPOINT 的默认参数。

每个 Dockerfile 只能有一条 CMD 命令。如果指定了多条命令,只有最后一条会被执行。如果用户启动容器时手动指定了运行的命令(作为 run 的参数),则会覆盖掉 CMD 指定的命令。

如容器启动时进入 bash

CMD /bin/bash

或者可以用 exec 写法

CMD ["/bin/bash"]

ENTRYPOINTCMD 同时给出时,CMD 中的内容会作为 ENTRYPOINT 定义命令的参数,最终执行容器启动的还是 ENTRYPOINT 中给出的命令。

2.5 LABEL

LABEL 指令用来指定生成镜像的元数据标签信息。格式为:

LABEL <key>=<value><key>=<value><key>=<value>...

例如:

LABEL version="1.0"
LABEL description="This text illustrates \ that label-values can span multiple lines."

2.6 EXPOSE

声明镜像内服务所监听的端口。EXPOSE 命名适用于设置容器对外映射的容器端口号,格式为

EXPOSE <port>[<port>...]

例如:

EXPOSE 22 80 8443

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

在启动容器时需要使用 -PDocker 主机会自动分配一个宿主机的临时端口转发到指定的端口;使用 -p,则可以具体指定哪个宿主机的本地端口会映射过来。

Tomcat 容器内使用的端口 8081,则用 EXPOSE 命令可以告诉外界该容器的 8081 端口对外,在构建镜像时用 Docker run -p 可以设置暴露的端口对宿主机器端口的映射。

EXPOSE 8081

EXPOSE 8081 其实等价于 Docker run -p 8081 当需要把 8081 端口映射到宿主机中的某个端口(如8888)以便外界访问时,则可以用 Docker run -p 8888:8081

2.7 ENV

指定环境变量,在镜像生成过程中会被后续 RUN 指令使用,在镜像启动的容器中也会存在。ENV 命名用于设置容器的环境变量,这些变量以 key=value 的形式存在,在容器内被脚本或者程序调用,容器运行的时候这个变量也会保留。格式为

ENV <key>  <value>
或
ENV <key>=<value>...

例如:

ENV PG_MAJOR 9.3
ENV PG_VERSION 9.3.4
RUN curl -SL http://example.com/postgres-$PG_VERSION.tar.xz | tar -xJC /usr/src/
postgress && ...
ENV PATH /usr/local/postgres-$PG_MAJOR/bin:$PATH

指令指定的环境变量在运行时可以被覆盖掉,如

docker run --env <key>=<value> built_image

在使用 ENV 设置环境变量时,有几点需要注意:

  • 具有传递性,也就是当前镜像被用作其它镜像的基础镜像时,新镜像会拥有当前这个基础镜像所有的环境变量;

  • ENV 定义的环境变量,可以在 Dockerfile 被后面的所有指令( CMD 除外)中使用,但不能被 Docker run 的命令参数引用 。 如:

ENV Tomcat_home_name Tomcat_7
RUN mkdir $Tomcat_home_name
  • 由于环境变量在容器运行时依然有效,所以运行容器时我们还可以对其进行覆盖,在创建容器时使用 -e 或是 --env 选项,可以对环境变量的值进行修改或定义新的环境变量。除了 ENV 之外,docker run -e 也可以设置环境变量传入容器内。
docker run -d Tomcat -e "Tomcat_home_name=Tomcat_7"

这样我们进入容器内部用 ENV 可以看到 Tomcat_home_name 这个环境变量。

通过 ENV 指令和 ARG 指令所定义的参数,在使用时都是采用 $ + NAME 这种形式来占位的,所以它们之间的定义就存在冲突的可能性。对于这种场景,大家只需要记住,ENV 指令所定义的变量,永远会覆盖 ARG 所定义的变量,即使它们定时的顺序是相反的。

与参数变量 ARG 只能影响构建过程不同,环境变量不仅能够影响构建,还能够影响基于此镜像创建的容器

  • 环境变量设置的实质,其实就是定义操作系统环境变量,所以在运行的容器里,一样拥有这些变量,而容器中运行的程序也能够得到这些变量的值。
  • 另一个不同点是,环境变量的值不是在构建指令中传入的,而是在 Dockerfile 中编写的,所以如果我们要修改环境变量的值,我们需要到 Dockerfile 修改。不过即使这样,只要我们将 ENV 定义放在 Dockerfile 前部容易查找的地方,其依然可以很快的帮助我们切换镜像环境中的一些内容。

2.8 ADD

作用和使用方法和 COPY 一样。该命令将复制指定的 src 路径下的内容到容器中的 dest 路径下。格式为

ADD <src> <dest>

其中

  • src 可以是 Dockerfile 所在目录的一个相对路径(文件或目录),也可以是一个 URL ,还可以是一个 tar 文件(如果为 tar 文件,会自动解压到 dest 路径下)。
  • dest 可以是镜像内的绝对路径,或者相对于工作目录( WORKDIR )的相对路径。路径支持正则格式,例如:
ADD *.c /code/

2.9 COPY

COPY 命令用于将宿主机器上的的文件复制到镜像内,如果目的位置不存在,Docker 会自动创建。但宿主机器用要复制的目录必须是和 Dockerfile 文件同级目录下。 格式为

COPY [--chown=<user>:<group>] <源路径>... <目标路径>
COPY [--chown=<user>:<group>] ["<源路径1>",... "<目标路径>"]

复制本地主机的 src (为 Dockerfile 所在目录的相对路径、文件或目录)下的内容到镜像中的 dest 下。目标路径不存在时,会自动创建。路径同样支持正则格式。当使用本地目录为源目录时,推荐使用COPY

COPY 命令和 ADD 类似,唯一的不同是 ADD 会自动解压压缩包,还可以直接下载 url 中的文件但是官方建义使用 wget 或者 curl 代替 ADD

# 拷贝并解压
ADD nickdir.tar.gz .
# 仅拷贝
ADD https://example.com/big.tar.xz /usr/src/things/
RUN tar -xJf /usr/src/things/big.tar.xz -C /usr/src/things

应该改成这样子

RUN mkdir -p /usr/src/things \
&& curl -SL https://example.com/big.tar.xz \
| tar -xJC /usr/src/things \
&& make -C /usr/src/things all

2.10 ENTRYPOINT

指定镜像的默认入口命令,该入口命令会在启动容器时作为根命令执行,所有传入值作为该命令的参数。支持两种格式:

ENTRYPOINT ["executable", "param1", "param2"](exec调用执行)
ENTRYPOINT command param1 param2(shell中执行)

此时,CMD 指令指定值将作为根命令的参数。每个 Dockerfile 中只能有一个 ENTRYPOINT,当指定多个时,只有最后一个有效。在运行时,可以被 --entrypoint 参数覆盖掉,如 docker run--entrypoint

ENTRYPOINT 的作用和用法和 CMD 一模一样,但是 ENTRYPOINT 有和 CMD 有 2 处不一样:

  • CMD 的命令会被 Docker run 的命令覆盖而 ENTRYPOINT 不会;
  • ENTRYPOINT 指令的优先级高于 CMD 指令。CMDENTRYPOINT 都存在时,CMD 的指令变成了 ENTRYPOINT 的参数,两者拼接之后,才是最终执行的命令。并且此 CMD 提供的参数会被 Docker run 后面的命令覆盖;

CMDENTRYPOINT 命令相同点:

  1. 为启动的容器指定默认要运行的程序,程序运行结束,容器也就结束,所以如果想要容器长期运行就让这个命令指定的命令长期运行。
  2. CMD 指令,仅最后一个生效。

CMDENTRYPOINT 命令不同点:

  1. CMD 指令指定的程序可被 docker run 命令行参数中指定要运行的程序所覆盖,但 ENTRYPOINT 不会。

命令加参数的形式

ENTRYPOINT [ "echo", "a" ]
$ docker run  test
a

加参数,但是不会替换

ENTRYPOINT [ "echo", "a" ]
$ docker run  test b
a b

CMDENTRYPOINT 提供默认参数

ENTRYPOINT [ "echo", "a" ]
CMD ["b"]
$ docker run  test
a b

加参数 c 会替换 CMD 提供的参数

ENTRYPOINT [ "echo", "a" ]
CMD ["b"]
$ docker run  test c
a c

ENTRYPOINTCMD 的组合示例:
关系
有的读者会存在疑问,既然两者都是用来定义容器启动命令的,为什么还要分成两个,合并为一个指令岂不是更方便吗?

这其实在于 ENTRYPOINTCMD 设计的目的是不同的。

  • ENTRYPOINT 指令主要用于对容器进行一些初始化;
  • CMD 指令则用于真正定义容器中主程序的启动命令;

另外,我们之前谈到创建容器时可以改写容器主程序的启动命令,而这个覆盖只会覆盖 CMD 中定义的内容,而不会影响 ENTRYPOINT 中的内容。

我们依然以之前的 Redis 镜像为例,这是 Redis 镜像中对 ENTRYPOINTCMD 的定义。

## ......

COPY docker-entrypoint.sh /usr/local/bin/

ENTRYPOINT ["docker-entrypoint.sh"]

## ......

CMD ["redis-server"]

可以很清晰的看到,CMD 指令定义的正是启动 Redis 的服务程序,而 ENTRYPOINT 使用的是一个外部引入的脚本文件。

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

我们来看看 Redis 中的 ENTRYPOINT 脚本,可以看到其中会根据脚本参数进行一些处理,而脚本的参数,其实就是 CMD 中定义的内容。

#!/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 里的命令。

所以说,虽然 Docker 对容器启动命令的结合机制为 CMD 作为 ENTRYPOINT 的参数,合并后执行 ENTRYPOINT 中的定义,但实际在我们使用中,我们还会在 ENTRYPOINT 的脚本里代理到 CMD 命令上。

2.11 VOLUME

创建一个数据卷挂载点。格式为

VOLUME ["/data"]

可以从本地主机或其他容器挂载数据卷,一般用来存放数据库和需要保存的数据等。VOLUME 用来创建一个可以从本地主机或其他容器挂载的挂载点。

但使用数据卷需要我们在创建容器时通过 -v 选项来定义,而有时候由于镜像的使用者对镜像了解程度不高,会漏掉数据卷的创建,从而引起不必要的麻烦。

VOLUME 指令中定义的目录,在基于新镜像创建容器时,会自动建立为数据卷,不需要我们再单独使用 -v 选项来配置了。

例如我们知道 TomcatWebapps 目录是放 Web 应用程序代码的地方,此时我们要把 Webapps 目录挂载为匿名卷,这样任何写入 Webapps 中的心都不会被记录到容器的存储层,让容器存储层无状态化。

如创建 TomcatWebapps 目录的一个挂载点

 VOLUME /usr/local/Tomcat/Webapps

这样,在运行容器时,也可以用过 Docker run -v 来把匿名挂载点挂载都宿主机器上的某个目录,如

docker run -d -v /home/Tomcat_Webapps:/usr/local/Tomcat/Webapps

2.12 USER

指定运行容器时的用户名或UID,后续的RUN等指令也会使用指定的用户身份。需要注意的是这个用户必须是已经存在,否则无法指定。格式为

USER daemon

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

RUN groupadd -r postgres && useradd -r -g postgres postgres

要临时获取管理员权限可以使用 gosu 或 sudo 。

USER 指令和 WORKDIR 相似,都是改变环境状态并影响以后的层。 WORKDIR 是改变工作目录, USER 则是改变之后层的执行 RUN , CMD 以及 ENTRYPOINT 这类命令的身份。 注意, USER 只是帮助你切换到指定用户而已,这个用户必须是事先建立好的,否则无法切换。

RUN groupadd -r redis && useradd -r -g redis redis
USER redis
RUN [ "redis-server" ]

如果以 root 执行的脚本,在执行期间希望改变身份,比如希望以某个已经建立好的用户来运行某个服务进程,不要使用 su 或者 sudo,这些都需要比较麻烦的配置,而且在 TTY 缺失的环境下经常出错。建议使用 gosu 。

# 建立 redis 用户,并使用 gosu 换另一个用户执行命令
RUN groupadd -r redis && useradd -r -g redis redis
# 下载 gosu
RUN wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/1.12/gosu-amd64" \
 && chmod +x /usr/local/bin/gosu \
 && gosu nobody true
# 设置 CMD,并以另外的用户执行
CMD [ "exec", "gosu", "redis", "redis-server" ]

2.13 WORKDIR

为后续的 RUNCMDENTRYPOINT 指令配置工作目录。其效果类似于 Linux 命名中的 cd 命令,用于目录的切换,但是和 cd 不一样的是:如果切换到的目录不存在,WORKDIR 会为此创建目录。格式为

WORKDIR /path/to/workdir

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

WORKDIR /a
WORKDIR b
WORKDIR c
RUN pwd

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

2.14 ARG

指定一些镜像内使用的参数(例如版本号信息等),这些参数在执行 docker build 命令时才以 --build-arg <varname>=<value> 格式传入。格式为

ARG  <name> [=<default value>]

示例

docker build --build-arg <name>=<value> . 

来指定参数值。

示例,Dockefile 文件内容

FROM debian:stretch-slim

## ......

ARG TOMCAT_MAJOR
ARG TOMCAT_VERSION

## ......

RUN wget -O tomcat.tar.gz "https://www.apache.org/dyn/closer.cgi?action=download&filename=tomcat/tomcat-$TOMCAT_MAJOR/v$TOMCAT_VERSION/bin/apache-tomcat-$TOMCAT_VERSION.tar.gz"

## ......

构建命令

$ sudo docker build --build-arg TOMCAT_MAJOR=8 --build-arg TOMCAT_VERSION=8.0.53 -t tomcat:8.0 ./tomcat

2.15 ONBUILD

配置当所创建的镜像作为其他镜像的基础镜像时,所执行的创建操作指令。意思就是:这个镜像创建后,如果其它镜像以这个镜像为基础,会先执行这个镜像的 ONBUILD 命令。格式为

ONBUILD [INSTRUCTION]

例如,Dockerfile 使用如下的内容创建了镜像 image-A:

[...]
ONBUILD ADD . /app/src
ONBUILD RUN /usr/local/bin/python-build --dir /app/src
[...]

如果基于 image-A 创建新的镜像时,新的 Dockerfile 中使用 FROM image-A 指定基础镜像,会自动执行 ONBUILD 指令的内容,等价于在后面添加了两条指令:

FROM image-A
#Automatically run the following
ADD . /app/src
RUN /usr/local/bin/python-build --dir /app/src

使用ONBUILD指令的镜像,推荐在标签中注明,例如ruby: 1.9-onbuild。

2.16 STOPSIGNAL

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

STOPSIGNAL signal

2.17 HEALTHCHECK

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

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

OPTION支持:

  • interval=DURATION(默认为:30s):过多久检查一次;
  • timeout=DURATION(默认为:30s):每次检查等待结果的超时;
  • retries=N(默认为:3):如果失败了,重试几次才最终确定失败。

2.18 SHELL

指定其他命令使用 shell 时的默认 shell 类型。默认值为 ["/bin/sh","-c"] 。注意对 于 Windows 系统,建议在 Dockerfile 开头添加 #escape=` 来指定转义信息。

3. 创建镜像

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

基本的格式为

docker build [OPTIONS] PATH | URL | -

OPTIONS 有很多指令,下面列举几个常用的:

  • --build-arg=[] :设置镜像创建时的变量;
  • -f :指定要使用的 Dockerfile 路径;
  • --force-rm :设置镜像过程中删除中间容器;
  • --rm :设置镜像成功后删除中间容器;
  • --tag, -t: 镜像的名字及标签,通常 name:tag 或者 name 格式。

docker build 可以接收一个参数,需要特别注意的是,这个参数为一个目录路径 ( 本地路径或 URL 路径 ),而并非 Dockerfile 文件的路径。在 docker build 里,这个我们给出的目录会作为构建的环境目录,我们很多的操作都是基于这个目录进行的。

例如,在我们使用 COPY 或是 ADD 拷贝文件到构建的新镜像时,会以这个目录作为基础目录。

该命令将读取指定路径下(包括子目录)的 Dockerfile,并将该路径下的所有内容发送给 Docker 服务端,由服务端来创建镜像。因此除非生成镜像需要,否则一般建议放置 Dockerfile 的目录为空目录。

有两点经验:

  • 如果使用非内容路径下的 Dockerfile,可以通过 -f 选项来指向文件系统中任何位置的 Dockerfile
docker build -f /path/to/a/Dockerfile .
  • 要指定生成镜像的标签信息,可以使用 -t 选项。例如,指定 Dockerfile 所在路径为 /tmp/docker_builder/,并且希望生成镜像标签为 build_repo:first_image,可以使用下面的命令:
$ docker build -t build_repo:first_image /tmp/docker_builder/
$ docker build -t webapp:latest ./webapp

执行命名之后,会看到控制台逐层输出构建内容,直到输出两个 Successfully 即为构建成功。

4.使用.dockerignore文件

可以通过 .dockerignore 文件(每一行添加一条匹配模式)来让 Docker 忽略匹配模式路径下的目录和文件。例如:

# comment
*/temp*
*/*/temp*
tmp?

其中:

  • * 表示任意多个字符
  • ? 表示单个字符
  • ! 表示不匹配(即不忽略指定的路径或文件)

5. 镜像最佳实践

  • 精简镜像用途:尽量让每个镜像的用途都比较集中单一,避免构造大而复杂,多功能的镜像;
  • 选用合适的基础镜像:容器的核心是应用,选择过大的父镜像(如 Ubuntu 系统镜像)会造成生成应用镜像臃肿,推荐选用较为小巧的系统镜像(如 alpinebusyboxdebian);
  • 提供注释和维护者的信息;
  • 正确的使用版本号:可以避免环境不一致的问题;
  • 减少镜像层数:尽量合并 RUNADDCOPY 等指令;
  • 使用 .dockerignore 文件;
  • 及时删除临时文件和缓存文件:特别是在执行 apt-get 指令后,/var/cache/apt 下面会缓存了一些安装包;
  • 减少外部源的干扰:如果确实要从外部引入数据,则需要指定持久的地址,并带有版本信息等,让他人可以复用而不报错;
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐