Dockerfile详解


Dockerfile是一个组合映像命令的文本;可以使用在命令行中调用任何命令;Docker通过dockerfile中的指令自动生成镜像。

通过docker build -t repository:tag ./ 即可构建,要求:./下存在Dockerfile文件

之前我们聊的镜像分层,这个层怎么分的,就是由Dockerfile中的每一条指令构成

编写规则

  • 文件名必须是 Dockerfile
  • Dockerfile中所用的所有文件一定要和Dockerfile文件在同一级父目录下
  • Dockerfile中相对路径默认都是Dockerfile所在的目录
  • Dockerfile中一能写到一行的指令,一定要写到一行,因为每条指令都被视为一层,层多了执行效率就慢
  • Dockerfile中指令大小写不敏感,但指令都用大写(约定俗成)
  • Dockerfile 非注释行第一行必须是 FROM
  • Dockerfile 工作空间目录下支持隐藏文件(.dockeringore),类似于git的.gitingore

反向解析镜像Dockerfile

DockerImage2Df


指令详解

FROM:基础镜像


FROM <image>:<tag> [as other_name]      # tag可选;不写默认是latest版
  • FROM是Dockerfile文件开篇第一个非注释行代码
  • 用于为镜像文件构建过程指定基础镜像,后续的指令都基于该基础镜像环境运行
  • 基础镜像可以是任何一个镜像文件
  • as other_name是可选的,通常用于多阶段构建(有利于减少镜像大小)
  • 使用是通过--from other_name使用,例如COPY --from other_name

LABEL:镜像描述信息


LABEL author="zp wang <test@qq.com>"
LABEL describe="test image"

# 或
LABEL author="zp wang <test@qq.com>" describe="test image"

# 或
LABEL author="zp wang <test@qq.com>" \
      describe="test image"
  • LABEL指令用来给镜像以键值对的形式添加一些元数据信息
  • 可以替代MAINTAINER指令
  • 会集成基础镜像中的LABEL,key相同会被覆盖

MAINTAINER:添加作者信息


MAINTAINER zp wang <test@163.com>
  • 慢慢废弃

COPY:从构建主机复制文件到镜像中


COPY <src> <dest>

COPY ["<src>", "<src>", ... "<dest>"]
  • <src>:要复制的源文件或目录,支持通配符
  • <src>必须在build所在路径或子路径下,不能是其父目录
  • <src>是目录。其内部的文件和子目录都会递归复制,但<src>目录本身不会被复制
  • 如果指定了多个<src>或使用了通配符,这<dest>必须是一个目录,且必须以/结尾
  • <dest>:目标路径,即镜像中文件系统的路径
  • <dest>如果不存在会自动创建,包含其父目录路径也会被创建


# 拷贝一个文件
COPY testFile /opt/

# 拷贝一个目录
COPY testDir /opt/testDir
  • testDir下所有文件和目录都会被递归复制
  • 目标路径要写testDir,否则会复制到/opt下

ADD:从构建宿主机复制文件到镜像中

类似于COPY指令,但ADD支持tar文件还让URL路径


ADD <src> <dest>

ADD ["<src>","<src>"... "<dest>"]
  • <src>如果是一个压缩文件(tar),被被解压为一个目录,如果是通过URL下载一个文件不会被解压
  • <src>如果是多个,或使用了通配符,则<dest>必须是以/结尾的目录,否则<src>会被作为一个普通文件,<src>的内容将被写入到<dest>

WORKDIR:设置工作目录

类似于cd命令,为了改变当前的目录域

此后RUN、CMD、ENTRYPOINT、COPY、ADD等命令都在此目录下作为当前工作目录


WORKDIR /opt
  • 如果设置的目录不存在会自动创建,包括他的父目录
  • 一个Dockerfile中WORKDIR可以出现多次,其路径也可以为相对路径,相对路径是基于前一个WORKDIR路径
  • WORKDIR也可以调用ENV指定的变量

ENV:设置镜像中的环境变量


# 一次设置一个
ENV <key> <value>

# 一次设置多个
ENV <key>=<value> <key1>=<value1> <key2>=<value2> .....

