☰
【FDE系列】阶段2:Day 42:Dockerfile 实战 — 把你的应用打包成镜像
2026/10/29 1:53:52 网站建设 项目流程

📚前言

📒FDE系列内容总纲:

【大纲】FDE 前沿部署工程师学习系列教程-CSDN博客

🚄前置课程列表:

见文档结尾附录。


🚀阶段2·Day 42:Dockerfile 实战 — 把你的应用打包成镜像

FDE 学习系列教程 · 第二阶段 · 第 5 周 · Day 2预计时长:3 小时 | 难度:★★★☆☆ | 前置知识:Day 41 容器操作、FastAPI、pip 依赖管理

📌一句话目标:看懂并写出生产可用的 Dockerfile,理解层缓存机制,把 FastAPI 项目构建成镜像、跑成容器,一条命令在任何机器上复现你的应用。


🧑‍🤝‍🧑 开场:从"用别人的镜像"到"造自己的镜像"

昨天你跑的是nginx、python这些官方现成镜像——环境是别人配好的,你只能用。

但 FDE 交付的核心是你自己写的应用。客户要的不是一个 Python 解释器,而是你的工单系统。所以今天要做的事是:把"Python 环境 + 第三方依赖 + 你的代码"逐层叠加,做成一个专属镜像。

做这件事的"配方文件"就叫Dockerfile——一个没有后缀名的纯文本文件,里面一行一行写着"从什么基础开始、装什么、拷什么、启动时跑什么"。Docker 读着这份配方,就能自动把镜像构建出来。

昨天:docker pull nginx 用别人做好的镜像(下载现成的) 今天:docker build -t myapi . 按自己写的 Dockerfile 造镜像(亲手做) 有了自己的镜像之后: 你的笔记本 build 一次 ──► 镜像传到客户服务器 / 镜像仓库 │ 客户机器 docker run 一下 │ 你的工单系统立刻跑起来 (不用装 Python、不用 pip install,环境 100% 一致)

📖 一、先认识 Dockerfile 的 7 个核心指令

别被满屏大写指令吓到,常用的就 7 个。先混个眼熟,待会儿逐个用:

指令

大白话

类比做菜

FROM

基于哪个基础镜像开始

选什么锅底

WORKDIR

设定容器内的工作目录

进厨房站哪个工位

COPY

把宿主机文件拷进镜像

把食材拿进厨房

RUN

构建时执行命令(装依赖等)

洗菜、切菜、腌制

ENV

设置环境变量

调固定的火候参数

EXPOSE

声明容器监听的端口(文档作用)

挂个"出餐口"指示牌

CMD

容器启动时默认执行的命令

最终开火出锅

构建时(build 那一刻执行) 运行时(run 那一刻才执行) ──────────────────────── ──────────────────────── FROM / WORKDIR / COPY / RUN CMD ENV / EXPOSE 这些是在"造镜像" 这是在"启容器" 做一次,结果烤进镜像 每次 docker run 都跑一遍

💡 最关键的区分:RUN 在构建镜像时执行一次,结果固化进镜像;CMD 在容器每次启动时执行。装依赖用 RUN,启动服务用 CMD,千万别混。


🖥️ 二、准备项目:就用你的 FastAPI

在 WSL 里建一个干净的练习项目(或直接用第 2 周的工单项目,今天先用最小版本):

mkdir -p ~/docker_lab/fde_api && cd ~/docker_lab/fde_api

项目结构今天长这样:

fde_api/ ├── main.py ← FastAPI 代码 ├── requirements.txt ← 依赖清单 ├── Dockerfile ← 今天的主角(待会写) └── .dockerignore ← 待会写,控制哪些文件不进镜像

main.py:

from fastapi import FastAPI import uvicorn import sys import os app = FastAPI(title="FDE API", version="1.0") @app.get("/") def root(): return { "msg": "Hello from Docker!", "python": sys.version.split()[0], "env": os.getenv("APP_ENV", "dev"), } @app.get("/health") def health(): return {"status": "ok"} @app.get("/info") def info(): return { "service": "fde-api", "version": "1.0", "docs": "/docs", } if __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=8000)

