Set up NGINX with automatic SSL certificate management using Let's Encrypt and Certbot, including security headers and automated renewal for production-ready HTTPS termination.
Prerequisites
- Domain name pointing to your server
- Root or sudo access
- Port 80 and 443 accessible
What this solves
SSL termination handles encrypted connections at your web server, decrypting HTTPS traffic before passing it to backend applications. This tutorial sets up NGINX with Let's Encrypt certificates through Certbot, giving you free, automatically renewing SSL certificates with proper security headers for production use.
Step-by-step installation
Update system packages
Start by updating your package manager to ensure you get the latest versions of NGINX and Certbot.
sudo apt update && sudo apt upgrade -y
Install NGINX web server
Install NGINX which will handle SSL termination and serve as your reverse proxy or web server.
sudo apt install -y nginx
Install Certbot and NGINX plugin
Certbot automates Let's Encrypt certificate requests and renewals. The NGINX plugin handles automatic configuration updates.
sudo apt install -y certbot python3-certbot-nginx
Enable and start NGINX
Enable NGINX to start automatically on boot and start the service now.
sudo systemctl enable --now nginx
sudo systemctl status nginx
Configure firewall for HTTP and HTTPS
Open ports 80 and 443 to allow web traffic. Port 80 is needed for Let's Encrypt validation and HTTP to HTTPS redirects.
sudo ufw allow 'Nginx Full'
sudo ufw reload
Create basic NGINX configuration
Set up a basic server block for your domain. Replace example.com with your actual domain name.
server {
listen 80;
server_name example.com www.example.com;
root /var/www/html;
index index.html index.htm;
location / {
try_files $uri $uri/ =404;
}
# Let's Encrypt challenge location
location /.well-known/acme-challenge/ {
root /var/www/html;
}
}
Enable the site configuration
Create a symbolic link to enable the site and test the NGINX configuration syntax.
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Obtain SSL certificate with Certbot
Request a Let's Encrypt certificate for your domain. Certbot will automatically modify your NGINX configuration to include SSL settings.
sudo certbot --nginx -d example.com -d www.example.com
Configure enhanced security headers
Add security headers to protect against common web vulnerabilities. Update your NGINX configuration with these headers.
server {
listen 443 ssl http2;
server_name example.com www.example.com;
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; preload" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:;" always;
root /var/www/html;
index index.html index.htm;
location / {
try_files $uri $uri/ =404;
}
}
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$server_name$request_uri;
}
Optimize SSL configuration
Configure additional SSL optimizations for better performance and security. Add these settings to your main NGINX configuration.
http {
# SSL Session Cache
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# SSL Stapling
ssl_stapling on;
ssl_stapling_verify on;
# DNS resolver for SSL stapling
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
# Existing configuration...
}
Test and reload configuration
Verify the NGINX configuration syntax and reload to apply all changes.
sudo nginx -t
sudo systemctl reload nginx
Set up automatic certificate renewal
Certbot installs a systemd timer for automatic renewals. Verify it's active and test the renewal process.
sudo systemctl status certbot.timer
sudo certbot renew --dry-run
Create renewal hook script
Create a hook script to reload NGINX after certificate renewal to ensure new certificates are used immediately.
#!/bin/bash
nginx -t && systemctl reload nginx
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
Configure SSL termination for backend applications
Set up reverse proxy configuration
Configure NGINX to terminate SSL and proxy requests to backend applications. This example proxies to a Node.js app running on port 3000.
upstream backend {
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;
}
server {
listen 443 ssl http2;
server_name app.example.com;
ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/app.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; preload" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
# Proxy configuration
location / {
proxy_pass http://backend;
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;
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# Health check endpoint
location /health {
access_log off;
proxy_pass http://backend/health;
proxy_set_header Host $host;
}
}
server {
listen 80;
server_name app.example.com;
return 301 https://$server_name$request_uri;
}
Enable backend application site
Enable the backend application configuration and obtain SSL certificates for it.
sudo ln -s /etc/nginx/sites-available/app.example.com /etc/nginx/sites-enabled/
sudo certbot --nginx -d app.example.com
sudo nginx -t
sudo systemctl reload nginx
Set up monitoring and logging
Configure access and error logging
Set up detailed logging for SSL termination monitoring and troubleshooting.
http {
# Log format with SSL information
log_format ssl_combined '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'ssl_protocol=$ssl_protocol ssl_cipher=$ssl_cipher';
access_log /var/log/nginx/access.log ssl_combined;
error_log /var/log/nginx/error.log warn;
# Existing configuration...
}
Set up log rotation
Configure logrotate to manage NGINX log files and prevent disk space issues.
/var/log/nginx/*.log {
daily
missingok
rotate 14
compress
delaycompress
notifempty
create 644 www-data adm
postrotate
if [ -f /var/run/nginx.pid ]; then
kill -USR1 cat /var/run/nginx.pid
fi
endscript
}
Verify your setup
Test your SSL termination configuration with these verification commands.
# Check NGINX status and configuration
sudo systemctl status nginx
sudo nginx -t
Verify certificate details
sudo certbot certificates
Test SSL configuration
curl -I https://example.com
ssl-cert-check -s example.com -p 443
Check renewal timer
sudo systemctl list-timers certbot.timer
Test HTTP to HTTPS redirect
curl -I http://example.com
You can also use online SSL testing tools to verify your configuration:
# Test with OpenSSL
echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null | openssl x509 -noout -dates
Check SSL Labs rating (replace with your domain)
echo "Test your SSL configuration at: https://www.ssllabs.com/ssltest/analyze.html?d=example.com"
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| Certificate request fails | Domain not pointing to server | Verify DNS A record points to server IP |
| 502 Bad Gateway errors | Backend application down | Check backend service status and proxy_pass configuration |
| SSL certificate not found | Certbot failed to modify config | Run sudo certbot --nginx -d yourdomain.com again |
| Mixed content warnings | HTTP resources on HTTPS page | Update all resource URLs to HTTPS or relative paths |
| Certificate renewal fails | Webroot path inaccessible | Verify /.well-known/acme-challenge/ location in NGINX config |
| HSTS warnings in browser | Previous HTTP access cached | Clear browser cache or wait for HSTS max-age expiry |
Next steps
- Configure NGINX reverse proxy with advanced load balancing
- Set up NGINX monitoring with Prometheus and Grafana
- Configure NGINX rate limiting and DDoS protection
- Optimize NGINX performance for high-traffic websites
- Configure NGINX virtual hosts with SSL certificates for multiple domains
Running this in production?
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' # No Color
# Configuration
DOMAIN=""
EMAIL=""
WEBROOT="/var/www/html"
# Usage function
usage() {
echo "Usage: $0 -d domain.com -e email@domain.com"
echo " -d: Domain name (required)"
echo " -e: Email for Let's Encrypt (required)"
echo " -h: Show this help"
exit 1
}
# Parse command line arguments
while getopts "d:e:h" opt; do
case $opt in
d) DOMAIN="$OPTARG" ;;
e) EMAIL="$OPTARG" ;;
h) usage ;;
*) usage ;;
esac
done
# Validate required arguments
if [[ -z "$DOMAIN" || -z "$EMAIL" ]]; then
echo -e "${RED}Error: Domain and email are required${NC}"
usage
fi
# Validate email format
if [[ ! "$EMAIL" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
echo -e "${RED}Error: Invalid email format${NC}"
exit 1
fi
# Check if running as root or with sudo
if [[ $EUID -ne 0 ]]; then
echo -e "${RED}Error: This script must be run as root or with sudo${NC}"
exit 1
fi
# Auto-detect distribution
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_SITES_DIR="/etc/nginx/sites-available"
NGINX_ENABLED_DIR="/etc/nginx/sites-enabled"
USE_SITES_ENABLED=true
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_UPDATE="dnf update -y"
PKG_INSTALL="dnf install -y"
FIREWALL_CMD="firewall-cmd"
NGINX_SITES_DIR="/etc/nginx/conf.d"
USE_SITES_ENABLED=false
;;
amzn)
PKG_MGR="yum"
PKG_UPDATE="yum update -y"
PKG_INSTALL="yum install -y"
FIREWALL_CMD="firewall-cmd"
NGINX_SITES_DIR="/etc/nginx/conf.d"
USE_SITES_ENABLED=false
;;
*)
echo -e "${RED}Error: Unsupported distribution: $ID${NC}"
exit 1
;;
esac
else
echo -e "${RED}Error: Cannot detect distribution${NC}"
exit 1
fi
# Cleanup function for rollback
cleanup() {
echo -e "${YELLOW}Cleaning up...${NC}"
systemctl stop nginx 2>/dev/null || true
if [[ "$USE_SITES_ENABLED" == true ]]; then
rm -f "$NGINX_ENABLED_DIR/$DOMAIN" 2>/dev/null || true
fi
rm -f "$NGINX_SITES_DIR/$DOMAIN.conf" 2>/dev/null || true
}
# Set trap for cleanup on error
trap cleanup ERR
echo -e "${GREEN}=== NGINX SSL Setup with Let's Encrypt ===${NC}"
echo "Domain: $DOMAIN"
echo "Distribution: $PRETTY_NAME"
# Step 1: Update system packages
echo -e "${GREEN}[1/9] Updating system packages...${NC}"
$PKG_UPDATE
# Step 2: Install NGINX
echo -e "${GREEN}[2/9] Installing NGINX...${NC}"
$PKG_INSTALL nginx
# Step 3: Install Certbot
echo -e "${GREEN}[3/9] Installing Certbot...${NC}"
if [[ "$PKG_MGR" == "apt" ]]; then
$PKG_INSTALL certbot python3-certbot-nginx
else
$PKG_INSTALL certbot python3-certbot-nginx
fi
# Step 4: Enable and start NGINX
echo -e "${GREEN}[4/9] Enabling and starting NGINX...${NC}"
systemctl enable nginx
systemctl start nginx
# Step 5: Configure firewall
echo -e "${GREEN}[5/9] Configuring firewall...${NC}"
if [[ "$FIREWALL_CMD" == "ufw" ]]; then
ufw --force enable
ufw allow 'Nginx Full'
ufw reload
else
systemctl enable firewalld
systemctl start firewalld
firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --add-service=https
firewall-cmd --reload
fi
# Step 6: Create webroot directory
echo -e "${GREEN}[6/9] Creating webroot directory...${NC}"
mkdir -p "$WEBROOT"
chown -R nginx:nginx "$WEBROOT" 2>/dev/null || chown -R www-data:www-data "$WEBROOT" 2>/dev/null || true
chmod 755 "$WEBROOT"
# Step 7: Create basic NGINX configuration
echo -e "${GREEN}[7/9] Creating NGINX configuration...${NC}"
if [[ "$USE_SITES_ENABLED" == true ]]; then
CONFIG_FILE="$NGINX_SITES_DIR/$DOMAIN"
else
CONFIG_FILE="$NGINX_SITES_DIR/$DOMAIN.conf"
fi
cat > "$CONFIG_FILE" << EOF
server {
listen 80;
server_name $DOMAIN www.$DOMAIN;
root $WEBROOT;
index index.html index.htm;
location / {
try_files \$uri \$uri/ =404;
}
# Let's Encrypt challenge location
location /.well-known/acme-challenge/ {
root $WEBROOT;
}
}
EOF
# Enable site if using sites-enabled structure
if [[ "$USE_SITES_ENABLED" == true ]]; then
ln -sf "$CONFIG_FILE" "$NGINX_ENABLED_DIR/$DOMAIN"
fi
# Test NGINX configuration
nginx -t
systemctl reload nginx
# Create a simple index page
echo "<h1>Welcome to $DOMAIN</h1>" > "$WEBROOT/index.html"
chown nginx:nginx "$WEBROOT/index.html" 2>/dev/null || chown www-data:www-data "$WEBROOT/index.html" 2>/dev/null || true
chmod 644 "$WEBROOT/index.html"
# Step 8: Obtain SSL certificate
echo -e "${GREEN}[8/9] Obtaining SSL certificate...${NC}"
certbot --nginx -d "$DOMAIN" -d "www.$DOMAIN" --email "$EMAIL" --agree-tos --non-interactive
# Step 9: Enhanced SSL configuration with security headers
echo -e "${GREEN}[9/9] Applying enhanced SSL configuration...${NC}"
cat > "$CONFIG_FILE" << EOF
server {
listen 443 ssl http2;
server_name $DOMAIN www.$DOMAIN;
ssl_certificate /etc/letsencrypt/live/$DOMAIN/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/$DOMAIN/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; preload" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:;" always;
root $WEBROOT;
index index.html index.htm;
location / {
try_files \$uri \$uri/ =404;
}
}
server {
listen 80;
server_name $DOMAIN www.$DOMAIN;
return 301 https://\$server_name\$request_uri;
}
EOF
# Test and reload NGINX
nginx -t
systemctl reload nginx
# Verification
echo -e "${GREEN}=== Verification ===${NC}"
echo "✓ NGINX status:"
systemctl status nginx --no-pager -l
echo "✓ SSL certificate status:"
certbot certificates
echo "✓ Testing HTTPS connection:"
if curl -s -I "https://$DOMAIN" | head -1 | grep -q "200\|301\|302"; then
echo -e "${GREEN}✓ HTTPS is working${NC}"
else
echo -e "${YELLOW}⚠ HTTPS test inconclusive${NC}"
fi
echo -e "${GREEN}=== Setup Complete ===${NC}"
echo "Your NGINX server with SSL is now configured!"
echo "Domain: https://$DOMAIN"
echo "Certificate auto-renewal is enabled via systemd timer"
# Remove error trap
trap - ERR
Review the script before running. Execute with: bash install.sh