4、Dockerfile 基础指令:FROM、RUN、CMD、ENTRYPOINT 详解

聊 Dockerfile,其实就是在聊怎么把“应用”打包成“镜像”的行动指南。

我个人习惯把 Dockerfile 看作是一份食谱。FROM 是选锅,RUN 是下料翻炒,CMD 和 ENTRYPOINT 就是上桌前的最后一道工序——决定这盘菜端上去是怎么个吃法。

我这里总结了四个最核心的指令。搞懂了它们,你基本就能写出像样的 Dockerfile 了。

4.1 FROM:镜像的“地基”

FROM 是 Dockerfile 的第一行。它指定了你的新镜像是从哪个基础镜像构建出来的。

你可以理解为,所有的楼房都是从地基上建起来的。FROM 就是这个地基。

语法非常简单:

FROM [--platform=<平台>] <镜像名>[:<标签>] [AS <阶段名>]

常用的几种写法:

# 指定版本,最推荐的做法
FROM ubuntu:22.04

# 基于 Alpine,镜像极小
FROM alpine:3.19

# 多阶段构建,给阶段起个名
FROM golang:1.21 AS builder
FROM scratch
为什么推荐固定版本? 我见过不少项目用 FROM ubuntu:latest,结果某一天 latest 标签指向了新版本,编译器行为变了,整个 CI 就炸了。 固定版本锁住行为,这是可重复计算的第一步。

4.2 RUN:在构建时执行命令

RUN 是在镜像构建过程中执行的命令。每一条 RUN 都会在当前镜像层之上创建一个新层,并提交结果。

你可以用它安装软件包、编译代码、创建目录等等。

RUN apt-get update && apt-get install -y \
    build-essential \
    libssl-dev

RUN mkdir /app
RUN ["executable", "param1", "param2"]  # exec 形式

这里有一个极其常见的坑。

注意缓存失效问题 我曾遇到同事把 apt-get update 和 apt-get install 拆成两条 RUN,结果每次改其他文件后,update 层缓存失效,安装包也要重新下载,构建慢得离谱。 正确的做法是把 update 和 install 合并到一层。

再举一个反面例子。我之前给一个 Python 项目写 Dockerfile,发现有人这么写:

# 这样写会让"安装依赖"和"复制代码"耦合,改一下代码就要重新装一遍依赖
RUN pip install -r requirements.txt
COPY . /app

你应该把“不变的部分”放在前面:

# 先把 requirements.txt 复制进来,利用缓存安装依赖
COPY requirements.txt /app/
RUN pip install -r /app/requirements.txt

# 最后复制代码,改动代码时不会触发 pip install
COPY . /app

说白了就是一句话:**把变化慢的放前面,变化快的放后面**。

4.3 CMD vs ENTRYPOINT:容器启动时跑什么?

这两个指令最容易让人困惑。我直接说结论:

指令 是否可被覆盖 典型用法
CMD 是。docker run 后面加的参数会覆盖 CMD 提供默认命令或参数
ENTRYPOINT 否(除非用 --entrypoint 强制覆盖) 固定容器启动时要执行的程序
两者配合使用 ENTRYPOINT 固定,CMD 提供默认参数 容器作为可执行工具

4.3.1 CMD:提供默认的启动命令

CMD 有三种写法:

# shell 形式
CMD command param1 param2

# exec 形式(推荐)
CMD ["executable", "param1", "param2"]

# 作为 ENTRYPOINT 的默认参数
CMD ["param1", "param2"]

举个例子:

FROM ubuntu:22.04
CMD ["echo", "Hello Docker"]

这个镜像启动后会默认打印 "Hello Docker"。但如果你运行 docker run myimage ls,CMD 会被覆盖,执行的是 ls。

为什么会推荐 exec 形式? 因为 shell 形式本质上是 /bin/sh -c "你的命令",子进程信号处理有问题,关闭容器时可能不会正确退出。

4.3.2 ENTRYPOINT:把容器“固化”为一个程序

ENTRYPOINT 一旦设置,容器启动时一定会运行它。docker run 后面跟的参数会被当作这个程序的参数。

FROM ubuntu:22.04
ENTRYPOINT ["echo"]

运行 docker run myimage Hello World,实际执行的是 echo Hello World,输出 "Hello World"。

我印象很深的一个场景是做计算工具镜像。我们希望用户跑 docker run mytool --input data.txt,工具名是固定的,参数才是用户提供的。

那 Dockerfile 就该这么写:

FROM ubuntu:22.04
COPY mytool /usr/local/bin/mytool
ENTRYPOINT ["mytool"]
CMD ["--help"]

用户什么都不加,默认显示帮助。用户加参数,就执行对应的功能。

我个人习惯把 CMD 和 ENTRYPOINT 一起用。ENTRYPOINT 固定主程序,CMD 提供最常用的默认参数。 这样容器既“安全”(不会被用户误覆盖主程序),又“好用”(默认行为合理)。

4.4 一张图理清指令关系

Dockerfile 核心指令流程图 Dockerfile 核心指令流程图 FROM:选择基础镜像 指定操作系统或运行时 RUN:构建阶段执行 安装依赖、编译、创建目录 多条 RUN 指令 → 镜像分层(注意缓存与合并) CMD:提供默认启动命令 ENTRYPOINT:固化主程序 docker run 参数可覆盖 docker run 参数作为追加参数 最佳实践:ENTRYPOINT固定 + CMD提供默认参数

4.5 实战时的核心要点

  1. FROM 不要用 latest,固定一个具体版本,比如 ubuntu:22.04 或 python:3.11-slim
  2. RUN 尽量合并,apt-get update 和 apt-get install 放一起,减少镜像层数
  3. 把“不变”放前面,充分利用构建缓存,加快迭代速度
  4. CMD 和 ENTRYPOINT 二选一或配合,不要同时用两种语义完全不同的写法
我自己的项目里,如果镜像最终是个“工具”,就老老实实写 ENTRYPOINT + CMD。 如果是跑一个 Web 服务,CMD 就够了,毕竟没人会去覆盖 nginx 或 uwsgi 的启动方式。

嗯,这四个指令就是 Dockerfile 的灵魂。把 FROM 选对地基,RUN 组织好工序,最后用 CMD 和 ENTRYPOINT 控制启动行为,你的镜像构建之路就基本稳了。