2. Docker基础回顾:镜像分层原理、Dockerfile基础指令、构建上下文

说实话,很多人学Docker都是囫囵吞枣。会用docker run跑个nginx就觉得会了。但真到要自己写Dockerfile、优化镜像体积的时候,就抓瞎了。

我个人习惯是,先把底层原理搞明白。镜像分层这东西,你理解了它,后面所有优化技巧都是顺理成章的事。

2.1 镜像分层原理

Docker镜像不是一整块大文件。它是一层一层堆叠起来的。每一层,都对应Dockerfile里的一条指令。

我举个例子。你写个简单的Dockerfile:

FROM ubuntu:22.04
RUN apt-get update && apt-get install -y python3
COPY app.py /app/
CMD ["python3", "/app/app.py"]

这个镜像构建完,会有4层:

  • 基础层:ubuntu:22.04 本身
  • 安装层:apt-get 安装python3产生的变化
  • 文件层:COPY指令添加的app.py
  • 元数据层:CMD指令记录的启动命令

每一层都是只读的。你想想看,这有什么好处?

缓存复用。如果下次构建时,Dockerfile前三条指令没变,那前三层直接命中缓存。只有变了的那一层及其后面的层才需要重新构建。

我在项目中遇到过一个问题:团队里有人把apt-get install和COPY写在一起。结果每次改代码,都要重新安装一遍依赖。构建一次十几分钟,大家苦不堪言。

核心要点:把不常变的内容(系统依赖、基础工具)放在Dockerfile前面。把经常变的内容(代码、配置文件)放在后面。这样能最大化利用缓存。

为了让你更直观地理解,我画了一张图:

Docker 镜像分层结构 CMD ["python3", "/app/app.py"] 第4层 COPY app.py /app/ 第3层 RUN apt-get install -y python3 第2层 FROM ubuntu:22.04 第1层 构建顺序 → 每层只读 可缓存复用 上层依赖下层

小技巧:用 docker history 镜像名 可以查看镜像的分层历史。我经常用这个命令来排查镜像为什么这么大。

2.2 Dockerfile基础指令

Dockerfile的指令不多,但每个都有讲究。我挑几个最常用的说说。

FROM

一切的基础。指定基础镜像。我建议尽量选官方镜像,比如 python:3.11-slim 而不是 python:3.11。slim版本能省不少空间。

RUN

执行命令。注意,每个RUN都会产生一个新层。所以别写一堆RUN,能合并就合并。

# 不推荐
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y vim

# 推荐
RUN apt-get update && \
    apt-get install -y curl vim && \
    rm -rf /var/lib/apt/lists/*

嗯,这里要注意。最后那个rm -rf是干嘛的?清理apt缓存。这些缓存留在镜像里毫无意义,只会让镜像变大。

COPY 与 ADD

两者都能把文件从宿主机拷贝到镜像里。区别是ADD支持自动解压tar包、支持URL下载。但我个人习惯只用COPY。为什么?ADD的行为太隐晦了。你想想看,一个ADD package.tar.gz /app/,它到底是在拷贝还是在解压?代码可读性很差。

避坑指南:我曾经在项目里看到有人用ADD从URL下载文件。结果网络一波动,构建就失败。而且下载的文件没有缓存,每次构建都要重新下载。后来全改成COPY + RUN curl了,可控性高很多。

CMD 与 ENTRYPOINT

这两个容易搞混。简单说:

  • CMD:提供默认启动命令,可以被 docker run 后面的参数覆盖
  • ENTRYPOINT:固定启动命令,不容易被覆盖

我常用的组合是:

ENTRYPOINT ["python3"]
CMD ["app.py"]

这样用户想跑别的脚本时,只需要 docker run myimage other.py 就行。灵活又清晰。

2.3 构建上下文

这个概念很多人会忽略。构建上下文,就是你执行 docker build 时指定的那个目录。

docker build -t myapp:latest .

这个 . 就是构建上下文。Docker会把整个上下文目录打包发送给Docker守护进程。哪怕Dockerfile里只COPY了一个文件,整个目录都会被发送过去。

我见过最夸张的例子:有人把项目根目录作为构建上下文,里面有个 node_modules 文件夹,足足2GB。每次构建光传输上下文就要等几分钟。

最佳实践:使用 .dockerignore 文件。把不需要的文件排除掉,比如 node_modules、.git、__pycache__ 等。语法和 .gitignore 一样。

举个例子,一个Python项目的 .dockerignore:

.git
__pycache__
*.pyc
.env
venv/
.idea/
*.log

这样构建上下文就干净多了。传输快,构建也快。

还有一个细节:Dockerfile里引用文件时,路径是相对于构建上下文的。你不能 COPY ../something /app/,因为上级目录不在上下文中。这个限制我一开始也觉得烦,但后来想想,这其实是在逼你保持项目结构清晰。

指令 作用 个人建议
FROM 指定基础镜像 优先选 slim/alpine 版本
RUN 执行命令 合并多个RUN,清理缓存
COPY 拷贝文件 比ADD更可控,推荐使用
CMD 默认启动命令 配合ENTRYPOINT使用更灵活
.dockerignore 排除无关文件 必须用,能省大量时间

好了,这一章的内容就这些。说白了,镜像分层是理解Docker的基石,Dockerfile指令是构建镜像的工具,构建上下文是容易被忽视的细节。这三样搞明白了,后面讲镜像裁剪的时候你就能跟上节奏了。

可可精品资料库