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 看起来很像,但用法完全不同。我把它们的关系整理成了这张表:

特性CMDENTRYPOINT
是否可被覆盖是,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 指令执行流水线 FROM 选择基础镜像 WORKDIR 设置工作目录 COPY 复制文件 RUN 安装依赖 ENV 配置环境变量 USER 切换用户 ENTRYPOINT / CMD 指定启动命令 批次一致性的关键 每条指令都锁定版本、明确路径、避免外部依赖,才能保证镜像每次构建完全一致。

嗯,到这里你应该明白了吧。Dockerfile 不是简单的命令堆叠,而是一种"环境契约"。每一行指令都在定义你的运行环境。写好了,构建即部署,跑哪都一样;写不好,今天能跑明天崩,换台机器就报错。

我个人建议,写 Dockerfile 的时候,心里默念三个原则:锁定(固定版本)、精简(减少层数)、显式(不依赖隐式行为)。做到这三点,你的镜像构建就稳了。