使用环境变量的方式


$varname
${varname}
${varname:-default value}           # 设置一个默认值,如果varname未被设置,值为默认值
${varname:+default value}           # 设置默认值;不管值存不存在都使用默认值

USER:设置启动容器的用户


# 使用用户名
USER testuser

# 使用用户的UID
USER UID

RUN:镜像构建时执行的命令


# 语法1,shell 形式
RUN command1 && command2

# 语法2,exec 形式
RUN ["executable","param1","[aram2]"]


# 示例
RUN echo 1 && echo 2 

RUN echo 1 && echo 2 \
    echo 3 && echo 4

RUN ["/bin/bash","-c","echo hello world"]
  • RUN 在下一次建构期间,会优先查找本地缓存,若不想使用缓存可以通过--no-cache解除
  • docker build --no-cache
  • RUN 指令指定的命令是否可以执行取决于 基础镜像
  • shell形式
  • 默认使用/bin/sh -c 执行后面的command
  • 可以使用 &&\ 连接多个命令
  • exec形式
  • exec形式被解析为JSON序列,这意味着必须使用双引号 ""
  • 与 shell 形式不同,exec 形式不会调用shell解析。但exec形式可以运行在不包含shell命令的基础镜像中
  • 例如:RUN ["echo","$HOME"] ;这样的指令 $HOME并不会被解析,必须RUN ["/bin/sh","-c","echo $HOME"]

EXPOSE:为容器打开指定的监听端口以实现与外部通信


EXPOSE <port>/<protocol>

EXPOSE 80
EXPOSE 80/http
EXPOSE 2379/tcp
  • <port>:端口号
  • <protocol>:协议类型,默认TCP协议,tcp/udp/http/https
  • 并不会直接暴露出去,docker run时还需要-P指定才可以,这里更像是一个说明

VOLUME:实现挂载功能,将宿主机目录挂载到容器中


VOLUME ["/data"]                    # [“/data”]可以是一个JsonArray ,也可以是多个值

VOLUME /var/log 
VOLUME /var/log /opt
  • 三种写法都是正确的
  • VOLUME类似于docker run -v /host_data /container_data 。
  • 一般不需要在Dockerfile中写明,且在Kubernetes场景几乎没用

CMD:为容器设置默认启动命令或参数


# 语法1,shell形式
CMD command param1 param2 ...

# 语法2,exec形式
CMD ["executable","param1","param2"]

# 语法3,还是exec形式,不过仅设置参数
CMD ["param1","param2"]
  • CMD运行结束后容器将终止,CMD可以被docker run后面的命令覆盖
  • 一个Dockerfile只有顺序向下的最后一个CMD生效
  • 语法1,shell形式,默认/bin/sh -c
  • 此时运行为shell的子进程,能使用shell的操作符(if环境变量? *通配符等)
  • 注意:进程在容器中的 PID != 1,这意味着该进程并不能接受到外部传入的停止信号docker stop
  • 语法2,exec形式CMD ["executable","param1","param2"]
  • 不会以/bin/sh -c运行(非shell子进程),因此不支持shell的操作符
  • 若运行的命令依赖shell特性,可以手动启动CMD ["/bin/sh","-c","executable","param1"...]
  • 语法3,exec形式CMD ["param1","param2"]
  • 一般结合ENTRYPOINT指令使用

ENTRYPOINT:用于为容器指定默认运行程序或命令

与CMD类似,但存在区别,主要用于指定启动的父进程,PID=1


# 语法1,shell形式
ENTRYPOINT command

# 语法2,exec形式
ENTRYPOINT ["/bin/bash","param1","param2"]
  • ENTRYPOINT设置默认命令不会被docker run命令行指定的参数覆盖,指定的命令行会被当做参数传递给ENTRYPOINT指定的程序。
  • docker run命令的 --entrypoint选项可以覆盖ENTRYPOINT指令指定的程序
  • 一个Dockerfile中可以有多个ENTRYPOINT,但只有最后一个生效
  • ENTRYPOINT主要用于启动父进程,后面跟的参数被当做子进程来启动
CMD和ENTRYPOINT组合情况说明

具体情况比较多,如表格所示

No ENTRYPOINT

ENTRYPOINT exec_entry p1_entry

ENTRYPOINT ["exec_entry","p1_entry"]

No CMD

error,not allowed

/bin/sh -c exec_entry p1_entry

exec_entry p1_entry

CMD ["exec_cmd","p1_cmd"]

exec_cmd p1_cmd

/bin/sh -c exec_entry p1_entry

exec_entry p1_entry exec_cmd p1_cmd

CMD ["p1_cmd","p2_cmd"]

p1_cmd p2_cmd

/bin/sh -c exec_entry p1_entry

exec_entry p1_entry p1_cmd p2_cmd

CMD exec_cmd p1_cmd

exec_cmd p1_cmd

/bin/sh -c exec_entry p1_entry

exec_entry p1_entry /bin/sh -c exec_cmd p1_cmd

使用exec模式与shell模式,执行ENTRYPOINT和CMD的区别


ARG:指定环境变量用于构建过程


ARG name[=default value]

ARG test_name
ARG nother_name=wzp
  • ARG指令定义的参数,在构建过程以docker build --build-arg test_name=test 形式赋值
  • ARG中没有设置默认值,构建时将抛出警告:[Warning] One or more build-args..were not consumed
  • Docker默认存在的ARG 参数,可以在--build-arg时直接使用
  • HTTP_PROXY/http_proxy/HTTPS_PROXY/https_proxy/FTP_PROXY/ftp_proxy/NO_PROXY/no_proxy

ONBUILD:为镜像添加触发器

ONBUILD可以为镜像添加一个触发器,其参数可以是任意一个Dockerfile指令。


ONBUILD <dockerfile_exec> <param1> <param2>

ONBUILD RUN mkdir mydir
  • 该指令,对于使用该Dockerfile构建的镜像并不会生效,只有当其他Dockerfile以当前镜像作为基础镜像时被触发
  • 例如:Dockfile A 构建了镜像A,Dockfile B中设置FROM A,此时构建镜像B是会运行ONBUILD设置的指令

STOPSINGAL:设置停止时要发送给PID=1进程的信号

主要的目的是为了让容器内的应用程序在接收到signal之后可以先做一些事情,实现容器的平滑退出,如果不做任何处理,容器将在一段时间之后强制退出,会造成业务的强制中断,这个时间默认是10s。


STOPSIGNAL signal
  • 默认的停止信号为:SIGTERM,也可以通过docker run -s指定

HEALTHCHECK:指定容器健康检查命令

当在一个镜像指定了 HEALTHCHECK 指令后,用其启动容器,初始状态会为 starting,在 HEALTHCHECK 指令检查成功后变为 healthy,如果连续一定次数失败,则会变为 unhealthy


HEALTHCHECK [OPTIONS] CMD command

# 示例
HEALTHCHECK --interval=5s --timeout=3s \
    CMD curl -fs http://localhost/ || exit 1        # 如果执行不成功返回1
  • 出现多次,只有最后一次生效
  • OPTIONS选项
  • --interval=30:两次健康检查的间隔,默认为 30 秒;
  • --timeout=30:健康检查命令运行的超时时间,超过视为失败,默认30秒;
  • --retries=3:指定失败多少次视为unhealth,默认3次
  • 返回值
  • 0:成功; 1:失败; 2:保留

SHELL:指定shell形式的默认值

SHELL 指令可以指定 RUN、ENTRYPOINT、CMD 指令的 shell,Linux 中默认为["/bin/sh", "-c"] ,Windows默认["CMD","/S","/C"]

通常用于构建Windows用的镜像


SHELL ["/bin/bash","-c"]

SHELL ["powershell", "-command"]

# 示例,比如在Windows时,默认shell是["CMD","/S","/C"]
RUN powershell -command Execute-MyCmdlet -param1 "c:\foo.txt"
# docker调用的是cmd /S /C powershell -command Execute-MyCmdlet -param1 "c:\foo.txt"
RUN ["powershell", "-command", "Execute-MyCmdlet", "-param1 \"c:\\foo.txt\""]
# 这样虽然没有调用cmd.exe 但写起来比较麻烦,所以可以通过SHELL  ["powershell", "-command"] 这样省去前边的powershell -command

思考

Dockerfile需要一个基础镜像,基础镜像怎么来的?

能不能自己制作基础镜像?

  • 基础镜像:是否基于其他镜像制作还是自己制作就取决于第一行的FROM
  • scratch:特殊的字段,表示一个空镜像

FROM scratch:表示使用一个空镜像(FROM 子句是告诉Docker引擎构建镜像上下文的开始,必须存在);

如果自己制作基础镜像,那么必要的二进制文件及目录是必要的。

从0构建镜像的前提是要制作出自己的 OS 二进制版本包

参考:centos7官方镜像的制作,https://github.com/CentOS/sig-cloud-instance-images/tree/b2d195220e1c5b181427c3172829c23ab9cd27eb/docker

Dockerfile_JAVA


Dockerfile_Dockerfile_02

构建一个运行在一个我自己剪裁过的基础系统二进制包 上的 Java8 镜像


# 通过centos7的二进制包(假设是我自己做了系统裁剪移植)
FROM scratch
ADD centos-7-x86_64-docker.tar.xz /

RUN yum install -y wget && \
    yum clean all && \
    ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
    echo "Asia/Shanghai" > /etc/timezone

ENV JAVA_VERSION jdk1.8.0_271
ENV JAVA_HOME /usr/lib/${JAVA_VERSION}
ENV PATH ${JAVA_HOME}/bin:$PATH

# 涉及改变镜像大小的指令,尽量放到同一行,这样构建过程中的删除指令对减小体积才能生效
RUN wget --no-cookies --no-check-certificate --header "Cookie: gpw_e24=http%3A%2F%2Fwww.oracle.com%2F; oraclelicense=accept-securebackup-cookie" \
    http://download.oracle.com/otn-pub/java/jdk/8u271-b09/61ae65e088624f5aaa0b1d2d801acb16/jdk-8u271-linux-x64.tar.gz -O /usr/lib/jdk-8u271-linux-x64.tar.gz && \
    tar -xf /usr/lib/jdk-8u271-linux-x64.tar.gz -C /usr/lib/ && \
    rm -rf /usr/lib/jdk-8u271-linux-x64.tar.gz && \
    rm -rf ${JAVA_HOME}/src.zip \
           ${JAVA_HOME}/lib/visualvm \
           ${JAVA_HOME}/jre/lib/plugin.jar \
           ${JAVA_HOME}/jre/bin/javaws \
           ${JAVA_HOME}/jre/lib/desktop \
           ${JAVA_HOME}/jre/plugin \
           ${JAVA_HOME}/jre/lib/deploy* \
           ${JAVA_HOME}/jre/lib/amd64/libglass.so \
           ${JAVA_HOME}/jre/lib/amd64/libgstreamer-lite.so \
           ${JAVA_HOME}/jre/lib/amd64/libjavafx*.so \
           ${JAVA_HOME}/jre/lib/amd64/libjfx*.so

CMD ['/bin/bash']

Dockerfile 编写的最佳实践规范

选⽤最⼩化基础镜像

编写 Dockerfile 时,通常使⽤⼀个通⽤的容器镜像作为基础镜像,例如: eclipse-temurin:8-alpine ,选⽤最

⼩化基础镜像,即只包含项⽬确实需要的系统⼯具和库的镜像,较⼩的基础镜像可以确保在⼀个新节点上拉起容器

时有更快的启动时间(节省了从镜像仓库拉取镜像的⽹络请求时间),并且能够最⼩化系统的攻击⾯,确保所⽤的

操作系统是安全的,⼀般推荐采⽤以 alpine 系统为基础的基础镜像。


# 举一些例子
golang:1.11-alpine3.9
golang:1.15
openjdk:8
adoptopenjdk:8u242-b08-jdk-hotspot-bionic
node:10-alpine
node:16.14.0
node:12-buster-slim
python:3.7-stretch
python:3.7-slim
python:3.7-alpine
nginx:alpine
busybox:alpine
centos:alpine
避免不需要的包

为了降低复杂性、减少依赖、减少安全⻛险、减⼩镜像⽂件⼤⼩、节约构建时间,应该避免构建镜像过程中安装任何不必要的包,例如:不需要在应⽤镜像内包含⽂本编辑器(vim)等。

每个容器只运⾏⼀个进程或应⽤

尽管单个容器确实可以运⾏多个应⽤程序,但出于以下原因,需要尽可能考虑遵循“每个容器⼀个应⽤程序“(以下简称单进程容器)的最佳实践:

  • 单进程容器架构更简单,⽔平伸缩更加容易,⽐如,在⼀个容器内同时包含 tomcat 和 mysql 等,容器的⽔ 平伸缩就会变得⾮常复杂甚⾄⽆法进⾏⽔平伸缩。
  • 单进程容器的排障更简单,进⾏问题排查时,不必对容器内整个系统的各个部分进⾏排查,使应⽤更具有可移 植性和可预测性。
  • 单进程容器使应⽤程序的⽣命周期管理更灵活,应⽤进程即容器主进程,应⽤异常退出容器即会⾃动销毁并重 新启动,使应⽤拥有故障⾃愈的能⼒。
  • 从安全性和隔离性⻆度来看,单进程容器能够提供更安全的服务和应⽤程序间的隔离,以保持更稳定的安全状 态。
构建时使⽤.dockerignore ⽂件

使⽤ Dockerfile 构建镜像时建议将 Dockerfile 放置⼀个新的空⽬录下,将构建所需要的⽂件添加到该⽬录中,为 了提⾼构建镜像的效率,可以在该⽬录下新建⼀个 .dockerignore ⽂件来指定要忽略的⽂件和目录。 .dockerignore ⽂件的排除模式语法和 git 的 .gitignore ⽂件相似。

最⼩化镜像层数

构建镜像时,Dockerfile 的每⼀条 RUN 指令都会在镜像上构建⼀层,为了减⼩镜像⽂件⼤⼩,建议在 Dockerfile可读性(也包括⻓期的可维护性)和减⼩层数之间做⼀个平衡,⽐如,将多个 RUN 指令进⾏合并执⾏(通过 &&符串连多条 shell 命令),可有效减少镜像的层数。

合理使⽤构建缓存

镜像的构建过程中,会顺序执⾏ Dockerfile 中的指令,在执⾏每条指令之前,会先从缓存中查找是否已经存在可重 ⽤的镜像,如果有就使⽤现存的镜像,不再重复创建,以加快镜像构建过程。如果不想在构建过程中使⽤缓存,可 在 docker build 命令中使⽤ --no-cache=true 选项,镜像缓存的基本规则包含:

  • 从⼀个基础镜像开始(FROM 指令指定),下⼀条指令将和该基础镜像的所有⼦镜像进⾏匹配,检查这些⼦ 镜像被创建时使⽤的指令是否和被检查的指令完全⼀样。如果不是,则缓存失效。
  • 在⼤多数情况下,只需要简单地对⽐ Dockerfile 中的指令和⼦镜像。然⽽,有些指令需要更多的检查和解 释。
  • 对于 ADD 和 COPY 指令,镜像中对应⽂件的内容也会被检查,每个⽂件都会计算出⼀个校验和。⽂件的最后 修改时间和最后访问时间不会纳⼊校验。在缓存的查找过程中,会将这些校验和和已存在镜像中的⽂件校验和 进⾏对⽐。如果⽂件有任何改变,⽐如内容和元数据,则缓存失效。
  • 除了 ADD 和 COPY 指令,缓存匹配过程不会查看临时容器中的⽂件来决定缓存是否匹配。例如,当执⾏完 RUN apt-get -y update 指令后,容器中⼀些⽂件被更新,但 Docker 不会检查这些⽂件。这种情况下,只有 指令字符串本身被⽤来匹配缓存。
  • ⼀旦缓存失效,所有后续的 Dockerfile 指令都将产⽣新的镜像,缓存不会被使⽤。
让应⽤程序容器内进程ID=1

