【Docker学习】3.Dockerfile


1 前言

前两章,我们大致介绍了Docker的基础操作和常见命令,实际生产开发过程中,企业或组织都有自己的一套产品和应用,类似以前Windows出现过很多定制化的系统,如Ghost xxxx,深度等。那对于Docker构建的镜像,是怎么实现定制化或个性化或一致性环境的镜像呢?本章将给予你答案。

2 命令

Dockerfile 由一行行命令语句组成,并且支持以 # 开头的注释行。使用docker build即可执行脚本构建镜像,自动地去做一些事(同类似于travis-ci中的.travis.yml)。

2.1 格式

Dockerfile 的格式统一为:

# Comment
INSTRUCTION arguments

注意:必须以FROM BASE_IMAGE开头指定基础镜像!!!除开解析器指令注释具有全局作用域的ARG

2.2 语法

Dockerfile 基本的语法如下:

  • 使用 # 来注释
  • MAINTAINER:镜像作者信息,新版本已废弃。推荐使用LABEL指令;
  • LABEL:添加镜像的元数据,使用键值对的形式;
  • FROM:指令告诉 Docker 使用哪个镜像作为基础;
  • RUN:开头的指令会在创建中运行,比如安装一个软件包,在这里使用 yum 来安装了一些软件;
  • COPY:从 Docker 宿主机复制文件至创建的新镜像文件;
  • ADD:类似于 COPY 指令,ADD 支持 tar 文件和 URL 路径;
  • WORKDIR:用于为 Dockerfile 中所有的 RUN、CMD、ENTRYPOINT、COPY、ADD 指定设定工作目录;
  • VOLUME:数据卷,用于在 image 中创建挂载点目录,以挂载 Docker host 上的卷或者其他容器上的卷;
  • EXPOSE:为容器打开指定的监听端口以实现与外部通信;
  • ENV:用于为镜像定义所需的环境变量,可以被 Dockerfile 文件中其他命令调用(ENV、ADD、COPY、RUN、CMD);
  • ARG:定义在构建过程中传递给构建器的变量,可使用docker build命令设置;
  • CMD:启动容器指定默认要运行的程序或命令,默认”/bin/sh -c”运行;
  • ENTRYPOINT:类型 CMD 指令的功能,用于为容器指定默认运行程序或命令;
  • USER:指定当前用户。

下面介绍几个常用的命令,其他命令的具体用法,可参考官方文档,见文章末尾参考文献。

2.2.1 FROM

指定基础镜像。

2.2.1.1 格式

FROM BASE_IMAGE
# 或者
FROM BASE_IMAGE:Tag

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

2.2.1.2 举例

FROM centos:7

2.2.2 RUN

构建镜像时执行的命令。

2.2.2.1 格式

  • shell 格式:

    #shell 格式:
    RUN <命令行命令>
    # <命令行命令> 等同于,在终端操作的 shell 命令。
    # 例如:
    # RUN apt-get update
    # RUN apt-get install -y curl
  • exec 格式:

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

shell格式是最常用的,它允许您更轻松地将较长的指令分解为多行,可以使用换行符转义符或使用heredocs,例如:
RUN <<EOF
apt-get update
apt-get install -y curl
EOF

注意:Dockerfile 的指令每执行一次都会在 docker 上新建一层。所以过多无意义的层,会造成镜像膨胀过大。例如:
FROM centos
RUN yum -y 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 -y install wget
&& wget -O redis.tar.gz “http://download.redis.io/releases/redis-5.0.3.tar.gz
&& tar -xvf redis.tar.gz
如上,以 && 符号连接命令,这样执行后,只会创建 1 层镜像。

2.2.3 ENV

设置环境变量,定义了环境变量,那么在后续的指令中,就可以使用这个环境变量。

2.2.3.1 格式

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

2.2.3.2 举例

以下示例设置NODE_VERSION = 7.2.0, 在后续的指令中可以通过$NODE_VERSION引用:

