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:
brew install nginx
brew services start nginxCheck 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:
sudo apt update
sudo apt install nginx
sudo systemctl enable --now nginxLinux · Fedora
For Fedora, use its package manager:
sudo dnf install nginx
sudo systemctl enable --now nginxOn 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:
winget install --id nginxinc.nginx --exactFor 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 ↗
cd C:\nginx
.\nginx.exe
.\nginx.exe -vThe 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:
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.htmlIn native Windows PowerShell, create the two pages like this:
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:
cd ~/nginx-lb-demo/server1
python3 -m http.server 8001cd ~/nginx-lb-demo/server2
python3 -m http.server 8002In native Windows PowerShell, open two windows and run one server in each:
Set-Location "$HOME\nginx-lb-demo\server1"
py -3 -m http.server 8001Set-Location "$HOME\nginx-lb-demo\server2"
py -3 -m http.server 8002Check each server directly in another terminal. These requests bypass NGINX:
curl http://127.0.0.1:8001/
curl http://127.0.0.1:8002/In PowerShell, use curl.exe to run curl 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:
nginx -tOn 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:
nano /opt/homebrew/etc/nginx/loadbalancer.confOn Linux, open the file in the packaged config folder; on native Windows, open the file under NGINX’s extracted conf folder:
sudo nano /etc/nginx/conf.d/loadbalancer.confnotepad C:\nginx\conf\loadbalancer.confPut 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 ↓
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:
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:
nginx -t
nginx -s reloadsudo nginx -t
sudo systemctl reload nginxcd C:\nginx
.\nginx.exe -t
.\nginx.exe -s reloadContinue 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:
for i in {1..6}; do
printf "Request %s: " "$i"
curl -sS "http://127.0.0.1:8080/?check=$i"
doneIn native Windows PowerShell, use this loop instead:
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.
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:
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 includesloadbalancer.confinsidehttp { ... }, then runnginx -tand reload. On Homebrew, make sure the default welcome-site server block is not also claiming port8080. - 502 Bad Gateway: check that both Python server terminals are still running and that the direct
curlchecks to ports8001and8002work. - Same server seems to appear repeatedly: inspect
X-Upstream-Server, change thecheckquery value, and make sure the upstream block lists both servers without anip_hash,hash, orleast_conndirective.
When you finish, press Ctrl+C in each Python server terminal. To stop NGINX, run:
nginx -s quitcd C:\nginx
.\nginx.exe -s quitInstall 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 ↗.