自建博客 Nginx 基本配置

从一个 Nginx 配置问题学到的

本博客虽然是静态博客,为了提升国内访问速度,从 GitHub Pages 上迁到了云主机,用 Nginx 搭建的静态网站服务。最近在整理博客站点时发现如果输错 URL 不是返回404页面,而是返回500错误,经定位发现是 Nginx 配置的问题。因此周末花了一天时间好好看了下 Nginx 相关配置指导,正好也做一个学习总结。末尾我会说明下这个问题以及修复方法。

Nginx 介绍

Nginx 是俄罗斯程序员 Igor Sysoev 于2004年开发的 web 服务器软件。2019年 Nginx 被 F5 以6.7亿美元的价格收购。有两件关于 Nginx 的知名新闻,一则是2019年 Rambler 公司(也是 Igor 开发 Nginx 时所服务的公司)起诉 Igor 及 F5 公司,声称他们侵犯了 Rambler 的知识产权。该事件引发 IT 界强烈争议而后变成“协商解决”而暂停。另一则是前段时间俄罗斯出兵乌克兰,F5 宣布从俄罗斯撤出业务并停止接纳俄罗斯对 Nginx 开源贡献。

Nginx 目前已超过 Apache 成为占有率最高的 web 服务器软件,市场占有率超过30%,相对老牌的 IIS 和 Apache 发展速度很快。

nginx market share

Nginx 可以做静态站点 HTTP/HTTPS 服务、反向代理、负载均衡。关于反向代理(Reverse Proxy)解释如下图示:我们在访问互联网时可以设置代理(Proxy),这样客户端对互联网的请求和数据接收是通过这个代理转发的。而所谓反向代理则是来转发互联网上用户对服务器的请求,Nginx 则提供了灵活的配置和高效的转发能力。

proxy-vs-reverse-proxy

安装及配置

通过包管理工具很容易安装 Nginx, 如 Ubuntu: sudo apt install nginx 或 Mac: brew install nginx. 安装好后启动 Nginx 服务,即可以通过服务器 IP 访问 Nginx 服务。

Nginx 配置文件结构

打开 Nginx 安装目录可以看到 Nginx 的配置文件 nginx.conf ,一般在 /etc/nginx 路径下。Nginx 配置脚本中 # 开头为注释,其他为配置指令(Nginx 术语为 directive)。

#user  nobody;
worker_processes  1;

#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;

