之前学习部署 Docker 应用时,我们搭建过一个 redis 服务,然后编写并运行了一个统计访问次数的 flask 应用。
现在,我们使用 Dockerfile,将这个 flask 应用也制作成镜像,此外,在这个镜像中,可以包含一个 helloworld 二进制程序,这个 helloworld
的源码就是我们学习 rootfs 时用到的 helloworld.c。
1. 实战:直接构建镜像
首先 我们需要新建一个目录 dockerfiledir,用于存放 Dockerfile 文件。
- 1
- 2
- 3
新建一个目录code,用来存放flask和c的源代码。
- 1
将之前 app.py 和 helloworld.c 两个源码文件放入到 code 目录下,当前的目录结构应该是这样的:
进入 dockerfiledir 目录,编辑 Dockerfile 文件:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 1
- 1
解决这个问题,需要引入一个重要的概念——构建上下文。
docker build .
命令在执行时,当前目录.
被指定成了构建上下文,此目录中的所有文件或目录都将被发送到 Docker 引擎中去,Dockerfile中的切换目录和复制文件等操作只会对上下文中的内容生效。
我们需要将 code 目录纳入到上下文中,一个直接的方法是,调整dockerfile中的COPY指令的路径。
- 1
- 2
- 3
然后将 code 所在的目录指定为构建上下文。由于我们当前的目录是 dockerfiledir
- 1
如果你留意查看构建过程,会发现类似这样的提示:
- 1
如果..
目录除了code和dockerfiledir,还包含其他的文件或目录,docker build
也会将这个数据传输给Docker,这会增加构建时间。
避免这种情况,有两种解决方法:
使用
.dockerignore
文件:在构建上下文的目录下新建一个.dockerignore
文件来指定在传递给 docker 时需要忽略掉的文件或文件夹。.dockerignore 文件的排除模式语法和 Git 的 .gitignore 文件相似。使用一个干净的目录作为构建上下文(推荐):使用 Dockerfile 构建镜像时最好是将 Dockerfile 放置在一个新建的空目录下。然后将构建镜像所需要的文件添加到该目录中。
在我们当前的示例中,将code目录移入dockerfiledir。
- 1
- 1
- 2
确保部署之前的 redis 容器正常启动,然后在 Docker 宿主机的浏览器中访问http://127.0.0.1:5000
:
说明 myhello 中的 flask 应用已经正常运行了。接下来,我们再运行测试一下编译的 helloworld。
- 1
- 1
2. 改进: 使用多阶段构建
在镜像构建过程中,我们的 helloworld.c 源码以及相关编译工具和依赖也被构建到了镜像中,这导致我们最终得到的镜像偏大。
理想状态应该是使用了一个系统镜像生成的容器,编译源码后再将编译的程序导入到最终的镜像中,这样就会缩减体积,并且将不同目的的操作有效分离开,但是按照我们之前掌握的知识,这样实现需要两个Dockerfile 文件。
使用多阶段构建,我们可以在一个 Dockerfile
中使用多个 FROM 语句。每个 FROM 指令都可以使用不同的镜像,并表示开始一个新的构建阶段。很方便的将一个阶段的文件复制到另外一个阶段,在最终的镜像中保留下需要的内容即可。
我们还是在 Dockerfile 文件的同一目录,新建一个新的构建脚本,命名为 Dockerfile-multi-stage 便于区分:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
执行 build 命令:
- 1
使用此镜像运行一个容器:
- 1
- 2
自行测试一下这个容器吧。
3. 小结
通过以上内容,相信大家对 Dockerfile 的使用又有了新的认知,我们在构建镜像的时候,一定要有合理的规划, 在自己不熟悉的基础镜像上定义镜像的时候,不妨先用它运行一个容器,在容器中过一遍流程, 弄清最终的镜像中到底应该包含哪些内容,再来调整构建脚本。
这里有一些 Dockerfile 的一般规范:
- 通过 Dockerfile 构建的镜像所启动的容器越快越好,这样可以快速启停增删容器服务(下面几条也是为第1条服务的);
- 避免安装不必要的包,必要时使用多阶段构建;
- 一个容器尽量只专注做一件事情;
- 最小化镜像层数, 将重复功能的
RUN、COPY、ADD
等指令缩减合并, 但一定要保证 Dockerfile 可读性。
当然,这些建议仅供参考,不要拘泥于它,要根据自己的使用场景来做权衡。