feat: 阶段五 - 部署上线与安全加固

- Dockerfile: backend (FastAPI), celery-worker, web (多阶段构建 + Nginx)
- docker-compose.yml: 完整 5 服务编排 (postgres + redis + backend + celery + nginx)
- Nginx 反向代理配置 + gzip 压缩 + API 代理
- 安全增强: 请求大小限制、文件类型白名单(.docx/.txt/.pdf)、安全中间件
- .dockerignore: 排除 venv/node_modules 等
- .env.example: 精简为生产关键变量
- package.json: postinstall 自动复制 TinyMCE 自托管文件
- deploy.sh: 一键部署脚本
This commit is contained in:
zwt13703 2026-07-06 16:57:00 +08:00
parent 5fecbab6bf
commit 017a19515c
14 changed files with 247 additions and 21 deletions

View File

@ -1,23 +1,11 @@
# Database # Database
POSTGRES_USER=docforge POSTGRES_USER=docforge
POSTGRES_PASSWORD=docforge POSTGRES_PASSWORD=change-this-password
POSTGRES_DB=docforge POSTGRES_DB=docforge
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
# Redis # Security (generate with: python -c "import secrets; print(secrets.token_urlsafe(32))")
REDIS_URL=redis://localhost:6379/0 SECRET_KEY=change-me-to-a-random-secret-key
# Backend # AI (optional, can be configured in the web UI)
SECRET_KEY=your-fernet-key-change-in-production OPENAI_API_KEY=
DEFAULT_MODEL_ID= AZURE_API_KEY=
# File Storage
STORAGE_ROOT=./storage
# Celery
CELERY_BROKER_URL=redis://localhost:6379/0
CELERY_RESULT_BACKEND=redis://localhost:6379/1
# CORS
CORS_ORIGINS=http://localhost:3000,http://localhost:5173

9
backend/.dockerignore Normal file
View File

@ -0,0 +1,9 @@
venv/
__pycache__/
*.pyc
.env
.git
storage/
alembic/versions/
*.egg-info/
.pytest_cache/

21
backend/Dockerfile Normal file
View File

@ -0,0 +1,21 @@
FROM python:3.12-slim
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc libpq-dev && \
rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN mkdir -p /app/storage/templates /app/storage/ref_files /app/storage/results
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

19
backend/Dockerfile.celery Normal file
View File

@ -0,0 +1,19 @@
FROM python:3.12-slim
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc libpq-dev && \
rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN mkdir -p /app/storage/templates /app/storage/ref_files /app/storage/results
CMD ["celery", "-A", "app.tasks.celery_app", "worker", "--loglevel=info", "--concurrency=4"]

View File