当⼀个容器被销毁(即停⽌运⾏)时,操作系统会发送 SIGTERM 给容器内的1号进程(pid=1,可通过 ps -ef 命令查看),1号进程进程接收到信号后可进⾏容器终⽌前的资源释放等处理操作。 例如:构建镜像时,可利⽤系统的 exec 命令特性,为应⽤启动编写独⽴的 shell 脚本⽂件,以 exec 形式启动相关 进程。

合理利⽤环境变量

应⽤镜像应具有环境⽆关性,即需要确保可以在可预测的任何环境中都⽆需重新构建应⽤镜像即可直接运⾏容器,与环境相关的相关数据应尽可能的通过环境变量⽅式在镜像中引⽤,以确保容器的可移植性和可运维能⼒。例如:JVM启动参数 JAVA_OPTS( -Xmxn 、 -Xmsn 、 -Dproperty=value 等)、或 Spring Boot 的分区配置定义(spring.profiles.active指定的分区名)等。

出于安全考虑,应禁⽌将应⽤密钥(如:数据库密码、API访问凭据等)保存⾄镜像内.

设置健康检查

可以在K8S层面设置也可以在Dockerfile层面设置

Dockerfile生产示例


# Build the manager binary
FROM golang:1.16 as builder
WORKDIR /workspace

ARG go_proxy=https://goproxy.cn
ENV GO111MODULE=on \
    GOPROXY=${go_proxy} \
    CGO_ENABLED=0

# Copy the Go Modules manifests
COPY go.mod go.mod
COPY go.sum go.sum
# cache deps before building and copying source so that we don't need to re-download as much
# and so that source changes don't invalidate our downloaded layer
# ENV GOPROXY https://goproxy.cn/
RUN go mod download

# Copy the go source
COPY main.go main.go

#COPY api/ api/
COPY controllers/ controllers/
COPY pkg/ pkg/

# Build
RUN go build -ldflags '-w -s' -a -o manager main.go

# Use distroless as minimal base image to package the manager binary
# Refer to https://github.com/GoogleContainerTools/distroless for more details
# FROM gcr.azk8s.cn/distroless/static:latest
FROM alpine:3.14

RUN  sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories && apk update && apk add subversion

WORKDIR /
COPY --from=builder /workspace/manager .
# USER nonroot:nonroot

ENTRYPOINT ["/manager"]


FROM golang:1.15-alpine as builder
WORKDIR /app
COPY test.go  /app
RUN go build -o test test.go

FROM alpine:3.16

RUN apk --no-cache --update add \
    python3 cmd:pip3 vim curl python3-dev gcc musl-dev openssh skopeo openjdk8 zip make

COPY requirements.txt .
COPY test.zip .
RUN pip3 install --upgrade pip && pip3 install -r /requirements.txt \
    && unzip -o -d . /test.zip && rm -f test.zip

COPY --from=builder /app/test /usr/local/bin/test

WORKDIR /app
COPY . /app
RUN rm -f /app/test.zip

CMD ["/app/docker-run-cmd.sh"]


# Version 1.0
# Base images 基础镜像
FROM centos:alpine

#MAINTAINER 维护者信息
LABEL maintainer="test@test.io"

#ENV 设置环境变量
ENV LANG en_US.UTF-8
ENV LC_ALL en_US.UTF-8

#RUN 执行以下命令
RUN curl -so /etc/yum.repos.d/Centos-7.repo http://mirrors.aliyun.com/repo/Centos-7.repo
RUN yum install -y  python36 python3-devel gcc pcre-devel zlib-devel make net-tools

#工作目录
WORKDIR /opt/app

#拷贝文件至工作目录
COPY . .

#安装nginx
RUN tar -zxf nginx-1.13.7.tar.gz -C /opt  && cd /opt/nginx-1.13.7 && ./configure --prefix=/usr/local/nginx \
&& make && make install && ln -s /usr/local/nginx/sbin/nginx /usr/bin/nginx

RUN cp app.conf /usr/local/nginx/conf/app.conf

#安装依赖的插件
RUN pip3 install -i http://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com -r requirements.txt

RUN chmod +x run.sh && rm -rf ~/.cache/pip

#EXPOSE 映射端口
EXPOSE 8002

#容器启动时执行命令
CMD ["./run.sh"]