iaun 2 месяцев назад
Родитель
Сommit
032f4d1d69
6 измененных файлов с 226 добавлено и 3 удалено
  1. 39 0
      .dockerignore
  2. 59 0
      DOCKER_DEPLOY.md
  3. 46 0
      Dockerfile
  4. 22 2
      backend/app/__init__.py
  5. 1 1
      backend/config/settings.py
  6. 59 0
      docker-compose.yml

+ 39 - 0
.dockerignore

@@ -0,0 +1,39 @@
+# Git
+.git
+.gitignore
+
+# Python
+__pycache__
+*.py[cod]
+*$py.class
+*.so
+.Python
+.pytest_cache
+*.egg-info
+.eggs
+venv
+backend/venv
+.env
+
+# Node
+node_modules
+frontend/node_modules
+
+# IDE
+.vscode
+.idea
+*.swp
+*.swo
+
+# Build artifacts
+frontend/dist
+*.log
+
+# Local data
+backend/instance
+backend/uploads
+backend/reports
+data/
+
+# Kiro
+.kiro

+ 59 - 0
DOCKER_DEPLOY.md

@@ -0,0 +1,59 @@
+# Docker 部署指南
+
+## 快速启动
+
+```bash
+# 构建并启动所有服务
+docker-compose up -d --build
+
+# 查看日志
+docker-compose logs -f
+
+# 停止服务
+docker-compose down
+```
+
+## 服务说明
+
+| 服务 | 端口 | 说明 |
+|------|------|------|
+| web | 5000 | 前端 + 后端 API |
+| worker | - | Celery 异步任务处理 |
+| redis | 6379 | 消息队列 |
+
+## 单独运行命令
+
+```bash
+# 只运行 Web 服务
+docker run -p 5000:5000 your-image-name
+
+# 只运行 Worker
+docker run your-image-name celery -A celery_worker.celery worker --loglevel=info
+
+# 初始化数据库
+docker-compose exec web python init_db.py
+```
+
+## 环境变量
+
+在项目根目录创建 `.env` 文件:
+
+```env
+SECRET_KEY=your-secret-key-here
+JWT_SECRET_KEY=your-jwt-secret-here
+ENCRYPTION_KEY=your-encryption-key-here
+```
+
+## 数据持久化
+
+数据存储在 `./data/` 目录:
+- `data/instance/` - SQLite 数据库
+- `data/uploads/` - 上传文件
+- `data/reports/` - 生成的报告
+
+## 生产环境建议
+
+1. 使用 PostgreSQL 替代 SQLite
+2. 配置 HTTPS (nginx 反向代理)
+3. 设置强密码的环境变量
+4. 配置日志收集

+ 46 - 0
Dockerfile

