> For AI agents: the complete documentation index is available at https://www.tteam.icu/llms.txt, the full documentation bundle is available at https://www.tteam.icu/llms-full.txt.

# nginx

## 目录

```tree
nginx
├── config
│   ├── nginx
│   │   ├── conf.d/
│   │   ├── lua/
│   │   │   └── filter_body.lua
│   │   ├── json.log-format
│   │   ├── json.stream-log-format
│   │   ├── websocket-json.log-format
│   │   ├── lua.conf
│   │   └── monitor.conf
│   └── nginx.conf
├── cut_nginx_log.sh
└── setup.sh
```

## 常用命令

```bash
cd /usr/local/nginx/sbin/
./nginx              # 启动
./nginx -s stop      # 停止
./nginx -s quit      # 安全退出
./nginx -s reload    # 重新加载配置文件
ps aux|grep nginx    # 查看 Nginx 进程

# 统计请求 IP 和个数
zcat *.log.gz | jq -r '.remote_addr' | sort | uniq -c | sort -nr
```

## 基本配置

```bash
sendfile on; # html 下的配置，可以直接把文件发给用户，不经过 nginx
listen 80; # 开放端口
server_name 127.0.0.1; # 域名或者 IP
index index.html index.htm; # 默认页面

# root 和 alias 的区别，如需要访问 /usr/local/nginx/html/images，访问的路径是域名加 /images:
# root 的处理结果是: root 路径 + location 路径
location /images {
    root /usr/local/nginx/html;
}
# alias 的处理结果是: 使用 alias 路径替换 location 路径
location /images {
    alias /usr/local/nginx/html/images;
}
include /etc/nginx/conf.d/*.conf; # 写在 http 下，可以读取目录下的多个配置文件，不需要改主文件
```

## 反向代理

最主要的是 `proxy_pass` 这个参数。

```bash
location / {
    proxy_pass http://127.0.0.1:5230/;
    rewrite ^/(.*)$ /$1 break;
    proxy_redirect off;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Upgrade-Insecure-Requests 1;
    proxy_set_header X-Forwarded-Proto https;
}
```

```bash
location /api/ {
    proxy_pass http://127.0.0.1:3000/api/
}
```

在后端的基础上增加路径层数:

```
location / {
    rewrite  ^/proxy/(.*)$ /$1 break;
    proxy_pass http://127.0.0.1:3000/api/
}
```

### 动静分离

可以把静态文件部署在 Nginx 上。

```bash
location ~* \.(gif|jpg|jpeg)${
    root /html;
}
```

```bash
location ^~ /static/ {
    root /webroot/static/;
}
location ~* \.(html|gif|jpg|jpeg|png|css|js|ico)$ {
    root /webroot/res/;
}
```

### 重写(URLRewrite)

```bash
rewrite ^/[0-9]+.html$ /index.html?testParam=$1 break; # $1 表示第一个匹配的字符串
```

