「CONTAINER-TECHNOLOGY」- Dockerfile

简单 Dockerfile 示例

FROM ubuntu:14.04
MAINTAINER James Turnbull "jam@example.com"
RUN apt-get update && apt-get install -y nginx
RUN echo "Hi, I am in your container. " > /usr/share/nginx/html/index.html
EXPOSE 80

Dockerfile 由指令和参数组成,每个指令为大写字母,后跟参数,按照从上到下的顺序执行;

每个指令都会创建一个新的镜像层,并进行提交。Dockerfile 的执行流程如下:
1)从基础镜像(使用 FROM 指定)运行一个容器;
2)执行 Dockerfile 中的一条指令,对容器进行修改;
3)在指令结束后,执行提交动作,提交一个新的镜像层;
4)基于刚才提交的镜像层,运行一个新的容器;
5)回到 2)直到所有指令执行完成;

如果 Dockerfile 执行失败了,我们也会得到一个可以的镜像,然后可以在这个镜像的基础上进行调试;

默认情况下,命令在 /bin/sh -c 中执行,如果在不支持 shell 的平台上、或希望使用 exec 执行,则可以使用 RUN 命令的另一种格式:

RUN [ "apt-get", "install", "-y", "nginx" ]

使用数组来传递参数;

指令 EXPOSE 用于告诉 Docker 该”容器内的应用程序将会使用哪些端口“,但这并不意味着可以自动访问任意容器运行中服务的端口(这里是 80)。出于安全原因,需要用户使用 docker run 运行容器时来指定打开的端口;

常用 Dockerfile 指令

Dockerfile reference
Dockerfile COPY vs ADD: key differences and best practices

CMD

Dockerfile reference | Docker Documentation / CMD

指定容器启动时运行的命令,和 docker run 命令非常相似。但是,在 docker run 中指定的命令会覆盖 CMD 指定的命令;

该指令有三种形式:
1)CMD [“executable”,”param1″,”param2″] (exec form, this is the preferred form)
2)CMD [“param1″,”param2”] (as default parameters to ENTRYPOINT)
3)CMD command param1 param2 (shell form)

该指令可以使用多次,但是只有最后一个会生效

例如CMD ["/bin/bash"],或者传递参数CMD ["/bin/bash", "-l"]

最好通过数组的形式传递命令和参数。如果直接指定命令,则 Docker 会在命令前添加 /bin/sh -c 来执行命令,容易产生意料之外的行为;

ENTRYPOINT

Dockerfile reference | Docker Documentation / ENTRYPOINT

# The exec form
ENTRYPOINT ["executable", "param1", "param2"]

# The shell form
ENTRYPOINT command param1 param2

该命令与 RUN 命令非常相似,但是该命令不会被 docker run 所覆盖,而在 docekr run 中指定的命令,会被当作参数传递给 ENTRYPOINT 指定的命令。除此之外,二者的语法、数组形式是一样的;

能够将 CMD 与 ENTRYPOINT 组合使用:

ENTRYPINT ["/usr/sbin/nginx"]
CMD ["-h"]

当执行 docker run 时,如果未指定命令,则容器启动时执行 /usr/sbin/nignx -h 命令。如果指定了命令,如 -g “daemon off;”,则会覆盖 CMD 指令,执行指定的命令,这里是 /usr/sbin/nginx -h 命令;

如果要覆盖 ENTRYPOINT 命令,需要使用 –entrypoint 选项;

当构建镜像时,Only the last ENTRYPOINT instruction in the Dockerfile will have an effect. 如果未指定 Entrypoint 指令,则容器会保留基础镜像(FROM)中的 Entrypoint 配置;

RUN

Best practices for writing Dockerfiles | Docker Documentation

当使用 apt-get 指令时,官方最佳实践建议采用如下形式:

RUN apt-get update && apt-get install -y \
    package-bar \
    package-baz \
    && rm -rf /var/lib/apt/lists/*

WORKDIR

切换工作目录。在 Docker 中切换到指定工作目录,然后执行命令。使用-w选项覆盖工作目录;

ENV

在构建过程中定义环境变量。从 Docker 1.4 开始,一条指令可以同时定义多个环境变量;

ENV RVM_PATH=/home/rvm RVM_ARCHFLAGS="-arch i386"

在其他指令中,可以直接使用这些环境变量:WORKDIR $RVM_PATH

如果有必要可以使用反斜线进行转义;

这些变量会保存到容器中,即在容器中执行 env 时,可以看到这些环境变量;

也可以通过 docker run -e 选项传递变量,但是这些变量只会在运行时有效;

USER

指定以什么用户去运行。可以使 UID 和 GID 的组合:

USER user


USER user:group


USER uid


USER uid:gid


USER user:gid


USER uid:gorup

比如:USER nginx

可以使用 docker run -u 来覆盖。如果未设置 USER 指令,则默认为 root 用户;

6.VOLUME

向基于镜像创建的容器中添加卷。卷可以存在于一个或多个容器内的特定的目录。该目录可以绕过联合文件系统,并提供向下共享数据、数据持久化;

卷可以在容器间共享和重用;

一个容器可以不是必须和其他容器共享卷;

对卷的修改立即生效;

对卷的修改不会对更新镜像产生影响;

卷会一个存在知道没有任何容器再使用它;

通过该功能可以将数据添加到镜像,而不是提交到镜像,并且可以在多个容器之间共享这些内容。可以通过该功能测试容器、内部程序源码,管理日志等等;

例如:VOLUME ["/opt/project", "/data"]

docker cp | Docker Documentation」「Use volumes | Docker Documentation

7.ADD

将文件复制到镜像中,文件只能位于构建上下文中;

例如:ADD foo.txt /opt/application/foo.txt

「源文件」文件、目录、URL。另外,如果「源文件」是归档文件(gzip, bzip2, xz)则会自动解压目的目录中;解压时,如果目的文件已存在,则不会复制,即不会覆盖。另外,从 URL 中读取的归档文件不会自动解压;

Docker 通过「地址参数末尾的字符」来判断文件是「目录」还是「文件」;

如果目的目录不存在,Docker 会递归创建。新文件和目录的权限默认为 0755,UID=0,GID=0;

通过 ADD 会使构建缓存无效,其后的指令不会使用构建缓存;

COPY

在构建上下文中,复制其内的文件和目录到容器中,与 ADD 不同的是,COPY 不会做额外的操作;

文件或目录的 UID=0,GID=0;文件和目录的元数据也会被复制到容器中的文件和目录上。同样,如果目录不存在,Docker 会递归创建;

Q:复制文件中的内容到镜像中,而非目录本身?
A:COPY downloads/ /tmp/downloads/,That will copy the contents of the downloads directory into a directory called /tmp/downloads/ in the image.
R:ADD or COPY a folder in Docker

9.LABEL

用于添加元数据,已键值对的形式存在:

LABEL version=”1.0″

LABEL lacation=”Somewhere” type=”Data Center” role=”Web Server”

一条指令中可以存在多个元数据,键值对形式,元数据间以空格分隔。推荐多条元数据放在一个 LABEL 指令中,以防止创建过多的镜像层。通过 docker inspect 来查看元数据;

10.STOPSIGNAL

指定 docker stop 时发送给容器的信号。这个信号必须是内核中合法的信号值,例如 9,或者信号名 SIGNAME,例如 SIGKILL;

11.ARG

在 Docker 1.9 中引入该指令,该指令指定在 docker build 命令运行时,传递给构建运行时的变量,只需要在构建时指定–build-arg 标志即可。用户只能在构建时指定定义过的参数;

ARG build

ARG webapp_user=root

上面的示例中,第二条 ARG 设置了默认值 root。如果未使用–build-arg 指定该参数,则使用默认值;

例如,docker build --build-arg build=1234 -t jamti/webapp .,设置 build 参数的值为 1234,而 webapp_user 将使用默认值 root;

!!!不要使用该功能传递密钥等类型的参数;

在 Docker 中预定义了一组 ARG 变量,可以在构建时直接使用,无需再定义一次。「Predefined ARGs

12.ONBUILD

为镜像添加触发器。当该镜像会用做其他镜像的基础镜像时,该镜像中触发器会被执行;

触发器在构建过程中执行当初添加的指令,可以将其视为实在 FROM 之后执行的

ONBUILD ADD . /app/src

ONBUILD RUN cd /app/src && make

在构建镜像时,这些触发器会被加入到镜像中。使用docker inspect来查看加入镜像中的触发器;

通过这种方式可以为应用程序进行一些特定的配置和设置构建信息;

!!!触发器只能被继承一次!!!有些命令不能放在触发器中,这是为了防止产生递归调用;

性质

多阶段构建 | Multi-stage builds

https://docs.docker.com/build/building/multi-stage/

FROM golang:1.23 AS build
WORKDIR /src
COPY <<EOF /src/main.go
package main

EOF
RUN go build -o /bin/hello ./main.go

FROM scratch
COPY --from=build /bin/hello /bin/hello
CMD ["/bin/hello"]

应用

场景 | 编写 Dockerfile 最佳实践

Best practices for writing Dockerfiles

通过 wget 下载,需要注意如下事项:

  • 合并 RUN 指令:
    • 通过 && 将多个命令合并为一个 RUN 指令,减少镜像层数;
    • 用 \ 换行提高可读性;
  • 清理无用文件:
    • 删除下载的临时文件,例如 rm -f xxxxxxx.tar.xz
    • 使用 Alpine 的 –virtual .deps 标记临时依赖,安装后通过 apk del .deps 删除
RUN wget -O node.tar.xz "https://nodejs.org/dist/v20.12.2/SHASUMS256.txt" && \
    grep "node-v20.12.2-linux-x64.tar.xz" SHASUMS256.txt | sha256sum -c - && \
    wget -O node.tar.xz "https://nodejs.org/dist/v20.12.2/node-v20.12.2-linux-x64.tar.xz"
    
RUN apk add --no-cache --virtual .deps wget tar && \
    wget ... && \
    apk del .deps

场景 | 从镜像中还原 Dockerfile 文件

How to generate a Dockerfile from an image?

从镜像中生成 Dockerfile 文件:

#!/bin/sh

docker pull centurylink/dockerfile-from-image
alias dfimage="docker run -v /var/run/docker.sock:/var/run/docker.sock --rm centurylink/dockerfile-from-image"
dfimage --help

存在局限性;

场景 | 解析 Dockerfile 文件

docker-java-parser – dockerfile 解析类库(Java)
GitHub/ThStock/docker-java-parser
Home » com.github.thstock » docker-java-parser

Dockerfile df = Dockerfile.parse(new File("Dockerfile"));
df.getFrom(); // alpine:3.7
df.getLabels(); // {maintainer=ThStock@example.org}
df.getEnv(); // {VERSION=9}