diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 0440787..cd7ae42 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -1,141 +1,98 @@ -# PANJIT 文件翻譯系統 - 部署手冊 +# PANJIT 文件翻譯系統 - 部署指南 -## 部署概述 +本指南說明如何在公司內部以 Docker 方式部署系統至生產環境,並提供日常維運要點。 -本系統已完成生產環境準備,包含完整的 Docker 配置和環境設定。系統使用 12010 端口,符合公司端口規範 (12010-12019)。 +## 生產最佳化更新(重要) +- 後端以 Gunicorn + eventlet 啟動(WSGI 入口:`wsgi:app`),提升併發與穩定性。 +- Socket.IO 啟用 Redis message queue(`REDIS_URL`),支援多進程/多副本一致廣播。 +- Celery worker 預設併發提高至 8,可依 CPU 與佇列長度再水平擴展。 +- Redis 僅供容器內部使用,Compose 預設不再對外暴露 6379。 +- 新增套件內根路由提供 SPA 與 `/api`、`/api/health`(`/api/v1/health` 仍由健康檢查藍圖提供)。 ## 系統架構 -``` -┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ -│ Frontend │ │ Backend │ │ Celery │ -│ (Vue.js) │ │ (Flask) │ │ (Worker) │ -│ Port: 12010 │────│ Port: 12010 │────│ 獨立容器 │ -└─────────────────┘ └─────────────────┘ └─────────────────┘ - │ │ │ - └───────────────────────┼───────────────────────┘ - │ - ┌─────────────────┐ - │ Redis │ - │ (Queue/Cache) │ - │ Port: 6379 │ - └─────────────────┘ -``` +- 前端:Vue(Vite 打包後為靜態檔,容器內由後端服務) +- 後端:Flask + Flask-SocketIO(eventlet)+ SQLAlchemy + JWT +- 佇列:Celery(Redis broker/result) +- 資料庫:MySQL(透過 SQLAlchemy 連線池) -**架構優化說明:** -- Flask 應用專注於 Web 服務,不再內建 Celery Worker -- Celery Worker 運行在獨立容器中,提供更好的資源管理 -- 消除了重複的 worker 實例,避免任務重複執行風險 -- 心跳同步更加穩定,減少集群管理開銷 +## 需求與準備 + +- Docker 20.10+、Docker Compose 1.28+ +- 4GB 以上可用記憶體、20GB 以上可用磁碟空間 +- 內部網路可存取 MySQL、LDAP、SMTP、Dify API ## 快速部署 -### 前置需求 -- Docker 20.10+ -- Docker Compose 1.28+ -- 至少 4GB 可用記憶體 -- 至少 20GB 可用磁碟空間 - -### 一鍵部署 ```bash -# 1. 進入專案目錄 +# 1) 進入專案目錄 cd Document_translator_V2 -# 2. 建置並啟動所有服務 +# 2) 建置並啟動(首次執行會自動 build) docker-compose up -d -# 3. 檢查服務狀態 +# 3) 檢查服務狀態 docker-compose ps -# 4. 查看日誌 +# 4) 追蹤應用日誌 docker-compose logs -f app ``` -### 驗證部署 +驗證健康與前端: + ```bash -# 檢查主應用健康狀態 curl http://localhost:12010/api/v1/health - -# 檢查前端訪問 curl http://localhost:12010/ +``` -# 檢查 Celery Worker 狀態 +檢查 Celery worker: + +```bash docker-compose exec celery-worker celery -A celery_app inspect active ``` ## 詳細部署步驟 -### 1. 環境準備 +### 1) 主機檢查 -**檢查系統資源** ```bash -# 檢查記憶體 +# 記憶體 / 磁碟 / 埠使用 free -h - -# 檢查磁碟空間 df -h +netstat -tulpn | grep 12010 || ss -lntp | grep 12010 -# 檢查端口占用 -netstat -tulpn | grep 12010 -``` - -**檢查 Docker 環境** -```bash +# Docker 狀態 docker --version docker-compose --version docker system info ``` -### 2. 配置文件檢查 - -系統已包含完整的生產環境配置: - -**資料庫配置** -- MySQL 主機:mysql.theaken.com:33306 -- 資料庫:db_A060 -- 連接已內建在 Docker 映像中 - -**郵件配置** -- SMTP 服務器:mail.panjit.com.tw -- 端口:25 (無認證) - -**LDAP 配置** -- 服務器:panjit.com.tw -- 認證已配置完成 - -### 3. 建置映像 +### 2) 建置映像 ```bash -# 建置主應用映像 docker build -t panjit-translator:latest . - -# 檢查映像大小 docker images panjit-translator ``` -### 4. 啟動服務 +### 3) 使用 Docker Compose 啟動(推薦) -**使用 Docker Compose (推薦)** ```bash -# 啟動所有服務 docker-compose up -d - -# 分別檢查各服務 docker-compose ps docker-compose logs app docker-compose logs celery-worker docker-compose logs redis ``` -**手動 Docker 部署** +### 4) 純 Docker 佈署(可選) + ```bash -# 啟動 Redis +# 啟動 Redis(內部使用,無需對外開放) docker run -d --name panjit-redis \ - -p 6379:6379 \ -v redis_data:/data \ redis:7-alpine -# 啟動主應用 +# 啟動主應用(Gunicorn + eventlet, 12010) docker run -d --name panjit-translator \ -p 12010:12010 \ -v $(pwd)/uploads:/app/uploads \ @@ -145,212 +102,61 @@ docker run -d --name panjit-translator \ -e REDIS_URL=redis://redis:6379/0 \ panjit-translator:latest -# 啟動 Celery Worker +# 啟動 Celery Worker(可調整並行度) docker run -d --name panjit-worker \ -v $(pwd)/uploads:/app/uploads \ -v $(pwd)/cache:/app/cache \ --link panjit-redis:redis \ -e REDIS_URL=redis://redis:6379/0 \ panjit-translator:latest \ - celery -A celery_app worker --loglevel=info + celery -A celery_app worker --loglevel=info --concurrency=8 ``` -### 5. 服務驗證 +## 驗證與健康檢查 -**健康檢查** ```bash -# API 健康檢查 -curl -f http://localhost:12010/api/v1/health +# 健康檢查(API 藍圖) +curl http://localhost:12010/api/v1/health -# 預期回應: -{ - "status": "healthy", - "timestamp": "2025-09-04T12:00:00Z", - "service": "PANJIT Document Translator API", - "version": "2.0.0" -} +# 前端/靜態頁 +curl http://localhost:12010/ + +# WebSocket(瀏覽器端透過前端頁面測試) ``` -**功能測試** +## 擴展與監控 + ```bash -# 測試 LDAP 連接 (需要有效帳號) -curl -X POST http://localhost:12010/api/v1/auth/login \ - -H "Content-Type: application/json" \ - -d '{"username": "test@panjit.com.tw", "password": "password"}' - -# 測試檔案上傳端點 -curl -f http://localhost:12010/api/v1/files/supported-formats -``` - -**Celery 檢查** -```bash -# 檢查 Worker 狀態 -docker-compose exec celery-worker celery -A celery_app inspect active - -# 檢查佇列狀態 -docker-compose exec celery-worker celery -A celery_app inspect reserved -``` - -## 監控和維護 - -### 日誌管理 -```bash -# 查看實時日誌 -docker-compose logs -f - -# 查看特定服務日誌 -docker-compose logs -f app -docker-compose logs -f celery-worker - -# 查看錯誤日誌 -docker-compose logs app | grep ERROR -``` - -### 性能監控 -```bash -# 檢查容器資源使用 +# 觀察資源 docker stats -# 檢查服務狀態 +# 觀察容器狀態 docker-compose ps -# 檢查健康狀況 -docker inspect panjit-translator-app --format='{{json .State.Health}}' +# 擴展 Celery Worker 副本(例如 3 副本) +docker-compose up -d --scale celery-worker=3 ``` -### 備份和恢復 +## 安全與網路 + ```bash -# 備份上傳檔案 -tar -czf uploads_backup_$(date +%Y%m%d).tar.gz uploads/ - -# 備份快取 -tar -czf cache_backup_$(date +%Y%m%d).tar.gz cache/ - -# 備份日誌 -tar -czf logs_backup_$(date +%Y%m%d).tar.gz logs/ -``` - -## 更新和升級 - -### 更新應用 -```bash -# 1. 備份重要數據 -docker-compose exec app tar -czf /app/backup_$(date +%Y%m%d).tar.gz uploads/ cache/ - -# 2. 停止服務 -docker-compose down - -# 3. 更新代碼和重新建置 -git pull origin main -docker-compose build - -# 4. 啟動服務 -docker-compose up -d - -# 5. 驗證更新 -curl http://localhost:12010/api/v1/health -``` - -### 滾動更新(零停機時間) -```bash -# 1. 建置新映像 -docker build -t panjit-translator:v2.1.0 . - -# 2. 更新 docker-compose.yml 中的映像版本 -# 3. 逐一重啟服務 -docker-compose up -d --no-deps app -docker-compose up -d --no-deps celery-worker -``` - -## 故障排除 - -### 常見問題 - -**1. 容器無法啟動** -```bash -# 檢查端口占用 -sudo netstat -tulpn | grep 12010 - -# 檢查映像是否存在 -docker images panjit-translator - -# 檢查容器日誌 -docker-compose logs app -``` - -**2. 資料庫連接失敗** -```bash -# 測試資料庫連接 -docker-compose exec app python -c " -import pymysql -try: - conn = pymysql.connect( - host='mysql.theaken.com', - port=33306, - user='A060', - password='WLeSCi0yhtc7', - database='db_A060' - ) - print('資料庫連接成功') - conn.close() -except Exception as e: - print(f'資料庫連接失敗: {e}') -" -``` - -**3. Celery Worker 無法啟動** -```bash -# 檢查 Redis 連接 -docker-compose exec app python -c " -import redis -try: - r = redis.Redis.from_url('redis://redis:6379/0') - r.ping() - print('Redis 連接成功') -except Exception as e: - print(f'Redis 連接失敗: {e}') -" - -# 重啟 Worker -docker-compose restart celery-worker -``` - -### 緊急恢復 -```bash -# 完全重置並重啟 -docker-compose down -v -docker-compose up -d - -# 清理未使用的映像和容器 -docker system prune -f - -# 重新建置 -docker-compose build --no-cache -docker-compose up -d -``` - -## 安全配置 - -### 防火牆設定 -```bash -# 開放必要端口 +# 僅開放必要端口(應用 12010) sudo ufw allow 12010/tcp -# 限制 Redis 端口(僅本機) -sudo ufw deny 6379/tcp +# Redis 預設不對外開放;如需遠端維運才開放 6379 並限管理網段 +# sudo ufw allow from <管理網段> to any port 6379 proto tcp ``` -### SSL/TLS 配置 -如需 HTTPS,建議在前端配置 Nginx 反向代理: +如需 HTTPS,建議於前端加 Nginx/Traefik 反向代理: ```nginx server { listen 443 ssl; server_name translator.panjit.com.tw; - + ssl_certificate /path/to/certificate.crt; ssl_certificate_key /path/to/private.key; - + location / { proxy_pass http://localhost:12010; proxy_set_header Host $host; @@ -361,24 +167,81 @@ server { } ``` +## 疑難排解(內部) + +資料庫連線測試(內部憑證): + +```bash +docker-compose exec app python -c " +import pymysql +try: + conn = pymysql.connect( + host='mysql.theaken.com', + port=33306, + user='A060', + password='WLeSCi0yhtc7', + database='db_A060' + ) + print('資料庫連線成功') + conn.close() +except Exception as e: + print(f'資料庫連線失敗: {e}') +" +``` + +Redis 連線測試: + +```bash +docker-compose exec app python -c " +import redis +try: + r = redis.Redis.from_url('redis://redis:6379/0') + r.ping() + print('Redis 連線成功') +except Exception as e: + print(f'Redis 連線失敗: {e}') +" +``` + +重建與清理: + +```bash +docker-compose down -v +docker system prune -f +docker-compose build --no-cache +docker-compose up -d +``` + +## 維運與更新 + +```bash +# 備份重要資料(uploads/cache/logs) +docker-compose exec app tar -czf /app/backup_$(date +%Y%m%d).tar.gz uploads/ cache/ + +# 更新程式碼與重建 +docker-compose down +git pull origin main +docker-compose build +docker-compose up -d + +# 驗證 +curl http://localhost:12010/api/v1/health +``` + +零停機滾動更新(僅針對單一服務重新拉起): + +```bash +docker-compose up -d --no-deps app +docker-compose up -d --no-deps celery-worker +``` + ## 聯繫支援 -如遇到部署問題,請聯繫: - -**PANJIT IT Team** +PANJIT IT Team(內部) - Email: it-support@panjit.com.tw -- 內線電話: 2481 +- 分機: 2481 - 緊急支援: 24/7 待命 -**系統資訊** -- 版本:v2.1.0 (優化版) -- 部署日期:2025-09-11 -- 維護人員:System Administrator -- 更新內容: - - 優化 Celery 架構,移除重複 worker - - 修正 Excel 翻譯輸出格式 - - 清理開發階段檔案 - - 簡化 Docker 映像構建 - --- -*本部署手冊適用於 PANJIT 文件翻譯系統 v2.1.0* \ No newline at end of file +本文件適用於 PANJIT 文件翻譯系統 v2.1.0 + diff --git a/Dockerfile b/Dockerfile index 855787c..8004eaf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -62,9 +62,9 @@ COPY --from=frontend-builder /app/frontend/dist ./static # Create required directories RUN mkdir -p uploads logs scripts -# Create startup script (removed redundant celery worker) +# Create startup script using Gunicorn + eventlet for production RUN echo '#!/bin/bash' > /app/start.sh && \ - echo 'python app.py' >> /app/start.sh && \ + echo 'exec gunicorn -k eventlet -w 1 -b 0.0.0.0:12010 wsgi:app' >> /app/start.sh && \ chmod +x /app/start.sh # Set permissions @@ -83,4 +83,4 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ CMD curl -f http://localhost:12010/api/v1/health || exit 1 # Start application -CMD ["/app/start.sh"] \ No newline at end of file +CMD ["/app/start.sh"] diff --git a/app/__init__.py b/app/__init__.py index 18cb738..09a43f2 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -138,6 +138,13 @@ def create_app(config_name=None): # 初始化 WebSocket from app.websocket import init_websocket app.socketio = init_websocket(app) + + # 註冊 Root 路由(提供 SPA 與基本 API 資訊) + try: + from app.root import root_bp + app.register_blueprint(root_bp) + except Exception as e: + app.logger.warning(f"Root routes not registered: {e}") app.logger.info("Flask application created successfully") return app @@ -215,4 +222,4 @@ def create_default_admin(): print(f"Failed to create default admin: {str(e)}") -# 導入模型在需要時才進行,避免循環導入 \ No newline at end of file +# 導入模型在需要時才進行,避免循環導入 diff --git a/app/root.py b/app/root.py new file mode 100644 index 0000000..8311c10 --- /dev/null +++ b/app/root.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Root routes and static file serving for SPA in production. + +These were originally defined in the top-level app.py. Moving them into the +package allows a clean WSGI entry (wsgi:app) without importing app.py. +""" + +from datetime import datetime +from flask import Blueprint, current_app, send_from_directory + +root_bp = Blueprint('root', __name__) + + +@root_bp.route('/') +def index(): + try: + return send_from_directory('/app/static', 'index.html') + except Exception: + # Fallback API info when frontend is not present + return { + 'application': 'PANJIT Document Translator', + 'version': '1.0.0', + 'status': 'running', + 'api_base_url': '/api/v1', + 'note': 'Frontend files not found, serving API info' + } + + +@root_bp.route('/') +def serve_static(path): + try: + return send_from_directory('/app/static', path) + except Exception: + # SPA fallback + return send_from_directory('/app/static', 'index.html') + + +@root_bp.route('/api') +def api_info(): + return { + 'api_version': 'v1', + 'base_url': '/api/v1', + 'endpoints': { + 'auth': '/api/v1/auth', + 'files': '/api/v1/files', + 'jobs': '/api/v1/jobs', + 'admin': '/api/v1/admin', + 'health': '/api/v1/health' + }, + 'documentation': 'Available endpoints provide RESTful API for document translation' + } + + +@root_bp.route('/api/health') +def health_check(): + # Keep a simple health endpoint here for compatibility + return { + 'status': 'healthy', + 'timestamp': datetime.utcnow().isoformat(), + 'service': 'PANJIT Document Translator API', + 'version': '1.0.0' + }, 200 diff --git a/app/websocket.py b/app/websocket.py index 48aebea..47a808b 100644 --- a/app/websocket.py +++ b/app/websocket.py @@ -8,6 +8,7 @@ Created: 2024-01-28 Modified: 2024-01-28 """ +import os from flask_socketio import SocketIO, emit, join_room, leave_room, disconnect from flask_jwt_extended import decode_token, get_jwt from flask import request @@ -17,7 +18,9 @@ import logging # 初始化 SocketIO socketio = SocketIO( cors_allowed_origins="*", - async_mode='threading', + # Use eventlet for production and enable Redis message queue for multi-process/replica support + async_mode='eventlet', + message_queue=os.getenv('REDIS_URL'), logger=True, engineio_logger=False ) @@ -227,4 +230,4 @@ def init_websocket(app): """ socketio.init_app(app) logger.info("WebSocket initialized") - return socketio \ No newline at end of file + return socketio diff --git a/docker-compose.yml b/docker-compose.yml index 39eb271..d5e7449 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,8 +3,7 @@ services: redis: image: redis:7-alpine container_name: panjit-translator-redis - ports: - - "6379:6379" + # Redis only for internal network use; no public port exposure volumes: - redis_data:/data restart: unless-stopped @@ -50,7 +49,7 @@ services: environment: - REDIS_URL=redis://redis:6379/0 restart: unless-stopped - command: celery -A celery_app worker --loglevel=info --concurrency=4 + command: celery -A celery_app worker --loglevel=info --concurrency=8 healthcheck: test: ["CMD", "celery", "-A", "celery_app", "inspect", "ping"] interval: 30s @@ -82,4 +81,4 @@ volumes: networks: default: - name: panjit-translator-network \ No newline at end of file + name: panjit-translator-network diff --git a/wsgi.py b/wsgi.py new file mode 100644 index 0000000..654489d --- /dev/null +++ b/wsgi.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +WSGI entrypoint for production (Gunicorn) + +This creates the Flask application via the application factory in app/__init__.py, +so we avoid importing the top-level app.py to prevent name clashes with the +package module name "app". +""" + +from app import create_app + +app = create_app() +