60天企业 AI 实战训练营 · 阶段 1 · 第 1 周

Docker 与
AI 运行环境

能够将 Python / AI 应用封装为可复制的运行环境

Day 05 讲师 · 包学斌 2026 · 09 含实操 · 请提前装好 Docker Desktop
app:v1 deps python:3.11 debian:12

按 → 或 空格键 翻页 · F 全屏

今天这堂课解决什么问题

课程目标

01
环境可复制
同一份 Dockerfile 在谁机器上构建都一样 —— "在我电脑上是好的"成为历史。
02
隔离与干净
每个服务一个容器,想删就删、想换就换,互不拖累,系统永远干净。
03
工程化交付
镜像就是版本化交付物:一条命令启动,回滚只是换个 tag。
它在 60 天课程中的位置
Day 2-3 · Python 企业开发基础已完成
Day 4 · Git 与企业软件开发规范已完成
Day 5 · Docker 与 AI 运行环境今天
Day 6 · HTTP、REST API 与 FastAPI下一课
Day 7 · 大语言模型工程基础
阶段 1 交付底线:Git 管理 ✓ · Docker 部署 · 结构化日志 · 健康检查 —— 今天拿下第二道门槛
互动 · 举手投票 + 点击选择

你装环境的真实经历是?

凭直觉点一个 —— 都装过环境的,不用不好意思

点一个选项,看看它意味着什么 →
01
Part 01

为什么需要容器

"在我电脑上是好的" —— 这句话在企业里等于事故报告

Linux 环境漂移 依赖地狱 交付即镜像
Part 01 · 为什么需要容器

没有容器的世界:环境地狱

你的电脑
跑通了
Python 3.11.6
CUDA 12.1 · cuDNN 8.9
pandas 2.2.2 · numpy 1.26
同事的电脑
起不来
numpy 2.0 装了新库
pandas 被连带升级
ImportError: 冲突
服务器
更起不来
glibc 太老
CUDA 11.8 ≠ 12.1
没有 root 权限装东西
差异不在代码,而在环境:解释器、依赖、系统库、驱动。所以企业交付的不是"代码包 + 安装文档",而是连环境一起打包的运行单元。
Part 01 · 为什么需要容器

容器 vs 虚拟机:轻在哪

虚拟机 VM 分钟级启动 · GB 级
Hypervisor 上跑一个完整客户操作系统(内核+驱动+全家桶)。隔离最强,但笨重:启动慢、占用大、镜像几十 GB。
容器 Container 秒级启动 · MB 级
共享宿主机内核,只隔离文件系统、进程、网络命名空间。轻快到可以"用完即删",把环境当随时可重建的临时物。
一句话:虚拟机隔离了整个计算机,容器只隔离"用户空间" —— 这正是它能做到秒级启动、随开随删的原因。
Part 01 · 为什么需要容器

Docker 三件套(对照 Git 记)

Image 镜像
可复制的软件环境模板:分层只读文件系统。类比 Git 里的 commit —— 不可变快照。
Container 容器
镜像运行起来的实例:共享内核、彼此隔离、天生可弃。类比 checkout 出来的工作区。
Registry 仓库
存镜像、分镜像的地方:Docker Hub / 企业自建 Harbor。类比 GitHub
昨天 ↔ 今天
Git 管代码:commit 快照 · 分支 · push;Docker 管环境:image 快照 · 容器 · push。Dockerfile 就是环境的"源代码" —— 所以它必须进 Git。
02
Part 02

镜像与容器

镜像是分层只读的模板,容器是它的运行实例

Docker 分层只读 写时复制 秒级启动
Part 02 · 镜像与容器

镜像:一层一层叠出来的

每点一次,多叠一层 —— 按 Dockerfile 的构建顺序

4COPY src/ /app/src你的代码2 KB
3RUN pip install -r requirements.txt210 MB
2FROM python:3.11-slim解释器与标准库55 MB
1FROM debian:12基础系统层75 MB
层间共享
同事的镜像也有 1、2 层?pull 只拉缺的层,几 MB 就完事。
全部只读
层一旦生成就不变;"改镜像"只能重新构建出新层。
Registry 分发
Docker Hub / 企业 Harbor:镜像按层上传下载,版本即 tag。
Part 02 · 镜像与容器

容器:镜像跑起来的样子

每点一次,看下面发生什么 —— 关键词:写时复制(Copy-on-Write)

