This commit is contained in:
beabigegg
2025-09-23 08:27:58 +08:00
parent ed9250db1a
commit 0a89c19fc9
11 changed files with 230 additions and 38 deletions

View File

@@ -62,11 +62,6 @@ COPY --from=frontend-builder /app/frontend/dist ./static
# Create required directories # Create required directories
RUN mkdir -p uploads logs scripts 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 # Set permissions
RUN useradd -m -u 1000 appuser && \ RUN useradd -m -u 1000 appuser && \
chown -R appuser:appuser /app && \ chown -R appuser:appuser /app && \
@@ -82,5 +77,5 @@ EXPOSE 12010
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:12010/api/v1/health || exit 1 CMD curl -f http://localhost:12010/api/v1/health || exit 1
# Start application # Run with Gunicorn for production (supports high concurrency)
CMD ["/app/start.sh"] 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"]

17
Dockerfile.redis Normal file
View File

@@ -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"]

View File

@@ -48,14 +48,20 @@ PANJIT 文件翻譯系統是一個企業級的多語言文件翻譯平台,支
# 1. 進入專案目錄 # 1. 進入專案目錄
cd Document_translator_V2 cd Document_translator_V2
# 2. 建置並啟動所有服務 # 2. 建置並啟動所有服務(強制重建以確保使用最新代碼)
docker-compose up -d docker-compose up -d --build
# 3. 檢查服務狀態 # 3. 檢查服務狀態
docker-compose ps docker-compose ps
# 4. 訪問系統 # 4. 訪問系統
curl http://localhost:12010/api/v1/health curl http://localhost:12010/api/v1/health
# 5. 停止服務
docker-compose down
# 6. 查看日誌
docker-compose logs -f
``` ```
詳細部署說明請參考 [DEPLOYMENT.md](DEPLOYMENT.md) 詳細部署說明請參考 [DEPLOYMENT.md](DEPLOYMENT.md)

View File

@@ -135,9 +135,9 @@ def create_app(config_name=None):
# 創建 Celery 實例 # 創建 Celery 實例
app.celery = make_celery(app) app.celery = make_celery(app)
# 初始化 WebSocket # WebSocket 功能完全禁用
from app.websocket import init_websocket app.logger.info("🔌 [WebSocket] WebSocket 服務已禁用")
app.socketio = init_websocket(app) app.socketio = None
# 註冊 Root 路由(提供 SPA 與基本 API 資訊) # 註冊 Root 路由(提供 SPA 與基本 API 資訊)
try: try:

View File

@@ -439,8 +439,8 @@ class NotificationService:
logger.info(f"資料庫通知已創建: {notification.notification_uuid} for user {user_id}") logger.info(f"資料庫通知已創建: {notification.notification_uuid} for user {user_id}")
# 觸發 WebSocket 推送 # WebSocket 推送已禁用
self._send_websocket_notification(notification) # self._send_websocket_notification(notification)
return notification return notification
@@ -611,16 +611,14 @@ class NotificationService:
def _send_websocket_notification(self, notification: Notification): def _send_websocket_notification(self, notification: Notification):
""" """
通過 WebSocket 發送通知 通過 WebSocket 發送通知 - 已禁用
Args: Args:
notification: 通知對象 notification: 通知對象
""" """
try: # WebSocket 功能已完全禁用
from app.websocket import send_notification_to_user logger.debug(f"WebSocket 推送已禁用,跳過通知: {notification.notification_uuid}")
send_notification_to_user(notification.user_id, notification.to_dict()) pass
except Exception as e:
logger.error(f"WebSocket 推送通知失敗: {e}")
def get_unread_count(self, user_id: int) -> int: def get_unread_count(self, user_id: int) -> int:
""" """

View File

