Configure nginx as a secure reverse proxy with Let's Encrypt SSL certificates, security headers, and DDoS protection. Learn to proxy backend applications, implement rate limiting, and harden your web server configuration.
Prerequisites
- Root or sudo access
- Domain name pointing to server
- Backend application running on port 3000
What this solves
This tutorial helps you set up nginx as a secure reverse proxy that forwards client requests to backend applications while providing SSL termination, security headers, and protection against common attacks. You'll learn to configure SSL certificates with automatic renewal, implement rate limiting, and apply security hardening measures for production environments.
Step-by-step installation
Update system packages
Start by updating your package manager to ensure you have the latest security updates and package information.
sudo apt update && sudo apt upgrade -y
Install nginx and required packages
Install nginx web server along with certbot for Let's Encrypt SSL certificate management and curl for testing.
sudo apt install -y nginx certbot python3-certbot-nginx curl ufw
Configure firewall rules
Open HTTP and HTTPS ports in your firewall to allow web traffic to reach nginx.
sudo ufw enable
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw status
Start and enable nginx
Enable nginx to start automatically on boot and start the service immediately.
sudo systemctl enable --now nginx
sudo systemctl status nginx
Create nginx main configuration
Configure the main nginx settings with security-focused parameters and optimized performance settings.
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /run/nginx.pid;
Load dynamic modules
include /usr/share/nginx/modules/*.conf;
events {
worker_connections 1024;
use epoll;
multi_accept on;
}
http {
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 4096;
server_tokens off;
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
# Rate limiting
limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
limit_conn_zone $binary_remote_addr zone=conn_limit_per_ip:10m;
# SSL Configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_stapling on;
ssl_stapling_verify on;
# Gzip Settings
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types
application/atom+xml
application/javascript
application/json
application/rss+xml
application/vnd.ms-fontobject
application/x-font-ttf
application/x-web-app-manifest+json
application/xhtml+xml
application/xml
font/opentype
image/svg+xml
image/x-icon
text/css
text/plain
text/x-component;
# Load modular configuration files
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
Create reverse proxy configuration
Create a server block configuration for your domain with reverse proxy settings that forward requests to a backend application.
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name example.com www.example.com;
# SSL Configuration (will be added by certbot)
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
# Security headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# Rate limiting
limit_req zone=general burst=20 nodelay;
limit_conn conn_limit_per_ip 10;
# Logging
access_log /var/log/nginx/example.com.access.log main;
error_log /var/log/nginx/example.com.error.log;
# Root location - proxy to backend
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
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_cache_bypass $http_upgrade;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
proxy_busy_buffers_size 8k;
}
# API endpoints with stricter rate limiting
location /api/login {
limit_req zone=login burst=5 nodelay;
proxy_pass http://127.0.0.1:3000;
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;
}
# Static assets with caching
location /static/ {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_cache_valid 200 302 10m;
proxy_cache_valid 404 1m;
add_header Cache-Control "public, max-age=3600";
}
# Health check endpoint
location /health {
proxy_pass http://127.0.0.1:3000/health;
access_log off;
}
# Block common attack patterns
location ~* \.(sql|bak|backup|dump)$ {
deny all;
return 404;
}
# Block access to hidden files
location ~ /\. {
deny all;
return 404;
}
}
Enable the site configuration
Create the sites-enabled directory if it doesn't exist and enable your site configuration.
sudo mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled
sudo ln -sf /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo nginx -t
Obtain SSL certificate with Let's Encrypt
Use certbot to automatically obtain and configure SSL certificates for your domain. Replace example.com with your actual domain.
sudo certbot --nginx -d example.com -d www.example.com --email admin@example.com --agree-tos --no-eff-email
sudo systemctl reload nginx
Configure automatic certificate renewal
Set up a cron job to automatically renew SSL certificates before they expire.
sudo crontab -e
Add this line to run certificate renewal twice daily:
0 /12 /usr/bin/certbot renew --quiet && /usr/bin/systemctl reload nginx
Create upstream configuration for load balancing
If you have multiple backend servers, create an upstream block for load balancing and high availability.
upstream backend_servers {
least_conn;
server 127.0.0.1:3000 max_fails=3 fail_timeout=30s;
server 127.0.0.1:3001 max_fails=3 fail_timeout=30s backup;
keepalive 32;
}
server {
listen 443 ssl http2;
server_name api.example.com;
ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
location / {
proxy_pass http://backend_servers;
proxy_http_version 1.1;
proxy_set_header Connection "";
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;
}
}
Configure log rotation
Set up log rotation to prevent nginx logs from consuming too much disk space.
/var/log/nginx/*.log {
daily
missingok
rotate 14
compress
delaycompress
notifempty
create 644 nginx adm
sharedscripts
postrotate
if [ -f /var/run/nginx.pid ]; then
kill -USR1 cat /var/run/nginx.pid
fi
endscript
}
Set proper file permissions
Ensure nginx configuration files have secure permissions. Never use chmod 777 as it gives full access to all users.
sudo chown -R root:root /etc/nginx/
sudo chmod 644 /etc/nginx/nginx.conf
sudo chmod 644 /etc/nginx/sites-available/*
sudo chmod 644 /etc/nginx/conf.d/*
sudo chmod 755 /etc/nginx/sites-available/
sudo chmod 755 /etc/nginx/sites-enabled/
Test and reload nginx configuration
Test the nginx configuration for syntax errors and reload the service to apply all changes.
sudo nginx -t
sudo systemctl reload nginx
sudo systemctl status nginx
Verify your setup
Test your nginx reverse proxy configuration with these verification commands:
# Check nginx status
sudo systemctl status nginx
Test SSL certificate
curl -I https://example.com
Check SSL certificate expiration
sudo certbot certificates
Test rate limiting (should get 429 after multiple requests)
for i in {1..15}; do curl -I https://example.com; done
Verify security headers
curl -I https://example.com | grep -E "(X-Frame-Options|X-XSS-Protection|Strict-Transport-Security)"
Check nginx error logs
sudo tail -f /var/log/nginx/error.log
Test backend connectivity
curl -H "Host: example.com" http://127.0.0.1:3000
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| 502 Bad Gateway | Backend server not running | Start your backend application on port 3000 |
| Certificate not found | Let's Encrypt failed | Check domain DNS, run sudo certbot --nginx -v |
| Permission denied on logs | Wrong log file ownership | sudo chown nginx:adm /var/log/nginx/*.log |
| Rate limiting not working | Zone not properly configured | Check nginx error log, verify zone syntax in nginx.conf |
| SSL handshake errors | Weak cipher configuration | Update ssl_ciphers to include modern ciphers |
| nginx won't start | Configuration syntax error | Run sudo nginx -t to check configuration |
| Headers not appearing | add_header in wrong context | Move add_header to server or location block |
Next steps
- Configure NGINX rate limiting and DDoS protection with advanced security rules
- Install and configure NGINX with HTTP/3 and modern security headers
- Configure nginx load balancing with health checks and failover
- Setup nginx with ModSecurity web application firewall
- Monitor nginx performance with Prometheus and Grafana
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'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Global variables
DOMAIN="${1:-}"
BACKEND_IP="${2:-127.0.0.1:3000}"
NGINX_USER="nginx"
NGINX_CONFIG_DIR="/etc/nginx"
NGINX_SITES_DIR=""
NGINX_MAIN_CONFIG="/etc/nginx/nginx.conf"
# Usage message
usage() {
echo "Usage: $0 <domain> [backend_ip:port]"
echo "Example: $0 example.com 127.0.0.1:8080"
exit 1
}
# Logging functions
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Cleanup function
cleanup() {
if [ $? -ne 0 ]; then
log_error "Installation failed. Cleaning up..."
systemctl stop nginx 2>/dev/null || true
fi
}
trap cleanup ERR
# Check prerequisites
check_prerequisites() {
if [[ $EUID -ne 0 ]]; then
log_error "This script must be run as root or with sudo"
exit 1
fi
if [[ -z "$DOMAIN" ]]; then
log_error "Domain name is required"
usage
fi
if ! command -v systemctl &> /dev/null; then
log_error "systemctl is required but not installed"
exit 1
fi
}
# Detect distribution
detect_distro() {
if [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_UPDATE="apt update && apt upgrade -y"
PKG_INSTALL="apt install -y"
FIREWALL_CMD="ufw"
NGINX_USER="www-data"
NGINX_SITES_DIR="/etc/nginx/sites-available"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_UPDATE="dnf update -y"
PKG_INSTALL="dnf install -y"
FIREWALL_CMD="firewalld"
# Install EPEL for certbot
dnf install -y epel-release 2>/dev/null || true
;;
amzn)
PKG_MGR="yum"
PKG_UPDATE="yum update -y"
PKG_INSTALL="yum install -y"
FIREWALL_CMD="firewalld"
amazon-linux-extras install epel -y 2>/dev/null || true
;;
*)
log_error "Unsupported distribution: $ID"
exit 1
;;
esac
else
log_error "Cannot detect distribution - /etc/os-release not found"
exit 1
fi
# Set sites directory for RHEL-based systems
if [[ -z "$NGINX_SITES_DIR" ]]; then
NGINX_SITES_DIR="/etc/nginx/conf.d"
fi
}
# Update system packages
update_system() {
echo "[1/8] Updating system packages..."
eval "$PKG_UPDATE"
log_success "System packages updated"
}
# Install required packages
install_packages() {
echo "[2/8] Installing nginx and required packages..."
local packages="nginx certbot python3-certbot-nginx curl"
if [[ "$PKG_MGR" == "apt" ]]; then
packages="$packages ufw"
else
packages="$packages firewalld"
fi
eval "$PKG_INSTALL $packages"
log_success "Packages installed successfully"
}
# Configure firewall
configure_firewall() {
echo "[3/8] Configuring firewall..."
if [[ "$FIREWALL_CMD" == "ufw" ]]; then
ufw --force enable
ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw --force reload
else
systemctl enable --now firewalld
firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --add-service=https
firewall-cmd --permanent --add-service=ssh
firewall-cmd --reload
fi
log_success "Firewall configured"
}
# Start and enable nginx
start_nginx() {
echo "[4/8] Starting and enabling nginx..."
systemctl enable nginx
systemctl start nginx
log_success "Nginx started and enabled"
}
# Create main nginx configuration
create_main_config() {
echo "[5/8] Creating nginx main configuration..."
cat > "$NGINX_MAIN_CONFIG" << 'EOF'
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /run/nginx.pid;
include /usr/share/nginx/modules/*.conf;
events {
worker_connections 1024;
use epoll;
multi_accept on;
}
http {
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 4096;
server_tokens off;
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
# Rate limiting
limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
limit_conn_zone $binary_remote_addr zone=conn_limit_per_ip:10m;
# SSL Configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_stapling on;
ssl_stapling_verify on;
# Gzip Settings
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types
application/atom+xml
application/javascript
application/json
application/rss+xml
application/vnd.ms-fontobject
application/x-font-ttf
application/x-web-app-manifest+json
application/xhtml+xml
application/xml
font/opentype
image/svg+xml
image/x-icon
text/css
text/plain
text/x-component;
include /etc/nginx/conf.d/*.conf;
EOF
# Add sites-enabled include for Debian-based systems
if [[ "$PKG_MGR" == "apt" ]]; then
echo " include /etc/nginx/sites-enabled/*;" >> "$NGINX_MAIN_CONFIG"
mkdir -p /etc/nginx/sites-enabled
fi
echo "}" >> "$NGINX_MAIN_CONFIG"
# Fix user directive for Debian-based systems
if [[ "$PKG_MGR" == "apt" ]]; then
sed -i "s/user nginx;/user $NGINX_USER;/" "$NGINX_MAIN_CONFIG"
fi
chmod 644 "$NGINX_MAIN_CONFIG"
log_success "Main nginx configuration created"
}
# Create site configuration
create_site_config() {
echo "[6/8] Creating site configuration for $DOMAIN..."
local config_file
if [[ "$PKG_MGR" == "apt" ]]; then
config_file="/etc/nginx/sites-available/$DOMAIN"
else
config_file="/etc/nginx/conf.d/$DOMAIN.conf"
fi
cat > "$config_file" << EOF
server {
listen 80;
server_name $DOMAIN;
location / {
return 301 https://\$server_name\$request_uri;
}
location /.well-known/acme-challenge/ {
root /var/www/html;
}
}
server {
listen 443 ssl http2;
server_name $DOMAIN;
# SSL certificates will be added by certbot
# Security headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# Rate limiting
limit_req zone=general burst=20 nodelay;
limit_conn conn_limit_per_ip 10;
location / {
proxy_pass http://$BACKEND_IP;
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 \$host;
proxy_set_header X-Forwarded-Port \$server_port;
# 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;
}
}
EOF
chmod 644 "$config_file"
# Enable site for Debian-based systems
if [[ "$PKG_MGR" == "apt" ]]; then
ln -sf "$config_file" "/etc/nginx/sites-enabled/$DOMAIN"
rm -f /etc/nginx/sites-enabled/default
fi
log_success "Site configuration created"
}
# Obtain SSL certificate
obtain_ssl_certificate() {
echo "[7/8] Obtaining SSL certificate for $DOMAIN..."
# Test nginx configuration
nginx -t
systemctl reload nginx
# Create webroot directory
mkdir -p /var/www/html
chown "$NGINX_USER:$NGINX_USER" /var/www/html
# Obtain certificate
certbot --nginx -d "$DOMAIN" --non-interactive --agree-tos --email "admin@$DOMAIN" --redirect
# Set up automatic renewal
(crontab -l 2>/dev/null; echo "0 12 * * * /usr/bin/certbot renew --quiet") | crontab -
log_success "SSL certificate obtained and auto-renewal configured"
}
# Verify installation
verify_installation() {
echo "[8/8] Verifying installation..."
# Test nginx configuration
if nginx -t; then
log_success "Nginx configuration is valid"
else
log_error "Nginx configuration test failed"
return 1
fi
# Check if nginx is running
if systemctl is-active --quiet nginx; then
log_success "Nginx is running"
else
log_error "Nginx is not running"
return 1
fi
# Test HTTP redirect
if curl -sI "http://$DOMAIN" | grep -q "301\|302"; then
log_success "HTTP to HTTPS redirect is working"
else
log_warn "HTTP redirect test failed - this is normal if DNS is not configured yet"
fi
log_success "Installation completed successfully!"
echo
echo "Next steps:"
echo "1. Point your domain DNS A record to this server's IP address"
echo "2. Your backend application should be running on $BACKEND_IP"
echo "3. Test your site at https://$DOMAIN"
}
# Main execution
main() {
log_info "Starting nginx reverse proxy installation for $DOMAIN"
check_prerequisites
detect_distro
update_system
install_packages
configure_firewall
start_nginx
create_main_config
create_site_config
obtain_ssl_certificate
verify_installation
}
main "$@"
Review the script before running. Execute with: bash install.sh