Nginx from Zero to a Working Reverse Proxy, with Brew and Self-Signed SSL
Is the article's TOC too long and inconvenient to read? On PC, you can check out my Two TamperMonkey Scripts to Solve Three Pain Points of the Juejin Reading Experience, which can solve some Juejin reading experience issues.
My "Getting Started" series of articles:
- Getting Started with Mac, This One Article Is Enough
- Getting Started with iTerm + OMZ, This One Article Is Enough
- Getting Started with Firefox Developer Edition and Mobile, This One Article Is Enough
- Getting Started with WebStorm, This One Article Is Enough
- Getting Started with VSCode, This One Article Is Enough
- Getting Started with Vim, This One Article Is Enough
- Getting Started with Git, This One Article Is Enough
- Getting Started with Docusaurus, This One Article Is Enough
- Getting Started with Nginx, This One Article Is Enough ← This article
🎙️ Preface
You definitely know Nginx, but do you know how to "use" it? To be honest, I'm not very good at it. This is probably because at previous companies, such tasks were usually handled by dedicated personnel.
However, as the saying goes, "more skills, less pressure." Whether you are a front-end or back-end developer, you need a basic understanding of Nginx. Whether for daily development or for quickly deploying your own projects, it can be useful.
TL;DR
This article is a front-end-oriented introduction to Nginx. It introduces how to install it, and how to start, stop, reload, and check configurations via the command line. Through different practical configurations: static hosting, CDN proxying, etc., it helps developers quickly get started with the basic knowledge and common skills of Nginx essential for daily development and deployment.
Main Content
Suitable Readers
- Have some command-line basics
- Students who want a preliminary understanding of Nginx
- Students who want to know how to configure static hosting, CDN proxying, and other skills with Nginx
You Will Learn
- The correct pronunciation of Nginx
- Common Nginx commands (and how to operate Nginx with
systemctlandbrewcommands) - The difference between a conventional Nginx installation and one installed via
brew - Common Nginx proxy modes, static hosting, and CDN proxying
Edit History
| Date | Version Description |
|---|---|
| 2023/08/02 | V1 |
😴 Getting to Know Nginx
Pronunciation
You might need to re-acquaint yourself with Nginx, starting with how to pronounce it. I estimate that 9.5 out of 10 people mispronounce the name Nginx. Have you been reading it as "En-jin-ks"? You say wrong... The correct way is to pronounce the "X" fully: "En-jin-Eks". So, it should actually be written as: NginX.
The first sentence on the Nginx official website teaches you how to pronounce it correctly (and then what it is):
nginx ("engine x") is an HTTP web server, reverse proxy, content cache, load balancer, TCP/UDP proxy server, and mail proxy server.
Core Features
- Web Server: Hosts websites and static files
- Reverse Proxy: This is Nginx's most core use case (and the term you hear associated with it most often)
- Load Balancing: Distributes traffic evenly across multiple backend servers
- Gateway Capabilities: HTTPS certificates, caching, rate limiting, URL rewriting, Gzip compression, long connection optimization, etc.
Core Advantages
- Lightweight, extremely low resource (memory/CPU) usage
- High concurrency, easily handles 10,000+ concurrent connections on a single machine
- Stable and reliable
- Simple configuration
- Strong extensibility
Must-See for Beginners
Nginx's official website... is so rudimentary it doesn't look like the project is still alive. Crucially, many links useful for beginners are hidden very deep. Placed here for quick reference:
The official websites of Nginx, Apache, Tomcat, and Jetty are all similarly rudimentary. Does being ugly mean living longer?
📦 Installation
Generally, nginx is likely already installed on your Linux server. Online machines, whether for testing or production, should not be trifled with. For practice, it is recommended to install it on your own computer.
But I estimate many people will be discouraged at this step... The official installation documentation can't be said to have much beginner-friendly content, it's more like it wasn't written at all.
For Mac and Ubuntu users, it's relatively straightforward:
- Mac - Recommend installing with
brew:brew install nginx - Linux - Nginx has support for most well-known Linux distributions
- Windows - Download and extract
- Docker - I played around with it briefly and concluded it's "not suitable for practice"
For Linux, taking the beginner-friendly Ubuntu as an example, execute the following commands (you will get an error without sudo):
sudo apt update # to avoid the `Invalid operation install` error
sudo apt install nginx
If you still want to try the Docker method yourself:
docker pull nginx:latest
docker pull docker.1ms.run/nginx:latest # If the above can't be pulled due to mysterious forces, try this
Below, I will document installing and using Nginx on Mac via brew.
Brew Installation
The installation is as simple as ever. Execute brew install nginx and wait a moment:
Note the warnings (Caveats) at the end:
- Docroot is
/opt/homebrew/var/www, not the default/var/www - The port is 8080, not the default 80
- The sub-configuration directory is
servers, not the default/etc/nginx/conf.d/*.conf
Points 2 and 3 above can be found in /opt/homebrew/etc/nginx/nginx.conf:
http {
...
server {
listen 8080;
...
}
...
include servers/*; # Suggest changing * to *.conf
}
It is recommended to change include servers/*; to include servers/*.conf;. This way, dropping SSL key or pem files into it won't cause errors.
Here is an overview of the files under /opt/homebrew/etc/nginx after installation:
Starting and Stopping
You have two ways to start and stop Nginx:
| Start | Stop | |
|---|---|---|
| nginx | nginx |
nginx -s stop |
| brew | brew services start nginx |
brew services stop nginx |
No matter which method you use to start, visiting http://127.0.0.1:8080 will show a very plain page, indicating a successful start:
The HTML file above corresponds to
/opt/homebrew/var/www/index.html.
It is important to note that if started with nginx, it cannot be stopped via brew (because brew services is unaware of it); however, if started with brew, it can still be stopped via nginx. But! It is recommended not to mix them, otherwise you will encounter a "Bootstrap failed: 5" startup error like the one below (of course, the solution is simple, just execute brew services reload, but I was once confused by this and reinstalled...).
Therefore, it is recommended to only use brew for starting and stopping, just like using only systemctl on a server.
Path Differences
An Nginx installation via brew has some path differences compared to one on a server (or installed on Ubuntu), besides the configuration directory mentioned earlier. Generally, /op/homebrew is prepended to the default paths. Here are the common paths:
| Path | Conventional | brew |
|---|---|---|
| Bin File | /usr/sbin/nginx |
/opt/homebrew/bin/nginx |
| Install Directory | /etc/nginx |
/opt/homebrew/Cellar/nginx/{version} |
| Config File | /etc/nginx/nginx.conf |
/opt/homebrew/etc/nginx/nginx.conf |
| Config Directory | /etc/nginx/conf.d/ |
/opt/homebrew/etc/nginx/servers/ |
| Log Directory | /var/log/nginx |
/opt/homebrew/var/log/nginx |
| Static Files | /var/www |
/opt/homebrew/var/www |
Common Command Comparison
We already know that Nginx can be started and stopped via brew services, and on a server, you will be told it's best to use systemctl for operations. The table below organizes common commands related to Nginx:
| Operation | Native Command (Cross-system) | brew | systemctl |
|---|---|---|---|
| Start | nginx |
brew services start nginx |
systemctl start nginx |
| Stop | nginx -s stop / nginx -s quit |
brew services stop nginx |
systemctl stop nginx |
| Restart | nginx -s stop && nginx |
brew services restart nginx |
systemctl restart nginx |
| Reload Config | nginx -s reload |
- | systemctl reload nginx |
| Check Config | nginx -t / nginx -T |
- | - |
| Check Status | None (need to use ps aux | grep nginx to view processes) |
brew services info nginx |
systemctl status nginx |
| Some notes: |
- Stop:
stopVSquit:nginx -s stopis a "forceful stop," immediately terminating all processes, which will interrupt services;nginx -s quitis a "graceful stop," waiting for all requests to finish processing before exiting, with no service interruption; usequitin production environments. - Restart: Using
brew/systemctladds service status validation compared tonginx -s stop && nginx. - Check Config
-tVS-T:-tonly tells right from wrong with concise output, while-Talso displays detailed configuration content; generally,-tis sufficient. - Although
brew servicesdocumentation doesn't mentionreload, you can executebrew services reload nginx, and its effect is actually the same asrestart.
Other brew Commands
In addition to the common commands mentioned above, using brew, there are these commands related to Nginx:
brew services listView all services managed bybrew, can see ifnginxis running.brew info nginxView basic info like thenginxversion.brew upgrade nginxUpgrade.brew outdated nginxCheck if an upgrade is needed.brew uninstall nginxUninstall.brew reinstall nginxReinstall.
🚀 Configuration in Practice
We skip the dry and obscure configuration theory and jump straight into practice. Below, I will operate on the premise of an actual server, not the local brew nginx.
Typically, we rarely pay attention to or modify nginx.conf (the main configuration file). We just need to drop domain.conf files into the sub-configuration directory conf.d (or servers for brew).
Next, suppose I have a front-end project now. It could be a Vite project or an SSG project. Let's take the local project documentation of the SSG framework Docusaurus I talked about before as an example.
I will now gradually deploy it to a certain domain name.
About Domain Names
First, we need a domain name. Whether a domain name can be picked up by Nginx mainly depends on whether the domain name resolution DNS can hit the machine where Nginx is located.
If you already have a main domain, the simplest way is to add a second-level domain to it, like doc.company.com. In the Alibaba Cloud "Cloud Resolution DNS" console, you can quickly add a subdomain (free of charge), just by pointing the subdomain to the IP where the Nginx server is located:
Now we are just deploying locally to see the effect, so we only need to add a Host record, for example:
127.0.0.1 doc.test
Static Hosting
The static hosting method requires uploading the project build artifacts to the corresponding server directory /var/www/doc, and then adding conf.d/doc.<company-domain>.conf.
For local testing, add /opt/homebrew/etc/nginx/servers/doc.test.conf:
server {
listen 80;
server_name doc.test;
root /....../documentation/build; # Point directly to the project's build directory here, saving the copy step
# Handle root path requests
location / {
try_files $uri $uri/ /index.html; # Support single-page application routing
}
# Static resource cache configuration (optional but recommended)
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 configuration
error_page 404 /404.html;
location = /404.html {
internal;
}
}
If this method is directly applied to a real server, note that root is not a relative path relative to /var/www; an absolute path must be written. Nginx has no such convention. You might have this idea from seeing root html in nginx.conf, but this is an Nginx built-in convention; the html directory is fixed under the configuration prefix (like /usr/share/nginx/html), which is a standardized relative path usage.
Additionally, this deployment method is very troublesome—you need the credentials and skills to upload directories to the server. Fortunately, you can use SFTP tools like Transmit for operation. Here, it is recommended to use rsync, a native Linux command also supported on Mac.
rsync -rvz --delete local_directory/ user@server:/var/www/doc/
-rPreserve directory structure-vVerbose output-zCompress during transfer, speeding up upload--deletersyncdefaults to incremental updates, which, while not a problem, can produce junk files. This parameter deletes first, then uploads.local_directory/The trailing/means synchronize all contents within the directory, not the directory itself./var/www/doc/The trailing/ensures content is synchronized into the target directory, creating it automatically if it doesn't exist.
CDN Proxy
As mentioned above, deploying files directly to the server, while simple and crude, is quite troublesome and will also consume the server's storage resources.
Suppose now, I have already published the front-end resources (including an index.html) to a CDN https://somecdn.com/documentation/1.0.0/. The Nginx configuration can also be changed to a CDN proxy method:
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/; # Note: must end with /, otherwise 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, to avoid double compression
error_page 404 = /index.html;
# if ($request_uri = /) {
# rewrite ^ /index.html;
# }
}
# Static resource cache configuration (optional)
# 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;
# }
}
This way, every time a CDN release is made, as long as the version number doesn't change (like in a testing environment), there is no need for deployment; and if a production release is made, only the version number on the production machine needs to be updated.
By viewing the response headers of the index.html request, you can see the difference between the two approaches:
Additionally, there is another significant difference between static hosting and CDN proxying. The core configuration for static hosting is root, and it doesn't matter if it ends with / or not. However, the core proxy_pass for CDN proxying must end with /, otherwise it will result in a 404.
HTTPS
Next, as an experienced developer, you will think of the HTTPS issue.
Sometimes, during local development, joint debugging, or mini-program/local H5 debugging, HTTPS might be mandatory, such as for mini-program requests, Safari cross-origin, PWA, security interface verification, etc. Local Nginx only supports HTTP by default. You need to manually generate a local self-signed SSL certificate and configure Nginx to achieve local trusted HTTPS access.
Local SSL Certificate
Below, we will create a new ssl directory under the brew Nginx configuration directory to store SSL files:
# Enter the nginx directory (modify according to your own environment)
cd /opt/homebrew/etc/nginx
mkdir ssl && cd ssl
# Generate RSA private key + self-signed certificate (valid for 10 years)
openssl req -x509 -newkey rsa:4096 -nodes -keyout local.key -out local.crt -days 3650
It will ask a few questions, just answer them casually:
This generates two files in the ssl directory: the public key local.crt and the private key local.key.
Configuring SSL
Then, based on the previous configuration, we add SSL-related settings and simultaneously make port 80 do a 301 (permanent redirect) to HTTPS:
server {
listen 80;
server_name doc.test;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name doc.test;
# Core ssl certificate configuration
ssl_certificate /opt/homebrew/etc/nginx/ssl/local.crt;
ssl_certificate_key /opt/homebrew/etc/nginx/ssl/local.key;
# Basic ssl optimization configuration
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, to avoid double compression
error_page 404 = /index.html;
# if ($request_uri = /) {
# rewrite ^ /index.html;
# }
}
# Static resource cache configuration (optional)
# 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;
# }
}
Bypassing the Insecure Warning
After restarting Nginx, you will find that the browser does not trust this certificate:
This is normal; after all, this certificate is not of legitimate origin. But at the same time, congratulations, you have successfully configured SSL. You can bypass it through manual intervention:
- Firefox / Chrome: Advanced → Continue to site (unsafe)
- Safari: Trust the certificate
- Mini-program development tools: Check Do not verify valid domain names, HTTPS, TLS
Here is the access effect after configuring SSL:
About Production Environments
Local SSL signatures are only for us developers to play with. In production environments, a formal trusted SSL certificate issued by a CA authority must be used.
Moreover, certificates have an expiration date. The default free certificates from Alibaba Cloud / Tencent Cloud are generally single-domain certificates valid for 90 days (one certificate can only be used for a specific domain); for more convenient, longer-lasting wildcard certificates (recommended, one certificate for multiple subdomains), you'll need to spend some money.
Logs
Checking logs is an essential skill. Just remember the tail command:
| Default | brew Installation | |
|---|---|---|
| Access Log | tail -100f /var/log/nginx/access.log |
tail -100f /opt/homebrew/var/log/nginx/access.log |
| Error Log | tail -100f /var/log/nginx/error.log |
tail -100f /opt/homebrew/var/log/nginx/access.log |
You already know Nginx can host multiple Servers. A thoughtful person like you will also realize that if the access.log and error.log above mix logs from all Servers, it will cause unnecessary trouble. You can configure the log path to a dedicated file under the corresponding service. For example, if you have a Server for verynb.com:
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;
...
}
📕 Configuration Theory Knowledge
Variables
Nginx has many built-in variables, which are the $xx you see in configuration files. Here are some core variables (compiled by AI):
| Variable Name | Meaning | Typical Use Case |
|---|---|---|
$remote_addr |
Client's IP address (public/private) | Logging, rate limiting, hotlink protection |
$remote_port |
Port number of the client's connection to Nginx | Log troubleshooting, connection count statistics |
$remote_user |
Username of the client authenticated via HTTP | Access control, log auditing |
$http_user_agent |
Client's User-Agent (browser/device info) | Device adaptation, anti-crawler |
$http_referer |
Source page of the client's request (Referer header) | Hotlink protection, source statistics |
$http_cookie |
Cookie information carried by the client | Authentication, personalized configuration |
$request_method |
HTTP request method | Restrict request methods, logging |
$request_uri |
Complete URI requested by the client (including parameters) | Logging, redirection |
$request_filename |
Local file path corresponding to the request (static resource scenario) | Static resource caching, file access control |
$uri |
Request URI (without parameters, decoded) | Reverse proxy, path matching |
$args |
URL parameters of the request | Parameter forwarding, logging |
$host |
Hostname of the request (prioritizes Host header, then server domain name) | Multi-domain configuration, reverse proxy |
$server_name |
server_name of the current server block in Nginx config (fixed value) |
Multi-domain differentiation, logging |
$server_port |
Port number on which Nginx received the request | Port adaptation, logging |
$scheme |
Protocol of the request http/https |
Force HTTPS redirect, logging |
$status |
HTTP status code returned by Nginx to the client | Log statistics, error monitoring |
$bytes_sent |
Number of bytes sent by Nginx to the client | Traffic statistics, rate limiting |
$request_time |
Total request processing time (in seconds, with decimals) | Performance monitoring, slow request troubleshooting |
$proxy_add_x_forwarded_for |
Concatenates the client's real IP (X-Forwarded-For header), passed to the backend | Backend obtains client's real IP |
$upstream_addr |
Address of the backend server forwarded to by the reverse proxy (IP: port) | Backend cluster troubleshooting, logging |
$upstream_status |
HTTP status code returned by the backend server | Backend error monitoring |
$upstream_response_time |
Response time of the backend server (reverse proxy scenario) | Backend performance monitoring |
Nginx Core Configuration Items
Nginx's default core configuration file nginx.conf can be divided into 6 modules:
- Global Block: Global configuration, effective globally.
- events: Configures settings affecting Nginx server's network connections with users.
- http: The most frequently used configuration, for configuring proxy, caching, logging, and third-party module settings.
- server: Configures parameters for virtual hosts. An http block can have multiple server blocks.
- location: Used to configure matching URIs.
- upstream: Configures specific backend server addresses, load balancing configuration.
🙋 FAQ
❓ How to view all domain names being proxied by Nginx?
This is a fairly common need; I just want to see what services this machine is hosting. But annoyingly, Nginx does not provide such a convenience. Fortunately, there is a workaround:
nginx -T 2>/dev/null | grep -E "\sserver_name\s+" | grep -v "#" | awk '{print $2}' | tr ';' ' ' | tr ',' ' '
A few points need clarification for the above command:
- Don't ask me about details like
2>/dev/null. - Actually, it doesn't represent the word "currently," because it just extracts all fields after
server_namefrom the output ofnginx -Tusing regex. This might include commented-out parts, or Nginx might not even be running.
❓ What's going on when the real CDN file is accessed?
Theoretically, when using a CDN proxy, you shouldn't see the CDN's address, especially not in the browser's address bar. But what happened when it did appear?
The reason is actually quite simple: you likely mistyped the CDN's https as http, triggering a 302 redirect from the CDN, causing the page address to be forcefully forwarded to https.