refactor: 优化 Docker 部署配置

- 后端使用 uv 管理依赖,镜像内包含所有依赖
- 前端使用生产构建 + serve 提供静态文件
- 添加 WeasyPrint 完整系统依赖
- 修复 PDF 报告 Logo 显示问题
- 添加 .dockerignore 优化构建
- 更新部署文档和 GitHub Actions 工作流
- 前端端口从 5173 改为 3000
This commit is contained in:
lintsinghua 2025-12-05 20:51:22 +08:00
parent db3d8fd9f8
commit 33c4df9645
14 changed files with 220 additions and 249 deletions

View File

@ -115,10 +115,11 @@ jobs:
# 打包 Docker 配置文件
tar -czf release/xcodereviewer-docker-${{ steps.version.outputs.VERSION }}.tar.gz \
docker-compose.yml \
Dockerfile \
nginx.conf \
backend/Dockerfile \
backend/.dockerignore \
frontend/Dockerfile \
frontend/.dockerignore \
frontend/docker-entrypoint.sh \
backend/env.example \
frontend/.env.example
@ -206,12 +207,12 @@ jobs:
- name: 设置 Docker Buildx
uses: docker/setup-buildx-action@v3
# 16. 构建并推送前端 Docker 镜像(生产环境 Nginx
# 16. 构建并推送前端 Docker 镜像
- name: 构建并推送前端 Docker 镜像
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
context: ./frontend
file: ./frontend/Dockerfile
push: true
platforms: linux/amd64,linux/arm64
tags: |

View File

@ -1,35 +0,0 @@
# 多阶段构建 - 构建阶段
FROM node:20-alpine AS builder
# 设置工作目录
WORKDIR /app
# 安装 pnpm
RUN npm install -g pnpm
# 复制前端项目文件
COPY frontend/package.json frontend/pnpm-lock.yaml ./
# 安装依赖
RUN pnpm install --frozen-lockfile
# 复制前端源代码
COPY frontend/ .
# 构建应用
RUN pnpm build
# 生产阶段 - 使用 nginx 提供静态文件服务
FROM nginx:alpine
# 复制自定义 nginx 配置
COPY nginx.conf /etc/nginx/conf.d/default.conf
# 从构建阶段复制构建产物
COPY --from=builder /app/dist /usr/share/nginx/html
# 暴露端口
EXPOSE 80
# 启动 nginx
CMD ["nginx", "-g", "daemon off;"]

View File