@ -3,6 +3,7 @@ from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form, Q
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select from sqlalchemy import select
from app.core.database import get_db from app.core.database import get_db
from app.core.security_middleware import validate_file_extension
from app.models.generation_point import GenerationPoint from app.models.generation_point import GenerationPoint
from app.models.template import Template from app.models.template import Template
from app.models.ai_model import AIModel from app.models.ai_model import AIModel
@ -39,7 +40,8 @@ async def create_generation_point(
raise HTTPException(status_code=404, detail="模型不存在") raise HTTPException(status_code=404, detail="模型不存在")
ref_file_path = None ref_file_path = None
if ref_file: if ref_file and ref_file.filename:
validate_file_extension(ref_file.filename)
ref_file_path = await save_upload(ref_file, REF_FILES_DIR) ref_file_path = await save_upload(ref_file, REF_FILES_DIR)
point = GenerationPoint( point = GenerationPoint(

View File

@ -4,6 +4,7 @@ from fastapi.responses import Response
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select from sqlalchemy import select
from app.core.database import get_db from app.core.database import get_db
from app.core.security_middleware import validate_file_extension
from app.models.template import Template from app.models.template import Template
from app.schemas.template import TemplateResponse, TemplateListItem, HTMLContentResponse, HTMLUpdateRequest from app.schemas.template import TemplateResponse, TemplateListItem, HTMLContentResponse, HTMLUpdateRequest
from app.services.file_storage import save_upload, get_file_content, delete_file, TEMPLATES_DIR from app.services.file_storage import save_upload, get_file_content, delete_file, TEMPLATES_DIR
@ -21,6 +22,7 @@ async def upload_template(
if not file.filename or not file.filename.endswith(".docx"): if not file.filename or not file.filename.endswith(".docx"):
raise HTTPException(status_code=400, detail="仅支持 .docx 文件") raise HTTPException(status_code=400, detail="仅支持 .docx 文件")
validate_file_extension(file.filename)
file_path = await save_upload(file, TEMPLATES_DIR) file_path = await save_upload(file, TEMPLATES_DIR)
file_content = await get_file_content(file_path) file_content = await get_file_content(file_path)
html_content = await docx_to_html(file_content) html_content = await docx_to_html(file_content)

View File

@ -0,0 +1,32 @@
from fastapi import Request, HTTPException
from starlette.middleware.base import BaseHTTPMiddleware
from app.core.config import get_settings
settings = get_settings()
class SecurityMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
if request.method in ("POST", "PUT", "PATCH"):
content_length = request.headers.get("content-length")
if content_length and int(content_length) > settings.MAX_UPLOAD_SIZE:
raise HTTPException(
status_code=413,
detail=f"文件大小不能超过 {settings.MAX_UPLOAD_SIZE // 1024 // 1024}MB",
)
response = await call_next(request)
return response
ALLOWED_EXTENSIONS = {".docx", ".txt", ".pdf"}
def validate_file_extension(filename: str) -> str:
import os
ext = os.path.splitext(filename)[1].lower()
if ext not in ALLOWED_EXTENSIONS:
raise HTTPException(
status_code=400,
detail=f"不支持的文件类型: {ext},允许的类型: {', '.join(ALLOWED_EXTENSIONS)}",
)
return ext

View File

@ -1,6 +1,7 @@
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from app.core.config import get_settings from app.core.config import get_settings
from app.core.security_middleware import SecurityMiddleware
from app.api.router import api_router from app.api.router import api_router
settings = get_settings() settings = get_settings()
@ -18,6 +19,8 @@ app.add_middleware(
allow_headers=["*"], allow_headers=["*"],
) )
app.add_middleware(SecurityMiddleware)
app.include_router(api_router) app.include_router(api_router)

34
deploy.sh Executable file
View File

@ -0,0 +1,34 @@
#!/bin/bash
set -e
echo ">>> 启动 Doc Forge Reds <<<"
if ! command -v docker &> /dev/null; then
echo "错误: 需要安装 Docker"
exit 1
fi
if [ ! -f .env ]; then
echo "请先复制 .env.example 为 .env 并填写必要配置"
exit 1
fi
echo "1. 构建镜像..."
docker compose build
echo "2. 启动服务..."
docker compose up -d
echo "3. 等待数据库就绪..."
sleep 5
echo "4. 执行数据库迁移..."
docker compose exec backend alembic upgrade head
echo ""
echo ">>> 启动完成 <<<"
echo "前端地址: http://localhost:3000"
echo "后端文档: http://localhost:8000/docs"
echo ""
echo "查看日志: docker compose logs -f"
echo "停止服务: docker compose down"

View File

@ -15,6 +15,7 @@ services:
interval: 5s interval: 5s
timeout: 5s timeout: 5s
retries: 5 retries: 5
restart: unless-stopped
redis: redis:
image: redis:7-alpine image: redis:7-alpine
@ -28,7 +29,73 @@ services:
interval: 5s interval: 5s
timeout: 5s timeout: 5s
retries: 5 retries: 5
restart: unless-stopped
backend:
build:
context: ./backend
dockerfile: Dockerfile
container_name: docforge-backend
environment:
POSTGRES_USER: ${POSTGRES_USER:-docforge}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-docforge}
POSTGRES_DB: ${POSTGRES_DB:-docforge}
POSTGRES_HOST: postgres
POSTGRES_PORT: 5432
REDIS_URL: redis://redis:6379/0
CELERY_BROKER_URL: redis://redis:6379/0
CELERY_RESULT_BACKEND: redis://redis:6379/1
SECRET_KEY: ${SECRET_KEY}
STORAGE_ROOT: /app/storage
CORS_ORIGINS: '["http://localhost:3000","http://localhost:80","http://localhost"]'
ports:
- "8000:8000"
volumes:
- storage_data:/app/storage
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
restart: unless-stopped
celery-worker:
build:
context: ./backend
dockerfile: Dockerfile.celery
container_name: docforge-celery
environment:
POSTGRES_USER: ${POSTGRES_USER:-docforge}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-docforge}
POSTGRES_DB: ${POSTGRES_DB:-docforge}
POSTGRES_HOST: postgres
POSTGRES_PORT: 5432
REDIS_URL: redis://redis:6379/0
CELERY_BROKER_URL: redis://redis:6379/0
CELERY_RESULT_BACKEND: redis://redis:6379/1
SECRET_KEY: ${SECRET_KEY}
STORAGE_ROOT: /app/storage
volumes:
- storage_data:/app/storage
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
restart: unless-stopped
nginx:
build:
context: ./web
dockerfile: Dockerfile
container_name: docforge-nginx
ports:
- "3000:80"
depends_on:
- backend
restart: unless-stopped
volumes: volumes:
postgres_data: postgres_data:
redis_data: redis_data:
storage_data:

5
web/.dockerignore Normal file
View File

@ -0,0 +1,5 @@
node_modules/
dist/
.git
*.local
public/tinymce/

18
web/Dockerfile Normal file
View File

@ -0,0 +1,18 @@
FROM node:22-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run postinstall && npm run build
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

26
web/nginx.conf Normal file
View File

@ -0,0 +1,26 @@
server {
listen 80;
server_name _;
client_max_body_size 100M;
root /usr/share/nginx/html;
index index.html;
location /api/ {
proxy_pass http://backend:8000;
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;
proxy_read_timeout 300s;
}
location / {
try_files $uri $uri/ /index.html;
}
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
gzip_min_length 1000;
}

View File

@ -6,8 +6,8 @@
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "tsc -b && vite build", "build": "tsc -b && vite build",
"lint": "oxlint", "preview": "vite preview",
"preview": "vite preview" "postinstall": "cp -r node_modules/tinymce public/tinymce 2>/dev/null || true"
}, },
"dependencies": { "dependencies": {
"@ant-design/icons": "^6.3.2", "@ant-design/icons": "^6.3.2",