Nginx配置踩坑记录

Nginx配置踩坑记录

Nginx用了很多年了,从简单的静态站点到复杂的反向代理,踩过不少坑。记录一下常见配置和遇到的问题。

基础配置

配置文件结构

Nginx主配置文件通常位于/etc/nginx/nginx.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
user nginx;
worker_processes auto;

events {
worker_connections 1024;
}

http {
include 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;
error_log /var/log/nginx/error.log;

sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;

include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}

核心指令

指令 上下文 说明 示例
server http 定义虚拟主机 server { ... }
location server 定义请求处理规则 location / { ... }
listen server 监听端口 listen 80;
server_name server 服务器名称 server_name example.com;
root location 网站根目录 root /var/www/html;
proxy_pass location 反向代理地址 proxy_pass http://backend;

虚拟主机配置

单站点配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
server {
listen 80;
server_name example.com www.example.com;

root /var/www/example.com;
index index.html index.htm;

# 静态文件缓存
location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}

# 主location
location / {
try_files $uri $uri/ /index.html;
}

# 安全:禁止访问隐藏文件
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
}

多虚拟主机

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 站点1 - 主站
server {
listen 80;
server_name example.com;
root /var/www/example.com;
index index.html;
}

# 站点2 - 博客
server {
listen 80;
server_name blog.example.com;
root /var/www/blog;
index index.php;

location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}

# 站点3 - API服务
server {
listen 80;
server_name api.example.com;

location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}

反向代理

