This article focuses on using Nginx to provide HTTPS load balancing for a Spring Boot cluster. It addresses plaintext traffic, single points of failure, and complex certificate management, and covers Certbot certificate issuance, reverse proxying, health checks, and session persistence. Keywords: Nginx, SSL, HTTPS load balancing.
Technical specification snapshot
| Parameter | Description |
|---|---|
| Core languages | Nginx configuration, Bash, Java |
| Transport protocols | HTTP, HTTPS, TLS 1.2/1.3 |
| Deployment model | Nginx reverse proxy + multiple Spring Boot instances |
| Reference traction | Original article: about 1k views, 16 likes, 12 saves |
| Core dependencies | Nginx, Certbot, Spring Boot 3.2, Maven |
AI Visual Insight: The image is a cover-style technical illustration that introduces the load balancing topic. It serves more as content navigation than as an architecture diagram. It does not directly show nodes, links, ports, or protocol details, so it works better as a section opener than as a deployment reference.
HTTPS load balancing is a foundational capability at the web entry layer
As a system evolves from a single instance to multiple instances, the entry layer must solve three problems at once: encrypted transport, traffic distribution, and fault isolation. Nginx handles SSL termination and reverse proxying, making it the most common implementation path.
The risks of a traditional HTTP architecture are straightforward: plaintext data can be intercepted, a single backend failure can bring down the entire service, and certificates and security policies are difficult to manage consistently. Once you introduce HTTPS load balancing, you can centralize all of these concerns at the entry layer.
The architecture’s key responsibilities must be defined first
A standard topology is a public-facing Nginx instance plus private application instances. Clients access only port 443. Nginx completes the TLS handshake and then forwards requests to backend services on ports 8081, 8082, 8083, and so on.
Client -> Nginx:443 -> Spring Boot:8081/8082/8083
This means SSL termination happens at Nginx, while the backend handles only HTTP business requests, reducing encryption and decryption overhead at the application layer.
Backend services must expose observable health endpoints first
Load balancing does not start with writing Nginx config. It starts with preparing backend services that can return instance identity. A minimal workable implementation is a Spring Boot application that exposes two endpoints: /api/health and /api/user.
@RestController
public class HealthController {
@Value("${server.port}")
private int port;
@GetMapping("/api/health")
public String healthCheck() {
// Return the port and timestamp so you can identify the backend instance that handled the request
String time = LocalDateTime.now()
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
return String.format("Backend Service on port %d - Time: %s", port, time);
}
}
This code makes load balancing results visible, which helps you verify traffic distribution and failover behavior.
The multi-instance startup method should stay simple
You can start multiple instances of the same JAR on different ports to simulate a backend cluster locally. This is a fast way to verify whether the Nginx upstream configuration works as expected.
mvn clean package -DskipTests
# Start three backend instances to simulate cluster nodes
nohup java -jar target/backend-service-1.0.0.jar --server.port=8081 > app1.log 2>&1 &
nohup java -jar target/backend-service-1.0.0.jar --server.port=8082 > app2.log 2>&1 &
nohup java -jar target/backend-service-1.0.0.jar --server.port=8083 > app3.log 2>&1 &
These commands quickly bring up a three-node backend and create a minimal load balancing test environment.
The certificate issuance process determines whether HTTPS can be deployed reliably
For production, Let’s Encrypt with Certbot is the recommended choice. The prerequisites are that your domain points to the public IP of the Nginx server, and that ports 80 and 443 are open.
When you request a certificate, standalone mode temporarily occupies port 80, so stop Nginx before running Certbot. After issuance succeeds, fullchain.pem and privkey.pem can be used directly by Nginx.
sudo systemctl stop nginx
sudo certbot certonly --standalone -d your-domain.com
# Test automatic renewal
sudo certbot renew --dry-run
These commands complete certificate issuance and renewal validation, which are prerequisites for HTTPS deployment.
Private key permissions must be strictly restricted
privkey.pem is the most sensitive file, so set its permissions to 600. If the private key leaks, an attacker can impersonate your legitimate site for a man-in-the-middle attack even if your application itself has not been compromised.
Nginx configuration is the control plane for HTTPS load balancing
Your entry-layer configuration should cover four categories at the same time: the upstream backend group, HTTP-to-HTTPS redirection from port 80 to 443, SSL certificates and protocols, and security headers. Round-robin is the default strategy and is a good first production configuration.
upstream backend_servers {
server 127.0.0.1:8081;
server 127.0.0.1:8082;
server 127.0.0.1:8083;
}
server {
listen 80;
server_name your-domain.com;
location / {
# Force plaintext requests to redirect to HTTPS
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl http2;
server_name your-domain.com;
ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
location / {
# Forward requests to the backend node group
proxy_pass http://backend_servers;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
This configuration completes three core tasks: HTTPS access, traffic forwarding, and preserving the original client source information.
Health checks require an open source workaround
The open source edition of Nginx does not provide active health checks, so you can use a Shell script to generate the upstream configuration dynamically. The core idea is to write only healthy nodes into a temporary config file and then reload Nginx after comparing the changes.
#!/bin/bash
BACKENDS=("127.0.0.1:8081" "127.0.0.1:8082" "127.0.0.1:8083")
TEMP_CONF="/tmp/backend-upstream.conf"
echo "upstream backend_servers {" > $TEMP_CONF
for backend in "${BACKENDS[@]}"; do
# Probe the health endpoint and write only successful nodes into upstream
curl -sf http://$backend/api/health >/dev/null && echo " server $backend;" >> $TEMP_CONF
done
echo "}" >> $TEMP_CONF
This script dynamically filters available backends so requests do not continue to hit failed instances.
Session persistence and security hardening directly affect production availability
If your application includes shopping carts, login state, or short-lived sessions, pure round-robin can cause state drift. The simplest option is ip_hash, but it can become unbalanced in NAT scenarios. A more reliable approach is to write a sticky cookie at the application layer and let Nginx route based on that cookie.
You should also address performance and security together. Enable worker_processes auto, gzip, and keepalive, and disable server_tokens. At the same time, restrict request methods and add rate limiting to reduce probing and DDoS risk.
server_tokens off;
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
location /api/ {
# Apply rate limiting to API requests to suppress burst abuse
limit_req zone=api burst=20 nodelay;
proxy_pass http://backend_servers;
}
This configuration hides version information and adds basic rate limiting protection for your API.
Logs are the source of truth for troubleshooting and capacity planning
Add fields such as request_time and upstream_response_time to the access log. If you encounter 502 errors, timeouts, or uneven traffic distribution, logs are more reliable than guesswork and can tell you directly whether the issue comes from the handshake, the connection, or a slow upstream response.
Common failures usually fall into three connection-path categories
The first category is 502 Bad Gateway. Check whether backend instances are alive and whether Nginx can reach the backend ports. The second category is certificate failure, which is usually caused by renewal failure, DNS errors, or incorrect certificate paths. The third category is uneven load distribution, which is often related to ip_hash, performance differences between nodes, or NAT scenarios.
FAQ uses a structured question-and-answer format
1. Why do I still get a 502 response after HTTPS is configured correctly?
Answer: First verify that the backend process is still running, then check the proxy_pass target, port connectivity, and error logs. In most cases, a 502 is not an SSL problem. It means the upstream service is not responding or the upstream configuration is not working.
2. Why does a Let’s Encrypt certificate still expire automatically?
Answer: The default certificate lifetime is 90 days. If certbot renew is not scheduled to run regularly, or if port 80 validation fails, renewal will fail. You should run --dry-run regularly to verify the renewal path.
3. Should I choose IP hash or cookies for session persistence?
Answer: For small-scale, low-requirement scenarios, start with ip_hash. If you need consistent login state or serve users behind NAT, prefer cookie-based routing, or move to a shared session solution such as Redis.
Core summary: This article reconstructs an HTTPS load balancing architecture built on Nginx, SSL/TLS, and Spring Boot. It covers certificate issuance, reverse proxying, health checks, session persistence, security hardening, and troubleshooting workflows, making it a practical guide for building a reliable, secure, and scalable production entry layer.