xiaowei-system/skills/devops/nginx/SKILL.md

8.7 KiB
Raw Blame History

tags name description triggers version
devops
nginx Nginx configuration — reverse proxy, SSL/TLS, load balancing, caching, and security hardening.
nginx
反向代理
reverse proxy
负载均衡
ssl
letsencrypt
certbot
1.0.0

Nginx

Directory Layout (Debian/Ubuntu)

/etc/nginx/
  nginx.conf           # main config
  sites-available/     # site configs
  sites-enabled/       # enabled sites (symlinks)
  conf.d/              # extra config snippets
  snippets/            # reusable config snippets
  ssl/                 # SSL certificates

Basic Site Config

server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;
}

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

    ssl_certificate     /etc/ssl/certs/example.com.crt;
    ssl_certificate_key /etc/ssl/private/example.com.key;
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
    ssl_prefer_server_ciphers off;

    root /var/www/html;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }
}

Reverse Proxy (API / Web App)

server {
    listen 80;
    server_name api.example.com;
    return 301 https://$host$request_uri;
}

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

    ssl_certificate     /etc/ssl/certs/api.example.com.crt;
    ssl_certificate_key /etc/ssl/private/api.example.com.key;

    # Reverse proxy to local app
    location / {
        proxy_pass         http://127.0.0.1:3000;
        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-Port  $server_port;

        # Timeouts
        proxy_connect_timeout 60s;
        proxy_read_timeout    60s;
        proxy_send_timeout    60s;

        # WebSocket support
        proxy_http_version 1.1;
        proxy_set_header Upgrade    $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

Load Balancing (Upstream)

upstream backend {
    least_conn;                      # least connections algorithm
    server 10.0.0.2:3000 weight=3;   # weight param
    server 10.0.0.3:3000 weight=1;
    server 10.0.0.4:3000 backup;      # backup server
}

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

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

Path-based Routing

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

    location /api/ {
        proxy_pass http://127.0.0.1:8000/;  # trailing slash strips /api/
    }

    location /blog/ {
        proxy_pass http://127.0.0.1:8080/;
    }

    location /static/ {
        alias /var/www/static/;
        expires 30d;
        add_header Cache-Control "public, immutable";
    }
}

Rate Limiting

# Define rate limit zone
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;

server {
    location /api/ {
        limit_req zone=api_limit burst=20 nodelay;
        proxy_pass http://127.0.0.1:8000;
    }
}

Caching

proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=api_cache:10m
                 max_size=1g inactive=60m use_temp_path=off;

server {
    location /api/ {
        proxy_cache             api_cache;
        proxy_cache_valid       200 5m;
        proxy_cache_valid       404 1m;
        proxy_cache_use_stale   error timeout updating http_500 http_502 http_503;
        add_header X-Cache-Status $upstream_cache_status;

        proxy_pass http://127.0.0.1:8000;
    }
}

Security Hardening

# Hide nginx version
server_tokens off;

# Security headers
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';" always;

# Block sensitive paths
location ~ /\. {
    deny all;
}

Commands

# Test config (always do this before reload)
nginx -t

# Reload (no downtime)
sudo nginx -s reload

# Stop / restart
sudo nginx -s stop
sudo nginx -s restart

# Full syntax check including included files
nginx -T

Certbot (Let's Encrypt)

# Install
sudo apt install certbot python3-certbot-nginx

# Obtain certificate
sudo certbot --nginx -d example.com -d www.example.com

# Auto-renew (already set up by package, but verify)
sudo certbot renew --dry-run

# Check renewal timer
sudo systemctl list-timers | grep certbot

Troubleshooting

Problem Fix
nginx: [emerg] could not build Run nginx -t to see syntax error with line number
connect() failed Backend service not running — check curl localhost:PORT/health
SSL errors Verify cert/key paths, check openssl x509 -in crt -text -noout
502 Bad Gateway Proxy pass endpoint unreachable, check firewall/security groups
Redirect loop proxy_set_header X-Forwarded-Proto mismatch
Slow downloads Check send_timeout, large client header buffers

Access Log Analytics

Custom Log Format

For real-time request analytics, define a custom log_format outside the server block (at the http level or before any server block):

# 在 http 块或 conf 顶部定义
log_format analytics '$http_x_real_ip - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent"';

server {
    ...
    access_log /var/log/nginx/app_access.log analytics;
}

字段说明:

字段 说明 使用场景
$remote_addr 客户端 IP 基础统计
$http_x_real_ip 上游设置的客户端真实 IP 有反向代理时用(如 nginx → gunicorn → app
$http_x_forwarded_for 代理链 IP 列表 多层代理追踪
$request "METHOD /path HTTP/1.1" 路由统计
$status HTTP 状态码 错误率监控
$body_bytes_sent 响应体大小 流量统计
$request_time 请求处理耗时(秒) 性能监控

Python Log Parser (Flask API)

配合 Flask 路由,读取 nginx 或 gunicorn access log 解析统计数据:

import re, os, json
from datetime import datetime

@app.route('/api/stats')
def api_stats():
    access_log = '/var/log/nginx/app_access.log'
    if not os.path.exists(access_log):
        return jsonify({'error': 'no log file'})
    
    with open(access_log) as f:
        lines = f.readlines()
    
    total = len(lines)
    today_str = datetime.now().strftime('%d/%b/%Y')
    today_lines = [l for l in lines if today_str in l]
    
    # 按天统计
    daily = {}
    for line in lines:
        m = re.search(r'\[(\d+)/(\w+)/(\d{4})', line)
        if m:
            day = f'{m.group(3)}-{m.group(2)}-{m.group(1)}'
            daily[day] = daily.get(day, 0) + 1
    
    # 按 API 路径统计
    api_calls = {}
    for line in lines:
        m = re.search(r'"(?:GET|POST|PUT|DELETE) (/\S+)', line)
        if m:
            path = m.group(1)
            api_calls[path] = api_calls.get(path, 0) + 1
    
    return jsonify({
        'total_requests': total,
        'daily': dict(sorted(daily.items())),
        'today': {'requests': len(today_lines)},
        'api_calls': dict(sorted(api_calls.items())),
    })

⚠️ 常见陷阱

  1. $remote_addr 在反向代理下不真实: nginx 代理到 gunicorn 时 $remote_addr 可能是 127.0.0.1。必须在上游(第一个 nginxproxy_set_header X-Real-IP $remote_addr,日志记录用 $http_x_real_ip
  2. gunicorn 自己的 access_log 如果 gunicorn 也配了 --access-logfile,那里记录的是 proxy 到 app 的流量。两套日志要区分用途nginx 日志看外部流量gunicorn 日志看内部请求。
  3. 文件轮转: access_log 文件应当配置 logrotate防止单文件无限增长撑满磁盘。

Useful Snippets

Basic auth protection

auth_basic           "Restricted";
auth_basic_user_file /etc/nginx/.htpasswd;

Generate .htpasswd

# Create (first user)
sudo htpasswd -c /etc/nginx/.htpasswd admin

# Add user
sudo htpasswd /etc/nginx/.htpasswd anotheruser

Force HTTPS

if ($scheme != "https") {
    return 301 https://$host$request_uri;
}

Gzip compression

gzip on;
gzip_types text/plain application/json application/javascript text/css;
gzip_min_length 1000;