前言
如果说 Docker 镜像是一个"已经打包好的安装包",那么 Dockerfile 就是生成这个安装包的 "施工图纸" 。它是 Docker 生态中最核心的概念之一------没有 Dockerfile,镜像只能依赖手动操作或 **docker commit**逐层堆叠,无法实现标准化、自动化和版本控制。
本文将系统讲解 Dockerfile 的核心概念、逐条解析常用指令,并通过一个完整的 FastAPI 项目示例,手把手演示从编写 Dockerfile 到构建镜像、运行容器的全流程。掌握本文内容后,你就具备了为任意项目编写 Dockerfile 的能力。
目录
- 一、Dockerfile 是什么
- 二、为什么需要 Dockerfile
- 三、Dockerfile 的工作原理
- 四、Dockerfile 指令详解
- 4.1 FROM ------ 指定基础镜像
- 4.2 WORKDIR ------ 设置工作目录
- 4.3 COPY ------ 复制文件到镜像
- 4.4 ADD ------ 高级文件复制
- 4.5 RUN ------ 构建时执行命令
- 4.6 ENV ------ 设置环境变量
- 4.7 ARG ------ 构建时参数
- 4.8 EXPOSE ------ 声明端口
- 4.9 CMD ------ 容器启动默认命令
- 4.10 ENTRYPOINT ------ 入口点命令
- 4.11 VOLUME ------ 声明挂载点
- 4.12 USER ------ 指定运行用户
- 4.13 LABEL ------ 添加元数据
- 4.14 HEALTHCHECK ------ 健康检查
- 4.15 SHELL ------ 指定默认 Shell
- 4.16 .dockerignore ------ 构建排除文件
- 五、指令执行顺序与分层机制
- 六、实战:完整 Dockerfile 编写与构建
- 6.1 项目结构
- 6.2 Dockerfile 逐行解析
- 6.3 构建镜像
- 6.4 运行容器
- 6.5 常用构建参数
- 七、Dockerfile 最佳实践
- 八、总结
一、Dockerfile 是什么
Dockerfile 是一个纯文本文件,文件名通常就是 Dockerfile(无后缀),其中按行书写了一系列 构建指令,每一条指令描述了镜像构建过程中的一个步骤。
你可以把它理解为一份 自动化脚本 :当你执行 docker build 命令时,Docker 引擎会逐行读取 Dockerfile 中的指令,在一个临时的中间容器中依次执行这些操作(安装软件、复制文件、设置环境变量......),最终将所有操作层层叠加、固化,生成一个不可变的 Docker 镜像。
核心公式:
Dockerfile + docker build = Docker Image
施工图纸 构建命令 构建产物
一句话定义: Dockerfile 是镜像的源代码,是用声明式语法描述的"如何从零搭建一个运行环境"的标准化配方。
二、为什么需要 Dockerfile
2.1 手动构建的困境
在没有 Dockerfile 的年代,构建镜像的典型方式是:
- 1.手动启动一个基础容器
- 2.进入容器,手动安装依赖、复制文件、修改配置
- 3.执行
docker commit将容器状态保存为镜像
这种方式存在严重问题:
| 问题 | 具体表现 |
|---|---|
| 不透明 | 别人拿到镜像后,完全不知道它是怎么构建出来的 |
| 不可重复 | 过了一周,连自己都忘了当时装了什么、改了什么 |
| 不可审计 | 无法追溯变更历史,出了问题难以排查 |
| 不可自动化 | 全靠人工操作,无法集成到 CI/CD 流水线 |
2.2 Dockerfile 带来的三大价值
(1)环境标准化
代码和运行环境被 一同定义 在 Dockerfile 中。无论在哪台机器上构建,只要 Dockerfile 相同,产出的镜像就 完全一致。"在我本地是好的"这类问题被彻底杜绝。
(2)可重复性与版本控制
Dockerfile 是纯文本文件,可以像代码一样纳入 Git 管理。每次环境变更都有迹可循、可回滚、可 Code Review。构建过程是透明的------任何人都可以通过阅读 Dockerfile 镜像的完整构建逻辑。
(3)自动化流水线的基石
Dockerfile 是 CI/CD(持续集成 / 持续部署)的核心组件。代码提交后,CI 服务器自动读取 Dockerfile、构建镜像、推送至仓库、部署至生产环境------全流程无需人工干预。
代码提交 → CI 服务器读取 Dockerfile → 自动构建镜像
│
▼
推送至镜像仓库 → CD 服务器拉取镜像 → 自动部署到生产环境
三、Dockerfile 的工作原理
3.1 构建过程详解
执行 docker build 时,Docker 内部的工作流程如下:
用户执行 docker build
│
▼
Docker Daemon 读取 Dockerfile
│
▼
找到 FROM 指定的基础镜像(如 python:3.11-slim)
│
▼
基于基础镜像创建一个临时中间容器
│
▼
执行第一条指令(如 WORKDIR /app)
│
▼
执行完毕后,将容器的文件系统变更保存为一个新层(Layer)
│
▼
基于这个新层创建一个新的临时容器
│
▼
执行下一条指令(如 COPY requirements.txt .)
│
▼
再次保存为新层 → 销毁临时容器
│
▇
(重复以上过程,逐条指令执行)
│
▇
▼
所有指令执行完毕
│
▼
将所有层叠加在一起,生成最终镜像
│
▼
为镜像打上 -t 指定的标签(如 my-app:1.0)
3.2 构建上下文(Build Context)
执行 docker build 时,最后一个参数 . 指定的是 构建上下文(Build Context) 的路径:
bash
docker build -t my-app .
# ↑ 这个 . 就是构建上下文
构建上下文是 Docker Daemon 在构建过程中可以访问的文件目录。Dockerfile 中的 COPY 和 ADD 指令只能操作构建上下文范围内的文件------无法访问上下文之外的文件。
bash
构建上下文(项目根目录 /)
│
├── Dockerfile
├── requirements.txt ← COPY 可以访问
├── app/
│ └── main.py ← COPY 可以访问
└── docs/
└── readme.md ← COPY 可以访问
────────── 上下文边界 ──────────
/tmp/secret.key ← COPY 无法访问(不在上下文中)
注意: 构建时 Docker 会将整个上下文目录发送给 Docker Daemon。如果目录中有大量无关文件(如
node_modules、.git),会显著拖慢构建速度。应使用.dockerignore文件排除不需要的文件(后续详解)。
3.3 分层缓存机制
Docker 的构建缓存是提升构建效率的核心机制。每一条指令执行后都会生成一个缓存层,下次构建时 Docker 会检查:
- 1.该指令本身是否发生了变化?
- 2.该指令涉及的文件是否发生了变化?
如果两者都没变,Docker 直接复用缓存层,跳过执行,极大地加速了构建过程。
bash
第一次构建:全部指令逐一执行,耗时较长
Layer 1: FROM python:3.11-slim ✓ 执行
Layer 2: WORKDIR /app ✓ 执行
Layer 3: COPY requirements.txt . ✓ 执行
Layer 4: RUN pip install ... ✓ 执行(耗时最长)
Layer 5: COPY . . ✓ 执行
Layer 6: CMD ["uvicorn", ...] ✓ 执行
第二次构建(仅修改了 app/main.py):
Layer 1: FROM python:3.11-slim → 缓存命中,跳过
Layer 2: WORKDIR /app → 缓存命中,跳过
Layer 3: COPY requirements.txt . → 缓存命中,跳过(文件未变)
Layer 4: RUN pip install ... → 缓存命中,跳过
Layer 5: COPY . . → 缓存失效,重新执行(文件变了)
Layer 6: CMD ["uvicorn", ...] → 依赖上层变化,重新执行
这就是为什么最佳实践中要先 COPY 依赖清单、再 COPY 源代码------将变动频率低的指令放在前面,变动频率高的放在后面,最大化缓存命中率。
四、Dockerfile 指令详解
下面逐条讲解 Dockerfile 的常用指令,按使用频率排列。
4.1 FROM ------ 指定基础镜像
语法:
bash
FROM <镜像名>:<标签>
说明:
FROM 必须是 Dockerfile 中的 第一条有效指令 (除了 ARG)。它指定了新镜像的基础------后续所有操作都在这个基础镜像之上进行。
示例:
bash
# 官方 Python 精简版(基于 Debian)
FROM python:3.11-slim
# 官方 Python Alpine 版(更小,约 50MB)
FROM python:3.11-alpine
# 官方 Node.js LTS 版
FROM node:20-bookworm-slim
# 官方 Ubuntu 基础镜像
FROM ubuntu:22.04
# 不依赖任何基础镜像(从零构建,仅用于特殊场景)
FROM scratch
标签选择建议:
| 标签模式 | 示例 | 适用场景 |
|---|---|---|
| 精确版本 | python:3.11.7-slim |
生产环境(保证完全可重复) |
| 次版本 | python:3.11-slim |
日常开发(兼容性与稳定性的平衡) |
latest |
python:latest |
不推荐(行为不可预测) |
slim |
python:3.11-slim |
推荐(去除不必要的工具,体积更小) |
alpine |
python:3.11-alpine |
追求极致小体积(注意 libc 兼容性) |
4.2 WORKDIR ------ 设置工作目录
语法:
WORKDIR <路径>
说明:
设置后续指令(RUN、CMD、COPY、ADD)的 默认工作目录。如果目录不存在,Docker 会自动创建。
示例:
bash
WORKDIR /app
# 后续的 COPY 操作会将文件复制到 /app/ 下
COPY . .
# RUN 指令会在 /app/ 目录下执行
RUN pip install -r requirements.txt
# 可以多次切换工作目录
WORKDIR /app/src
# 此时 pwd 为 /app/src
为什么不使用
RUN cd /app? 因为RUN创建新层后,cd的效果不会保留到下一层。WORKDIR是持久性的,影响后续所有指令。
4.3 COPY ------ 复制文件到镜像
语法:
COPY [--chown=<用户>:<组>] <源路径> <目标路径>
说明:
将构建上下文中的文件或目录复制到镜像的指定位置。这是最常用的文件操作指令。
示例:
bash
# 复制单个文件
COPY requirements.txt .
# 复制整个目录(将 src/ 目录内容复制到 /app/src/)
COPY src/ /app/src/
# 复制时设置文件所有者
COPY --chown=appuser:appuser . /app
# 使用通配符
COPY *.py /app/
COPY config/*.yml /app/config/
# 多文件复制(Docker 17.09+)
COPY requirements.txt app.py ./
4.4 ADD ------ 高级文件复制
语法:
bash
ADD [--chown=<用户>:<组>] <源路径> <目标路径>
说明:
ADD 与 COPY 功能类似,但有两个额外能力:
- 1.自动解压 :如果源文件是 tar 压缩包(
.tar、.tar.gz、.bz2等),ADD 会自动解压到目标路径 - 2.支持 URL :可以从远程 URL 下载文件(但不推荐,建议用
RUN curl代替)
示例:
bash
# 自动解压本地压缩包
ADD app-v1.0.tar.gz /app/
# 从远程 URL 下载(不推荐,缓存行为不可控)
ADD https://example.com/file.txt /app/
COPY 与 ADD 的选择原则:
默认使用
COPY。 只有在需要自动解压 tar 包时才使用ADD。COPY的行为更明确、更可预测。
4.5 RUN ------ 构建时执行命令
语法:
bash
# Shell 形式(默认,通过 /bin/sh -c 执行)
RUN <命令>
# Exec 形式(直接执行,不经过 Shell)
RUN ["可执行文件", "参数1", "参数2"]
说明:
RUN 在当前镜像层之上执行命令,并将执行结果保存为新的一层。这是安装软件、编译代码、配置环境的核心指令。
示例:
bash
# Shell 形式 ------ 最常用,支持管道、变量替换等 Shell 特性
RUN apt-get update && apt-get install -y \
curl \
wget \
vim \
&& rm -rf /var/lib/apt/lists/*
# 安装 Python 依赖(--no-cache-dir 减小镜像体积)
RUN pip install --no-cache-dir -r requirements.txt
# Exec 形式 ------ 不经过 Shell,适合执行二进制文件
RUN ["/bin/bash", "-c", "echo hello"]
最佳实践------合并 RUN 指令:
每一条 RUN 都会产生一个新的层。应将相关的命令用 && 合并为一条 RUN,减少层数、减小镜像体积:
bash
# ✅ 推荐:合并为一条 RUN,且在同一层清理缓存
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl wget \
&& rm -rf /var/lib/apt/lists/*
# ❌ 不推荐:拆成多条 RUN,每条各自产生一层
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y wget
RUN rm -rf /var/lib/apt/lists/*
4.6 ENV ------ 设置环境变量
语法:
bash
ENV <键>=<值>
ENV <键1>=<值1> <键2>=<值2>
说明:
设置的环境变量在 构建阶段 (后续 RUN 指令)和 运行阶段 (容器运行时)均生效。容器运行时可通过 docker run -e 覆盖。
示例:
bash
# 设置单个环境变量
ENV APP_ENV=production
# 设置多个环境变量
ENV PYTHONPATH=/app \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
# 后续 RUN 指令可以直接使用这些变量
RUN echo "当前环境: $APP_ENV"
ENV 与 ARG 的关键区别:
| 维度 | ENV | ARG |
|---|---|---|
| 生效阶段 | 构建阶段 + 运行阶段 | 仅构建阶段 |
| 容器运行时可见 | 是 | 否 |
可被 docker run -e 覆盖 |
是 | 不适用 |
| 定义方式 | Dockerfile 中写死 | 构建时通过 --build-arg 传入 |
4.7 ARG ------ 构建时参数
语法:
ARG <参数名>[=<默认值>]
说明:
定义在 docker build 时可以通过 --build-arg 传入的变量。仅在构建阶段有效,不会出现在最终镜像的运行环境中。
示例:
bash
# Dockerfile 中定义 ARG
ARG PYTHON_VERSION=3.11
FROM python:${PYTHON_VERSION}-slim
# 可以在 RUN 中使用
ARG APP_VERSION
RUN echo "Building version: $APP_VERSION"
bash
# 构建时传入参数值
docker build \
--build-arg PYTHON_VERSION=3.12 \
--build-arg APP_VERSION=2.0.0 \
-t my-app .
4.8 EXPOSE ------ 声明端口
语法:
EXPOSE <端口>[/<协议>]
说明:
EXPOSE 是一个 声明性指令 ,仅起到文档说明的作用------它告诉使用者该镜像内的服务监听了哪个端口。它不会自动将端口映射到宿主机。
实际的端口映射需要在 docker run 时通过 -p 参数指定。
示例:
bash
# 声明监听 8003 端口(TCP,默认)
EXPOSE 8003
# 声明监听 80 端口(TCP)和 443 端口(TCP)
EXPOSE 80 443
# 声明监听 53 端口(UDP)
EXPOSE 53/udp
bash
# EXPOSE 只是声明,实际映射需要 -p 参数
docker run -p 8003:8003 my-app # 宿主机 8003 → 容器 8003
docker run -p 9090:8003 my-app # 宿主机 9090 → 容器 8003
4.9 CMD ------ 容器启动默认命令
语法:
bash
# Exec 形式(推荐)
CMD ["可执行文件", "参数1", "参数2"]
# Shell 形式
CMD 命令 参数1 参数2
说明:
CMD 指定容器启动时默认执行的命令。一个 Dockerfile 中只有最后一个 CMD 生效(多个 CMD 只有最后一个有效)。CMD 可以被 docker run 后面的命令覆盖。
两种形式的区别:
bash
# Exec 形式(推荐):直接执行可执行文件,不经过 Shell
# 进程是 PID 1,能正确接收和处理信号(如 SIGTERM)
CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "8003"]
# Shell 形式:实际执行的是 /bin/sh -c "uvicorn server:app ..."
# 进程是 sh 的子进程,不是 PID 1,可能无法正确接收信号
CMD uvicorn server:app --host 0.0.0.0 --port 8003
为什么推荐 Exec 形式? 在 Docker 中,PID 1 进程承担了特殊的"初始化进程"角色,负责接收系统信号(如停止容器时的 SIGTERM)并做优雅退出。Shell 形式下,PID 1 是
/bin/sh,你的应用是它的子进程,可能无法正确接收这些信号,导致容器停止时无法优雅关闭。
CMD 可被运行命令覆盖:
bash
# Dockerfile 中定义的默认命令
CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "8003"]
bash
# 默认行为:执行 CMD 定义的命令
docker run my-app
# 覆盖 CMD:执行 /bin/bash 而非 uvicorn(容器启动后直接进入 Shell)
docker run -it my-app /bin/bash
# 覆盖 CMD:执行 Python 脚本
docker run my-app python manage.py migrate
4.10 ENTRYPOINT ------ 入口点命令
语法:
bash
# Exec 形式(推荐)
ENTRYPOINT ["可执行文件", "参数1", "参数2"]
# Shell 形式
ENTRYPOINT 命令 参数1 参数2
说明:
ENTRYPOINT 与 CMD 类似,但有一个关键区别:ENTRYPOINT 不容易被 docker run 后面的命令覆盖 (除非使用 --entrypoint 参数)。
ENTRYPOINT 通常用于将容器配置为一个固定的可执行程序,CMD 则作为其默认参数。
ENTRYPOINT 与 CMD 的配合使用:
bash
# 定义固定的入口点
ENTRYPOINT ["python", "app.py"]
# CMD 作为 ENTRYPOINT 的默认参数
CMD ["--port", "8003"]
bash
# 实际执行:python app.py --port 8003
docker run my-app
# 传入自定义参数覆盖 CMD:python app.py --port 9090
docker run my-app --port 9090
# 强制覆盖 ENTRYPOINT(少用)
docker run --entrypoint /bin/bash my-app
CMD 与 ENTRYPOINT 对比:
| 维度 | CMD | ENTRYPOINT |
|---|---|---|
是否可被 docker run 覆盖 |
容易覆盖 | 不易覆盖(需 --entrypoint) |
| 典型用途 | 容器的默认启动命令 | 容器的固定可执行程序 |
| 多条指令 | 只有最后一条生效 | 只有最后一条生效 |
| 推荐用法 | 独立使用或与 ENTRYPOINT 配合 | 与 CMD 配合使用 |
4.11 VOLUME ------ 声明挂载点
语法:
bash
VOLUME ["<路径1>", "<路径2>", ...]
VOLUME <路径>
说明:
声明容器中的某个目录为匿名卷。运行容器时如果未显式挂载,Docker 会自动创建一个匿名卷挂载到该路径,确保该目录的数据持久化。
示例:
bash
# 声明数据库数据目录为卷
VOLUME /var/lib/mysql
# 声明多个卷
VOLUME ["/data", "/logs"]
注意:
VOLUME指令会导致后续指令对该目录的修改无效(因为运行时会挂载外部卷覆盖该目录)。如果需要在构建时向卷目录写入初始数据,应将VOLUME放在最后。
4.12 USER ------ 指定运行用户
语法:
USER <用户名>[:<用户组>]
说明:
指定后续指令(RUN、CMD、ENTRYPOINT)以哪个用户身份执行。默认以 root 身份运行,出于安全考虑,生产环境建议创建并切换到非 root 用户。
示例:
bash
# 创建非 root 用户
RUN groupadd -r appuser && useradd -r -g appuser appuser
# 切换用户(后续所有指令都以 appuser 身份执行)
USER appuser
4.13 LABEL ------ 添加元数据
语法:
LABEL <键>=<值> <键>=<值> ...
说明:
为镜像添加键值对形式的元数据标签,用于标记镜像的作者、版本、描述等信息。不会影响镜像的功能,只用于信息管理。
示例:
bash
LABEL maintainer="zhangsan@example.com"
LABEL version="1.0"
LABEL description="FastAPI + LLM 应用镜像"
# 也可以写成一行
LABEL maintainer="zhangsan@example.com" \
version="1.0" \
description="FastAPI + LLM 应用镜像"
查看镜像的 LABEL:
docker inspect --format '{{json .Config.Labels}}' my-app
4.14 HEALTHCHECK ------ 健康检查
语法:
bash
HEALTHCHECK [选项] CMD <检查命令>
HEALTHCHECK NONE # 禁用基础镜像的健康检查
选项说明:
| 选项 | 默认值 | 说明 |
|---|---|---|
--interval |
30s | 两次检查之间的间隔 |
--timeout |
30s | 单次检查超时时间 |
--start-period |
0s | 容器启动后的等待时间(此期间检查失败不计入) |
--retries |
3 | 连续失败几次后标记为 unhealthy |
示例:
bash
# 每 30 秒访问一次健康检查接口,超时 5 秒,3 次失败标记为不健康
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD curl -f http://localhost:8003/health || exit 1
查看容器健康状态:
bash
docker ps
# STATUS 列会显示 healthy / unhealthy
4.15 SHELL ------ 指定默认 Shell
语法:
SHELL ["可执行文件", "参数"]
说明:
指定 RUN 指令的 Shell 形式所使用的默认 Shell。Linux 默认为 ["/bin/sh", "-c"],Windows 默认为 ["cmd", "/S", "/C"]。
示例:
bash
# 将默认 Shell 改为 bash
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
# 后续 RUN 的 Shell 形式将使用 bash 执行
RUN echo "hello" | grep "hello"
为什么要设置
pipefail? 默认的/bin/sh不会因为管道中某个命令失败而终止整个命令。加上-o pipefail后,管道中任意一个命令失败,整条RUN就会失败,有助于及时发现构建错误。
4.16 .dockerignore ------ 构建排除文件
说明:
在项目根目录创建 .dockerignore 文件,列出不需要发送到构建上下文的文件和目录。它的语法类似 .gitignore。
示例:
bash
# .dockerignore
# 版本控制
.git
.gitignore
# 依赖目录(在容器内会重新安装)
node_modules
__pycache__
*.pyc
.venv
venv
# IDE 配置
.vscode
.idea
*.swp
# 日志和临时文件
*.log
tmp/
.env
# 文档和测试(构建镜像时不需要)
docs/
tests/
README.md
# Docker 自身
Dockerfile
docker-compose.yml
.dockerignore
作用:
- 1.加速构建 :不发送无关文件(如
node_modules可能有几百 MB),显著减少构建上下文大小 - 2.保护隐私 :避免将
.env、密钥文件等敏感信息意外打包进镜像 - 3.保证安全 :防止
.git目录泄露
五、指令执行顺序与分层机制
5.1 指令分类总结
Dockerfile 的指令可以按作用阶段分为两大类:
构建时指令 (在 docker build 过程中执行):
| 指令 | 作用 | 是否生成新层 |
|---|---|---|
FROM |
指定基础镜像 | 继承基础镜像的层 |
RUN |
执行命令(安装依赖等) | 是 |
COPY |
复制文件到镜像 | 是 |
ADD |
高级文件复制(可解压) | 是 |
ENV |
设置环境变量 | 是 |
ARG |
定义构建参数 | 是 |
WORKDIR |
设置工作目录 | 是 |
USER |
设置运行用户 | 是 |
LABEL |
添加元数据 | 是 |
VOLUME |
声明卷挂载点 | 是 |
SHELL |
指定默认 Shell | 是 |
EXPOSE |
声明端口 | 是(仅元数据) |
HEALTHCHECK |
定义健康检查 | 是(仅元数据) |
运行时指令(在容器启动时生效):
| 指令 | 作用 | 是否可被覆盖 |
|---|---|---|
CMD |
容器启动默认命令 | 可被 docker run 覆盖 |
ENTRYPOINT |
容器入口点命令 | 需 --entrypoint 覆盖 |
5.2 指令执行顺序
bash
FROM ← 第 1 步:拉取基础镜像
│
LABEL ← 添加元数据(不影响构建逻辑)
│
ARG ← 定义构建参数
│
ENV ← 设置环境变量
│
WORKDIR ← 设置工作目录
│
COPY ← 复制依赖清单文件(先复制,利用缓存)
│
RUN ← 安装依赖(这一步最耗时,放在前面可复用缓存)
│
COPY ← 复制项目源代码(变动频繁,放在后面)
│
RUN ← 可能的后续操作(编译、压缩等)
│
USER ← 切换运行用户
│
EXPOSE ← 声明端口
│
VOLUME ← 声明挂载点
│
HEALTHCHECK ← 定义健康检查
│
CMD ← 定义容器启动命令(最后执行)
六、实战:完整 Dockerfile 编写与构建
6.1 项目结构
以一个 FastAPI + LLM 应用为例,项目目录结构如下:
bash
docker_demo_project/ ← 项目根目录(构建上下文)
│
├── __001__fastapi/ ← FastAPI 应用目录
│ ├── server.py ← FastAPI 入口文件
│ └── ...
│
├── __002__docker/ ← Docker 配置目录
│ └── Dockerfile ← Dockerfile 文件
│
├── requirements.txt ← Python 依赖清单
├── .env ← 环境变量文件(不打包进镜像)
└── README.md
6.2 Dockerfile 逐行解析
bash
# ============================================
# 基础镜像:官方 Python 3.11 精简版
# slim 版基于 Debian,去除了编译工具等非必要包
# 体积约为完整版的 1/3,但足以运行大多数 Python 应用
# ============================================
FROM python:3.11-slim
# ============================================
# 设置容器内工作目录为 /app
# 所有后续指令的默认执行路径
# 目录不存在时会自动创建
# ============================================
WORKDIR /app
# ============================================
# 设置环境变量
# PYTHONPATH: 让 Python 能正确找到项目中的模块
# PYTHONDONTWRITEBYTECODE: 不生成 .pyc 字节码文件(减小体积)
# PYTHONUNBUFFERED: 禁用 Python 输出缓冲(日志实时可见)
# ============================================
ENV PYTHONPATH=/app \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
# ============================================
# 第一步:只复制依赖清单文件
# 利用 Docker 分层缓存机制------
# 只要 requirements.txt 没变,依赖安装层就不会重新构建
# 即使源代码频繁变动,也不需要重新安装依赖
# ============================================
COPY requirements.txt .
# ============================================
# 安装 Python 依赖
# --no-cache-dir: 不缓存 pip 下载的包,减小镜像体积
# -r: 从文件中读取依赖列表
# 安装的包包括 fastapi、uvicorn、langchain-openai 等
# ============================================
RUN pip install --no-cache-dir -r requirements.txt
# ============================================
# 第二步:复制整个项目源代码到 /app
# 放在依赖安装之后,这样修改代码不会触发重新安装依赖
# ============================================
COPY . .
# ============================================
# 切换到 FastAPI 应用所在的子目录
# 后续的 CMD 将在这个目录下执行
# ============================================
WORKDIR /app/__001__fastapi
# ============================================
# 声明容器内服务监听 8003 端口
# 这是一个文档说明性质的指令,不会自动映射到宿主机
# 实际映射需要在 docker run 时通过 -p 参数指定
# ============================================
EXPOSE 8003
# ============================================
# 容器启动时的默认命令
# 使用 exec 形式(JSON 数组),而非 Shell 形式
# 优势:uvicorn 作为 PID 1 直接运行,能正确接收系统信号
# --host 0.0.0.0: 监听所有网络接口(容器外部可访问)
# --port 8003: 监听 8003 端口
# ============================================
CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "8003"]
分层与缓存分析:
bash
构建过程的层结构:
Layer 1: FROM python:3.11-slim ← 基础镜像层(约 130MB)
Layer 2: WORKDIR /app ← 设置工作目录(极小)
Layer 3: ENV PYTHONPATH=... ← 设置环境变量(极小)
Layer 4: COPY requirements.txt . ← 复制依赖清单(极小)
Layer 5: RUN pip install ... ← 安装依赖(约 100-200MB,耗时最长)
Layer 6: COPY . . ← 复制项目代码(变动最频繁)
Layer 7: WORKDIR /app/__001__fastapi ← 切换目录(极小)
Layer 8: EXPOSE 8003 ← 声明端口(极小)
Layer 9: CMD ["uvicorn", ...] ← 启动命令(极小)
缓存策略:
修改源代码 → 仅 Layer 6 及之后失效 → Layer 1-5 命中缓存
修改依赖 → Layer 4 失效 → Layer 4 及之后全部重建
(所以依赖清单应尽量少变动,与源代码分开复制)
6.3 构建镜像
进入项目根目录,执行构建命令:
bash
# 进入项目目录
cd d:\PyCharmProject\docker_demo_project
# 构建镜像
# -f: 指定 Dockerfile 的路径(不在默认位置时使用)
# -t: 指定镜像的名称和标签(name:tag)
# .: 指定构建上下文为当前目录
docker build -f __002__docker/Dockerfile -t fastapi-llm .
构建命令逐参数解析:
bash
docker build -f __002__docker/Dockerfile -t fastapi-llm .
│ │ │ │
│ │ │ └── 构建上下文:当前目录
│ │ └── 镜像标签:fastapi-llm:latest
│ └── Dockerfile 路径(相对于构建上下文)
└── 构建命令
构建输出示例:
bash
[+] Building 45.2s (10/10) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> [internal] load .dockerignore 0.0s
=> [internal] load metadata for docker.io/library/python:3.11 2.1s
=> [1/5] FROM docker.io/library/python:3.11-slim@sha256:... 8.3s
=> [2/5] WORKDIR /app 0.1s
=> [3/5] COPY requirements.txt . 0.0s
=> [4/5] RUN pip install --no-cache-dir -r requirements.txt 28.6s
=> [5/5] COPY . . 0.1s
=> exporting to image 5.8s
=> => naming to docker.io/library/fastapi-llm:latest 0.0s
验证镜像是否构建成功:
bash
docker images fastapi-llm
# REPOSITORY TAG IMAGE ID CREATED SIZE
# fastapi-llm latest a1b2c3d4e5f6 10 seconds ago 285MB
6.4 运行容器
bash
docker run --rm \
-p 8003:8003 \
--env-file D:\my_duyi_project_dir\my_env\wuhan3\.env \
fastapi-llm
命令逐参数解析:
bash
docker run --rm -p 8003:8003 --env-file .../env fastapi-llm
│ │ │ │ │
│ │ │ │ └── 使用的镜像名称
│ │ │ └── 加载外部环境变量文件
│ │ └── 端口映射:宿主机 8003 → 容器 8003
│ └── 容器停止后自动删除(不保留容器实例)
└── 运行命令
参数说明:
| 参数 | 作用 |
|---|---|
--rm |
容器退出后自动删除容器实例,避免产生大量已停止的容器 |
-p 8003:8003 |
将宿主机的 8003 端口映射到容器的 8003 端口 |
--env-file |
从文件加载环境变量(如 API Key、数据库密码等),避免在命令行中明文暴露 |
运行后验证:
bash
# 浏览器访问
http://localhost:8003
# 或使用 curl
curl http://localhost:8003/docs # FastAPI 自动生成的 Swagger 文档
curl http://localhost:8003/health # 健康检查接口(如果实现了的话)
6.5 常用构建参数
bash
# 基础构建
docker build -t my-app .
# 指定 Dockerfile 路径
docker build -f path/to/Dockerfile -t my-app .
# 传入构建参数
docker build --build-arg APP_VERSION=2.0 --build-arg ENV=prod -t my-app .
# 不使用缓存(完全重新构建)
docker build --no-cache -t my-app .
# 指定构建目标阶段(多阶段构建时使用)
docker build --target production -t my-app .
# 限制构建平台(跨平台构建)
docker build --platform linux/amd64 -t my-app .
七、Dockerfile 最佳实践
7.1 合理排序指令,最大化缓存命中
核心原则:将变动频率低的指令放前面,变动频率高的放后面。
bash
# ✅ 推荐顺序
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt . # 低频变动 → 放前面
RUN pip install -r requirements.txt # 依赖安装(耗时)→ 充分利用缓存
COPY . . # 高频变动(代码)→ 放后面
CMD ["uvicorn", "server:app"]
bash
# ❌ 错误顺序:COPY . . 放在前面,任何文件变动都会导致后续全部重建
FROM python:3.11-slim
WORKDIR /app
COPY . . # 任何改动 → 缓存全部失效
RUN pip install -r requirements.txt # 每次都要重新安装,非常浪费时间
CMD ["uvicorn", "server:app"]
7.2 合并 RUN 指令,减少镜像层数
bash
# ✅ 合并为一条 RUN,在同一层清理缓存
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
curl \
wget \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# ✅ pip 安装使用 --no-cache-dir
RUN pip install --no-cache-dir -r requirements.txt
7.3 使用多阶段构建减小镜像体积
对于需要编译的语言(如 Go、Java、C++),多阶段构建可以将编译环境和运行环境分开,极大减小最终镜像体积:
bash
# ========== 第一阶段:构建 ==========
FROM python:3.11 AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
# ========== 第二阶段:运行 ==========
FROM python:3.11-slim
WORKDIR /app
# 只从第一阶段复制安装好的依赖,不带编译工具
COPY --from=builder /install /usr/local
COPY . .
CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "8003"]
7.4 使用非 root 用户运行
bash
FROM python:3.11-slim
# 创建应用用户
RUN groupadd -r appuser && useradd -r -g appuser -d /app -s /sbin/nologin appuser
WORKDIR /app
COPY --chown=appuser:appuser . .
# 切换到非 root 用户
USER appuser
CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "8003"]
7.5 其他实践要点
| 实践 | 说明 |
|---|---|
使用 .dockerignore |
排除无关文件,加速构建,防止敏感信息泄露 |
优先使用 COPY 而非 ADD |
COPY 行为更明确,ADD 仅在需要自动解压时使用 |
使用 Exec 形式的 CMD |
确保应用是 PID 1,能正确处理信号 |
| 使用精确版本标签 | 生产环境避免 latest,保证构建可重复 |
添加 HEALTHCHECK |
让 Docker 和编排工具能监控容器健康状态 |
使用 LABEL 标记元数据 |
便于镜像管理和团队协作 |
| 每个容器只运行一个进程 | 遵循单一职责原则,便于管理和扩缩容 |
八、总结
本文围绕 Dockerfile 这一核心概念,从以下维度进行了系统讲解:
Dockerfile 是什么:镜像的"施工图纸",一个纯文本格式的自动化构建脚本。通过声明式的指令描述了从基础环境到应用部署的完整流程。
为什么需要它:解决了手动构建不可重复、不可追溯、不可自动化的问题。它是环境标准化、构建可重复性和 CI/CD 自动化的基石。
核心指令掌握:
bash
五大核心指令(必须掌握):
FROM → 指定基础镜像(起点)
WORKDIR → 设置工作目录(舞台)
COPY → 复制文件到镜像(搬运材料)
RUN → 构建时执行命令(施工操作)
CMD → 容器启动默认命令(开幕演出)
常用辅助指令:
ENV → 设置环境变量
EXPOSE → 声明服务端口
ARG → 构建时参数
ENTRYPOINT → 容器入口点
.dockerignore → 排除无关文件
构建与运行:
bash
编写 Dockerfile
│
│ docker build -t <镜像名> .
▼
生成镜像(Image)
│
│ docker run -p <端口>:<端口> <镜像名>
▼
容器运行(Container) → 外界可访问
最佳实践核心 :合理排序指令最大化缓存、合并 RUN 减少层数、使用 .dockerignore 加速构建、Exec 形式 CMD 确保信号处理、生产环境使用非 root 用户和精确版本标签。
掌握 Dockerfile 的编写,你就掌握了 Docker 技术栈中最核心的能力------将任何应用标准化打包为可移植、可重复、可自动化的容器镜像。