@ -95,7 +95,7 @@ cp backend/env.example backend/.env
docker-compose up -d
```
🎉 **搞定!** 打开 http://localhost:5173 开始体验吧!
🎉 **搞定!** 打开 http://localhost:3000 开始体验吧!
### 演示账户

12
backend/.dockerignore Normal file
View File

@ -0,0 +1,12 @@
.venv
__pycache__
*.pyc
.git
.gitignore
*.md
.env
.vscode
.DS_Store
uploads/
.mypy_cache
.ruff_cache

View File

@ -4,37 +4,45 @@ WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV PIP_NO_CACHE_DIR=1
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
# 清除代理设置,避免容器内网络问题
ENV http_proxy=
ENV https_proxy=
ENV HTTP_PROXY=
ENV HTTPS_PROXY=
# 安装系统依赖(包含 WeasyPrint 所需的库和中文字体支持)
RUN for i in 1 2 3; do \
RUN rm -f /etc/apt/apt.conf.d/proxy.conf 2>/dev/null || true && \
unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY all_proxy ALL_PROXY && \
apt-get update && \
apt-get install -y --no-install-recommends \
gcc \
libpq-dev \
# WeasyPrint 依赖
curl \
# WeasyPrint 完整依赖
libpango-1.0-0 \
libpangoft2-1.0-0 \
libpangocairo-1.0-0 \
libcairo2 \
libgdk-pixbuf2.0-0 \
libgdk-pixbuf-2.0-0 \
libffi-dev \
libglib2.0-0 \
shared-mime-info \
# 字体支持(中文)
fonts-noto-cjk \
fonts-noto-cjk-extra \
fontconfig \
&& fc-cache -fv \
&& rm -rf /var/lib/apt/lists/* \
&& break || sleep 5; \
done
&& fc-cache -fv \
&& rm -rf /var/lib/apt/lists/*
# 安装 uv
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
# 复制依赖文件
COPY pyproject.toml .
COPY requirements.txt .
COPY pyproject.toml uv.lock ./
# 安装 Python 依赖
RUN pip install --no-cache-dir -r requirements.txt
# 使用 uv 安装依赖(确保无代理)
RUN unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY all_proxy ALL_PROXY && \
uv sync --frozen --no-dev
# 复制应用代码
COPY . .

View File

@ -375,13 +375,18 @@ class ReportGenerator:
"""读取并编码 Logo 图片"""
try:
current_dir = os.path.dirname(os.path.abspath(__file__))
# 回退三级到项目根目录: services -> app -> backend -> root
project_root = os.path.abspath(os.path.join(current_dir, '../../../'))
logo_path = os.path.join(project_root, 'frontend/public/images/logo_nobg.png')
# 尝试多个可能的路径
possible_paths = [
# Docker 容器内路径
os.path.join(current_dir, '../../static/images/logo_nobg.png'),
# 本地开发路径
os.path.abspath(os.path.join(current_dir, '../../../frontend/public/images/logo_nobg.png')),
]
if os.path.exists(logo_path):
with open(logo_path, "rb") as image_file:
return base64.b64encode(image_file.read()).decode('utf-8')
for logo_path in possible_paths:
if os.path.exists(logo_path):
with open(logo_path, "rb") as image_file:
return base64.b64encode(image_file.read()).decode('utf-8')
except Exception as e:
print(f"Error loading logo: {e}")
return ""

Binary file not shown.

After

Width:  |  Height:  |  Size: 774 KiB

View File

@ -18,11 +18,10 @@ services:
- xcodereviewer-network
backend:
build:
build:
context: ./backend
volumes:
- ./backend:/app
- ./backend/uploads:/app/uploads
- backend_uploads:/app/uploads
ports:
- "8000:8000"
env_file:
@ -32,23 +31,19 @@ services:
depends_on:
db:
condition: service_healthy
command: sh -c "alembic upgrade head && uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload"
command: sh -c ".venv/bin/alembic upgrade head && .venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 8000"
networks:
- xcodereviewer-network
frontend:
build:
context: ./frontend
volumes:
- ./frontend:/app
- /app/node_modules
ports:
- "5173:5173"
- "3000:3000"
environment:
- VITE_API_BASE_URL=http://backend:8000/api/v1
- VITE_API_BASE_URL=http://localhost:8000/api/v1
depends_on:
- backend
command: npm run dev -- --host
networks:
- xcodereviewer-network
@ -58,3 +53,4 @@ networks:
volumes:
postgres_data:
backend_uploads:

View File

@ -26,10 +26,10 @@ cp backend/env.example backend/.env
# 编辑 backend/.env配置 LLM API Key
# 3. 启动所有服务
docker-compose up -d
docker compose up -d
# 4. 访问应用
# 前端: http://localhost:5173
# 前端: http://localhost:3000
# 后端 API: http://localhost:8000/docs
```
@ -89,26 +89,26 @@ LLM_MODEL=gpt-4o-mini
```bash
# 3. 启动所有服务
docker-compose up -d
docker compose up -d
# 4. 查看服务状态
docker-compose ps
docker compose ps
# 5. 查看日志
docker-compose logs -f
docker compose logs -f
```
### 服务说明
| 服务 | 端口 | 说明 |
|------|------|------|
| `frontend` | 5173 | React 前端应用(开发模式,支持热重载 |
| `backend` | 8000 | FastAPI 后端 API |
| `frontend` | 3000 | React 前端应用(生产构建,使用 serve 提供静态文件 |
| `backend` | 8000 | FastAPI 后端 API(使用 uv 管理依赖) |
| `db` | 5432 | PostgreSQL 15 数据库 |
### 访问地址
- 前端应用: http://localhost:5173
- 前端应用: http://localhost:3000
- 后端 API: http://localhost:8000
- API 文档 (Swagger): http://localhost:8000/docs
- API 文档 (ReDoc): http://localhost:8000/redoc
@ -117,95 +117,70 @@ docker-compose logs -f
```bash
# 停止所有服务
docker-compose down
docker compose down
# 停止并删除数据卷(清除数据库)
docker-compose down -v
docker compose down -v
# 重新构建镜像
docker-compose build --no-cache
docker compose build --no-cache
# 查看特定服务日志
docker-compose logs -f backend
docker compose logs -f backend
# 进入容器调试
docker-compose exec backend bash
docker-compose exec db psql -U postgres -d xcodereviewer
docker compose exec backend sh
docker compose exec db psql -U postgres -d xcodereviewer
```
---
## 生产环境部署
生产环境建议使用 Nginx 提供前端静态文件服务,并配置 HTTPS。
Docker Compose 默认配置已适用于生产环境:
### 方式一:使用预构建 Docker 镜像
- 前端:构建生产版本,使用 serve 提供静态文件服务
- 后端:使用 uv 管理依赖,镜像内包含所有依赖
- 数据库:使用 Docker Volume 持久化数据
```bash
# 拉取最新镜像
docker pull ghcr.io/lintsinghua/xcodereviewer-frontend:latest
docker pull ghcr.io/lintsinghua/xcodereviewer-backend:latest
### 生产环境安全建议
# 启动后端和数据库
docker-compose up -d db backend
1. **修改默认密钥**:务必修改 `SECRET_KEY` 为随机字符串
2. **配置 HTTPS**:使用 Nginx 反向代理并配置 SSL 证书
3. **限制 CORS**:在生产环境配置具体的前端域名
4. **数据库安全**:修改默认数据库密码,限制访问 IP
5. **API 限流**:配置 Nginx 或应用层限流
6. **日志监控**:配置日志收集和监控告警
7. **删除演示账户**:生产环境请删除或禁用 demo 账户
# 启动前端Nginx 生产镜像)
docker run -d \
--name xcodereviewer-frontend \
-p 80:80 \
--network xcodereviewer-network \
ghcr.io/lintsinghua/xcodereviewer-frontend:latest
```
### Nginx 反向代理配置(可选)
### 方式二:本地构建生产镜像
```bash
# 构建前端生产镜像(使用 Nginx
docker build -t xcodereviewer-frontend .
# 运行前端容器
docker run -d \
-p 80:80 \
--name xcodereviewer-frontend \
xcodereviewer-frontend
# 后端和数据库仍使用 docker-compose
docker-compose up -d db backend
```
### 方式三:手动部署
#### 前端部署
```bash
cd frontend
# 安装依赖
pnpm install
# 构建生产版本
pnpm build
# 将 dist 目录部署到 Nginx/Apache 等 Web 服务器
```
Nginx 配置示例:
如需使用 Nginx 提供 HTTPS 和统一入口:
```nginx
server {
listen 80;
server_name your-domain.com;
root /var/www/xcodereviewer/dist;
index index.html;
return 301 https://$server_name$request_uri;
}
# 前端路由支持
server {
listen 443 ssl http2;
server_name your-domain.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
# 前端
location / {
try_files $uri $uri/ /index.html;
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# API 代理
location /api {
proxy_pass http://localhost: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;
@ -214,38 +189,6 @@ server {
}
```
#### 后端部署
```bash
cd backend
# 创建虚拟环境
python -m venv .venv
source .venv/bin/activate
# 安装依赖
pip install -r requirements.txt
# 配置环境变量
cp env.example .env
# 编辑 .env 文件
# 初始化数据库
alembic upgrade head
# 使用 Gunicorn 启动(生产环境)
gunicorn app.main:app -w 4 -k uvicorn.workers.UvicornWorker -b 0.0.0.0:8000
```
### 生产环境安全建议
1. **修改默认密钥**:务必修改 `SECRET_KEY` 为随机字符串
2. **配置 HTTPS**:使用 Let's Encrypt 或其他 SSL 证书
3. **限制 CORS**:在生产环境配置具体的前端域名
4. **数据库安全**:修改默认数据库密码,限制访问 IP
5. **API 限流**:配置 Nginx 或应用层限流
6. **日志监控**:配置日志收集和监控告警
---
## 本地开发部署
@ -259,13 +202,13 @@ gunicorn app.main:app -w 4 -k uvicorn.workers.UvicornWorker -b 0.0.0.0:8000
| Node.js | 18+ | 前端运行环境 |
| Python | 3.13+ | 后端运行环境 |
| PostgreSQL | 15+ | 数据库 |
| pnpm | 8+ | 推荐的包管理器 |
| pnpm | 8+ | 推荐的前端包管理器 |
| uv | 最新版 | 推荐的 Python 包管理器 |
### 数据库准备
```bash
# 方式一:使用 Docker 启动 PostgreSQL
# 方式一:使用 Docker 启动 PostgreSQL(推荐)
docker run -d \
--name xcodereviewer-db \
-e POSTGRES_USER=postgres \
@ -275,7 +218,6 @@ docker run -d \
postgres:15-alpine
# 方式二:使用本地 PostgreSQL
# 创建数据库
createdb xcodereviewer
```
@ -285,25 +227,21 @@ createdb xcodereviewer
# 1. 进入后端目录
cd backend
# 2. 创建虚拟环境(推荐使用 uv
uv venv
source .venv/bin/activate # Linux/macOS
# 或 .venv\Scripts\activate # Windows
# 2. 安装 uv如未安装
curl -LsSf https://astral.sh/uv/install.sh | sh
# 3. 安装依赖
uv pip install -e .
# 或使用 pip
pip install -r requirements.txt
# 3. 同步依赖
uv sync
# 4. 配置环境变量
cp env.example .env
# 编辑 .env 文件,配置数据库和 LLM 参数
# 5. 初始化数据库
alembic upgrade head
uv run alembic upgrade head
# 6. 启动后端服务(开发模式,支持热重载)
uvicorn app.main:app --reload --port 8000
uv run uvicorn app.main:app --reload --port 8000
```
### 前端启动
@ -314,9 +252,8 @@ cd frontend
# 2. 安装依赖
pnpm install
# 或 npm install / yarn install
# 3. 配置环境变量(可选,也可使用运行时配置
# 3. 配置环境变量(可选)
cp .env.example .env
# 4. 启动开发服务器
@ -339,10 +276,10 @@ pnpm format
# 后端类型检查
cd backend
mypy app
uv run mypy app
# 后端代码格式化
ruff format app
uv run ruff format app
```
---
@ -364,10 +301,10 @@ XCodeReviewer 采用前后端分离架构,所有数据存储在后端 PostgreS
```bash
# 导出 PostgreSQL 数据
docker-compose exec db pg_dump -U postgres xcodereviewer > backup.sql
docker compose exec db pg_dump -U postgres xcodereviewer > backup.sql
# 恢复数据
docker-compose exec -T db psql -U postgres xcodereviewer < backup.sql
docker compose exec -T db psql -U postgres xcodereviewer < backup.sql
```
---
@ -380,7 +317,7 @@ docker-compose exec -T db psql -U postgres xcodereviewer < backup.sql
```bash
# 检查端口占用
lsof -i :5173
lsof -i :3000
lsof -i :8000
lsof -i :5432
@ -391,29 +328,37 @@ lsof -i :5432
```bash
# 检查数据库容器状态
docker-compose ps db
docker compose ps db
# 查看数据库日志
docker-compose logs db
docker compose logs db
# 确保数据库健康检查通过后再启动后端
docker-compose up -d db
docker-compose exec db pg_isready -U postgres
docker-compose up -d backend
docker compose up -d db
docker compose exec db pg_isready -U postgres
docker compose up -d backend
```
**Q: 构建时网络问题(代理相关)**
如果构建时遇到网络问题,检查 Docker Desktop 的代理设置:
1. 打开 Docker Desktop → Settings → Resources → Proxies
2. 关闭代理或配置正确的代理地址
3. 重启 Docker Desktop
4. 重新构建:`docker compose build --no-cache`
### 后端相关
**Q: PDF 导出功能报错WeasyPrint 依赖问题)**
WeasyPrint 需要系统级依赖Docker 镜像已包含。本地开发时:
Docker 镜像已包含 WeasyPrint 所需的系统依赖。本地开发时需要安装
```bash
# macOS
brew install pango cairo gdk-pixbuf libffi
# Ubuntu/Debian
sudo apt-get install libpango-1.0-0 libpangoft2-1.0-0 libcairo2 libgdk-pixbuf2.0-0
sudo apt-get install libpango-1.0-0 libpangoft2-1.0-0 libcairo2 libgdk-pixbuf-2.0-0 libglib2.0-0
# Windows - 参见 FAQ.md 中的详细说明
```
@ -435,14 +380,14 @@ LLM_GAP_MS=3000
**Q: 前端无法连接后端 API**
检查 `frontend/.env` 中的 API 地址配置:
Docker Compose 部署时,前端通过 `http://localhost:8000/api/v1` 访问后端。确保:
1. 后端容器正常运行:`docker compose ps backend`
2. 后端端口 8000 可访问:`curl http://localhost:8000/docs`
本地开发时,检查 `frontend/.env` 中的 API 地址配置:
```env
# 本地开发
VITE_API_BASE_URL=http://localhost:8000/api/v1
# Docker Compose 部署
VITE_API_BASE_URL=/api
```
---

8
frontend/.dockerignore Normal file
View File

@ -0,0 +1,8 @@
node_modules
dist
.git
.gitignore
*.md
.env*
.vscode
.DS_Store

View File

@ -1,23 +1,56 @@
FROM node:20-alpine
FROM node:20-alpine AS builder
WORKDIR /app
# 安装 pnpm
RUN npm install -g pnpm
# 清除代理设置
ENV http_proxy=
ENV https_proxy=
ENV HTTP_PROXY=
ENV HTTPS_PROXY=
# 安装 pnpm确保无代理
RUN unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY all_proxy ALL_PROXY && \
npm install -g pnpm
# 复制依赖文件
COPY package.json pnpm-lock.yaml ./
# 安装依赖
RUN pnpm install
# 安装依赖(确保无代理)
RUN unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY all_proxy ALL_PROXY && \
pnpm install --frozen-lockfile
# 复制源代码
COPY . .
# 暴露端口
EXPOSE 5173
# 构建时使用占位符,运行时替换
ENV VITE_API_BASE_URL=__API_BASE_URL__
# 开发模式启动命令
CMD ["pnpm", "dev", "--host"]
# 构建生产版本
RUN pnpm build
# 生产镜像
FROM node:20-alpine
WORKDIR /app
# 清除代理设置
ENV http_proxy=
ENV https_proxy=
ENV HTTP_PROXY=
ENV HTTPS_PROXY=
# 安装 serve确保无代理
RUN unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY all_proxy ALL_PROXY && \
npm install -g serve
# 复制构建产物
COPY --from=builder /app/dist ./dist
# 复制启动脚本
COPY docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh
EXPOSE 3000
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["serve", "-s", "dist", "-l", "3000"]

View File

@ -0,0 +1,10 @@
#!/bin/sh
# 替换 API 地址占位符
API_URL="${VITE_API_BASE_URL:-http://localhost:8000/api/v1}"
# 在所有 JS 文件中替换占位符
find /app/dist -name '*.js' -exec sed -i "s|__API_BASE_URL__|${API_URL}|g" {} \;
# 执行原始命令
exec "$@"

26
frontend/nginx.conf Normal file
View File

@ -0,0 +1,26 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# 处理 SPA 路由
location / {
try_files $uri $uri/ /index.html;
}
# 代理 API 请求到后端
location /api/ {
proxy_pass http://backend: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;
proxy_set_header X-Forwarded-Proto $scheme;
}
# 缓存静态资源
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}

View File

@ -1,38 +0,0 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# Gzip 压缩配置
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/json application/javascript;
# 安全头部
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# 处理 React Router 的客户端路由
location / {
try_files $uri $uri/ /index.html;
}
# 静态资源缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# API 代理(如果需要)
# location /api {
# proxy_pass http://backend:8000;
# proxy_http_version 1.1;
# proxy_set_header Upgrade $http_upgrade;
# proxy_set_header Connection 'upgrade';
# proxy_set_header Host $host;
# proxy_cache_bypass $http_upgrade;
# }
}