Set up NGINX as a reverse proxy with SSL termination, load balancing across multiple backend servers, and automatic HTTPS redirection for production-ready high availability web infrastructure.
Prerequisites
- Root or sudo access
- Domain name pointing to your server
- Multiple backend application servers
- Basic understanding of web server concepts
What this solves
NGINX reverse proxy with SSL termination allows you to centralize SSL certificate management while distributing traffic across multiple backend application servers. This setup improves performance, enables zero-downtime deployments, and provides a single point for security hardening and caching.
Step-by-step installation
Update system packages and install NGINX
Start by updating your package manager and installing NGINX with all required modules for reverse proxy functionality.
sudo apt update && sudo apt upgrade -y
sudo apt install -y nginx openssl curl
Enable and start NGINX service
Enable NGINX to start automatically on boot and verify it's running correctly.
sudo systemctl enable nginx
sudo systemctl start nginx
sudo systemctl status nginx
Configure firewall rules
Open HTTP and HTTPS ports in the system firewall to allow web traffic.
sudo ufw allow 'Nginx Full'
sudo ufw status
Create SSL certificates directory
Create a secure directory structure for SSL certificates with appropriate permissions.
sudo mkdir -p /etc/nginx/ssl
sudo chmod 700 /etc/nginx/ssl
Generate self-signed SSL certificate for testing
Create a temporary self-signed certificate for initial testing. Replace with Let's Encrypt or commercial certificates in production.
sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout /etc/nginx/ssl/example.com.key \
-out /etc/nginx/ssl/example.com.crt \
-subj "/C=US/ST=State/L=City/O=Organization/CN=example.com"
Set SSL certificate permissions
Secure the SSL certificates with proper ownership and restrictive permissions.
sudo chown root:root /etc/nginx/ssl/*
sudo chmod 600 /etc/nginx/ssl/example.com.key
sudo chmod 644 /etc/nginx/ssl/example.com.crt
Create upstream configuration
Define upstream server groups for load balancing across multiple backend application servers.
upstream app_servers {
least_conn;
server 192.168.1.10:8080 weight=3 max_fails=3 fail_timeout=30s;
server 192.168.1.11:8080 weight=3 max_fails=3 fail_timeout=30s;
server 192.168.1.12:8080 weight=2 max_fails=3 fail_timeout=30s backup;
}
upstream api_servers {
ip_hash;
server 192.168.1.20:3000 weight=1 max_fails=2 fail_timeout=20s;
server 192.168.1.21:3000 weight=1 max_fails=2 fail_timeout=20s;
}
Health check configuration
server {
listen 127.0.0.1:8081;
location /nginx-health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}
Create SSL configuration snippet
Define reusable SSL settings with modern security headers and cipher suites.
# SSL Configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
SSL session cache and timeout
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
OCSP stapling
ssl_stapling on;
ssl_stapling_verify on;
Security headers
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Frame-Options DENY always;
add_header X-Content-Type-Options nosniff always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
Remove server tokens
server_tokens off;
Configure main reverse proxy virtual host
Create the primary virtual host configuration with SSL termination and reverse proxy settings.
server {
listen 80;
server_name example.com www.example.com;
# Redirect all HTTP to HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name example.com www.example.com;
# SSL certificates
ssl_certificate /etc/nginx/ssl/example.com.crt;
ssl_certificate_key /etc/nginx/ssl/example.com.key;
# Include SSL configuration
include /etc/nginx/conf.d/ssl.conf;
# Logging
access_log /var/log/nginx/reverse-proxy.access.log;
error_log /var/log/nginx/reverse-proxy.error.log;
# Main application proxy
location / {
proxy_pass http://app_servers;
proxy_http_version 1.1;
# Proxy headers
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
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;
proxy_set_header X-Forwarded-Host $server_name;
# Proxy timeouts
proxy_connect_timeout 5s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# Buffer settings
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
proxy_busy_buffers_size 8k;
# Cache control
proxy_cache_bypass $http_upgrade;
}
# API endpoint with session persistence
location /api/ {
proxy_pass http://api_servers/;
proxy_http_version 1.1;
# Same proxy headers as above
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
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;
# API-specific timeouts
proxy_connect_timeout 3s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
}
# Static assets with caching
location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff|woff2)$ {
proxy_pass http://app_servers;
proxy_cache_valid 200 1h;
expires 1h;
add_header Cache-Control "public, immutable";
}
# Health check endpoint
location /health {
access_log off;
proxy_pass http://app_servers/health;
proxy_connect_timeout 1s;
proxy_send_timeout 1s;
proxy_read_timeout 1s;
}
}
Enable the virtual host
Create a symbolic link to enable the reverse proxy configuration and remove the default site.
sudo ln -s /etc/nginx/sites-available/reverse-proxy /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
Optimize NGINX configuration
Update the main NGINX configuration for better performance and security.
user www-data;
worker_processes auto;
pid /run/nginx.pid;
events {
worker_connections 1024;
use epoll;
multi_accept on;
}
http {
# Basic settings
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
client_max_body_size 16M;
# MIME types
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Logging
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" '
'rt=$request_time uct="$upstream_connect_time" '
'uht="$upstream_header_time" urt="$upstream_response_time"';
access_log /var/log/nginx/access.log main;
error_log /var/log/nginx/error.log warn;
# Gzip compression
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/xml+rss
application/atom+xml;
# Rate limiting
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=general:10m rate=1r/s;
# Connection limiting
limit_conn_zone $binary_remote_addr zone=conn_limit_per_ip:10m;
limit_conn conn_limit_per_ip 20;
# Include configurations
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
Set up Let's Encrypt SSL certificates
Install Certbot and obtain production SSL certificates from Let's Encrypt.
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com
Configure automatic certificate renewal
Set up automatic renewal for Let's Encrypt certificates using a systemd timer.
sudo systemctl enable certbot.timer
sudo systemctl start certbot.timer
sudo systemctl status certbot.timer
Test and reload NGINX configuration
Verify the configuration syntax and reload NGINX to apply all changes.
sudo nginx -t
sudo systemctl reload nginx
Verify your setup
Test your reverse proxy configuration with these verification commands:
# Check NGINX status and configuration
sudo systemctl status nginx
sudo nginx -T
Test HTTP to HTTPS redirect
curl -I http://example.com
Test SSL certificate
openssl s_client -connect example.com:443 -servername example.com
Test load balancer health
curl -k https://example.com/health
Check upstream server status
echo "GET /nginx-health HTTP/1.0\r\n\r\n" | nc 127.0.0.1 8081
Monitor access logs
sudo tail -f /var/log/nginx/reverse-proxy.access.log
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| 502 Bad Gateway error | Backend servers not responding | Check upstream server status and network connectivity |
| SSL certificate errors | Wrong certificate path or permissions | Verify certificate files with sudo nginx -t |
| Rate limiting blocks legitimate users | Too restrictive rate limits | Adjust rate limits in nginx.conf or add IP whitelist |
| Slow response times | Poor upstream configuration | Tune proxy timeouts and buffer settings |
| Session persistence not working | Wrong load balancing method | Use ip_hash for sticky sessions |
Next steps
- Configure NGINX reverse proxy with advanced caching and load balancing
- Set up NGINX web application firewall with ModSecurity 3
- Implement HAProxy SSL termination with Let's Encrypt certificates
- Configure NGINX rate limiting and advanced security rules
- Set up NGINX monitoring with Prometheus and Grafana dashboards
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# Default values
DOMAIN="${1:-example.com}"
BACKEND_IPS="${2:-192.168.1.10:8080,192.168.1.11:8080}"
# Usage message
usage() {
echo "Usage: $0 [domain] [backend_ips]"
echo "Example: $0 example.com '192.168.1.10:8080,192.168.1.11:8080'"
exit 1
}
# Logging functions
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
# Cleanup on failure
cleanup() {
log_error "Script failed. Rolling back changes..."
systemctl stop nginx 2>/dev/null || true
rm -rf /etc/nginx/ssl 2>/dev/null || true
rm -f /etc/nginx/sites-available/reverse-proxy 2>/dev/null || true
rm -f /etc/nginx/conf.d/upstream.conf 2>/dev/null || true
rm -f /etc/nginx/conf.d/ssl.conf 2>/dev/null || true
rm -f /etc/nginx/conf.d/reverse-proxy.conf 2>/dev/null || true
}
trap cleanup ERR
# Check prerequisites
check_prereqs() {
if [[ $EUID -ne 0 ]]; then
log_error "This script must be run as root or with sudo"
exit 1
fi
if [[ "$DOMAIN" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
log_error "Please provide a valid domain name, not an IP address"
usage
fi
}
# Detect distribution
detect_distro() {
if [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_INSTALL="apt install -y"
PKG_UPDATE="apt update && apt upgrade -y"
FIREWALL_CMD="ufw"
NGINX_CONF_DIR="/etc/nginx/sites-available"
NGINX_ENABLED_DIR="/etc/nginx/sites-enabled"
USE_SITES_AVAILABLE=true
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_INSTALL="dnf install -y"
PKG_UPDATE="dnf update -y"
FIREWALL_CMD="firewalld"
NGINX_CONF_DIR="/etc/nginx/conf.d"
USE_SITES_AVAILABLE=false
;;
amzn)
PKG_MGR="yum"
PKG_INSTALL="yum install -y"
PKG_UPDATE="yum update -y"
FIREWALL_CMD="firewalld"
NGINX_CONF_DIR="/etc/nginx/conf.d"
USE_SITES_AVAILABLE=false
;;
*)
log_error "Unsupported distro: $ID"
exit 1
;;
esac
else
log_error "Cannot detect distribution"
exit 1
fi
log_info "Detected distribution: $PRETTY_NAME"
}
# Configure firewall
configure_firewall() {
if [[ "$FIREWALL_CMD" == "ufw" ]]; then
if command -v ufw >/dev/null 2>&1; then
ufw allow 'Nginx Full' || true
fi
elif [[ "$FIREWALL_CMD" == "firewalld" ]]; then
if systemctl is-active --quiet firewalld; then
firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --add-service=https
firewall-cmd --reload
fi
fi
}
# Main installation
main() {
echo "[1/10] Checking prerequisites..."
check_prereqs
echo "[2/10] Detecting distribution..."
detect_distro
echo "[3/10] Updating system and installing packages..."
$PKG_UPDATE
$PKG_INSTALL nginx openssl curl
echo "[4/10] Configuring NGINX service..."
systemctl enable nginx
systemctl start nginx
echo "[5/10] Configuring firewall..."
configure_firewall
echo "[6/10] Creating SSL directory and certificates..."
mkdir -p /etc/nginx/ssl
chmod 700 /etc/nginx/ssl
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout /etc/nginx/ssl/${DOMAIN}.key \
-out /etc/nginx/ssl/${DOMAIN}.crt \
-subj "/C=US/ST=State/L=City/O=Organization/CN=${DOMAIN}"
chown root:root /etc/nginx/ssl/*
chmod 600 /etc/nginx/ssl/${DOMAIN}.key
chmod 644 /etc/nginx/ssl/${DOMAIN}.crt
echo "[7/10] Creating upstream configuration..."
cat > /etc/nginx/conf.d/upstream.conf << EOF
upstream app_servers {
least_conn;
EOF
IFS=',' read -ra BACKENDS <<< "$BACKEND_IPS"
for backend in "${BACKENDS[@]}"; do
echo " server $backend weight=3 max_fails=3 fail_timeout=30s;" >> /etc/nginx/conf.d/upstream.conf
done
cat >> /etc/nginx/conf.d/upstream.conf << EOF
}
server {
listen 127.0.0.1:8081;
location /nginx-health {
access_log off;
return 200 "healthy\\n";
add_header Content-Type text/plain;
}
}
EOF
echo "[8/10] Creating SSL configuration..."
cat > /etc/nginx/conf.d/ssl.conf << 'EOF'
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Frame-Options DENY always;
add_header X-Content-Type-Options nosniff always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
server_tokens off;
EOF
echo "[9/10] Creating reverse proxy configuration..."
PROXY_CONF_FILE=""
if [[ "$USE_SITES_AVAILABLE" == true ]]; then
PROXY_CONF_FILE="$NGINX_CONF_DIR/reverse-proxy"
else
PROXY_CONF_FILE="$NGINX_CONF_DIR/reverse-proxy.conf"
fi
cat > "$PROXY_CONF_FILE" << EOF
server {
listen 80;
server_name $DOMAIN www.$DOMAIN;
return 301 https://\$server_name\$request_uri;
}
server {
listen 443 ssl http2;
server_name $DOMAIN www.$DOMAIN;
ssl_certificate /etc/nginx/ssl/$DOMAIN.crt;
ssl_certificate_key /etc/nginx/ssl/$DOMAIN.key;
location / {
proxy_pass http://app_servers;
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;
proxy_connect_timeout 5s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
proxy_buffering on;
proxy_buffer_size 8k;
proxy_buffers 8 8k;
}
location /health {
access_log off;
return 200 "OK";
add_header Content-Type text/plain;
}
}
EOF
if [[ "$USE_SITES_AVAILABLE" == true ]]; then
rm -f /etc/nginx/sites-enabled/default
ln -sf "$NGINX_CONF_DIR/reverse-proxy" "$NGINX_ENABLED_DIR/reverse-proxy"
fi
echo "[10/10] Testing and reloading NGINX..."
nginx -t
systemctl reload nginx
log_info "NGINX reverse proxy installation completed successfully!"
log_info "Domain: $DOMAIN"
log_info "SSL Certificate: /etc/nginx/ssl/$DOMAIN.crt (self-signed)"
log_info "Backend servers: $BACKEND_IPS"
log_info "Health check: http://127.0.0.1:8081/nginx-health"
log_warn "Remember to replace self-signed certificates with proper SSL certificates in production"
# Verification
if systemctl is-active --quiet nginx && curl -k -s https://localhost/health | grep -q "OK"; then
log_info "✓ NGINX is running and responding correctly"
else
log_error "✗ NGINX may not be configured correctly"
exit 1
fi
}
# Validate arguments
if [[ $# -gt 2 ]]; then
usage
fi
main "$@"
Review the script before running. Execute with: bash install.sh