From 8ef43029d577a03bc6e30f605d49a48f087d606c Mon Sep 17 00:00:00 2001 From: david_bai Date: Fri, 10 Oct 2025 20:49:17 +0800 Subject: [PATCH] fix(deploy+docker+frontend): enforce same-origin via Nginx, disable Next Image optimization in Docker, allow Socket.IO polling fallback, and improve health checks and access info - generate-config.sh: add --with-nginx flag handling; when enabled, set NEXT_PUBLIC_API_URL empty to use same-origin /api and /socket.io; add BACKEND_INTERNAL_URL for SSR/internal fetch; adjust lan-tls HTTPS (8443) and TLS generation policy - deploy.sh: show only valid access URLs when Nginx is enabled (gateway URLs), avoid misleading :3002/:3001 entries - frontend (env/webrtc): return mutable transports [websocket,polling]; use empty signaling server for same-origin; comments in English - frontend (next.config): support NEXT_IMAGE_UNOPTIMIZED to turn off image optimization in Docker - frontend (health): prefer BACKEND_INTERNAL_URL for internal health checks, fallback to public URL/localhost - docker-compose + Dockerfile(frontend): pass NEXT_IMAGE_UNOPTIMIZED and BACKEND_INTERNAL_URL envs --- deploy.sh | 28 ++++++++++++++---- docker-compose.yml | 3 ++ docker/scripts/generate-config.sh | 36 +++++++++++++++++++---- frontend/Dockerfile | 4 +++ frontend/app/api/health/detailed/route.ts | 15 +++++----- frontend/app/config/environment.ts | 16 +++++----- frontend/lib/webrtcService.ts | 5 +++- frontend/next.config.mjs | 4 ++- 8 files changed, 83 insertions(+), 28 deletions(-) diff --git a/deploy.sh b/deploy.sh index 506afc7..009aa89 100644 --- a/deploy.sh +++ b/deploy.sh @@ -582,8 +582,13 @@ show_deployment_info() { echo " API: http://$domain_name:${backend_port:-3001}" fi elif [[ -n "$public_ip" ]]; then - echo " Public access: http://$public_ip:${frontend_port:-3002}" - echo " API: http://$public_ip:${backend_port:-3001}" + if [[ "$WITH_NGINX" == "true" ]]; then + echo " Public access: http://$public_ip" + echo " API: http://$public_ip" + else + echo " Public access: http://$public_ip:${frontend_port:-3002}" + echo " API: http://$public_ip:${backend_port:-3001}" + fi else # Fallback: show LAN and localhost if public IP is unavailable echo " Frontend: http://localhost:${frontend_port:-3002}" @@ -591,13 +596,24 @@ show_deployment_info() { fi else # Private/basic: localhost + LAN - echo " Frontend: http://localhost:${frontend_port:-3002}" - echo " Backend API: http://localhost:${backend_port:-3001}" + if [[ "$WITH_NGINX" == "true" ]]; then + # When Nginx is enabled and frontend uses same-origin API, prefer the gateway as the primary entry + echo " Frontend: http://localhost" + echo " API: http://localhost" + else + echo " Frontend: http://localhost:${frontend_port:-3002}" + echo " Backend API: http://localhost:${backend_port:-3001}" + fi if [[ -n "$local_ip" ]] && [[ "$local_ip" != "127.0.0.1" ]]; then echo "" echo -e "${BLUE}🌐 LAN Access:${NC}" - echo " Frontend: http://$local_ip:${frontend_port:-3002}" - echo " Backend API: http://$local_ip:${backend_port:-3001}" + if [[ "$WITH_NGINX" == "true" ]]; then + echo " Frontend: http://$local_ip" + echo " API: http://$local_ip" + else + echo " Frontend: http://$local_ip:${frontend_port:-3002}" + echo " Backend API: http://$local_ip:${backend_port:-3001}" + fi fi fi diff --git a/docker-compose.yml b/docker-compose.yml index 103dc15..beccd39 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -62,11 +62,14 @@ services: - NEXT_PUBLIC_TURN_HOST=${NEXT_PUBLIC_TURN_HOST} - NEXT_PUBLIC_TURN_USERNAME=${NEXT_PUBLIC_TURN_USERNAME} - NEXT_PUBLIC_TURN_PASSWORD=${NEXT_PUBLIC_TURN_PASSWORD} + - NEXT_IMAGE_UNOPTIMIZED=${NEXT_IMAGE_UNOPTIMIZED:-true} container_name: privydrop-frontend restart: unless-stopped environment: - NODE_ENV=production - NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL:-http://localhost:3001} + - BACKEND_INTERNAL_URL=${BACKEND_INTERNAL_URL:-http://backend:3001} + - NEXT_IMAGE_UNOPTIMIZED=${NEXT_IMAGE_UNOPTIMIZED:-true} - PORT=3002 - HOSTNAME=0.0.0.0 - NODE_EXTRA_CA_CERTS=/opt/privydrop/ssl/ca-cert.pem diff --git a/docker/scripts/generate-config.sh b/docker/scripts/generate-config.sh index 3a9e071..0807c6e 100755 --- a/docker/scripts/generate-config.sh +++ b/docker/scripts/generate-config.sh @@ -30,6 +30,7 @@ log_error() { # Defaults and global parameters WITH_TURN="${WITH_TURN:-false}" +WITH_NGINX="${WITH_NGINX:-false}" TURN_EXTERNAL_IP_OVERRIDE="" TURN_MIN_PORT_DEFAULT=49152 TURN_MAX_PORT_DEFAULT=49252 @@ -95,6 +96,7 @@ Options: public: Public HTTP + TURN (no domain) full: Domain + HTTPS (Let’s Encrypt) + TURN --with-turn Enable TURN in any mode. Default external-ip=LOCAL_IP + --with-nginx Indicate Nginx reverse proxy is enabled (frontdoor same-origin) --turn-external-ip IP Explicit TURN external-ip; if not set, use PUBLIC_IP, otherwise fallback to LOCAL_IP --turn-port-range R TURN UDP port range, format MIN-MAX; default 49152-49252 --domain DOMAIN Domain (for Nginx/certs/TURN realm, e.g., turn.DOMAIN) @@ -174,6 +176,7 @@ generate_env_file() { # Compute access endpoints for different deployment modes # Support both localhost and host IP for browser access; helpful for Docker direct access or local debugging local cors_origin="http://${LOCAL_IP}:3002,http://localhost:3002" + # API URL exposed to browser. When WITH_NGINX=true, prefer same-origin (empty => use relative /api) local api_url="http://${LOCAL_IP}:3001" local ssl_mode="none" local turn_enabled="false" @@ -188,22 +191,36 @@ generate_env_file() { lan-http) # Allow both dev ports and nginx origins to avoid CORS when --with-nginx is used cors_origin="http://${LOCAL_IP}:3002,http://localhost:3002,http://${LOCAL_IP},http://localhost" - api_url="http://${LOCAL_IP}:3001" + if [[ "$WITH_NGINX" == "true" ]]; then + # Same-origin via Nginx (frontend uses relative /api) + api_url="" + else + api_url="http://${LOCAL_IP}:3001" + fi ;; lan-tls) if [[ "$WEB_HTTPS_ENABLED" == "true" ]]; then HTTPS_LISTEN_PORT="8443" - # Allow common dev origins to avoid CORS when accessing via http://localhost and :3002 - # Keep API URL on HTTPS to go through nginx TLS + # Allow HTTP for local debug; HTTPS is exposed on 8443 by default cors_origin="https://${LOCAL_IP}:${HTTPS_LISTEN_PORT},https://localhost:${HTTPS_LISTEN_PORT},http://${LOCAL_IP},http://${LOCAL_IP}:3002,http://localhost,http://localhost:3002" - api_url="https://${LOCAL_IP}:${HTTPS_LISTEN_PORT}" + if [[ "$WITH_NGINX" == "true" ]]; then + # Same-origin via Nginx (relative /api), TLS is terminated by Nginx + api_url="" + else + api_url="https://${LOCAL_IP}:${HTTPS_LISTEN_PORT}" + fi ssl_mode="self-signed" fi ;; public) local effective_public_host="${PUBLIC_IP:-$LOCAL_IP}" cors_origin="http://${effective_public_host}:3002,http://localhost:3002,http://${effective_public_host},http://localhost" - api_url="http://${effective_public_host}:3001" + if [[ "$WITH_NGINX" == "true" ]]; then + # Same-origin via Nginx gateway + api_url="" + else + api_url="http://${effective_public_host}:3001" + fi turn_enabled="true" ;; full) @@ -287,6 +304,11 @@ HTTP_PORT=80 HTTPS_PORT=${HTTPS_LISTEN_PORT:-443} DOCKER_HTTPS_CONTAINER_PORT=${docker_https_container_port} +# ============================================================================= +# Internal backend URL for server-side (frontend container only) +# ============================================================================= +BACKEND_INTERNAL_URL=http://backend:3001 + # ============================================================================= # Redis config # ============================================================================= @@ -924,6 +946,10 @@ main() { ENABLE_SNI443=false shift ;; + --with-nginx) + WITH_NGINX=true + shift + ;; --enable-web-https) WEB_HTTPS_ENABLED=true HTTPS_LISTEN_PORT="8443" diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 4142b86..800aa3c 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -28,12 +28,14 @@ ARG NEXT_PUBLIC_API_URL ARG NEXT_PUBLIC_TURN_HOST ARG NEXT_PUBLIC_TURN_USERNAME ARG NEXT_PUBLIC_TURN_PASSWORD +ARG NEXT_IMAGE_UNOPTIMIZED # Inject public env vars during frontend build (for client direct access to backend and TURN) ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL} ENV NEXT_PUBLIC_TURN_HOST=${NEXT_PUBLIC_TURN_HOST} ENV NEXT_PUBLIC_TURN_USERNAME=${NEXT_PUBLIC_TURN_USERNAME} ENV NEXT_PUBLIC_TURN_PASSWORD=${NEXT_PUBLIC_TURN_PASSWORD} +ENV NEXT_IMAGE_UNOPTIMIZED=${NEXT_IMAGE_UNOPTIMIZED} # Set environment variables ENV NEXT_TELEMETRY_DISABLED 1 @@ -81,7 +83,9 @@ ARG NEXT_PUBLIC_API_URL ARG NEXT_PUBLIC_TURN_HOST ARG NEXT_PUBLIC_TURN_USERNAME ARG NEXT_PUBLIC_TURN_PASSWORD +ARG NEXT_IMAGE_UNOPTIMIZED ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL} ENV NEXT_PUBLIC_TURN_HOST=${NEXT_PUBLIC_TURN_HOST} ENV NEXT_PUBLIC_TURN_USERNAME=${NEXT_PUBLIC_TURN_USERNAME} ENV NEXT_PUBLIC_TURN_PASSWORD=${NEXT_PUBLIC_TURN_PASSWORD} +ENV NEXT_IMAGE_UNOPTIMIZED=${NEXT_IMAGE_UNOPTIMIZED} diff --git a/frontend/app/api/health/detailed/route.ts b/frontend/app/api/health/detailed/route.ts index 40c6b0e..36679e4 100644 --- a/frontend/app/api/health/detailed/route.ts +++ b/frontend/app/api/health/detailed/route.ts @@ -17,14 +17,14 @@ export async function GET(request: NextRequest) { environment: process.env.NODE_ENV || 'development' }; - // 检查后端API连接 + // Check backend API connectivity const backendHealth = await checkBackendHealth(); if (backendHealth.status !== 'connected') { errors.push('Backend API connection failed'); status = 'degraded'; } - // 系统信息 + // System info snapshot const systemInfo = { runtime: process.env.NEXT_RUNTIME || 'nodejs', nextjs: { @@ -64,10 +64,11 @@ export async function GET(request: NextRequest) { } } -// 检查后端API健康状态 +// Check backend API health async function checkBackendHealth() { try { - const backendUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'; + // Prefer container-internal URL, then public URL, then localhost fallback + const backendUrl = process.env.BACKEND_INTERNAL_URL || process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'; const start = Date.now(); const response = await fetch(`${backendUrl}/health`, { @@ -75,7 +76,7 @@ async function checkBackendHealth() { headers: { 'Content-Type': 'application/json', }, - // 设置超时时间 + // Timeout to avoid long-hanging connections in degraded networks signal: AbortSignal.timeout(5000) }); @@ -101,7 +102,7 @@ async function checkBackendHealth() { } catch (error) { return { status: 'disconnected', - backendUrl: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001', + backendUrl: process.env.BACKEND_INTERNAL_URL || process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001', error: error instanceof Error ? error.message : 'Unknown error' }; } @@ -114,4 +115,4 @@ function formatBytes(bytes: number): string { const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; -} \ No newline at end of file +} diff --git a/frontend/app/config/environment.ts b/frontend/app/config/environment.ts index 1d03b48..2a8f094 100644 --- a/frontend/app/config/environment.ts +++ b/frontend/app/config/environment.ts @@ -1,3 +1,5 @@ +import { ManagerOptions, SocketOptions } from "socket.io-client"; + export const config = { API_URL: process.env.NEXT_PUBLIC_API_URL!, USE_HTTPS: process.env.NODE_ENV !== "development", @@ -54,14 +56,12 @@ export const getIceServers = () => { return iceServers; }; -export const getSocketOptions = () => { - return config.USE_HTTPS - ? { - secure: true, - path: "/socket.io/", - transports: ["websocket"], - } - : undefined; +export const getSocketOptions = (): Partial => { + // Allow polling fallback; do not force "secure" here — protocol will be inferred + return { + path: "/socket.io/", + transports: ["websocket", "polling"], + }; }; export const getFetchOptions = (options: RequestInit = {}): RequestInit => { diff --git a/frontend/lib/webrtcService.ts b/frontend/lib/webrtcService.ts index 5257327..633c348 100644 --- a/frontend/lib/webrtcService.ts +++ b/frontend/lib/webrtcService.ts @@ -18,10 +18,13 @@ class WebRTCService { private static instance: WebRTCService; private constructor() { + const apiUrl = (config.API_URL || "").trim(); + // Use same-origin when API_URL is empty string — socket.io accepts empty string for same-origin + const signalingServer: string = apiUrl.length > 0 ? apiUrl : ""; const webRTCConfig = { iceServers: getIceServers(), socketOptions: getSocketOptions() || {}, - signalingServer: config.API_URL, + signalingServer, }; this.sender = new WebRTC_Initiator(webRTCConfig); diff --git a/frontend/next.config.mjs b/frontend/next.config.mjs index 494ebbf..cdaa853 100644 --- a/frontend/next.config.mjs +++ b/frontend/next.config.mjs @@ -12,6 +12,8 @@ const withMDX = createMDX({ const nextConfig = { pageExtensions: ['ts', 'tsx', 'js', 'jsx', 'md', 'mdx'], images: { + // Disable optimization inside Docker to avoid container loopback fetch failures (502) + unoptimized: process.env.NEXT_IMAGE_UNOPTIMIZED === 'true', remotePatterns: [ { protocol: 'https', @@ -28,4 +30,4 @@ const nextConfig = { }, } -export default withMDX(nextConfig); \ No newline at end of file +export default withMDX(nextConfig);