Dockerfile多阶段构建镜像
之前学习部署 Docker 应用时,我们搭建过一个 redis 服务,然后编写并运行了一个统计访问次数的 flask 应用。
现在,我们使用 Dockerfile,将这个 flask 应用也制作成镜像,此外,在这个镜像中,可以包含一个 helloworld 二进制程序,这个 helloworld
的源码就是我们学习 rootfs 时用到的 helloworld.c。
1. 实战:直接构建镜像
首先 我们需要新建一个目录 dockerfiledir,用于存放 Dockerfile 文件。
mkdir dockerfiledir
# 在这个目录下新建个空文件 Dockerfile,之后填充内容
touch dockerfiledir/Dockerfile
新建一个目录code,用来存放flask和c的源代码。
mkdir code
将之前 app.py 和 helloworld.c 两个源码文件放入到 code 目录下,当前的目录结构应该是这样的:
进入 dockerfiledir 目录,编辑 Dockerfile 文件:
# 从 ubuntu系统镜像开始构建
FROM ubuntu
# 标记镜像维护者信息
MAINTAINER user <user@imooc.com>
# 切换到镜像的/app目录,不存在则新建此目录
WORKDIR /app
# 将 宿主机的文件拷贝到容器中
COPY ../code/app.py .
COPY ../code/helloworld.c .
# 安装依赖 编译helloworld
RUN apt update >/dev/null 2>&1 && \
apt install -y gcc python3-flask python3-redis >/dev/null 2>&1 && \
cc /app/helloworld.c -o /usr/bin/helloworld
# 设定执行用户为user
RUN useradd user
USER user
# 设定flask所需的环境变量
ENV FLASK_APP app
# 默认启动执行的命令
CMD ["flask", "run", "-h", "0.0.0.0"]
# 将flask的默认端口暴露出来
EXPOSE 5000
然后执行:
docker build .
出现如下报错:
COPY failed: Forbidden path outside the build context: ../code/app.py ()
解决这个问题,需要引入一个重要的概念——构建上下文。
docker build .
命令在执行时,当前目录.
被指定成了构建上下文,此目录中的所有文件或目录都将被发送到 Docker 引擎中去,Dockerfile中的切换目录和复制文件等操作只会对上下文中的内容生效。
Tips:在默认情况下,如果不额外指定 Dockerfile 的话,会将构建上下文对应的目录下 Dockerfile 的文件作为 Dockerfile。但这只是默认行为,实际上 Dockerfile 的文件名并不要求必须为 Dockerfile,而且并不要求必须位于上下文目录中,比如可以用
-f ../demo.txt
参数指定父级目录的demo.txt文件作为 Dockerfile。一般来说,我们习惯使用默认的文件名 Dockerfile,将其置于镜像构建上下文目录
.
中。
我们需要将 code 目录纳入到上下文中,一个直接的方法是,调整dockerfile中的COPY指令的路径。
# 将 .. 改为 .
COPY ./code/app.py .
COPY ./code/helloworld.c .
然后将 code 所在的目录指定为构建上下文。由于我们当前的目录是 dockerfiledir,所以我们执行:
docker build -f ./Dockerfile ..
如果你留意查看构建过程,会发现类似这样的提示:
Sending build context to Docker daemon 421.309 MB
如果..
目录除了code和dockerfiledir,还包含其他的文件或目录,docker build
也会将这个数据传输给Docker,这会增加构建时间。
避免这种情况,有两种解决方法:
-
使用
.dockerignore
文件:在构建上下文的目录下新建一个.dockerignore
文件来指定在传递给 docker 时需要忽略掉的文件或文件夹。.dockerignore 文件的排除模式语法和 Git 的 .gitignore 文件相似。 -
使用一个干净的目录作为构建上下文(推荐):使用 Dockerfile 构建镜像时最好是将 Dockerfile 放置在一个新建的空目录下。然后将构建镜像所需要的文件添加到该目录中。
在我们当前的示例中,将code目录移入dockerfiledir。
mv ../code .
现在的目录层级如下:
执行 docker build -t myhello .
执行构建即可获得我们的自定义镜像 myhello。
使用镜像 myhello 创建 myhello 容器:
# 这里使用--net=host,方便使用之前章节中部署的redis容器服务,与之进行数据交换
docker run -dit --net=host --name myhello myhello
确保部署之前的 redis 容器正常启动,然后在 Docker 宿主机的浏览器中访问http://127.0.0.1:5000
:
说明 myhello 中的 flask 应用已经正常运行了。接下来,我们再运行测试一下编译的 helloworld。
docker exec myhello /usr/bin/helloworld
得到输出:
Hello, World!
Tips: myhello容器已经完成任务,记得执行
docker rm -f myhello
删除它.
2. 改进: 使用多阶段构建
在镜像构建过程中,我们的 helloworld.c 源码以及相关编译工具和依赖也被构建到了镜像中,这导致我们最终得到的镜像偏大。
理想状态应该是使用了一个系统镜像生成的容器,编译源码后再将编译的程序导入到最终的镜像中,这样就会缩减体积,并且将不同目的的操作有效分离开,但是按照我们之前掌握的知识,这样实现需要两个Dockerfile 文件。
使用多阶段构建,我们可以在一个 Dockerfile
中使用多个 FROM 语句。每个 FROM 指令都可以使用不同的镜像,并表示开始一个新的构建阶段。很方便的将一个阶段的文件复制到另外一个阶段,在最终的镜像中保留下需要的内容即可。
我们还是在 Dockerfile 文件的同一目录,新建一个新的构建脚本,命名为 Dockerfile-multi-stage 便于区分:
#从ubuntu镜像开始构建, 将第一阶段命名为`build`,在其他阶段需要引用的时候使用`--from=build`参数即可。
FROM ubuntu AS build
# 将宿主机的源码拷贝到镜像中
COPY ./code/helloworld.c .
# 安装依赖 并编译源码
RUN apt update >/dev/null 2>&1 && \
apt install -y gcc >/dev/null 2>&1 && \
cc helloworld.c -o /usr/bin/helloworld
# 第二阶段 从官方的python:alpine基础镜像开始构建
FROM python:alpine
# 镜像维护者信息
MAINTAINER user <user@imooc.com>
# 将第一阶段构建的helloworld 导入到此镜像中
COPY --from=build /usr/bin/helloworld /usr/bin/helloworld
# 安装flask 和 redis 的依赖
RUN pip install flask redis >/dev/null 2>&1
# 设定镜像在切换到/app目录路径
WORKDIR /app
# 将源码导入到镜像
COPY ./code/app.py .
# 设定执行用户为user
RUN useradd user
USER user
# 设定flask所需的环境变量
ENV FLASK_APP app
# 默认启动执行的命令
CMD ["flask", "run", "-h", "0.0.0.0"]
# 将flask的默认端口暴露出来
EXPOSE 5000
执行 build 命令:
docker build -f Dockerfile-multi-stage -t myhello-multi-stage .
使用此镜像运行一个容器:
# 这里使用--net=host,方便使用之前章节中部署的redis容器服务,与之进行数据交换
docker run -dit --net=host --name myhello-multi-stage myhello-multi-stage
自行测试一下这个容器吧。
3. 小结
通过以上内容,相信大家对 Dockerfile 的使用又有了新的认知,我们在构建镜像的时候,一定要有合理的规划, 在自己不熟悉的基础镜像上定义镜像的时候,不妨先用它运行一个容器,在容器中过一遍流程, 弄清最终的镜像中到底应该包含哪些内容,再来调整构建脚本。
这里有一些 Dockerfile 的一般规范:
- 通过 Dockerfile 构建的镜像所启动的容器越快越好,这样可以快速启停增删容器服务(下面几条也是为第1条服务的);
- 避免安装不必要的包,必要时使用多阶段构建;
- 一个容器尽量只专注做一件事情;
- 最小化镜像层数, 将重复功能的
RUN、COPY、ADD
等指令缩减合并, 但一定要保证 Dockerfile 可读性。
当然,这些建议仅供参考,不要拘泥于它,要根据自己的使用场景来做权衡。