← ProgrammingBack to top

Nginx: Web Server and Reverse Proxy

Nginx is a high-performance Web Server and Reverse Proxy (reverse proxy server).

Main uses:

  • Provide static resources (HTML, JS, CSS, pictures).
  • Reverse proxy Django, FastAPI and other backends.
  • HTTPS / TLS terminated.
  • HTTP compression and caching.
  • Load balancing and rate limiting.
  • WebSocket, SSE and other long-lasting connections or streaming response proxies.

For the React + Django + Docker project:

Browser
   ↓
Cloudflare(Optional)
   ↓
Nginx
   ├── /          → React SPA
   ├── /assets/   → JS / CSS
   ├── /api/      → Django DRF
   ├── /admin/    → Django Admin
   └── /static/   → Django Static Files

1. nginx.conf configuration structure

Nginx uses a different configuration context (Context) to organize the configuration.

events {
    worker_connections 1024;
}

http {
    server {
        listen 80;
        server_name example.com;

        location / {
            # ...
        }

        location /api/ {
            # ...
        }
    }
}

Main levels:

nginx.conf
├── Main Context
├── events
└── http
    ├── upstream
    ├── server
    │   ├── location /
    │   └── location /api/
    └── server
Contextfunction
mainGlobal settings, such as worker_processes
eventsConnection event handling
httpPublic configuration of HTTP service
upstreamDefine backend server group
serverVirtual host, handles specified ports and domain names
locationMatch and process the request by URI

worker_processes and worker_connections

worker_processes auto;

events {
    worker_connections 1024;
}
  • worker_processes: Number of Nginx Worker processes, auto is usually determined based on available CPU resources.
  • worker_connections: The maximum number of connections allowed per Worker, not the maximum number of requests.

The theoretical connection capacity is roughly limited by the product of the two. However, during reverse proxy, a client request may occupy both the client connection and the upstream connection, so the product cannot be directly regarded as the maximum number of concurrent users.

Configuration files in Docker

Official Nginx images usually use:

/etc/nginx/nginx.conf
/etc/nginx/conf.d/default.conf

Generally speaking:

  • nginx.conf: Contains top-level structures such as events and http.
  • conf.d/default.conf: Usually only server Block is defined, introduced by the main configuration through include.

Therefore, the configuration file mounted to conf.d/default.conf generally does not need to write http {} and events {}.

2. Server Block and HTTPS

HTTP redirect HTTPS

server {
    listen 80;
    server_name example.com www.example.com;

    return 301 https://$host$request_uri;
}

For example:

http://example.com/blog?id=1
        ↓ 301
https://example.com/blog?id=1
  • listen 80: Monitor port 80 of HTTP.
  • server_name: Matches the requested domain name.
  • return 301: Permanent redirect.
  • $host: The requested host name parsed by Nginx.
  • $request_uri: Original request URI, containing Query String.

HTTPS Server

server {
    listen 443 ssl;
    server_name example.com;

    ssl_certificate /etc/ssl/fullchain.pem;
    ssl_certificate_key /etc/ssl/privkey.pem;

    client_max_body_size 10M;

    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log warn;
}

Nginx is responsible for the TLS handshake, certificate, and encrypted HTTP connection.

When using Cloudflare, you can use:

Browser
   ↓ HTTPS
Cloudflare
   ↓ HTTPS
Nginx
   ↓ HTTP(internal network)
Django

Typically Cloudflare Full (strict) should be used, and a valid certificate configured for the origin site, ensuring that the connection from Cloudflare to Nginx is also authenticated.

3. Location: URI matching

location is used to select the processing method based on request URI.

location / {
    # ...
}

location /api/ {
    # ...
}

Common matching methods

Grammarmeaning
location = /apiexact match
location /api/prefix matching
location ^~ /assets/Prefix matching and skipping regular matching checks
location ~ \.css$Case-sensitive regular matching
location ~* \.(png|jpg)$Case-insensitive regular matching

Match priority

