3. Dockerfile 基础:编写第一个 Dockerfile 与常用指令详解
Dockerfile 是 Docker 生态中用于定义镜像构建规则的声明式文本文件。通过逐条指令,开发者可以精确控制基础操作系统、依赖安装、文件复制、环境变量、启动命令等环节。本章将带领你从零编写第一个 Dockerfile,并深入剖析 FROM、RUN、COPY、CMD、ENTRYPOINT 这五个核心指令的用法与差异。
3.1 第一个 Dockerfile:从零构建一个 Python 后端服务
以下是一个最小可用的 Dockerfile,它基于 Python 3.11-slim 镜像,安装依赖、复制源码并启动一个 Flask 应用:
# 1. 指定基础镜像
FROM python:3.11-slim
# 2. 设置工作目录
WORKDIR /app
# 3. 复制依赖文件并安装 Python 包
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 4. 复制应用源码
COPY . .
# 5. 暴露端口
EXPOSE 5000
# 6. 设置容器启动命令
CMD ["python", "app.py"]
构建并运行该镜像:
docker build -t my-flask-app .
docker run -d -p 5000:5000 my-flask-app
requirements.txt 的复制与 pip install 放在源码复制之前,可以利用 Docker 的层缓存机制。只要 requirements.txt 未变化,后续构建会直接复用该层,大幅加速构建速度。
3.2 常用指令详解
Dockerfile 包含数十条指令,但 FROM、RUN、COPY、CMD、ENTRYPOINT 是构建任何镜像都绕不开的核心指令。下表对比了它们的作用与关键差异:
| 指令 | 作用阶段 | 执行时机 | 主要用途 | 是否创建新层 |
|---|---|---|---|---|
| FROM | 构建 | 构建时 | 指定基础镜像,所有后续指令都基于此镜像 | 是 |
| RUN | 构建 | 构建时 | 执行命令(如安装软件包、创建目录) | 是 |
| COPY | 构建 | 构建时 | 将本地文件/目录复制到镜像中 | 是 |
| CMD | 运行 | 容器启动时 | 提供默认启动命令,可被 docker run 参数覆盖 |
否 |
| ENTRYPOINT | 运行 | 容器启动时 | 定义容器的主进程,不易被覆盖 | 否 |
3.3 FROM:镜像的基石
FROM 必须是 Dockerfile 的第一条非注释指令。它指定了构建所依赖的基础镜像,可以是官方镜像(如 ubuntu:22.04、node:18-alpine)或私有仓库中的自定义镜像。
- 语法:
FROM [--platform=<平台>] <镜像名>[:<标签>] [AS <别名>] - 多阶段构建: 使用
AS为阶段命名,后续阶段可通过COPY --from=<别名>从上一阶段复制产物,实现镜像瘦身。 - 空镜像:
FROM scratch用于构建极简镜像(如 Go 静态编译二进制),不包含任何操作系统文件。
python:3.11-alpine);避免使用 latest 标签,应指定明确版本号以保证构建可复现。
3.4 RUN:在构建时执行命令
RUN 指令会在当前镜像层之上执行命令,并将结果提交为新层。每条 RUN 都会增加一层,因此应尽量合并命令以减少层数。
# ❌ 不推荐:多条 RUN 产生多层
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*
# ✅ 推荐:合并为一条 RUN,并清理缓存
RUN apt-get update && \
apt-get install -y curl && \
rm -rf /var/lib/apt/lists/*
- Shell 格式:
RUN apt-get install -y curl,默认使用/bin/sh -c执行。 - Exec 格式:
RUN ["apt-get", "install", "-y", "curl"],不会触发 shell 解析,更安全。
3.5 COPY:将文件注入镜像
COPY 将构建上下文(通常是 Dockerfile 所在目录)中的文件或目录复制到镜像内的目标路径。它只支持从本地复制,不支持 URL 或压缩包自动解压。
# 复制单个文件
COPY app.py /app/app.py
# 复制整个目录(保留目录结构)
COPY ./src /app/src
# 多阶段构建中从上一阶段复制
COPY --from=builder /app/dist /app/dist
COPY 的源路径必须位于构建上下文内。使用 .. 或绝对路径(如 /root)会导致构建失败。建议将 Dockerfile 放在项目根目录,并利用 .dockerignore 排除无关文件。
3.6 CMD 与 ENTRYPOINT:容器启动的指挥官
CMD 和 ENTRYPOINT 都用于定义容器启动时执行的命令,但行为有本质区别。理解它们的交互方式是编写正确 Dockerfile 的关键。
3.6.1 CMD:提供默认参数
CMD 为容器提供默认的启动命令或参数。如果用户在 docker run 后附加了命令,CMD 会被完全覆盖。
# 三种写法
CMD ["python", "app.py"] # Exec 格式(推荐)
CMD python app.py # Shell 格式
CMD ["python", "app.py", "--port", "8080"] # 带默认参数
3.6.2 ENTRYPOINT:固定主进程
ENTRYPOINT 将容器配置为一个可执行程序。用户附加的命令会作为参数传递给 ENTRYPOINT,而不是覆盖它。
# 固定主命令为 curl
ENTRYPOINT ["curl"]
CMD ["--help"] # 默认参数
# docker run my-curl https://example.com
# 实际执行:curl https://example.com
3.6.3 组合使用的最佳实践
将 ENTRYPOINT 设为固定主程序,CMD 提供默认参数,是 Docker 官方推荐的模式。这种组合既保证了容器的单一职责,又提供了灵活的默认行为。
3.7 指令对比速查表
| 特性 | CMD | ENTRYPOINT |
|---|---|---|
是否可被 docker run 覆盖 |
是(完全覆盖) | 否(仅追加参数) |
| 推荐使用场景 | 提供默认参数或可替换的命令 | 固定容器主程序(如 nginx、redis) |
| Shell 格式 | CMD command arg1 |
ENTRYPOINT command arg1 |
| Exec 格式 | CMD ["executable","arg1"] |
ENTRYPOINT ["executable","arg1"] |
与 docker run --entrypoint 的关系 |
不影响 | 可被 --entrypoint 覆盖 |
3.8 本章小结
通过本章的学习,你已经掌握了 Dockerfile 的五个核心指令:
- FROM 选择基础镜像,是构建的起点。
- RUN 在构建时执行命令,注意合并以减少层数。
- COPY 将本地文件注入镜像,注意构建上下文限制。
- CMD 提供默认启动命令,可被用户覆盖。
- ENTRYPOINT 固定主进程,与 CMD 配合实现灵活的参数传递。
ENTRYPOINT 固定为 ping 命令,CMD 默认参数为 8.8.8.8。构建后运行 docker run my-ping -c 3,观察实际执行的命令。