写点什么

好焦虑,怎么才能写好 Dockerfile?

作者:java易二三
  • 2023-08-22
    湖南
  • 本文字数:6030 字

    阅读完需:约 20 分钟

Dockerfile 是 Docker 用来构建镜像的文本文件,包括自定义的指令和格式。可以通过 docker build 命令从 Dockerfile 中构建镜像。用户可以通过统一的语法命令来根据需求进行配置,通过这份统一的配置文件,在不同的文件上进行分发,需要使用时就可以根据配置文件进行自动化构建,这解决了开发人员构建镜像的复杂过程。

Dockerfile 的使用

Dockerfile 描述了组装对象的步骤,其中每条指令都是单独运行的。除了 FROM 指令,其他每条命令都会在上一条指令所生成镜像的基础上执行,执行完后会生成一个新的镜像层,新的镜像层覆盖在原来的镜像之上从而形成了新的镜像。Dockerfile 所生成的最终镜像就是在基础镜像上面叠加一层层的镜像层组建的。

Dockerfile 指令

Dockerfile 的基本格式如下:

# CommentINSTRUCTION arguments
复制代码

在 Dockerfile 中,指令(INSTRUCTION)不区分大小写,但是为了与参数区分,推荐大写。Docker 会顺序执行 Dockerfile 中的指令,第一条指令必须是 FROM 指令,它用于指定构建镜像的基础镜像。在 Dockerfile 中以 #开头的行是注释,而在其他位置出现的 #会被当成参数。

Dockerfile 中的指令有 FROM、MAINTAINER、RUN、CMD、EXPOSE、ENV、ADD、COPY、ENTRYPOING、VOLUME、USER、WORKDIR、ONBUILD,错误的指令会被忽略。下面将详细讲解一些重要的 Docker 指令。

FROM

格式: FROM <image> 或者 FROM <image>:<tag>

FROM 指令的功能是为后面的指令提供基础镜像,因此 Dockerfile 必须以 FROM 指令作为第一条非注释指令。从公共镜像库中拉取镜像很容易,基础镜像可以选择任何有效的镜像。在一个 Dockerfile 中 FROM 指令可以出现多次,这样会构建多个镜像。tag 的默认值是 latest,如果参数 image 或者 tag 指定的镜像不存在,则返回错误。

ENV

格式: ENV <key> <value> 或者 ENV <key>=<value> ...

ENV 指令可以为镜像创建出来的容器声明环境变量。并且在 Dockerfile 中,ENV 指令声明的环境变量会被后面的特定指令(即 ENV、ADD、COPY、WORKDIR、EXPOSE、VOLUME、USER)解释使用。

其他指令使用环境变量时,使用格式为 $variable_name 或者 ${variable_name}。如果在变量面前添加斜杠\可以转义。如\$foo 或者\${foo}将会被转换为 $foo 和 ${foo},而不是环境变量所保存的值。另外,ONBUILD 指令不支持环境替换。

COPY

格式: COPY <src> <dest>

COPY 指令复制所指向的文件或目录,将它添加到新镜像中,复制的文件或目录在镜像中的路径是<dest>。<src>所指定的源可以有多个,但必须是上下文根目录中的相对路径。不能只用形如 COPY ../something /something 这样的指令。此外,<src>可以使用通配符指向所有匹配通配符的文件或目录,例如,COPY home* /mydir/ 表示添加所有以"hom"开头的文件到目录/mydir/中。

<dest>可以是文件或目录,但必须是目标镜像中的绝对路径或者相对于 WORKDIR 的相对路径(WORKDIR 即 Dockerfile 中 WORKDIR 指令指定的路径,用来为其他指令设置工作目录)。若<dest>以反斜杠/结尾则其指向的是目录;否则指向文件。<src>同理。若<dest>是一个文件,则<src>的内容会被写到<dest>中;否则<src>指向的文件或目录中的内容会被复制添加到<dest>目录中。当<src>指定多个源时,<dest>必须是目录。如果<dest>不存在,则路径中不存在的目录会被创建。

ADD

格式:ADD <src> <dest>

ADD 与 COPY 指令在功能上很相似,都支持复制本地文件到镜像的功能,但 ADD 指令还支持其他功能。<src>可以是指向网络文件的 URL,此时若<dest>指向一个目录,则 URL 必须是完全路径,这样可以获得网络文件的文件名 filename,该文件会被复制添加到<dest>/<filename>。比如 ADDhttp://example.com/config.property / 会创建文件/config.property。