Basic rules:

  1. Exact match = takes precedence.
  2. Find the longest prefix match.
  3. If the longest prefix uses ^~, select it directly.
  4. Otherwise, the regular rules Location are checked in the configuration order, and the first matching regular rule takes effect.
  5. When there is no regular match, the longest prefix found previously is used.

Note: URI matches do not include Query String.

4. Reverse Proxy: reverse proxy

Assume Django is listening in Docker:

backend:8000

Nginx:

location /api/ {
    proxy_pass http://backend:8000;

    proxy_set_header Host $host;
    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;
}

Request:

GET https://example.com/api/v1/users/me/
                   ↓
                 Nginx
                   ↓
GET http://backend:8000/api/v1/users/me/

Where backend can be the service name of Docker Compose.

The trailing slash of proxy_pass

This is where things can easily go wrong.

location /api/ {
    proxy_pass http://backend:8000;
}

Preserve path prefix:

/api/v1/users/ → /api/v1/users/

However:

location /api/ {
    proxy_pass http://backend:8000/;
}

will replace the matching /api/ prefix with /:

/api/v1/users/ → /v1/users/

The reason is that the second proxy_pass contains URI /, and Nginx will perform corresponding path replacement.

For projects where Django already has the /api/v1/ prefix defined, it is common to use proxy_pass http://backend:8000; without the trailing slash.

5. proxy_set_header: Pass request information

Headerfunction
HostOriginal requested domain name
X-Real-IPTell the backend client IP
X-Forwarded-ForRecord IP in the proxy chain
X-Forwarded-ProtoTell the backend the original request protocol
proxy_set_header Host $host;
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;

Common variables:

variablemeaning
$hostResolved request hostname
$remote_addrThe client address currently recognized by Nginx
$proxy_add_x_forwarded_forOriginal X-Forwarded-For appended to the current client address
$schemeThe current connection is using HTTP or HTTPS
$request_uriOriginal URI, containing Query String
$uriCurrently normalized to URI, excluding Query String
$http_upgradeUpgrade Header in request

Cloudflare vs real client IP

If using Cloudflare proxy:

Browser IP
    ↓
Cloudflare IP
    ↓
Nginx

The $remote_addr that Nginx sees by default may be the Cloudflare node IP.

Guest IP can be restored through the Real IP module:

# Just for illustration, replace with Cloudflare Complete official announcement IP segment
set_real_ip_from <trusted_cloudflare_cidr>;
real_ip_header CF-Connecting-IP;

You must only trust real Cloudflare proxy addresses and ensure that the origin does not accept requests from untrusted proxies that can be forged.

After recovery, $remote_addr will represent the client address identified by the trusted proxy information.

The Cloudflare IP list requires regular maintenance. Refer to Cloudflare official document .

6. Static Files and SPA routing

Vite build results:

dist/
├── index.html
└── assets/
    ├── index-Ab12.js
    └── index-Cd34.css

Nginx:

root /usr/share/nginx/html;
index index.html;

location / {
    try_files $uri $uri/ /index.html;
}

Processing flow:

GET /profile
      ↓
Check /usr/share/nginx/html/profile
      ↓
does not exist
      ↓
Return index.html
      ↓
React Router Process /profile

root and alias

root splices request URI to the static file root directory:

location /assets/ {
    root /usr/share/nginx/html;
}

Request:

/assets/app.js
→ /usr/share/nginx/html/assets/app.js

alias replaces matching Location prefixes with the specified file system path:

location /media/ {
    alias /data/media/;
}

Request:

/media/avatar/a.jpg
→ /data/media/avatar/a.jpg

For directory type Location, the alias path usually needs to keep the tail / corresponding to the prefix.

7. HTTP cache

For static files generated by Vite with Hash:

location /assets/ {
    try_files $uri =404;
    expires 1y;
    add_header Cache-Control "public, immutable";
}

This way:

index-A1b2.js

Can be cached for one year.

Build after update generates:

index-C3d4.js

The browser will treat it as a new resource.

index.html

location = /index.html {
    add_header Cache-Control "no-cache";
}

no-cache means that verification is required before caching is used, rather than disabling caching completely.

