System Design / Practical

Test a load balancer with NGINX

Run two small web servers, send requests through NGINX, and inspect which server answered each request.

1. Install NGINX

Choose the instructions for your operating system. NGINX uses port 8080 by default when installed through Homebrew on macOS; other packages may use port 80 for their welcome page. Our demo’s load balancer will listen on port 8080.

macOS · Homebrew

If Homebrew is installed, install NGINX and start its service:

Terminal · Install and start NGINX on macOS
brew install nginx
brew services start nginx

Check the installation with nginx -v. Homebrew’s NGINX config folder is usually /opt/homebrew/etc/nginx on Apple Silicon or /usr/local/etc/nginx on Intel Macs.

Linux · Ubuntu or Debian

Install the distribution’s NGINX package and start it:

Terminal · Install and start NGINX on Ubuntu/Debian
sudo apt update
sudo apt install nginx
sudo systemctl enable --now nginx

Linux · Fedora

For Fedora, use its package manager:

Terminal · Install and start NGINX on Fedora
sudo dnf install nginx
sudo systemctl enable --now nginx

On these Linux distributions, the main config is usually /etc/nginx/nginx.conf. The packaged config normally includes files from /etc/nginx/conf.d/ inside its HTTP section.

Windows · WinGet or official zip

If you have Microsoft’s WinGet package manager, you can install the available NGINX package from its community manifest:

PowerShell · Install with WinGet
winget install --id nginxinc.nginx --exact

For this lesson, the official zip method makes the configuration location predictable. Download the Windows build, extract it to C:\nginx, and start it from PowerShell. The official Windows build is marked beta, so use it here for learning rather than production.

Download NGINX for Windows ↗ · View the WinGet community manifest ↗

PowerShell · Start NGINX for Windows
cd C:\nginx
.\nginx.exe
.\nginx.exe -v

The main config is C:\nginx\conf\nginx.conf. PowerShell alternatives for the server setup and tests appear below. For NGINX’s Windows config, use forward slashes in paths.

2. Start two web servers

Each server needs a different page so we can tell which one answered. These commands create two small folders and pages. Run them one at a time in Terminal:

Terminal · Create two different pages
mkdir -p ~/nginx-lb-demo/server1 ~/nginx-lb-demo/server2
printf '<h1>Welcome to Server 1</h1>\n' > ~/nginx-lb-demo/server1/index.html
printf '<h1>Welcome to Server 2</h1>\n' > ~/nginx-lb-demo/server2/index.html

In native Windows PowerShell, create the two pages like this:

PowerShell · Create two different pages
New-Item -ItemType Directory -Force "$HOME\nginx-lb-demo\server1" | Out-Null
New-Item -ItemType Directory -Force "$HOME\nginx-lb-demo\server2" | Out-Null
Set-Content -Path "$HOME\nginx-lb-demo\server1\index.html" -Value '<h1>Welcome to Server 1</h1>'
Set-Content -Path "$HOME\nginx-lb-demo\server2\index.html" -Value '<h1>Welcome to Server 2</h1>'

Open two Terminal windows or tabs. Start one server in each and leave both running:

Terminal 1 · Start Server 1
cd ~/nginx-lb-demo/server1
python3 -m http.server 8001
Terminal 2 · Start Server 2
cd ~/nginx-lb-demo/server2
python3 -m http.server 8002

In native Windows PowerShell, open two windows and run one server in each:

PowerShell · Start Server 1
Set-Location "$HOME\nginx-lb-demo\server1"
py -3 -m http.server 8001
PowerShell · Start Server 2
Set-Location "$HOME\nginx-lb-demo\server2"
py -3 -m http.server 8002

Check each server directly in another terminal. These requests bypass NGINX:

Terminal · Check each backend directly
curl http://127.0.0.1:8001/
curl http://127.0.0.1:8002/

In PowerShell, use curl.exe to run curl directly:

PowerShell · Check each backend directly
curl.exe http://127.0.0.1:8001/
curl.exe http://127.0.0.1:8002/

The first response should say Welcome to Server 1, and the second should say Welcome to Server 2. These two Python processes are the backends (the servers NGINX will forward requests to).

3. Configure NGINX

Check which main configuration file NGINX is using:

Terminal · Find and validate the NGINX config
nginx -t

On the Apple Silicon Homebrew setup used for this demo, the main file is /opt/homebrew/etc/nginx/nginx.conf. It includes loadbalancer.conf from inside its http { ... } section, so the active file for this demo is /opt/homebrew/etc/nginx/loadbalancer.conf. Intel Mac Homebrew commonly uses /usr/local/etc/nginx. On Ubuntu/Debian or Fedora, save the snippet as /etc/nginx/conf.d/loadbalancer.conf. For the official Windows zip, save it as C:/nginx/conf/loadbalancer.conf and add include conf/loadbalancer.conf; inside C:/nginx/conf/nginx.conf’s http { ... } block. If nginx -t reports a different main config path, use the matching folder for your installation.

Open the included file in a text editor. For this Apple Silicon Homebrew path, one terminal editor option is:

Terminal · Open the included NGINX config
nano /opt/homebrew/etc/nginx/loadbalancer.conf

On Linux, open the file in the packaged config folder; on native Windows, open the file under NGINX’s extracted conf folder:

