镜像层的概念
所有开始学docker的文章都是翻译来自官方的镜像’docker/getting-started’,有兴趣的朋友可以执行下面的命令直接运行这个容器app
docker run -d -p 80:80 docker/getting-started
然后通过访问 http://localhost:80 端口来访问这个应用
镜像分层
您知道吗,您可以查看镜像的构成要素?使用该docker image history命令,您可以查看用于在镜像中创建每个图层的命令。
使用该docker image history命令查看dockerapp您在本教程前面创建的镜像中的图层。
docker image history docekrapp
您应该得到类似这样的输出(日期/ID 可能不同)。
E:\code\blog\Blog>docker image history dangkei/dockerapp
IMAGE CREATED CREATED BY SIZE COMMENT
e79ba6df98b4 3 days ago CMD ["node" "src/index.js"] 0B buildkit.dockerfile.v0
<missing> 3 days ago RUN /bin/sh -c yarn install --production # b… 85.2MB buildkit.dockerfile.v0
<missing> 3 days ago COPY . . # buildkit 4.62MB buildkit.dockerfile.v0
<missing> 3 days ago WORKDIR /dockerapp 0B buildkit.dockerfile.v0
<missing> 4 days ago RUN /bin/sh -c apk add --no-cache python g++… 205MB buildkit.dockerfile.v0
<missing> 4 days ago MAINTAINER Docker dangkei <dangkei@163.com> 0B buildkit.dockerfile.v0
<missing> 2 weeks ago /bin/sh -c #(nop) CMD ["node"] 0B
<missing> 2 weeks ago /bin/sh -c #(nop) ENTRYPOINT ["docker-entry… 0B
<missing> 2 weeks ago /bin/sh -c #(nop) COPY file:238737301d473041… 116B
<missing> 2 weeks ago /bin/sh -c apk add --no-cache --virtual .bui… 7.62MB
<missing> 2 weeks ago /bin/sh -c #(nop) ENV YARN_VERSION=1.22.5 0B
<missing> 2 weeks ago /bin/sh -c addgroup -g 1000 node && addu… 75.7MB
<missing> 2 weeks ago /bin/sh -c #(nop) ENV NODE_VERSION=12.22.3 0B
<missing> 3 months ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B
<missing> 3 months ago /bin/sh -c #(nop) ADD file:282b9d56236cae296… 5.62MB
每条线代表镜像中的一个层。此处的显示在底部显示底部,在顶部显示最新层。使用它,您还可以快速查看每一层的大小,帮助诊断大镜像。
您会注意到有几行被截断了。如果你添加–no-trunc标志,你会得到完整的输出(是的…有趣的是你如何使用截断的标志来获得未截断的输出,是吧?)
docker image history --no-trunc dangkei/dockerapp
层缓存
既然您已经看到了分层的作用,那么有一个重要的知识需要你来学习帮助减少容器镜像的构建时间。
首先一旦层发生变化,所有下游层也必须重新创建
让我们再看一次我们使用的 Dockerfile…
# This is a comment
FROM node:12-alpine
MAINTAINER Docker dangkei <dangkei@163.com>
RUN apk add --no-cache python g++ make
WORKDIR /dockerapp
COPY . .
RUN yarn install --production
CMD ["node", "src/index.js"]
当我们更新我们的应用重新创建镜像时
E:\code\dockerapp>docker build -f dockerfile -t dockerapp .
[+] Building 119.9s (9/9) FINISHED
=> [internal] load build definition from dockerfile-copy 1.8s
=> => transferring dockerfile: 263B 0.0s
=> [internal] load .dockerignore 2.7s
=> => transferring context: 34B 0.0s
=> [internal] load metadata for docker.io/library/node:12-alpine 0.0s
=> [1/4] FROM docker.io/library/node:12-alpine 0.0s
=> [internal] load build context 1.2s
=> => transferring context: 2.85kB 0.0s
=> CACHED [2/4] WORKDIR /dockerapp 0.2s
=> [3/4] COPY . . 2.3s
=> [4/4] RUN yarn install --production 105.6s
=> exporting to image 6.3s
=> => exporting layers 5.0s
=> => writing image sha256:729ecf2798f37b7c1f8d9229c8f985e2c8851514a072d0cb6668f41b5c36157a 0.2s
=> => naming to docker.io/library/dockerapp 0.2s
回到镜像历史输出,我们看到 Dockerfile 中的每个命令都变成了镜像中的一个新层。您可能还记得,当我们对映像进行更改时,必须重新安装yarn
依赖项。有没有办法解决这个问题?每次构建时都传递相同的依赖关系没有多大意义,对吧?
注意:
=> [4/4] RUN yarn install --production 105.6s
每次重新构建镜像都要105.6,如果分层多,层又很大的话真时很浪费时间。为了解决这个问题,我们需要重构我们的 Dockerfile 以帮助支持依赖项的缓存。对于基于节点的应用程序,这些依赖项在package.json文件中定义。那么,如果我们先只复制那个文件,安装依赖项,然后再复制其他所有东西呢?然后,我们只在 .yaml 文件发生更改时重新创建yarn
依赖项package.json。有道理吗?
更新 Dockerfile 以package.json首先复制,安装依赖项,然后复制其他所有内容。
# This is a comment
FROM node:12-alpine
MAINTAINER Docker dangkei <dangkei@163.com>
WORKDIR /dockerapp
COPY package.json yarn.lock ./
RUN yarn install --production
COPY . .
CMD ["node", "src/index.js"]
.dockerignore使用以下内容在与 Dockerfile 相同的文件夹中创建一个文件。
node_modules
.dockerignore文件是有选择地仅复制镜像相关文件的简单方法。您可以在此处阅读更多相关信息。在这种情况下,node_modules应在COPY第二步中省略该文件夹,否则可能会覆盖由该RUN步骤中的命令创建的文件。有关为什么建议将其用于 Node.js 应用程序和其他最佳实践的更多详细信息,请查看他们关于Dockerizing a Node.js Web 应用程序的指南。
现在docker build -t dockerapp .再次使用构建 Docker镜像。这一次,您的输出应该看起来有点不同。
使用docker build. 看到下面的输出
E:\code\dockerapp>docker build -f dockerfile -t dockerapp .
[+] Building 11.8s (10/10) FINISHED
=> [internal] load build definition from dockerfile 1.9s
=> => transferring dockerfile: 253B 0.0s
=> [internal] load .dockerignore 2.7s
=> => 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 1.3s
=> => transferring context: 2.62kB 0.0s
=> CACHED [2/5] WORKDIR /dockerapp 0.0s
=> CACHED [3/5] COPY package.json yarn.lock ./ 0.0s
=> CACHED [4/5] RUN yarn install --production 0.0s
=> [5/5] COPY . . 2.2s
=> exporting to image 4.0s
=> => exporting layers 2.5s
=> => writing image sha256:82f9d29c08aaf923900c9ed401965923ee94906d338541ecd60e03e6fce00baa 0.2s
=> => naming to docker.io/library/dockerapp 0.2s
您会看到所有图层都已重建。非常好,因为我们对 Dockerfile 进行了相当多的更改。
首先,您应该注意到构建速度要快得多!而且,您会看到步骤
=> CACHED [4/5] RUN yarn install --production 0.0s
0秒已缓存,万岁!我们正在使用构建缓存。推送和拉取此镜像并对其进行更新也会更快。万岁!
多阶段构建
虽然我们不会在本教程中深入探讨它,但多阶段构建是一个非常强大的工具,可以帮助使用多个阶段来创建镜像。它们有几个优点:
将构建时依赖项与运行时依赖项分开
通过仅传送您的应用程序需要运行的内容来减少整体镜像大小
Maven/Tomcat 示例
在构建基于 Java 的应用程序时,需要 JDK 将源代码编译为 Java 字节码。但是,生产中不需要该 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标志覆盖)。
反应示例
在构建 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 容器中。很酷吧?
回顾
通过稍微了解镜像的结构,我们可以更快地构建镜像并减少更改。扫描镜像让我们相信我们正在运行和分发的容器是安全的。多阶段构建还通过将构建时依赖项与运行时依赖项分开来帮助我们减少整体镜像大小并提高最终容器的安全性。