基础反向代理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
server {
listen 80;
server_name app.example.com;

location / {
proxy_pass http://localhost:8080;

# 设置请求头
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_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
}

WebSocket代理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
server {
listen 80;
server_name ws.example.com;

location /socket.io/ {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;

# WebSocket必需头
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";

proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;

# 长连接设置
proxy_read_timeout 86400;
}
}

代理第三方CDN资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
server {
listen 80;
server_name myapp.example.com;

# 代理LinkedIn头像
location ^~ /third_image/ {
add_header 'Access-Control-Allow-Origin' '$http_origin' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS' always;

proxy_pass https://media.licdn.com/;
proxy_set_header Referer "";
proxy_hide_header X-Frame-Options;
}
}

负载均衡

负载均衡算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
upstream backend {
# 轮询(默认)
server 192.168.1.10:8080 weight=5;
server 192.168.1.11:8080 weight=5;

# IP哈希(会话保持)
ip_hash;

# 最少连接
least_conn;

# 健康检查
server 192.168.1.12:8080 backup;
server 192.168.1.13:8080 down;
}

server {
listen 80;
server_name lb.example.com;

location / {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}

负载均衡策略对比

策略 指令 适用场景
轮询 默认 无状态服务,服务器性能相近
加权轮询 weight= 服务器性能不均衡
IP哈希 ip_hash 需要会话保持
最少连接 least_conn 长连接服务
一致性哈希 hash $request_uri 缓存场景

HTTPS配置

HTTP跳转HTTPS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
server {
listen 80;
server_name example.com www.example.com;

# 所有HTTP请求跳转到HTTPS
return 301 https://$server_name$request_uri;
}

server {
listen 443 ssl http2;
server_name example.com www.example.com;

# SSL证书配置
ssl_certificate /etc/nginx/ssl/example.com.crt;
ssl_certificate_key /etc/nginx/ssl/example.com.key;

# SSL优化配置
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;

root /var/www/example.com;
index index.html;
}

自动升级不安全请求

1
2
3
4
5
6
7
8
9
10
11
12
server {
listen 443 ssl;
server_name example.com;

# 自动将HTTP资源升级为HTTPS
add_header Content-Security-Policy "upgrade-insecure-requests" always;

location / {
root /var/www/example.com;
try_files $uri $uri/ /index.html;
}
}

错误页面处理

自定义错误页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
server {
listen 80;
server_name example.com;

error_page 404 /404.html;
error_page 500 502 503 504 /50x.html;

location = /404.html {
root /var/www/errors;
internal;
}

location = /50x.html {
root /var/www/errors;
internal;
}

location / {
root /var/www/example.com;
try_files $uri $uri/ =404;
}
}

代理后端错误页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
server {
listen 80;
server_name app.example.com;

proxy_intercept_errors on;

location / {
proxy_pass http://backend;
proxy_set_header Host $host;
}

# 自定义404跳转
error_page 404 = @notfound;
location @notfound {
return 301 https://www.baidu.com;
}
}

URL重写与重定向

rewrite指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
server {
listen 80;
server_name example.com;

# 内部代理(URL不变)
location = /play {
proxy_pass http://example.com/play.html;
}

# 301永久重定向(URL变化)
location = /old-page {
return 301 /new-page;
}

# rewrite重写
location /products/ {
rewrite ^/products/(.*)$ /items/$1 last;
}
}

rewrite标志

标志 说明
last 重写后重新匹配location
break 重写后停止处理
redirect 302临时重定向
permanent 301永久重定向

跨域处理(CORS)

基础跨域配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
server {
listen 80;
server_name api.example.com;

location / {
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Requested-With' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Max-Age' 1728000 always;

if ($request_method = 'OPTIONS') {
return 204;
}

proxy_pass http://localhost:3000;
}
}

动态跨域配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
map $http_origin $cors_origin {
default "";
"~^https?://localhost(:[0-9]+)?$" $http_origin;
"~^https?://.*\.example\.com$" $http_origin;
"https://trusted-site.com" $http_origin;
}

server {
listen 80;
server_name api.example.com;

location / {
add_header 'Access-Control-Allow-Origin' $cors_origin always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;

if ($request_method = 'OPTIONS') {
return 204;
}

proxy_pass http://localhost:3000;
}
}

性能优化

静态文件优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
server {
listen 80;
server_name static.example.com;
root /var/www/static;

# 启用gzip压缩
gzip on;
gzip_vary on;
gzip_types text/plain text/css application/json application/javascript text/xml;
gzip_min_length 1000;

# 静态文件缓存
location ~* \.(jpg|jpeg|png|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}

# CSS/JS缓存
location ~* \.(css|js)$ {
expires 30d;
add_header Cache-Control "public";
access_log off;
}

# HTML不缓存
location ~* \.html$ {
expires -1;
add_header Cache-Control "no-store, no-cache, must-revalidate";
}
}

连接优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
events {
worker_processes auto;
worker_connections 4096;
use epoll;
multi_accept on;
}

http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;

keepalive_timeout 30;
keepalive_requests 1000;

client_max_body_size 50m;
client_body_buffer_size 128k;
client_header_buffer_size 1k;

open_file_cache max=1000 inactive=20s;
open_file_cache_valid 30s;
open_file_cache_min_uses 2;
open_file_cache_errors on;
}

安全加固

安全响应头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
server {
listen 443 ssl;
server_name example.com;

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 Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

location / {
root /var/www/example.com;
index index.html;
}
}

访问控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
server {
listen 80;
server_name admin.example.com;

# IP白名单
allow 192.168.1.0/24;
allow 10.0.0.0/8;
deny all;

# 基础认证
location /admin {
auth_basic "Admin Area";
auth_basic_user_file /etc/nginx/.htpasswd;
try_files $uri $uri/ =404;
}

# 限速
location /api/ {
limit_req zone=api_limit burst=10 nodelay;
limit_conn addr_limit 10;
proxy_pass http://backend;
}
}

# 限流配置(http块中)
http {
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
limit_conn_zone $binary_remote_addr zone=addr_limit:10m;
}

坑1:location匹配顺序

Nginx location匹配顺序很重要:

  1. = 精确匹配
  2. ^~ 前缀匹配(匹配后不再检查正则)
  3. ~~* 正则匹配
  4. 普通前缀匹配
1
2
3
4
5
location = /exact {  # 精确匹配 }
location ^~ /static { # 前缀匹配 }
location ~ \.php$ { # 正则匹配 }
location / { # 通用匹配 }
}

坑2:proxy_pass斜杠问题

1
2
3
4
5
6
7
8
9
# 带斜杠 - 替换location部分
location /api/ {
proxy_pass http://backend/; # /api/users -> /users
}

# 不带斜杠 - 保留完整路径
location /api/ {
proxy_pass http://backend; # /api/users -> /api/users
}

坑3:配置重载不生效

有时候改了配置重载没效果:

1
2
3
4
5
6
7
8
# 测试配置语法
nginx -t

# 完整重启
sudo systemctl restart nginx

# 或者强制重载
sudo nginx -s reload

总结

Nginx配置常见坑点:

  1. location匹配顺序要注意,精确匹配放前面
  2. proxy_pass斜杠有无区别大
  3. HTTPS配置要配全,包括中间证书
  4. 静态文件缓存要合理设置过期时间
  5. 跨域配置要注意预检请求(OPTIONS)处理
  6. 配置修改后要restart,reload有时候不生效

Nginx配置看着简单,但细节很多。建议每次修改后都用nginx -t检查语法。