LMX
BlogSelectedWorkSponsorsAbout
Command Palette

Search for a command to run...

Blog

Easy-to-Miss Nginx Details

LMXDec 11, 202510 min read#Nginx

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:

  • /test matches /test, /test/abc, /test123, and /test-123.
  • /test/ matches paths beginning with /test/, but not /test or /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 nginx binary in PATH, 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 syntax

A safe server workflow is:

nginx -t
systemctl reload nginx
systemctl status nginx

# Use the direct method when systemctl is unavailable.
sudo nginx -s reload

Prefer 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 nginx

For 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-release

Download 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.deb

Transfer 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-*.deb

Start and verify the service:

sudo systemctl start nginx
sudo systemctl enable nginx
sudo systemctl status nginx

nginx -v
curl http://localhost

Building 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:

  1. MIME type: Serve .wasm as application/wasm.
  2. Artifact handling: Set correct content types and encodings for uncompressed, gzip, and Brotli WASM, data, and JavaScript files.
  3. Static caching: Give hashed build assets long-lived cache headers.
  4. HTML updates: Disable HTML caching so users receive the latest deployment.
  5. Performance: Enable appropriate compression.
  6. Security: Deny hidden and backup files and add basic response headers.

Discussion

Support the author
Previous post

Can you install packages without internet? Setting up a private npm repository on an intranet

Next post

Worked hard on Vue3 projects for a year and a half, summarizing 16 coding standards

LMX BlogWeb · TypeScript · Vue · Node.js
Crafted by
@LMXZQQ
Build
unavailable
Date
2026-10-30
Articles
29
Deployed on
Nginx
Source code
GitHub
License
MIT License
Typeface
Geist
Stack
  • next@16.4.3
  • react@19.3.1
  • @base-ui/react@1.8.0
  • shadcn@4.12.0
  • tailwindcss@4.3.3
Analytics
  • OpenPanel
  • Google Analytics
For agents
  • llms.txt
  • index.md
Inspired by
  1. Tailwind CSS
  2. shadcn/ui
  3. Vercel
  4. Apple
  5. Evil Charts
  6. Devouring Details
  7. Skiper UI
  8. Making Software
  9. shadcncraft
Approved by
静水流深 / LMX
1602
Command Palette

Search for a command to run...