diff --git a/Dockerfile b/Dockerfile index 8004eaf..a3b9ffe 100644 --- a/Dockerfile +++ b/Dockerfile @@ -62,11 +62,6 @@ COPY --from=frontend-builder /app/frontend/dist ./static # Create required directories RUN mkdir -p uploads logs scripts -# Create startup script using Gunicorn + eventlet for production -RUN echo '#!/bin/bash' > /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 RUN useradd -m -u 1000 appuser && \ chown -R appuser:appuser /app && \ @@ -82,5 +77,5 @@ EXPOSE 12010 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"] +# Run with Gunicorn for production (supports high concurrency) +CMD ["gunicorn", "--bind", "0.0.0.0:12010", "--worker-class", "gthread", "--workers", "4", "--threads", "8", "--timeout", "600", "--keep-alive", "10", "--max-requests", "2000", "--max-requests-jitter", "200", "--forwarded-allow-ips", "*", "--access-logfile", "-", "wsgi:app"] diff --git a/Dockerfile.redis b/Dockerfile.redis new file mode 100644 index 0000000..49ec1b7 --- /dev/null +++ b/Dockerfile.redis @@ -0,0 +1,17 @@ +# Redis for PANJIT Document Translator +FROM redis:7-alpine + +# Set container labels for identification +LABEL application="panjit-document-translator" +LABEL component="redis" +LABEL version="v2.0" +LABEL maintainer="PANJIT IT Team" + +# Copy custom redis configuration if needed +# COPY redis.conf /usr/local/etc/redis/redis.conf + +# Expose the default Redis port +EXPOSE 6379 + +# Use the default Redis entrypoint +# CMD ["redis-server", "/usr/local/etc/redis/redis.conf"] \ No newline at end of file diff --git a/README.md b/README.md index 66209e3..5dd127a 100644 --- a/README.md +++ b/README.md @@ -48,14 +48,20 @@ PANJIT 文件翻譯系統是一個企業級的多語言文件翻譯平台,支 # 1. 進入專案目錄 cd Document_translator_V2 -# 2. 建置並啟動所有服務 -docker-compose up -d +# 2. 建置並啟動所有服務(強制重建以確保使用最新代碼) +docker-compose up -d --build # 3. 檢查服務狀態 docker-compose ps # 4. 訪問系統 curl http://localhost:12010/api/v1/health + +# 5. 停止服務 +docker-compose down + +# 6. 查看日誌 +docker-compose logs -f ``` 詳細部署說明請參考 [DEPLOYMENT.md](DEPLOYMENT.md) diff --git a/app/__init__.py b/app/__init__.py index 09a43f2..db69c52 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -135,9 +135,9 @@ def create_app(config_name=None): # 創建 Celery 實例 app.celery = make_celery(app) - # 初始化 WebSocket - from app.websocket import init_websocket - app.socketio = init_websocket(app) + # WebSocket 功能完全禁用 + app.logger.info("🔌 [WebSocket] WebSocket 服務已禁用") + app.socketio = None # 註冊 Root 路由(提供 SPA 與基本 API 資訊) try: diff --git a/app/services/notification_service.py b/app/services/notification_service.py index 1086c39..f3360e5 100644 --- a/app/services/notification_service.py +++ b/app/services/notification_service.py @@ -439,8 +439,8 @@ class NotificationService: logger.info(f"資料庫通知已創建: {notification.notification_uuid} for user {user_id}") - # 觸發 WebSocket 推送 - self._send_websocket_notification(notification) + # WebSocket 推送已禁用 + # self._send_websocket_notification(notification) return notification @@ -611,16 +611,14 @@ class NotificationService: def _send_websocket_notification(self, notification: Notification): """ - 通過 WebSocket 發送通知 - + 通過 WebSocket 發送通知 - 已禁用 + Args: notification: 通知對象 """ - try: - from app.websocket import send_notification_to_user - send_notification_to_user(notification.user_id, notification.to_dict()) - except Exception as e: - logger.error(f"WebSocket 推送通知失敗: {e}") + # WebSocket 功能已完全禁用 + logger.debug(f"WebSocket 推送已禁用,跳過通知: {notification.notification_uuid}") + pass def get_unread_count(self, user_id: int) -> int: """ diff --git a/app/websocket.py b/app/websocket.py.disabled similarity index 100% rename from app/websocket.py rename to app/websocket.py.disabled diff --git a/docker-compose.yml b/docker-compose.yml index d5e7449..84dbade 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,22 +1,27 @@ services: # Redis 服務 (Celery 後端和緩存) redis: - image: redis:7-alpine + image: panjit-translator:redis + build: + context: . + dockerfile: Dockerfile.redis container_name: panjit-translator-redis # Redis only for internal network use; no public port exposure volumes: - redis_data:/data restart: unless-stopped command: redis-server --appendonly yes + networks: + - panjit-translator-network # 主應用服務 app: - build: + image: panjit-translator:main + build: context: . dockerfile: Dockerfile - container_name: panjit-translator-app - ports: - - "12010:12010" + container_name: translator-app + # No external port; only Nginx exposes ports volumes: - ./uploads:/app/uploads - ./cache:/app/cache @@ -25,19 +30,34 @@ services: - redis environment: - REDIS_URL=redis://redis:6379/0 + - LDAP_SERVER=panjit.com.tw + - LDAP_PORT=389 + - LDAP_USE_SSL=false + - LDAP_SEARCH_BASE=DC=panjit,DC=com,DC=tw + - LDAP_USER_LOGIN_ATTR=userPrincipalName + - DEV_MODE=false + - DISABLE_WEBSOCKET=true restart: unless-stopped + deploy: + resources: + limits: + memory: 1.5G + cpus: '1.0' + reservations: + memory: 512M + cpus: '0.5' healthcheck: test: ["CMD", "curl", "-f", "http://localhost:12010/api/v1/health"] interval: 30s timeout: 10s retries: 3 start_period: 40s + networks: + - panjit-translator-network # Celery Worker 服務 celery-worker: - build: - context: . - dockerfile: Dockerfile + image: panjit-translator:main container_name: panjit-translator-worker volumes: - ./uploads:/app/uploads @@ -46,22 +66,33 @@ services: depends_on: - redis - app + pull_policy: never environment: - REDIS_URL=redis://redis:6379/0 + - DEV_MODE=false + - DISABLE_WEBSOCKET=true restart: unless-stopped - command: celery -A celery_app worker --loglevel=info --concurrency=8 + command: celery -A celery_app worker --loglevel=info --concurrency=4 --max-memory-per-child=200000 + deploy: + resources: + limits: + memory: 1G + cpus: '0.8' + reservations: + memory: 256M + cpus: '0.3' healthcheck: test: ["CMD", "celery", "-A", "celery_app", "inspect", "ping"] interval: 30s timeout: 10s retries: 3 start_period: 40s + networks: + - panjit-translator-network # Celery Beat 調度服務 (可選,如果需要定期任務) celery-beat: - build: - context: . - dockerfile: Dockerfile + image: panjit-translator:main container_name: panjit-translator-beat volumes: - ./uploads:/app/uploads @@ -70,15 +101,35 @@ services: depends_on: - redis - app + pull_policy: never environment: - REDIS_URL=redis://redis:6379/0 + - DEV_MODE=false + - DISABLE_WEBSOCKET=true restart: unless-stopped command: celery -A celery_app beat --loglevel=info + networks: + - panjit-translator-network + + # Nginx reverse proxy + nginx: + image: panjit-translator:nginx + build: + context: ./nginx + dockerfile: Dockerfile + container_name: panjit-translator-nginx + depends_on: + - app + ports: + - "12010:12010" + restart: unless-stopped + networks: + - panjit-translator-network volumes: redis_data: driver: local networks: - default: - name: panjit-translator-network + panjit-translator-network: + driver: bridge diff --git a/frontend/src/utils/websocket.js b/frontend/src/utils/websocket.js index d281d1e..7bfa66e 100644 --- a/frontend/src/utils/websocket.js +++ b/frontend/src/utils/websocket.js @@ -20,6 +20,16 @@ class WebSocketService { * 初始化並連接 WebSocket */ connect() { + // 檢查 WebSocket 是否被禁用 + const devMode = import.meta.env.VITE_DEV_MODE === 'true' + const isProd = import.meta.env.PROD + const wsDisabled = import.meta.env.VITE_DISABLE_WEBSOCKET === 'true' + + if (!devMode || isProd || wsDisabled) { + console.log('🔌 [WebSocket] WebSocket 連接已禁用,跳過連接') + return + } + if (this.socket) { return } @@ -28,7 +38,7 @@ class WebSocketService { // 建立 Socket.IO 連接 const wsUrl = import.meta.env.VITE_WS_BASE_URL || 'http://127.0.0.1:12010' console.log('🔌 [WebSocket] 嘗試連接到:', wsUrl) - + this.socket = io(wsUrl, { path: '/socket.io/', transports: ['polling'], @@ -271,6 +281,15 @@ class WebSocketService { * @param {string} jobUuid - 任務 UUID */ subscribeToJob(jobUuid) { + // 檢查 WebSocket 是否被禁用 + const devMode = import.meta.env.VITE_DEV_MODE === 'true' + const isProd = import.meta.env.PROD + const wsDisabled = import.meta.env.VITE_DISABLE_WEBSOCKET === 'true' + + if (!devMode || isProd || wsDisabled) { + return // WebSocket 被禁用,靜默返回 + } + if (!this.socket || !this.isConnected) { // 靜默處理,避免控制台警告 return @@ -334,6 +353,15 @@ class WebSocketService { * @param {Object} data - 事件資料 */ emit(event, data) { + // 檢查 WebSocket 是否被禁用 + const devMode = import.meta.env.VITE_DEV_MODE === 'true' + const isProd = import.meta.env.PROD + const wsDisabled = import.meta.env.VITE_DISABLE_WEBSOCKET === 'true' + + if (!devMode || isProd || wsDisabled) { + return // WebSocket 被禁用,靜默返回 + } + if (this.socket && this.isConnected) { this.socket.emit(event, data) } @@ -345,6 +373,15 @@ class WebSocketService { * @param {Function} callback - 回調函數 */ on(event, callback) { + // 檢查 WebSocket 是否被禁用 + const devMode = import.meta.env.VITE_DEV_MODE === 'true' + const isProd = import.meta.env.PROD + const wsDisabled = import.meta.env.VITE_DISABLE_WEBSOCKET === 'true' + + if (!devMode || isProd || wsDisabled) { + return // WebSocket 被禁用,靜默返回 + } + if (this.socket) { this.socket.on(event, callback) } @@ -401,6 +438,17 @@ export const websocketService = new WebSocketService() // 自動連接(在需要時) export const initWebSocket = () => { + // 檢查是否禁用 WebSocket (多種方式) + const devMode = import.meta.env.VITE_DEV_MODE === 'true' + const isProd = import.meta.env.PROD + const wsDisabled = import.meta.env.VITE_DISABLE_WEBSOCKET === 'true' + + if (!devMode || isProd || wsDisabled) { + console.log('🔌 [WebSocket] WebSocket 連接已禁用', { devMode, isProd, wsDisabled }) + return + } + + console.log('🔌 [WebSocket] 嘗試初始化 WebSocket 連接') websocketService.connect() } diff --git a/nginx/Dockerfile b/nginx/Dockerfile new file mode 100644 index 0000000..a76ce32 --- /dev/null +++ b/nginx/Dockerfile @@ -0,0 +1,10 @@ +FROM nginx:1.25-alpine + +# Copy custom nginx configuration +COPY nginx.conf /etc/nginx/nginx.conf + +# Expose port +EXPOSE 12010 + +# Start nginx +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/nginx/nginx.conf b/nginx/nginx.conf new file mode 100644 index 0000000..646c36f --- /dev/null +++ b/nginx/nginx.conf @@ -0,0 +1,67 @@ +user nginx; +worker_processes auto; + +events { + worker_connections 1024; + multi_accept on; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 4096; + + gzip on; + gzip_comp_level 5; + gzip_min_length 1024; + gzip_proxied any; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml; + + upstream app_backend { + server translator-app:12010 max_fails=3 fail_timeout=10s; + keepalive 64; + } + + server { + listen 12010; + server_name _; + + # Adjust for document uploads (can be large) + client_max_body_size 500m; + + # Proxy API requests to Flask/Gunicorn + location /api/ { + proxy_pass http://app_backend; + proxy_http_version 1.1; + 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 600s; # Longer timeout for translation processing + proxy_send_timeout 600s; + proxy_connect_timeout 10s; + proxy_buffering off; # Disable buffering for real-time progress + } + + # All other routes (frontend SPA and static) via backend + location / { + proxy_pass http://app_backend; + proxy_http_version 1.1; + 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; + proxy_send_timeout 120s; + proxy_connect_timeout 5s; + proxy_buffering on; + proxy_buffers 32 32k; + proxy_busy_buffers_size 64k; + } + } +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index e036ce6..27acede 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ Flask==3.0.0 Flask-SQLAlchemy==3.1.1 Flask-Session==0.5.0 Flask-Cors==4.0.0 -Flask-SocketIO==5.3.6 +# Flask-SocketIO==5.3.6 # Temporarily disabled Flask-JWT-Extended==4.6.0 # Database @@ -33,7 +33,7 @@ pysbd==0.3.4 python-dotenv==1.0.0 Werkzeug==3.0.1 gunicorn==21.2.0 -eventlet==0.33.3 +gevent==23.9.1 # Email Jinja2==3.1.2