Dockerfile如何优化?注意:千万不要只说减少层数

如果优化 Dockerfile?

小伙伴没有回答好,只是提到了减少镜像层数。

一般来说,面试的小伙伴,大部分都会说

  1. 使用更小的基础镜像, 比如 alpine.
  2. 减少镜像层数, 比如 使用 && 符号将命令链接起来。
  3. 给基础镜像打上 安全补丁

但这些,其实都是单点的优化。优化 Dockerfile 的核心是 合理分层、构建一个精良的基础镜像


为什么要优化镜像

首先,回到起点。为啥要优化 镜像?优化镜像的好处是:

  • 一个小镜像有什么好处 : 分发更快,存储更少,加载更快。
  • 镜像臃肿带来了什么问题 : 存储过多,分发更慢且浪费带宽更多。

镜像的构成

其次,来看看镜像的构成。从两个维度来看:

  • 俯瞰镜像 : 就是一个删减版的操作系统。
  • 侧看镜像 : 由一层层的 layer 堆叠而成

那么问题来了

应该如何优化镜像?

举个例子 docker build

  • Dockerfile v1
# v1
FROM nginx:1.15-alpine
RUN echo "hello"
RUN echo "demo best practise"
ENTRYPOINT [ "/bin/sh" ]
  • Dockerfile v2
# v2
FROM nginx:1.15-alpine
RUN echo "hello"
RUN echo "demo best practise"
ENTRYPOINT [ "/bin/sh" ]

1st build

全新构建

# docker build -t demo:0.0.1 .                          
Sending build context to Docker daemon  2.048kB
Step 1/4 : FROM nginx:1.15-alpine
 ---> 9a2868cac230
Step 2/4 : RUN echo "hello"
 ---> Running in d301b4b3ed55
hello
Removing intermediate container d301b4b3ed55
 ---> 6dd2a7773bbc
Step 3/4 : RUN echo "demo best practise"
 ---> Running in e3084037668e
demo best practise
Removing intermediate container e3084037668e
 ---> 4588ecf9837a
Step 4/4 : ENTRYPOINT [ "/bin/sh" ]
 ---> Running in d63f460347ff
Removing intermediate container d63f460347ff
 ---> 77b52d828f21
Successfully built 77b52d828f21
Successfully tagged demo:0.0.1

2nd build

Dockerfile 与 1st build 完全一致, 命令仅修改 build tag , 从 0.0.1 到 0.0.2

# docker build -t demo:0.0.2 .
Sending build context to Docker daemon  4.096kB
Step 1/4 : FROM nginx:1.15-alpine
 ---> 9a2868cac230
Step 2/4 : RUN echo "hello"
 ---> Using cache
 ---> 6dd2a7773bbc
Step 3/4 : RUN echo "demo best practise"
 ---> Using cache
 ---> 4588ecf9837a
Step 4/4 : ENTRYPOINT [ "/bin/sh" ]
 ---> Using cache
 ---> 77b52d828f21
Successfully built 77b52d828f21
Successfully tagged demo:0.0.2

可以看到,

  1. 每层 layer 都使用 cache (—> Using cache) ,并未重新构建。
  2. 我们可以通过 docker image ls |grep demo 看到, demo:0.0.1 与 demo:0.0.2 的 layer hash 是相同。

所以从根本上来说, 这两个镜像就是同一个镜像,虽然都是 build 出来的。

3rd build

这次, 我们将Dockerfile 02的 第三层 RUN echo “demo best practise” 变更为 RUN echo “demo best practise 02”

docker build -t demo:0.0.3 .
Sending build context to Docker daemon  4.608kB
Step 1/4 : FROM nginx:1.15-alpine
 ---> 9a2868cac230
Step 2/4 : RUN echo "hello"
 ---> Using cache
 ---> 6dd2a7773bbc
Step 3/4 : RUN echo "demo best practise 02"
 ---> Running in c55f94e217bd
demo best practise 02
Removing intermediate container c55f94e217bd
 ---> 46992ea04f49
Step 4/4 : ENTRYPOINT [ "/bin/sh" ]
 ---> Running in f176830cf445
Removing intermediate container f176830cf445
 ---> 2e2043b7f3cb
Successfully built 2e2043b7f3cb
Successfully tagged demo:0.0.3

可以看到 ,

  1. 第二层仍然使用 cache
  2. 但是第三层已经生成了新的 hash 了
  3. 虽然第四层的操作没有变更,但是由于上层的镜像已经变化了,所以第四层本身也发生了变化。

注意: 每层在 build 的时候都是依赖于上册 —> Running in f176830cf445。

4th build

第四次构建, 这次使用 --no-cache 不使用缓存, 模拟在另一台电脑上进行 build 。

# docker build -t demo:0.0.4 --no-cache .  
Sending build context to Docker daemon  5.632kB
Step 1/4 : FROM nginx:1.15-alpine
 ---> 9a2868cac230
Step 2/4 : RUN echo "hello"
 ---> Running in 7ecbed95c4cd