📌 注意host="0.0.0.0":容器里的服务必须监听 0.0.0.0(所有网卡),不能是 127.0.0.1。如果只监听 127.0.0.1,端口映射做了也访问不到——因为那是容器内部的"本地回环",外部进不来。这是 Docker 化应用的头号新手坑。

requirements.txt:

fastapi==0.115.0 uvicorn==0.30.0

💡 依赖锁版本号(==0.115.0而不是fastapi)是容器化的关键:保证每次构建装到的依赖一模一样。不锁版本,今天 build 和下个月 build 可能装到不同的新版,行为不一致——又回到"环境漂移"的老问题。


🖥️ 三、写第一个 Dockerfile

在项目根目录新建文件,文件名就叫Dockerfile(没有后缀名,D 大写是惯例):

# ========================================== # FDE API 镜像配方(第一版,能跑) # ========================================== # ① FROM:选基础镜像——一切的起点 # python:3.12-slim 是官方 Python 的精简 Debian 版,约 120MB FROM python:3.12-slim # ② WORKDIR:设定容器内工作目录 # 不存在会自动创建;后续所有相对路径都以它为基准 WORKDIR /app # ③ COPY:把依赖清单拷进镜像 COPY requirements.txt . # ④ RUN:构建时安装依赖(结果固化进镜像) # --no-cache-dir:pip 不缓存安装包,减小镜像体积 # -i:用国内清华镜像源加速(国内构建强烈建议) RUN pip install --no-cache-dir -r requirements.txt \ -i https://pypi.tuna.tsinghua.edu.cn/simple # ⑤ COPY:把项目代码拷进镜像 COPY . . # ⑥ EXPOSE:声明容器服务端口(主要是文档/提示作用) EXPOSE 8000 # ⑦ CMD:容器启动时默认执行的命令 # exec 格式(JSON 数组写法),信号能正确传递,推荐 CMD ["python", "main.py"]

每个指令逐个拆

FROM python:3.12-slim

镜像标签的选择是门学问: python:3.12 完整 Debian 版,约 900MB+,构建工具齐全但臃肿 python:3.12-slim 精简 Debian 版,约 120MB,绝大多数应用够用 ⭐推荐 python:3.12-alpine Alpine 版,约 45MB,最小但偶尔有 C 扩展依赖装不上

新手用 slim 最稳:体积小,且因为是 Debian 系,apt-get装系统库方便。

WORKDIR /app

设定后,后面的 COPY、RUN 都在/app下进行。没有它,文件会散落在根目录/,还可能遇到权限问题。相当于"进容器后先cd /app"。

COPY . .

第一个.是构建上下文(你执行 build 的宿主机目录),第二个.是容器内当前工作目录(即 /app)。连起来:把宿主机当前目录的内容拷进容器的 /app。

CMD 的两种写法

# exec 格式(推荐):JSON 数组,命令和参数分开 CMD ["python", "main.py"] # shell 格式:会被包一层 /bin/sh -c,信号传递有问题(Ctrl+C 可能停不干净) CMD python main.py

📖 四、.dockerignore:别把垃圾打进镜像

构建时COPY . .会把当前目录所有文件拷进镜像。但你的目录里有一堆不该进镜像的东西:

fde_api/ ├── venv/ ← 虚拟环境,几百MB!容器里要重新装,绝不能拷 ├── __pycache__/ ← Python 缓存,没用 ├── .git/ ← Git 历史,可能很大 ├── .env ← 密钥/密码!打进镜像会泄露,最危险 ├── *.md ← 说明文档,运行时不需要 └── app.log ← 本地日志

新建.dockerignore(语法和 .gitignore 一样):

# Python 缓存 __pycache__/ *.py[cod] # 虚拟环境 venv/ .venv/ # 环境变量与密钥(重点!) .env .env.* # Git .git/ .gitignore # 日志与临时文件 *.log *.tmp # 文档和 IDE *.md .vscode/ .idea/ # Docker 自身(没必要套娃) Dockerfile docker-compose.yml

📌不写 .dockerignore 的两个后果:

  1. 镜像被 venv/.git 撑大几百 MB,构建还慢

  2. .env里的数据库密码被打进镜像,谁拿到镜像谁就能解出密钥——安全事故

每个项目第一件事:写 .dockerignore。


🖥️ 五、构建镜像:docker build

文件齐了,开始构建:

cd ~/docker_lab/fde_api # 语法:docker build -t 镜像名:标签 构建上下文路径 docker build -t fde-api:1.0 .

参数拆解:

docker build -t fde-api:1.0 . │ └──┬──┘ └┬┘ │ │ └─ 构建上下文(build context):点表示当前目录 │ │ Docker 会把这个目录打包发给构建引擎 │ └─ 标签 tag:名字:版本 └─ --tag 的缩写,给镜像起名

观察构建输出,每一步对应 Dockerfile 的一层:

[+] Building 18.2s (10/10) FINISHED => [internal] load build definition from Dockerfile => => transferring dockerfile: 512B => [internal] load .dockerignore => [internal] load metadata for docker.io/library/python:3.12-slim => [1/6] FROM python:3.12-slim@sha256:... => [2/6] WORKDIR /app => [3/6] COPY requirements.txt . => [4/6] RUN pip install --no-cache-dir -r requirements.txt ... ← 这层最耗时 => [5/6] COPY . . => [6/6] CMD ["python", "main.py"] => exporting to image => => writing image sha256:a1b2c3... => => naming to docker.io/library/fde-api:1.0

看一下成果:

docker images # REPOSITORY TAG IMAGE ID SIZE # fde-api 1.0 a1b2c3d4e5f6 198MB # python 3.12-slim 9f.... 125MB

198MB = 125MB 基础镜像 + 你的依赖和代码。


🖥️ 六、把镜像跑成容器

# 用昨天学的参数:后台、命名、端口映射 docker run -d --name fde-api -p 8000:8000 fde-api:1.0 # 看日志,确认 uvicorn 起来了 docker logs fde-api # INFO: Started server process [1] # INFO: Uvicorn running on http://0.0.0.0:8000 # INFO: Application startup complete.

从 Windows 浏览器或另开终端验证:

curl http://localhost:8000/ # {"msg":"Hello from Docker!","python":"3.12.x","env":"dev"} curl http://localhost:8000/health # {"status":"ok"} curl http://localhost:8000/info # {"service":"fde-api","version":"1.0","docs":"/docs"}

浏览器打开 **http://localhost:8000/docs**,连 Swagger 文档都在。你本机没装这些依赖,它们全活在容器里。

再玩一下环境变量(Dockerfile 里的 ENV / run 时的 -e):

docker rm -f fde-api # -e 在运行时注入环境变量,覆盖镜像里的默认值 docker run -d --name fde-api -p 8000:8000 -e APP_ENV=production fde-api:1.0 curl http://localhost:8000/ # {"msg":"Hello from Docker!","python":"3.12.x","env":"production"}

进容器里看看你的代码:

docker exec -it fde-api bash # root@a1b2c3:/app# ls # main.py requirements.txt # root@a1b2c3:/app# python --version # Python 3.12.x # exit

📖 七、层缓存:让构建快 10 倍的关键机制

这个概念听着唬人,拆开看就那么回事,而且它解释了为什么 Dockerfile 要"先拷 requirements.txt,再拷代码"那个奇怪的顺序。

镜像是一层一层叠起来的

Dockerfile 的每条指令生成一层,层是可以缓存复用的:

Dockerfile 镜像层 ─────────── ───────────────────── FROM python:3.12-slim ──► Layer 1 基础环境(125MB,几乎不变) WORKDIR /app ──► Layer 2 建目录 COPY requirements.txt . ──► Layer 3 依赖清单 RUN pip install ... ──► Layer 4 装好的依赖(最耗时!) COPY . . ──► Layer 5 你的代码(经常变) EXPOSE 8000 ──► Layer 6 CMD ["python","main.py"] ──► Layer 7

