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

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. 進入專案目錄
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)

View File

@@ -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:

View File

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

View File

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

View File

@@ -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()
}

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-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