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:
parent
5fecbab6bf
commit
017a19515c
24
.env.example
24
.env.example
|
|
@ -1,23 +1,11 @@
|
|||
# Database
|
||||
POSTGRES_USER=docforge
|
||||
POSTGRES_PASSWORD=docforge
|
||||
POSTGRES_PASSWORD=change-this-password
|
||||
POSTGRES_DB=docforge
|
||||
POSTGRES_HOST=localhost
|
||||
POSTGRES_PORT=5432
|
||||
|
||||
# Redis
|
||||
REDIS_URL=redis://localhost:6379/0
|
||||
# Security (generate with: python -c "import secrets; print(secrets.token_urlsafe(32))")
|
||||
SECRET_KEY=change-me-to-a-random-secret-key
|
||||
|
||||
# Backend
|
||||
SECRET_KEY=your-fernet-key-change-in-production
|
||||
DEFAULT_MODEL_ID=
|
||||
|
||||
# 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
|
||||
# AI (optional, can be configured in the web UI)
|
||||
OPENAI_API_KEY=
|
||||
AZURE_API_KEY=
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
venv/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.env
|
||||
.git
|
||||
storage/
|
||||
alembic/versions/
|
||||
*.egg-info/
|
||||
.pytest_cache/
|
||||
|
|
@ -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"]
|
||||
|
|
@ -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"]
|
||||
|
|
@ -3,6 +3,7 @@ from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form, Q
|
|||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
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.template import Template
|
||||
from app.models.ai_model import AIModel
|
||||
|
|
@ -39,7 +40,8 @@ async def create_generation_point(
|
|||
raise HTTPException(status_code=404, detail="模型不存在")
|
||||
|
||||
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)
|
||||
|
||||
point = GenerationPoint(
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ from fastapi.responses import Response
|
|||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from app.core.database import get_db
|
||||
from app.core.security_middleware import validate_file_extension
|
||||
from app.models.template import Template
|
||||
from app.schemas.template import TemplateResponse, TemplateListItem, HTMLContentResponse, HTMLUpdateRequest
|
||||
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"):
|
||||
raise HTTPException(status_code=400, detail="仅支持 .docx 文件")
|
||||
|
||||
validate_file_extension(file.filename)
|
||||
file_path = await save_upload(file, TEMPLATES_DIR)
|
||||
file_content = await get_file_content(file_path)
|
||||
html_content = await docx_to_html(file_content)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from app.core.config import get_settings
|
||||
from app.core.security_middleware import SecurityMiddleware
|
||||
from app.api.router import api_router
|
||||
|
||||
settings = get_settings()
|
||||
|
|
@ -18,6 +19,8 @@ app.add_middleware(
|
|||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
app.add_middleware(SecurityMiddleware)
|
||||
|
||||
app.include_router(api_router)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -15,6 +15,7 @@ services:
|
|||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
restart: unless-stopped
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
|
|
@ -28,7 +29,73 @@ services:
|
|||
interval: 5s
|
||||
timeout: 5s
|
||||
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:
|
||||
postgres_data:
|
||||
redis_data:
|
||||
storage_data:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
node_modules/
|
||||
dist/
|
||||
.git
|
||||
*.local
|
||||
public/tinymce/
|
||||
|
|
@ -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;"]
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -6,8 +6,8 @@
|
|||
"scripts": {
|
||||
"dev": "vite",
|
||||
"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": {
|
||||
"@ant-design/icons": "^6.3.2",
|
||||
|
|
|
|||
Loading…
Reference in New Issue