NOTE: Paths such as /profile for SPA may return to HTML via a /index.html internal redirect, so confirmation that the final HTML response has the appropriate cache Header is also required.

Do not cache the fallback HTML of SPA for a long time, and do not let non-existent JS files fall back to HTML.

8. gzip / Brotli compression

Nginx can compress the text HTTP response to reduce the network transmission volume.

gzip

gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_comp_level 6;

gzip_types
    text/plain
    text/css
    application/javascript
    application/json
    image/svg+xml;

Brotli

Requires installation of compatible Brotli Nginx module:

brotli on;
brotli_comp_level 5;

brotli_types
    text/plain
    text/css
    application/javascript
    application/json
    image/svg+xml;

Both algorithms can be configured simultaneously.

Browser
    ↓ Accept-Encoding: br, gzip
Nginx
    ↓ Content-Encoding: br
Compressed Response

Usually Brotli takes precedence, gzip as fallback.

Note: The official nginx:alpine image usually cannot directly enable the Brotli command. You must first ensure that the corresponding module exists and is loaded correctly.

9. Request size and proxy timeout

client_max_body_size

client_max_body_size 10M;

Limit client request body size.

Exceeding the limit usually returns:

413 Content Too Large

Suitable for restricting avatars, pictures and other uploaded content.

proxy timeout

proxy_connect_timeout 5s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
instructionsfunction
proxy_connect_timeoutTimeout for establishing an upstream connection
proxy_send_timeoutThe allowed timeout between two consecutive write operations to the upstream
proxy_read_timeoutThe timeout allowed between two consecutive read operations from the upstream

proxy_read_timeout is not the total execution time limit for the entire HTTP request.

If the backend keeps sending data, this timeout may not be triggered even if the total response time exceeds 60 seconds.

10. Buffering: response buffering

Nginx enables proxy response buffering by default:

proxy_buffering on;

Generally the API response can be left as default.

How it works:

Django
   ↓
Nginx Buffers
   ↓
Browser

Nginx will read the upstream response and buffer it as much as possible, which will help reduce the occupation of the backend by slow clients.

But for interfaces such as SSE that need to transmit data immediately, they can be closed:

proxy_buffering off;

At this time, the response will be delivered to the client in a more timely manner.

proxy_request_buffering is another configuration, which controls whether the request body is buffered in Nginx first and cannot be confused with response buffering.

11. WebSocket proxy

WebSocket requires special Header when upgrading HTTP/1.1 connections.

location /ws/ {
    proxy_pass http://backend:8000;

    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";

    proxy_read_timeout 3600s;
}

Basic process:

Browser
   ↓ HTTP Upgrade
Nginx
   ↓
ASGI / WebSocket Server
   ↓
Persistent Connection

For configurations that mix normal HTTP with WebSocket, Connection Header can also be set dynamically via map.

Note: Django itself does not directly provide complete WebSocket application routing, generally requiring Django Channels or other ASGI WebSocket processing solutions.

12. SSE / Streaming

SSE (Server-Sent Events) is used by the server to continuously send events to the client. The communication direction is mainly:

Server → Browser

Typical scenario:

  • AI Token streaming output.
  • Mission progress updates.
  • Real-time notifications.
  • Server event push.

Nginx configuration

location /api/v1/stream/ {
    proxy_pass http://backend:8000;

    proxy_http_version 1.1;
    proxy_buffering off;
    proxy_cache off;

    proxy_read_timeout 300s;

    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;
}

Key configuration:

  • proxy_buffering off: Avoid Nginx buffer response and delay event output.
  • proxy_cache off: Do not use the Nginx proxy cache.
  • proxy_read_timeout 300s: Allow longer wait times between upstream reads.

The backend usually returns:

Content-Type: text/event-stream
Cache-Control: no-cache

Response content:

data: {"message":"Hello"}

data: {"message":"World"}

Each event is separated by a blank line.

For connections that have no new events for a long time, the server can send heartbeats periodically to avoid the connection from timing out due to idleness.

Alternatively, you can have the backend return:

X-Accel-Buffering: no

