Docker镜像构建最佳实践

来自泡泡学习笔记
跳到导航 跳到搜索

镜像分层

使用docker image history命令,您可以查看创建镜像中的每个层的命令。

  1. 使用docker image history命令查看您创建的getting-started镜像的层。

     docker image history getting-started
  2. 您应该会得到类似以下的输出。

     IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
     a78a40cbf866        18 seconds ago      /bin/sh -c #(nop)  CMD ["node" "src/index.j…    0B                  
     f1d1808565d6        19 seconds ago      /bin/sh -c yarn install --production            85.4MB              
     a2c054d14948        36 seconds ago      /bin/sh -c #(nop) COPY dir:5dc710ad87c789593…   198kB               
     9577ae713121        37 seconds ago      /bin/sh -c #(nop) WORKDIR /app                  0B                  
     b95baba1cfdb        13 days ago         /bin/sh -c #(nop)  CMD ["node"]                 0B                  
     <missing>           13 days ago         /bin/sh -c #(nop)  ENTRYPOINT ["docker-entry…   0B                  
     <missing>           13 days ago         /bin/sh -c #(nop) COPY file:238737301d473041…   116B                
     <missing>           13 days ago         /bin/sh -c apk add --no-cache --virtual .bui…   5.35MB              
     <missing>           13 days ago         /bin/sh -c #(nop)  ENV YARN_VERSION=1.21.1      0B                  
     <missing>           13 days ago         /bin/sh -c addgroup -g 1000 node     && addu…   74.3MB              
     <missing>           13 days ago         /bin/sh -c #(nop)  ENV NODE_VERSION=12.14.1     0B                  
     <missing>           13 days ago         /bin/sh -c #(nop)  CMD ["/bin/sh"]              0B                  
     <missing>           13 days ago         /bin/sh -c #(nop) ADD file:e69d441d729412d24…   5.59MB   

    每行代表镜像中的一个层。此处显示底部为基础层,顶部为最新的层。使用此信息,您还可以快速查看每个层的大小,帮助诊断大型镜像。

    您会注意到,几行被截断了。如果您添加–no-trunc标志,您将获得完整的输出。

     docker image history --no-trunc getting-started

层缓存

现在您已经看到了分层的工作原理,有一个重要的教训可以帮助减少构建容器镜像的时间。一旦一个层发生变化,所有下游层也必须重新创建。

看一下您为入门应用程序创建的以下Dockerfile。

# syntax=docker/dockerfile:1
FROM node:18-alpine
WORKDIR /app
COPY . .
RUN yarn install --production
CMD ["node", "src/index.js"]

回顾一下镜像历史输出,您会看到Dockerfile中的每个命令都成为镜像中的一个新层。您可能还记得,当您对镜像进行更改时,必须重新安装yarn依赖项。每次构建时都固定包含相同的依赖项是没有多大意义的。

