Nginx:静态站点与反向代理配置 · 文件、依赖与隔离目录至可选上游的完整源码
本篇以Nginx 静态站与代理为例;前置是主配置、conf_prefix、Host/SNI 和上游地址。源码与输入与下文配置共用同一份文件,示例未执行。
前置为路径与代理语义。示例未执行;Nginx配置未由nginx -t检验。
Nginx:静态站点与反向代理配置
本篇以Nginx 静态站与代理为例;前置是主配置、conf_prefix、Host/SNI 和上游地址。源码与输入与下文配置共用同一份文件,示例未执行。
前置为路径与代理语义。示例未执行;Nginx配置未由nginx -t检验。
文件、依赖与隔离目录
Nginx 完整示例位于examples/m5-nginx,主配置nginx.conf、共享conf/http-common.conf和conf/site.conf、三个html文件、可选upstream.mjs及无第三方依赖的package.json全部在仓库。读者自跑时复制到专用/tmp/m5-nginx并创建logs、temp/client、temp/proxy目录;配置的root与alias都指向该目录,不能只改-p却忘了绝对路径。
运行前提是读者已安装Linux上受维护的Nginx Open Source、普通用户能读取公开文件且写专用日志和临时目录,8088和9088未占用;可选上游使用Node.js >=22标准库,无需npm install。所有监听均为回环地址,不依赖容器、云账号、域名DNS或仓库站点后端。TLS另需正确证书,见维护说明。Windows路径与原生能力差异见平台边界,不把本例当作跨平台实测。
{
"name": "m5-nginx-optional-backend",
"private": true,
"type": "module",
"engines": { "node": ">=22" },
"dependencies": {}
}主配置与共享HTTP参数
Nginx 主配置把未知Host交给默认站点404,只为lab.local提供内容。HTTP共享参数显式限制请求体、头读取与空闲时间,JSON日志所有数值也按字符串写入,避免无上游时的短横线破坏JSON。没有外部mime.types依赖,文件扩展映射在示例内完整声明。
worker_processes 1;
pid logs/nginx.pid;
error_log logs/error.log warn;
events { worker_connections 256; }
http {
include conf/http-common.conf;
server {
listen 127.0.0.1:8088 default_server;
server_name _;
return 404;
}
server {
listen 127.0.0.1:8088;
server_name lab.local;
include conf/site.conf;
}
}types {
text/html html;
text/css css;
text/plain txt;
}
default_type application/octet-stream;
server_tokens off;
client_max_body_size 1m;
client_header_timeout 10s;
client_body_timeout 10s;
send_timeout 10s;
keepalive_timeout 15s;
client_body_temp_path temp/client;
proxy_temp_path temp/proxy;
log_format lab escape=json
'{"time":"$time_iso8601","id":"$request_id",'
'"method":"$request_method","uri":"$uri",'
'"status":"$status","bytes":"$body_bytes_sent",'
'"request_time":"$request_time","upstream":"$upstream_addr",'
'"upstream_status":"$upstream_status",'
'"connect_time":"$upstream_connect_time",'
'"header_time":"$upstream_header_time",'
'"response_time":"$upstream_response_time"}';
access_log logs/access.jsonl lab;
upstream lab_backend {
server 127.0.0.1:9088;
keepalive 4;
}单worker和小连接数仅用于阅读,不能作为生产容量建议。logs与temp不在公开root内,避免日志、临时请求体被静态读取。发布文件不含配置、证书、源码或符号链接;配置文件可被运行用户读取,不应由上游请求修改。
站点路由与公开文件
Nginx 静态路由把根站普通缺失文件保留为404,把/app/下的虚拟页面回退到应用壳;固定alias仅公开guide.txt。精确/healthz返回Nginx存活文字,不检查后端。API独立选择代理处理,不依赖文件存在。absolute_redirect默认on,会把这类本地重定向生成绝对Location;示例在server层显式absolute_redirect off,因此/api和/app的308才按约定返回固定相对Location(分别/api/、/app/)。不能只凭return后写的是路径就认定响应头也是相对路径。官方重定向指令
root /tmp/m5-nginx/html;
index index.html;
autoindex off;
absolute_redirect off;
add_header X-Content-Type-Options nosniff always;
add_header X-Request-ID $request_id always;
location = /healthz {
default_type text/plain;
return 200 "nginx alive\n";
}
location = /api { return 308 /api/; }
location ^~ /api/ {
proxy_pass http://lab_backend/;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host lab-backend;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Request-ID $request_id;
proxy_connect_timeout 2s;
proxy_send_timeout 5s;
proxy_read_timeout 5s;
proxy_next_upstream off;
proxy_hide_header X-Request-ID;
}
location = /download/guide.txt {
alias /tmp/m5-nginx/html/guide.txt;
}
location ~ /\. { return 404; }
location = /app { return 308 /app/; }
location /app/ {
try_files $uri $uri/ /app/index.html;
}
location / {
try_files $uri $uri/ =404;
}<!doctype html>
<html lang="zh-CN">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Nginx 请求路径实验</title>
<h1>Nginx 静态入口</h1>
<p>静态页面由 Nginx 提供;API 是独立的可选上游。</p>
<ul>
<li><a href="/app/lesson">应用路由回退</a></li>
<li><a href="/download/guide.txt">精确 alias 文件</a></li>
<li><a href="/api/status?mode=brief">只读 API</a></li>
</ul>
</html><!doctype html>
<html lang="zh-CN">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>应用壳页面</title>
<h1>应用路由回退页</h1>
<p>这是静态应用壳示例,没有 JavaScript 路由器。访问 /app/lesson 预期也得到此页。</p>
</html>Nginx lab: exact alias selects this fixed file.隐藏路径正则不会保护^~ /api/内部,但这个上游不读取文件,只接受固定请求目标。生产中任何对外路径仍要审阅对应处理器的访问控制,不能凭前端菜单未展示就视为私有。
可选上游的完整源码
Nginx 可选上游只提供/status、/status?mode=brief、/fail和/slow,其他目标404,非GET/HEAD请求405。固定7秒延迟用于读者观察5秒代理读超时,参数不能改变延迟或外连目标。响应使用JSON序列化,既不读取用户指定文件也不拼接HTML;故意失败路径不应加入正式服务。
// Optional reader-run backend. Node.js >=22, built-in modules only.
import { createServer } from 'node:http';
const server = createServer((req, res) => {
const requestId = req.headers['x-request-id'] ?? '';
const send = (status, value) => {
const body = JSON.stringify(value) + '\n';
res.writeHead(status, {
'Content-Type': 'application/json; charset=utf-8',
'Content-Length': Buffer.byteLength(body),
'X-Request-ID': requestId,
});
res.end(body);
};
if (req.method !== 'GET' && req.method !== 'HEAD') {
req.resume();
res.setHeader('Allow', 'GET, HEAD');
send(405, { error: 'method not allowed' });
return;
}
// Compare the received request target exactly: no user-controlled filesystem,
// subprocess, destination URL, HTML insertion, or query-driven delay.
if (req.url === '/status' || req.url === '/status?mode=brief') {
send(200, { ok: true, target: req.url, requestId });
} else if (req.url === '/fail') {
send(503, { error: 'planned failure', requestId });
} else if (req.url === '/slow') {
const timer = setTimeout(() => {
if (!res.destroyed) send(200, { ok: true, requestId });
}, 7000);
res.on('close', () => clearTimeout(timer));
} else {
send(404, { error: 'unknown target', requestId });
}
});
server.requestTimeout = 10000;
server.headersTimeout = 5000;
server.keepAliveTimeout = 5000;
server.maxHeadersCount = 64;
server.listen(9088, '127.0.0.1');
for (const signal of ['SIGINT', 'SIGTERM']) {
process.on(signal, () => {
server.close();
server.closeAllConnections();
});
}Node源码的node --check仅检查语法,不import、不启动它,也不证明HTTP超时与连接关闭有效。服务示例应由读者在自己的隔离目录按README自跑,并记录Node和Nginx实际版本。
继续阅读
- 排错
- 知识专题
- 知识专题