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
This commit is contained in:
david_bai
2025-10-10 20:49:17 +08:00
parent 975f6e74ad
commit 8ef43029d5
8 changed files with 83 additions and 28 deletions
+22 -6
View File
@@ -582,8 +582,13 @@ show_deployment_info() {
echo " API: http://$domain_name:${backend_port:-3001}" echo " API: http://$domain_name:${backend_port:-3001}"
fi fi
elif [[ -n "$public_ip" ]]; then elif [[ -n "$public_ip" ]]; then
echo " Public access: http://$public_ip:${frontend_port:-3002}" if [[ "$WITH_NGINX" == "true" ]]; then
echo " API: http://$public_ip:${backend_port:-3001}" 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 else
# Fallback: show LAN and localhost if public IP is unavailable # Fallback: show LAN and localhost if public IP is unavailable
echo " Frontend: http://localhost:${frontend_port:-3002}" echo " Frontend: http://localhost:${frontend_port:-3002}"
@@ -591,13 +596,24 @@ show_deployment_info() {
fi fi
else else
# Private/basic: localhost + LAN # Private/basic: localhost + LAN
echo " Frontend: http://localhost:${frontend_port:-3002}" if [[ "$WITH_NGINX" == "true" ]]; then
echo " Backend API: http://localhost:${backend_port:-3001}" # 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 if [[ -n "$local_ip" ]] && [[ "$local_ip" != "127.0.0.1" ]]; then
echo "" echo ""
echo -e "${BLUE}🌐 LAN Access:${NC}" echo -e "${BLUE}🌐 LAN Access:${NC}"
echo " Frontend: http://$local_ip:${frontend_port:-3002}" if [[ "$WITH_NGINX" == "true" ]]; then
echo " Backend API: http://$local_ip:${backend_port:-3001}" 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
fi fi
+3
View File
@@ -62,11 +62,14 @@ services:
- NEXT_PUBLIC_TURN_HOST=${NEXT_PUBLIC_TURN_HOST} - NEXT_PUBLIC_TURN_HOST=${NEXT_PUBLIC_TURN_HOST}
- NEXT_PUBLIC_TURN_USERNAME=${NEXT_PUBLIC_TURN_USERNAME} - NEXT_PUBLIC_TURN_USERNAME=${NEXT_PUBLIC_TURN_USERNAME}
- NEXT_PUBLIC_TURN_PASSWORD=${NEXT_PUBLIC_TURN_PASSWORD} - NEXT_PUBLIC_TURN_PASSWORD=${NEXT_PUBLIC_TURN_PASSWORD}
- NEXT_IMAGE_UNOPTIMIZED=${NEXT_IMAGE_UNOPTIMIZED:-true}
container_name: privydrop-frontend container_name: privydrop-frontend
restart: unless-stopped restart: unless-stopped
environment: environment:
- NODE_ENV=production - NODE_ENV=production
- NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL:-http://localhost:3001} - 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 - PORT=3002
- HOSTNAME=0.0.0.0 - HOSTNAME=0.0.0.0
- NODE_EXTRA_CA_CERTS=/opt/privydrop/ssl/ca-cert.pem - NODE_EXTRA_CA_CERTS=/opt/privydrop/ssl/ca-cert.pem
+31 -5
View File
@@ -30,6 +30,7 @@ log_error() {
# Defaults and global parameters # Defaults and global parameters
WITH_TURN="${WITH_TURN:-false}" WITH_TURN="${WITH_TURN:-false}"
WITH_NGINX="${WITH_NGINX:-false}"
TURN_EXTERNAL_IP_OVERRIDE="" TURN_EXTERNAL_IP_OVERRIDE=""
TURN_MIN_PORT_DEFAULT=49152 TURN_MIN_PORT_DEFAULT=49152
TURN_MAX_PORT_DEFAULT=49252 TURN_MAX_PORT_DEFAULT=49252
@@ -95,6 +96,7 @@ Options:
public: Public HTTP + TURN (no domain) public: Public HTTP + TURN (no domain)
full: Domain + HTTPS (Lets Encrypt) + TURN full: Domain + HTTPS (Lets Encrypt) + TURN
--with-turn Enable TURN in any mode. Default external-ip=LOCAL_IP --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-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 --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) --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 # Compute access endpoints for different deployment modes
# Support both localhost and host IP for browser access; helpful for Docker direct access or local debugging # 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" 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 api_url="http://${LOCAL_IP}:3001"
local ssl_mode="none" local ssl_mode="none"
local turn_enabled="false" local turn_enabled="false"
@@ -188,22 +191,36 @@ generate_env_file() {
lan-http) lan-http)
# Allow both dev ports and nginx origins to avoid CORS when --with-nginx is used # 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" 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) lan-tls)
if [[ "$WEB_HTTPS_ENABLED" == "true" ]]; then if [[ "$WEB_HTTPS_ENABLED" == "true" ]]; then
HTTPS_LISTEN_PORT="8443" HTTPS_LISTEN_PORT="8443"
# Allow common dev origins to avoid CORS when accessing via http://localhost and :3002 # Allow HTTP for local debug; HTTPS is exposed on 8443 by default
# Keep API URL on HTTPS to go through nginx TLS
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" 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" ssl_mode="self-signed"
fi fi
;; ;;
public) public)
local effective_public_host="${PUBLIC_IP:-$LOCAL_IP}" local effective_public_host="${PUBLIC_IP:-$LOCAL_IP}"
cors_origin="http://${effective_public_host}:3002,http://localhost:3002,http://${effective_public_host},http://localhost" 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" turn_enabled="true"
;; ;;
full) full)
@@ -287,6 +304,11 @@ HTTP_PORT=80
HTTPS_PORT=${HTTPS_LISTEN_PORT:-443} HTTPS_PORT=${HTTPS_LISTEN_PORT:-443}
DOCKER_HTTPS_CONTAINER_PORT=${docker_https_container_port} 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 # Redis config
# ============================================================================= # =============================================================================
@@ -924,6 +946,10 @@ main() {
ENABLE_SNI443=false ENABLE_SNI443=false
shift shift
;; ;;
--with-nginx)
WITH_NGINX=true
shift
;;
--enable-web-https) --enable-web-https)
WEB_HTTPS_ENABLED=true WEB_HTTPS_ENABLED=true
HTTPS_LISTEN_PORT="8443" HTTPS_LISTEN_PORT="8443"
+4
View File
@@ -28,12 +28,14 @@ ARG NEXT_PUBLIC_API_URL
ARG NEXT_PUBLIC_TURN_HOST ARG NEXT_PUBLIC_TURN_HOST
ARG NEXT_PUBLIC_TURN_USERNAME ARG NEXT_PUBLIC_TURN_USERNAME
ARG NEXT_PUBLIC_TURN_PASSWORD ARG NEXT_PUBLIC_TURN_PASSWORD
ARG NEXT_IMAGE_UNOPTIMIZED
# Inject public env vars during frontend build (for client direct access to backend and TURN) # 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_API_URL=${NEXT_PUBLIC_API_URL}
ENV NEXT_PUBLIC_TURN_HOST=${NEXT_PUBLIC_TURN_HOST} ENV NEXT_PUBLIC_TURN_HOST=${NEXT_PUBLIC_TURN_HOST}
ENV NEXT_PUBLIC_TURN_USERNAME=${NEXT_PUBLIC_TURN_USERNAME} ENV NEXT_PUBLIC_TURN_USERNAME=${NEXT_PUBLIC_TURN_USERNAME}
ENV NEXT_PUBLIC_TURN_PASSWORD=${NEXT_PUBLIC_TURN_PASSWORD} ENV NEXT_PUBLIC_TURN_PASSWORD=${NEXT_PUBLIC_TURN_PASSWORD}
ENV NEXT_IMAGE_UNOPTIMIZED=${NEXT_IMAGE_UNOPTIMIZED}
# Set environment variables # Set environment variables
ENV NEXT_TELEMETRY_DISABLED 1 ENV NEXT_TELEMETRY_DISABLED 1
@@ -81,7 +83,9 @@ ARG NEXT_PUBLIC_API_URL
ARG NEXT_PUBLIC_TURN_HOST ARG NEXT_PUBLIC_TURN_HOST
ARG NEXT_PUBLIC_TURN_USERNAME ARG NEXT_PUBLIC_TURN_USERNAME
ARG NEXT_PUBLIC_TURN_PASSWORD ARG NEXT_PUBLIC_TURN_PASSWORD
ARG NEXT_IMAGE_UNOPTIMIZED
ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL} ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
ENV NEXT_PUBLIC_TURN_HOST=${NEXT_PUBLIC_TURN_HOST} ENV NEXT_PUBLIC_TURN_HOST=${NEXT_PUBLIC_TURN_HOST}
ENV NEXT_PUBLIC_TURN_USERNAME=${NEXT_PUBLIC_TURN_USERNAME} ENV NEXT_PUBLIC_TURN_USERNAME=${NEXT_PUBLIC_TURN_USERNAME}
ENV NEXT_PUBLIC_TURN_PASSWORD=${NEXT_PUBLIC_TURN_PASSWORD} ENV NEXT_PUBLIC_TURN_PASSWORD=${NEXT_PUBLIC_TURN_PASSWORD}
ENV NEXT_IMAGE_UNOPTIMIZED=${NEXT_IMAGE_UNOPTIMIZED}
+8 -7
View File
@@ -17,14 +17,14 @@ export async function GET(request: NextRequest) {
environment: process.env.NODE_ENV || 'development' environment: process.env.NODE_ENV || 'development'
}; };
// 检查后端API连接 // Check backend API connectivity
const backendHealth = await checkBackendHealth(); const backendHealth = await checkBackendHealth();
if (backendHealth.status !== 'connected') { if (backendHealth.status !== 'connected') {
errors.push('Backend API connection failed'); errors.push('Backend API connection failed');
status = 'degraded'; status = 'degraded';
} }
// 系统信息 // System info snapshot
const systemInfo = { const systemInfo = {
runtime: process.env.NEXT_RUNTIME || 'nodejs', runtime: process.env.NEXT_RUNTIME || 'nodejs',
nextjs: { nextjs: {
@@ -64,10 +64,11 @@ export async function GET(request: NextRequest) {
} }
} }
// 检查后端API健康状态 // Check backend API health
async function checkBackendHealth() { async function checkBackendHealth() {
try { 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 start = Date.now();
const response = await fetch(`${backendUrl}/health`, { const response = await fetch(`${backendUrl}/health`, {
@@ -75,7 +76,7 @@ async function checkBackendHealth() {
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
// 设置超时时间 // Timeout to avoid long-hanging connections in degraded networks
signal: AbortSignal.timeout(5000) signal: AbortSignal.timeout(5000)
}); });
@@ -101,7 +102,7 @@ async function checkBackendHealth() {
} catch (error) { } catch (error) {
return { return {
status: 'disconnected', 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' error: error instanceof Error ? error.message : 'Unknown error'
}; };
} }
@@ -114,4 +115,4 @@ function formatBytes(bytes: number): string {
const sizes = ['B', 'KB', 'MB', 'GB']; const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k)); const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
} }
+8 -8
View File
@@ -1,3 +1,5 @@
import { ManagerOptions, SocketOptions } from "socket.io-client";
export const config = { export const config = {
API_URL: process.env.NEXT_PUBLIC_API_URL!, API_URL: process.env.NEXT_PUBLIC_API_URL!,
USE_HTTPS: process.env.NODE_ENV !== "development", USE_HTTPS: process.env.NODE_ENV !== "development",
@@ -54,14 +56,12 @@ export const getIceServers = () => {
return iceServers; return iceServers;
}; };
export const getSocketOptions = () => { export const getSocketOptions = (): Partial<ManagerOptions & SocketOptions> => {
return config.USE_HTTPS // Allow polling fallback; do not force "secure" here — protocol will be inferred
? { return {
secure: true, path: "/socket.io/",
path: "/socket.io/", transports: ["websocket", "polling"],
transports: ["websocket"], };
}
: undefined;
}; };
export const getFetchOptions = (options: RequestInit = {}): RequestInit => { export const getFetchOptions = (options: RequestInit = {}): RequestInit => {
+4 -1
View File
@@ -18,10 +18,13 @@ class WebRTCService {
private static instance: WebRTCService; private static instance: WebRTCService;
private constructor() { 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 = { const webRTCConfig = {
iceServers: getIceServers(), iceServers: getIceServers(),
socketOptions: getSocketOptions() || {}, socketOptions: getSocketOptions() || {},
signalingServer: config.API_URL, signalingServer,
}; };
this.sender = new WebRTC_Initiator(webRTCConfig); this.sender = new WebRTC_Initiator(webRTCConfig);
+3 -1
View File
@@ -12,6 +12,8 @@ const withMDX = createMDX({
const nextConfig = { const nextConfig = {
pageExtensions: ['ts', 'tsx', 'js', 'jsx', 'md', 'mdx'], pageExtensions: ['ts', 'tsx', 'js', 'jsx', 'md', 'mdx'],
images: { images: {
// Disable optimization inside Docker to avoid container loopback fetch failures (502)
unoptimized: process.env.NEXT_IMAGE_UNOPTIMIZED === 'true',
remotePatterns: [ remotePatterns: [
{ {
protocol: 'https', protocol: 'https',
@@ -28,4 +30,4 @@ const nextConfig = {
}, },
} }
export default withMDX(nextConfig); export default withMDX(nextConfig);