前言
经过前三周的洗礼,我们的 AI 聊天应用已经从零到一具备了完整的前后端功能。但作为一名全栈工程师,让应用真正跑在公网上才是检验学习成果的最终标准。本周的任务就是将 Week3 的应用部署到云服务器,并通过自定义域名访问。
这篇文章记录了我在部署过程中踩过的每一个坑,希望能帮你少走弯路。
一、部署架构设计
在动手之前,先想清楚我们要达到的效果:
纯文本
用户浏览器
↓ http://qishuixian.com/chat
宿主机 Nginx (监听 80 端口)
↓
Docker 容器网络
├── frontend (chat-frontend:80) → 静态文件
└── backend (chat-backend:8000) → FastAPI 服务
关键决策:
- 前端和后端各自独立容器化,通过 Docker Compose 编排
- 宿主机安装 Nginx 作为反向代理,统一入口
- 前端容器内部 Nginx 只负责提供静态文件,不处理路由
- 后端容器暴露 8000 端口供宿主机 Nginx 转发
二、前端容器改造
2.1 生产环境基础路径
由于我们的域名下可能有多个项目(如 /chat、/bold),前端需要知道自己运行在子路径下。Vite 提供了环境变量控制 base 路径:
.env.production
ini
VITE_BASE_URL=/chat/
vite.config.ts
ts
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '')
return {
plugins: [vue()],
base: env.VITE_BASE_URL || '/',
}
})
这样构建后,所有资源路径都会变成 /chat/assets/xxx.js。
2.2 多阶段 Dockerfile
前端 Dockerfile 采用多阶段构建,分离构建环境和运行环境:
dockerfile
# 1. 构建阶段
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# 2. 运行阶段
FROM nginx:alpine
# 复制构建产物到子目录 /chat/
COPY --from=builder /app/dist /usr/share/nginx/html/chat/
# 复制自定义 Nginx 配置文件(注意文件名和路径)
# COPY frontend-chat.conf /etc/nginx/conf.d/default.conf
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
注意:dist 被复制到了 /usr/share/nginx/html/chat/ 子目录,而不是根目录。
2.3 容器内 Nginx 配置
为了让 Nginx 正确处理 /chat/ 路径,需要一份专门的配置文件:
frontend-chat.conf
nginx
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html/chat;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
}
三、后端容器
后端相对简单,只需将 FastAPI 应用打包:
dockerfile
# ============ 第一阶段:安装依赖 ============
FROM python:3.11-slim AS builder
WORKDIR /app
# 复制依赖文件并安装(使用国内镜像加速,可选)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# ============ 第二阶段:运行阶段 ============
FROM python:3.11-slim
WORKDIR /app
# 从 builder 阶段复制已安装的依赖
COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
COPY --from=builder /usr/local/bin /usr/local/bin
# 复制项目代码(包含 main.py 及其他所有文件)
COPY . .
# 显式再次复制 main.py,确保它一定存在于容器中(防止 .dockerignore 或缓存干扰)
COPY ./main.py /app/main.py
# 暴露端口
EXPOSE 8000
# 启动命令
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
四、Docker Compose 编排
生产环境使用预构建镜像,不挂载源码:docker-compose.prod.yml
services:
backend:
image: chat-backend:latest
container_name: chat-backend
ports:
- "8000:8000"
volumes:
- ./data:/app/data
- ./uploads:/app/uploads
environment:
- DATABASE_URL=sqlite+aiosqlite:////app/data/app.db
restart: always
networks:
- mynet
frontend:
image: chat-frontend:latest
container_name: chat-frontend
ports:
- "8080:80"
depends_on:
- backend
restart: always
networks:
- mynet
networks:
mynet:
driver: bridge
五、宿主机 Nginx 配置
在服务器上安装 Nginx 后,编写反向代理配置:
/etc/nginx/sites-available/chat
nginx
server {
listen 80;
server_name qishuixian.com www.qishuixian.com;
# ========== 1. 前端页面:指向 chat-frontend (宿主机8080端口) ==========
location /chat/ {
proxy_pass http://localhost:8080/; # 宿主机端口映射
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
try_files $uri $uri/ /chat/index.html; # 处理前端路由History模式
}
# ========== 2. 后端 API:指向 chat-backend (宿主机8000端口) ==========
location /api/ {
proxy_pass http://localhost:8000/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# ========== 3. WebSocket ==========
location /ws {
proxy_pass http://localhost:8000/ws;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
}
# ========== 4. 静态文件 ==========
location /uploads/ {
proxy_pass http://localhost:8000/uploads/;
proxy_set_header Host $host;
}
}
关键点:
- 前端请求转发到
localhost:8080(容器映射端口) - 后端 API 和 WebSocket 转发到
localhost:8000 - 使用
try_files处理 SPA 路由(刷新不 404)
六、生产部署流程
6.1 本地构建镜像
bash
docker build -t chat-frontend:latest -f frontend/Dockerfile frontend/
docker build -t chat-backend:latest -f backend/Dockerfile backend/
6.2 导出并上传
bash
docker save chat-frontend:latest -o frontend.tar
docker save chat-backend:latest -o backend.tar
scp frontend.tar backend.tar root@<SERVER_IP>:/opt/chat/
scp docker-compose.yml nginx.conf root@<SERVER_IP>:/opt/chat/
6.3 服务器加载并启动
bash
ssh root@<SERVER_IP>
cd /opt/chat
docker load -i backend.tar
docker load -i frontend.tar
docker compose up -d
6.4 配置 Nginx 并放行端口
bash
sudo nginx -t
sudo systemctl reload nginx
别忘了在云控制台安全组放行 TCP 80 端口。
七、踩坑实录
坑 1:rolldown 原生模块缺失
错误信息: Cannot find module '@rolldown/binding-linux-x64-musl'
原因: 基础镜像 node:20-alpine 使用 musl libc,而 rolldown 没有为其提供预编译二进制。
解决: 将基础镜像改为 node:20-slim(基于 Debian,使用 glibc)。
坑 2:前端容器返回 500
现象: curl http://localhost/chat/ 返回 500。
原因: 容器内 Nginx 配置的 root 指向了 /usr/share/nginx/html,而前端文件在 /chat/ 子目录下。
解决: 修改容器内 Nginx 配置,将 root 改为 /usr/share/nginx/html/chat。
坑 3:Nginx 报错 host not found in upstream "frontend"
原因: 宿主机 Nginx 无法解析 Docker 容器名。
解决: 使用 localhost:8080 代替容器名 frontend:80。
坑 4:浏览器访问超时
原因: 云服务器安全组未放行 80 端口,或 Ubuntu UFW 防火墙未开启。
解决: 在安全组添加入方向 TCP:80,并执行 sudo ufw allow 80/tcp。
坑 5:构建时镜像被白名单拦截
错误信息: this image is not in the allowlist
原因: 服务器配置了镜像加速器白名单,但本地镜像未加载成功。
解决: 确保 docker load 成功,并使用 docker compose build --no-cache --pull never。
八、域名与备案
8.1 域名解析
在腾讯云 DNSPod 添加 A 记录:
@→ 服务器公网 IPwww→ 服务器公网 IP
8.2 ICP 备案
中国大陆服务器必须备案,周期约 10~20 个工作日。备案期间域名无法访问,建议暂停 DNS 解析。
备案流程:
- 在腾讯云控制台提交网站备案申请
- 填写网站信息(服务名称、网站内容等)
- 等待管局审核
- 审核通过后恢复解析
九、总结
Week4 的核心收获不是学会了某个新框架,而是打通了从代码到用户的最后一公里。回顾整个过程:
- 容器化思维:将应用及其依赖打包成标准单元,实现"一次构建,到处运行"。
- 网络拓扑理解:区分宿主机网络、Docker 网络、容器内部网络,搞清楚请求流向。
- 错误排查能力 :从 500 错误到
host not found,每一次报错都是一次深入学习的机会。 - 合规意识:在中国大陆运营网站,ICP 备案是绕不开的一环。
下周我们将进入 RAG 知识库的学习,期待与你在向量检索的世界相遇!
项目地址: https://github.com/qishuixian/ai-fullstack-journey/tree/main/week4
在线体验: http://qishuixian.com/chat