@@ -1,22 +1,27 @@
services: services:
# Redis 服務 (Celery 後端和緩存) # Redis 服務 (Celery 後端和緩存)
redis: redis:
image: redis:7-alpine image: panjit-translator:redis
build:
context: .
dockerfile: Dockerfile.redis
container_name: panjit-translator-redis container_name: panjit-translator-redis
# Redis only for internal network use; no public port exposure # Redis only for internal network use; no public port exposure
volumes: volumes:
- redis_data:/data - redis_data:/data
restart: unless-stopped restart: unless-stopped
command: redis-server --appendonly yes command: redis-server --appendonly yes
networks:
- panjit-translator-network
# 主應用服務 # 主應用服務
app: app:
image: panjit-translator:main
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
container_name: panjit-translator-app container_name: translator-app
ports: # No external port; only Nginx exposes ports
- "12010:12010"
volumes: volumes:
- ./uploads:/app/uploads - ./uploads:/app/uploads
- ./cache:/app/cache - ./cache:/app/cache
@@ -25,19 +30,34 @@ services:
- redis - redis
environment: environment:
- REDIS_URL=redis://redis:6379/0 - 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 restart: unless-stopped
deploy:
resources:
limits:
memory: 1.5G
cpus: '1.0'
reservations:
memory: 512M
cpus: '0.5'
healthcheck: healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:12010/api/v1/health"] test: ["CMD", "curl", "-f", "http://localhost:12010/api/v1/health"]
interval: 30s interval: 30s
timeout: 10s timeout: 10s
retries: 3 retries: 3
start_period: 40s start_period: 40s
networks:
- panjit-translator-network
# Celery Worker 服務 # Celery Worker 服務
celery-worker: celery-worker:
build: image: panjit-translator:main
context: .
dockerfile: Dockerfile
container_name: panjit-translator-worker container_name: panjit-translator-worker
volumes: volumes:
- ./uploads:/app/uploads - ./uploads:/app/uploads
@@ -46,22 +66,33 @@ services:
depends_on: depends_on:
- redis - redis
- app - app
pull_policy: never
environment: environment:
- REDIS_URL=redis://redis:6379/0 - REDIS_URL=redis://redis:6379/0
- DEV_MODE=false
- DISABLE_WEBSOCKET=true
restart: unless-stopped 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: healthcheck:
test: ["CMD", "celery", "-A", "celery_app", "inspect", "ping"] test: ["CMD", "celery", "-A", "celery_app", "inspect", "ping"]
interval: 30s interval: 30s
timeout: 10s timeout: 10s
retries: 3 retries: 3
start_period: 40s start_period: 40s
networks:
- panjit-translator-network
# Celery Beat 調度服務 (可選,如果需要定期任務) # Celery Beat 調度服務 (可選,如果需要定期任務)
celery-beat: celery-beat:
build: image: panjit-translator:main
context: .
dockerfile: Dockerfile
container_name: panjit-translator-beat container_name: panjit-translator-beat
volumes: volumes:
- ./uploads:/app/uploads - ./uploads:/app/uploads
@@ -70,15 +101,35 @@ services:
depends_on: depends_on:
- redis - redis
- app - app
pull_policy: never
environment: environment:
- REDIS_URL=redis://redis:6379/0 - REDIS_URL=redis://redis:6379/0
- DEV_MODE=false
- DISABLE_WEBSOCKET=true
restart: unless-stopped restart: unless-stopped
command: celery -A celery_app beat --loglevel=info 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: volumes:
redis_data: redis_data:
driver: local driver: local
networks: networks:
default: panjit-translator-network:
name: panjit-translator-network driver: bridge

View File

@@ -20,6 +20,16 @@ class WebSocketService {
* 初始化並連接 WebSocket * 初始化並連接 WebSocket
*/ */
connect() { 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) { if (this.socket) {
return return
} }
@@ -271,6 +281,15 @@ class WebSocketService {
* @param {string} jobUuid - 任務 UUID * @param {string} jobUuid - 任務 UUID
*/ */
subscribeToJob(jobUuid) { 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) { if (!this.socket || !this.isConnected) {
// 靜默處理,避免控制台警告 // 靜默處理,避免控制台警告
return return
@@ -334,6 +353,15 @@ class WebSocketService {
* @param {Object} data - 事件資料 * @param {Object} data - 事件資料
*/ */
emit(event, 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) { if (this.socket && this.isConnected) {
this.socket.emit(event, data) this.socket.emit(event, data)
} }
@@ -345,6 +373,15 @@ class WebSocketService {
* @param {Function} callback - 回調函數 * @param {Function} callback - 回調函數
*/ */
on(event, 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) { if (this.socket) {
this.socket.on(event, callback) this.socket.on(event, callback)
} }
@@ -401,6 +438,17 @@ export const websocketService = new WebSocketService()
// 自動連接(在需要時) // 自動連接(在需要時)
export const initWebSocket = () => { 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() websocketService.connect()
} }

10
nginx/Dockerfile Normal file
View File

@@ -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;"]

67
nginx/nginx.conf Normal file
View File

@@ -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;
}
}
}

View File

@@ -3,7 +3,7 @@ Flask==3.0.0
Flask-SQLAlchemy==3.1.1 Flask-SQLAlchemy==3.1.1
Flask-Session==0.5.0 Flask-Session==0.5.0
Flask-Cors==4.0.0 Flask-Cors==4.0.0
Flask-SocketIO==5.3.6 # Flask-SocketIO==5.3.6 # Temporarily disabled
Flask-JWT-Extended==4.6.0 Flask-JWT-Extended==4.6.0
# Database # Database
@@ -33,7 +33,7 @@ pysbd==0.3.4
python-dotenv==1.0.0 python-dotenv==1.0.0
Werkzeug==3.0.1 Werkzeug==3.0.1
gunicorn==21.2.0 gunicorn==21.2.0
eventlet==0.33.3 gevent==23.9.1
# Email # Email
Jinja2==3.1.2 Jinja2==3.1.2