ENV NODE_VERSION 7.2.0

RUN curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.xz" \
&& curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc"

2.2.4 ARG

构建参数,与 ENV 作用一致。不过作用域不一样。ARG 设置的环境变量仅对 Dockerfile 内有效,也就是说只有docker build的过程中有效,构建好的镜像内不存在此环境变量。也称为构建时变量

构建命令docker build中可以用--build-arg <参数名>=<值>来覆盖。

2.2.4.1 格式

ARG <参数名>[=<默认值>]

如果在Dockerfile中期望使用各种ARG变量(无默认值),但在运行build命令时未提供任何变量,则会出现错误消息。
[Warning] One or more build-args [foo] were not consumed.

但是,在构建镜像后,可以通过查看镜像docker history来轻松检查ARG值。因此,对于敏感数据而言,它们是一个糟糕的选择。

2.2.4.2 举例

ARG some_variable_name
# or with a hard-coded default:
#ARG some_variable_name=default_value

RUN echo "Oh dang look at that $some_variable_name"
# you could also use braces - ${some_variable_name}

从命令行构建Docker镜像时,可以使用--build-arg来设置ARG值:

$ docker build --build-arg some_variable_name=a_value

使用上述Dockerfile运行该命令将导致打印以下行(以及其他内容):

Oh dang look at that a_value

更多关于ARG的用法和注意事项,参考ARG官方文档

ARG与ENV作用范围

ARG与ENV可用性与重写样例如下图所示:

ARG与ENV可用性概览和样例

2.2.5 CMD

构建镜像后调用,也就是在容器启动时才进行调用。

类似于 RUN 指令,用于运行程序,但二者运行的时间点不同:

  • CMD 在docker run时运行。
  • RUN 是在docker build

2.2.5.1 格式

CMD <shell 命令> 
CMD ["<可执行文件或命令>","<param1>","<param2>",...] 
CMD ["<param1>","<param2>",...]  # 该写法是为 ENTRYPOINT 指令指定的程序提供默认参数

推荐使用第二种格式,执行过程比较明确。第一种格式实际上在运行的过程中也会自动转换成第二种格式运行,并且默认可执行文件是 sh。

2.2.5.2 举例

CMD echo "This is a test." | wc -l
CMD ["/usr/bin/wc","--help"]

注意:如果 Dockerfile 中如果存在多个 CMD 指令,仅最后一个生效。

2.2.6 EXPOSE

指定于外界交互的端口

2.2.6.1 格式

# 格式:
EXPOSE <port> [<port>...]

2.2.6.2 举例

EXPOSE 80 443
EXPOSE 8080    
EXPOSE 11211/tcp 11214/udp

注:
EXPOSE并不会让容器的端口访问到主机。要使其可访问,需要在docker run运行容器时通过-p来发布这些端口,或通过-P参数来发布EXPOSE指定的所有端口

如果没有暴露端口,后期也可以通过-p 8080:80方式映射端口,但是不能通过-P形式映射.

2.2.7 ADD

将本地文件添加到容器中,tar类型文件会自动解压(网络压缩资源不会被解压),可以访问网络资源,类似wget

2.2.7.1 格式

ADD <src>... <dest>
ADD ["<src>",... "<dest>"] 用于支持包含空格的路径

2.2.7.2 举例

# 添加所有以"hom"开头的文件
ADD hom* /mydir/

# ? 替代一个单字符,例如:"home.txt"
ADD hom?.txt /mydir/

# 添加 "test" 到 `WORKDIR`/relativeDir/
ADD test relativeDir/

# 添加 "test" 到 /absoluteDir/
ADD test /absoluteDir/

2.2.8 COPY

类似ADD,但是是不会自动解压文件,也不能访问网络资源。

2.2.8.1 格式

# <dest> 是绝对路径,或相对于 WORKDIR 的路径
COPY [--chown=<user>:<group>] [--chmod=<perms>] <src>... <dest>
# 这种形式对于包含空格的路径是必需的
COPY [--chown=<user>:<group>] [--chmod=<perms>] ["<src>",... "<dest>"]

