Configure NGINX virtual hosts with SSL certificates for multiple domains

Intermediate 25 min May 06, 2026 66 views
Ubuntu 24.04 Debian 12 AlmaLinux 9 Rocky Linux 9

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
sudo dnf update -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
sudo dnf install -y nginx certbot python3-certbot-nginx
sudo dnf install -y epel-release

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/
Never use chmod 777. It gives every user on the system full access to your files. Instead, use proper ownership with chown and minimal permissions like 755 for directories and 644 for files.

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
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload

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
Note: Make sure your domains are pointing to your server's IP address via DNS before running this command. Let's Encrypt needs to verify domain ownership through HTTP challenges.

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

Running this in production?

Want this handled for you? Setting this up once is straightforward. Keeping it patched, monitored, backed up and performant across environments is the harder part. See how we run infrastructure like this for European teams.

Automated install script

Run this to automate the entire setup

Need help?

Don't want to manage this yourself?

We handle managed cloud infrastructure for businesses that depend on uptime. From initial setup to ongoing operations.