← Back to all blogs
Nginx Load Balancer Setup – Step-by-Step Tutorial
Sat Feb 28 20268 minIntermediate

Nginx Load Balancer Setup – Step-by-Step Tutorial

A comprehensive tutorial that walks you through installing, configuring, and testing Nginx as a load balancer, complete with architecture insights and troubleshooting advice.

#nginx#load balancer#devops#infrastructure#high availability

Introduction

Modern web applications rarely rely on a single server. Distributing traffic across multiple back‑end instances improves responsiveness, fault tolerance, and scalability. Nginx, a lightweight yet powerful reverse proxy, has become the de‑facto standard for HTTP load balancing because of its event‑driven architecture, low memory footprint, and extensive module ecosystem.

Why Choose Nginx for Load Balancing?

  • Performance: Handles tens of thousands of concurrent connections with minimal CPU consumption.
  • Flexibility: Supports round‑robin, least‑connections, IP‑hash, and custom load‑balancing algorithms.
  • Observability: Built‑in status module and easy integration with Prometheus or Grafana.
  • Security: Can terminate TLS, enforce access controls, and hide internal network topology.

This tutorial assumes familiarity with Linux command‑line tools and basic networking concepts. By the end of the guide you will have a production‑ready Nginx load balancer that distributes traffic to a pool of web servers, optionally performing health checks and SSL termination.

Prerequisites & Architecture Overview

Before writing a single line of configuration, verify that the following prerequisites are satisfied:

ItemMinimum Requirement
Operating SystemUbuntu 20.04 LTS or CentOS 8
Nginx version1.21.0 or later (compiled with http_ssl_module and http_stub_status_module)
Backend servers2+ instances running HTTP on port 8080
IP connectivityAll nodes must reach each other on the load‑balancer IP and backend ports
Optional - DNSA wildcard or A record that points to the load‑balancer public address

High‑Level Architecture

The diagram below illustrates a typical deployment:

+-------------------+ +-------------------+ | Client / User | --> | Nginx LB (Port 80/443) | +-------------------+ +----------+--------+ | +-------------------------+------------------------+ | | | +--------v------+ +--------v------+ +--------v------+ | Web Server 1 | | Web Server 2 | | Web Server N | +---------------+ +---------------+ +---------------+

  • The Nginx Load Balancer accepts inbound HTTP/HTTPS traffic, terminates TLS if configured, and forwards requests to the healthiest backend.
  • Health checks are performed by the ngx_http_upstream_check_module (optional) or by probing the built‑in status endpoint.
  • All communication between the load balancer and back‑ends remains within a private VPC or subnet, reducing exposure to the internet.

Understanding this flow helps you decide where to place logging, monitoring agents, and security controls.

Step‑By‑Step Configuration

The following procedure covers installation, upstream definition, server block creation, health‑check integration, and TLS termination.

1. Install Nginx with Required Modules

bash

Ubuntu

sudo apt update sudo apt install -y nginx

Verify modules

nginx -V 2>&1 | grep -E "http_ssl_module|http_stub_status_module"

If the output does not list --with-http_ssl_module or --with-http_stub_status_module, you may need to install the official Nginx mainline package or compile from source.

2. Define the Upstream Pool

Create a file named /etc/nginx/conf.d/upstream.conf:

nginx

/etc/nginx/conf.d/upstream.conf

upstream app_backend { # Least connections algorithm reduces load on busy servers least_conn;

# Define each backend server; adjust IPs as needed
server 10.0.1.11:8080 max_fails=3 fail_timeout=30s;
server 10.0.1.12:8080 max_fails=3 fail_timeout=30s;
server 10.0.1.13:8080 max_fails=3 fail_timeout=30s;

# Optional: enable session persistence via IP hash
# ip_hash;

}

  • least_conn is often preferable to the default round‑robin when back‑ends have heterogeneous processing capacity.
  • max_fails and fail_timeout ensure that a server flagged as unhealthy is bypassed for a configurable interval.

3. Create the Public Server Block

Add /etc/nginx/sites‑available/load_balancer.conf and link it to sites-enabled:

nginx

/etc/nginx/sites-available/load_balancer.conf

server { listen 80; listen 443 ssl http2; server_name www.example.com api.example.com;

# -------------------------------------------------
# TLS configuration (use Let's Encrypt or your CA)
# -------------------------------------------------
ssl_certificate /etc/ssl/certs/example.com.crt;
ssl_certificate_key /etc/ssl/private/example.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;

# -------------------------------------------------
# Proxy settings
# -------------------------------------------------
location / {
    proxy_pass http://app_backend;
    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;

    # Timeout values
    proxy_connect_timeout 5s;
    proxy_send_timeout 30s;
    proxy_read_timeout 30s;
}

# -------------------------------------------------
# Status endpoint for monitoring
# -------------------------------------------------
location /nginx_status {
    stub_status;
    allow 127.0.0.1;   # Restrict to localhost or internal monitoring IPs
    deny all;
}

}

Enable the site:

bash sudo ln -s /etc/nginx/sites-available/load_balancer.conf /etc/nginx/sites-enabled/

4. Optional: Active Health Checks

If you prefer proactive health verification, install the third‑party module ngx_http_upstream_check_module. On Ubuntu:

bash sudo apt install -y libnginx-mod-http-upstream-check

Then extend upstream.conf:

nginx upstream app_backend { least_conn; server 10.0.1.11:8080; server 10.0.1.12:8080; server 10.0.1.13:8080;

# Active health check definition
check interval=3000 rise=2 fall=5 timeout=1000 type=http;
check_http_send "HEAD /health HTTP/1.0\r\n\r\n";
check_http_expect_alive http_2xx;

}

A /health endpoint returning 200 OK on each backend will keep the pool accurate.

5. Validate and Reload

bash

Test configuration syntax

sudo nginx -t

Reload without dropping connections

sudo systemctl reload nginx

If any errors appear, review the line numbers reported by nginx -t and correct the offending directive.

6. Verify Operation

bash curl -I http://<lb-ip>/nginx_status

You should see a short status payload similar to:

Active connections: 3 server accepts handled requests 30 30 120 Reading: 0 Writing: 1 Waiting: 2

This confirms that the proxy is alive and that client connections are being processed.

Validation, Monitoring, and Scaling

A load balancer is only as good as its observability. This section describes practical techniques for confirming correct behavior, integrating with monitoring stacks, and preparing the architecture for horizontal growth.

1. Functional Tests

TestCommandExpected Result
Round‑Robin distributionfor i in {1..9}; do curl -s http://<lb-ip>/hostname; doneHostnames from different back‑ends appear in a balanced order.
Fail‑over handlingsudo iptables -I INPUT -s 10.0.1.11 -j DROP then repeat the requestRequests skip the blocked node and continue without 5xx errors.
TLS handshakeopenssl s_client -connect <lb-ip>:443 -servername www.example.comCertificate chain matches the one configured in ssl_certificate.

Automate these steps with a CI/CD pipeline (GitHub Actions, GitLab CI) to catch regressions before deployment.

2. Metrics Export

Enable the Prometheus exporter by adding the following location block to the server configuration:

nginx location /metrics { stub_status; # Prometheus format can be produced by the nginx‑prometheus‑exporter binary }

Deploy the nginx‑prometheus‑exporter container alongside Nginx:

yaml version: "3" services: nginx: image: nginx:1.23-alpine ports: - "80:80" - "443:443" volumes: - ./nginx/conf.d:/etc/nginx/conf.d exporter: image: nginx/nginx-prometheus-exporter:latest ports: - "9113:9113" environment: - NGINX_STATUS_URL=http://nginx/nginx_status

Grafana dashboards can now visualize nginx_upstream_response_time_seconds, nginx_http_requests_total, and nginx_upstream_currently_up metrics.

3. Log Management

Configure structured JSON logs to simplify ingestion by ELK or Loki:

nginx log_format json_combined escape=json '{' '"time":"$time_iso8601",' '"remote_addr":"$remote_addr",' '"host":"$host",' '"request":"$request",' '"status":$status,' '"bytes_sent":$body_bytes_sent,' '"request_time":$request_time,' '"upstream_addr":"$upstream_addr",' '"upstream_status":"$upstream_status"' '}'; access_log /var/log/nginx/access.json json_combined;

4. Scaling the Pool

When traffic spikes, you can add new backend servers without touching the Nginx configuration-provided you use DNS‑based service discovery:

nginx upstream app_backend { least_conn; # Resolve hostnames at runtime every 30 seconds resolver 127.0.0.53 valid=30s; server backend.service.internal:8080; }

Update the DNS record backend.service.internal to point to additional IPs, and Nginx will automatically incorporate them after the TTL expires.

5. High Availability for the Load Balancer Itself

Deploy two Nginx instances behind a floating IP (e.g., Keepalived VRRP) or in a Kubernetes Service of type LoadBalancer. The core configuration remains identical; only the startup scripts differ.

By combining functional testing, real‑time metrics, and automated scaling, you create a resilient front‑end that can sustain millions of requests per day.

FAQs

Q1: Can Nginx handle TCP/UDP load balancing, or is it limited to HTTP? A: Yes. The stream context in Nginx enables generic TCP or UDP load balancing. The configuration mirrors the HTTP upstream block but lives under stream {} and requires the ngx_stream_core_module.

Q2: What is the difference between passive and active health checks? A: Passive checks rely on connection errors or non‑2xx responses observed during normal traffic. Active checks periodically probe a dedicated endpoint (e.g., /health) regardless of client requests, providing faster detection of silent failures.

Q3: How do I migrate from a single Nginx instance to a highly available pair without downtime? A: Use a virtual IP managed by Keepalived. Start the second Nginx with an identical configuration, synchronize the /etc/nginx directory via rsync or a configuration management tool, then transition the VRRP master role. Clients will see a seamless fail‑over.

Conclusion

Deploying Nginx as a load balancer blends simplicity with enterprise‑grade features. By following the step‑by‑step instructions, you acquire:

  • A secure entry point that terminates TLS and forwards traffic using a chosen algorithm.
  • Built‑in health monitoring and optional active checks that keep the pool reliable.
  • Rich observability through the Stub Status module, Prometheus exporter, and JSON‑formatted logs.
  • A scalable architecture that grows with DNS‑based service discovery or container orchestration.

The same configuration patterns extend to Kubernetes Ingress controllers, micro‑service meshes, and edge‑proxy scenarios, making Nginx a versatile cornerstone of modern DevOps pipelines. Keep the configuration under version control, test each change in a staging environment, and automate reloads to maintain high availability as traffic evolves.