app:v1 镜像 只读 · 不动
COPY src/
pip deps
python:3.11
所有容器共享这三层,没人能改它。
容器 A 可写层 运行中 → 已停止
日志、临时文件都写进自己的薄薄一层,镜像纹丝不动。
容器 B 可写层
同一个镜像,可以同时起 N 个互不干扰的容器。
docker rm 容器A —— 可写层被整个丢弃,日志、临时文件全部消失,镜像毫发无损。
所以规则很明确:要活下来的数据别放容器里 —— 挂 Volume,下一节讲。
Part 02 · 镜像与容器

日常命令速查

docker pull python:3.11-slim # 拉镜像 docker build -t app:v0.1 . # 构建+tag docker run -d -p 8080:8000 app:v0.1 # 起容器 docker run --rm -it python:3.11-slim # 临时容器 docker ps / docker ps -a # 看容器 docker logs -f <id> # 跟日志
docker exec -it <id> bash # 进容器 docker stop <id> # 优雅停止 docker rm <id> / rmi <img> # 删除 docker system df # 磁盘占用 docker compose up -d # 一键整套 docker tag a:v0.1 hub/a:v0.1 # 推送前改名
新人心法
迷路时只做两件事:docker ps -adocker logs —— 先看清有哪些容器、它说了什么,再决定下一步。90% 的"容器恐慌"靠这两条化解。
实操 ① · 10 分钟 · 需提前安装 Docker Desktop

第一个容器

