4. Dockerfile 编写基础:FROM、RUN、COPY、CMD 等指令详解
好,咱们开始进入正题。上一章我们把 Docker 的基本概念捋了一遍,这一章就动手写真正的 Dockerfile 了。
说实话,我第一次写 Dockerfile 的时候,以为就是把命令一行行堆进去就行。结果镜像直接干到 3 个 G,跑起来还各种报错。后来才明白,每个指令都有它自己的“脾气”。
我们一个一个来过。这章会覆盖五个最常用的指令:FROM、RUN、COPY、CMD,外加一个我经常用的 WORKDIR。学完你就能写出一份能用的 MaxQuant 容器基础。
4.1 FROM:一切的基础
FROM 是每个 Dockerfile 必须有的第一行。它告诉 Docker 用哪个镜像作为起点。说白了,就是“我站在谁的肩上”。
对于 MaxQuant,我们需要一个有 Java 运行环境的 Linux 系统。我推荐用 eclipse-temurin:17-jdk-focal,它基于 Ubuntu 20.04,稳定且包管理方便。千万别用 scratch,除非你想从头搭整个操作系统——这在生产里几乎没意义。
FROM eclipse-temurin:17-jdk-focal
这里有个坑:基础镜像的 tag 一定要写具体,别用 latest。我记得有一次没指定版本,结果第二天构建失败,因为镜像维护者更新了底层库。从此我所有项目都用完整版号。
| 基础镜像选择 | 适用场景 | 大小(约) |
|---|---|---|
| eclipse-temurin:17-jdk-focal | Java 应用标准容器(推荐) | 450 MB |
| ubuntu:20.04 | 需要自己装 JDK 时 | 70 MB |
| alpine:3.16 | 极致压缩镜像体积 | 5 MB |
4.2 WORKDIR:别在根目录里乱扔东西
我以前犯过一个低级错误:把所有文件都 COPY 到根目录下。结果镜像挺大,管理和迁移都混乱。后来养成习惯,第一件事就是设工作目录。
WORKDIR /opt/maxquant
这行之后,所有 COPY、RUN、CMD 都在这个路径下执行。相当于 cd 到了那个目录。如果目录不存在,Docker 会自动创建。
我个人习惯把工作目录放在 /opt 下,但你也用 /app 或 /home,看团队规范。
4.3 COPY:把文件搬进镜像
MaxQuant 的安装包、脚本、配置文件,怎么进到容器里?靠 COPY。
COPY MaxQuant/. /opt/maxquant
左边是宿主机路径(相对 Dockerfile 的构建上下文),右边是容器内目标路径。注意一点:COPY 只复制目录下的 内容,不包含目录本身。想带目录层级?写成 COPY MaxQuant /opt/ 就会把整个 MaxQuant 文件夹扔进去。
COPY --chown=1000:1000 可以指定文件所属用户和组,适合在容器里跑非 root 进程。
我见过有人用 ADD 代替 COPY。ADD 可以自动解压 tar 包,但行为不够透明。项目里我统一用 COPY,除非确实需要解压,否则减少不确定性。
4.4 RUN:构建时的“灵魂”指令
RUN 在构建镜像时执行,常用于装依赖、配置环境、编译代码。每个 RUN 会创建一层新镜像,所以层数多了镜像会膨胀。
以 MaxQuant 为例,我们需要装好运行所需的系统库,并设置环境变量:
RUN apt-get update && \
apt-get install -y --no-install-recommends \
libc6 \
libstdc++6 \
procps && \
rm -rf /var/lib/apt/lists/*
这里有个经典教训:记得在最后清理 apt 缓存,否则镜像会多出几十 MB 无用数据。我早期做的一个镜像,就是因为没写 rm -rf,体积直接翻倍。
如何优化 RUN 指令?把多个命令用 && 串为一行。比如:
RUN wget https://github.com/MaxQuant/MaxQuant/releases/download/v2.4.0/MaxQuant_2.4.0.tar.gz && \
tar xzf MaxQuant_2.4.0.tar.gz && \
rm MaxQuant_2.4.0.tar.gz
这样做的好处是只产生一层镜像。虽然看起来长,但实际更高效。
4.5 CMD:容器启动时“跑什么”
CMD 定义容器启动后的默认命令。它不参与构建,只在 docker run 时生效。如果用户手动传参,CMD 会被覆盖。
MaxQuant 通常以命令行方式执行,比如处理一个 mzML 文件:
CMD ["java", "-jar", "/opt/maxquant/MaxQuant.jar", "analysis.xml"]
注意我用的是 JSON 数组格式(exec form),而不是 CMD java -jar ...(shell form)。shell form 会启动 /bin/sh -c,可能引入信号处理问题和环境变量展开异常。我踩过这个坑:某次部署,shell form 导致 SIGTERM 没传给 Java 进程,容器无法优雅关闭。
如果想同时提供默认参数又允许用户自定义,你可以结合 ENTRYPOINT 和 CMD。像这样:
ENTRYPOINT ["java", "-jar", "/opt/maxquant/MaxQuant.jar"]
CMD ["default.xml"]
用户执行 docker run my-image custom.xml,就会用 custom.xml 替换默认参数。这就是设计的灵活性。
4.6 完整 Dockerfile 示例
我们把这几个指令串起来,写一个针对 MaxQuant 2.4.0 的基础 Dockerfile:
FROM eclipse-temurin:17-jdk-focal
WORKDIR /opt/maxquant
COPY MaxQuant_2.4.0.tar.gz .
RUN tar xzf MaxQuant_2.4.0.tar.gz && \
rm MaxQuant_2.4.0.tar.gz
ENV MAXQUANT_HOME=/opt/maxquant
CMD ["java", "-jar", "/opt/maxquant/MaxQuant.jar"]
这里面我加了一个 ENV 指令来设置环境变量,它也是常用指令之一,但本章不展开。注意顺序:我把 RUN 放在 COPY 之后,因为构建上下文的文件只在 COPY 后可见。
4.7 常见错误及我的避坑列表
写 Dockerfile 最容易犯的错,我列了个清单,每个都是我流过的血泪:
- 忘记写 .dockerignore:构建上下文混入 .git 目录、node_modules 等,镜像体积爆炸。在项目根目录加一个 .dockerignore 文件:
.git
*.md
node_modules
__pycache__
- RUN 行太多,层数失控:每个 RUN 产生一层。我一个同事写了 20 个 RUN,镜像又大又慢。合并为 3~5 个合理。
- 硬编码版本号:把下载 URL 的版本号写在 Dockerfile 里,下次升级要改多个地方。建议用 ARG 参数化:
ARG MAXQUANT_VERSION=2.4.0
COPY MaxQuant_${MAXQUANT_VERSION}.tar.gz .
- 没有清理临时文件:apt 安装后不删缓存、wget 下载后不删压缩包。每个 RUN 结束后顺手清理。
- 用 root 运行服务:容器里跑 Java 进程如果是 root,万一被利用权限太大。创建一个非 root 用户:
RUN useradd -m -s /bin/bash maxquant
USER maxquant