AI 全栈学习之旅 - Week4:容器化部署与生产环境实战

前言

经过前三周的洗礼,我们的 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 记录:

  • @ → 服务器公网 IP
  • www → 服务器公网 IP

8.2 ICP 备案

中国大陆服务器必须备案,周期约 10~20 个工作日。备案期间域名无法访问,建议暂停 DNS 解析。

备案流程:

  1. 在腾讯云控制台提交网站备案申请
  2. 填写网站信息(服务名称、网站内容等)
  3. 等待管局审核
  4. 审核通过后恢复解析

九、总结

Week4 的核心收获不是学会了某个新框架,而是打通了从代码到用户的最后一公里。回顾整个过程:

  1. 容器化思维:将应用及其依赖打包成标准单元,实现"一次构建,到处运行"。
  2. 网络拓扑理解:区分宿主机网络、Docker 网络、容器内部网络,搞清楚请求流向。
  3. 错误排查能力 :从 500 错误到 host not found,每一次报错都是一次深入学习的机会。
  4. 合规意识:在中国大陆运营网站,ICP 备案是绕不开的一环。

下周我们将进入 RAG 知识库的学习,期待与你在向量检索的世界相遇!


项目地址:https://github.com/qishuixian/ai-fullstack-journey/tree/main/week4

在线体验:http://qishuixian.com/chat

相关推荐
翻身的咸鱼ing14 分钟前
ArkTS 学习笔记
笔记·学习
春涧草茶16 分钟前
慢就是快 - 10-2函数 特殊字面量 返回值
python
程序猿炎义17 分钟前
【llm-algo-leetcode学习笔记】训练侧显存优化
人工智能·笔记·学习
动词ing18 分钟前
【学习笔记】数据结构(数组滑动窗口+无重复字符的最长字串题目)
数据结构·笔记·学习
慢云智慧空间24 分钟前
空间智能与计算机视觉的本质差异:不止于识别,更是空间决策能力
人工智能·python·计算机视觉
田径他爸来了1 小时前
嵌入式学习第32天(同时多个用户访问的服务器)
运维·服务器·学习
典典分享指南1 小时前
Windows 自动化踩坑实录
python·intellij-idea·pyqt·emacs·visual studio
工业一体机老司机1 小时前
Python实现工业一体机OPC-UA通信:从SDK选型到数据采集实战
开发语言·python·单片机
威联通网络存储1 小时前
TS-h2477AXU-RP 在化工制造数据场景的部署
python·制造