安全检查

当你已经构建了一个镜像,最好使用 docker scan 命令进行安全漏洞检查。Docker 已经与 Synk 合作,去提供安全漏洞检查服务。

你必须登录 Docker Hub 才能检查你的镜像。使用 docker scan --login,然后使用 docker scan <image-name> 检查镜像。

例如,你可以检查之前构建的 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

输出列出了漏洞的类别,一个 URL 去了解更多,解决漏洞的相关库版本。

这里有一些其他选项,你可以阅读 docker scan 文档

除了在命令行检查你新构建的镜像,你也可以配置 Docker Hub 去自动检查所有上传的镜像,你可以同时在 Docker Hub 和 Docker Desktop 看见结果。

docker ps 筛选某个容器 docker scan_docker ps 筛选某个容器


镜像层次

使用 docker image history 命令,你可以看见创建镜像每一层的命令。

  1. 使用 docker image history 命令去看 getting-started 镜像的层次。
docker image history getting-started

你应该得到如下输出。

IMAGE          CREATED        CREATED BY                                      SIZE      COMMENT
e569a14f58f5   2 days ago     EXPOSE map[3000/tcp:{}]                         0B        buildkit.dockerfile.v0
<missing>      2 days ago     CMD ["node" "src/index.js"]                     0B        buildkit.dockerfile.v0
<missing>      2 days ago     RUN /bin/sh -c yarn install --production # b…   83.8MB    buildkit.dockerfile.v0
<missing>      2 days ago     COPY . . # buildkit                             58.2MB    buildkit.dockerfile.v0
<missing>      2 days ago     WORKDIR /app                                    0B        buildkit.dockerfile.v0
<missing>      2 days ago     RUN /bin/sh -c apk add --no-cache python2 g+…   223MB     buildkit.dockerfile.v0
<missing>      4 weeks ago    /bin/sh -c #(nop)  CMD ["node"]                 0B
<missing>      4 weeks ago    /bin/sh -c #(nop)  ENTRYPOINT ["docker-entry…   0B
<missing>      4 weeks ago    /bin/sh -c #(nop) COPY file:4d192565a7220e13…   388B
<missing>      4 weeks ago    /bin/sh -c apk add --no-cache --virtual .bui…   7.84MB
<missing>      4 weeks ago    /bin/sh -c #(nop)  ENV YARN_VERSION=1.22.17     0B
<missing>      4 weeks ago    /bin/sh -c addgroup -g 1000 node     && addu…   77.6MB
<missing>      4 weeks ago    /bin/sh -c #(nop)  ENV NODE_VERSION=12.22.10    0B
<missing>      3 months ago   /bin/sh -c #(nop)  CMD ["/bin/sh"]              0B
<missing>      3 months ago   /bin/sh -c #(nop) ADD file:9233f6f2237d79659…   5.59MB

每一行都代表镜像的一层。你可以看见每层的大小,去帮助处理大型镜像。

  1. 你会注意到有几行是截断的,如果你添加 --no-trunc 标签,你将会得到全部输出。
docker image history --no-trunc getting-started

层缓存

现在你已经实际看见层次了,这里有个重要的方法去帮助减少构建镜像的次数

一旦一个层被改变,所有下游层不得不被重新构建。

下面我们看一个 Dockerfile

# syntax=docker/dockerfile:1
FROM node:12-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:12-alpine
 WORKDIR /app
 COPY package.json yarn.lock ./
 RUN yarn install --production
 COPY . .
 CMD ["node", "src/index.js"]
  1. 在与 Dockerfile 相同文件夹下,创建一个文件 .dockerignore,并添加如下内容。
node_modules

.dockerignore 文件是一个简单的方法去仅仅复制镜像相关文件。你可以在了解更多。在这个案例中,node_modules 文件夹应该在第二个 COPY 步骤被忽略,因为他可能会重写在 RUN 步骤所创建的文件。

如果你想了解更多 Node.js 应用的细节,你可以看这篇文档

  1. 使用 docker build 构建一个新的镜像。
docker build -t getting-started .

输出如下:

[+] Building 18.6s (10/10) FINISHED
 => [internal] load build definition from Dockerfile                                                                0.0s
 => => transferring dockerfile: 212B                                                                                0.0s
 => [internal] load .dockerignore                                                                                   0.0s
 => => transferring context: 53B                                                                                    0.0s
 => [internal] load metadata for docker.io/library/node:12-alpine                                                   0.0s
 => [internal] load build context                                                                                   0.5s
 => => transferring context: 3.29kB                                                                                 0.4s
 => [1/5] FROM docker.io/library/node:12-alpine                                                                     0.0s
 => CACHED [2/5] WORKDIR /app                                                                                       0.0s
 => [3/5] COPY package.json yarn.lock ./                                                                            0.0s
 => [4/5] RUN yarn install --production                                                                            16.7s
 => [5/5] COPY . .                                                                                                  0.1s
 => exporting to image                                                                                              1.2s
 => => exporting layers                                                                                             1.1s
 => => writing image sha256:8d86e3a560798b86a9e0be58cfa7120c353686d6673433189ffe1a8634d442db                        0.0s
 => => naming to docker.io/library/getting-started                                                                  0.0s

我们看到镜像的所有层次都被重新构建。

  1. 下面,对 src/static/index.html 文件做一个改变(例如改变 titleThe Awesome Todo App)。
  2. 再次使用 docker build -t getting-started . 构建 Docker 镜像,这次的输出和之前有点不同。
[+] Building 0.2s (10/10) FINISHED
 => [internal] load build definition from Dockerfile                                                                0.0s
 => => transferring dockerfile: 32B                                                                                 0.0s
 => [internal] load .dockerignore                                                                                   0.0s
 => => transferring context: 34B                                                                                    0.0s
 => [internal] load metadata for docker.io/library/node:12-alpine                                                   0.0s
 => [1/5] FROM docker.io/library/node:12-alpine                                                                     0.0s
 => [internal] load build context                                                                                   0.0s
 => => transferring context: 3.44kB                                                                                 0.0s
 => CACHED [2/5] WORKDIR /app                                                                                       0.0s
 => CACHED [3/5] COPY package.json yarn.lock ./                                                                     0.0s
 => CACHED [4/5] RUN yarn install --production                                                                      0.0s
 => [5/5] COPY . .                                                                                                  0.0s
 => exporting to image                                                                                              0.1s
 => => exporting layers                                                                                             0.0s
 => => writing image sha256:3da77ddc82b27f14cf76726efd1e856da84d5cd8344b17c885282abbe827b624                        0.0s
 => => naming to docker.io/library/getting-started                                                                  0.0s

你会明显发现,构建更快了,这是因为我们使用了构建缓存。


多阶段构建

多阶段构建是一个有力的工具去构建镜像,优势如下:

  • 将构建时依赖和运行时依赖分离。
  • 通过发送你的应用需要运行的依赖去减小整个镜像的大小。

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),我们从 build 阶段复制文件。最终的镜像只是最后被创建的阶段(可以使用 --target 进行重写)。

React 实例

当我们构建 React 应用时,我们需要一个 Node 环境去编译 JS 代码(常是 JSX),SASS stylesheets 和更多在静态 HTML、JS、CSS。如果我们不做服务器端渲染,我们不需要在生产环境构建中使用 Node。所以,我们可以部署静态资源在一个静态 nginx 容器。

# syntax=docker/dockerfile:1
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 容器。