0%

docker 镜像构建篇

本文介绍 Docker 镜像构建中的注意事项

Docker 的使用场景

构建运行环境

一个程序的运行环境往往很复杂,如深度学习程序的运行环境,可能需要指定版本的操作系统,CUDA,cuDNN,gcc,python,各种依赖等等,自己从头搭建一个运行环境是很麻烦的,这时有人将运行环境构建成 base 镜像,然后基于 base 镜像运行程序,省去了自己搭建环境的过程

比如想使用 paddle 的训练、源码编译,直接运行对应的环境镜像,将代码挂载进去,就可以直接训练和编译了

1
docker run --name paddle -itd -v $PWD:/paddle registry.baidubce.com/paddlepaddle/paddle:2.4.1 /bin/bash

不过要注意,base镜像使用的 kernal 指令还是宿主机提供的,所以如果要求的操作系统是 Linux,而宿主机是 Mac,即使在 Mac 上运行了 centos 或 ubantu 的 base 镜像,也是无法运行的

临时的文件下载服务

在公司远程服务器上需要下载文件时,出于安全考虑,不能通过 xftp 等软件连接,这时想从服务器上下载一个文件,就比较麻烦了,这时可以用 docker 创建一个 httpserver 容器,临时开通一个 http 服务

1
docker run -itd --name my-apache-app -p 8080:80 -v "$PWD":/usr/local/apache2/htdocs/ httpd:2.4

下载完文件记得关闭这个容器,防止产生安全问题

服务部署

k8s 管理 docker 容器完成服务部署、调度、扩缩容等

镜像

镜像的分层结构

docker_layers.drawio

镜像分层最大的一个好处就是共享资源

比如:有多个镜像都从相同的 base 镜像构建而来,那么宿主机只需在磁盘上保存一份 base 镜像;同时内存中也只需加载一份 base 镜像,就可以为所有容器服务了

Q:如果多个容器共享一份基础镜像,当某个容器修改了基础镜像的内容,比如 /etc 下的文件,这时其他容器的 /etc 是否也会被修改?

A:不会的,修改会被限制在单个容器内,这就是容器的写时复制

镜像是如何被多个容器共享的

当容器启动时,一个新的可写层被加载到镜像顶部,称为容器层,容器层之下的叫做镜像层

只有容器层是可写的,容器层下面的所有镜像层都是只读的,所以对容器文件的改动都只会发生在容器层中

镜像层数量可能会很多,所有镜像层会联合在一起组成一个统一的文件系统。如果不同层中有一个相同路径的文件,比如 /etc,上层的 /etc 会覆盖下层的 /etc,也就是说用户只能访问到上层中的文件 /etc。在容器层中,用户看到的是一个叠加之后的文件系统

容器层如何变更文件

操作 实现
添加文件 新文件被写到容器层
读取文件 从上往下依次在各镜像层中查找此文件,找到后,打开并读入内存
修改文件 从上往下依次在各镜像层中查找此文件,找到后,将其复制到容器层,然后修改
删除文件 从上往下依次在各镜像层中查找此文件,找到后,会在容器层中记录此删除操作

写时复制 Copy On Write

只有当需要修改时才复制一份数据,这种特性被称作 Copy on Write。容器层保存的是镜像变化的部分,不会对镜像本身进行任何修改

Dockerfile

部署服务时,需要使用我们自己服务的镜像,就要用到 Dockerfile

Dockerfile 常用指令

指令 说明
FROM 指定 base 镜像
MAINTAINER 设置镜像的作者
WORKDIR 为后面的 RUN, CMD, ENTRYPOINT, ADD 或 COPY 指令设置镜像中的当前工作目录。
COPY 将文件从 build context 复制到镜像内
ADD 与 COPY 类似,不同的是,如果 src 是归档文件(tar, zip, tgz, xz 等),文件会被自动解压
ENV 设置环境变量
EXPOSE 指定容器中的进程会监听某个端口
VOLUME 将文件或目录声明为 volume
RUN 在容器中运行指定的命令
CMD 容器启动时运行指定的命令
Dockerfile 中可以有多个 CMD 指令,但只有最后一个生效
CMD 可以被 docker run 之后的参数替换
ENTRYPOINT 设置容器启动时运行的命令。
Dockerfile 中可以有多个 ENTRYPOINT 指令,但只有最后一个生效。CMD 或 docker run 之后的参数会被当做参数传递给 ENTRYPOINT

在执行 docker build -t {app}:{version} . 命令末尾的 . 指明 build context 为当前目录。Docker 默认会从 build context 寻找 Dockerfile 文件,也可以通过 -f 指定 Dockerfile 的位置

如:

1
2
3
4
FROM ubuntu
RUN apt-get update
RUN apt-get install -y vim
CMD ["/bin/bash"]

Dockerfile 构建过程

以上面dockerfile内容为例

  1. Docker 将 build context 中的所有文件发送给 Docker daemon
  2. 执行 FROM,下载 base 镜像
  3. 执行 RUN,创建临时容器 A,在临时容器中执行 apt-get update
  4. 执行成功后,将容器 A 保存为临时镜像 B
  5. 通过临时镜像 B,创建临时容器 C,在临时容器中执行apt-get install -y vim
  6. 执行成功后,将容器 C 保存为临时镜像 D
  7. 删除临时容器
  8. 镜像构建成功,最终镜像为镜像D

Dockerfile 的最佳实践

base 镜像的选择

  1. 使用固定版本的 base 镜像,不使用 latest 版本
  2. 使用轻量级 alpine 的发行版

尽量利用镜像层缓冲

在执行 build 时,每个镜像层都会被缓存下来,当需要构建时,若某一层指令或内容没有发生变更,会直接利用缓存层,提高构建速度

若某一层的命令或内容发生了变更,意味着后面的所有的镜像层缓存都不可被复用

Q:为什么某一层发生变更,后面的镜像层就不能被复用了呢?

A:先说下 git commit,这个命令用于创建镜像,在用 dockerfile 构建镜像时,底层是用 git commit 不断的创建镜像,包括若干临时镜像和一个最终镜像;比如在构建过程中,临时镜像A包括镜像层1-4,那么在执行下一个构建语句时,需要通过镜像A运行容器C,在容器C中执行相应的命令,再将容器C保存为镜像B,这样镜像B就包含了镜像层1-5。所以,如果某一层发生变更,该层对应的临时镜像以及后面的临时镜像都变了,无法复用

所以这个问题是带有欺骗性的,看起来是镜像层的复用,实际上是临时镜像的复用,在构建过程中,只有临时镜像被保存,没有镜像层被保存

  1. 将 dockerfile 涉及到的文件或内容,从最少变更步骤到最频繁变更进行排序,不频繁的放在上面,来提高缓存层复用率,提高镜像的构建速度

例如,有些 dockerfile 需要将项目文件拷贝到镜像中,这些文件是频繁变更的,要尽量往后放

镜像层尽量少

dockerfile中的每条指定都会创建一个镜像层(待验证),有些相同类型的操作,如果分开写,会导致生成的镜像文件存在多个镜像层中

  1. 将同类型的操作用 &&\ 来结合,减少镜像层,便于维护
1
2
3
4
ENV GO111MODULE=on \
GOPROXY="https://goproxy.io"
RUN yum makecache && \
yum -y install nginx mysql

使用 .dockerignore 文件

在构建镜像时,Docker Client 会将 build context 中的所有文件发送给 Docker daemon,如果 build context 下的文件体积很大,会导致编译时间很长甚至失败,还可能导致镜像体积过大

本人遇到的一个项目,由于上线流水线不健全,需要本地打镜像,当时本地日志打在项目目录下,且缺少 .dockerignore 文件,每次构建镜像时,都会将日志文件发送给 Docker daemon,还会 COPY 到镜像内,不仅编译时间长,且镜像体积大

如果通过流水线从代码库拉代码,正常情况下是不会有本地产物的,不过也可以通过 .dockeringore 文件去掉一些镜像不需要的文件,加快构建速度

  1. 在根目录下,可使用 .dockerfileignore 文件,排除掉无需加入的目录或文件,其编写的方式和 .gitignore 相同,加快构建速度

安全性

  1. 容器中使用非 root 用户启动应用
1
2
3
4
FROM node:17.0.1-alpine
...
USER work
...

其他方面

  1. 使用 RUN 指令安装依赖和软件包
  2. 不要使用 RUN apt-get upgrade
  3. 使用 RUN apt-get update && apt-get install -y 不要分成两个 RUN
  4. 清理掉 apt 缓存 rm -rf /var/lib/apt/lists/* 减少镜像体积
  5. 用于安装依赖的文件单独 COPY,如 requirements.txt

下面是一个完整的栗子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
FROM python:alpine3.17
RUN apt-get update && apt-get install -y \
aufs-tools \
automake \
build-essential \
curl \
dpkg-sig \
libcap-dev \
libsqlite3-dev \
mercurial \
reprepro \
ruby1.9.1 \
ruby1.9.1-dev \
s3cmd=1.1.* \
&& rm -rf /var/lib/apt/lists/*
USER work
WOKRDIR /home/work
COPY requirements.txt .
RUN pip install --requirement requirements.txt
COPY app .
EXPOSE 8000
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]

Dockerfile最佳实践

dockerfile 最佳实践