@bhushanwho

an nginx primer

Sep 07, 2026

example.conf

here’s a complete, working nginx config for a reverse proxy setup with load balancing. everything below is a breakdown of this.


upstream backend_pool {
    least_conn;
    server 10.0.0.1:8080 weight=3;
    server 10.0.0.2:8080;
    server 10.0.0.3:8080 backup;
}

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

    location / {
        proxy_pass http://backend_pool;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    location /static/ {
        root /var/www;
    }

    location /images/ {
        alias /var/www/media/images/;
    }
}

request comes in on port 80, nginx checks it against each location block, and either forwards it into backend_pool or serves a file straight off disk.

the upstream block


upstream backend_pool {
    least_conn;
    server 10.0.0.1:8080 weight=3;
    server 10.0.0.2:8080;
    server 10.0.0.3:8080 backup;
}

this defines a named pool of backend servers. it does nothing by itself — a server block has to reference it via proxy_pass for it to matter.

other per-server options you’ll run into: max_fails and fail_timeout (how many failures in what window before nginx marks a server dead), and down (manually take a server out of rotation without deleting the line).

the server block


server {
    listen 80;
    server_name myapp.com www.myapp.com;
    ...
}

this is what actually receives connections.

the location block


location / {
    proxy_pass http://backend_pool;
    ...
}

location matches request paths against a pattern and decides what happens to matching requests. a server block usually has several of these, one per route or asset type. matching isn’t strictly first-match — nginx has its own precedence rules (exact match, then longest prefix, then regex), so more specific locations don’t have to be listed in any particular order to win.

proxy_pass vs root vs alias

these three are the actual workhorses inside a location block.

proxy_pass forwards the request to another server and relays the response back. use it for anything dynamic — an app server, an api, another nginx instance.


location /api/ {
    proxy_pass http://backend_pool;
}

the trailing slash on the target url changes the forwarded path:


location /api/ {
    proxy_pass http://10.0.0.1:8080/;
}
# /api/users -> forwarded as /users (prefix stripped)

location /api/ {
    proxy_pass http://10.0.0.1:8080;
}
# /api/users -> forwarded as /api/users (full path kept)

root serves a file straight from disk, appending the full request path to the root path.


location /static/ {
    root /var/www;
}
# /static/logo.png -> /var/www/static/logo.png

alias also serves from disk, but replaces the location prefix instead of appending to it.


location /images/ {
    alias /var/www/media/images/;
}
# /images/logo.png -> /var/www/media/images/logo.png

mixing up root and alias is a common source of 404s and wrong-file bugs. root keeps the matched prefix in the final path; alias throws it away.

proxy_set_header


proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

when nginx proxies a request, the backend sees nginx as the client by default, not the original visitor. these headers pass the real information through:

load balancing methods

round robin

the default. no directive needed. requests rotate through the server list in order — one, two, three, back to one. stateless, no memory of past requests.

least_conn


upstream backend_pool {
    least_conn;
    server 10.0.0.1:8080;
    server 10.0.0.2:8080;
}

sends each new request to whichever backend currently has the fewest open connections. better than round robin when requests vary a lot in how long they take to handle.

ip_hash


upstream sticky {
    ip_hash;
    server 10.0.0.1:8080;
    server 10.0.0.2:8080;
}

hashes the client’s ip address to consistently pick the same backend for the same client. this is the standard open-source way to get sticky behavior.

hash (generic)


upstream backend_pool {
    hash $request_uri consistent;
    server 10.0.0.1:8080;
    server 10.0.0.2:8080;
}

like ip_hash but you choose the key — a url, a header, a cookie value, whatever nginx variable makes sense. consistent enables consistent hashing, which minimizes remapping when servers are added or removed (regular hashing reshuffles almost everything when the pool size changes; consistent hashing only reshuffles a fraction).

when to actually use sticky/hash-based routing

only when the backend is holding state locally — session data in memory, a websocket connection, an in-progress upload. if session state lives in a shared store like redis, none of this matters and plain round robin or least_conn works fine, which is usually the better place to put that state anyway since it also gives you failover.