跳到主要内容

镜像构建技巧

原页面

安全检查

生成镜像后,最好用 docker scan 来检查安全漏洞。Docker 已经和 Snyk 合作提供漏洞扫描服务。

比如,检查 getting-started 镜像:

docker scan getting-started

该扫描使用的是一个不断更新的漏洞数据库,你会在输出中看到各种各样的扫描出来的漏洞,可能是这样的:

✗ Low severity vulnerability found in freetype/freetype
Description: CVE-2020-15999
Info: https://snyk.io/vuln/SNYK-ALPINE310-FREETYPE-1019641
Introduced through: freetype/freetype@2.10.0-r0, gd/libgd@2.2.5-r2
From: freetype/freetype@2.10.0-r0
From: gd/libgd@2.2.5-r2 > freetype/freetype@2.10.0-r0
Fixed in: 2.10.0-r1

✗ Medium severity vulnerability found in libxml2/libxml2
Description: Out-of-bounds Read
Info: https://snyk.io/vuln/SNYK-ALPINE310-LIBXML2-674791
Introduced through: libxml2/libxml2@2.9.9-r3, libxslt/libxslt@1.1.33-r3, nginx-module-xslt/nginx-module-xslt@1.17.9-r1
From: libxml2/libxml2@2.9.9-r3
From: libxslt/libxslt@1.1.33-r3 > libxml2/libxml2@2.9.9-r3
From: nginx-module-xslt/nginx-module-xslt@1.17.9-r1 > libxml2/libxml2@2.9.9-r3
Fixed in: 2.9.9-r4

输出列出了漏洞的类型,了解更多的链接,以及相关的能修复漏洞的库。

更多详见 docker scan 文档

也可以配置 Docker Hub 来自动扫描新添加的容器,你可以在 Docker Hub 或 Docker Desktop 中看到结果。

镜像分层 Image Layering

使用 docker image history 可以查看创建镜像每一层时所使用的命令。

  1. 使用 docker image history 来查看 getting-started 的分层。
    docker image history getting-started
    输出应当类似于这样。
    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
    每一行代表一层,最新的一层在最顶部。通过这个可以诊断较大的镜像。
  2. 可以加上 --no-trunc 来获得完成的输出。
    docker image history --no-trunc getting-started

层缓存 Layer Caching

其实有一种技巧能减少容器镜像的构建时间。

一旦有一层变化了,所有的下游层都需要重建。

回顾一下之前使用的 Dockerfile ...

FROM node:12-alpine
WORKDIR /app
COPY . .
RUN yarn install --production
CMD ["node", "src/index.js"]

当我们对镜像进行修改,yarn 则需要被重新安装。我们可以创建组织我们的 Dockerfile 来支持依赖的缓存。对于基于 Node 的应用,这些依赖在 package.json 中定义。因此,我们可以先只拷贝里面的文件,安装完所有依赖(Docker 会缓存依赖),然后再将别的东西(源代码)拷走,这样下次构建镜像的时候,无需重新安装依赖,用 Docker 缓存好的代替即可。

  1. 更新 Dockerfile ,先拷贝 package.json 安装依赖,再拷贝其余的文件。
    FROM node:12-alpine
    WORKDIR /app
    COPY package.json yarn.lock ./
    RUN yarn install --production
    COPY . .
    CMD ["node", "src/index.js"]
  2. 创建 .dockerignore (和 Dockerfile 同目录),然后写入以下内容。
    node_modules
    .dockerignore 能够有选择的拷贝只和镜像相关的文件。可以在这里查看更多。在上面的情境中,应该在第二个 COPY 中省略 node_modules 文件夹需要,否则可能会覆盖 RUN 命令中创建的文件。更多细节以及 Node 应用中的最佳实践可以看在 Node.js 应用中实践 Docker
  3. 然后,构建。
    docker build -t getting-started .
    应当看到这样的输出...
    Sending build context to Docker daemon  219.1kB
    Step 1/6 : FROM node:12-alpine
    ---> b0dc3a5e5e9e
    Step 2/6 : WORKDIR /app
    ---> Using cache
    ---> 9577ae713121
    Step 3/6 : COPY package.json yarn.lock ./
    ---> bd5306f49fc8
    Step 4/6 : RUN yarn install --production
    ---> Running in d53a06c9e4c2
    yarn install v1.17.3
    [1/4] Resolving packages...
    [2/4] Fetching packages...
    info fsevents@1.2.9: The platform "linux" is incompatible with this module.
    info "fsevents@1.2.9" is an optional dependency and failed compatibility check. Excluding it from installation.
    [3/4] Linking dependencies...
    [4/4] Building fresh packages...
    Done in 10.89s.
    Removing intermediate container d53a06c9e4c2
    ---> 4e68fbc2d704
    Step 5/6 : COPY . .
    ---> a239a11f68d8
    Step 6/6 : CMD ["node", "src/index.js"]
    ---> Running in 49999f68df8f
    Removing intermediate container 49999f68df8f
    ---> e709c03bc597
    Successfully built e709c03bc597
    Successfully tagged getting-started:latest
  4. 然后,为了实验,我们修改一下 src/static/index.html (比如修改 <title> 为 “The Awesome Todo App”)
  5. 再次构建镜像,可以注意到我们的操作被缓存了(Using cache)。
    Sending build context to Docker daemon  219.1kB
    Step 1/6 : FROM node:12-alpine
    ---> b0dc3a5e5e9e
    Step 2/6 : WORKDIR /app
    ---> Using cache
    ---> 9577ae713121
    Step 3/6 : COPY package.json yarn.lock ./
    ---> Using cache
    ---> bd5306f49fc8
    Step 4/6 : RUN yarn install --production
    ---> Using cache
    ---> 4e68fbc2d704
    Step 5/6 : COPY . .
    ---> cccde25a3d9a
    Step 6/6 : CMD ["node", "src/index.js"]
    ---> Running in 2be75662c150
    Removing intermediate container 2be75662c150
    ---> 458e5c6f080c
    Successfully built 458e5c6f080c
    Successfully tagged getting-started:latest

多阶段构建

这里只是大概讲讲这个方面。多阶段构建是个强大的工具来帮助我们尝试用多个阶段来构建一个镜像。这有以下好处:

  • 从运行时以来中分离构建时依赖
  • 通过仅分发应用所需运行的内容来减小镜像尺寸

Maven/Tomcat 样例

当构建一个基于 Java 的应用,需要使用 JDK 来编译源代码。但是,JDK 在生产中并不需要。同时,你可能会用 Maven/Gradle 来构建应用。这些也不应该在镜像中存在。多阶段构建可以帮忙这一点。

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 开始),我们复制了 build 阶段的文件。最终的镜像只有最终阶段的创建(可以用 --target 修饰符进行覆盖,具体可参考stackoverflow)。

React 样例

当构建 React 应用时,我们需要 Node 环境来编译 JS 代码(通常为 JSX )、SASS 样式表和静态的 HTML、JS 和 CSS 。如果我们不用到服务端渲染。我们甚至不需要一个 Node 环境在我们的生产构建中。为什么不只在一个 nginx 容器中分发静态资源呢?

FROM node:12 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

这里我们用 node:12 进行了构建,然后拷贝输出到 nginx 容器中。