怎样编写正确、高效的 Dockerfile

基础镜像

FROM 基础镜像
基础镜像的选择非常关键:

  • 如果关注的是镜像的安全和大小,那么一般会选择 Alpine;
  • 如果关注的是应用的运行稳定性,那么可能会选择 Ubuntu、Debian、CentOS。

构建上下文与.gitignore

真正的镜像构建工作是由服务器端的“Docker daemon”来完成的,所以“docker”客户端就只能把“构建上下文”目录打包上传(显示信息 Sending build context to Docker daemon ),这样服务器才能够获取本地的这些文件。
“构建上下文”其实与 Dockerfile 并没有直接的关系,它其实指定了要打包进镜像的一些依赖文件。而 COPY 命令也只能使用基于“构建上下文”的相对路径,因为“Docker daemon”看不到本地环境,只能看到打包上传的那些文件。但这个机制也会导致一些麻烦,如果目录里有的文件(例如 readme/.git/.svn 等)不需要拷贝进镜像,docker 也会一股脑地打包上传,效率很低。为了避免这种问题,你可以在“构建上下文”目录里再建立一个 .dockerignore 文件,语法与 .gitignore 类似,排除那些不需要的文件。

# docker ignore
*.swp
*.sh
*.git

指令

每个指令都会生成一个镜像层,所以 Dockerfile 里最好不要滥用指令,尽量精简合并,否则会太多的层会导致镜像臃肿不堪。

COPY

在本机上开发测试时会产生一些源码、配置等文件,需要打包进镜像里,可以使用 COPY 命令,它的用法和 Linux 的 cp 差不多,不过拷贝的源文件必须是“构建上下文”路径里的,不能随意指定文件。也就是说,如果要从本机向镜像拷贝文件,就必须把这些文件放到一个专门的目录,然后在 docker build 里指定“构建上下文”到这个目录才行。

COPY ./a.txt  /tmp/a.txt    # 把构建上下文里的a.txt拷贝到镜像的/tmp目录
COPY /etc/hosts  /tmp       # 错误!不能使用构建上下文之外的文件

RUN

Dockerfile 里最重要的一个指令 RUN ,它可以执行任意的 Shell 命令,比如更新系统、安装应用、下载文件、创建目录、编译程序等等,实现任意的镜像构建步骤,非常灵活。
RUN 通常会是 Dockerfile 里最复杂的指令,会包含很多的 Shell 命令,但 Dockerfile 里一条指令只能是一行,所以有的 RUN 指令会在每行的末尾使用续行符 \,命令之间也会用 && 来连接,这样保证在逻辑上是一行

RUN apt-get update \
    && apt-get install -y \
        build-essential \
        curl \
        make \
        unzip \
    && cd /tmp \
    && curl -fSL xxx.tar.gz -o xxx.tar.gz\
    && tar xzf xxx.tar.gz \
    && cd xxx \
    && ./config \
    && make
    && make clean

把这些 Shell 命令集中到一个脚本文件里,用 COPY 命令拷贝进去再用 RUN 来执行:

COPY setup.sh  /tmp/                # 拷贝脚本到/tmp目录

RUN cd /tmp && chmod +x setup.sh \  # 添加执行权限
    && ./setup.sh && rm setup.sh    # 运行脚本然后再删除

RUN 指令实际上就是 Shell 编程,如果你对它有所了解,就应该知道它有变量的概念,可以实现参数化运行,这在 Dockerfile 里也可以做到,需要使用两个指令 ARG 和 ENV。

ARG IMAGE_BASE="node"
ARG IMAGE_TAG="alpine"

ENV PATH=$PATH:/tmp
ENV DEBUG=OFF

EXPOSE

EXPOSE,它用来声明容器对外服务的端口号,对现在基于 Node.js、Tomcat、Nginx、Go 等开发的微服务系统来说非常有用:

EXPOSE 443           # 默认是tcp协议
EXPOSE 53/udp        # 可以指定udp协议

Docker多阶段构建

什么是多阶段构建

多阶段构建指在Dockerfile中使用多个FROM语句,每个FROM指令都可以使用不同的基础镜像,并且是一个独立的子构建阶段。使用多阶段构建打包Java/GO应用具有构建安全、构建速度快、镜像文件体积小等优点。

镜像构建的通用问题

镜像构建服务使用Dockerfile来帮助用户构建最终镜像,但在具体实践中,存在一些问题:

  • Dockerfile编写有门槛
    开发者(尤其是Java)习惯了语言框架的编译便利性,不知道如何使用Dockerfile构建应用镜像。

  • 镜像容易臃肿
    构建镜像时,开发者会将项目的编译、测试、打包构建流程编写在一个Dockerfile中。每条Dockerfile指令都会为镜像添加一个新的图层,从而导致镜像层次深,镜像文件体积特别大

  • 存在源码泄露风险
    打包镜像时,源代码容易被打包到镜像中,从而产生源代码泄漏的风险。

多阶段构建优势

针对Java这类的编译型语言,使用Dockerfile多阶段构建,具有以下优势:

  • 保证构建镜像的安全性
    当您使用Dockerfile多阶段构建镜像时,需要在第一阶段选择合适的编译时基础镜像,进行代码拷贝、项目依赖下载、编译、测试、打包流程。在第二阶段选择合适的运行时基础镜像,拷贝基础阶段生成的运行时依赖文件。最终构建的镜像将不包含任何源代码信息。

  • 优化镜像的层数和体积
    构建的镜像仅包含基础镜像和编译制品,镜像层数少,镜像文件体积小。

  • 提升构建速度
    使用构建工具(Docker、Buildkit等),可以并发执行多个构建流程,缩短构建耗时。

使用多阶段构建Dockerfile

以Java Maven项目为例,在Java Maven项目中新建Dockerfile文件,并在Dockerfile文件添加以下内容。

该Dockerfile文件使用了二阶段构建。

  1. 第一阶段:选择Maven基础镜像(Gradle类型也可以选择相应Gradle基础镜像)完成项目编译,拷贝源代码到基础镜像并运行RUN命令,从而构建Jar包。
  2. 第二阶段:拷贝第一阶段生成的Jar包到OpenJDK镜像中,设置CMD运行命令。
# First stage: complete build environment
FROM maven:3.5.0-jdk-8-alpine AS builder

# add pom.xml and source code
ADD ./pom.xml pom.xml
ADD ./src src/

# package jar
RUN mvn clean package

# Second stage: minimal runtime environment
From openjdk:8-jre-alpine

# copy jar from the first stage
COPY --from=builder target/my-app-1.0-SNAPSHOT.jar my-app-1.0-SNAPSHOT.jar

EXPOSE 8080

CMD ["java", "-jar", "my-app-1.0-SNAPSHOT.jar"]

go项目两阶段构建示例

# First stage: complete build environment
FROM golang:1.17 AS builder
ENV GOSUMDB=off
ENV GOPROXY=//goproxy.cn,direct
WORKDIR /go/src

# compile
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on go build -o httpserver main.go

# Second stage: minimal runtime environment
# copy binary file
FROM alpine:3.9.4
RUN mkdir /app \
     # 日志路径,务必和应用中日志路径保持一致
    && mkdir /var/log/httpserver \
    && addgroup -g 10001 httpserver \
    && adduser -S -u 10001 -G httpserver httpserver
COPY --from=builder /go/src/httpserver /app/httpserver
COPY --from=builder /go/src/cert /app/cert/

RUN chown -R 10001:10001 /app \
    && chown -R 10001:10001 /var/log/httpserver

USER httpserver
WORKDIR /app
ENTRYPOINT ["./httpserver"]
Tags: