入坑 Nginx,看这一篇就够了

492 阅读13分钟

头图

文章的 TOC 太多了,看起来不方便?PC 端可以看看我的 《两个 TamperMonkey 脚本,解决掘金阅读体验的三点不爽》,可以解决一些掘金阅读体验问题。

我写的「入坑」系列文章:

  1. 《入坑 Mac,看这一篇就够了》
  2. 《入坑 iTerm + OMZ,看这一篇就够了》
  3. 《入坑 Firefox Developer Edition 及 Mobile 版,看这一篇就够了》
  4. 《入坑 WebStorm,看这一篇就够了》
  5. 《入坑 VSCode,看这一篇就够了》
  6. 《入坑 Vim,看这一篇就够了》
  7. 《入坑 Git,看这一篇就够了》
  8. 《入坑 Docusaurus,看这一篇就够了》
  9. 《入坑 Nginx,看这一篇就够了》← 本文
  10. 《入坑 UniApp 写小程序,一些感受》

🎙️ 前言

你一定知道 Nginx,那么你「会用」么?说来惭愧,我不太会。这大概是由于之前的公司,此类事情一般都有专门的人负责。

然所谓「技多不压身」,无论你是前端后端,得对 Nginx 有基础的了解,不论是平常开发,还是希望把自己的项目快速落地,都可能用得着。

TL;DR

本文是前端向的 Nginx 入门文章。介绍如何安装、以及如何通过命令行启停、重载、配置检查等。通过不同的实战配置:静态托管、CDN 代理等,帮助开发者快速上手日常开发与部署必备的 Nginx 的基础知识和常用技巧。

主要内容

适合读者

  • 有一定的命令行基础
  • 想初步了解 Nginx 的同学
  • 想知道 Nginx 怎么配置静态托管、CDN 代理等技巧的同学

你将了解

  • Nginx 的正确读音
  • Nginx 的常用命令(以及如何用 systemctlbrew 命令操作 Nginx)
  • 常规的 Nginx 和使用 brew 安装的有什么区别
  • Nginx 常用代理模式,静态托管和 CDN 代理

编辑历史

日期版本说明
2023/08/02V1

😴 认识 Nginx

读音

你或许需要重新认识下 Nginx?

你可能一直都没有念对它的发音。这不怪你,事实上,10 个里面有 9.5 个人都会念错。你是不是一直这么读:「恩金克斯」?You say wrong...正确地读法是把「X」念全了:「恩金·埃克斯」。所以,其实应该这么写:NginX。

Nginx 官网 的第一句话就是教你如何正确地发音(然后才是它是什么):

nginx ("engine x") is an HTTP web server, reverse proxy, content cache, load balancer, TCP/UDP proxy server, and mail proxy server.

核心功能

  • Web 服务器:托管网站、静态文件
  • 反向代理:这是 Nginx 最核心的用途(也是你听到最多的关联词)
  • 负载均衡:把流量均匀分给多台后端服务器
  • 网关能力:HTTPS 证书、缓存、限流、URL 重写、Gzip 压缩、长连接优化等

核心优势

  • 轻量,占用资源(内存 / CPU)极低
  • 高并发,单机轻松扛 1 万+ 并发
  • 稳定可靠
  • 配置简单
  • 扩展性强

新手必看

Nginx 的官网…简陋得不像这东西还活着一样,关键是很多对新手有用的链接都藏得很深,放这里以快速参考:

Nginx、Apache、Tomcat、Jetty 这几个兄弟的官网都差不多简陋,难道长得丑,活得久?

📦 安装

一般情况呢,你的 Linux 服务器上一定已经装好了 nginx。线上的机器,不论测试还是生产,都不可亵玩焉,作为练手使用,建议在自己电脑上装一个。

但估计很多人会在这一步就被劝退…官网的 安装文档,不能说并没太多新手友好的东西,只能说跟没写没啥区别。

用 Mac 和 Ubuntu 的同学相对比较简便:

  • Mac - 推荐使用 brew 安装 brew install nginx
  • Linux - Nginx 对大多数耳熟能详的 Linux 发行版 都有支持
  • Windows - 下载 后解压
  • Docker - 我简单玩了一下,结论是「用来练手不合适」

Linux 中以对新手友好的 Ubuntu 为例,执行以下命令(不用 sudo 会报错):

sudo apt update           # 以避免 `Invalid operation install` 的错误
sudo apt install nginx

