您好, 欢迎来到 !    登录 | 注册 | | 设为首页 | 收藏本站

Dockerfile多阶段构建镜像

之前学习部署 Docker 应用时,我们搭建过 redis 服务,然后编写并运行了访问的 flask 应用。

现在,我们使用 Dockerfile,将这个 flask 应用也制作成镜像,此外,个镜像中,可以包含 helloworld 二进制程序,这个 helloworld 的源码就是我们学习 rootfs 时用到的 helloworld.c。

首先 我们需要新建目录 dockerfiledir,用于存放 Dockerfile 。

mkdir dockerfiledir
# 个目录下新建个空 Dockerfile,之后填充 
touch dockerfiledir/Dockerfile

新建目录code,用来存放flask和c的源。

mkdir code

将之前 和 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 -flask -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 : 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它.

在镜像构建过程中,我们的 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

自行测试一下这个容器吧。

通过以上,相信大家对 Dockerfile 的使用又有了新的认知,我们在构建镜像的时候,一定要有合理的规划, 在自己不熟悉的基础镜像上定义镜像的时候,不妨先用它运行容器,在容器中过一遍流程, 弄清最终的镜像中到底应该包含哪些,再来调整构建脚本。

这里有一些 Dockerfile 的一般规范:

当然,这些建议仅供参考,不要拘泥于它,要根据自己的使用场景来做权衡。


联系我
置顶