Set up multiple domains on a single NGINX server with automatic SSL certificates from Let's Encrypt. Handle domain routing, certificate management, and secure configurations for production hosting.
Prerequisites
- Root or sudo access
- Domain names pointing to your server
- Basic understanding of DNS
- Firewall configured for HTTP/HTTPS
What this solves
When hosting multiple websites or applications on a single server, you need virtual hosts to route traffic based on domain names. This tutorial shows you how to configure NGINX to serve multiple domains, each with its own SSL certificate from Let's Encrypt, ensuring secure connections and proper traffic routing.
Step-by-step configuration
Update system packages
Start by updating your package manager to ensure you get the latest versions of all packages.
sudo apt update && sudo apt upgrade -y
Install NGINX and Certbot
Install NGINX web server and Certbot for Let's Encrypt SSL certificate management. Certbot will handle automatic certificate issuance and renewal.
sudo apt install -y nginx certbot python3-certbot-nginx
Enable and start NGINX
Start NGINX and enable it to start automatically on boot. This ensures your web server is always running.
sudo systemctl enable --now nginx
sudo systemctl status nginx
Create directory structure for domains
Create directories to store website files for each domain. This keeps content organized and separated per domain.
sudo mkdir -p /var/www/example.com/html
sudo mkdir -p /var/www/api.example.com/html
sudo mkdir -p /var/www/blog.example.com/html
Set proper ownership and permissions
Set correct ownership to the web server user and secure permissions. The web server needs read access to serve files, but files should not be world-writable.
sudo chown -R www-data:www-data /var/www/
sudo chmod -R 755 /var/www/
Create test content for each domain
Create simple HTML files to test that each virtual host is working correctly. This helps verify routing before adding SSL certificates.
<!DOCTYPE html>
<html>
<head>
<title>Welcome to example.com</title>
</head>
<body>
<h1>example.com is working</h1>
<p>This is the main domain.</p>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<title>API - api.example.com</title>
</head>
<body>
<h1>api.example.com is working</h1>
<p>This is the API subdomain.</p>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<title>Blog - blog.example.com</title>
</head>
<body>
<h1>blog.example.com is working</h1>
<p>This is the blog subdomain.</p>
</body>
</html>
Create virtual host configuration for main domain
Create the first virtual host configuration file. This defines how NGINX handles requests for your main domain.
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
root /var/www/example.com/html;
index index.html index.htm index.nginx-debian.html;
location / {
try_files $uri $uri/ =404;
}
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
# Logging
access_log /var/log/nginx/example.com.access.log;
error_log /var/log/nginx/example.com.error.log;
}
Create virtual host configuration for API subdomain
Create a separate configuration for your API subdomain. This allows different configurations and routing rules per domain.
server {
listen 80;
listen [::]:80;
server_name api.example.com;
root /var/www/api.example.com/html;
index index.html index.htm;
location / {
try_files $uri $uri/ =404;
}
# API-specific configurations
location /api/ {
# Add CORS headers for API
add_header Access-Control-Allow-Origin "*";
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS";
add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization";
try_files $uri $uri/ =404;
}
# Security headers
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
# Logging
access_log /var/log/nginx/api.example.com.access.log;
error_log /var/log/nginx/api.example.com.error.log;
}
Create virtual host configuration for blog subdomain
Create the third virtual host for your blog subdomain. Each domain can have unique configurations and security settings.
server {
listen 80;
listen [::]:80;
server_name blog.example.com;
root /var/www/blog.example.com/html;
index index.html index.htm;
location / {
try_files $uri $uri/ =404;
}
# Cache static assets for blog
location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# Logging
access_log /var/log/nginx/blog.example.com.access.log;
error_log /var/log/nginx/blog.example.com.error.log;
}
Enable virtual hosts
Create symbolic links to enable each virtual host configuration. NGINX only loads configurations from the sites-enabled directory.
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo ln -s /etc/nginx/sites-available/api.example.com /etc/nginx/sites-enabled/
sudo ln -s /etc/nginx/sites-available/blog.example.com /etc/nginx/sites-enabled/
Remove default NGINX configuration
Remove the default NGINX site to prevent conflicts with your virtual hosts.
sudo rm /etc/nginx/sites-enabled/default
Test NGINX configuration
Test the NGINX configuration for syntax errors before reloading. This prevents breaking your web server with invalid configurations.
sudo nginx -t
Reload NGINX configuration
Reload NGINX to apply the new virtual host configurations.
sudo systemctl reload nginx
Configure firewall for HTTP and HTTPS
Open firewall ports for web traffic. Port 80 is for HTTP and port 443 is for HTTPS traffic.
sudo ufw allow 'Nginx Full'
sudo ufw status
Obtain SSL certificates for all domains
Use Certbot to obtain Let's Encrypt SSL certificates for all domains. This command will automatically configure NGINX for HTTPS and set up certificate renewal.
sudo certbot --nginx -d example.com -d www.example.com -d api.example.com -d blog.example.com
Configure automatic certificate renewal
Set up automatic renewal for Let's Encrypt certificates. Certificates expire every 90 days and need to be renewed automatically.
sudo systemctl status certbot.timer
sudo certbot renew --dry-run
Enhance security with additional SSL configuration
Add additional SSL security settings to your main NGINX configuration. This improves SSL security and performance.
# SSL Security 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-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
OCSP Stapling
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
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;
Test and reload final configuration
Test the complete configuration including SSL settings and reload NGINX.
sudo nginx -t
sudo systemctl reload nginx
Verify your setup
Test that each domain is working correctly with SSL certificates:
# Test HTTP redirects to HTTPS
curl -I http://example.com
curl -I http://api.example.com
curl -I http://blog.example.com
Test HTTPS connections
curl -I https://example.com
curl -I https://api.example.com
curl -I https://blog.example.com
Check SSL certificate details
echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null | openssl x509 -noout -dates
Check NGINX status
sudo systemctl status nginx
Check certificate renewal timer
sudo systemctl status certbot.timer
Visit each domain in your browser to verify:
- https://example.com - should show main domain content
- https://api.example.com - should show API domain content
- https://blog.example.com - should show blog domain content
- All should have valid SSL certificates (green lock icon)
- HTTP requests should redirect to HTTPS automatically
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| Domain shows NGINX default page | Virtual host not enabled or default site still active | Check sudo nginx -T | grep server_name and remove default site |
| SSL certificate error | DNS not pointing to server or firewall blocking | Verify DNS with nslookup example.com and check firewall rules |
| Permission denied errors | Incorrect file ownership or permissions | Run sudo chown -R www-data:www-data /var/www/ and sudo chmod -R 755 /var/www/ |
| NGINX configuration test fails | Syntax error in configuration files | Check sudo nginx -t output for line numbers and fix syntax errors |
| Certificate renewal fails | Certbot can't access domains for renewal | Ensure domains resolve correctly and check sudo certbot certificates |
| Mixed content warnings | HTTP resources loaded on HTTPS pages | Update all resource URLs to HTTPS or use protocol-relative URLs |
Next steps
- Configure NGINX rate limiting and DDoS protection to secure your domains
- Monitor NGINX performance with Prometheus and Grafana for comprehensive observability
- Set up NGINX reverse proxy with SSL termination for application backends
- Configure NGINX caching with Redis backend for improved performance
- Set up NGINX log analysis with ELK stack for advanced monitoring
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
# Global variables
NGINX_SITES_PATH=""
NGINX_USER=""
STEP_COUNT=0
TOTAL_STEPS=12
# Usage message
usage() {
echo "Usage: $0 <primary_domain> [subdomain1] [subdomain2] ..."
echo "Example: $0 example.com api.example.com blog.example.com"
echo "Note: Domains must be pointing to this server's IP for SSL to work"
exit 1
}
# Print colored output
print_status() {
local color=$1
local message=$2
echo -e "${color}${message}${NC}"
}
# Progress counter
progress() {
STEP_COUNT=$((STEP_COUNT + 1))
print_status $YELLOW "[${STEP_COUNT}/${TOTAL_STEPS}] $1"
}
# Error cleanup
cleanup() {
if [ $? -ne 0 ]; then
print_status $RED "Installation failed. Cleaning up..."
systemctl stop nginx 2>/dev/null || true
rm -f /etc/nginx/sites-available/nginx-vhosts-* 2>/dev/null || true
rm -f /etc/nginx/conf.d/nginx-vhosts-* 2>/dev/null || true
rm -rf /var/www/nginx-vhost-* 2>/dev/null || true
fi
}
trap cleanup ERR
# Check prerequisites
check_prereqs() {
if [[ $EUID -ne 0 ]]; then
print_status $RED "This script must be run as root or with sudo"
exit 1
fi
if [ $# -lt 1 ]; then
usage
fi
}
# Detect distribution and set package manager
detect_distro() {
progress "Detecting distribution and configuring package manager"
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"
NGINX_SITES_PATH="/etc/nginx/sites-available"
NGINX_USER="www-data"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_INSTALL="dnf install -y"
PKG_UPDATE="dnf update -y"
NGINX_SITES_PATH="/etc/nginx/conf.d"
NGINX_USER="nginx"
;;
amzn)
PKG_MGR="yum"
PKG_INSTALL="yum install -y"
PKG_UPDATE="yum update -y"
NGINX_SITES_PATH="/etc/nginx/conf.d"
NGINX_USER="nginx"
;;
*)
print_status $RED "Unsupported distribution: $ID"
exit 1
;;
esac
print_status $GREEN "Detected: $PRETTY_NAME using $PKG_MGR"
else
print_status $RED "Cannot detect distribution - /etc/os-release not found"
exit 1
fi
}
# Update system packages
update_system() {
progress "Updating system packages"
$PKG_UPDATE
}
# Install required packages
install_packages() {
progress "Installing NGINX and Certbot"
if [ "$PKG_MGR" = "dnf" ]; then
$PKG_INSTALL epel-release
fi
$PKG_INSTALL nginx certbot python3-certbot-nginx
}
# Configure firewall
configure_firewall() {
progress "Configuring firewall"
if command -v ufw >/dev/null 2>&1; then
ufw allow 'Nginx Full' 2>/dev/null || true
elif command -v firewall-cmd >/dev/null 2>&1; then
firewall-cmd --permanent --add-service=http 2>/dev/null || true
firewall-cmd --permanent --add-service=https 2>/dev/null || true
firewall-cmd --reload 2>/dev/null || true
fi
}
# Start and enable NGINX
start_nginx() {
progress "Starting and enabling NGINX"
systemctl enable nginx
systemctl start nginx
systemctl is-active nginx >/dev/null || {
print_status $RED "Failed to start NGINX"
exit 1
}
}
# Create directory structure
create_directories() {
progress "Creating directory structure for domains"
for domain in "$@"; do
mkdir -p "/var/www/${domain}/html"
print_status $GREEN "Created directory for $domain"
done
chown -R $NGINX_USER:$NGINX_USER /var/www/
chmod -R 755 /var/www/
}
# Create test content
create_test_content() {
progress "Creating test content for each domain"
for domain in "$@"; do
cat > "/var/www/${domain}/html/index.html" << EOF
<!DOCTYPE html>
<html>
<head>
<title>Welcome to ${domain}</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body { font-family: Arial, sans-serif; margin: 40px; }
h1 { color: #333; }
.status { background: #e8f5e8; padding: 20px; border-radius: 5px; }
</style>
</head>
<body>
<div class="status">
<h1>${domain} is working!</h1>
<p>NGINX virtual host is properly configured.</p>
<p>Server time: $(date)</p>
</div>
</body>
</html>
EOF
chown $NGINX_USER:$NGINX_USER "/var/www/${domain}/html/index.html"
chmod 644 "/var/www/${domain}/html/index.html"
done
}
# Create virtual host configurations
create_vhost_configs() {
progress "Creating virtual host configurations"
for domain in "$@"; do
if [ "$NGINX_SITES_PATH" = "/etc/nginx/sites-available" ]; then
config_file="/etc/nginx/sites-available/${domain}"
ln_target="/etc/nginx/sites-enabled/${domain}"
else
config_file="/etc/nginx/conf.d/${domain}.conf"
fi
cat > "$config_file" << EOF
server {
listen 80;
listen [::]:80;
server_name ${domain};
root /var/www/${domain}/html;
index index.html index.htm index.nginx-debian.html;
location / {
try_files \$uri \$uri/ =404;
}
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
# Disable server tokens
server_tokens off;
# Logging
access_log /var/log/nginx/${domain}.access.log;
error_log /var/log/nginx/${domain}.error.log;
# Security: deny access to hidden files
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
}
EOF
chmod 644 "$config_file"
# Enable site on Debian-based systems
if [ "$NGINX_SITES_PATH" = "/etc/nginx/sites-available" ]; then
ln -sf "$config_file" "$ln_target"
fi
print_status $GREEN "Created configuration for $domain"
done
}
# Test NGINX configuration
test_nginx_config() {
progress "Testing NGINX configuration"
nginx -t || {
print_status $RED "NGINX configuration test failed"
exit 1
}
systemctl reload nginx
}
# Install SSL certificates
install_ssl() {
progress "Installing SSL certificates with Let's Encrypt"
print_status $YELLOW "Note: Domains must be pointing to this server for SSL to work"
for domain in "$@"; do
print_status $YELLOW "Attempting SSL certificate for $domain..."
if certbot --nginx -d "$domain" --non-interactive --agree-tos --email "admin@$domain" --redirect; then
print_status $GREEN "SSL certificate installed for $domain"
else
print_status $YELLOW "SSL certificate installation failed for $domain - continuing with HTTP"
fi
done
# Set up auto-renewal
if ! crontab -l 2>/dev/null | grep -q "certbot renew"; then
(crontab -l 2>/dev/null; echo "0 12 * * * /usr/bin/certbot renew --quiet") | crontab -
print_status $GREEN "SSL auto-renewal configured"
fi
}
# Final verification
verify_installation() {
progress "Verifying installation"
if ! systemctl is-active nginx >/dev/null; then
print_status $RED "NGINX is not running"
exit 1
fi
for domain in "$@"; do
if [ -d "/var/www/${domain}" ]; then
print_status $GREEN "✓ Directory structure for $domain"
fi
if curl -s -H "Host: $domain" http://localhost/ | grep -q "$domain is working"; then
print_status $GREEN "✓ Virtual host for $domain is responding"
else
print_status $YELLOW "⚠ Virtual host for $domain may not be working properly"
fi
done
print_status $GREEN "Installation completed successfully!"
print_status $YELLOW "Next steps:"
echo " 1. Point your domains to this server's IP address"
echo " 2. Test each domain in a web browser"
echo " 3. Replace test content in /var/www/<domain>/html/"
echo " 4. SSL certificates will auto-renew via cron"
}
# Main execution
main() {
check_prereqs "$@"
detect_distro
update_system
install_packages
configure_firewall
start_nginx
create_directories "$@"
create_test_content "$@"
create_vhost_configs "$@"
test_nginx_config
install_ssl "$@"
verify_installation "$@"
}
main "$@"
Review the script before running. Execute with: bash install.sh