安全扫描

镜像构建完成之后,最好使用 docker scan 命令对其进行扫描以查找安全漏洞。Docker 与 Snyk 合作提供漏洞扫描服务。
例如,要扫描我们自己创建的 todo-app 镜像,只需执行以下命令:

  1. docker scan todo-app

扫描命令使用了一个不断更新的漏洞数据库,如果没有发现漏洞,类似如下输出:

  1. ~ docker scan todo-app
  2. Docker Scan relies upon access to Snyk, a third party provider, do you consent to proceed using Snyk? (y/N)
  3. y
  4. Testing todo-app...
  5. ...
  6. Tested 16 dependencies for known vulnerabilities, no vulnerable paths found.
  7. For more free scans that keep your images secure, sign up to Snyk at https://dockr.ly/3ePqVcp
  8. ~

如果发现了漏洞,类似如下输出:

  1. Low severity vulnerability found in freetype/freetype
  2. Description: CVE-2020-15999
  3. Info: https://snyk.io/vuln/SNYK-ALPINE310-FREETYPE-1019641
  4. Introduced through: freetype/freetype@2.10.0-r0, gd/libgd@2.2.5-r2
  5. From: freetype/freetype@2.10.0-r0
  6. From: gd/libgd@2.2.5-r2 > freetype/freetype@2.10.0-r0
  7. Fixed in: 2.10.0-r1
  8. Medium severity vulnerability found in libxml2/libxml2
  9. Description: Out-of-bounds Read
  10. Info: https://snyk.io/vuln/SNYK-ALPINE310-LIBXML2-674791
  11. Introduced through: libxml2/libxml2@2.9.9-r3, libxslt/libxslt@1.1.33-r3, nginx-module-xslt/nginx-module-xslt@1.17.9-r1
  12. From: libxml2/libxml2@2.9.9-r3
  13. From: libxslt/libxslt@1.1.33-r3 > libxml2/libxml2@2.9.9-r3
  14. From: nginx-module-xslt/nginx-module-xslt@1.17.9-r1 > libxml2/libxml2@2.9.9-r3
  15. Fixed in: 2.9.9-r4

列出了漏洞类型、了解更多信息的 URL 等等,最重要的是哪个版本修复了该漏洞,以便我们及时更新到安全的版本。
更多关于漏洞扫描的信息可以查看 docker scan documentation
除了在命令行上扫描新生成的镜像外,还可以 配置 Docker Hub 来自动扫描所有新推送的镜像,然后在 Docker Hub 和 Docker Desktop 中查看安全扫描结果。类似下图的显示:
构建 Docker 镜像的最佳实践 - 图1

镜像分层

  1. 可以通过 docker image history 命令查看镜像的创建历史,镜像中每个图层的都执行了哪些命令。

    1. docker image history todo-app

    你应该能看到类似如下的输出:

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

    每一行代表镜像中的一层,默认最新的一层显示在最上方。使用这条命令,还可以快速查看镜像中每一层的大小。
    2. 命令默认会对较长的内容进行自动省略处理以保持整洁的输出。如果想查看完整的内容,可以添加 --no-trunc 标志。

    1. docker image history --no-trunc todo-app

    层缓存

    现在我们已经知道了镜像是分层的,再记住一点:一旦某一层有更新,该层以及它的所有下层也必须重新构建。利用好这一特性将有助于减少构建镜像所需花费的时间。
    回顾之前创建的 Dockerfile 文件内容

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

    对照上一步执行 docker image history 的输出结果,会发现 Dockerfile 文中的每一行命令都对应到镜像中的一个层。你应该还记得,在之前的章节当我们只对源码做一小点文本的修改,但是在更新镜像时,同样必须重新执行每个层的命令,比如重新之下 yarn install 安装依赖等等。显然每次都执行 yarn 命令安装依赖是多余的,有没有解决方法?
    要解决这个问题,需要对 Dockerfile 文件进行修改,调整几条命令的执行顺序就可以利用镜像的缓存功能避免执行多余的 yarn install 操作了。对于基于 Node 的应用程序,这些依赖项是在 package.json 文件中定义的。因此,我们只需先把该文件复杂到容器,再安装依赖,然后复制剩下的内容。这样调整之后,只有当 package.json 文件有变动时才会执行 yarn install 操作。

  2. 如上所述调整我们的 Dockerfile 文件:增加一行 COPY package.json yarn.lock ./;同时将 COPY . . 移到 RUN yarn install --production 之后

    1. FROM node:12-alpine
    2. WORKDIR /app
    3. COPY package.json yarn.lock ./
    4. RUN yarn install --production
    5. COPY . .
    6. CMD ["node", "src/index.js"]
  3. 在与 Dockerfile 相同的目录下新创建一个名为 .dockerignore 的文件,内容如下:

    1. node_modules
  4. .dockerignore 文件可以告诉 Docker 在构建镜像时忽略某些文件或文件夹,由于 node_modules 文件夹是在执行 yarn install 命令时自动生成的,因此它不需要作为镜像的固有内容,忽略它才是对的,而且还可以减少镜像的大小。点击 这里 查看更多关于 .dockerignore 的信息。

  5. 再次使用 docker build -t todo-app . 命令构建新镜像
    会看到类似如下输出:

    1. Sending build context to Docker daemon 219.1kB
    2. Step 1/6 : FROM node:12-alpine
    3. ---> b0dc3a5e5e9e
    4. Step 2/6 : WORKDIR /app
    5. ---> Using cache
    6. ---> 9577ae713121
    7. Step 3/6 : COPY package.json yarn.lock ./
    8. ---> bd5306f49fc8
    9. Step 4/6 : RUN yarn install --production
    10. ---> Running in d53a06c9e4c2
    11. yarn install v1.17.3
    12. [1/4] Resolving packages...
    13. [2/4] Fetching packages...
    14. info fsevents@1.2.9: The platform "linux" is incompatible with this module.
    15. info "fsevents@1.2.9" is an optional dependency and failed compatibility check. Excluding it from installation.
    16. [3/4] Linking dependencies...
    17. [4/4] Building fresh packages...
    18. Done in 10.89s.
    19. Removing intermediate container d53a06c9e4c2
    20. ---> 4e68fbc2d704
    21. Step 5/6 : COPY . .
    22. ---> a239a11f68d8
    23. Step 6/6 : CMD ["node", "src/index.js"]
    24. ---> Running in 49999f68df8f
    25. Removing intermediate container 49999f68df8f
    26. ---> e709c03bc597
    27. Successfully built e709c03bc597
    28. Successfully tagged todo-app:latest
  6. 这一次你会看到所有层都已重建,因为我们更改了 Dockerfile 文件。

  7. 现在我们要再一次修改源码,将 src/static/index.html 文件第 11 行 <title> 标签的内容改为 “The Awesome Todo App”
  8. 再次使用 docker build -t todo-app . 命令构建镜像。这次的输出应该看起来有些不同了:

    1. Sending build context to Docker daemon 219.1kB
    2. Step 1/6 : FROM node:12-alpine
    3. ---> b0dc3a5e5e9e
    4. Step 2/6 : WORKDIR /app
    5. ---> Using cache
    6. ---> 9577ae713121
    7. Step 3/6 : COPY package.json yarn.lock ./
    8. ---> Using cache
    9. ---> bd5306f49fc8
    10. Step 4/6 : RUN yarn install --production
    11. ---> Using cache
    12. ---> 4e68fbc2d704
    13. Step 5/6 : COPY . .
    14. ---> cccde25a3d9a
    15. Step 6/6 : CMD ["node", "src/index.js"]
    16. ---> Running in 2be75662c150
    17. Removing intermediate container 2be75662c150
    18. ---> 458e5c6f080c
    19. Successfully built 458e5c6f080c
    20. Successfully tagged todo-app:latest
  9. 这次应该能明显的感觉到构建速度快了很多!因为有好几个层使用了缓存。

    多阶段构建

    虽然在本教程中不会做太多的介绍,但是多阶段构建非常有用。

    Maven/Tomcat 示例

    在构建基于 Java 的应用程序时,需要使用 JDK 将 Java 源代码编译为 Java 字节码。然而, 在生产环境中不需要完整的 JDK,只需 JRE 即可。另外,你可能正在使用 Maven 或 Gradle 之类的工具来帮助构建应用程序,在最终的镜像中也不需要这类开发阶段使用到的工具。使用多阶段构建可以解决这些问题。

    1. FROM maven AS build
    2. WORKDIR /app
    3. COPY . .
    4. RUN mvn package
    5. FROM Tomcat
    6. COPY --from=build /app/target/file.war /usr/local/tomcat/webapps

    在此示例中,我们使用一个称为 build 的阶段,通过 Maven 构建 Java 应用程序。第二个阶段从 FROM tomcat 开始,我们从 build 这个第一阶段的结果中复制文件。默认情况下最终的镜像内容由最后一个阶段创建(可以使用 --target 标志自己指定)。

    React 示例

    在构建 React 应用程序时,我们需要一个 Node 环境来编译 JS 代码、SASS 样式、以及更多的静态 HTML、JS 和 CSS。如果我们不需要服务器端渲染,则在生产环境中我们甚至都不需要 Node 环境,只需将编译好之后的静态资源放到镜像中即可,如下所示:

    1. FROM node:12 AS build
    2. WORKDIR /app
    3. COPY package* yarn.lock ./
    4. RUN yarn install
    5. COPY public ./public
    6. COPY src ./src
    7. RUN yarn run build
    8. FROM nginx:alpine
    9. COPY --from=build /app/build /usr/share/nginx/html

    回顾

    在本节中,先通过对我们的镜像进行安全扫描,以确保我们正在运行和分享的镜像是安全的。然后了解了一些镜像的结构及特点,对我们应用程序的 Dockerfile 文件稍作修改,以便更快地构建镜像。最后通过多阶段构建,将开发环境和生产环境需要的依赖区分开,不止可以减少镜像的大小,还能提高容器的安全性。

    原始资料:Image building tips

点击查看【bilibili】