A collection of useful but often overlooked Nginx details: proxy path behavior, location priority, reload commands, offline installation, and deployment configuration for Unity WebAssembly applications.
Here are several Nginx details I encountered in practice. They are easy to overlook, but understanding them can prevent confusing deployment problems.
Question 1: Trailing Slashes and Location Matching
Compare these configurations:
# Configuration 1
location /test {
proxy_pass http://192.186.0.1:8080;
}
# Configuration 2
location /test {
proxy_pass http://192.186.0.1:8080/;
}The only visible difference is the trailing / in proxy_pass, but it changes the upstream URI. For an incoming request to /test/file/getList:
- Configuration 1 sends
http://192.186.0.1:8080/test/file/getList. - Configuration 2 sends
http://192.186.0.1:8080/file/getList.
In other words, when proxy_pass includes a URI—in this case /—Nginx replaces the matching location prefix with that URI. If the backend route includes /test, omit the trailing slash. If /test exists only as a frontend reverse-proxy prefix, include it.
Although the short form is convenient, an explicit rewrite can be easier for teammates unfamiliar with Nginx to understand:
location /test {
rewrite ^/test/(.*)$ /$1 break;
proxy_pass http://192.186.0.1:8080;
}Now compare the location itself with and without a trailing slash:
location /test {
proxy_pass http://192.186.0.1:8080;
}
location /test/ {
proxy_pass http://192.186.0.1:8080;
}These prefix matches are different:
/testmatches/test,/test/abc,/test123, and/test-123./test/matches paths beginning with/test/, but not/testor/test123.
| Request | /test | /test/ | Selected match |
|---|---|---|---|
/test | ✅ | ❌ | /test |
/test/ | ✅ | ✅ | /test/ |
/test/abc | ✅ | ✅ | /test/ |
/test123 | ✅ | ❌ | /test |
/test-123 | ✅ | ❌ | /test |
When both ordinary prefixes match, Nginx chooses the longest one. A useful location-priority mnemonic is:
Exact `=` comes first.
Prefix `^~` prevents regular-expression checks.
Regular expressions `~` and `~*` are tested in configuration order.
Ordinary prefixes use the longest match.In practice, it is often faster to consult documentation or have an agent inspect nginx.conf than to memorize every rule. Understanding the principle still matters when you review the result.
The following compact e-commerce example demonstrates all four matching categories:
server {
listen 80;
server_name shop.example.com;
root /var/www/shop;
# 1. Exact matches have the highest priority.
location = / {
return 200 "Home page [exact match =]";
add_header Content-Type text/plain;
}
location = /robots.txt {
return 200 "User-agent: *\nDisallow: /admin/";
add_header Content-Type text/plain;
}
location = /favicon.ico {
log_not_found off;
access_log off;
expires 30d;
}
# 2. ^~ selects a prefix and skips regular-expression locations.
location ^~ /static/ {
alias /var/www/shop/static/;
expires 30d;
add_header Cache-Control "public, immutable";
}
location ^~ /uploads/ {
alias /var/www/shop/uploads/;
expires 7d;
}
location ^~ /. {
deny all;
}
# 3. Regular expressions are checked in declaration order.
location ~ \.(jpg|jpeg|png|gif|webp|svg|ico)$ {
expires 30d;
add_header Cache-Control "public";
}
location ~* \.(css|js)$ {
expires 7d;
add_header Cache-Control "public";
}
location ~* \.(ttf|woff|woff2|eot)$ {
expires 365d;
add_header Cache-Control "public, immutable";
add_header Access-Control-Allow-Origin *;
}
location ~ \.(bak|backup|old|tmp)$ {
deny all;
}
# 4. Ordinary prefixes use the longest matching prefix.
location /api/v2/ { proxy_pass http://backend_v2; }
location /api/v1/ { proxy_pass http://backend_v1; }
location /api/ { proxy_pass http://backend; }
location /product/ {
try_files $uri $uri/ /product/index.html;
}
location /user/ {
try_files $uri $uri/ /user/index.html;
}
location /admin/ {
auth_basic "Admin Area";
auth_basic_user_file /etc/nginx/.htpasswd;
}
# Catch-all fallback.
location / {
try_files $uri $uri/ /index.html;
}
}Expected matches:
| Request URI | Selected location | Priority type | Explanation |
| ----------------------- | ----------------- | ---------------- | -------------------------- | ----------------------- |
| / | = / | Exact | Highest priority |
| /index.html | / | Ordinary prefix | Catch-all |
| /robots.txt | = /robots.txt | Exact | Exact match |
| /static/css/style.css | ^~ /static/ | Preferred prefix | Skips regex checks |
| /uploads/avatar.jpg | ^~ /uploads/ | Preferred prefix | Skips image regex |
| /images/logo.png | image regex | Regex | Matches the extension |
| /js/app.JS | ~\* \.(css | js)$ | Case-insensitive regex | Matches uppercase .JS |
| /api/v2/products | /api/v2/ | Longest prefix | Most specific API prefix |
| /api/v1/users | /api/v1/ | Longest prefix | More specific than /api/ |
| /api/orders | /api/ | Ordinary prefix | Generic API route |
| /.git/config | ^~ /. | Preferred prefix | Access denied |
| /backup.bak | backup regex | Regex | Access denied |
Question 2: nginx -s reload vs. systemctl reload nginx
Both reload Nginx gracefully, but they operate differently.
nginx -s reload is Nginx's native signal command:
- Sends a reload signal directly to the Nginx master process.
- Loads new configuration without interrupting existing connections.
- Requires the
nginxbinary inPATH, or an absolute path such as/usr/sbin/nginx. - Works without systemd.
systemctl reload nginx asks systemd to manage the service:
- Uses the distribution's Nginx service unit.
- Also performs a graceful reload.
- Requires a systemd-based Linux environment.
- Is the standard choice on modern CentOS, RHEL, Ubuntu, and similar distributions.
Common commands:
systemctl enable nginx # Start at boot
systemctl start nginx # Start the service
systemctl status nginx # Inspect service status
systemctl stop nginx # Stop the service
systemctl restart nginx # Restart; existing connections may be interrupted
systemctl reload nginx # Gracefully reload configuration
nginx -t # Validate Nginx configuration syntaxA safe server workflow is:
nginx -t
systemctl reload nginx
systemctl status nginx
# Use the direct method when systemctl is unavailable.
sudo nginx -s reloadPrefer systemctl when the operating system manages Nginx as a service. Use the native nginx command in local environments or systems without systemd.
Question 3: Installing Nginx Offline
An online Ubuntu or Debian installation is straightforward:
sudo apt update
sudo apt install nginx
sudo systemctl start nginx
sudo systemctl enable nginxFor an offline server, first identify its architecture and distribution:
uname -m
# x86_64 -> Intel/AMD 64-bit
# aarch64 -> ARM 64-bit
# armv7l -> ARM 32-bit
cat /etc/os-releaseDownload a package matching both the operating system and architecture on a connected machine. Examples from the original article:
# CentOS 7 x86_64
wget http://nginx.org/packages/centos/7/x86_64/RPMS/nginx-1.24.0-1.el7.ngx.x86_64.rpm
# Ubuntu Jammy ARM64
wget http://nginx.org/packages/ubuntu/pool/nginx/n/nginx/nginx_1.24.0-1~jammy_arm64.debTransfer the package to the server, then install it with the appropriate package manager:
# CentOS/RHEL RPM
cd /tmp
sudo rpm -ivh nginx-*.rpm
# Ubuntu/Debian DEB
cd /tmp
sudo dpkg -i nginx-*.debStart and verify the service:
sudo systemctl start nginx
sudo systemctl enable nginx
sudo systemctl status nginx
nginx -v
curl http://localhostBuilding from source is another option, but it is generally unnecessary unless custom modules or build flags are required.
Question 4: Deploying a Unity WebAssembly Build
A normally configured static server may fail to load a Unity WebGL build with an error such as:
Failed to load module script: The server responded with a non-JavaScript MIME type of
application/wasm.
Two areas need attention.
First, configure the correct MIME type. Add this to Nginx's mime.types if it is missing:
application/wasm wasm;Second, serve uncompressed, gzip, and Brotli build artifacts with the correct content encodings and types. The following configuration is a practical starting point; change the domain, root, and port for your environment.
server {
listen 80;
server_name your-domain.com;
root /var/www/unity-webgl;
index index.html;
charset utf-8;
access_log /var/log/nginx/unity-game-access.log;
error_log /var/log/nginx/unity-game-error.log;
location ~ \.wasm$ {
types { application/wasm wasm; }
add_header Cache-Control "public, max-age=31536000, immutable";
add_header Access-Control-Allow-Origin *;
}
location ~ \.wasm\.gz$ {
add_header Content-Encoding gzip;
add_header Content-Type application/wasm;
add_header Cache-Control "public, max-age=31536000, immutable";
}
location ~ \.wasm\.br$ {
add_header Content-Encoding br;
add_header Content-Type application/wasm;
add_header Cache-Control "public, max-age=31536000, immutable";
}
location ~ \.data$ {
types { application/octet-stream data; }
add_header Cache-Control "public, max-age=31536000, immutable";
}
location ~ \.data\.gz$ {
add_header Content-Encoding gzip;
add_header Content-Type application/octet-stream;
add_header Cache-Control "public, max-age=31536000, immutable";
}
location ~ \.data\.br$ {
add_header Content-Encoding br;
add_header Content-Type application/octet-stream;
add_header Cache-Control "public, max-age=31536000, immutable";
}
location ~ \.js\.gz$ {
add_header Content-Encoding gzip;
add_header Content-Type application/javascript;
add_header Cache-Control "public, max-age=31536000, immutable";
}
location ~ \.js\.br$ {
add_header Content-Encoding br;
add_header Content-Type application/javascript;
add_header Cache-Control "public, max-age=31536000, immutable";
}
location /StreamingAssets/ {
add_header Cache-Control "public, max-age=31536000, immutable";
}
location /Build/ {
add_header Cache-Control "public, max-age=31536000, immutable";
}
location /TemplateData/ {
add_header Cache-Control "public, max-age=86400";
}
location ~* \.(jpg|jpeg|png|gif|ico|svg|css)$ {
add_header Cache-Control "public, max-age=2592000";
}
# Do not cache HTML so deployments become visible immediately.
location ~ \.html$ {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
}
location / {
try_files $uri $uri/ /index.html;
}
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_min_length 1024;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/xml
application/wasm
application/octet-stream;
location ~ /\. { deny all; }
location ~ ~$ { deny all; }
add_header X-Content-Type-Options "nosniff";
add_header X-Frame-Options "SAMEORIGIN";
}The core requirements are:
- MIME type: Serve
.wasmasapplication/wasm. - Artifact handling: Set correct content types and encodings for uncompressed, gzip, and Brotli WASM, data, and JavaScript files.
- Static caching: Give hashed build assets long-lived cache headers.
- HTML updates: Disable HTML caching so users receive the latest deployment.
- Performance: Enable appropriate compression.
- Security: Deny hidden and backup files and add basic response headers.
Discussion
Support the author