为了解决这个问题,您需要重新构建Dockerfile以支持依赖项的缓存。对于基于Node的应用程序,这些依赖项在package.json文件中定义。您可以首先只复制该文件,安装依赖项,然后再复制其他所有内容。然后,只有在package.json发生更改时才重新创建yarn依赖项。

  1. 更新Dockerfile,首先复制package.json,在安装依赖项,然后再复制其他所有内容。

     # syntax=docker/dockerfile:1
     FROM node:18-alpine
     WORKDIR /app
     COPY package.json yarn.lock ./
     RUN yarn install --production
     COPY . .
     CMD ["node", "src/index.js"]
  2. 在与Dockerfile相同的文件夹中创建一个名为.dockerignore的文件,并包含以下内容。

     node_modules

    .dockerignore文件是一种选择性地仅复制与镜像相关的文件的简便方法。在这种情况下,第二个COPY步骤应该省略node_modules文件夹,因为否则它可能会覆盖由RUN步骤中的命令创建的文件。

  3. 使用docker build构建一个新的镜像。

     docker build -t getting-started .

    您应该会看到以下输出。

     [+] Building 16.1s (10/10) FINISHED
     => [internal] load build definition from Dockerfile
     => => transferring dockerfile: 175B
     => [internal] load .dockerignore
     => => transferring context: 2B
     => [internal] load metadata for docker.io/library/node:18-alpine
     => [internal] load build context
     => => transferring context: 53.37MB
     => [1/5] FROM docker.io/library/node:18-alpine
     => CACHED [2/5] WORKDIR /app
     => [3/5] COPY package.json yarn.lock ./
     => [4/5] RUN yarn install --production
     => [5/5] COPY . .
     => exporting to image
     => => exporting layers
     => => writing image     sha256:d6f819013566c54c50124ed94d5e66c452325327217f4f04399b45f94e37d25
     => => naming to docker.io/library/getting-started
  4. 现在,对src/static/index.html文件进行更改。例如,将

    <title>

    更改为”The Awesome Todo App”。

  5. 现在再次使用docker build -t getting-started .构建Docker镜像。这次,您的输出应该稍有不同。

     [+] Building 1.2s (10/10) FINISHED
     => [internal] load build definition from Dockerfile
     => => transferring dockerfile: 37B
     => [internal] load .dockerignore
     => => transferring context: 2B
     => [internal] load metadata for docker.io/library/node:18-alpine
     => [internal] load build context
     => => transferring context: 450.43kB
     => [1/5] FROM docker.io/library/node:18-alpine
     => CACHED [2/5] WORKDIR /app
     => CACHED [3/5] COPY package.json yarn.lock ./
     => CACHED [4/5] RUN yarn install --production
     => [5/5] COPY . .
     => exporting to image
     => => exporting layers
     => => writing image     sha256:91790c87bcb096a83c2bd4eb512bc8b134c757cda0bdee4038187f98148e2eda
     => => naming to docker.io/library/getting-started

    首先,您应该注意到构建速度更快了。而且,您会看到有几个步骤正在使用先前缓存的层。推送和拉取此镜像以及对其进行更新也将更快。

多阶段构建

多阶段构建是一种非常强大的工具,可以使用多个阶段来创建镜像。它们具有以下几个优势:

  • 将构建时依赖与运行时依赖分离
  • 通过仅打包应用程序运行所需的内容来减小镜像大小

Maven/Tomcat示例

在构建基于Java的应用程序时,您需要JDK将源代码编译为Java字节码。但是,该JDK在生产环境中是不需要的。此外,您可能会使用Maven或Gradle等工具来帮助构建应用程序。在最终镜像中也不需要这些工具。多阶段构建可以帮助解决这个问题。

# syntax=docker/dockerfile:1
FROM maven AS build
WORKDIR /app
COPY . .
RUN mvn package

FROM tomcat
COPY --from=build /app/target/file.war /usr/local/tomcat/webapps 

在此示例中,您使用一个阶段(称为build)来使用Maven执行实际的Java构建。在第二个阶段(从FROM tomcat开始),您从构建阶段复制文件。最终镜像只是最后一个阶段的创建结果,可以使用–target标志进行覆盖。

React示例

在构建React应用程序时,您需要一个Node环境来将JS代码(通常是JSX)、SASS样式表等编译为静态HTML、JS和CSS。如果您不进行服务器端渲染,甚至在生产构建中也不需要Node环境。您可以将静态资源放入一个静态的nginx容器中。

# syntax=docker/dockerfile:1
FROM node:18 AS build
WORKDIR /app
COPY package* yarn.lock ./
RUN yarn install
COPY public ./public
COPY src ./src
RUN yarn run build

FROM nginx:alpine
COPY --from=build /app/build /usr/share/nginx/html

在上一个Dockerfile示例中,它使用node:18镜像执行构建(最大化层缓存),然后将输出复制到一个nginx容器中。