Dockerfile 完全指南:从指令详解到实战构建

前言

如果说 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. 1.手动启动一个基础容器
  2. 2.进入容器,手动安装依赖、复制文件、修改配置
  3. 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 中的 COPYADD 指令只能操作构建上下文范围内的文件------无法访问上下文之外的文件

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. 1.该指令本身是否发生了变化?
  2. 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 <路径> 

说明:

设置后续指令(RUNCMDCOPYADD)的 默认工作目录。如果目录不存在,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=<用户>:<组>] <源路径> <目标路径> 

说明:

ADDCOPY 功能类似,但有两个额外能力:

  1. 1.自动解压 :如果源文件是 tar 压缩包(.tar.tar.gz.bz2 等),ADD 会自动解压到目标路径
  2. 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 包时才使用 ADDCOPY 的行为更明确、更可预测。

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"

ENVARG 的关键区别:

维度 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

说明:

ENTRYPOINTCMD 类似,但有一个关键区别: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 <用户名>[:<用户组>] 

说明:

指定后续指令(RUNCMDENTRYPOINT)以哪个用户身份执行。默认以 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. 1.加速构建 :不发送无关文件(如 node_modules 可能有几百 MB),显著减少构建上下文大小
  2. 2.保护隐私 :避免将 .env、密钥文件等敏感信息意外打包进镜像
  3. 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 技术栈中最核心的能力------将任何应用标准化打包为可移植、可重复、可自动化的容器镜像

相关推荐
zhangfeng11332 小时前
超算中心 高性能计算 slurm的linux版本 centos7,如何安装docker,如何安装torch2.4
linux·运维·服务器·开发语言·人工智能·机器学习·docker
曲幽3 小时前
你的FastAPI又在服务器上“跑不起来”了?来,今天咱把打包这件事彻底聊透
linux·windows·python·docker·fastapi·web·pyinstaller·nssm·services
_可乐无糖3 小时前
踩完坑之后的总结:Windows安装docker
运维·windows·docker·容器
zhangfeng11334 小时前
,在slurm中也能安装ubundu了,Singularity(现叫 Apptainer)不需要root权限的容器方案,对比docker
运维·人工智能·机器学习·docker·容器
李白的天不白4 小时前
服务器无法连接到 Docker Hub 的官方镜像仓库
运维·服务器·docker
Irene19914 小时前
在 VSCode WSL 中安装 Docker 插件(Docker 插件连接 WSL)
vscode·docker
IDIOT___IDIOT4 小时前
Docker 集群运行 Spark 的一些记录
docker·容器·spark
北执南念5 小时前
Docker实用篇2
运维·docker·容器
踏着七彩祥云的小丑5 小时前
AI学习——Docker 打包与部署
人工智能·学习·docker·ai