重新 build 时,Docker 从第一层开始检查:这一层的输入和上次一样吗?一样就直接用缓存,不重新执行。一旦某一层变了,它自己和后面所有层的缓存全部失效。

关键:依赖层要放在代码层前面

❌ 错误顺序(先拷全部代码,再装依赖): COPY . . 你改了一行 main.py RUN pip install -r req.txt │ ▼ COPY 这层变了 → 缓存失效 ▼ 连带着 RUN pip install 也必须重跑! (每次改代码都重新下载安装几十个依赖,几分钟) ✅ 正确顺序(先拷依赖清单,装依赖,最后才拷代码): COPY requirements.txt . 改 main.py 不影响这个文件 RUN pip install ... │ ▼ requirements.txt 没变 COPY . . 依赖层命中缓存,秒过!✅ ▼ 只有最后的代码层重新构建(秒级)

亲手验证缓存效果:

# ① 第二次构建(什么都不改):全程 CACHED,秒完 docker build -t fde-api:1.0 . # => CACHED [3/6] COPY requirements.txt . # => CACHED [4/6] RUN pip install ... # 几乎不花时间 # ② 改一行 main.py(比如改个字符串),再构建 # 观察:1~4 层全 CACHED,只有 [5/6] COPY . . 重新执行 docker build -t fde-api:1.0 . # ③ 故意改 requirements.txt(加一行依赖),再构建 # 观察:从第 3 层开始缓存失效,pip install 重新跑

📌Dockerfile 排序黄金法则:把变化频率低的放前面,变化频率高的放后面。基础系统 → 系统依赖 → Python 依赖 → 业务代码。这是构建速度优化的核心。


📖 八、镜像标签管理:版本号别只会用 latest

# 给同一个镜像打多个标签(像起别名) docker tag fde-api:1.0 fde-api:latest docker tag fde-api:1.0 fde-api:20260920 docker images | grep fde-api # fde-api 1.0 a1b2c3 # fde-api latest a1b2c3 ← 同一个镜像 ID,两个名字 # fde-api 20260920 a1b2c3 # 删除标签(还有别的标签指向时,只删名字不删镜像) docker rmi fde-api:20260920
标签策略建议(FDE 交付): fde-api:1.0 语义版本:功能变更时递增(推荐主用) fde-api:1.0.2 更精细的补丁号 fde-api:20260920 日期版本:每天构建的产物 fde-api:latest 最新版:方便,但生产环境【不要】依赖它 (latest 会变,你无法确定跑的到底是哪版) ⚠️ 客户现场部署:明确指定版本号 fde-api:1.0 这样回滚时 docker run fde-api:0.9 就行,清清楚楚

❌ 常见翻车点对照表

坑

现象

解决

服务监听 127.0.0.1

容器起来了,宿主机访问不到

uvicorn 必须--host 0.0.0.0

忘写 .dockerignore

镜像几百 MB,build 还慢

排除 venv/.git/.env

依赖放代码后面 COPY

改一行代码重装几分钟依赖

先 COPY requirements 再装再拷代码

CMD 用 shell 格式

docker stop 半天停不掉

用 exec 格式["python","main.py"]

用完整 python:3.12

镜像 900MB+

换 slim 或 alpine

依赖不锁版本

不同时间 build 结果不一致

requirements.txt 用==

构建上下文太大

日志卡 "sending build context"

.dockerignore 排除大文件

改了代码没重新 build

容器里还是旧代码

改代码后重新docker build再 run

⚠️ 最后一条特别重要:代码打进镜像后是"快照"。你之后在宿主机改 main.py,正在运行的容器不会自动更新——要么重新 build + 重建容器,要么开发时用-v把代码目录挂载进去(代码实时同步,适合开发;生产用构建进镜像的方式)。


