Docker镜像构建:Dockerfile编写基础——FROM、RUN、COPY、CMD
我记得自己刚接触Docker那会儿,第一件事就是写Dockerfile。那时候觉得不过是一堆指令堆砌,结果构建出来的镜像动不动就几个G,Run起来还各种报错。后来才明白,Dockerfile虽然就几个关键指令,但每一条都藏着坑。今天咱们就把FROM、RUN、COPY、CMD这四个最基础也最重要的指令掰开揉碎聊透。
1. FROM —— 镜像的“地基”
FROM是Dockerfile的第一条指令(除了ARG可以放在前面),说白了就是声明你要基于哪个基础镜像来构建。没有FROM,后面的指令全白搭。
我个人习惯把基础镜像选得尽量“瘦”。比如生信分析常用的biocontainers镜像,有的版本自带完整工具链,有的只装了个alpine。我遇到过项目组统一用ubuntu:22.04当基础镜像,结果一个镜像就占800MB,后续分层还特别多。后来我们切换成continuumio/miniconda3:latest,体积直接砍半。
| 基础镜像类型 | 体积(举例) | 适用场景 |
|---|---|---|
| scratch | 0 B | 静态编译二进制,极少用 |
| alpine | ~5 MB | 最小化环境,需手动装包 |
| ubuntu:22.04 | ~80 MB | 通用生信开发环境 |
| biocontainers/bwa | ~200 MB | 预装特定工具 |
:latest还是固定版本号——我上次被:latest坑过,某天它更新了,我的工作流全崩了。
2. RUN —— “干活”的指令
RUN在构建时执行命令,最常用的就是安装软件包、配置环境。但RUN也是最容易导致镜像膨胀的元凶。
你想想看,每执行一个RUN就会产生一个镜像层。层越多,拉取和存储开销越大。我见过有人把每个命令都写成一个RUN:
RUN apt-get update
RUN apt-get install -y python3
RUN apt-get install -y samtools
RUN pip install biopython
结果镜像有二十多层。正确的做法是:把相关的命令用 && 连在一起,一个RUN搞定。
RUN apt-get update && apt-get install -y \
python3 \
samtools \
&& pip install biopython \
&& rm -rf /var/lib/apt/lists/*
还有一个常犯的错误:安装完包后不清缓存。比如apt-get的/var/lib/apt/lists/*,pip的~/.cache/pip。这些文件在构建阶段就永远留在镜像里了。所以我总在RUN的最后加一句清理。
3. COPY —— 把文件“搬”进镜像
COPY用来把构建上下文中的文件或目录复制到镜像里。注意它和ADD的区别:ADD可以处理URL和解压,但咱们生信工作流中尽量用COPY,行为更可控。
我遇到过那种“直接COPY整个项目目录”的做法:
COPY . /app
结果.dockerignore没写,把本地几百MB的临时数据和.git目录全塞进去了。后来我强制团队在项目根目录加.dockerignore,比如:
__pycache__/
*.bam
*.fastq
.git/
.vscode/
*.pyc
COPY还有个细节:权限问题。默认COPY过来的文件属主是root。有时生信工具需要非root用户运行,我一般会在FROM之后先创建用户:
RUN groupadd -r bio && useradd -r -g bio bio
COPY --chown=bio:bio scripts/ /home/bio/scripts/
/opt/tools/而不是./tools/,避免混淆。CMD或ENTRYPOINT中使用绝对路径也更安全。
4. CMD —— 容器启动后的“默认命令”
CMD定义了容器运行时默认执行的命令。它有三种写法:shell格式、exec格式、作为ENTRYPOINT的参数。
| 写法 | 示例 | 特点 |
|---|---|---|
| shell格式 | CMD python app.py | 会在shell中执行,支持环境变量替换 |
| exec格式 | CMD ["python", "app.py"] | JSON数组,直接执行,不会触发sh,更可控 |
| 作为ENTRYPOINT的参数 | CMD ["--help"] | 当用了ENTRYPOINT时,CMD作为默认参数 |
我最推荐用exec格式。为什么?因为shell格式会用/bin/sh -c来启动,进程ID是1的不再是你的主程序,而是sh,sh收到SIGTERM后不会传递给子进程,导致容器无法优雅关闭。这个问题我在生产环境排查了整整一个下午才找到原因。
ENTRYPOINT ["samtools"]
CMD ["--help"]
这样运行容器时直接docker run myimage view -h就可以,而docker run myimage默认打印帮助。
重要提示:RUN与CMD的区别不可混淆
- 执行时机不同:RUN在构建阶段执行,CMD在运行容器时执行。
- 用途不同:RUN用于安装配置,CMD用于指定默认程序。
- 层数影响:每条RUN产生一个新层;CMD不会增加层,它只是元数据。
CMD pip install -r requirements.txt。结果每次启动容器都要重新安装,完全违背了“构建一次到处运行”的原则。正确做法是把安装放到RUN里。
知识体系总览:Dockerfile构建核心逻辑
下面这张图把FROM、RUN、COPY、CMD以及它们之间的关系、常见陷阱和最佳实践串起来,方便你对照回顾。
这张图表达了:Dockerfile里FROM是起点,RUN和COPY把构建上下文变成镜像内容,CMD定义启动行为。右边的“最佳实践”和左边的“常见陷阱”是我在实际项目中反复踩过的坑,你对照着检查自己的Dockerfile,基本能避免80%的问题。
组合示例:一个正经的生信Dockerfile
我拿一个bwa+samtools的镜像举个例子,你看怎么写才规范:
FROM ubuntu:22.04 AS build
RUN apt-get update && apt-get install -y \
bwa \
samtools \
&& rm -rf /var/lib/apt/lists/*
COPY my_scripts /opt/scripts/
COPY --chmod=755 run_pipeline.sh /usr/local/bin/
RUN chmod +x /opt/scripts/*.pl
ENTRYPOINT ["/usr/local/bin/run_pipeline.sh"]
CMD ["--help"]
注意到了吗?我用了AS build(多阶段构建的写法),虽然本章不展开,但多阶段构建能进一步压缩镜像体积。后面我专门有一节聊多阶段构建。
总结一下今天这章的核心:写Dockerfile不是堆指令,而是设计分层。FROM选对底子,RUN合并精简,COPY精准且注意权限,CMD用exec格式配合ENTRYPOINT。这四个指令掌握好,你的镜像质量和构建效率都能上一个台阶。