1确认环境就绪:docker --version && docker info
2docker run hello-world —— 体验"拉镜像 → 起容器 → 打印 → 退出"全过程
3docker images 看刚拉下来的镜像;docker ps -a 看退出的容器
4不装 Python 用上 Python:docker run --rm -it python:3.11-slim(exit 退出)
5清理现场:docker rm $(docker ps -aq)(PowerShell 用 docker ps -aq | % { docker rm $_ }
10:00
验收标准
docker images 能看到 hello-world 与 python 镜像
python:3.11-slim 里成功进了 REPL
能对同伴说清 image 与 container 的区别

卡住就举手 —— 实操环节助教会巡场

互动 · 快问快答

对错判断:三道送命题

先举手投票,再点击揭晓答案

1. 容器一删,容器里写的日志文件也跟着没了?
对 —— 容器天生可弃。日志要么打到 stdout(docker logs 收集),要么挂 Volume 落盘。
✓ 对
2. 镜像就是一个大压缩包,pull 就是把它解压出来?
错 —— 镜像是一组分层只读文件系统,按层共享、按层下载,内容寻址。
✗ 错
3. 同一个镜像在一台机器上只能起一个容器?
错 —— 镜像只读,N 个容器各有一份自己的可写层,同时跑、互不干扰。
✗ 错
03
Part 03

Dockerfile:环境即代码

依赖、文件、启动命令,全部写进版本库

Docker 指令即层 缓存友好 可复现
Part 03 · Dockerfile

六个核心指令 + 最小可用模板

FROM基础镜像 —— 一切的起点
COPY把文件拷进镜像
RUN构建时执行命令(产生新层)
ENV声明运行时环境变量
EXPOSE文档声明端口(不真正开放)
CMD容器启动的默认命令
FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . EXPOSE 8000 CMD ["python", "serve.py"]

进阶:ENTRYPOINT 固定入口 + CMD 可覆盖参数 —— 容器既能固定行为又能灵活传参。

Part 03 · Dockerfile

层缓存:改一行代码,重建要多久

场景:只改了 app.py 的一行代码,重新 docker build

1FROM python:3.11-slimhit ✓
2COPY requirements.txt .hit ✓
3RUN pip install -r requirements.txthit ✓(省 3 分钟)
4COPY . .(含你改的 app.py)rebuild ✗
5CMD ["python", "app.py"]rebuild ✗
规则
某一层变了,它之后的所有层都重跑。所以:不常变的放前面,常变的放后面
黄金顺序
COPY requirements.txtRUN pip install → 最后才 COPY 源码。依赖不变时,改代码只需秒级重建。
互动 · 找茬游戏

这些 Dockerfile 写法合格吗?

先心里给个判断,再点击揭晓

FROM python:latest 基础镜像天天变,今天和昨天构建出两个不同的镜像 —— 锁定 python:3.11-slim
RUN pip install --no-cache-dir -r requirements.txt 一层装完依赖,且不留 pip 缓存
COPY . . 放在 RUN pip install 之前 源码一改依赖层全重跑 —— 先装依赖,最后才 COPY 源码
ENV OPENAI_API_KEY=sk-xxxx ENV 留在镜像层里,拿到镜像就拿到密钥 —— 用运行时 -e / --env-file 注入
RUN apt-get install -y --no-install-recommends gcc 不拖一堆推荐包,镜像更小
EXPOSE 8000 就对外开放了 8000 端口 EXPOSE 只是文档声明,真正开放靠 -p 端口映射
04
Part 04

数据、端口与配置

数据活得比容器久,密钥不写死在代码里

Docker Volume 持久化 端口映射 环境变量注入
Part 04 · 数据、端口与配置

Volume:把数据放在容器之外

每点一次,看下面发生什么

etl 容器 可写层 + 挂载
处理结果写进 /app/data(挂载点,不在可写层里)
named volume: etl-data Docker 管理 · 独立存活
模型缓存 · 向量库数据 · 日志目录 —— 都该住这里。
新容器(重建后) 挂同一个卷
数据原样都在 —— 换容器不换数据
两种挂法
-v etl-data:/app/data named volume(推荐);-v ./data:/app/data bind mount(开发时方便看文件)。
企业落地
模型权重走 Volume / 对象存储(S3);跨节点用远程存储(NFS);敏感数据不要用 bind mount 暴露,注意挂载权限。
Part 04 · 数据、端口与配置

Port:宿主机端口 → 容器端口

每点一次,看请求怎么走

宿主机 对外 8080
curl http://localhost:8080
浏览器 / 上游服务都打这里
-p 8080:80 iptables / NAT 转发
容器内 监听 80
nginx / uvicorn
只在自己的命名空间里听
两条铁律
EXPOSE 只是文档声明,不实际开放端口;生产环境不直接暴露容器端口 —— 前面接 Nginx / Ingress 做 TLS 终结与限流。内部服务(DB、Redis)绝不映射到宿主机
Part 04 · 数据、端口与配置

环境变量:密钥的正确入口

12-Factor:配置与代码分离
同一镜像从 dev 跑到 prod,变的只有配置。注入方式:-e KEY=value--env-file .env;K8s 用 ConfigMap / Secret。
源码里只留模板
.env.example 入 Git(只有键名没有值),真实 .env 进 .gitignore —— 呼应昨天的 Day 4。
也别打进镜像
ENV 写死密钥 = 密钥永久留在镜像层里。谁拉到镜像,谁就拿到密钥。
# 本地开发(.env 不进 Git) docker run --rm \ --env-file .env \ -p 8080:8000 \ app:v0.1 # CI / 生产(由平台注入) docker run -d \ -e DB_HOST=$DB_HOST \ -e MODEL_PATH=/models/v3 \ app:v0.1
常见坑
把 .env 提交进 Git;容器日志里打印完整环境变量 —— 密钥泄漏等同生产事故。
实操 ② · 12 分钟 · 两人一组

给昨天的仓库写 Dockerfile

1进入 day04-lab(昨天的仓库),新建 requirements.txtpandas==2.2.2(锁版本!)
2新建 Dockerfile:slim 基础镜像 → 先 COPY requirements → pip install → 再 COPY 源码
3docker build -t day05-etl:v0.1 . —— 盯着输出看每一层
4docker run --rm -v ${PWD}/data:/app/data day05-etl:v0.1 跑通数据处理
5再 build 一次 —— 观察哪些层显示 CACHED
12:00
验收标准
容器里能读到 /app/data 并输出结果
docker images 镜像体积 < 300MB
第二次 build 依赖层显示 CACHED

卡住就举手 —— 实操环节助教会巡场

05
Part 05

Compose 与企业落地

从"一个容器"到"一套服务",交付物就是镜像

Compose 一键多服务 健康检查 版本即交付
Part 05 · Compose 与企业落地

Compose:一套服务一个 YAML

services: etl: build: . image: day05-etl:v0.1 volumes: - ./data:/app/data env_file: .env restart: unless-stopped cache: image: redis:7-alpine # 同网络内可达,无需映射端口
一条命令
docker compose up -d --build 全部起;logs -f 看日志;down 全部收。
适用边界
本地开发与 POC 演示首选 Compose;生产多节点迁移 Kubernetes
常见坑
depends_on 只保证启动顺序不保证就绪 —— 等依赖就绪要配 healthcheck + condition: service_healthy
Part 05 · Compose 与企业落地

企业视角:交付物 = 镜像

版本即交付
镜像 tag 与 git tag 对应:app:v1.2.3。CI 构建完自动 push 到 Harbor。
分发即部署
目标机器 docker pull + run —— 不再传"压缩包 + 安装文档"。
回滚秒级
出问题?run 回上一版 tag。旧镜像还在仓库里躺着,随时可切。
企业的验收标准很朴素:任何人、任何机器,一条命令跑起来。做到这一点的团队,才有资格谈"上线"。
Part 05 · Compose 与企业落地

容器避坑清单

1
基础镜像用 latest
构建结果天天变,"上礼拜还能跑"成为玄学 —— 基础镜像锁定具体版本。
2
日志只写在容器里
容器一删全没了 —— 输出到 stdout(平台统一收集),或挂 Volume。
3
不清理 pip / apt 缓存
镜像动辄几个 G —— --no-cache-dir + 清理列表,AI 镜像也要"瘦身"。
4
把 DB / Redis 端口映射到宿主机公网
-p 0.0.0.0:5432:5432 等于裸奔 —— 内部服务走容器网络,不对外。
06
Part 06

AI 项目的容器规范

AI 项目的特殊问题:大模型、数据、一天跑通

Python 精简基础镜像 模型外挂 新人一天跑通
Part 06 · AI 项目容器规范

AI 服务的标准 Dockerfile

# ---- 阶段1:构建 ---- FROM python:3.11-slim AS builder WORKDIR /app COPY requirements.txt . RUN pip install --prefix=/install \ --no-cache-dir -r requirements.txt # ---- 阶段2:运行 ---- FROM python:3.11-slim WORKDIR /app COPY --from=builder /install /usr/local COPY src/ src/ ENV PYTHONUNBUFFERED=1 HEALTHCHECK --interval=30s CMD python healthcheck.py ENTRYPOINT ["python", "-m", "src.serve"] CMD ["--port", "8000"]
多阶段构建工具不进最终镜像,体积砍半
healthz健康检查 —— 阶段 1 交付底线之一
UNBUFFERED日志实时到 stdout,不滞留容器
模型外挂权重走 Volume / S3,不 COPY 进镜像
入口分离ENTRYPOINT 固定入口 + CMD 默认参数
实操 ③ · 15 分钟 · 综合演练

从零搭一个规范容器

1新建 .dockerignoredata/ models/ .git .env __pycache__
2新建 .env.example(只有键名);确认真实 .env 不会进镜像
3新建 compose.yaml:etl 服务 + volumes: ./data:/app/data + env_file
4docker compose up -d --build 跑通;docker compose logs 看输出
5验证:docker run --rm day05-etl:v0.1 cat .env 应该报"文件不存在"
15:00
验收清单
compose up 一条命令跑通
镜像里没有 .env 与 data/
镜像 tag 规范:name:version
Dockerfile 已提交 Git(规范 message)

卡住就举手 —— 实操环节助教会巡场

Part 06 · AI 项目容器规范 · 互动讨论

三大血泪坑

1
把 3GB 模型 COPY 进镜像
换模型就要重建镜像、全量分发 —— 模型走 Volume / 对象存储,镜像只带代码。
2
镜像不打 tag,仓库里全是 latest 和 <none>
线上出问题想回滚,却不知道上一版是哪个 —— tag 规范是纪律。
3
把容器当虚拟机用:ssh 进去手动改配置
环境又漂移了 —— 一切改动回到 Dockerfile 与 compose.yaml,重建而不是手补。
课堂讨论 · 2 分钟
容器运行时往 /tmp 写了 500MB 中间文件,删掉容器重建,这些文件还在吗?镜像会变大吗?
答案:不在,也不会
容器内改动进可写层,随容器删除而消失;镜像分层只读,永不变大。这正是设计意图 —— 要留下的放 Volume,能重建的放 Dockerfile
总结

今天你带走了什么

原理
镜像 = 分层只读文件系统,层间共享
原理
容器 = 镜像运行实例 + 独立可写层
原理
一次构建,处处运行 —— 环境跟着代码走
原理
容器 ≠ 虚拟机:共享内核,秒级启停
规范
基础镜像锁版本,拒绝 latest
规范
先装依赖后拷源码,吃满构建缓存
规范
无状态容器,数据挂 Volume
规范
密钥走环境变量,源码只留模板
协作
镜像 tag 对应 git tag,可回滚
协作
Registry 分发:push 一次,处处 pull
协作
Compose 描述多服务,up 一键起
心法
迷路先 docker ps -a + docker logs
"容器不是把环境装进盒子,而是把环境写成代码。"
课后作业 · 明日预告

今天到此,环境起步

作业 1 · 容器化迁移
给 day04-lab 补齐 Dockerfile + .dockerignore,镜像体积 < 300MB,push 到课程仓库。
作业 2 · 文档
README 增加"docker compose 三步跑通"一节 —— 呼应 Day 4 的"新人一天跑通"。
作业 3 · 阅读
Docker Get Started Part 1-4:docs.docker.com/get-started
明日预告 · Day 06
HTTP、REST API 与 FastAPI
今天环境能跑了;明天让全世界通过 HTTP 调用你的服务 —— 状态码、资源化设计、类型安全的 AI 微服务。

谢谢 · Q&A —— 实操问题随时在群里 @ 包学斌

Day 05 · Docker 与 AI 运行环境 · 包学斌
1 / 32