Nginx buffering is disabled for specific responses, but must be confirmed not to be disabled by proxy_ignore_headers.

13. Load balancing

When multiple backend instances exist:

upstream django_backend {
    server backend1:8000;
    server backend2:8000;
    server backend3:8000;
}

server {
    location /api/ {
        proxy_pass http://django_backend;
    }
}

Architecture:

             ┌── Django 1
Browser      │
   ↓         ├── Django 2
Nginx ───────┤
             └── Django 3

The default common polling method is used to allocate requests.

Other strategies include:

  • least_conn: Prefer backends with fewer active connections.
  • ip_hash: Relatively fixed allocation based on client IP.

For small projects deployed on a single Docker Compose VPS, multi-instance load balancing is not necessarily required.

14. rate limiting (Rate Limiting)

Nginx can limit the request frequency according to IP and other conditions.

Defined in the http context:

limit_req_zone
    $binary_remote_addr
    zone=api:10m
    rate=10r/s;

In Location use:

location /api/ {
    limit_req zone=api burst=20;
    limit_req_status 429;

    proxy_pass http://backend:8000;
}

Key parameters:

parametersfunction
$binary_remote_addrBinary representation of client IP
zone=api:10mrate limiting state shared memory area
rate=10r/s10 requests per second on average
burst=20Allow for a certain degree of request bursting
limit_req_status 429Returns 429 when rate limiting is triggered

When nodelay is not set, some excess requests within Burst may be delayed rather than passed immediately.

When using Cloudflare, you should first correctly restore the real client IP, otherwise the current may be limited according to the Cloudflare node IP, causing multiple real users to share the same rate limiting identity.

In addition, Nginx rate limiting cannot replace the user-level rate limiting and business permission control within Django.

15. Logs and troubleshooting

Access Log

Record HTTP access status:

access_log /var/log/nginx/access.log;

Usually includes:

  • Request IP.
  • The request method is URI.
  • HTTP status code.
  • Response size.
  • Request time (depends on log format).

Error Log

error_log /var/log/nginx/error.log warn;

Logs proxy failures, connection errors, partial upstream errors, and errors for Nginx itself.

Docker Log

The official Nginx Docker mirror usually has access logs and error logs connected to stdout/stderr.

You can use:

docker compose logs -f nginx

View container logs.

Configuration test

nginx -t

Check the configuration syntax and related files.

See the full configuration actually loaded:

nginx -T

Reload configuration:

nginx -s reload

Docker environment:

docker compose exec nginx nginx -t
docker compose exec nginx nginx -s reload

16. Responsibilities of Nginx and Django

FunctionNginxDjango
HTTPS / TLSMainly responsible forUsually not dealt with directly
static filesResponsible for providingResponsible for building or collecting
reverse proxyresponsibleReceive proxy requests
HTTP compressionusually responsibleOptional
HTTP cacheStatic resources and proxy cachingBusiness cache
API routingforwardBusiness routing
Authenticationusually irresponsibleresponsible
Authorizationusually irresponsibleresponsible
File upload size limitFirst level restrictionsBusiness level verification
Request rate limitingIP/request level rate limitingUser/business level rate limiting
Background tasksirresponsibleCelery and other task systems

Summary

Nginx The most important knowledge can be summarized as:

modulecore concepts
Configuration structureevents、http、server、location
Request routingserver_name、location、proxy_pass
static filesroot、alias、try_files
network securityTLS, Real IP, rate limiting
Performance optimizationgzip/Brotli、Cache-Control、Buffering
long connectionWebSocket、SSE
Operation and maintenanceAccess Log、Error Log、nginx -t、Reload

For the React + Django + Docker Compose project, it is most worth mastering:

  1. location matching and proxy_pass path handling.
  2. Header pass-through and proxy trust configuration between Nginx and Django.
  3. try_files processing of React SPA.
  4. Long-term caching of static resources with HTML revalidation.
  5. gzip/Brotli compression.
  6. HTTP request timeout, Buffering and SSE.
  7. Log and configuration checks in Docker environments.

Reference documentation