#pid        logs/nginx.pid;

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  logs/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  65;

    #gzip  on;

    server {
        listen       8080;
        server_name  localhost;

        #charset koi8-r;

        #access_log  logs/host.access.log  main;

        location / {
            root   html;
            index  index.html index.htm;
        }
    }

    include servers/*;
}

Nginx 配置文件主要分成三块(Nginx 术语叫 context):全局块(main context)、events 块、http 块。events 块 和 http 块都被包含在 {} 中,没有没包含在 {} 中的配置指令就是全局块的内容。

全局块中主要是一些 Nginx 服务器的全局配置指令,如 log 日志存放位置、Nginx PID 文件路径、Nginx 运行 worker 进程数等。如上例中从配置文件开头到 events 块之间的内容为全局块(大部分被注释)。

events 块中配置影响 Nginx 服务器与用户的网络连接性能。如上例中 worker_connections 1024 表示一个 worker process 最大的连接数为1024,注意这个数字不能超过系统支持打开的最大文件数,也不能超过单个进程支持打开的最大文件数。

查看 Linux 下打开文件数限制:cat /proc/sys/fs/file-nr 。修改文件 /etc/sysctl.conf

查看 Linux 下单进程打开文件数限制:ulimit -n 。 修改文件:/etc/security/limits.conf

搭建博客服务器主要需要修改 http 块中的配置指令。

http 块配置

http 块中还可以包含 server 块,除此之外是 http 块的全局配置,包含连接超时时间、cache设置、传输文件压缩配置等。

上例中 include servers/* 表示可以将 servers 目录下的配置文件都包含进来,如果有多个服务配置,可以将这些配置分布在不同配置文件中,便于管理。

Server 块的典型配置如:

server {
    listen 80 default_server;
    listen [::]:80 default_server;
    server_name  hutusi.com www.hutusi.com;

    root /var/www/blog;
    location / {
        index  index.html index.htm;
    }

    error_page 404 /404.html;
    # redirect server error pages to the static page /50x.html
    error_page   500 502 503 504  /50x.html;

listen 指令表示服务监听的IP 和端口, default_server 表明这个 server 块是默认的服务。

例如 listen 127.0.0.1 表示只监听来自 127.0.0.1 这个 IP 并请求80端口的请求(不指定端口则默认为 80);而 listen 8080 表示监听来自所有 IP 并请求 8080 端口的请求。

第二行的 listen [::]:80 表示支持 IPv6,监听来自于 IPv6 的连接。

server_name 指令配置虚拟主机的名称,可以有多个名称并列,如:

server_name hutusi.com www.hutusi.com;
server_name localhost;   # 注意:这里的 localhost 与上面 listen 中的 localhost 含义是不一样的

root 指令配置请求寻找资源的根目录;该指令可以在 http、server 或 location 块中设置,一般在 server 和 location 块中设置。

error_page 指令设置自定义错误页面来代替 Nginx 默认提供的错误界面,比如例子中的error_page 404 /404.html 表示用根目录的 404.html 页面来作为 404 页面。

location 块配置

配置最为灵活的是 location 块,它表示了 Nginx 对于不同 URI 请求的特定处理配置,按 URI 的规则匹配来处理。语法规则如下:

location [ = | ~ | ~* | ^~ ] uri { ... }

location 中的 uri 有两种,一种是字符串前缀,一种是正则表达式。上述语法中 [] 方括号中为修饰可选项,其中 =^~ 使用字符串前缀,~~* 使用正则表达式。如果不加该可修饰, Nginx 会将配置 uri 当做字符串前缀与请求 URI 进行匹配。

在定义了一组 location 时,Nginx 的匹配过程如下:

Nginx 首先检查使用前缀字符串定义的 location。在这些 location 中,具有最长匹配前缀的 location 被选中并被记住。然后检查正则表达式,按照它们在配置文件中出现的顺序。正则表达式的搜索在第一次匹配时终止,并使用相应的 location。如果没有找到匹配的正则表达式,那么就使用先前记忆的最长匹配前缀的 location。

Nginx 对 location 的匹配规则优先级如下:

SSL 配置

博客网站需要支持 HTTPS 协议的话需要对有 SSL 证书并且在 Nginx 上做 SSL 配置。(如果用的是 GitHub Pages 或 Netlify 等服务就不用这么费事了。)证书可以在云厂商免费申请,或用 Let’s Encrypt 生成,这里就不赘述。证书下载后,可以开始 Nginx SSL 配置。

在配置文件中增加一个 SSL server 块:

server {
    listen 443 ssl http2 default_server;
    listen [::]:443 ssl http2 default_server;
    server_name  hutusi.com www.hutusi.com;
    
    ssl_certificate cert/hutusi.com.pem;
    ssl_certificate_key cert/hutusi.com-key.pem;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout  10m;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;
    
    root /var/www/blog;
    location / {
        index  index.html index.htm;
    }
    
    error_page 404 /404.html;
}

可以看出,除了listen 指令和增加了 SSL 指令部分外,其他跟非 SSL 配置是一样的。

listen 443 ssl http2 监听 SSL 默认端口 443,以及支持 HTTP/2 协议。

增加的几行 SSL 配置指令:

ssl_certificatessl_certificate_key 分别指定证书和证书私钥所存放的位置。

ssl_session_cachessl_session_timeout 设置 session 缓存大小和超时时间。

ssl_ciphers 设置选择加密算法套件,其中 !aNULL!MD5 前面的 ! 表明不选用 aNULL 和 MD5 类算法套件,而 HIGH 代表了一组高强度加密算法,可以通过如下命令查看算法清单:

$ openssl ciphers -V 'HIGH'
          0xCC,0xA9 - ECDHE-ECDSA-CHACHA20-POLY1305 TLSv1.2 Kx=ECDH     Au=ECDSA Enc=ChaCha20-Poly1305 Mac=AEAD
          0xCC,0xA8 - ECDHE-RSA-CHACHA20-POLY1305 TLSv1.2 Kx=ECDH     Au=RSA  Enc=ChaCha20-Poly1305 Mac=AEAD
          0xCC,0xAA - DHE-RSA-CHACHA20-POLY1305 TLSv1.2 Kx=DH       Au=RSA  Enc=ChaCha20-Poly1305 Mac=AEAD
          ......

ssl_prefer_server_ciphers on 表明设置协商加密算法时,优先使用服务端的加密套件,而不是客户端浏览器的加密套件。

如果想让用户访问 http 链接也指向 https 时,需要在监听 80 的 server 块配置 rewrite 或 return 301 重定向(return 301 性能更优一些)。

server {
    listen 80;
    listen [::]:80;
    server_name hutusi.com www.hutusi.com;
    return 301 https://hutusi.com$request_uri;
}

本地调试

本机安装 Nginx 就可以配置静态服务器进行调试,Mac 和 Linux 都比较方便。只是如果要配置 SSL 的话需要用到 mkcert 这个工具来制作本地签名证书。在 Mac 下通过命令 brew install mkcert 安装,然后运行命令 mkcert -install 来创建本地 CA(授权中心),并将该 CA 加入本机可信 CA (需要管理员账号授权)。

接着,就可以生成多域名证书了,该证书会输出到当前目录下。Nginx 调试中可以直接使用此证书。

$ mkcert example.com "*.example.com" example.test localhost 127.0.0.1 ::1

问题实例

问题描述

访问不存在的页面时返回的不是 404 页面而是 500 错误页面,比如浏览器中输入 https://hutusi.com/hello 则返回 500 错误。

问题原因

查看 Nginx 日志,发现报了这条错误:

2022/04/09 11:29:01 [error] 1683343#1683343: *49420 rewrite or internal redirection cycle while processing "////////////hello.html.html.html.html.html.html.html.html.html.html.html", client: 101.228.159.74, server: hutusi.com, request: "GET /hello HTTP/2.0", host: "hutusi.com"

大概是 rewrite 进入死循环了。打开配置文件,发现是这么写的:

location / {
    # Rewrite .html
    if (!-e $request_filename) {
        rewrite ^(.*)$ /$1.html last;
    }
}

这里的逻辑:如果找不到页面(if 条件),则重定向到新地址(在原地址后追加 .html)。其中,rewrite 指令的前两个参数分别是原 URI 和替换后的 URI,^(.*)$ $1 都是正则和正则替换语法。最后的参数 last 表示停止处理当前的 rewrite 并开始根据替换后的 URI 搜索新的 location 匹配。

原来这样配置的目的主要是因为博客文章生成的 URL 为 [https://hutusi.com/articles/rms](https://hutusi.com/articles/rms).html 这样,而我不想再访问时加 .html ,希望更简洁的 URL 类似 https://hutusi.com/articles/rms ,因此加了这段 rewrite 配置在匹配不到 URL 时重定向到追加了 .html 的 URL 页面。但当访问到真不存在的 URL 时,根据逻辑将追加 .html ,依然匹配不到,再继续追加,也就导致了死循环错误。

解决方法

找到原因,解决方法就很简单了,不再用 if 条件判断(官网也不建议用 if ),而是改用 location 的正则匹配:

location ~ ^/articles/.*(?<!\.html)$ {
    rewrite ^(.*)$ /$1.html last;
}

附: Nginx 官方文档

博客 nginx

知识共享许可协议 本文采用「CC BY-SA 4.0」知识共享许可协议,如果还喜欢其他文章, 欢迎订阅“胡涂说”博客
公众号
微信公众号同步更新,欢迎关注😊
对我博客最大的鼓励来自于你的评论,欢迎选择 来回复, 也可以在 GitHub discussion 留言。