全栈 RAG 项目容器化部署全流程技术文档
场景:Java 21 + Spring AI + Vue 3 + Milvus 在阿里云 3.5GB 内存服务器上的极限部署。
1. 核心技术背景与挑战
- 内存约束:宿主机 3.5GB 内存。需运行 Java、Nginx、Milvus、Etcd、Minio,内存分配必须极其精确。
- 通信隔离 :Docker 容器内
127.0.0.1指向容器自身,无法访问宿主机或其他容器,必须通过环境变量注入内网 IP。 - 流式响应:AI 对话需要 SSE (Server-Sent Events) 支持,Nginx 默认缓存机制会破坏流式打字机效果。
2. 宿主机环境配置 (关键细节)
2.1 环境变量持久化
为了确保后端能连接 Milvus 和 OpenAI,必须在服务器全局配置变量。
-
操作文件 :
/etc/environment -
细节 :不要在变量名前加
export,直接写KEY="VALUE"。 -
配置内容 :
bashPATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin" CLOUD_IP="172.22.229.185" # 阿里云内网IP,通过 hostname -I 获取 OPENAI_API_KEY="sk-xxxxxxxxxxxx" -
生效方法 :执行
source /etc/environment。
2.2 内存赤字账本 (优化目标)
| 组件 | 内存需求 | 最终限制 (Docker Limit) | 优化手段 |
|---|---|---|---|
| Java (Spring AI) | ~1.0GB | 512MB | 虚拟线程 + SerialGC + Xmx256m |
| Milvus | ~1.0GB | 1GB | 保持单机版最小化运行 |
| Nginx (Vue) | ~100MB | 不限制 (实际约3MB) | Alpine 轻量镜像 |
3. 前后端容器化方案
3.1 后端:Dockerfile (Java 21 极致版)
避坑细节:使用 JRE 而非 JDK 缩小体积;针对低内存强制使用串行 GC。
dockerfile
# 第一阶段:Maven 编译
FROM maven:3.9.6-eclipse-temurin-21-alpine AS build
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline -B
COPY src ./src
RUN mvn clean package -DskipTests
# 第二阶段:运行
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=build /app/target/*.jar app.jar
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
# 核心 JVM 参数细节:
# -Xmx256m: 堆内存留一半给 Metaspace 和线程栈
# -XX:+UseSerialGC: 显著降低内存开销,适合单核/双核云服务器
# -Djdk.virtualThreadScheduler.parallelism=1: 限制虚拟线程并行度
ENTRYPOINT ["java", \
"-Xmx256m", "-Xms256m", \
"-XX:MaxMetaspaceSize=128m", \
"-XX:+UseSerialGC", \
"-Xss256k", \
"-XX:+ExitOnOutOfMemoryError", \
"-Djdk.virtualThreadScheduler.parallelism=1", \
"-jar", "app.jar"]
3.2 前端:Dockerfile (Vue 3 + Vite)
dockerfile
FROM node:20-alpine AS build-stage
WORKDIR /app
COPY package*.json ./
RUN npm install --registry=https://registry.npmmirror.com
COPY . .
RUN npm run build
FROM nginx:stable-alpine
COPY --from=build-stage /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
3.3 关键:Nginx 配置 (nginx.conf)
避坑细节:
- 路径匹配 :如果前端请求
/api/chat,location必须是/api/。若配置为/ai/会导致返回index.html源码。 - SSE 支持 :必须关闭
proxy_buffering。
nginx
server {
listen 80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
location /api/ {
# ai-rag-service 是 docker-compose 中的服务名
proxy_pass http://ai-rag-service:8081/api/;
# SSE 流式响应优化
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_buffering off;
proxy_cache off;
chunked_transfer_encoding on;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
4. 多服务编排 (docker-compose.yml)
yaml
version: '3.8'
services:
ai-rag-service:
build:
context: ../spring-ai-rag
container_name: ai-rag-backend
environment:
- CLOUD_IP=${CLOUD_IP} # 关键:从宿主机注入内网IP
- OPENAI_API_KEY=${OPENAI_API_KEY}
deploy:
resources:
limits:
memory: 512M
networks:
- ai-network
web-ui:
build: .
container_name: web-ui-app
ports:
- "80:80"
depends_on:
- ai-rag-service
networks:
- ai-network
networks:
ai-network:
driver: bridge
5. 自动化部署脚本 (deploy_all.sh)
避坑细节 :子 Shell 默认不继承 /etc/environment,脚本内必须手动 source。
bash
#!/bin/bash
set -e
# 1. 强制刷新环境变量
source /etc/environment
echo ">>> 加载 CLOUD_IP: $CLOUD_IP"
# 2. 路径对齐
UI_PATH="/usr/servise/web-ui"
BACKEND_PATH="/usr/servise/spring-ai-rag"
# 3. 代码同步
echo ">>> 正在从 Git 强制对齐源码..."
cd $BACKEND_PATH && git fetch --all && git reset --hard origin/main
cd $UI_PATH && git fetch --all && git reset --hard origin/main
# 4. 重新构建启动
echo ">>> 正在重启 Docker 服务..."
docker compose -f $UI_PATH/docker-compose.yml down
docker compose -f $UI_PATH/docker-compose.yml up -d --build --force-recreate
# 5. 清理碎片
docker image prune -f
echo ">>> 验证服务统计..."
docker stats --no-stream
6. 经典报错与全细节解决方案
Q1: 容器启动瞬间不停重启 (Restarting)
- 根源 :环境变量
${CLOUD_IP}为空,导致 Spring Boot 解析 URI 出现非法字符$,{。 - 解决 :在
/etc/environment设置变量并在脚本中执行source。
Q2: 接口返回的是 HTML 源代码 (Vue 首页)
- 现象 :Network 请求
chat?query=...返回的是<!DOCTYPE html>...。 - 根源 :Nginx
location路径匹配失效。比如配置了/ai/转发,前端却请求/api/。Nginx 找不到匹配,就走location /的try_files返回了首页。 - 解决 :确保 Nginx
location路径、前端baseURL、后端@RequestMapping三者路径前缀完全一致。
Q3: 报错 Host name cannot be null or empty
- 根源 :虽然配置了变量,但
docker run或docker-compose时没写-e CLOUD_IP=$CLOUD_IP,变量没传进容器。 - 验证 :
docker exec ai-rag-backend env | grep CLOUD_IP。
Q4: 浏览器显示内容没变 (缓存陷阱)
- 现象:明明改了代码并重启,浏览器还是报旧错误。
- 根源 :Headers 显示
200 OK (from disk cache),浏览器没向服务器发请求。 - 解决 :F12 勾选
Disable cache或Ctrl + F5强制刷新。
Q5: SSE 流式对话卡顿
- 根源 :Nginx 开启了
proxy_buffering,数据攒够了才发。 - 解决 :Nginx 配置加入
proxy_buffering off;。
7. 统一命令速查手册
7.1 服务监控
- 查看内存 CPU 实时占用 :
docker stats - 查看后端实时日志 :
docker logs -f ai-rag-backend - 查看容器最后 100 行日志 :
docker logs --tail 100 ai-rag-backend
7.2 网络验证
- 容器内验证连通性 :
docker exec -it web-ui-app curl -v http://ai-rag-service:8081/api/chat?query=ping - 查看宿主机内网 IP :
hostname -I - 外部测试 SSE 流 :
curl -N "http://8.140.221.150/api/chat?query=hello"
7.3 清理与维护
- 进入容器内部 :
docker exec -it ai-rag-backend sh - 强删冗余容器 :
docker rm -f spring-ai-rag(解决两个后端并存问题) - 清理无用镜像 :
docker image prune -f
8. 总结
本方案通过多阶段构建 解决了镜像体积问题,通过串行 GC 与内存限制 解决了低内存服务器的稳定性问题,通过 Nginx 反向代理与环境变量注入解决了前后端连通性与流式响应问题。这是一个完整的生产级单机 AI 部署闭环。
9. 附录:Docker 常用命令生产级大全
在本项目的运维中,熟练掌握以下命令可以解决 90% 的部署与排障问题。
9.1 容器生命周期管理
| 命令 | 说明 | 备注 (针对本项目) |
|---|---|---|
docker ps |
查看运行中的容器 | 检查 ai-rag-backend 是否处于 Up 状态 |
docker ps -a |
查看所有容器(含已停止的) | 找回因 OOM 崩溃退出的容器 |
docker stop <ID/Name> |
停止容器 | 部署新版本前通常需要停止旧容器 |
docker start <ID/Name> |
启动已停止的容器 | |
docker restart <ID/Name> |
重启容器 | 修改了宿主机 /etc/environment 后建议执行 |
docker rm -f <ID/Name> |
强制删除容器 | 用于清理重名的旧容器(如 spring-ai-rag) |
9.2 镜像操作 (Image)
| 命令 | 说明 | 备注 |
|---|---|---|
docker images |
列出本地所有镜像 | 检查镜像大小,本项目的 Java 镜像应在 160MB 左右 |
docker build -t <Name>:<Tag> . |
构建镜像 | 后端多阶段构建的核心命令 |
docker rmi <ImageID> |
删除镜像 | 节省磁盘空间 |
docker tag <Old> <New> |
镜像打标签 | 方便版本管理,如给时间戳版本打上 latest |
9.3 调试与排障 (Debug) ------ 最常用
-
查看实时日志:
bashdocker logs -f ai-rag-backend-f: 持续追踪;--tail 100: 只看最后 100 行。针对 AI 对话调试必用。 -
进入容器内部:
bashdocker exec -it ai-rag-backend sh注意:Alpine 镜像通常没有
bash,需使用sh。进入后可执行env查看变量。 -
查看容器详细配置(JSON):
bashdocker inspect ai-rag-backend用于排查:容器 IP 地址、挂载卷路径、OOM 记录(看
State里的OOMKilled)。 -
从宿主机拷贝文件到容器:
bashdocker cp /path/to/local/file ai-rag-backend:/app/config/
9.4 状态监控 (Monitor) ------ 内存受限环境必备
-
实时查看资源占用:
bashdocker stats在进行 AI 提问时观察
MEM %,如果接近 100% 且容器重启,需调大limit。 -
查看容器内进程:
bashdocker top ai-rag-backend -
查看 Docker 磁盘占用:
bashdocker system df
9.5 Docker Compose 专项 (编排运维)
在 /usr/servise/web-ui 目录下执行:
- 一键启动所有服务(后台):
docker compose up -d - 强制重新构建并启动:
docker compose up -d --build(修改了 Dockerfile 或 Nginx 配置后执行) - 停止并删除所有容器与网络:
docker compose down - 查看编排的服务日志:
docker compose logs -f
9.6 系统清理 (Maintenance)
-
清理所有虚悬镜像(Dangling):
bashdocker image prune -f本项目自动化脚本中已包含,防止多次构建撑爆磁盘。
-
一键清理无用资源(慎用):
bashdocker system prune -a会删除所有未使用的镜像、容器和网络。
10. 针对本项目的"救急"命令组合
场景 1:后端连不上 Milvus
bash
# 1. 确认宿主机变量是否正确
echo $CLOUD_IP
# 2. 确认变量是否传进容器
docker exec ai-rag-backend env | grep CLOUD_IP
# 3. 确认容器内网络是否通畅
docker exec -it ai-rag-backend ping $CLOUD_IP
场景 2:前端页面能开,但点击对话没反应(接口 502)
bash
# 1. 确认后端容器是否存活
docker ps | grep ai-rag-backend
# 2. 如果存活,检查 Nginx 转发逻辑
docker exec -it web-ui-app sh
# 进入后尝试连接后端容器的服务名和端口
curl http://ai-rag-service:8081/api/chat?query=ping
场景 3:服务器内存告急,想优化
bash
# 查看哪个组件吃内存最多
docker stats --no-stream --format "table {{.Name}}\t{{.MemUsage}}\t{{.MemPerc}}"
# 如果是 Java 占用过高且在持续增长,检查 JVM 参数中的 -Xmx 是否生效
11. 总结:生产部署口诀
- 改代码 →\rightarrow→ Git Push。
- 上服务器 →\rightarrow→
source /etc/environment刷新变量。 - 运行脚本 →\rightarrow→
./deploy_all.sh自动化打包重启。 - 盯监控 →\rightarrow→
docker stats观察内存是否稳定。 - 看日志 →\rightarrow→
docker logs -f ai-rag-backend确认 Spring 启动图案出现。
文档结束。 该手册可作为团队内部部署 Spring AI 相关项目的标准指南。