镜像构建技巧
安全检查
生成镜像后,最好用 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
可以查看创建镜像每一层时所使用的命令。
- 使用
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 - 可以加上
--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 缓存好的代替即可。
- 更新 Dockerfile ,先拷贝
package.json
安装依赖,再拷贝其余的文件。FROM node:12-alpine
WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn install --production
COPY . .
CMD ["node", "src/index.js"] - 创建
.dockerignore
(和 Dockerfile 同目录),然后写入以下内容。node_modules
.dockerignore
能够有选择的拷贝只和镜像相关的文件。可以在这里查看更多。在上面的情境中,应该在第二个COPY
中省略node_modules
文件夹需要,否则可能会覆盖RUN
命令中创建的文件。更多细节以及 Node 应用中的最佳实践可以看在 Node.js 应用中实践 Docker 。 - 然后,构建。应当看到这样的输出...
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 - 然后,为了实验,我们修改一下
src/static/index.html
(比如修改<title>
为 “The Awesome Todo App”) - 再次构建镜像,可以注意到我们的操作被缓存了(
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 容器中。