编写好Dockerfile,构建镜像,一切看似正常。但随后发现镜像大小超过1GB,即便是微小改动,重建也要花费数分钟,每次推送或拉取都感觉异常缓慢。
这并不罕见。如果不考虑基础镜像选择、构建上下文和缓存机制,直接编写Dockerfile,就会出现这些默认结果。修复这些问题并不需要彻底重构。只需几个有针对性的改动,就能将镜像体积缩小60%-80%,并将大多数重建过程从数分钟缩短至数秒。
本文将介绍五种实用技术,让Docker镜像更小、更快、更高效。
跟随本文操作,需要具备:
docker build命令的基本用法requirements.txt文件的Python项目(示例使用Python,但原则适用于任何编程语言)每个Dockerfile都以FROM指令开始,选择一个基础镜像。这个基础镜像就是应用程序运行的基础,其体积决定了在未添加任何自定义代码之前的最小镜像大小。
例如,官方python:3.11镜像是完整的基于Debian的镜像,包含了大多数应用程序永远不会用到的编译器、工具和软件包。
# 完整镜像 — 包含所有内容
FROM python:3.11
# Slim镜像 — 最小化Debian基础
FROM python:3.11-slim
# Alpine镜像 — 更小,基于musl的Linux
FROM python:3.11-alpine分别从每个镜像构建,并检查大小:
docker images | grep python仅改变Dockerfile中的一行,就会发现几百兆字节的差异。那么应该使用哪一个?
slim 对大多数Python项目来说是更安全的默认选择。它去掉了不必要的工具,但保留了众多Python包正确安装所需的C库。alpine 更小,但它使用不同的C库——musl而不是glibc——这可能导致与某些Python包的兼容性问题。最终调试pip install失败所花费的时间,可能比节省的镜像大小还要多。经验法则:从python:3.1x-slim开始。只有在确定依赖项兼容且需要额外缩减体积时,才切换到alpine。
Docker逐层构建镜像,一次一条指令。一旦某个层被构建,Docker会缓存它。在下一次构建时,如果没有影响该层的更改,Docker会复用缓存版本,跳过重建。
问题在于:如果一个层发生更改,其后的每一个层都会失效,并从头开始重建。
这对依赖项安装影响很大。下面是一个常见错误:
# 错误的层顺序 — 每次代码更改都会重新安装依赖项
FROM python:3.11-slim
WORKDIR /app
COPY . . # 复制所有内容,包括代码
RUN pip install -r requirements.txt # 位于COPY之后,任何文件更改都会导致其重新运行每次修改脚本中的一行代码,Docker都会使COPY . .层失效,然后从头重新安装所有依赖项。对于一个有着庞大requirements.txt的项目,每次重建都会浪费数分钟。
修复方法很简单:先复制那些最不常更改的内容。
# 正确的层顺序 — 除非requirements.txt更改,否则依赖项被缓存
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt . # 先只复制requirements文件
RUN pip install --no-cache-dir -r requirements.txt # 安装依赖 — 该层被缓存
COPY . . # 最后复制代码 — 只有这一层会在代码更改时重新运行
CMD ["python", "app.py"]现在,当更改app.py时,Docker会复用缓存的pip层,只重新运行最后的COPY . .。
经验法则:按照从最不常更改到最常更改的顺序排列COPY和RUN指令。始终先放依赖项,后放代码。
有些工具只在构建时需要——编译器、测试运行器、构建依赖项——但它们最终仍会出现在最终镜像中,导致镜像臃肿,包含运行应用程序永远不会用到的东西。
多阶段构建解决了这个问题。使用一个阶段来构建或安装所需的所有内容,然后只将最终的输出复制到一个干净、最小的最终镜像中。构建工具永远不会进入最终交付的镜像。
以下是一个Python示例,需要安装依赖项但希望保持最终镜像精简:
# 单阶段 — 构建工具最终会存在于最终镜像中
FROM python:3.11-slim
WORKDIR /app
RUN apt-get update && apt-get install -y gcc build-essential
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "app.py"]现在使用多阶段构建:
# 多阶段 — 构建工具只保留在构建阶段
# 阶段1: 构建器 — 安装依赖项
FROM python:3.11-slim AS builder
WORKDIR /app
RUN apt-get update && apt-get install -y gcc build-essential
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
# 阶段2: 运行时 — 仅包含所需内容的干净镜像
FROM python:3.11-slim
WORKDIR /app
# 仅从构建阶段复制已安装的包
COPY --from=builder /install /usr/local
COPY . .
CMD ["python", "app.py"]gcc和build-essential工具——编译某些Python包所需——已从最终镜像中消失。由于编译后的包已被复制过来,应用程序仍然可以正常工作。构建工具本身留在了构建阶段,Docker会丢弃该阶段。这种模式在Go或Node.js项目中效果更显著,编译器或大小达几百兆字节的node模块可以被完全排除在交付的镜像之外。
当使用apt-get安装系统包时,包管理器会下载包列表并缓存运行时不需要的文件。如果在单独的RUN指令中删除它们,这些文件仍然存在于中间层中,并且由于Docker的层机制,它们仍然会占用最终镜像的空间。
要真正移除它们,清理操作必须与安装操作在同一个RUN指令中完成。
# 在单独层中清理 — 缓存文件仍然使镜像臃肿
FROM python:3.11-slim
RUN apt-get update && apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/* # 该操作已在上一层提交
# 在同一层中清理 — 没有任何残留提交到镜像中
FROM python:3.11-slim
RUN apt-get update && apt-get install -y curl \
&& rm -rf /var/lib/apt/lists/*同样的逻辑适用于其他包管理器和临时文件。
经验法则:任何apt-get install都应该在同一个RUN命令中跟上&& rm -rf /var/lib/apt/lists/*。使之成为一种习惯。
运行docker build时,Docker会将构建目录中的所有内容发送给Docker守护进程作为构建上下文。这发生在Dockerfile中的任何指令运行之前,并且通常包含了几乎肯定不希望放入镜像中的文件。
没有.dockerignore文件,就会发送整个项目文件夹:.git历史、虚拟环境、本地数据文件、测试夹具、编辑器配置等等。这会拖慢每次构建,并有将敏感文件复制到镜像中的风险。
.dockerignore文件的工作原理与.gitignore完全相同;它告诉Docker在构建上下文中排除哪些文件和文件夹。
以下是一个典型的Python数据项目的.dockerignore示例(经过精简):
# Python
__pycache__/
*.pyc
*.pyo
*.pyd
.Python
*.egg-info/
# 虚拟环境
.venv/
venv/
env/
# 数据文件(不要将大型数据集构建到镜像中)
data/
*.csv
*.parquet
*.xlsx
# Jupyter
.ipynb_checkpoints/
*.ipynb
# 测试
tests/
pytest_cache/
.coverage
# 密钥 — 绝不能让这些进入镜像
.env
*.pem
*.key这能显著减少构建开始前发送给Docker守护进程的数据量。在大型数据项目中,如果项目文件夹中存放着parquet文件或原始CSV文件,这可能是所有五个实践中效果最显著的一个。
另外还有安全角度值得注意。如果项目文件夹中包含带有API密钥或数据库凭证的.env文件,忘记使用.dockerignore意味着这些秘密可能最终被构建到镜像中——尤其是存在一条宽泛的COPY . .指令时。
经验法则:始终将.env和任何凭证文件添加到.dockerignore中,同时也要添加那些不需要构建到镜像中的数据文件。对于敏感数据,还应使用Docker secrets。
这些技术都不需要深入的Docker知识;它们更多是习惯而非技术。坚持应用它们,镜像会更小,构建会更快,部署会更干净。
实践 | 解决的问题 |
|---|---|
Slim/Alpine基础镜像 | 仅从必要的操作系统包开始,确保更小的镜像 |
层顺序 | 避免每次代码更改都重新安装依赖项 |
多阶段构建 | 将构建工具排除在最终镜像之外 |
同层清理 | 防止apt缓存使中间层臃肿 |
.dockerignore | 减少构建上下文,防止密钥进入镜像 |
愉快地编码!FINISHED
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。