<src>还可以指向一个本地压缩归档文件,该文件会在复制到容器时会被解压提取,如 ADD sxample.tar.xz /。但是若 URL 中的文件为归档文件则不会被解压提取。

ADD 和 COPY 指令虽然功能相似,但一般推荐使用 COPY,因为 COPY 只支持本地文件,相比 ADD 而言,它更加透明。

EXPOSE

格式: EXPOSE <port> [<port>/<protocol>...]

EXPOSE 指令通知 Docker 该容器在运行时侦听指定的网络端口。可以指定端口是侦听 TCP 还是 UDP,如果未指定协议,则默认值为 TCP。这个指令仅仅是声明容器打算使用什么端口而已,并不会自动在宿主机进行端口映射,可以在运行的时候通过 docker -p 指定。

EXPOSE 80/tcpEXPOSE 80/udp
复制代码

USER

格式: USER <user>[:<group] 或者 USER <UID>[:<GID>]

USER 指令设置了 user name 和 user group(可选)。在它之后的 RUN,CMD 以及 ENTRYPOINT 指令都会以设置的 user 来执行。

WORKDIR

格式: WORKDIR /path/to/workdir

WORKDIR 指令设置工作目录,它之后的 RUN、CMD、ENTRYPOINT、COPY 以及 ADD 指令都会在这个工作目录下运行。如果这个工作目录不存在,则会自动创建一个。WORKDIR 指令可在 Dockerfile 中多次使用。如果提供了相对路径,则它将相对于上一个 WORKDIR 指令的路径。例如

WORKDIR /aWORKDIR bWORKDIR cRUN pwd
复制代码

输出结果是 /a/b/c

RUN

格式 1: RUN <command> (shell 格式)格式 2: RUN ["executable", "param1", "param2"] (exec 格式,推荐使用)

RUN 指令会在前一条命令创建出的镜像的基础上创建一个容器,并在容器中运行命令,在命令结束运行后提交容器为新镜像,新镜像被 Dockerfile 中的下一条指令使用。

RUN 指令的两种格式表示命令在容器中的两种运行方式。当使用 shell 格式时,命令通过/bin/sh -c 运行。当使用 exec 格式时,命令是直接运行的,容器不调用 shell 程序,即容器中没有 shell 程序。exec 格式中的参数会被当成 JSON 数组被 Docker 解析,故必须使用双引号而不能使用单引号。因为 exec 格式不会在 shell 中执行,所以环境变量的参数不会被替换。

比如执行 RUN ["echo", "$HOME"]指令时,$HOME 不会做变量替换。如果希望运行 shell 程序,指令可以写成 RUN ["/bin/bash", "-c", "echo", "$HOME"]。

CMD

CMD 指令有 3 种格式。

格式 1:CMD <command> (shell 格式)格式 2:CMD ["executable", "param1", "param2"] (exec 格式,推荐使用)格式 3:CMD ["param1", "param2"] (为 ENTRYPOINT 指令提供参数)

CMD 指令提供容器运行时的默认值,这些默认值可以是一条指令,也可以是一些参数。一个 Dockerfile 中可以有多条 CMD 指令,但只有最后一条 CMD 指令有效。 CMD ["param1", "param2"]格式是在 CMD 指令和 ENTRYPOINT 指令配合时使用的,CMD 指令中的参数会添加到 ENTRYPOING 指令中.使用 shell 和 exec 格式时,命令在容器中的运行方式与 RUN 指令相同。

不同之处在于,RUN 指令在构建镜像时执行命令,并生成新的镜像;CMD 指令在构建镜像时并不执行任何命令,而是在容器启动时默认将 CMD 指令作为第一条执行的命令。如果用户在命令行界面运行 docker run 命令时指定了命令参数,则会覆盖 CMD 指令中的命令。

ENTRYPOINT

ENTRYPOINT 指令有两种格式。

格式 1:ENTRYPOINT <command> (shell 格式)格式 2:ENTRYPOINT ["executable", "param1", "param2"] (exec 格式,推荐格式)

ENTRYPOINT 指令和 CMD 指令类似,都可以让容器在每次启动时执行相同的命令,但它们之间又有不同。一个 Dockerfile 中可以有多条 ENTRYPOINT 指令,但只有最后一条 ENTRYPOINT 指令有效。

