云原生容器化-3 Dockerfile

1.Dockerfile作用

用户可以使用两种方式构建Docker镜像: 手动方式和Dockerfile自动方式。
[1] 手动方式
运行基础镜像为容器后,根据业务需要进行定制化操作,然后手动通过docker commit命令将容器保存为镜像。
[2] Dockerfile
将依赖的基础镜像和定制化操作写在脚本中,由Docker引擎读取并执行脚本中的指令,然后生成镜像,这个脚本叫做Dockerfile.
推荐使用第二种,理由如下: Dockerfile通过脚本的方式定义依赖和构建步骤,从而确保了每次构建的一致性和可维护性。Dockerfile之于Docker,如同Jenkinsfile之于Jenkins。基于已有的Dockerfile,进行修改和扩展也变得极其方便。

2.Dockerfile属性介绍

FROM:
用于指定依赖的底层镜像,FROM必须是Dockerfile的首个命令。如果没有依赖镜像,使用FROM scratch表示.
一般可以指定Linux发行版本的Docker镜像,如Centos和Debian等, Java项目打包常见的有gradle:7.4.1-jdk8adoptopenjdk/maven-openjdk8.
ARG 和 ENV:
ARG用于声明在构建过程中使用的变量参数,仅在构建过程中生效。

#通过ARG定义了构建变量war
ARG war=target/*.war

#在Dockerfile中可以使用#{war}获取变量的值
COPY /tmp/build/${war} ./build/

Dockerfile中使用#进行注释.

ENV用于定义容器启动的环境变量,在容器启动后生效。

ENV TZ=Asia/Shanghai \
    JAVA_GC_FILE_SIZE=10m \
    JAVA_GC_FILE_COUNT=10 \

Dockerfile中使用\作为换行.

WORKDIR:
设置镜像在构建过程中的当前工作目录,相当于Linux中的cd命令,在容器构建过程中起作用。

WORKDIR /tmp/unpack

COPY和ADD:

COPY命令复制文件到镜像中

#格式:COPY 源路径 目标路径
COPY /tmp/unpack/BOOT-INF/lib ./lib

ADD命令与COPY命令相似,用于向容器中拷贝文件。区别在于,如果源文件是tar, zip, tgz等文件,文件会被自动加压缩到目标路径下。

EXPOSE 和 VOLUME:

EXPOSE指定容器中会监听的端口,VOLUME指定容器中的挂卷目录。二者本质上是文档化容器将监听的端口和将挂载的目录,实际的端口映射和文件挂载还是在docker run中通过-P和 -V指定。用于告诉开发人员,容器内部的服务将监听哪些端口以及挂载哪些目录,方便项目维护。

使用时 可以使用多个EXPOSE命令,或者使用一个EXPOSE命令,将多个端口通过空格分隔(VOLUMN用法于此相同):

EXPOSE 18201/tcp 5000/udp

等价于:

EXPOSE 18201/tcp 
EXPOSE 5000/udp

RUN:
RUN命令用于在容器中执行具体的操作,且每条RUN命令的执行,都会创建一个新镜像层。因此,对于没有变化的执行,可以合并在一条RUN命令中。

CMD 和 ENTRYPOINT:

说明: docker run执行在镜像名称后可以添加运行命令,后续使用docker-run-cmd表示。

CMD命令用于指定容器的默认执行命令,在容器启动时执行。当存在docker-run-cmd时CMD失效,即优先级docker-run-cmd高于CMD。比较好理解,定制化的参数优先级必然高于模板携带的参数。ENTRYPOINT用于指定容器的执行命令,不会被忽略。

CMD 和 ENTRYPOINT在Dockerfile中一般最多只有一个(可以指定多个,但仅最后一个命令有效).
CMD和ENTRYPOINT可以有两种格式的写法: Shell格式和Exec格式:

#Exec格式
CMD ["executable","param1","param2"]
ENTRYPOINT ["executable", "param1", "param2"]

#Shell格式
CMD command param1 param2
ENTRYPOINT command param1 param2

推荐使用EXEC格式,理由在下文进行说明。

根据是否定义了ENTRYPOINT,CMD 和 docker-run-cmd的组合可以分为两种模式:

[1] 命令模式: 当Dockerfile中定义了ENTRYPOINT时,CMD 和 docker-run-cmd作为默认命令。

[2] 参数模式: 当Dockerfile中定义了ENTRYPOINT时,CMD 和 docker-run-cmd沦为参数。

说明:Shell格式的ENTRYPOINT会忽略CMD 和 docker-run-cmd;只有Exec格式才会将CMD 和 docker-run-cmd拼接到ENTRYPOINT指令之后。

最佳实践:ENTRYPOINT用于指定命令模板,CMD提供默认参数,docker-run-cmd提供定制化操作,即要求ENTRYPOINT和CMD使用Exec格式。

除此之外,Dockerfile还有其他命令,如USER(指定容器运行时的用户,效果相当于chown)、HEALTHCHECK(容器健康状态检查)、LABEL (为镜像生成元数据标签信息)等,本文不再进行详细说明。

3.Dockerfile实现机制

在介绍Dockerfile实现机制之前,先介绍两个docker命令docker commit和docker build。

3.1 docker commit命令

对于已运行的容器,可通过docker commit将容器环境保存为镜像。使用如下案例进行说明。

【1】下载并运行httpd容器

#下载httpd镜像
[root@VM-4-6-centos ~]# docker pull httpd
Using default tag: latest
latest: Pulling from library/httpd
c57ee5000d61: Pull complete
ef22398cad3c: Pull complete
4f4fb700ef54: Pull complete
f420b40fd7be: Pull complete
ea4892b1a58d: Pull complete
1fe3871b50ff: Pull complete
Digest: sha256:5ee9ec089bab71ffcb85734e2f7018171bcb2d6707f402779d3f5b28190bb1af
Status: Downloaded newer image for httpd:latest
docker.io/library/httpd:latest

#将httpd镜像运行为name为seong-httpd的容器
[root@VM-4-6-centos ~]# docker run -d --name seong-httpd httpd
bafc6d34768cc80bbac03b2cd3020a5efbb202d23660b8012d1621e2c5b67155

【2】进入httpd容器内部,进行定制化操作

#进入seong-httpd容器内部
[root@VM-4-6-centos ~]# docker exec -it bafc6d34768cc8 bash

#创建/test/seong/test.txt文件,并向文件中输入测试数据
root@bafc6d34768c:/usr/local/apache2# mkdir -p /test/seong && cd /test/seong && touch test.txt && echo "test docker commit by httpd case" > test.txt

#退出容器
root@bafc6d34768c:/test/seong# exit
exit

【3】通过docker commit命令将运行的容器保存为镜像

#将运行的容器保存为httpd:beta-1镜像
[root@VM-4-6-centos ~]# docker commit bafc6d34768cc8 httpd:beta-1
sha256:ad7b48d6fefe19a381ea347087ddd9a0bd0d253e74f76380c7bb23985bf22b65
#查看镜像httpd:beta-1是否已经生成
[root@VM-4-6-centos ~]# docker images | grep httpd
httpd                                            beta-1             ad7b48d6fefe   41 seconds ago   167MB
httpd                                            latest             59bcd61b45fd   3 weeks ago      167MB

【4】将生成的镜像再次运行为容器

#将httpd:beta-1运行为容器
[root@VM-4-6-centos ~]# docker run -d --name seong-httpd-beta-1 httpd:beta-1
e134dd58105dd511638067316f954268c3482258387dc077a7cca3747e3a748c

【5】进入容器内部查看定制化操作

[root@VM-4-6-centos ~]# docker exec -it e134dd58105dd bash
root@e134dd58105d:/usr/local/apache2# cat /test/seong/test.txt
test docker commit by httpd case

3.2 docker build命令

Dockerfile需要被Docker引擎解析,然后生成docker镜像,用户可通过提供的docker build命令进行操作。
使用docker build命令构建镜像时,docker引擎需要镜像的名称(可以包含版本号),Dockerfile路径和构建上下文路径。
常用的命令格式如下:

docker build -t ${镜像名称} -f ${Dockerfile路径}  ${建上下文路径}

**说明:**镜像名称可以添加版本号,如seong:V1.0.0,不传默认我是latest; Dockerfile路径需要精确到文件名,可以使用相对路径或绝对路径;建上下文路径是COPY和ADD操作时的源路径。
另外,可以传入构建参数,如:–build-arg(设置构建时的环境变量),–no-cache(禁用缓存)等。

3.3 Dockerfile实现机制

几年前学习docker时记录的笔记如下: Docker底层都是通过docker commit生成镜像。区别在于Docker引擎在外层进行了一层封装,将读取和解析Dockerfile和执行Dockerfile中的指令通过docker build接口开放给了用户。
与《每天5分钟玩转Docker容器技术》中介绍的内容一致
再次实践操作发现,Docker引擎构建时直接操作的镜像,而没有docker commit的中间过程,猜想: Docker为了提高构建速度,在新版本中进行了优化。[对这部分熟悉的读者,请在留言区进行指点]

以下通过案例显示docker构建镜像的过程:
【1】创建Dockerfile文件,内容如下:

FROM httpd
WORKDIR /usr/local
COPY test.txt .
RUN echo "---test docker build---" >> test.txt

说明:当前路径下包含test.txt和Dockerfile两个文件,test.txt文件内容为abc.

【2】将Dockerfile构建为docker镜像

[root@VM-4-6-centos test-docker]# docker build --progress=plain -t httpd:beta2 .
#0 building with "default" instance using docker driver

#1 [internal] load build definition from Dockerfile
#1 transferring dockerfile: 131B done
#1 DONE 0.0s

#2 [internal] load .dockerignore
#2 transferring context: 2B done
#2 DONE 0.0s

#3 [internal] load metadata for docker.io/library/httpd:latest
#3 DONE 0.0s

#4 [1/4] FROM docker.io/library/httpd
#4 DONE 0.0s

#5 [internal] load build context
#5 transferring context: 29B done
#5 DONE 0.0s

#6 [3/4] COPY test.txt .
#6 CACHED

#7 [2/4] WORKDIR /usr/local
#7 CACHED

#8 [4/4] RUN echo "---test docker build---" >> test.txt
#8 CACHED

#9 exporting to image
#9 exporting layers done
#9 writing image sha256:559eaf86a53b35088adb3b5163be15714421285503447a68301ed83ea1c2f0ed done
#9 naming to docker.io/library/httpd:beta2 done
#9 DONE 0.0s

流程较为清晰:加载Dockerfile、依赖的httpd镜像、构建上下文, 对每个指令都会生产一个中间镜像。
【3】将生成的镜像运行为容器,并查看Dockerfile中的定制内容

#查看镜像是否生成
[root@VM-4-6-centos test-docker]# docker images | grep httpd
httpd                                            beta2              559eaf86a53b   About a minute ago   167MB
httpd                                            latest             59bcd61b45fd   3 weeks ago          167MB

#将镜像运行为容器
[root@VM-4-6-centos test-docker]# docker run -d --name seong-http-beta2 httpd:beta2
b3cb2c7282bf10e309ee49631b73e3bf0b9fdda834281185c690f558730d4397

#进入容器内部查看Dockerfile中的定制内容
[root@VM-4-6-centos test-docker]# docker exec -it b3cb2c7282bf10 bash
root@b3cb2c7282bf:/usr/local# cat /usr/local/test.txt
abc
---test docker build---
root@b3cb2c7282bf:/usr/local#

4.多阶段构建

多阶段构建(一个Dockerfile文件中包含多个FROM命令),具有减少镜像体积、提高安全性、加快构建速度的优点。早前的Docker版本不支持多阶段构建,需要将一个Dockerfile拆分成多个Dockerfile分别构建,并通过shell脚本进行粘连;或者使用一个Dockerfile将编译和运行放在一个镜像中。
多阶段构建引入后,可以在一个Dockerfile中定义多个构建任务。由此,可以将编译和运行分开,即运行环境不再需要包括编译环境的依赖,从而减少镜像的体积;且运行环境中只需要包含编译后的文件,不会包括编译环境中的源码,从而避免造成源码泄露风险;多个不依赖的构建任务可以并发执行,从而加快了构建速度。

使用方式

使用方式较为简单,结合如下案例进行说明:

#使用FROM...AS...形式定义, 声明该构建阶段为build 
FROM adoptopenjdk/maven-openjdk8 AS build
WORKDIR /tmp/build
COPY . .
RUN mvn clean package

FROM openjdk:8-alpine3.9
WORKDIR /tmp/unpack
#使用--from=build 表示COPY指令从build构建阶段拷贝文件,而非从构建上下文
COPY --from=build /tmp/build/target/*.war .
CMD ["java", "-jar", "test-1.0-SNAPSHOT.jar"]

案例中,使用多阶段构建将编译和运行环境进行了拆分:构建阶段依赖adoptopenjdk/maven-openjdk8镜像,运行环境仅依赖openjdk:8-alpine3.9镜像,而无需依赖adoptopenjdk/maven-openjdk8,减少了镜像的体积。
运行构建阶段中,可以通过–from={构建阶段名} 从其他构建阶段中获取数据。

注意:每个构建阶段内部相互隔离,不能使用其他构建阶段中通过ARG定义的参数。

5.案例

如果你的项目使用Dockerfile部署一个使用Maven打包的SpringBoot项目,可以使用如下Dockerfile模板:

# syntax = docker/dockerfile:1.3
FROM adoptopenjdk/maven-openjdk8 AS build
WORKDIR /tmp/build
COPY . .
COPY ./mount/package/settings.xml /usr/share/maven/conf/
RUN mvn clean package

FROM openjdk:8-alpine3.9 AS unpack
WORKDIR /tmp/unpack
ARG war=target/*.war
COPY --from=build /tmp/build/${war} .
RUN jar -xf *.war

FROM openjdk:8-jdk-alpine3.9
WORKDIR /usr/local/app
ENV TZ=Asia/Shanghai \
    JAVA_GC_FILE_SIZE=10m \
    JAVA_GC_FILE_COUNT=10 \
    JAVA_GC_FILE_LOC=./logs/gc \
    JAVA_HEAP_DUMP_LOC=./logs/heapdump \
    JAVA_JDWP_ADDRESS=5000 \
    JAVA_JMX_HOSTNAME=localhost \
    JAVA_JMX_PORT=1099
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
    && apk add --no-cache --virtual .java-dependencies tini \
    && mkdir -p /usr/local/app/logs/gc \
    && mkdir -p /usr/local/app/logs/heapdump
COPY --from=unpack /tmp/unpack/org ./org
COPY --from=unpack /tmp/unpack/WEB-INF/lib ./lib
COPY --from=unpack /tmp/unpack/META-INF ./META-INF
COPY --from=unpack /tmp/unpack/WEB-INF/classes ./
VOLUME ["/usr/local/app/config", "/usr/local/app/logs"]
EXPOSE 18201/tcp ${JAVA_JDWP_ADDRESS}/tcp ${JAVA_JMX_PORT}/tcp
ENTRYPOINT ["tini"]
CMD java -XX:+PrintCommandLineFlags -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${JAVA_HEAP_DUMP_LOC} \
        -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:${JAVA_GC_FILE_LOC}/gc-%t-%p.log -XX:+UseGCLogFileRotation \
        -XX:GCLogFileSize=${JAVA_GC_FILE_SIZE} -XX:NumberOfGCLogFiles=${JAVA_GC_FILE_COUNT} -XX:+PrintHeapAtGC \
        -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=${JAVA_JDWP_ADDRESS} \
        -Dcom.sun.management.jmxremote \
        -Djava.rmi.server.hostname=${JAVA_JMX_HOSTNAME} \
        -Dcom.sun.management.jmxremote.port=${JAVA_JMX_PORT} \
        -Dcom.sun.management.jmxremote.rmi.port=${JAVA_JMX_PORT} \
        -Dcom.sun.management.jmxremote.ssl=false \
        -Dcom.sun.management.jmxremote.authenticate=false \
        -Dcom.sun.management.jmxremote.local.only=false \
        -cp ./:./lib/* org.springframework.boot.loader.JarLauncher

另外,对于使用gradle打包的项目,编译的构建阶段可以使用gradle:7.4.1-jdk8作为依赖镜像。

你可能感兴趣的:(云原生)