Docker+FastAPI+千问API,复刻豆包式流式聊天界面
💡 核心目标:学会Docker容器化部署、FastAPI SSE流式输出,对接千问大模型,拥有一个和豆包长得几乎一样的聊天界面,实现"打字式"实时响应。
📌 前置准备:一台装了Windows/Mac/Linux的电脑、阿里云账号(用来申请千问API Key)、耐心(跟着敲,别跳步)
目录
-
一、Docker入门:从"是什么"到"装完能用"(零基础友好)
-
1.1 Docker到底是什么?大白话讲透
-
1.2 Docker安装(Windows/Mac/Linux全场景)
-
1.3 Docker常用命令(必背,90%场景够用)
-
二、Docker Compose:多容器一键启动神器
-
2.1 Docker Compose是什么?解决什么痛点?
-
2.2 Docker Compose安装(全系统)
-
2.3 Compose核心概念(新手必懂)
-
三、千问API准备:获取真实接口Key
-
四、项目实战:搭建后端+前端(核心环节)
-
4.1 项目目录结构(严格对照,别乱改)
-
4.2 后端开发:FastAPI+千问API+SSE流式输出
-
4.3 前端开发:复刻豆包式聊天界面(HTML+CSS+JS)
-
五、Docker配置:Dockerfile+docker-compose.yml逐行解析
-
5.1 Dockerfile:构建镜像的"菜谱"(逐行翻译)
-
5.2 docker-compose.yml:多容器编排配置(新手能懂)
-
六、一键启动项目:见证奇迹的时刻
-
6.1 环境变量配置(关键,别漏)
-
6.2 启动命令(复制粘贴即可)
-
6.3 运行效果演示
-
七、常见问题排查(避坑指南)
-
八、拓展方向(进阶可选)
一、Docker入门:从"是什么"到"装完能用"
1.1 Docker到底是什么?大白话讲透
很多新手刚接触Docker,被"镜像、容器"搞得头大,其实一句话就能懂:Docker就是一个"环境打包工具",解决"在我机子上能跑,服务器上就挂了"的千古难题。
举个生活化例子:你在自己家厨房(本地电脑)做了一道菜(写了代码),用的是自家的锅碗瓢盆(Python版本、依赖库),味道很好;但把菜谱(代码)拿到饭店厨房(服务器),因为锅碗瓢盆不一样(环境不同),做出来的菜就没法吃。
Docker的作用就是:把你家的锅碗瓢盆+菜谱+食材(代码+环境+依赖),全部打包成一个"密封的盒子"(镜像),这个盒子拿到任何一个装了Docker的厨房(电脑/服务器),打开就能做出一模一样的菜(运行代码),环境完全一致!
核心3个概念(记死,后面全用到):
-
镜像(Image):只读的"盒子模板",相当于"盖房子的设计图纸""蛋糕的模具",里面包含了运行代码所需的所有环境(Python、依赖库、配置),是死的文件。
-
容器(Container):镜像的运行实例,相当于"用图纸盖好的房子""用模具烤出来的蛋糕",是活的进程。一个镜像可以启动多个容器,彼此独立,互不干扰(比如一个模具烤10个蛋糕,每个蛋糕都是独立的)。
-
Dockerfile:构建镜像的"菜谱",是一个文本文件,里面写满了一条条指令,告诉Docker"先装什么、再复制什么、最后怎么启动",Docker照着这个菜谱,就能做出镜像。
🧠 醍醐灌顶总结:镜像=模板,容器=模板运行后的实例,Dockerfile=模板的制作步骤。
1.2 Docker安装(Windows/Mac/Linux全场景)
不搞复杂原理,直接上"复制粘贴"式安装步骤,全程下一步,新手也能搞定。
场景1:Windows 10/11 用户(最常用)
-
下载Docker Desktop:Docker Desktop官网(直接点击"Download for Windows")
-
安装:双击安装包,一路"Next",注意:安装过程中会提示"开启WSL 2",直接点击"确定",系统会自动安装WSL 2(如果没提示,后续打开Docker会报错,到时再手动开启即可)。
-
验证:安装完成后,打开Docker Desktop,等待鲸鱼图标变绿(大概1-2分钟),然后打开"命令提示符(CMD)"或"PowerShell",输入命令
docker --version,出现版本号(比如 Docker version 26.0.0, build 2ae903e),就是安装成功了。
⚠️ 注意:Windows家庭版可能需要手动开启WSL 2,具体步骤:控制面板→程序→启用或关闭Windows功能→勾选"适用于Linux的Windows子系统"和"虚拟机平台"→重启电脑,再重新安装Docker。
场景2:Mac 用户
-
下载Docker Desktop:Docker Desktop官网(点击"Download for Mac",根据自己的芯片选择Intel或Apple Silicon版本)。
-
安装:将下载的.dmg文件拖到"应用程序"文件夹,打开Docker,首次打开会提示"允许权限",点击"允许"即可。
-
验证:打开"终端",输入
docker --version,出现版本号即安装成功。
场景3:Linux 用户(服务器常用,以Ubuntu/Debian为例)
直接复制下面的命令,逐行粘贴到终端,回车执行即可(不要一次性复制所有,逐行来):
bash
# 1. 更新系统包索引(确保能下载到最新的Docker包)
sudo apt-get update
# 2. 安装依赖包,允许apt通过HTTPS访问仓库
sudo apt-get install
ca-certificates
curl
gnupg
lsb-release
# 3. 添加Docker官方GPG密钥(验证下载的包是官方的,防止篡改)
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
# 4. 设置Docker稳定版仓库(告诉系统从哪里下载Docker)
echo
"deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu
$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# 5. 再次更新包索引,安装Docker Engine(核心组件)
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io
# 6. 验证安装(查看Docker版本)
docker --version
✅ 安装成功标志:终端输出Docker版本号,无报错。
1.3 Docker常用命令(必背,90%场景够用)
新手不用记太多,先把这6个命令练熟,后续实战全程用到,每句命令都带解释,一看就懂。
| 命令 | 作用 | 示例 |
|---|---|---|
| docker --version | 查看Docker版本(验证是否安装成功) | docker --version |
| docker images | 查看本地所有镜像(相当于查看"所有模具") | docker images |
| docker ps | 查看正在运行的容器(相当于查看"正在烤的蛋糕") | docker ps |
| docker ps -a | 查看所有容器(包括停止的,相当于查看"所有烤过的蛋糕") | docker ps -a |
| docker stop 容器ID/容器名 | 停止正在运行的容器(相当于"停止烤蛋糕") | docker stop 123456(123456是容器ID的前6位) |
| docker rm 容器ID/容器名 | 删除容器(相当于"扔掉烤坏的蛋糕") | docker rm 123456 |
| docker exec -it 容器ID /bin/bash | 进入容器内部(相当于"打开蛋糕盒子,查看里面的东西") | docker exec -it 123456 /bin/bash |
🧠 醍醐灌顶总结:记住"镜像管模板,容器管运行",常用命令就围绕"查看、启动、停止、删除"这几个操作,多敲2遍就记住了。
二、Docker Compose:多容器一键启动神器
2.1 Docker Compose是什么?解决什么痛点?
咱们实战的项目,目前只需要一个"Web服务容器"(FastAPI应用),但实际开发中,你可能需要多个容器:Web容器+数据库容器(MySQL)+向量库容器(Chroma)。
如果没有Docker Compose,你需要:
-
手动启动数据库容器,配置端口、密码;
-
手动启动向量库容器,配置网络;
-
手动启动Web容器,配置和其他两个容器的连接;
-
一旦重启电脑,所有容器都要重新手动启动,麻烦到崩溃!
Docker Compose的作用就是:用一个配置文件(docker-compose.yml),定义所有需要的容器,然后敲一句命令,所有容器一键启动、自动配置网络、互相连通,重启后也能一键重新启动,彻底解放双手!
类比:你要做一顿饭,需要蒸米饭、炒青菜、炖排骨,每个菜用一个锅(容器)。没有Compose,你需要一个个开火、调火候;有了Compose,你只需要按一个按钮,所有锅同时开火、自动调好转火,做完自动关火。
2.2 Docker Compose安装(全系统)
重点:Windows和Mac用户,安装Docker Desktop时,已经自带Docker Compose,不需要单独安装!只有Linux用户需要手动安装。
Linux 用户安装步骤(Ubuntu/Debian为例)
bash
# 1. 下载Docker Compose(版本号可以去官网找最新的,这里用v2.20.2,稳定版)
sudo curl -L "https://github.com/docker/compose/releases/download/v2.20.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
# 2. 给下载的文件添加执行权限(让系统能运行它)
sudo chmod +x /usr/local/bin/docker-compose
# 3. 验证安装(查看版本号)
docker-compose --version
✅ 安装成功标志:终端输出 docker-compose version v2.20.2, build xxxxx
2.3 Compose核心概念(新手必懂)
Docker Compose的核心就是一个配置文件:docker-compose.yml,里面主要包含3个核心部分,后续实战会逐行解析,这里先提前了解:
-
version:Compose文件的格式版本(固定写3.8,最稳定、最通用);
-
services:定义所有需要启动的容器(每个容器就是一个service),比如web容器、数据库容器;
-
每个service下的配置:比如镜像怎么来、端口怎么映射、环境变量怎么传、容器怎么重启。
🧠 醍醐灌顶总结:Docker Compose就是"容器的管家",一个配置文件管所有容器,一键启动、一键停止,不用手动操作每个容器。
三、千问API准备:获取真实接口Key
咱们要实现"真实的AI聊天",不能用模拟接口,必须接入千问大模型的真实API,步骤很简单,全程免费(阿里云百炼有免费额度)。
-
访问阿里云百炼平台:阿里云百炼 - DashScope(复制链接到浏览器打开);
-
注册/登录阿里云账号(如果没有,注册一个,实名认证后即可使用);
-
登录后,点击左侧菜单栏的「API-KEY 管理」;
-
点击「创建API-KEY」,输入名称(随便起,比如"我的千问Key"),点击确定;
-
创建成功后,点击「复制」,保存好这个Key(格式是 sk-xxxxxx),不要泄露给别人(泄露后别人会用你的额度)。
⚠️ 注意:千问API有免费额度(qwen-turbo模型,足够咱们实战使用),如果免费额度用完,需要充值,新手先不用管,免费额度完全够用。
四、项目实战:搭建后端+前端(核心环节)
这部分是重点,咱们严格按照"目录结构→后端→前端"的顺序来,代码逐行注释,复制粘贴就能用,不用修改任何内容(除了千问API Key)。
4.1 项目目录结构(严格对照,别乱改)
先在你的电脑上创建一个文件夹(名字随便起,比如 my_qwen_chatbot),然后按照下面的结构创建子文件夹和文件,路径和文件名不能错,否则后续启动会报错。
text
my_qwen_chatbot/ # 项目根目录
├── app/ # 存放后端代码和前端页面
│ ├── main.py # FastAPI后端核心代码(接入千问API+SSE)
│ └── index.html # 高仿豆包的前端聊天页面
├── Dockerfile # 构建后端镜像的"菜谱"
├── docker-compose.yml # Docker Compose配置文件(一键启动)
└── requirements.txt # Python依赖包列表(告诉Docker要装哪些库)
✅ 操作建议:先创建根文件夹 my_qwen_chatbot,然后在里面创建 app 子文件夹,再依次创建其他文件,避免路径错误。
4.2 后端开发:FastAPI+千问API+SSE流式输出
后端核心功能:接收前端的问题,调用千问API,通过SSE流式输出(像ChatGPT、豆包一样,逐字返回回答),最后把接口暴露给前端。
第一步:先写 requirements.txt(告诉Docker要安装哪些Python依赖),在项目根目录创建 requirements.txt 文件,复制下面的内容:
text
fastapi==0.136.0 # 核心Web框架,轻量、快,支持异步
uvicorn==0.44.0 # ASGI服务器,用来运行FastAPI应用
sse-starlette==3.3.4 # 实现SSE流式输出的核心库
python-dotenv==1.2.2 # 读取环境变量(方便管理API Key)
dashscope==1.25.17 # 千问大模型官方SDK(必须装,否则无法调用千问API)
第二步:写后端核心代码 main.py,在 app 文件夹下创建 main.py 文件,复制下面的代码,不需要修改任何内容(千问API Key后续通过环境变量传入,不用写死在代码里):
python
import asyncio
from fastapi import FastAPI, Request
from fastapi.responses import FileResponse
from sse_starlette.sse import EventSourceResponse
import dashscope
# ================= 初始化配置 =================
app = FastAPI(title="千问流式聊天服务")
# 千问API配置(直接写死确保生效)
DASHSCOPE_API_KEY = "DASHSCOPE_API_KEY"
dashscope.api_key = DASHSCOPE_API_KEY
# ================= 核心:无缓冲千问流式调用 =================
async def call_qwen_stream(user_message: str):
"""
无缓冲流式调用,Python 3.13 原生asyncio.to_thread彻底解决同步转异步的缓冲问题
"""
# 同步生成器:直接从千问拿增量chunk,无任何中间攒包
def sync_qwen_stream():
responses = dashscope.Generation.call(
model="qwen-plus",
messages=[
{"role": "system", "content": "你是一个乐于助人的AI助手,名字叫千问。"},
{"role": "user", "content": user_message}
],
stream=True,
result_format='message',
# ✅ 最关键参数:禁用SDK内部缓冲,强制逐增量返回
incremental_output=True
)
# 逐一遍历增量chunk,来一个yield一个,终端可实时打印验证
for response in responses:
if response.status_code == 200:
chunk = response.output.choices[0].message.content
print(chunk, end="", flush=True) # 终端逐字打印,先确认后端是真流式
yield chunk
else:
error_msg = f"\n[调用出错] 状态码:{response.status_code},错误:{response.message}"
print(error_msg)
yield error_msg
break
# Python 3.9+ 原生同步转异步,无自定义队列的隐性bug
for chunk in await asyncio.to_thread(sync_qwen_stream):
yield chunk
# ================= API接口 =================
# 根路径:返回前端页面
@app.get("/")
async def get_index():
return FileResponse("index.html")
# 流式聊天接口:标准SSE协议,强制无缓存
@app.get("/chat/stream")
async def chat_stream(request: Request, query: str):
if not DASHSCOPE_API_KEY:
return EventSourceResponse([{"data": "请先配置千问API Key!"}])
# SSE事件生成器:来一个chunk发一个,无任何缓冲
async def event_generator():
async for token in call_qwen_stream(query):
if await request.is_disconnected():
print("客户端断开,终止流式输出")
break
# 标准SSE格式,强制换行,浏览器立即解析
yield f"data: {token}\n\n"
# ✅ 关键:强制禁用浏览器/代理缓存,确保逐字刷新
headers = {
"Cache-Control": "no-cache, no-store, must-revalidate, max-age=0",
"Connection": "keep-alive",
"X-Accel-Buffering": "no"
}
return EventSourceResponse(
event_generator(),
headers=headers,
ping=300 # 15秒心跳保活,防止连接断开
)
# 测试接口:模拟流式输出,排除千问SDK问题(可选)
@app.get("/test/stream")
async def test_stream(request: Request):
async def test_event_generator():
test_text = "这是测试流式输出,你应该能看到我逐字蹦出来!如果能看到,说明前后端传输完全正常。"
for char in test_text:
if await request.is_disconnected():
break
yield f"{char}\n\n"
await asyncio.sleep(0.1)
return EventSourceResponse(test_event_generator(), headers={"Cache-Control": "no-cache"})
🧠 醍醐灌顶总结:后端核心就是"接收问题→调用千问API→流式返回回答",SSE的作用是让服务器主动往前推数据,不用前端反复请求,实现"打字效果"。
4.3 前端开发:复刻豆包式聊天界面(HTML+CSS+JS)
前端核心需求:和豆包的聊天界面长得几乎一样,包含左侧侧边栏、右侧聊天区、用户输入框、消息气泡、打字光标动画,能对接后端SSE接口,实现实时聊天。
在 app 文件夹下创建 index.html 文件,复制下面的代码,不需要修改任何内容,直接用(样式已经调好,适配电脑端,和豆包界面高度相似):
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>千问 - 你的AI助手</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background-color: #f4f5f7;
height: 100vh;
display: flex;
overflow: hidden;
}
.sidebar {
width: 260px;
background-color: #ffffff;
border-right: 1px solid #e5e6eb;
display: flex;
flex-direction: column;
padding: 16px;
flex-shrink: 0;
}
.new-chat-btn {
width: 100%;
padding: 12px;
background-color: #1677ff;
color: white;
border: none;
border-radius: 8px;
font-size: 15px;
cursor: pointer;
margin-bottom: 20px;
}
.chat-area {
flex: 1;
display: flex;
flex-direction: column;
background-color: #f7f8fa;
}
.chat-header {
height: 60px;
background-color: #fff;
border-bottom: 1px solid #e5e6eb;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
font-weight: 600;
}
.messages-container {
flex: 1;
overflow-y: auto;
padding: 20px;
display: flex;
flex-direction: column;
}
.message-wrapper {
display: flex;
margin-bottom: 24px;
width: 100%;
}
.user-wrapper {
justify-content: flex-end;
}
.ai-wrapper {
justify-content: flex-start;
}
.avatar {
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
flex-shrink: 0;
margin: 0 12px;
}
.ai-avatar {
background-color: #1677ff;
color: white;
}
.user-avatar {
background-color: #f2f3f5;
color: #4e5969;
}
.message-bubble {
max-width: 70%;
padding: 12px 16px;
border-radius: 12px;
font-size: 15px;
line-height: 1.6;
word-wrap: break-word;
white-space: pre-wrap;
}
.ai-bubble {
background-color: #ffffff;
border-top-left-radius: 4px;
}
.user-bubble {
background-color: #1677ff;
color: white;
border-top-right-radius: 4px;
}
.typing-cursor {
display: inline-block;
width: 8px;
height: 18px;
background-color: #1d2129;
margin-left: 2px;
animation: blink 1s infinite;
vertical-align: middle;
}
@keyframes blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0; }
}
.input-area {
padding: 20px;
background-color: #fff;
border-top: 1px solid #e5e6eb;
flex-shrink: 0;
}
.input-wrapper {
max-width: 900px;
margin: 0 auto;
border: 1px solid #e5e6eb;
border-radius: 12px;
padding: 12px 16px;
display: flex;
align-items: flex-end;
background-color: #f7f8fa;
}
textarea {
flex: 1;
border: none;
background: transparent;
outline: none;
resize: none;
font-size: 15px;
max-height: 150px;
line-height: 1.5;
}
.send-btn {
background-color: #1677ff;
color: white;
border: none;
border-radius: 8px;
padding: 8px 16px;
cursor: pointer;
font-size: 14px;
margin-left: 12px;
}
.send-btn:disabled {
background-color: #c9cdd4;
cursor: not-allowed;
}
</style>
</head>
<body>
<div class="sidebar">
<button class="new-chat-btn" onclick="clearChat()">+ 新建对话</button>
<div class="history-list">
<div style="padding: 10px; text-align: center;">对话历史</div>
</div>
</div>
<div class="chat-area">
<div class="chat-header">千问</div>
<div class="messages-container" id="messagesContainer">
<div class="message-wrapper ai-wrapper">
<div class="avatar ai-avatar">包</div>
<div class="message-bubble ai-bubble">你好!我是千问,有什么可以帮你的吗?</div>
</div>
</div>
<div class="input-area">
<div class="input-wrapper">
<textarea id="userInput" placeholder="输入你的问题..." rows="1"></textarea>
<button class="send-btn" id="sendBtn">发送</button>
</div>
</div>
</div>
<script>
// ✅ 修复核心:等待页面DOM加载完成再执行JS
document.addEventListener('DOMContentLoaded', function() {
let isGenerating = false;
const textarea = document.getElementById('userInput');
const sendBtn = document.getElementById('sendBtn');
const messagesContainer = document.getElementById('messagesContainer');
// 自动调整输入框高度
textarea.addEventListener('input', function() {
this.style.height = 'auto';
this.style.height = Math.min(this.scrollHeight, 150) + 'px';
});
// 回车发送
textarea.addEventListener('keydown', function(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
// 点击发送
sendBtn.addEventListener('click', sendMessage);
// 清空对话
window.clearChat = function() {
isGenerating = false;
sendBtn.disabled = false;
sendBtn.innerText = '发送';
messagesContainer.innerHTML = `
<div class="message-wrapper ai-wrapper">
<div class="avatar ai-avatar">包</div>
<div class="message-bubble ai-bubble">你好!我是千问,有什么可以帮你的吗?</div>
</div>
`;
};
// 核心发送函数
async function sendMessage() {
if (isGenerating) return;
const query = textarea.value.trim();
if (!query) return;
// 添加用户消息
appendMessage('user', query);
textarea.value = '';
textarea.style.height = 'auto';
// 锁定状态
isGenerating = true;
sendBtn.disabled = true;
sendBtn.innerText = '生成中...';
// 创建AI消息框
const aiBubble = appendMessage('ai', '');
const contentDiv = aiBubble.querySelector('.message-content');
const cursor = document.createElement('span');
cursor.className = 'typing-cursor';
contentDiv.appendChild(cursor);
try {
const res = await fetch(`/chat/stream?query=${encodeURIComponent(query)}`);
const reader = res.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
const lines = chunk.split('\n');
for (let line of lines) {
// 全局清空所有 data: 前缀,不管在什么位置
let cleanLine = line.replace(/data:\s*/g, '').trim();
if (!cleanLine) continue;
cursor.remove();
contentDiv.innerHTML += cleanLine;
contentDiv.appendChild(cursor);
scrollToBottom();
}
}
} catch (err) {
console.error(err);
} finally {
// 解锁
cursor.remove();
isGenerating = false;
sendBtn.disabled = false;
sendBtn.innerText = '发送';
}
}
// 添加消息
function appendMessage(role, text) {
const wrapper = document.createElement('div');
wrapper.className = `message-wrapper ${role}-wrapper`;
const avatar = document.createElement('div');
avatar.className = `avatar ${role}-avatar`;
avatar.innerText = role === 'user' ? '我' : '包';
const bubble = document.createElement('div');
bubble.className = `message-bubble ${role}-bubble`;
const content = document.createElement('span');
content.className = 'message-content';
content.innerHTML = text;
bubble.appendChild(content);
if (role === 'ai') {
wrapper.appendChild(avatar);
wrapper.appendChild(bubble);
} else {
wrapper.appendChild(bubble);
wrapper.appendChild(avatar);
}
messagesContainer.appendChild(wrapper);
scrollToBottom();
return wrapper;
}
// 滚动到底部
function scrollToBottom() {
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
});
</script>
</body>
</html>
🧠 醍醐灌顶总结:前端核心就是"渲染界面+调用SSE接口+逐字更新DOM",打字光标动画、消息对齐、自动滚动,都是为了还原豆包的交互体验,代码已经全部写好,直接用即可。
五、Docker配置:Dockerfile+docker-compose.yml逐行解析
这部分是"容器化部署"的核心,也是新手最容易出错的地方,我会逐行解析每个配置的含义,让你不仅知道"写什么",还知道"为什么这么写"。
5.1 Dockerfile:构建镜像的"菜谱"
Dockerfile 是用来告诉Docker"怎么构建镜像"的文本文件,每一行都是一条指令,Docker会按照指令顺序,一步步构建出包含我们应用的镜像。
在项目根目录创建 Dockerfile 文件(没有后缀名),复制下面的代码,每一行都有详细注释:
dockerfile
# ================= 第一阶段:构建基础镜像 =================
# 1. FROM:指定基础镜像(相当于"买一个现成的空盒子")
# python:3.11-slim:轻量版Python 3.11镜像,体积小、启动快,足够咱们实战使用
FROM python:3.11-slim
# 2. LABEL:给镜像添加元数据(可选,用来标识镜像作者,显得专业)
LABEL maintainer="CSDN-博主"
# 3. WORKDIR:设置容器内的工作目录(相当于"在空盒子里创建一个文件夹,后续所有操作都在这个文件夹里")
# /app:容器内的目录,后续我们的代码都会放在这里
WORKDIR /app
# 4. COPY:复制宿主机的文件到容器内(宿主机就是你的电脑)
# 格式:COPY 宿主机路径 容器内路径
# 先复制requirements.txt:因为requirements.txt很少改,这样可以利用Docker的缓存层
# 只要requirements.txt不变,这一步就不会重新执行,安装依赖的速度会快很多
COPY requirements.txt .
# 这里的"."表示容器内的当前目录(也就是上面设置的/app目录)
# 5. RUN:在容器内执行命令(相当于"在空盒子里安装东西")
# 第一步:升级pip(避免pip版本过低,导致安装依赖失败)
# 第二步:安装requirements.txt里的所有依赖,用清华源加速(国内用户必加,否则速度很慢)
# --no-cache-dir:不缓存安装包,减少镜像体积
RUN pip install --no-cache-dir --upgrade pip -i https://pypi.tuna.tsinghua.edu.cn/simple
&& pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
# 6. COPY:复制宿主机的app文件夹(后端代码+前端页面)到容器内的/app/app目录
# 因为前面已经设置了WORKDIR /app,所以这里的路径是/app/app
COPY app /app/app
# 7. CMD:容器启动时默认执行的命令(相当于"打开盒子后,自动运行里面的程序")
# uvicorn app.main:app:启动FastAPI应用,app.main表示app文件夹下的main.py,app是FastAPI实例
# --host 0.0.0.0:允许容器外部访问(如果不设,只能在容器内部访问)
# --port 8000:容器内的端口,后续会映射到宿主机的8000端口
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
🧠 醍醐灌顶总结:Dockerfile的逻辑就是"先找一个基础盒子(基础镜像)→ 在盒子里创建文件夹(WORKDIR)→ 复制依赖清单(COPY requirements.txt)→ 安装依赖(RUN)→ 复制代码(COPY app)→ 设定启动命令(CMD)",一步步把我们的应用打包成镜像。
5.2 docker-compose.yml:多容器编排配置
咱们目前只需要一个Web容器(FastAPI应用),但配置文件按照多容器的标准来写,后续添加数据库、向量库时,直接在services里加就行,不用改整体结构。
在项目根目录创建 docker-compose.yml 文件,复制下面的代码,逐行解析:
yaml
# 1. version:指定Compose文件的格式版本(固定写3.8,最稳定、最通用,兼容所有Docker版本)
version: '3.8'
# 2. services:定义所有需要启动的容器(每个容器就是一个service)
# 这里我们只定义一个service:web(FastAPI应用容器)
services:
# 2.1 定义web服务(名字可以随便起,比如web、app,后续用这个名字操作容器)
web:
# build:告诉Compose,这个容器的镜像怎么来(这里是通过Dockerfile构建)
build:
context: . # context: . 表示使用当前目录(项目根目录)的Dockerfile来构建镜像
dockerfile: Dockerfile # 指定Dockerfile的文件名(默认就是Dockerfile,可省略,写出来更清晰)
# ports:端口映射(核心配置,让宿主机能访问容器内的服务)
# 格式:宿主机端口:容器内端口
# 这里把宿主机的8000端口,映射到容器内的8000端口(和Dockerfile里CMD指定的端口一致)
# 后续访问 http://localhost:8000 ,就相当于访问容器内的8000端口
ports:
- "8000:8000"
# environment:设置环境变量(关键!用来传入千问API Key,不用写死在代码里)
# DASHSCOPE_API_KEY:变量名,和后端main.py里os.getenv("DASHSCOPE_API_KEY")对应
# ${DASHSCOPE_API_KEY}:表示从宿主机的环境变量中读取这个值(后续会教怎么设置)
environment:
- DASHSCOPE_API_KEY=${DASHSCOPE_API_KEY}
# restart:容器重启策略(新手必设,避免意外退出后需要手动重启)
# always:无论容器因什么原因停止(除了手动停止),都会自动重启
restart: always
# volumes:数据卷映射(可选,这里用来实现"代码热更新",方便开发调试)
# 把宿主机的app文件夹(后端+前端代码),映射到容器内的/app/app文件夹
# 这样修改宿主机的代码后,容器内的代码会实时更新,不用重新构建镜像
volumes:
- ./app:/app/app
# networks:网络配置(可选,单容器可省略,多容器时用来让容器互相通信)
# 这里创建一个自定义网络,后续添加数据库等容器时,可加入同一个网络
networks:
- qwen-chatbot-network
# 3. networks:定义自定义网络(和上面services里的networks对应)
# driver: bridge:默认的桥接网络,适合本地开发场景
networks:
qwen-chatbot-network:
driver: bridge