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 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指令是构建镜像的工具,构建上下文是容易被忽视的细节。这三样搞明白了,后面讲镜像裁剪的时候你就能跟上节奏了。
可可精品资料库