hello
Removing intermediate container 7ecbed95c4cd
 ---> a1c998781f2e
Step 3/4 : RUN echo "demo best practise 02"
 ---> Running in e90dae9440c2
demo best practise 02
Removing intermediate container e90dae9440c2
 ---> 09bf3b4238b8
Step 4/4 : ENTRYPOINT [ "/bin/sh" ]
 ---> Running in 2ec19670cb14
Removing intermediate container 2ec19670cb14
 ---> 9a552fa08f73
Successfully built 9a552fa08f73
Successfully tagged demo:0.0.4

可以看到,

  1. 虽然和 3rd build 使用的 Dockerfile 相同, 但由于没有缓存,每一层都是重新 build 的。
  2. 虽然 demo:0.0.3 和 demo:0.0.4 在功能上是一致的。但是 他们的 layer 不同, 从根本上来说,他们是不同的镜像。

结论

1. 合理分层、构建一个精良的基础镜像

  1. 一个相对固定的 build 环境
  2. 善用 cache
  3. 构建 自己的基础镜像:其中就包括了

a. 安全补丁
b. 权限限制
c. 基础库依赖安装
d. 等…

2. 精简为美:一屋不扫何以扫天下

  1. 使用 .dockerignore 保持 context 干净
  2. 容器镜像环境清理
    a. 缓存清理
    b. multi stage build

参考文献

16 个赞

软件分享软件开发

感谢分享,多阶段构建+distroless这样的基础镜像能减少不少体积

这个标题,有那味了

2 个赞

图裂了

虽然说了很多,但是我感觉什么都没看到 :face_with_peeking_eye:

我这个要怎么优化呢

FROM python:3.9-slim

# 设置工作目录
WORKDIR /app

# 复制文件到容器的/app目录
COPY app /app
RUN chmod +x /app/entrypoint.sh

# 安装python依赖
RUN pip install --upgrade pip && pip install --no-cache-dir -r requirements.txt

# 添加环境变量
ENV PATH="/usr/local/bin:$PATH"

# 启动应用
CMD ["bash", "/app/entrypoint.sh"]

1 个赞

学习了!

编译出来放更小镜像运行?

听君一席话,胜似一席话 :clown_face:

我这个python脚本不用编译呀

脚本也能编译,能省掉python环境的空间

两个RUN可以写在一起,减少一层

不错哦,利用缓存加速构建(核心就是把不变的尽量放前面)和多层构建(一般是编译和运行环境分离),提高构建效率和减少镜像大小。
大家可以看一下 nerdctl 这个工具可以调试 dockerfile 文件,还提出了 lazy-pulling 这种提高拉去效率的功能。

1 个赞

构建的人爽了,使用的人层数太多了

2 个赞

层数多了,能共用的几率就越高,可以减少拉取时间啊。

Python Docker镜像最佳实践:

  • pip 小技巧: --no-cache-dir

  • 设置 2 个 Python 的环境变量

    • ENV PYTHONDONTWRITEBYTECODE 1
    • ENV PYTHONUNBUFFERED 1
  • 使用 Alpine 作为基础镜像(如果没有pandas sci-kit等cpython依赖)

  • 多阶段构建(python一般没必要,因为不能向go一样打包为单一二进制文件)


Python不像Go那样可以直接打包为二进制,主要是依赖占空间

FROM python:3.9-apline

# 设置python环境变量,不生成pyc文件,不缓冲输出
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

# 设置工作目录
WORKDIR /app

# 先安装python依赖,再复制文件,这样可以利用docker的缓存机制(这不能减少镜像大小,但可以减少构建时间)
RUN pip install --upgrade pip && pip install --no-cache-dir -r requirements.txt

# 复制文件到容器的/app目录
COPY app /app
RUN chmod +x /app/entrypoint.sh

# 添加环境变量
ENV PATH="/usr/local/bin:$PATH"

# 启动应用
CMD ["bash", "/app/entrypoint.sh"]
1 个赞

FROM python:alpine

感谢分享!
不过似乎不能将COPY放在pip install之后,不COPY进去就没有requirements.txt这个文件

主要改动是换成apline,设置python环境变量,效果显著,变化如下:
原196.6 MB,新91.1 MB

不过我的容器跑不起来了,哈哈哈,好像alpine里连bash都没有,换成sh重新构建可以用了

FROM python:3.9-alpine

# 设置python环境变量,不生成pyc文件,不缓冲输出
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

# 设置工作目录
WORKDIR /app

# 复制文件到容器的/app目录
COPY app /app
RUN chmod +x /app/entrypoint.sh

# 安装python依赖
RUN pip install --upgrade pip && pip install --no-cache-dir -r requirements.txt

# 添加环境变量
ENV PATH="/usr/local/bin:$PATH"

# 启动应用
CMD ["sh", "/app/entrypoint.sh"]

可以先COPY requirements.txt,当然这主是方便改动代码后部署测试是重新build快一点