注意:--chown--chmod功能仅在用于生成 Linux 容器的Dockerfile上受支持,在 Windows 容器上不起作用。

2.2.8.2 举例

# 要添加所有以“hom”开头的文件:
COPY hom* /mydir/

# ?替换为任何单个字符,例如“home.txt”
COPY hom?.txt /mydir/

# 下面的示例使用相对路径,并将“test.txt”添加到 <WORKDIR>/relativeDir/ 
COPY test.txt relativeDir/

# 此示例使用绝对路径,并将“test.txt”添加到 /absoluteDir/
COPY test.txt /absoluteDir/

2.2.9 VOLUME

指定持久化目录(指定此目录可以被挂载出去)。数据卷的使用,类似于 Linux 下对目录或文件进行mount

2.2.9.1 格式

VOLUME ["/path/to/to"]

2.2.9.2 举例

VOLUME ["/data"]
VOLUME ["/var/www", "/var/log/apache2", "/etc/apache/user"]

注:一个卷可以存在于一个或多个容器的指定目录,该目录可以绕过联合文件系统,并具有以下功能:

  • 卷可以容器间共享和重用
  • 容器并不一定要和其它容器共享卷
  • 修改卷后会立即生效
  • 对卷的修改不会镜像产生影响
  • 卷会一直存在,直到没有任何容器在使用它

2.2.10 WORKDIR

工作目录,类似于cd命令

2.2.10.1 格式

WORKDIR /path/to/workdir

2.2.10.2 举例

# 这时工作目录为/a
WORKDIR /a
# 这时工作目录为/a/b
WORKDIR b
# 这时工作目录为/a/b/c
WORKDIR c

也就是说,如果有多个WORKDIR,那最终的路径就是从上到下,如:/a/b/c,类似这样。

注:

  • 通过WORKDIR设置工作目录后,Dockerfile中其后的命令RUNCMDENTRYPOINTADDCOPY等命令都会在该目录下执行。
  • 在使用docker run运行容器时,可以通过-w参数覆盖构建时所设置的工作目录。

2.2.11 ENTRYPOINT

配置容器,使其可执行化。

2.2.11.1 格式

# 可执行文件, 优先
ENTRYPOINT ["executable", "param1", "param2"]
# shell内部命令
ENTRYPOINT command param1 param2

2.2.11.2 举例

FROM centos:7
ENTRYPOINT ["ls", "/usr/local"]
CMD ["/usr/local/tomcat"]

之后,docker run传递的参数,都会先覆盖cmd,然后由cmd传递给entrypoint ,做到灵活应用。

注:ENTRYPOINTCMD非常类似,不同的是通过docker run执行的命令不会覆盖ENTRYPOINT,而docker run命令中指定的任何参数,都会被当做参数再次传递给CMD

Dockerfile中只允许有一个ENTRYPOINT命令,多个指定时会覆盖前面的设置,而只执行最后的ENTRYPOINT指令。
通常情况下,ENTRYPOINTCMD一起使用,ENTRYPOINT写默认命令,当需要参数时候使用CMD传参。

3 实践

3.1 dockerfile编写

以我自己开发的聊天软件中的一个项目为例,实质上为一个Spring Cloud Gateway网关。以下为其dockerfile:

# 拉取JDK
FROM openjdk:8-jdk-alpine
# 挂在/tmp卷下
VOLUME /tmp
# 将taeyeonim-gateway.jar添加进容器
ADD taeyeonim-gateway.jar taeyeonim-gateway.jar
# java -jar启动
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/taeyeonim-gateway.jar"]

这里ADD的时候,很容易出错,因为一般dockerfile在项目中不会与源文件,如*.jar放在一起,所以容易出现Dockerfile ADD failed : No Source files were specified,比如下面这种错误:

Deploying '<unknown> Dockerfile: taeyeonim-gateway/src/main/docker/dockerfile'…
Building image…
Preparing build context archive…
[==================================================>]1/1 files
Done

Sending build context to Docker daemon…
[==================================================>]  288.0B
Done

Step 1/4 : FROM openjdk:8-jdk-alpine
 ---> a3562aa0b991
Step 2/4 : VOLUME /tmp
 ---> Using cache
 ---> 0b0f08654eb9
Step 3/4 : ADD taeyeonim-gateway.jar taeyeonim-gateway.jar
Error response from daemon: ADD failed: file not found in build context or excluded by .dockerignore: stat taeyeonim-gateway.jar: file does not exist
Failed to deploy '<unknown> Dockerfile: taeyeonim-gateway/src/main/docker/dockerfile': Can't retrieve image ID from build stream

即使配置全路径也不能解决。为啥呢?

原因呢很简单,ADD 命令用来从context上下文复制新文件、目录或远程文件url,并将它们添加到位于指定路径的映像文件系统中。所以如果dockerfile和源文件在同一个目录下就不会出现上述错误。

在IDEA中,对于两文件分别在不同目录的情况,可以通过指定context folder,使其指向target解决此问题。如下图所示(IDEA版本:IntelliJ IDEA 2021.3 (Ultimate Edition)):

指定 jar 包的路径

3.2 构建镜像

上图中,我们可以看到已经配置了镜像的Tag,则构建镜像的命令行形式如下:

docker build -f dockerfile -t im-gateway:v2 .
  • -t:用于指定镜像的名称为im-gateway:v2;
  • .:表示使用当前目录下的Dockerfile进行构建。

名称可以以name:version的形式指定,nameversion之间用冒号分隔。如果没有指定版本,则默认为latest

构建日志如下所示:

Deploying 'im-gateway:v2 Dockerfile: taeyeonim-gateway/src/main/docker/dockerfile'…
Building image…
Preparing build context archive…
[==================================================>]33/33 files
Done

Sending build context to Docker daemon…
[==================================================>] 82.20MB
Done

Step 1/4 : FROM openjdk:8-jdk-alpine
 ---> a3562aa0b991
Step 2/4 : VOLUME /tmp
 ---> Using cache
 ---> 3c8fa2e5827c
Step 3/4 : ADD taeyeonim-gateway.jar taeyeonim-gateway.jar
 ---> Using cache
 ---> cb2f85399674
Step 4/4 : ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/taeyeonim-gateway.jar"]
 ---> Using cache
 ---> 655e3800a65c

Successfully built 655e3800a65c
Successfully tagged im-gateway:v2
'im-gateway:v2 Dockerfile: taeyeonim-gateway/src/main/docker/dockerfile' has been deployed successfully.

通过docker客户端或者IDEA自带的工具都可以看到构建镜像成功后的镜像列表。

docker客户端镜像列表

IDEA镜像列表:

IDEA docker镜像列表

3.3 创建容器

两种方式创建容器,一种是通过docker客户端进行创建,一种是通过IDEA创建。

3.3.1 docker client方式

找到image列表中之前构建的Gateway镜像,点击Run按钮,弹出菜单。

docker客户端方式创建容器

填写好容器名称,这里我命名为Im-Gw

docker客户端方式创建容器-配置

点击Run,开始启动容器,成功后可以在容器列表看到。

docker客户端方式创建容器-启动

3.3.2 IDEA方式

找到IDEA services窗口,查看image列表,找到之前构建的镜像im-gateway,右键弹出菜单,选中create container

IDEA方式创建容器

填写容器名称,这里为IM-Gateway. 点击run运行容器。

IDEA方式创建容器-配置

同样可以在docker客户端看到启动成功后的docker,如3.3.1节中的容器列表图,也可以在IDEA中查看,只是没有那么好看。哈哈哈哈

IDEA方式创建容器-查看

4 参考文献


文章作者: Kezade
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Kezade !
评论
  目录