Dockerfile 指令详解:构建镜像的"配方"
说实话,Dockerfile 这东西,就是你构建镜像的"配方"。你想想看,做菜需要食谱,造房子需要蓝图,那构建容器镜像自然就需要一份清晰的 Dockerfile。我个人习惯把 Dockerfile 看作是一台"时光机器"——它记录了你如何从零开始搭建一个运行环境,每一步都清清楚楚。
但这里有个坑。很多新手觉得 Dockerfile 就是把命令堆在一起就行了。嗯,大错特错。指令的顺序、指令的选择、层数的控制,这些都直接决定了你的镜像能不能"批次一致"。我在项目中遇到过不少回回,开发环境跑的好好的,一上 CI 就崩了,查到最后都是 Dockerfile 写得不够严谨。
咱们一个一个来看。
FROM:一切的基础
所有 Dockerfile 都必须从 FROM 开始。它是你的基础镜像。说白了,你是在别人的肩膀上盖楼。
FROM ubuntu:22.04
FROM python:3.11-slim
FROM alpine:3.18
选基础镜像的门道:
- 不要直接写
FROM ubuntu—— 不写标签等于用最新版。今天构建没问题,明天可能就崩了。我在项目里吃过这个亏。 - 选官方镜像、固定版本标签。比如
node:18-alpine就比node:latest靠谱得多。 - alpine 版本体积小,但 musl libc 有时候和 glibc 不兼容。要不要用,得看你的依赖。
RUN:执行指令,产生新层
RUN 是在构建镜像时执行的命令。每执行一条 RUN,就会产生一个新层。这个层是镜像的一部分,会被缓存起来。
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
git \
&& rm -rf /var/lib/apt/lists/*
为什么要把多条命令连起来写?我解释一下:每一条 RUN 都是一个层,层多了镜像就大。更重要的是,Docker 在构建时会缓存已经生成过的层,如果后续层变化了,会重新构建。把相关的安装操作合到一条 RUN 里,就能减少层数,也减少缓存失效的概率。
我踩过的坑:
我曾经单独写了两条:RUN apt-get update 和 RUN apt-get install -y curl。结果后来 apt-get 缓存过期了,第二条老是报错。拆开写不仅层多,还容易导致构建不稳定。一条写完加清理缓存,才是正确的姿势。
CMD:容器启动后的默认任务
CMD 用来指定容器启动后默认执行的命令。它不会在构建时运行。
CMD ["python", "app.py"]
CMD ["sh", "-c", "node server.js"]
CMD nginx -g "daemon off;"
这里注意:CMD 有两种写法——exec 格式(数组形式)和 shell 格式。我个人建议用 exec 格式,更严谨。
还有一点:整个 Dockerfile 只能有一条 CMD 生效。如果你写了多条,最后一条会覆盖前面的。
ENTRYPOINT:入口点
ENTRYPOINT 和 CMD 看起来很像,但用法完全不同。我把它们的关系整理成了这张表:
| 特性 | CMD | ENTRYPOINT |
|---|---|---|
| 是否可被覆盖 | 是,docker run 参数可覆盖 | 否,除非加 --entrypoint |
| 典型用法 | 提供默认参数 | 固定执行程序 |
| 配合使用 | 作为 ENTRYPOINT 的默认参数 | 作为容器的主进程 |
常见的组合用法是这样的:
ENTRYPOINT ["python"]
CMD ["app.py"]
这样设计的好处是:用户可以在运行时 docker run myimage script.py 来替换默认脚本,但 python 解释器是固定的。这种做法我在微服务架构里经常用,既能保证一致性,又能灵活传参。
WORKDIR、COPY、ENV:辅助但关键
这几个指令虽然简单,但用不好会导致各种奇怪的问题。
WORKDIR 设置工作目录。如果目录不存在,它会自动创建。
WORKDIR /app
RUN pwd
# 输出 /app
我建议每条 COPY 之前都显式加上 WORKDIR。别偷懒。有一次我没加,结果文件被复制到了根目录,整个容器启动就报错。找了我两个小时才定位到。
COPY 和 ADD 的区别:
COPY只做单纯的复制ADD可以自动解压 tar 文件,也能从 URL 下载
但听我一句劝:能用 COPY 就别用 ADD。我见过一个同事用 ADD 从网上下载依赖包,结果构建时网络抖动,镜像构建就失败。依赖管理应该交给包管理器,而不是 Dockerfile。
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
ENV 设置环境变量。它会在构建时和运行时都生效。
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
批次一致性的核心:
要保证每次构建产生的镜像一模一样,你就必须:
- 固定
FROM的基础镜像标签 - 使用
COPY引入精确的代码版本 - 用
ENV控制环境变量 - 避免使用外部网络依赖
完整的示例
这里给你看一个生产级的 Dockerfile:
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY run.py .
ENV PYTHONUNBUFFERED=1
# 用户切为非 root,安全考量
RUN useradd -m -u 1000 appuser
USER appuser
ENTRYPOINT ["python"]
CMD ["run.py"]
你想想看,这个 Dockerfile 里,每一条指令都有它的道理。没有多余的层,没有模糊的依赖,每次构建出来的镜像几乎一模一样。
嗯,到这里你应该明白了吧。Dockerfile 不是简单的命令堆叠,而是一种"环境契约"。每一行指令都在定义你的运行环境。写好了,构建即部署,跑哪都一样;写不好,今天能跑明天崩,换台机器就报错。
我个人建议,写 Dockerfile 的时候,心里默念三个原则:锁定(固定版本)、精简(减少层数)、显式(不依赖隐式行为)。做到这三点,你的镜像构建就稳了。