📝 本课小结

知识点

一句话记住

Dockerfile

构建镜像的配方,无后缀文本文件

FROM

选基础镜像,推荐 python:3.12-slim

WORKDIR

设定工作目录 /app

COPY

拷文件进镜像

RUN

构建时执行(装依赖),结果固化

EXPOSE

声明端口(文档作用)

CMD

启动时执行,用 exec 数组格式

.dockerignore

排除 venv/.git/.env,瘦身又安全

docker build -t 名:版本 .

构建镜像,点是上下文

层缓存

每层可复用,某层变则其后全失效

排序法则

不变的在前,常变的代码在最后

0.0.0.0

容器服务必须监听它,否则外部访问不到

标签

用版本号,别在生产依赖 latest

代码更新

改完要重新 build 重建容器

🧠 核心认知:Dockerfile 本质是一份可复现的环境构建说明书。它把"新机器上从零搭环境"这件手工、易错的事,变成一条docker build命令。而层缓存机制让"可复现"和"构建快"两者兼得——写好指令顺序,是 Dockerfile 工程师的基本功。


📋 课后练习

练习 1:从零构建并运行(约 30 分钟)

不看教程,在一个新目录里独立完成:

  1. 写 main.py(含/、/health、/info三个接口)、requirements.txt、Dockerfile、.dockerignore

  2. docker build -t myapi:1.0 .构建成功

  3. docker run -d -p 8000:8000 myapi:1.0跑起来

  4. 用 curl 验证三个接口都通

  5. docker exec进容器确认代码和 Python 环境

练习 2:观察层缓存(约 20 分钟)

  1. 什么都不改连续 build 两次,记录第二次的耗时和 CACHED 提示

  2. 修改 main.py 里一个字符串再 build,记录从哪一步开始重新执行

  3. 在 requirements.txt 加一行(如requests==2.32.3)再 build,观察 pip 层重新执行

  4. 用自己的话回答:为什么 Dockerfile 要把 COPY requirements 放在 COPY . . 前面?

练习 3:加环境变量与标签(约 20 分钟)

  1. 在 Dockerfile 里加ENV APP_ENV=production,重新 build,访问/确认 env 变成 production

  2. 用docker run -e APP_ENV=staging临时覆盖,确认运行时的 -e 优先级更高

  3. 给镜像打myapi:latest标签,删除:1.0标签(保留 latest),观察镜像本身是否还在


🔭 下节预告

单个容器你玩得转了。但真实项目从来不只有一个容器——你的工单系统需要API + MySQL(明天可能还要加 Nginx、Redis)。一个个手动docker run,要记住一堆参数、还要操心启动顺序、网络互通,太痛苦。

明天学Docker Compose:把所有容器的配置写进一个docker-compose.yml,一条docker compose up -d把整套环境(API、数据库、网络、数据卷)一次性拉起来。这才是现代项目的标准打开方式,明天见!


🌍附录:前置课程列表

阶段一:认知启蒙(AI 认知与 FDE 角色)
AI 认知

【FDE系列】阶段1Day 1:AI 层级关系 — 四个嵌套的圈-CSDN博客

【FDE系列】阶段1Day 2:AI 三阶段发展史 — 会认 → 会判断 → 会创造-CSDN博客

【FDE系列】阶段1Day 3:符号 AI vs 机器学习 — 两条路线的本质区别-CSDN博客

【FDE系列】阶段1Day 4:Transformer 的历史意义 — 2017 年的分水岭-CSDN博客

【FDE系列】阶段1Day 5:本周复习与自测 — 检验你的 AI 认知地基-CSDN博客

【FDE系列】阶段1Day 6:Transformer 架构 — 一张图纸盖出千千万万栋楼-CSDN博客

【FDE系列】阶段1Day 7:LLM 本质 — 文字接龙机器-CSDN博客

【FDE系列】阶段1Day 8:Token — 模型眼中的最小单位-CSDN博客

【FDE系列】阶段1Day 9:AI 幻觉 — 为什么会一本正经地胡说八道-CSDN博客