![](https://img.tteam.icu/i/2022/08/22/xaqgqs-3.webp)

## 防盗链

写在 location 下:

```bash
valid_referers none 192.168.1.1 # 白名单 IP
if ($invalid_referer) {
    return 403; # 返回错误码
}
rewrite ^/ /403.png break;
```

## Keepalived 高可用

```bash
global_defs {
   router_id LB_102 # 名字
}

vrrp_instance VI_102 { # 名字
    state MASTER  # MASTER 和 BACKUP 表示主备优先级
    interface ens33 # 网卡
    virtual_router_id 51 # ID 需一样
    priority 100 # 优先级
    advert_int 1
    authentication { # 组的账号和密码
        auth_type PASS
        auth_pass 1111
    }
    virtual_ipaddress {
        192.168.8.200 # 虚拟漂移的 IP
    }
}
```

## SSL 证书配置

### HTTP

```bash
server {
    listen 443 ssl;
    server_name localhost;  # 域名
    ssl_certificate xxx.pem; # 证书
    ssl_certificate_key xxx.key; # 密钥
    ssl_session_timeout 5m;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;

    location / {
        proxy_pass http://127.0.0.1:5230/;
    }
}
```

### TCP

```bash
server {
	listen 443 ssl;
	ssl_certificate xxx.pem; #证书
	ssl_certificate_key xxx.key; #秘钥
    ssl_session_timeout 5m;
    ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_prefer_server_ciphers on;
    proxy_pass 127.0.0.1:5230;
}
```

### 自签 SSL 证书

```bash
openssl genrsa -out server.key 2048
openssl req -x509 -new -nodes -key server.key -sha256 -days 3650 -out server.crt
```

:::tip
由于 HTTP 协议默认的端口是 80，而 HTTPS 默认的端口是 443，如果想让 HTTP 的访问跳转到 HTTPS 的访问，可以做如下配置:
:::

```bash
server {
	listen 80;
	server_name ; #域名
	return 301 https://$server_name$request_uri;	
}
```

## 下载目录

```bash
location /data {
    alias   /data/res;  # 定义网站根目录，目录可以是相对路径也可以是绝对路径
    autoindex on; # 开启目录浏览功能
    autoindex_exact_size off; # 关闭详细文件大小统计，让文件大小显示 MB、GB 单位，默认为 b
    autoindex_localtime on; # 开启以服务器本地时区显示文件修改日期
    charset  utf-8,gbk; # 定义站点的默认页
    auth_basic "Please input password"; # 密码访问
    auth_basic_user_file /data/web/passwd;
}
```

### 密码访问


**centos**

```bash
yum install -y httpd-tools
htpasswd -c passwd user
```


**debian**

```bash
apt install -y apache2-utils
htpasswd -c passwd user
```


### 美化下载目录

```bash
sendfile    on;
add_after_body /.autoindex.html;
```

在目录页放入文件，需要把 `autoindex_exact_size` 打开。

::: details 美化文件

```html file="./nginx/autoindex.html" title="autoindex.html"
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>Nginx File Server</title>
    <style>
        :root {
            --bg: #f6f8fa;
            --card: #ffffff;
            --text: #24292f;
            --muted: #6e7781;
            --border: #d0d7de;
            --hover: #f3f4f6;
            --accent: #0969da;
        }

        body.dark {
            --bg: #0d1117;
            --card: #161b22;
            --text: #c9d1d9;
            --muted: #8b949e;
            --border: #30363d;
            --hover: #21262d;
            --accent: #2f81f7;
        }

        body {
            margin: 0;
            background: var(--bg);
            color: var(--text);
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
            display: flex;
            flex-direction: column;
            min-height: 100vh;
            transition: background 0.3s, color 0.3s;
        }

        body > hr {
            display: none;
        }

        .toolbar {
            background: var(--card);
            border-bottom: 1px solid var(--border);
            padding: 14px 18px;
            transition: background 0.3s, border-color 0.3s;
        }

        .toolbar-row {
            display: flex;
            gap: 10px;
            align-items: center;
        }

        .breadcrumb {
            font-size: 13px;
            margin-bottom: 10px;
        }

        .breadcrumb a {
            color: var(--accent);
            text-decoration: none;
        }

        .breadcrumb span {
            color: var(--muted);
            margin: 0 6px;
        }

        .breadcrumb .current {
            color: var(--muted);
        }

        input {
            height: 34px;
            padding: 0 12px;
            border-radius: 8px;
            border: 1px solid var(--border);
            background: var(--card);
            color: var(--text);
            width: 260px;
            transition: background 0.3s, border-color 0.3s, color 0.3s;
        }

        .download-btn {
            display: flex;
            align-items: center;
            justify-content: center;
        }

        .download-btn:hover {
            text-decoration: underline;
        }

        .list {
            max-width: 1400px;
            width: 95%;
            margin: 16px auto;
            background: var(--card);
            border: 1px solid var(--border);
            border-radius: 12px;
            overflow: hidden;
            transition: background 0.3s, border-color 0.3s;
            flex: none;
        }

        .row {
            display: grid;
            grid-template-columns: 1fr 200px 120px;
            gap: 12px;
            align-items: center;
            padding: 10px 16px;
            border-bottom: 1px solid var(--border);
            transition: background 0.3s, border-color 0.3s;
        }

        .row:hover {
            background: var(--hover);
        }

        .row a {
            color: var(--text);
            text-decoration: none;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
        }

        a.folder::before {
            content: "📁 ";
        }

        a.file::before {
            content: "📄 ";
        }

        a.img::before {
            content: "🖼️ ";
        }

        a.code::before {
            content: "📜 ";
        }

        mark {
            background: #fde68a;
            border-radius: 4px;
            padding: 0 2px;
        }

        #theme-toggle {
            cursor: pointer;
            border: 1px solid var(--border);
            border-radius: 6px;
            background: var(--card);
            color: var(--text);
            padding: 4px 8px;
            transition: background 0.3s, color 0.3s, border-color 0.3s;
        }

        /* 页脚固定底部 */
        .footer {
            text-align: center;
            padding: 16px 0;
            font-size: 14px;
            color: var(--muted);
            background: var(--card);
            border-top: 1px solid var(--border);
            margin-top: auto; /* 关键: 把页脚推到底部 */
            transition: background 0.3s, border-color 0.3s, color 0.3s;
        }

        .footer-link {
            color: #3b82f6;
            text-decoration: none;
            margin-left: 4px;
        }

        .footer-link:hover {
            text-decoration: underline;
        }

        /* 默认隐藏自定义界面，仅目录列表页显示 */
        .toolbar, .footer {
            display: none;
        }

        body.is-listing .toolbar,
        body.is-listing .footer {
            display: block;
        }

        body.is-listing .toolbar {
            position: sticky;
            top: 0;
            z-index: 10;
        }
    </style>
</head>
<body>

<div class="toolbar">
    <div class="breadcrumb" id="breadcrumb"></div>
    <div class="toolbar-row">
        <input id="search" placeholder="🔍 搜索文件(支持多关键词)">
        <button id="theme-toggle">🌓</button>
    </div>
</div>

<script>
    // 删除 nginx autoindex 标题
    document.querySelectorAll('h1').forEach(h => h.remove());

    // 文件浏览器逻辑
    const MONTH = {
        Jan: '01',
        Feb: '02',
        Mar: '03',
        Apr: '04',
        May: '05',
        Jun: '06',
        Jul: '07',
        Aug: '08',
        Sep: '09',
        Oct: '10',
        Nov: '11',
        Dec: '12'
    };

    function getType(name) {
        if (name.endsWith('/')) return 'folder';
        const ext = name.split('.').pop().toLowerCase();
        if (['png', 'jpg', 'jpeg', 'gif', 'svg', 'bmp'].includes(ext)) return 'img';
        if (['js', 'ts', 'go', 'py', 'sh', 'json', 'yaml', 'yml', 'md', 'html', 'css', 'conf', 'ini', 'log'].includes(ext)) return 'code';
        return 'file';
    }

    function formatSize(bytes) {
        const n = Number(bytes);
        if (!n || isNaN(n)) return '';
        if (n >= 1024 ** 3) return (n / 1024 ** 3).toFixed(2) + ' G';
        if (n >= 1024 ** 2) return (n / 1024 ** 2).toFixed(2) + ' M';
        return (n / 1024).toFixed(2) + ' K';
    }

    const rows = [];

    function transform() {
        const pre = document.querySelector('pre');
        if (!pre) return;
        // 是目录列表页，显示自定义界面
        document.body.classList.add('is-listing');
        const list = document.createElement('div');
        list.className = 'list';
        let row;
        pre.childNodes.forEach(node => {
            if (node.nodeType === 1) {
                row = document.createElement('div');
                row.className = 'row';
                const fileType = getType(node.text);
                const a = document.createElement('a');
                a.href = node.href;
                a.textContent = node.text;
                a.className = fileType;
                // 下载按钮 - 仅对非文件夹显示
                if (fileType !== 'folder') {
                    const btn = document.createElement('span');
                    btn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="currentColor" style="color: var(--text); vertical-align: middle;"><path d="M12 16l-5-5 1.4-1.45 2.6 2.6V4h2v8.15l2.6-2.6L17 11l-5 5zm-6 4q-.825 0-1.412-.587T4 18v-3h2v3h12v-3h2v3q0 .825-.588 1.413T18 20z"/></svg>';
                    const downloadLink = document.createElement('a');
                    downloadLink.href = node.href;
                    downloadLink.className = 'download-btn';
                    downloadLink.setAttribute('download', '');
                    downloadLink.style.textDecoration = 'none';
                    downloadLink.style.marginLeft = '6px';
                    downloadLink.style.display = 'inline-flex';
                    downloadLink.style.alignItems = 'center';
                    // 防止触发行点击
                    downloadLink.addEventListener('click', e => e.stopPropagation());
                    downloadLink.appendChild(btn);
                    a.appendChild(downloadLink);
                }
                row.appendChild(a);
                list.appendChild(row);
                row._name = node.text.toLowerCase();
                rows.push(row);
            }
            if (node.nodeType === 3 && row) {
                const text = node.nodeValue.replace(/\s+/g, ' ').trim();
                if (!text) return;
                const p = text.split(' ');
                if (p.length < 2) return;
                const [day, mon, year] = p[0].split('-');
                const time = p[1];
                const date = `${year}-${MONTH[mon] || mon}-${day} ${time}`;
                let size = '';
                if (p[2] && p[2] !== '-') {
                    size = /^\d+$/.test(p[2]) ? formatSize(p[2]) : p[2];
                }
                const d = document.createElement('div');
                d.textContent = date;
                const s = document.createElement('div');
                s.textContent = size;
                row.append(d, s);
            }
        });
        pre.remove();
        document.body.appendChild(list);
        renderBreadcrumb();
    }

    // 搜索功能
    document.getElementById('search').addEventListener('input', e => {
        const keys = e.target.value.toLowerCase().split(/\s+/).filter(Boolean);
        rows.forEach(r => {
            const hit = keys.every(k => r._name.includes(k));
            r.style.display = hit ? 'grid' : 'none';
            const a = r.querySelector('a');
            if (!keys.length) a.innerHTML = a.textContent;
            else a.innerHTML = a.textContent.replace(new RegExp(`(${keys.join('|')})`, 'ig'), '<mark>$1</mark>');
        });
    });

    // 面包屑
    function renderBreadcrumb() {
        const el = document.getElementById('breadcrumb');
        const parts = decodeURIComponent(location.pathname).split('/').filter(Boolean);
        let html = `<a href="/">🏠 root</a>`;
        let acc = '';
        parts.forEach((p, i) => {
            acc += '/' + p;
            html += '<span>/</span>';
            html += i === parts.length - 1 ? `<span class="current">${p}</span>` : `<a href="${acc}/">${p}</a>`;
        });
        el.innerHTML = html;
    }

    transform();

    // 三模式主题按钮
    const themeToggle = document.getElementById('theme-toggle');
    const darkQuery = window.matchMedia('(prefers-color-scheme: dark)');

    let mode = localStorage.getItem('theme') || 'auto'; // dark | light | auto

    function updateButtonIcon(m) {
        if (m === 'dark') themeToggle.textContent = '🌙';
        else if (m === 'light') themeToggle.textContent = '☀️';
        else themeToggle.textContent = '🌓';
    }

    function applyTheme(m) {
        if (m === 'dark') document.body.classList.add('dark');
        else if (m === 'light') document.body.classList.remove('dark');
        else document.body.classList.toggle('dark', darkQuery.matches);
        updateButtonIcon(m);
    }

    // 初始化主题
    applyTheme(mode);

    // 系统主题变化(仅 auto 生效)
    darkQuery.addEventListener('change', e => {
        if (mode === 'auto') applyTheme('auto');
    });

    // 按钮循环切换模式: dark → light → auto → dark
    themeToggle.addEventListener('click', () => {
        if (mode === 'dark') mode = 'light';
        else if (mode === 'light') mode = 'auto';
        else mode = 'dark';
        applyTheme(mode);
        localStorage.setItem('theme', mode);
    });
</script>

<!-- 页脚固定底部 -->
<footer class="footer">
    <span>© 2026 <a href="https://github.com/buyfakett" target="_blank" class="footer-link">buyfakett</a>. All rights reserved.</span>
</footer>

</body>
</html>

```

:::

### 隐藏版本号

```bash
# 不显示 openrestry 版本及信息
server_tokens off;
more_clear_headers 'Server';
```

## 伪造请求头

:::tip
当我们需要更换域名时，可以使用这个配置(在不改变原来 Nginx 的情况下)。

当我们修改了任何一个 `header` 参数，其他 `header` 参数就不会生效，需要重新设置。
:::

```bash
proxy_set_header Host xxx.com;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
```

## mirror 指令

### nginx\_http\_mirror\_module 模块特性

利用 mirror 模块，可以将线上实时流量拷贝至其他环境同时不影响源站请求的响应，因为 Nginx 会丢弃 mirror 的响应。

**mirror 模块可用于以下几个场景:**

- 通过预生产环境测试来观察新系统对生产环境流量的处理能力
- 复制请求日志以进行安全分析
- 如用户行为日志，客户和自己都需要一份

**将生产环境的流量拷贝到预上线环境或测试环境的好处:**

- 可以验证功能是否正常，以及服务的性能
- 用真实有效的流量请求去验证，又不用造数据，不影响线上正常访问
- 这跟灰度发布还不太一样，镜像流量不会影响真实流量
- 可以用来排查线上问题
- 重构，假如服务做了重构，这也是一种测试方式

Nginx 的流量镜像是**只复制镜像**，**发送到配置好的后端**，但是**后端响应返回到 Nginx 之后，Nginx 是自动丢弃掉的**，这个特性就**保证了镜像后端的任何处理不会影响到正常客户端的请求**。

### 流量镜像配置

mirror 中不支持配置 access\_log，解决方法: mirror-location 跳转到 server，在 server 中配置 access\_log。


**基础配置**

```bash file="./nginx/mirror.conf"
server {
    listen       80;
    server_name 192.168.1.1;

    location = /mirror1 {
        internal;
        #### address1 ####
        proxy_set_header Host mirror1.com;
        proxy_pass http://192.168.1.1:10001/api/service/list;
    }

    location = /mirror2 {
        internal;
        #### address2 ####
        proxy_set_header Host mirror2.com;
        proxy_pass http://192.168.1.1:10002/api/service/list;
    }

    location /api/service/list {
        access_log /data/logs/nginx/json_test_to_mirror.log json;
        mirror /mirror1;
        mirror /mirror2;
        proxy_pass http://192.168.1.1:8007;
    }

    location / {
        access_log /data/logs/nginx/json_test.log json;
        proxy_pass http://192.168.1.1:8007;
    }

}
server {
    # server没法设置为内部
    listen 172.168.1.58:10001;

    location / {
        internal;
        access_log /data/logs/nginx/json_testMirror1.log json;
        proxy_pass http://192.168.1.1:8008;
    }
 
}
server {
    # server没法设置为内部
    listen 192.168.1.1:10002;

    location / {
        internal;
        access_log /data/logs/nginx/json_testMirror2.log json;
        proxy_pass http://192.168.1.1:8009;
    }
 
}
```


**能获取日志配置**

```bash file="./nginx/get_mirror_log.conf"
server {
    listen       80;
    server_name 192.168.1.1;

    location = /mirror1 {
        internal;
        #### address1 ####
        proxy_set_header Host mirror1.com;
        proxy_pass http://127.0.0.1:8008/api/service/list;
    }

    location = /mirror2 {
        internal;
        #### address2 ####
        proxy_set_header Host mirror2.com;
        proxy_pass http://127.0.0.1:8009/api/service/list;
    }

    # 只转发这个接口
    location /api/service/list {
        access_log /data/logs/nginx/json_test_to_mirror.log json;
        mirror /mirror1;
        mirror /mirror2;
        proxy_pass http://127.0.0.1:8007;
    }

    location / {
        access_log /data/logs/nginx/json_test.log json;
        proxy_pass http://192.168.1.1:8007;
    }

}
```


## 常用案例

### 获取请求ip服务

```bash file="./nginx/get_ip.conf" title="get_ip.conf"
server {
	listen 80;
	server_name xxx.top;

	location / {
		access_log /data/logs/nginx/json_ip.log json;
		proxy_set_header Host $http_host;
        proxy_set_header X-Real-Ip $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_pass http://127.0.0.1:9999;
	}
}

server {
	listen 9999;

	location / {
		access_log off;
		# HTTP 响应头中添加 Date
		add_header Date $date_gmt;
		default_type application/json;
		return 200 "{\"ip\":\"$http_X_Real_Ip\"}";
	}
}
```

### 编写逻辑

#### 获取请求参数

使用 `$http_` 变量获取 `header`

使用 `$arg_` 变量获取 `URL` 参数

```lua
# 判断多个参数示例
set $flagts 0;
if ( $arg_aaa ~ "^aaa" ) {
    set $flagts "${flagts}1";
}
if ( $arg_bbb ~ "^bbb" ) {
    set $flagts "${flagts}1";
}
if ( $flagts = "011" ) {
    return 200;
}
```

### 同端口走不同逻辑

```bash
server {
    listen 80;
    server_name xxx.com;
    
    location / {
        return 404;
    }
}
server {
    listen 80 default_server;
    server_name _;
    
    location / {
        return "Hello, World!";
    }
}
```

### AB机房切换

通过 `map + include` 实现 upstream 切换：

```bash title="api.conf"
server {
    listen 80;
    server_name 127.0.0.1;

    location / {
        proxy_pass http://$upstream_api;
    }
}
```

```bash title="map.conf"
map $backend_mode $upstream_api {
    default api_a;
    b       api_b;
}
```

```bash title="upstream.conf"
upstream api_a {
    server 127.0.0.1:10000;
}

upstream api_b {
    server 127.0.0.1:10001;
}
```

:::tip
切换的时候修改`mode.conf`, 配置在一行内任意修改
:::

```bash title="mode.conf"
map "" $backend_mode {default b;}
```