若你还是想亲手试试 Docker 的方式:

docker pull nginx:latest
docker pull docker.1ms.run/nginx:latest # 上面这个可能因为神秘力量不能拉取,试试这个

以下,我将记录在 Mac 下通过 brew 安装和使用 Nginx。

Brew 安装

安装一如既往的简单,执行 brew install nginx,等一会儿就好了:

注意最后的警告(Caveats)部分:

  1. Docroot 是 /opt/homebrew/var/www,而不是默认的 /var/www
  2. 端口是 8080,而不是默认的 80
  3. 子配置目录是 servers,而不是默认的 /etc/nginx/conf.d/*.conf

以上 2 和 3 可以在 /opt/homebrew/etc/nginx/nginx.conf 找到:

http {
  ...
  
  server {
    listen 8080;
    ...
  }
  
  ...
  include servers/*; # 建议 * 改成 *.conf
}

建议把 include servers/*;改成 include servers/*.conf;,这样,往里面丢 SSL 的 keypem 文件就不会导致出错。

以下是安装后的 /opt/homebrew/etc/nginx 下的文件概览:

启停

你有两种方式启停 Nginx:

启动停止
nginxnginxnginx -s stop
brewbrew services start nginxbrew services stop nginx

无论你用以上哪种方式启动,访问 http://127.0.0.1:8080,你会看到一个很简陋的页面,说明启动成功:

上面这个 HTML 对应的文件在 /opt/homebrew/var/www/index.html

需要注意的是,如果使用 nginx 启动,不能通过 brew 停止(因为 brew services 感知不到);而使用 brew 启动,还是可以通过 nginx 进行停止。但!建议不要混着来,否则就会想以下这样,碰到「Bootstrap failed: 5」的启动错误(当然,解决的办法也简单,就是执行 brew services reload,但我曾经因此困惑而重装过…)。

因此建议只用 brew 启停,就像在服务器上只用 systemctl 启停一样。

路径差异

brew 安装,跟在服务器上(或者在 Ubuntu 是安装)的 Nginx,除了之前提到的配置目录外,还有一些别的路径区别,一般都是在默认的前面拼上 /op/homebrew,以下是常用的路径:

路径常规brew
Bin 文件/usr/sbin/nginx/opt/homebrew/bin/nginx
安装目录/etc/nginx/opt/homebrew/Cellar/nginx/{version}
配置文件/etc/nginx/nginx.conf/opt/homebrew/etc/nginx/nginx.conf
配置目录/etc/nginx/conf.d//opt/homebrew/etc/nginx/servers/
日志目录/var/log/nginx/opt/homebrew/var/log/nginx
静态文件/var/www/opt/homebrew/var/www

常用命令对比

之前我们已经知道,可以通过 brew services 启停 Nginx,而在服务器上你会被告知最好使用 systemctl 进行操作。下表整理了跟 Nginx 有关的常用命令:

操作原生命令(跨系统)brewsystemctl
启动nginxbrew services start nginxsystemctl start nginx
停止nginx -s stop / nginx -s quitbrew services stop nginxsystemctl stop nginx
重启nginx -s stop && nginxbrew services restart nginxsystemctl restart nginx
重载配置nginx -s reload-systemctl reload nginx
检查配置nginx -t / nginx -T--
查看状态无(需用 ps aux | grep nginx 查看进程)brew services info nginxsystemctl status nginx
一些说明:
  • 停止:stop VS quitnginx -s stop 是「暴力停止」,立刻终止所有进程,会中断业务;nginx -s quit 是「优雅停止」,等所有请求处理完再退出,无业务中断;生产环境请用 quit
  • 重启:使用 brew/systemctlnginx -s stop && nginx 多了服务状态校验
  • 检查配置 -t VS -T-t 只讲对错,输出简洁,-T 还会显示详细的配置内容;一般情况下用 -t 即可
  • 虽然 brew services 文档中并没有 reload,但你可以执行 brew services reload nginx,其效果其实跟 restart 是一样的

其他 brew 命令

除了以上常用命令中提到的,使用 brew,跟 Nginx 相关的,还有这些命令:

  • brew services list 查看由 brew 管理的所有服务,可以看 nginx 是否在运行
  • brew info nginx 查看 nginx 版本等基础信息
  • brew upgrade nginx 升级
  • brew outdated nginx 是否需要升级
  • brew uninstall nginx 卸载
  • brew reinstall nginx 重装

🚀 配置实战

我们跳过枯燥晦涩的配置理论知识,直接进入实战。为了方便操作,以下都将以本地的 brew nginx 为前提进行操作,在没有任何说明的情况下,重启指 brew services restart nginx,当然你可以用 nginx -s reload

通常情况下,我们很少会去关注或修改 nginx.conf(最主要的配置文件),只需要往子配置目录 conf.d(或 brew 的 servers) 里丢 域名.conf 就行了。

接下来,假设现在我有个前端项目,它可能是一个 Vite 项目,也可能是一个 SSG 项目。拿之前我讲过的 SSG 框架 Docusaurus 的本地项目 documentation 作例子。

我们现在将逐步在自己的电脑上,从本地静态托管到 CDN 代理在到 HTTPS,从而使你最终能够优雅高效地最终可以部署到真正的服务器 Nginx 上,又或者,当你服务器上有哪个 Nginx 的配置需要调优的时候,直到如何在自己电脑上进行实验。

关于域名

首先,我们需要一个域名。域名能否被 Nginx 接住,主要看域名解析 DNS 是否能命中 Nginx 所在的机器。

如果你已经有了一个主域名,最简单的就是给它添加一个二级域名,比如 doc.company.com。在阿里云「云解析 DNS」控制台,就可以快速添加一个子域名(不花钱),只需要让子域名指向 Nginx 服务器所在的 IP 即可:

现在我们只是本地部署看看效果,只需要增加一条 Host 记录,比如:

127.0.0.1 doc.test

静态托管

静态托管的方式,需要把项目构建产物上传到服务器对应的目录 /var/www/doc,然后添加 conf.d/doc.<company-domain>.conf

本地试验,新增 /opt/homebrew/etc/nginx/servers/doc.test.conf

server {
  listen 80;
  server_name doc.test;
  root /....../documentation/build; # 这里直接指导项目的构建目录,省去拷贝

  # 处理根路径请求
  location / {
    try_files $uri $uri/ /index.html; # 支持单页面应用路由
  }

  # 静态资源缓存配置(可选但推荐)
  location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|pdf|txt|map|json)$ {
    expires 30d;
    add_header Cache-Control "public";
    access_log off;
  }

  # 错误页面配置
  error_page 404 /404.html;
  location = /404.html {
    internal;
  }
}

如果是以这种方式直接应用于现实中的服务器的话,需要注意 root 并不是相对于 /var/www 的相对路径,需要写绝对路径,Nginx 并没有此类约定。你或许看到 nginx.confroot html 而产生此类想法,但这是 Nginx 内置的约定,html 目录固定在配置前缀下(如 /usr/share/nginx/html),属于标准化的相对路径用法。

另外就是,这种部署方式会很麻烦——你得有往服务器上传目录的资格和技巧。幸运的是,你可以利用类似 Transmit 的 SFTP 工具进行操作,这里推荐你使用 rsync,一个原生的 Linux 命令,Mac 下也支持。

rsync -rvz --delete 本地目录/ 用户@服务器:/var/www/doc/
  • -r 保留目录结构
  • -v 详细输出
  • -z 传输时压缩,加快上传速度
  • --delete rsync 默认增量更新,虽然不成问题,但会生产垃圾文件,此参数可以先删除再上传
  • 本地目录/ 末尾带 / 表示同步目录内的所有内容,而非目录本身
  • /var/www/doc/ 末尾带 / 确保内容同步到目标目录下,不存在时自动创建

CDN 代理

以上说到直接去服务器上部署文件,虽然简单粗暴,但却相当麻烦,还将占用服务器的存储资源。

假设现在,我已经把前端资源(包括一个 index.html)发布到了 CDN https://somecdn.com/documentation/1.0.0/,可以把 Nginx 的配置也改成 CDN 代理的方式:

server {
  listen 80;
  server_name doc.test;
  
  location / {
    proxy_ssl_server_name on;
    proxy_ssl_protocols TLSv1.2 TLSv1.3;
    proxy_pass https://somecdn.com/documentation/1.0.0/; # 注意必须以 / 结尾,否则 404
    proxy_set_header Host somecdn.com;
    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 Accept-Encoding "";
    proxy_redirect off;
    proxy_intercept_errors on;
    
    proxy_buffering on;
    proxy_buffer_size 4k;
    proxy_buffers 8 4k;
    proxy_busy_buffers_size 8k;
    
    gzip off; # gzip,避免重复压缩
    
    error_page 404 = /index.html;
    # if ($request_uri = /) {
    #   rewrite ^ /index.html;
    # }
  }
  
  # 静态资源缓存配置(可选)
  # location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|pdf|txt|map|json)$ {
  #   expires 30d;
  #   add_header Cache-Control "public";
  #   access_log off;
  # }
}

这样,每次发布 CDN,只要版本号不变(比如测试环境)就没有必要做部署;而如果进行了生产发布,则只需要更新生产机器上的版本号即可。

通过查看 index.html 请求的响应头,可以看到两个方案的差别:

另外,静态托管和 CDN 代理还有一个显著的区别,静态托管的核心配置是 root,有没有 / 结尾都可以,但 CDN 代理的核心 proxy_pass 却必须以 / 结束,否则将 404。

HTTPS

接下来,经验丰富的你会想到 HTTPS 的问题。

有些时候,本地开发、联调、小程序 / 本地H5调试时,可能会被强制要求 HTTPS,如小程序请求、Safari 跨域、PWA、安全接口校验等。本地 Nginx 默认仅支持 HTTP,需要手动生成本地自签名 SSL 证书并配置 Nginx,实现本地可信 HTTPS 访问。

本地 SSL 证书

以下,我们将在 brew Nginx 配置目录下新建一个 ssl 目录专门存放 SSL 文件:

# 进入 nginx 目录(根据自己环境修改)
cd /opt/homebrew/etc/nginx
mkdir ssl && cd ssl

# 生成 RSA 私钥 + 自签名证书(有效期10年)
openssl req -x509 -newkey rsa:4096 -nodes -keyout local.key -out local.crt -days 3650

它会问几个问题,随便回答就行:

这样就在 ssl 目录下生出了两个文件,公钥 local.crt 和私钥 local.key

配置 SSL

然后,我们基于之前的配置,添加 SSL 的相关配置,同时让 80 端口 301(永久重定向)到 HTTPS:

server {
  listen 80;
  server_name doc.test;
	return 301 https://$host$request_uri;
}

server {
  listen 443 ssl;
  server_name doc.test;
  
  # 核心 ssl 证书配置
  ssl_certificate /opt/homebrew/etc/nginx/ssl/local.crt;
  ssl_certificate_key /opt/homebrew/etc/nginx/ssl/local.key;
  	
  # 基础 ssl 优化配置
  ssl_protocols TLSv1.2 TLSv1.3;
  ssl_prefer_server_ciphers on;
  ssl_session_cache shared:SSL:10m;
  ssl_session_timeout 10m;
  
  location / {
    proxy_ssl_server_name on;
    proxy_ssl_protocols TLSv1.2 TLSv1.3;
    proxy_pass https://somecdn.com/documentation/1.0.0/;
    proxy_set_header Host somecdn.com;
    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 Accept-Encoding "";
    proxy_redirect off;
    proxy_intercept_errors on;
    
    proxy_buffering on;
    proxy_buffer_size 4k;
    proxy_buffers 8 4k;
    proxy_busy_buffers_size 8k;
    
    gzip off; # gzip,避免重复压缩
    
    error_page 404 = /index.html;
    # if ($request_uri = /) {
    #   rewrite ^ /index.html;
    # }
  }
  
  # 静态资源缓存配置(可选)
  # location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|pdf|txt|map|json)$ {
  #   expires 30d;
  #   add_header Cache-Control "public";
  #   access_log off;
  # }
}

绕过不安全提示

重启 Nginx 后,你会发现,浏览器并不认可此证书:

这是正常的,毕竟这个证书血统不正宗,但同时也恭喜你,配置 SSL 成功了,可以通过人工干预的方式绕过:

  • Firefox / Chrome:高级 → 继续访问(不安全)
  • Safari:信任证书
  • 小程序开发工具:勾选 不校验合法域名、HTTPS、TLS

以下是配置了 SSL 后的访问效果:

关于生产环境

本地 SSL 签名只能我们开发者自己玩玩,生产环境下就必须使用 CA 机构签发的正式可信 SSL 证书。

而且,证书是有时效的,阿里云 / 腾讯云默认免费证书一般都是时效 90 天的单域名证书(一个证书只能用于特定域名);更方便的长时效的范域名证书(推荐一个证书用于多个子域名),就得舍得花点银子了。

日志

查看日志是必备的技能,记住 tail 这一个命令即可:

默认brew 安装
访问日志tail -100f /var/log/nginx/access.logtail -100f /opt/homebrew/var/log/nginx/access.log
错误日志tail -100f /var/log/nginx/error.logtail -100f /opt/homebrew/var/log/nginx/access.log

你已经知道 Nginx 可以带多个 Server,会思考的你也一定会想到上面的 access.logerror.log 如果混杂了所有 Server 的日志将会带来不必要的麻烦。你可以在对应的服务下配置日志路径到专属的文件,比如你有个 verynb.com 的 Server:

server {
  listen 80;
  server_name verynb.com;
  
  access_log /var/log/nginx/verynb.com.access.log;
  error_log /var/log/nginx/verynb.com.error.log;
  
  ...
}

📕 配置理论知识

变量

Nginx 内置了很多 变量,也就是你在配置文件中看到的 $xx。以下是一些核心的变量(AI 整理):

变量名含义典型使用场景
$remote_addr客户端的 IP 地址(公网 / 内网)日志记录、限流、防盗链
$remote_port客户端与 Nginx 建立连接的端口号日志排查、连接数统计
$remote_user客户端通过 HTTP 认证的用户名权限控制、日志审计
$http_user_agent客户端的 User-Agent(浏览器 / 设备信息)适配不同设备、反爬虫
$http_referer客户端请求的来源页面(Referer 头)防盗链、统计来源
$http_cookie客户端携带的 Cookie 信息鉴权、个性化配置
$request_methodHTTP 请求方法限制请求方法、日志
$request_uri客户端请求的完整 URI(含参数)日志记录、重定向
$request_filename对应请求的本地文件路径(静态资源场景)静态资源缓存、文件访问控制
$uri请求的 URI(不含参数,已解码)反向代理、路径匹配
$args请求的 URL 参数参数透传、日志
$host请求的主机名(优先 Host 头,无则用服务器域名)多域名配置、反向代理
$server_nameNginx 配置中当前 serverserver_name(固定值)多域名区分、日志
$server_portNginx 接收请求的端口号端口适配、日志
$scheme请求的协议 http/https强制 HTTPS 跳转、日志
$statusNginx 返回给客户端的 HTTP 状态码日志统计、错误监控
$bytes_sentNginx 发送给客户端的字节数流量统计、限流
$request_time请求的总处理时间(秒,含小数)性能监控、慢请求排查
$proxy_add_x_forwarded_for拼接客户端真实 IP(X-Forwarded-For 头),传递给后端后端获取客户端真实 IP
$upstream_addr反向代理转发的后端服务器地址(IP: 端口)后端集群排查、日志
$upstream_status后端服务器返回的 HTTP 状态码后端错误监控
$upstream_response_time后端服务器的响应时间(反向代理场景)后端性能监控

Nginx 核心配置项

Nginx 的默认核心配置文件 nginx.conf,可分成 6 个模块:

  • 全局段:全局配置,对全局生效
  • events:配置影响 Nginx 服务器与用户的网络连接
  • http:最常用的配置,配置代理、缓存、日志等功能和第三方模块的配置
  • server:配置虚拟主机的相关参数,一个 http 块中可以有多个 server 块
  • location:用于配置匹配的 URI
  • upstream:配置后端服务器具体地址,负载均衡配置

🙋 FAQ

❓ 如何查看 Nginx 正在代理的所有域名?

这是一个算常见的需求,我就想看看这台机器承接了哪些业务。但可恨的是,Nginx 并不提供类似的便利。幸运的是,有曲线救国的办法:

nginx -T 2>/dev/null | grep -E "\sserver_name\s+" | grep -v "#" | awk '{print $2}' | tr ';' ' ' | tr ',' ' '

以上命令,有几点需要澄清:

  1. 不要问我诸如 如 2>/dev/null 的细节
  2. 其实它代表不了「正在」二字,因为它只是从 nginx -T 的输出中利用正则提取所有 server_name 后面的字段,可能包括注释掉的部分,也可能 Nginx 甚至都没有起来

❓ 访问到了真实的 CDN 文件怎么回事?

理论上,使用 CDN 代理,是不会看到 CDN 的地址的,尤其不可能在浏览器地址栏看到。但偏偏碰到了,是怎么回事?

原因其实也很简单,就是你很可能把 CDN 的 https 误写成了 http,导致触发了 CDN 的 302 跳转,导致页面地址被强行 Forward 到了 https