安全检查
当你已经构建了一个镜像,最好使用 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 image history
命令,你可以看见创建镜像每一层的命令。
- 使用
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
每一行都代表镜像的一层。你可以看见每层的大小,去帮助处理大型镜像。
- 你会注意到有几行是截断的,如果你添加
--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
依赖即可。
- 更新 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"]
- 在与 Dockerfile 相同文件夹下,创建一个文件
.dockerignore
,并添加如下内容。
node_modules
.dockerignore
文件是一个简单的方法去仅仅复制镜像相关文件。你可以在这了解更多。在这个案例中,node_modules
文件夹应该在第二个 COPY
步骤被忽略,因为他可能会重写在 RUN
步骤所创建的文件。
如果你想了解更多 Node.js 应用的细节,你可以看这篇文档。
- 使用
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
我们看到镜像的所有层次都被重新构建。
- 下面,对
src/static/index.html
文件做一个改变(例如改变title
为The Awesome Todo App
)。 - 再次使用
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 容器。