diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..b757d9f --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,56 @@ +# ===== Stage 1: Build dependencies ===== +FROM python:3.12-slim AS builder + +WORKDIR /app + +# Install uv +COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv + +# Copy dependency manifests +COPY backend/pyproject.toml . +COPY backend/.env.example . + +# Create venv and install deps +RUN uv venv /opt/venv && \ + uv pip install --python /opt/venv/bin/python \ + "fastapi>=0.104.0" \ + "uvicorn[standard]>=0.24.0" \ + "python-multipart>=0.0.6" \ + "langextract[all]>=0.1.0" \ + "langchain>=0.2.0" \ + "langchain-openai>=0.1.0" \ + "langgraph>=0.1.0" \ + "networkx>=3.0" \ + "python-dotenv>=1.0.0" \ + "requests>=2.31.0" \ + "beautifulsoup4>=4.12.0" + +# ===== Stage 2: Runtime ===== +FROM python:3.12-slim + +WORKDIR /app + +# Copy venv from builder +COPY --from=builder /opt/venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" + +# Copy backend source +COPY backend/ ./backend/ +# Copy mineru_mvp (used by indexing pipeline) +COPY mineru_mvp/ ./mineru_mvp/ + +# Create data directories +RUN mkdir -p /app/backend/data/uploads \ + /app/backend/data/jobs \ + /app/backend/data/kg \ + /app/backend/data/batches \ + /app/mineru_mvp/output + +# Update paths for container environment +ENV MINERU_PYTHON=/opt/venv/bin/python +ENV MINERU_PIPELINE=/app/mineru_mvp/pipeline.py + +EXPOSE 8000 + +WORKDIR /app/backend +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e5d9fac --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,48 @@ +version: "3.8" + +services: + backend: + build: + context: . + dockerfile: backend/Dockerfile + image: graphrag-backend:latest + container_name: graphrag-backend + restart: unless-stopped + ports: + - "8000:8000" + volumes: + - backend_data:/app/backend/data + - mineru_output:/app/mineru_mvp/output + env_file: + - backend/.env + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/api/v1/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 15s + networks: + - graphrag-net + + frontend: + build: + context: . + dockerfile: frontend/Dockerfile + image: graphrag-frontend:latest + container_name: graphrag-frontend + restart: unless-stopped + ports: + - "5173:80" + depends_on: + backend: + condition: service_healthy + networks: + - graphrag-net + +volumes: + backend_data: + mineru_output: + +networks: + graphrag-net: + driver: bridge diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..223f7f6 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,32 @@ +# ===== Stage 1: Build ===== +FROM node:22-alpine AS builder + +WORKDIR /app + +# Install pnpm +RUN corepack enable && corepack prepare pnpm@10 --activate + +# Copy package manifests +COPY frontend/package.json frontend/pnpm-lock.yaml ./ + +# Install deps +RUN pnpm install --frozen-lockfile + +# Copy source +COPY frontend/ ./ + +# Build +RUN pnpm build + +# ===== Stage 2: Serve with nginx ===== +FROM nginx:alpine + +# Copy built assets +COPY --from=builder /app/dist /usr/share/nginx/html + +# Copy nginx config +COPY frontend/nginx.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..d52bf24 --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,33 @@ +server { + listen 80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + # SPA fallback — all routes serve index.html + location / { + try_files $uri $uri/ /index.html; + } + + # Proxy API requests to backend + 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 120s; + } + + # Static asset caching + location /assets/ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # Gzip + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml; + gzip_min_length 256; +} diff --git a/k8s/README.md b/k8s/README.md new file mode 100644 index 0000000..8588b60 --- /dev/null +++ b/k8s/README.md @@ -0,0 +1,118 @@ +# GraphRAG Studio — Kubernetes 部署指南 + +## 目录结构 + +``` +k8s/ +├── base/ # 共享资源(所有环境通用) +│ ├── kustomization.yaml +│ ├── backend-deployment.yaml +│ ├── backend-service.yaml +│ ├── frontend-deployment.yaml +│ ├── frontend-service.yaml +│ └── persistent-volume-claims.yaml +├── overlays/ +│ ├── test/ # 测试环境 +│ │ ├── kustomization.yaml +│ │ ├── namespace.yaml # → graphrag-test +│ │ └── ingress.yaml # → test-graphrag.plfai.cn +│ └── prod/ # 生产环境 +│ ├── kustomization.yaml +│ ├── namespace.yaml # → graphrag-prod +│ ├── replicas-patch.yaml # → 后端 2 副本 +│ └── ingress.yaml # → graphrag.plfai.cn +└── README.md +``` + +## 环境差异 + +| 项目 | test | prod | +|------|------|------| +| 命名空间 | `graphrag-test` | `graphrag-prod` | +| 前端域名 | `test-graphrag.plfai.cn` | `graphrag.plfai.cn` | +| 后端域名 | `test-graphrag-backend.plfai.cn` | `graphrag-backend.plfai.cn` | +| 后端副本 | 1 | 2 | +| 资源名前缀 | `test-` | `prod-` | +| ConfigMap / Secret | 各自独立 | 各自独立 | + +## 架构 + +``` +浏览器 → Ingress + ├── test-graphrag.plfai.cn ──→ graphrag-test/frontend:80 + ├── test-graphrag-backend.plfai.cn ──→ graphrag-test/backend:8000 + ├── graphrag.plfai.cn ──→ graphrag-prod/frontend:80 + └── graphrag-backend.plfai.cn ──→ graphrag-prod/backend:8000 +``` + +## 部署 + +### 1. 配置密钥 + +```bash +# 编辑 test 环境 Secret +vim k8s/overlays/test/kustomization.yaml +# 修改 secretGenerator.literals 中的实际 Key + +# 编辑 prod 环境 Secret +vim k8s/overlays/prod/kustomization.yaml +# 修改 secretGenerator.literals 中的实际 Key +``` + +### 2. 构建镜像 + +```bash +# 在项目根目录执行 +docker build -t graphrag-backend:latest -f backend/Dockerfile . +docker build -t graphrag-frontend:latest -f frontend/Dockerfile . +``` + +### 3. 部署 + +```bash +# 部署测试环境 +kubectl apply -k k8s/overlays/test + +# 部署生产环境 +kubectl apply -k k8s/overlays/prod +``` + +### 4. 预览生成的 YAML(不实际部署) + +```bash +kubectl kustomize k8s/overlays/test +kubectl kustomize k8s/overlays/prod +``` + +### 5. 验证 + +```bash +# 检查所有 Pod +kubectl -n graphrag-test get pods +kubectl -n graphrag-prod get pods + +# 检查 Ingress +kubectl -n graphrag-test get ingress +kubectl -n graphrag-prod get ingress + +# 测试 +curl -s https://test-graphrag.plfai.cn # 前端 +curl -s https://test-graphrag-backend.plfai.cn/api/v1/health # 后端 +# 生产 +curl -s https://graphrag.plfai.cn # 前端 +curl -s https://graphrag-backend.plfai.cn/api/v1/health # 后端 +``` + +### 6. 删除 + +```bash +kubectl delete -k k8s/overlays/test +kubectl delete -k k8s/overlays/prod +``` + +## 注意事项 + +1. **Secret 安全**:Kustomize 的 `secretGenerator` 输出为 base64 编码,生产建议配合 External Secrets Operator 或 Sealed Secrets +2. **PVC**:base 中 PVC 使用 `ReadWriteOnce`,prod 后端 2 副本需确保存储类支持,或改用对象存储 +3. **Ingress Controller**:集群需已安装 nginx-ingress-controller +4. **TLS**:当前 Ingress 未配置 TLS,生产环境请添加 cert-manager 注解 diff --git a/k8s/base/backend-deployment.yaml b/k8s/base/backend-deployment.yaml new file mode 100644 index 0000000..925b98b --- /dev/null +++ b/k8s/base/backend-deployment.yaml @@ -0,0 +1,62 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: backend + labels: + app: graphrag + component: backend +spec: + replicas: 1 + selector: + matchLabels: + app: graphrag + component: backend + template: + metadata: + labels: + app: graphrag + component: backend + spec: + containers: + - name: backend + image: graphrag-backend:latest + imagePullPolicy: IfNotPresent + ports: + - containerPort: 8000 + protocol: TCP + envFrom: + - configMapRef: + name: graphrag-config + - secretRef: + name: graphrag-secrets + volumeMounts: + - name: backend-data + mountPath: /app/backend/data + - name: mineru-output + mountPath: /app/mineru_mvp/output + resources: + requests: + memory: "512Mi" + cpu: "250m" + limits: + memory: "2Gi" + cpu: "1000m" + livenessProbe: + httpGet: + path: /api/v1/health + port: 8000 + initialDelaySeconds: 20 + periodSeconds: 30 + readinessProbe: + httpGet: + path: /api/v1/health + port: 8000 + initialDelaySeconds: 10 + periodSeconds: 10 + volumes: + - name: backend-data + persistentVolumeClaim: + claimName: backend-data-pvc + - name: mineru-output + persistentVolumeClaim: + claimName: mineru-output-pvc diff --git a/k8s/base/backend-service.yaml b/k8s/base/backend-service.yaml new file mode 100644 index 0000000..5628100 --- /dev/null +++ b/k8s/base/backend-service.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + name: backend + labels: + app: graphrag + component: backend +spec: + type: ClusterIP + selector: + app: graphrag + component: backend + ports: + - name: http + port: 8000 + targetPort: 8000 + protocol: TCP diff --git a/k8s/base/frontend-deployment.yaml b/k8s/base/frontend-deployment.yaml new file mode 100644 index 0000000..5467c77 --- /dev/null +++ b/k8s/base/frontend-deployment.yaml @@ -0,0 +1,45 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: frontend + labels: + app: graphrag + component: frontend +spec: + replicas: 1 + selector: + matchLabels: + app: graphrag + component: frontend + template: + metadata: + labels: + app: graphrag + component: frontend + spec: + containers: + - name: frontend + image: graphrag-frontend:latest + imagePullPolicy: IfNotPresent + ports: + - containerPort: 80 + protocol: TCP + resources: + requests: + memory: "64Mi" + cpu: "100m" + limits: + memory: "256Mi" + cpu: "500m" + livenessProbe: + httpGet: + path: / + port: 80 + initialDelaySeconds: 5 + periodSeconds: 30 + readinessProbe: + httpGet: + path: / + port: 80 + initialDelaySeconds: 3 + periodSeconds: 10 diff --git a/k8s/base/frontend-service.yaml b/k8s/base/frontend-service.yaml new file mode 100644 index 0000000..d16f0b8 --- /dev/null +++ b/k8s/base/frontend-service.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + name: frontend + labels: + app: graphrag + component: frontend +spec: + type: ClusterIP + selector: + app: graphrag + component: frontend + ports: + - name: http + port: 80 + targetPort: 80 + protocol: TCP diff --git a/k8s/base/kustomization.yaml b/k8s/base/kustomization.yaml new file mode 100644 index 0000000..c1b6c79 --- /dev/null +++ b/k8s/base/kustomization.yaml @@ -0,0 +1,9 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - backend-deployment.yaml + - backend-service.yaml + - frontend-deployment.yaml + - frontend-service.yaml + - persistent-volume-claims.yaml diff --git a/k8s/base/persistent-volume-claims.yaml b/k8s/base/persistent-volume-claims.yaml new file mode 100644 index 0000000..09db2fd --- /dev/null +++ b/k8s/base/persistent-volume-claims.yaml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: backend-data-pvc +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Gi +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: mineru-output-pvc +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 5Gi diff --git a/k8s/overlays/prod/ingress.yaml b/k8s/overlays/prod/ingress.yaml new file mode 100644 index 0000000..a15addd --- /dev/null +++ b/k8s/overlays/prod/ingress.yaml @@ -0,0 +1,29 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: graphrag-ingress + annotations: + nginx.ingress.kubernetes.io/proxy-body-size: "200m" + nginx.ingress.kubernetes.io/proxy-read-timeout: "120" +spec: + rules: + - host: graphrag-backend.plfai.cn + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: backend + port: + number: 8000 + - host: graphrag.plfai.cn + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: frontend + port: + number: 80 diff --git a/k8s/overlays/prod/kustomization.yaml b/k8s/overlays/prod/kustomization.yaml new file mode 100644 index 0000000..4563134 --- /dev/null +++ b/k8s/overlays/prod/kustomization.yaml @@ -0,0 +1,29 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: graphrag-prod + +namePrefix: prod- + +resources: + - ../../base + - namespace.yaml + +configMapGenerator: + - name: graphrag-config + behavior: replace + literals: + - DEEPSEEK_BASE_URL=https://api.deepseek.com + - MINERU_PIPELINE=/app/mineru_mvp/pipeline.py + - MINERU_PYTHON=/opt/venv/bin/python + +secretGenerator: + - name: graphrag-secrets + behavior: replace + literals: + - DEEPSEEK_API_KEY=sk-prod-placeholder + - MINERU_API_TOKEN=prod-placeholder + +patches: + - path: ingress.yaml + - path: replicas-patch.yaml diff --git a/k8s/overlays/prod/namespace.yaml b/k8s/overlays/prod/namespace.yaml new file mode 100644 index 0000000..9ae94a6 --- /dev/null +++ b/k8s/overlays/prod/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: graphrag-prod diff --git a/k8s/overlays/prod/replicas-patch.yaml b/k8s/overlays/prod/replicas-patch.yaml new file mode 100644 index 0000000..4889eb2 --- /dev/null +++ b/k8s/overlays/prod/replicas-patch.yaml @@ -0,0 +1,6 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: backend +spec: + replicas: 2 diff --git a/k8s/overlays/test/ingress.yaml b/k8s/overlays/test/ingress.yaml new file mode 100644 index 0000000..15aa5ee --- /dev/null +++ b/k8s/overlays/test/ingress.yaml @@ -0,0 +1,29 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: graphrag-ingress + annotations: + nginx.ingress.kubernetes.io/proxy-body-size: "200m" + nginx.ingress.kubernetes.io/proxy-read-timeout: "120" +spec: + rules: + - host: test-graphrag-backend.plfai.cn + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: backend + port: + number: 8000 + - host: test-graphrag.plfai.cn + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: frontend + port: + number: 80 diff --git a/k8s/overlays/test/kustomization.yaml b/k8s/overlays/test/kustomization.yaml new file mode 100644 index 0000000..3066d8c --- /dev/null +++ b/k8s/overlays/test/kustomization.yaml @@ -0,0 +1,28 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: graphrag-test + +namePrefix: test- + +resources: + - ../../base + - namespace.yaml + +configMapGenerator: + - name: graphrag-config + behavior: replace + literals: + - DEEPSEEK_BASE_URL=https://api.deepseek.com + - MINERU_PIPELINE=/app/mineru_mvp/pipeline.py + - MINERU_PYTHON=/opt/venv/bin/python + +secretGenerator: + - name: graphrag-secrets + behavior: replace + literals: + - DEEPSEEK_API_KEY=sk-test-placeholder + - MINERU_API_TOKEN=test-placeholder + +patches: + - path: ingress.yaml diff --git a/k8s/overlays/test/namespace.yaml b/k8s/overlays/test/namespace.yaml new file mode 100644 index 0000000..766e446 --- /dev/null +++ b/k8s/overlays/test/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: graphrag-test