Day 57 / 共 68 天 · 阶段 11 部署
Docker:把 Agent 装进集装箱
阶段 10 你让 Agent 可信、可观测、省钱了,可它现在还只活在你的笔记本上。今天进入部署篇第一课——Docker:把你的 Agent 服务连同 Python、依赖包、配置,统统打包成一个标准「集装箱」,做到「在我电脑上能跑,搬到任何服务器上也一模一样能跑」,彻底告别「在我这儿是好的呀」的甩锅现场。学完你能把 Day 09 写的 FastAPI 服务打成镜像一键跑起来;明天(Day 58)学怎么用 K8s 同时调度一大堆这样的集装箱。
📍 你在阶段 11(部署 D57-61)的位置
D57 Docker→
D58 K8s 基础→
D59 CI/CD&IaC→
D60 云上部署→
D61 AI 网关
💡 用一个类比兜住今天(今天全程沿用「海运集装箱」的世界观)
部署一个程序 = 把货物海运到世界各地。Docker 镜像(image) = 一个装好货、封好口的标准集装箱(里面塞好了程序 + Python + 所有依赖);容器(container) = 用这个集装箱在某个港口实际卸货运行的一份实例;Dockerfile = 装箱说明书(告诉工人先放什么、再放什么);镜像仓库(registry) = 集装箱码头(存放和分发箱子)。集装箱的伟大之处:不管箱里装的是家具还是电器,轮船、吊车、卡车都用同一套标准搬运——Docker 让所有程序都能被同一套方式部署。
L01
为什么要 Docker:告别「在我这儿是好的」
🤔 痛点你本地跑得好好的 Agent,发给同事或部到服务器上,立马报错:Python 版本不对、少装了某个包、系统缺个字体库……来回折腾一整天。这就是经典的「环境不一致」地狱。
💡 本质Docker 把「程序 + 它需要的一切环境」一起打包,搬到哪里都自带环境,不再依赖目标机器装了啥。就像海运集装箱:发货前把货和填充物一起封进标准箱,不管到哪个港口,吊车都能原样搬下来,里面的东西一点没变。一次打包,处处运行(build once, run anywhere)。
图注:Docker 解决的不是「代码对不对」,而是「环境一不一致」——这是部署最大的一类坑。
👶 Docker 和虚拟机(VM)有啥不一样?虚拟机是「连整台电脑(含操作系统)一起虚拟」,又大又慢,像给每件货配一整艘船;Docker 容器共享宿主机的操作系统内核,只打包程序和依赖,轻量得多,秒级启动,像共用一艘船、各占一个标准箱位。所以 Docker 能在一台机器上同时跑几十个容器。
L02
镜像 vs 容器:模具和产品
🤔 痛点刚学 Docker 最容易懵的两个词:image(镜像)和 container(容器),到处都在说,分不清谁是谁。
💡 本质镜像 = 封好的集装箱(静态的、只读的模板);容器 = 用这个箱子在港口实际卸货运行的一份实例(动态的、在跑的)。一个镜像可以启动出好多个容器,就像同一张图纸(还记得 Day05 的 class/object 吗?)造出好多台机器。镜像是「死的模具」,容器是「活的产品」。
图注:想加机器抗流量?用同一个镜像多起几个容器就行——明天 K8s 干的就是这件事。
📝 举个例子:命令里体会两者的关系
docker build → 得到一个镜像(封箱);docker run 镜像名 → 启动一个容器(卸货运行);docker ps → 看现在有哪些容器在跑(哪些箱子正在被使用)。build 是名词工厂,run 是动词现场。L03
Dockerfile:一份装箱说明书
🤔 痛点镜像里到底该装什么、按什么顺序装?总不能手动一件件塞。需要一份可复现的「说明书」。
💡 本质Dockerfile 就是「装箱说明书」:一行一条指令,从上到下告诉 Docker「先拿个什么底座、再拷贝代码、再装依赖、最后启动命令怎么写」。照着它,任何人任何时候都能构建出一模一样的镜像。像宜家家具的图解说明:第一步、第二步……照做就还原。
# Dockerfile —— 每一行是一条「装箱指令」,从上往下执行
FROM python:3.11-slim # 1) 底座:一个已装好 Python 3.11 的精简系统
WORKDIR /app # 2) 在箱子里建个 /app 文件夹当工作台
COPY requirements.txt . # 3) 先只拷贝依赖清单(为了利用缓存,见下讲)
RUN pip install -r requirements.txt # 4) 照清单装依赖(Day05 的 -r 还记得吗)
COPY . . # 5) 再把项目所有代码拷进箱子
EXPOSE 8000 # 6) 声明:这个服务对外用 8000 端口
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
# 7) 箱子启动时默认跑的命令(启动 FastAPI 服务)
| 指令 | 大白话 |
|---|---|
FROM | 用哪个现成底座开始(别从零造系统) |
WORKDIR | 进到箱内哪个文件夹干活 |
COPY | 把宿主机的文件拷进箱子 |
RUN | 构建镜像时执行的命令(如装依赖) |
EXPOSE | 声明对外端口(文档作用为主) |
CMD | 容器启动时默认跑的命令 |
👶 小白:为什么不直接 COPY . . 一步拷完,还要先单独拷 requirements.txt?
👨🏫 老师:为了「省构建时间」!Docker 是一层一层缓存的:只要 requirements.txt 没变,「装依赖」那层就直接复用缓存,不用重装(装依赖往往最慢)。如果你把代码和依赖清单一起拷,那改一行代码就会让「装依赖」那层缓存失效,每次都得重装几分钟。先拷不常变的(依赖清单),后拷常变的(代码)——这是 Dockerfile 的黄金顺序,下一讲还会碰到。
L04
build & run:封箱与卸货
🤔 痛点写好 Dockerfile 了,怎么真的把它变成一个能跑的服务?
💡 本质两个核心动作:
docker build = 照说明书封箱(生成镜像);docker run = 把箱子拉到港口卸货运行(启动容器)。再加个端口映射,就能从你电脑的浏览器访问箱子里的服务。# 1) 封箱:照当前目录的 Dockerfile 构建镜像,起名 my-agent,版本 v1
docker build -t my-agent:v1 .
# └ -t 给镜像起名字:标签 └ . 表示「用当前目录的 Dockerfile」
# 2) 卸货运行:用镜像启动一个容器
docker run -p 8000:8000 my-agent:v1
# └ -p 宿主机端口:容器端口 把箱内 8000 接到你电脑的 8000
# 现在浏览器打开 http://localhost:8000/docs 就能访问箱子里的 FastAPI 了!
# 常用查看命令:
docker images # 看有哪些镜像(有哪些箱子模板)
docker ps # 看正在运行的容器(哪些箱子在跑)
docker logs 容器ID # 看某个容器的日志(对应 Day55 的可观测)
docker stop 容器ID # 停掉一个容器
👶
-p 8000:8000 两个 8000 啥区别?冒号左边是你电脑(宿主机)的端口,右边是箱子内部的端口。它俩不一定相同:-p 9000:8000 意思是「箱子里服务在 8000,我想从电脑的 9000 访问它」。集装箱是封闭的,不做端口映射,外面根本连不进箱里——这是新手最常见的「服务起来了却访问不到」的原因。L05
实战:把一个 Agent 服务打成镜像
🤔 痛点道理都懂,拿自己的 Agent 试试呢?尤其它还要读 API key,这种秘密怎么进箱子又不泄露?
💡 本质把 Day 09 的 FastAPI Agent 服务 + 一个 requirements.txt + 一个 Dockerfile 放一起,就能打包。关键铁律:API key 等秘密绝不写死进镜像(镜像会被分发,等于把钥匙贴门上),而是运行时用环境变量传进去。就像集装箱可以标准运输,但贵重钥匙你随身带、到港口再交。
# main.py —— 一个最小 Agent 服务(复习 Day09 FastAPI)
import os
from fastapi import FastAPI
app = FastAPI()
API_KEY = os.environ.get("LLM_API_KEY") # ✅ 从环境变量读,不写死在代码里
@app.get("/ask")
def ask(q: str):
# 这里正常调你的 Agent;演示就直接回显
return {"question": q, "answer": f"(用 key 尾号 …{str(API_KEY)[-4:]} 生成的回答)"}
@app.get("/health") # 健康检查接口:K8s / 负载均衡靠它判断「箱子还活着吗」
def health():
return {"status": "ok"}
# 构建 + 运行,运行时用 -e 把 key 作为环境变量注入(不进镜像)
docker build -t my-agent:v1 .
docker run -p 8000:8000 -e LLM_API_KEY="sk-你的密钥" my-agent:v1
# └ -e 名=值:运行时才把秘密塞进箱子
# 验证:另开一个终端
curl "http://localhost:8000/health" # → {"status":"ok"}
curl "http://localhost:8000/ask?q=你好" # → 看到回答
📝 举个例子:新增的 /health 为什么重要
线上环境(负载均衡、K8s)会每隔几秒
GET /health 一次,如果连不上就认为这个箱子挂了,自动重启或不再往它发请求。「健康检查接口 + 环境变量传密钥」是容器化服务的两个标配,面试和实战都常考。👶 别忘了 .dockerignore建一个
.dockerignore 文件(用法像 .gitignore),写上 .venv、__pycache__、.env、.git——避免把本地虚拟环境、密钥文件、一堆缓存也拷进镜像,既让镜像变小,又防止 .env 里的密钥意外进箱。L06
镜像瘦身 + docker compose 一瞥
🤔 痛点随手一打,镜像动不动 1~2 GB,推送慢、启动慢、还占空间。而且真实项目往往不止一个箱子(比如 Agent 服务 + Redis 缓存 + 向量库),一个个手动 run 太累。
💡 本质两个进阶点:① 瘦身——用
-slim 或 alpine 小底座、只拷必要文件、用 .dockerignore,让箱子尽量轻;② docker compose——用一个 docker-compose.yml 文件描述「我要几个箱子、怎么连」,一句 docker compose up 全部起来。就像一次运输多个集装箱,用一张总清单统一调度,不用一个个喊吊车。| 瘦身招数 | 效果 |
|---|---|
底座用 python:3.11-slim 而非完整版 | 基础镜像小几百 MB |
| 先拷 requirements、后拷代码(上一讲) | 改代码不用重装依赖,构建快 |
写好 .dockerignore | 不把 .venv/缓存/.git 拷进去 |
| 多阶段构建(multi-stage) | 构建用的工具不进最终镜像,进阶再学 |
# docker-compose.yml(示意):一次描述「Agent 服务 + Redis 缓存」两个箱子
# services:
# agent:
# build: . # 用当前目录 Dockerfile 构建
# ports: ["8000:8000"]
# environment: [LLM_API_KEY] # 从外部传密钥
# cache:
# image: redis:7 # 直接用现成 Redis 镜像(给 Day56 的缓存用)
docker compose up # 一句话把上面所有箱子一起起来,它们还能互相通信
docker compose down # 一句话全部停掉并清理
compose 语法细节别背,用时查文档。今天记住两句话就够:镜像要尽量瘦(slim 底座 + dockerignore + 分层缓存);多个箱子用 compose 一张清单统一起停。这也是 Day56 的缓存(Redis)、向量库落地时的常见搭法。
L07
今日小结 + 动手 10 分钟
🧠 今天你应该能回答
- Docker 到底解决了什么问题?和虚拟机有什么区别?
- 镜像和容器什么关系?(模具 vs 产品,一个镜像可起多个容器)
- Dockerfile 里
FROM/COPY/RUN/CMD/EXPOSE各干嘛?为什么先拷 requirements 再拷代码? docker build和docker run分别做什么?-p和-e是干嘛的?- API key 这种秘密该怎么进容器?为什么不能写死在镜像里?
- 怎么让镜像更小?docker compose 解决什么场景?
✋ 动手 10 分钟:把一个最小服务打成镜像跑起来
建一个文件夹,放三个文件,然后 build + run:
# 1) main.py(3 行的 FastAPI)
# from fastapi import FastAPI
# app = FastAPI()
# @app.get("/health")
# def health(): return {"status": "ok"}
# 2) requirements.txt
# fastapi
# uvicorn
# 3) Dockerfile
# FROM python:3.11-slim
# WORKDIR /app
# COPY requirements.txt .
# RUN pip install -r requirements.txt
# COPY . .
# CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
# 然后在该文件夹里执行:
docker build -t hello-agent:v1 . # 封箱
docker run -p 8000:8000 hello-agent:v1 # 卸货运行
# 浏览器开 http://localhost:8000/health → 看到 {"status":"ok"} 就成功了!
还没装 Docker?去装 Docker Desktop(Mac/Windows 一键装,自带图形界面能看到你的镜像和容器)。装不了也没关系,把上面三个文件和命令读懂、能讲出每行在干嘛,就达标了。
明日预告 · Day 58:你会打一个集装箱了。可线上要抗流量,得同时跑几十个一模一样的箱子,还要它们挂了自动重启、能滚动升级不停机——手动管这么多箱子会累死。明天学 Kubernetes(K8s):集装箱的「码头调度中心」,用 Pod / Deployment / Service 三个核心概念,帮你自动编排一大群容器。零基础只要懂个大概轮廓就行。