当使用 Shell 格式时,ENTRYPOINT 指令会忽略任何 CMD 指令和 docker run 命令的参数,并且会运行在 bin/sh -c 中。这意味着 ENTRYPOINT 指令进程为 bin/sh -c 的子进程,进程在容器中的 PID 将不是 1,且不能接受 Unix 信号。即当使用 docker stop <container>命令时,命令进程接收不到 SIGTERM 信号。

推荐使用 exec 格式,使用此格式时,docker run 传入的命令参数会覆盖 CMD 指令的内容并且附加到 ENTRYPOINT 指令的参数中。从 ENTRYPOINT 的使用中可以看出,CMD 可以是参数,也可以是指令,而 ENTRYPOINT 只能是命令;另外,docker run 命令提供的运行命令参数可以覆盖 CMD,但不能覆盖 ENTRYPOINT。

Dockerfile 实践心得

使用标签

给镜像打上标签,有利于帮助了解进镜像功能

谨慎选择基础镜像

选择基础镜像时,尽量选择当前官方镜像库的肩宽,不同镜像的大小不同,目前 Linux 镜像大小由如下关系:

busybox < debian < centos < ubuntu

同时在构建自己的 Docker 镜像时,只安装和更新必须使用的包。此外相比 Ubuntu 镜像,更推荐使用 Debian 镜像,因为它非常轻量级(目前其大小是在 100MB 以下),并且仍然是一个完整的发布版本。

充分利用缓存

Docker daemon 会顺序执行 Dockerfile 中的指令,而且一旦缓存失效,后续命令将不能使用缓存。为了有效地利用缓存,需要保证指令的连续性,尽量将所有 Dockerfile 文件相同的部分都放在前面,而将不同的部分放到后面。

正确使用 ADD 与 COPY 命令

当在 Dockerfile 中的不同部分需要用到不同的文件时,不要一次性地将这些文件都添加到镜像中去,而是在需要时添加,这样也有利于重复利用 docker 缓存。另外考虑到镜像大小问题,使用 ADD 指令去获取远程 URL 中的压缩包不是推荐的做法。应该使用 RUN wget 或 RUN curl 代替。这样可以删除解压后不在需要的文件,并且不需要在镜像中在添加一层。

错误做法:

ADD http://example.com/big.tar.xz /usr/src/things/RUN tar -xJf /usr/src/things/big.tar.xz -C /usr/src/thingsRUN make -C /usr/src/things all
复制代码

正确的做法:

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

RUN 指令

在使用较长的 RUN 指令时可以使用反斜杠\分隔多行。大部分使用 RUN 指令的常见是运行 apt-wget 命令,在该场景下请注意以下几点。

  1. 不要在一行中单独使用指令 RUN apt-get update。当软件源更新后,这样做会引起缓存问题,导致 RUN apt-get install 指令运行失败。所以,RUN apt-get update 和 RUN apt-get install 应该写在同一行。比如 RUN apt-get update && apt-get install -y package-1 package-2 package-3

  2. 避免使用指令 RUN apt-get upgrade 和 RUN apt-get dist-upgrade。因为在一个无特权的容器中,一些必要的包会更新失败。如果需要更新一个包(如 package-1),直接使用命令 RUN apt-get install -y package-1。

CMD 和 ENTRYPOINT 命令

CMD 和 ENTRYPOINT 命令指定是了容器运行的默认命令,推荐二者结合使用。使用 exec 格式的 ENTRYPOINT 指令设置固定的默认命令和参数,然后使用 CMD 指令设置可变的参数。

比如下面这个例子:

FROM busyboxWORKDIR /appCOPY run.sh /appRUN chmod +x run.shENTRYPOINT ["/app/run.sh"]CMD ["param1"]
复制代码

run.sh 内容如下:

#!/bin/shecho "$@"
复制代码

运行后输出结果为 param1, Dockerfile 中 CMD 和 ENTRYPOINT 的顺序不重要(CMD 写在 ENTRYPOINT 前后都可以)。

当在 windows 系统下 build dockerfile 你可能会遇到这个问题

standard_init_linux.go:207: exec user process caused "no such file or directory"
复制代码

这是因为 sh 文件的 fileformat 是 dos,这里需要修改为 unix,不需要下载额外的工具,一般我们机器上安装了 git 会自带 git bash,进入 git bash,使用 vi 编辑,在命令行模式下修改(:set ff=unix)。

不要再 Dockerfile 中做端口映射

使用 Dockerfile 的 EXPOSE 指令,虽然可以将容器端口映射在主机端口上,但会破坏 Docker 的可移植性,且这样的镜像在一台主机上只能启动一个容器。所以端口映射应在 docker run 命令中用-p 参数指定。