【FDE系列】阶段1Day 10:上下文窗口 — 模型的记忆力上限 + 本周复习-CSDN博客

【FDE系列】阶段1Day 11:Prompt — 给模型立规矩-CSDN博客

【FDE系列】阶段1Day 12:Memory — 让模型记住上下文

【FDE系列】阶段1Day 13:RAG — 给模型配图书管理员-CSDN博客

【FDE系列】阶段1Day 14:Tool Use — 让模型动手操作-CSDN博客

【FDE系列】阶段1Day 15:MCP — 统一的工具接口标准 + 第三周复习-CSDN博客

FDE 基础概念

【FDE系列】阶段1Day 16:什么是 FDE — 把 AI 变成客户结果的人-CSDN博客

【FDE系列】阶段1Day 17:FDE vs 传统实施 — 三大本质区别-CSDN博客

【FDE系列】阶段1Day 18:FDE 三重身份 + C6 胜任力模型-CSDN博客

【FDE系列】阶段1Day 19:七阶段行动路径 + 行业经验的价值-CSDN博客

【FDE系列】阶段1Day 20:阶段总结与产出物 — 第一阶段收官-CSDN博客


阶段二:技术地基(Python + FastAPI + SQL + Docker + API 集成)
Python基础

【FDE系列】阶段2:Day 21:Python 环境搭建 — 写出你的第一行代码-CSDN博客

【FDE系列】阶段2:Day 22:变量、数据类型、条件判断 — Python 的“记忆“和“判断“-CSDN博客

【FDE系列】阶段2:Day 23:循环与函数 — 让代码跑 100 遍、把逻辑打包复用-CSDN博客

【FDE系列】阶段2:Day 24:数据结构 — 列表、字典、集合、元组-CSDN博客

【FDE系列】阶段2:Day 25:文件读写与 JSON — 让程序连通外部数据(第一周收官)-CSDN博客

【FDE系列】阶段2:Day 26:模块化编程 — 把代码拆成“抽屉柜“-CSDN博客

【FDE系列】阶段2:Day 27:异常处理与日志 — 让程序“摔不烂、查得到“-CSDN博客

FastAPI入门到进阶

【FDE系列】阶段2:Day 28:FastAPI 入门 — 把你的函数变成 API 服务-CSDN博客

【FDE系列】阶段2:Day 29:FastAPI 进阶 — Pydantic 模型与完整 CRUD 实战-CSDN博客

【FDE系列】阶段2:Day 30:生产代码规范 — 测试、类型注解、配置管理(第二周收官)-CSDN博客

SQL基础

【FDE系列】阶段2:Day 31:SQL 基础 — 增删改查一把梭-CSDN博客

【FDE系列】阶段2:Day 32:多表查询 — JOIN 与聚合-CSDN博客

【FDE系列】阶段2:Day 33:进阶查询 — 窗口函数与 CTE-CSDN博客

【FDE系列】阶段2:Day 34:数据清洗 — 把脏数据捋干净-CSDN博客

【FDE系列】阶段2:Day 35:Python + SQL — 工单接入 MySQL + 本周收官-CSDN博客

Linux基础

【FDE系列】阶段2:Day 36:Linux 入门与文件操作 — 扔掉鼠标的第一天-CSDN博客

【FDE系列】阶段2:Day 37:权限、进程与文本三剑客-CSDN博客

【FDE系列】阶段2:Day 38:Shell 脚本 — 把命令串起来自动跑-CSDN博客

【FDE系列】阶段2:Day 39:Linux 综合实战 — 让服务无人值守-CSDN博客

【FDE系列】阶段2:Day 40:Shell 进阶 — 生产级脚本与本周收官-CSDN博客

Docker

【FDE系列】阶段2:Day 41:Docker 入门 — 把环境装进盒子-CSDN博客

【FDE系列】阶段2:Day 41(up详细版):Docker 入门 — 把环境装进盒子-CSDN博客

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询