Linux · Open the upstream snippet
sudo nano /etc/nginx/conf.d/loadbalancer.conf
Windows · Open the upstream snippet
notepad C:\nginx\conf\loadbalancer.conf

Put the following configuration in loadbalancer.conf. The upstream group contains both backend addresses. The response headers help us see the selected backend and avoid the browser reusing a cached page.

Download the NGINX config file ↓

loadbalancer.conf
upstream my_backend_servers {
    server 127.0.0.1:8001;
    server 127.0.0.1:8002;
}

server {
    listen 8080;
    server_name localhost;

    location / {
        proxy_pass http://my_backend_servers;
        add_header X-Upstream-Server $upstream_addr always;
        add_header Cache-Control "no-store" always;
    }
}

Homebrew’s main config in this demo includes loadbalancer.conf directly. Ubuntu and Fedora commonly include every file in conf.d automatically, so saving the snippet there is enough. If your NGINX setup does not already include the file or its folder, add the appropriate include once inside the main config’s http { ... } block:

nginx.conf · Include the demo config
include loadbalancer.conf;

For the native Windows zip install, the relative include is include conf/loadbalancer.conf;. Don’t add an explicit include if the config already loads this file through a wildcard.

Do not add a second copy if the include is already there. Validate the config and reload it using the command for your system:

Terminal · Validate and reload NGINX
nginx -t
nginx -s reload
Linux · Validate and reload the service
sudo nginx -t
sudo systemctl reload nginx
Windows PowerShell · Validate and reload
cd C:\nginx
.\nginx.exe -t
.\nginx.exe -s reload

Continue only when nginx -t reports that the syntax is okay and the test is successful.

4. Test round robin

Send several new requests to NGINX’s address on port 8080:

Terminal · Send six requests through NGINX
for i in {1..6}; do
  printf "Request %s: " "$i"
  curl -sS "http://127.0.0.1:8080/?check=$i"
done

In native Windows PowerShell, use this loop instead:

PowerShell · Send six requests through NGINX
1..6 | ForEach-Object {
  Write-Output "Request $_"
  curl.exe -sS "http://127.0.0.1:8080/?check=$($_)"
}

The response bodies should alternate between Welcome to Server 1 and Welcome to Server 2. Which server answers first can vary. NGINX uses round robin by default when no other balancing rule is configured, and both servers have the default equal weight.

In a browser, open http://localhost:8080/?check=1. Change the number after check= and reload for each request. Keep :8080 in the address: :8001 and :8002 go directly to one server, while a URL without a port uses the browser’s default HTTP port instead.

5. Inspect which backend answered

The Server: nginx response header identifies the proxy. The custom X-Upstream-Server header identifies the backend selected by NGINX. The config above adds it using NGINX’s $upstream_addr variable.

Terminal · Inspect response and upstream headers
curl -i "http://127.0.0.1:8080/?trace=1"
curl -i "http://127.0.0.1:8080/?trace=2"

In Windows PowerShell, use curl.exe -i:

PowerShell · Inspect response headers
curl.exe -i "http://127.0.0.1:8080/?trace=1"
curl.exe -i "http://127.0.0.1:8080/?trace=2"

Look for X-Upstream-Server: 127.0.0.1:8001 or X-Upstream-Server: 127.0.0.1:8002. In a browser, open Developer Tools → Network, select the page request, and inspect Response Headers. Cache-Control: no-store tells the browser not to keep a cached copy for this demo.

traceroute is not useful for seeing this choice. It shows network hops to a host; both backend servers are on the same computer, and NGINX’s selection happens inside the HTTP proxy.

6. Is this Layer 4 or Layer 7?

This demo is Layer 7. Its configuration is in NGINX’s http context, the listener handles HTTP, and proxy_pass http://... forwards HTTP requests. NGINX can inspect request details such as the host or path, though this demo sends every path to the same upstream group.

NGINX still uses TCP to carry the HTTP data. That does not make this a Layer 4 load-balancing setup: the proxy is handling the application protocol, HTTP. NGINX’s Layer 4 TCP/UDP proxying uses the separate stream context and stream proxy directives.

7. Troubleshooting and cleanup

  • NGINX welcome page: confirm you opened http://localhost:8080/, check that the main config includes loadbalancer.conf inside http { ... }, then run nginx -t and reload. On Homebrew, make sure the default welcome-site server block is not also claiming port 8080.
  • 502 Bad Gateway: check that both Python server terminals are still running and that the direct curl checks to ports 8001 and 8002 work.
  • Same server seems to appear repeatedly: inspect X-Upstream-Server, change the check query value, and make sure the upstream block lists both servers without an ip_hash, hash, or least_conn directive.

When you finish, press Ctrl+C in each Python server terminal. To stop NGINX, run:

Terminal · Stop NGINX
nginx -s quit
Windows PowerShell · Stop NGINX
cd C:\nginx
.\nginx.exe -s quit

Install information: Homebrew NGINX formula ↗, Ubuntu NGINX installation ↗, Fedora NGINX installation ↗, NGINX for Windows ↗, and WinGet’s NGINX community manifest ↗. Configuration and behavior follow the NGINX HTTP load-balancing guide ↗, upstream module reference ↗, and response-header module reference ↗.