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
+16
View File
@@ -582,8 +582,13 @@ show_deployment_info() {
echo " API: http://$domain_name:${backend_port:-3001}"
fi
elif [[ -n "$public_ip" ]]; then
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,15 +596,26 @@ show_deployment_info() {
fi
else
# Private/basic: localhost + LAN
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}"
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
if [[ "$WITH_NGINX" == "true" ]]; then
echo ""
+3
View File
@@ -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
+28 -2
View File
@@ -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 (Lets 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"
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"
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"
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"
+4
View File
@@ -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}
+7 -6
View File
@@ -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'
};
}
+7 -7
View File
@@ -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,
export const getSocketOptions = (): Partial<ManagerOptions & SocketOptions> => {
// Allow polling fallback; do not force "secure" here — protocol will be inferred
return {
path: "/socket.io/",
transports: ["websocket"],
}
: undefined;
transports: ["websocket", "polling"],
};
};
export const getFetchOptions = (options: RequestInit = {}): RequestInit => {
+4 -1
View File
@@ -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);
+2
View File
@@ -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',