4. Dockerfile 编写基础
好,我们开始聊 Dockerfile。说白了,这就是一份“自动做镜像的菜谱”。你写好每一步,Docker 照着执行,最终得到一个干净的、可复用的环境。我在早期做宏基因组分析流程时,就因为手写装环境,导致不同机器上结果不一致。后来彻底改成 Dockerfile,再没出过这种幺蛾子。
一句话总结:Dockerfile 就是镜像的构建脚本。每一行指令都会生成一个新的镜像层,层数越少,体积越小,构建越快。
4.1 核心指令速览
先从最常用的几个指令入手。我习惯把它们分成三组:基础定义、文件与依赖、运行时配置。
| 分类 | 指令 | 作用 |
|---|---|---|
| 基础定义 | FROM | 指定基础镜像,必须是第一条指令 |
| 基础定义 | LABEL | 添加元数据,比如作者、版本 |
| 文件与依赖 | RUN | 执行命令,通常用来安装软件包 |
| 文件与依赖 | COPY / ADD | 把本地文件拷进镜像(ADD 支持自动解压) |
| 文件与依赖 | WORKDIR | 设置工作目录,后面的指令都在这个目录下跑 |
| 运行时配置 | EXPOSE | 声明容器监听端口(文档作用,不实际打开) |
| 运行时配置 | CMD | 提供默认启动命令,可被 docker run 参数覆盖 |
| 运行时配置 | ENTRYPOINT | 固定启动命令,不易被覆盖 |
⚠ ADD 和 COPY 的区别:ADD 能自动解压 tar 文件,还能从 URL 下载。但我建议优先用 COPY,行为明确,不容易误触。你想想看,万一 URL 失效了,构建就卡死了。
4.2 一个真实案例:宏基因组分析基础镜像
我们直接拿宏基因组分析中最常用的软件——Kraken2 和 Bracken 来写个 Dockerfile。注意看每个指令的排列顺序。
# 基础镜像:Ubuntu 22.04 LTS,轻量且稳定
FROM ubuntu:22.04 AS builder
# 设置作者和版本
LABEL maintainer="yourname@lab.com" \
version="1.0" \
description="宏基因组分析基础镜像"
# 避免交互式安装弹窗
ENV DEBIAN_FRONTEND=noninteractive
# 先更新包管理器,再安装编译工具
RUN apt-get update && apt-get install -y \
build-essential \
wget \
zlib1g-dev \
libbz2-dev \
liblzma-dev \
&& rm -rf /var/lib/apt/lists/*
# 下载并编译 Kraken2
WORKDIR /opt
RUN wget https://github.com/DerrickWood/kraken2/archive/refs/tags/v2.1.2.tar.gz \
&& tar -xzf v2.1.2.tar.gz \
&& cd kraken2-2.1.2 \
&& ./install_kraken2.sh /opt/kraken2
# 复制本地准备好的 Bracken 脚本(假设已编译好)
COPY bracken-2.7.tar.gz /opt/
WORKDIR /opt
RUN tar -xzf bracken-2.7.tar.gz && rm bracken-2.7.tar.gz
# 设置 PATH
ENV PATH="/opt/kraken2:/opt/bracken-2.7:$PATH"
# 声明容器使用端口(此处仅为示例,实际宏基因组分析很少用端口)
EXPOSE 8080
# 默认进入工作目录
WORKDIR /data
# 容器启动时打印帮助信息
CMD ["kraken2", "--help"]
上面这段 Dockerfile 有几个关键点:
- 合并 RUN 层:apt-get update && install ... && rm 连在一起,只产生一层。我记得刚学 Docker 时没注意这个,镜像体积蹭蹭涨到 2G+,后来发现每多一个 RUN 就多一层,一层几十 MB 堆上去就炸了。
- WORKDIR 切换:每次 cd 都用 WORKDIR,比 RUN cd 更清晰,还能自动创建目录。
- ENV 设置:把 PATH 和环境变量统一写在最后,方便查看。
4.3 指令顺序:为什么 FROM 必须在最前面?
Docker 构建是按行执行的。每一行都生成一个中间镜像层。构建器会判断哪些层没变,然后复用缓存。所以指令顺序直接影响构建速度。
我给你的建议是:把不容易变的指令放在前面,容易变的放后面。比如:
- FROM、LABEL、ENV —— 几乎不变。
- 安装系统包 —— 偶尔变。
- COPY 本地代码/脚本 —— 经常变。
- 最后的 CMD/ENTRYPOINT —— 几乎不变。
如果反着写,每次改个脚本就得从头装系统包,浪费时间。我曾经在 CI 里吃过这个亏,每次修改一个 Python 文件,整个构建要重跑 5 分钟……后来调整顺序,缩短到 10 秒。
4.4 .dockerignore:你的“隐形过滤器”
你有没有遇到过:构建镜像时,不小心把本地的 huge fastq 文件或 .git 目录也拷进去了?结果镜像体积爆表,传输慢到哭。
.dockerignore 就是干这个的。它和 .gitignore 语法一致,放在 Dockerfile 同目录下。
# .dockerignore 示例
.git
*.fastq
*.big
data/raw/
__pycache__/
.venv/
别踩这个坑:如果不写 .dockerignore,COPY . /app 会把整个上下文目录全塞进去。我见过有人把 20G 的参考基因组数据库都拷进去了,镜像构建失败还找不到原因。
4.5 CMD vs ENTRYPOINT:这俩到底啥区别?
很多新手分不清。我直接说结论:
- CMD:默认命令,可以被
docker run后面的东西覆盖。 - ENTRYPOINT:固定入口,
docker run后面的东西会作为参数追加给它。
举个例子:
# Dockerfile 内
ENTRYPOINT ["kraken2"]
CMD ["--help"]
运行 docker run myimage --db /db 等价于 kraken2 --db /db。如果只用 CMD,那 --db /db 就会替换掉整个 CMD,达不到目的。所以宏基因组分析镜像通常用 ENTRYPOINT 固定主程序,CMD 给默认参数。
4.6 构建与调试小技巧
构建镜像的命令很简单:
docker build -t my-metagenomics:v1 .
但我一般会加几个参数调试:
docker build --no-cache -t my-metagenomics:v1 . # 强制重建,避免缓存捣乱
docker build --progress=plain -t my-metagenomics:v1 . # 输出详细日志,定位错误
如果构建到一半失败了,想进入中间层看看状态,可以:
docker run --rm -it <上一层镜像ID> bash
这个镜像 ID 可以在构建日志里看到:Step X/12 : ... ---> abc123。我经常用这个办法排查环境变量没生效或者文件权限问题。
4.7 多阶段构建:瘦身利器
宏基因组分析依赖很多编译工具,生成的可执行文件往往很小,但编译工具链可能占 1-2G。用多阶段构建就可以把最终镜像“瘦”下来。
# 第一阶段:编译
FROM ubuntu:22.04 AS builder
RUN apt-get update && apt-get install -y build-essential cmake git
WORKDIR /src
COPY sourmash-4.6.0.tar.gz .
RUN tar -xzf sourmash-4.6.0.tar.gz && cd sourmash-4.6.0 && make
# 第二阶段:只保留可执行文件和运行时库
FROM ubuntu:22.04 AS runtime
COPY --from=builder /src/sourmash-4.6.0/sourmash /usr/local/bin/
RUN apt-get update && apt-get install -y --no-install-recommends libgomp1 && rm -rf /var/lib/apt/lists/*
ENTRYPOINT ["sourmash"]
最终镜像只有几十 MB,而不是几个 G。这是实战中必须掌握的技巧。
4.8 知识体系图:Dockerfile 编写核心逻辑
4.9 写在最后:一个检查清单
每次写 Dockerfile 之前,我都会过一遍这个清单:
- ✅ 基础镜像选对了没?尽量用官方 slim 版,不要用 full。
- ✅ 有没有把缓存层和易变层分开?
- ✅ 是否添加了 .dockerignore?
- ✅ 最终镜像里有没有多余的编译工具?
- ✅ CMD/ENTRYPOINT 是否用的 JSON 数组格式(避免 shell 注入)?
说实话,这几点我一开始也没全做到。踩过几次坑之后,就老老实实列清单了。你如果按这个思路写,镜像体积、构建时间、可复现性都会好很多。