@@ -0,0 +1,46 @@
+# Multi-stage build: Frontend + Backend + Worker in one image
+# ============================================================
+
+# Stage 1: Build frontend
+FROM node:20-alpine AS frontend-builder
+
+WORKDIR /app/frontend
+COPY frontend/package.json frontend/yarn.lock ./
+RUN yarn install --frozen-lockfile
+
+COPY frontend/ ./
+RUN yarn build
+
+# Stage 2: Python runtime
+FROM python:3.11-slim AS runtime
+
+# Install system dependencies
+RUN apt-get update && apt-get install -y --no-install-recommends \
+    gcc \
+    libpq-dev \
+    && rm -rf /var/lib/apt/lists/*
+
+WORKDIR /app
+
+# Install Python dependencies
+COPY backend/requirements.txt ./
+RUN pip install --no-cache-dir -r requirements.txt gunicorn
+
+# Copy backend code
+COPY backend/ ./
+
+# Copy frontend build to static folder
+COPY --from=frontend-builder /app/frontend/dist ./static
+
+# Create necessary directories
+RUN mkdir -p uploads reports uploads/scan_data instance
+
+# Environment variables
+ENV FLASK_ENV=production
+ENV PYTHONUNBUFFERED=1
+
+# Expose port
+EXPOSE 5000
+
+# Default command: run web server
+CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "4", "run:app"]

+ 22 - 2
backend/app/__init__.py

@@ -1,5 +1,5 @@
 import os
-from flask import Flask
+from flask import Flask, send_from_directory
 from flask_sqlalchemy import SQLAlchemy
 from flask_migrate import Migrate
 from flask_cors import CORS
@@ -15,7 +15,13 @@ def create_app(config_name=None):
     if config_name is None:
         config_name = os.environ.get('FLASK_ENV', 'development')
     
-    app = Flask(__name__)
+    # Check if static folder exists (for Docker deployment)
+    static_folder = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'static')
+    if os.path.exists(static_folder):
+        app = Flask(__name__, static_folder=static_folder, static_url_path='')
+    else:
+        app = Flask(__name__)
+    
     app.config.from_object(config[config_name])
     
     # Initialize extensions
@@ -35,4 +41,18 @@ def create_app(config_name=None):
     from app.errors import register_error_handlers
     register_error_handlers(app)
     
+    # Serve frontend static files (for Docker deployment)
+    if app.static_folder and os.path.exists(app.static_folder):
+        @app.route('/')
+        def serve_index():
+            return send_from_directory(app.static_folder, 'index.html')
+        
+        @app.route('/<path:path>')
+        def serve_static(path):
+            # If file exists, serve it; otherwise serve index.html for SPA routing
+            file_path = os.path.join(app.static_folder, path)
+            if os.path.exists(file_path) and os.path.isfile(file_path):
+                return send_from_directory(app.static_folder, path)
+            return send_from_directory(app.static_folder, 'index.html')
+    
     return app

+ 1 - 1
backend/config/settings.py

@@ -7,7 +7,7 @@ class Config:
     
     # JWT Configuration
     JWT_SECRET_KEY = os.environ.get('JWT_SECRET_KEY', 'jwt-secret-key-change-in-production')
-    JWT_ACCESS_TOKEN_EXPIRES = timedelta(hours=1)
+    JWT_ACCESS_TOKEN_EXPIRES = timedelta(days=2)
     JWT_REFRESH_TOKEN_EXPIRES = timedelta(days=7)
     
     # SQLAlchemy Configuration

+ 59 - 0
docker-compose.yml

@@ -0,0 +1,59 @@
+version: '3.8'
+
+services:
+  # Redis for Celery
+  redis:
+    image: redis:7-alpine
+    ports:
+      - "6379:6379"
+    volumes:
+      - redis_data:/data
+    healthcheck:
+      test: ["CMD", "redis-cli", "ping"]
+      interval: 10s
+      timeout: 5s
+      retries: 5
+
+  # Web application (Frontend + Backend)
+  web:
+    build: .
+    ports:
+      - "5000:5000"
+    environment:
+      - FLASK_ENV=production
+      - SECRET_KEY=${SECRET_KEY:-change-me-in-production}
+      - JWT_SECRET_KEY=${JWT_SECRET_KEY:-change-jwt-secret}
+      - ENCRYPTION_KEY=${ENCRYPTION_KEY:-change-encryption-key}
+      - DATABASE_URL=sqlite:///instance/prod.db
+      - CELERY_BROKER_URL=redis://redis:6379/0
+      - CELERY_RESULT_BACKEND=redis://redis:6379/1
+    volumes:
+      - ./data/uploads:/app/uploads
+      - ./data/reports:/app/reports
+      - ./data/instance:/app/instance
+    depends_on:
+      redis:
+        condition: service_healthy
+
+  # Celery worker
+  worker:
+    build: .
+    command: celery -A celery_worker.celery worker --loglevel=info
+    environment:
+      - FLASK_ENV=production
+      - SECRET_KEY=${SECRET_KEY:-change-me-in-production}
+      - JWT_SECRET_KEY=${JWT_SECRET_KEY:-change-jwt-secret}
+      - ENCRYPTION_KEY=${ENCRYPTION_KEY:-change-encryption-key}
+      - DATABASE_URL=sqlite:///instance/prod.db
+      - CELERY_BROKER_URL=redis://redis:6379/0
+      - CELERY_RESULT_BACKEND=redis://redis:6379/1
+    volumes:
+      - ./data/uploads:/app/uploads
+      - ./data/reports:/app/reports
+      - ./data/instance:/app/instance
+    depends_on:
+      redis:
+        condition: service_healthy
+
+volumes:
+  redis_data: