Files
novalon-website/nginx-static-production.conf
T
zhangxiang 0b4dff0d37 fix(nginx): fall through to Next.js for root path instead of 403
try_files with $uri/ causes Nginx to treat / as a directory and return
403 when no index.html exists in dist root. Use $uri/index.html so
static directory indexes still work while unmatched routes (including /)
fall through to the Next.js runtime.
2026-08-18 12:53:43 +08:00

226 lines
8.7 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Novalon 生产环境 Nginx 配置(混合渲染)
#
# 架构:
# - 静态资源(/_next/static、/fonts、/uploads、图片)由 Nginx 直接服务并长期缓存。
# - /api/*、/admin/* 代理到 Next.js 运行时(SSR/API Routes)。
# - 页面请求优先命中 /var/www/novalon 中的预构建静态 HTML;未命中时回源到
# Next.js 运行时,用于 ISR 首次渲染、Draft Mode 预览及动态路由。
#
# 前置条件:
# - nextjs_app upstream 指向运行 `next start` 的 Next.js 服务。
# - 默认使用 docker-compose.server.yml 中的服务名 novalon-website:3000。
# - 验证:nginx -t && nginx -s reload
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
use epoll;
multi_accept on;
}
http {
resolver 127.0.0.11 valid=30s ipv6=off;
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
server_tokens off;
client_max_body_size 100M;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
limit_req_zone $binary_remote_addr zone=general:10m rate=100r/s;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
gzip on;
gzip_disable "msie6";
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+text text/javascript;
upstream gitea_app {
server gitea:3000;
}
upstream jenkins_app {
server jenkins:8080;
}
upstream novavis_authority {
server novavis-authority:8080;
}
# Next.js 混合渲染服务(SSR/ISR/API Routes
# 必须与 docker-compose.server.yml 中 Next.js 服务名/端口一致
upstream nextjs_app {
server novalon-website:3000;
}
# WebSocket 连接升级映射(Next.js HMR / 实时功能使用)
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
# 反向代理通用头(用于 /api/*、/admin/* 及 ISR 回源)
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_set_header X-Forwarded-Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
# Novalon 主站 - 静态文件 + SSR/ISR 回源
server {
listen 80;
server_name novalon.cn www.novalon.cn;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://www.novalon.cn$request_uri;
}
}
server {
listen 443 ssl;
http2 on;
server_name novalon.cn www.novalon.cn;
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://www.googletagmanager.com https://ssl.google-analytics.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https://www.google-analytics.com https://ssl.google-analytics.com; font-src 'self'; connect-src 'self' https://www.google-analytics.com https://ssl.google-analytics.com; frame-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self';" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), interest-cohort=()" always;
# 静态文件根目录
root /var/www/novalon;
index index.html;
# 静态资源长期缓存
location /_next/static/ {
expires 1y;
add_header Cache-Control "public, max-age=31536000, immutable";
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
try_files $uri =404;
}
# 字体文件缓存
location ~* \.(ttf|woff|woff2|eot)$ {
expires 1y;
add_header Cache-Control "public, max-age=31536000, immutable";
add_header Access-Control-Allow-Origin "*";
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
try_files $uri =404;
}
# 图片文件缓存
location ~* \.(svg|jpg|jpeg|png|gif|webp|avif|ico)$ {
expires 1y;
add_header Cache-Control "public, max-age=31536000, immutable";
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
try_files $uri =404;
}
# 上传媒体文件(本地存储时落盘到 public/uploads/
location /uploads/ {
expires 1y;
add_header Cache-Control "public, max-age=31536000, immutable";
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
try_files $uri =404;
}
# Let's Encrypt ACME challenge
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
# CMS / 联系表单 / 认证等 API 路由:禁止缓存,代理到 Next.js
location /api/ {
limit_req zone=general burst=50 nodelay;
proxy_pass http://nextjs_app;
proxy_hide_header X-Powered-By;
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
}
# 管理后台与 Draft Mode:代理到 Next.jsCookie 透传以支持 draft 预览
location /admin {
limit_req zone=general burst=20 nodelay;
proxy_pass http://nextjs_app;
proxy_hide_header X-Powered-By;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
}
# Next.js 静态导出的页面路由 + ISR/SSR 回源
# 1) 优先命中已构建的静态 HTML/资源(含目录 index.html
# 2) 未命中时转交给 Next.js 运行时(ISR 首次渲染、Draft 预览、动态路由)
# 注意:不能用 $uri/ 兜底目录,否则根路径会因目录无 index.html 直接 403 而不是回源
location / {
limit_req zone=general burst=20 nodelay;
try_files $uri $uri.html $uri/index.html @nextjs;
}
# ISR/SSR 回源 named location
location @nextjs {
proxy_pass http://nextjs_app;
proxy_hide_header X-Powered-By;
}
# 自定义 404 页面(Nginx 层面未命中且 Next.js 也返回 404 时展示)
error_page 404 /404.html;
# 优化文件传输
sendfile on;
tcp_nopush on;
tcp_nodelay on;
}
include /etc/nginx/conf.d/*.conf;
}