# 不要再Dockerfile中做如下映射EXPOSE 80:8080
# 仅暴露80端口,需要另做映射EXPOSE 80
复制代码

实践 Dockerfile 的写法

Java 服务的 DockerFile

FROM openjdk:8-jre-alpineENV spring_profiles_active=devENV env_java_debug_enabled=falseEXPOSE 8080WORKDIR /appADD target/smcp-web.jar /app/target/smcp-web.jarADD run.sh /appENTRYPOINT ./run.sh
复制代码

可以看到基础镜像是 openjdk,然后设置了两个环境变量,服务访问端口是 9090(意味着 springboot 应用中指定了 server.port=8080),设置了工作目录是/app。通过 ENTRYPOINT 设定了启动镜像时要启动的命令(./run.sh)。这个脚本中的内容如下:

#!/bin/sh# Set debug options if requiredif [ x"${env_java_debug_enabled}" != x ] && [ "${env_java_debug_enabled}" != "false" ]; then    java_debug_args="-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005"fi
# ex: env_jvm_flags="-Xmx1200m -XX:MaxRAM=1500m" for productionjava $java_debug_args $env_jvm_flags -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -jar target/smcp-web.jar
复制代码

如果我们要指定 jvm 的一些参数,可以通过在环境变量中设置 env_jvm_flags 来指定。

Maven Dockerfile

maven 的 Dockerfile 也写的很好,这里我发上来也给大家参考下

FROM openjdk:8-jdk
ARG MAVEN_VERSION=3.6.3ARG USER_HOME_DIR="/root"ARG SHA=c35a1803a6e70a126e80b2b3ae33eed961f83ed74d18fcd16909b2d44d7dada3203f1ffe726c17ef8dcca2dcaa9fca676987befeadc9b9f759967a8cb77181c0ARG BASE_URL=https://apache.osuosl.org/maven/maven-3/${MAVEN_VERSION}/binaries
RUN mkdir -p /usr/share/maven /usr/share/maven/ref \  && curl -fsSL -o /tmp/apache-maven.tar.gz ${BASE_URL}/apache-maven-${MAVEN_VERSION}-bin.tar.gz \  && echo "${SHA}  /tmp/apache-maven.tar.gz" | sha512sum -c - \  && tar -xzf /tmp/apache-maven.tar.gz -C /usr/share/maven --strip-components=1 \  && rm -f /tmp/apache-maven.tar.gz \  && ln -s /usr/share/maven/bin/mvn /usr/bin/mvn
ENV MAVEN_HOME /usr/share/mavenENV MAVEN_CONFIG "$USER_HOME_DIR/.m2"
COPY mvn-entrypoint.sh /usr/local/bin/mvn-entrypoint.shCOPY settings-docker.xml /usr/share/maven/ref/
ENTRYPOINT ["/usr/local/bin/mvn-entrypoint.sh"]CMD ["mvn"]
复制代码

可以看到它是基于 openjdk 这个基础镜像来创建的,先去下载 maven 的包,然后进行了安装。 然后又设置了 MAVEN_HOME 和 MAVEN_CONFIG 这两个环境变量,最后通过 mvn-entrypoing.sh 来进行了启动。

前端服务的两阶段构建

我有一个前端服务,目录结构如下:

$ ls frontend/myaccount/  resources/  third_party/
复制代码

myaccount 目录下是放置的 js,vue 等,resources 放置的是 css,images 等。third_party 放的是第三方应用。

这里采用了两阶段构建,即采用上一阶段的构建结果作为下一阶段的构建数据

FROM node:alpine as builderWORKDIR '/build'COPY myaccount ./myaccountCOPY resources ./resourcesCOPY third_party ./third_party
WORKDIR '/build/myaccount'
RUN npm installRUN npm rebuild node-sassRUN npm run build
RUN ls /build/myaccount/dist
FROM nginxEXPOSE 80COPY --from=builder /build/myaccount/dist /usr/share/nginx/html
复制代码

需要注意结尾的 --from=builder 这里和开头是遥相呼应的。

总结

我相信看完 dockerfile 指令,你看任何一个 dockerfile 应该都没有太大问题,不记得的命令回来翻一下就行了。如果你觉得还可以,关注下哟。

用户头像

java易二三

关注

还未添加个人签名 2021-11-23 加入

还未添加个人简介

评论

发布
暂无评论
好焦虑,怎么才能写好Dockerfile?_Java_java易二三_InfoQ写作社区