Create NGINX virtual hosts

This article shows you how to create NGINX® virtual hosts that serve
multiple web domains.

Introduction

Virtual hosts (also called server blocks in NGINX terminology) enable you to host multiple websites or applications on a single server. Each virtual host can serve different domains, respond to different ports, or handle traffic based on various conditions. This guide covers contemporary best practices for configuring NGINX virtual hosts on modern Linux systems.

Prerequisites

  • A Linux server with NGINX installed (Ubuntu 22.04/24.04, Debian 11/12, RHEL 9, Rocky Linux 9, or similar).
  • Root or sudo access.
  • Basic understanding of DNS configuration.
  • Domain names pointing to your server's IP address.

Understanding NGINX Directory Structure

Modern NGINX installations typically use this structure:

/etc/nginx/nginx.conf        - Main configuration file
/etc/nginx/sites-available/  - Individual site configurations (Ubuntu/Debian)
/etc/nginx/sites-enabled/    - Active site configurations (Ubuntu/Debian)
/etc/nginx/conf.d/           - Alternative for site configs (RHEL/Rocky/Alma/EL)
/var/www/                    - Default web root directory
/var/log/nginx               - Log files location
Note: RHEL-based systems typically use /etc/nginx/conf.d/ exclusively, while Debian-based systems use the sites-available/sites-enabled pattern.

Prepare Your Directory Structure

Create directories for each website:

$ sudo mkdir -p /var/www/example.com/public_html
$ sudo mkdir -p /var/www/example.com/logs
$ sudo mkdir -p /var/www/testsite.net/public_html
$ sudo mkdir -p /var/www/testsite.net/logs

Set appropriate ownership:

$ sudo chown -R $USER:$USER /var/www/example.com
$ sudo chown -R $USER:$USER /var/www/testsite.net

Set secure permissions:

$ sudo chmod -R 755 /var/www

Create Sample Content

Create a test page for the first site:

# cat > /var/www/example.com/public_html/index.html << 'EOF'
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Welcome to Example.com</title>
</head>
<body>
    <h1>Success! Example.com is working</h1>
    <p>This virtual host is configured correctly.</p>
</body>
</html>
EOF

Create a test page for the second site:

# cat > /var/www/testsite.net/public_html/index.html << 'EOF'
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Welcome to TestSite.net</title>
</head>
<body>
    <h1>Success! TestSite.net is working</h1>
    <p>This virtual host is configured correctly.</p>
</body>
</html>
EOF

Create Virtual Host Configuration Files

For Ubuntu/Debian Systems

Create the first virtual host configuration:

$ sudo vim /etc/nginx/sites-available/example.com

Add this configuration:

server {
    listen 80;
    listen [::]:80;
    
    server_name example.com www.example.com;
    
    root /var/www/example.com/public_html;
    index index.html index.htm index.php;
    
    access_log /var/www/example.com/logs/access.log;
    error_log /var/www/example.com/logs/error.log;
    
    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;
    
    # Disable access to hidden files
    location ~ /\. {
        deny all;
        access_log off;
        log_not_found off;
    }
}

Create the second virtual host:

$ sudo vim /etc/nginx/sites-available/testsite.net
server {
    listen 80;
    listen [::]:80;
    
    server_name testsite.net www.testsite.net;
    
    root /var/www/testsite.net/public_html;
    index index.html index.htm index.php;
    
    access_log /var/www/testsite.net/logs/access.log;
    error_log /var/www/testsite.net/logs/error.log;
    
    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;
    
    # Disable access to hidden files
    location ~ /\. {
        deny all;
        access_log off;
        log_not_found off;
    }
}

Enable the sites by creating symbolic links:

$ sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
$ sudo ln -s /etc/nginx/sites-available/testsite.net /etc/nginx/sites-enabled/

For RHEL/Rocky/AlmaLinux Systems

You can create the file in "/etc/nginx/conf.d/" or create a "/etc/nginx/vhosts/" as well.
However, you will need to mention it in the "/etc/nginx/nginx.conf" file to include the directory structure.

$ grep vhosts /etc/nginx/nginx.conf
    include /etc/nginx/vhosts/*.conf;

Create configuration files directly in /etc/nginx/conf.d/

$ sudo vim /etc/nginx/conf.d/example.com.conf

Use the same server block configuration as shown above. Repeat for additional sites with different filenames (e.g., testsite.net.conf).

Test and Apply Configuration

Always test your configuration before applying:

$ sudo nginx -t

You should see:

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

If tests pass, reload NGINX and check status:

$ sudo systemctl reload nginx
$ sudo systemctl status nginx

Configure Firewall

If you are using a firewall at the OS level, the below commands will be helpful.

Allow HTTP and HTTPS traffic:

For UFW (Ubuntu/Debian):

$ sudo ufw allow 'Nginx Full'

For firewalld (RHEL/Rocky):

sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload

Other Configuration Examples:

Virtual Host with PHP Support:

server {
    listen 80;
    server_name example.com www.example.com;
    root /var/www/example.com/public_html;
    index index.php index.html;
    
    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }
    
    location ~ \.php$ {
        fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }
}

Virtual Host with SSL/TLS (HTTPS)

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    
    server_name example.com www.example.com;
    root /var/www/example.com/public_html;
    
    ssl_certificate /etc/ssl/certs/example.com.crt;
    ssl_certificate_key /etc/ssl/private/example.com.key;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;
    
    location / {
        try_files $uri $uri/ =404;
    }
}

# HTTP to HTTPS redirect
server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;
    return 301 https://$server_name$request_uri;
}

Reverse Proxy Configuration

server {
    listen 80;
    server_name app.example.com;
    
    location / {
        proxy_pass http://localhost: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;
    }
}

Troubleshooting Common Issues

You can review the "basic-nginx-troubleshooting" article as well.

Check NGINX Error Logs:

$ sudo tail -f /var/log/nginx/error.log

Verify Port Listening:

$ sudo ss -tlnp | grep nginx

Test Configuration Syntax:

$ sudo nginx -t

Check SELinux Status (RHEL-based):

If you're having permission issues on RHEL systems:

$ sestatus                                          ### (if enforced you can check below)
$ sudo setsebool -P httpd_can_network_connect 1
$ sudo chcon -R -t httpd_sys_content_t /var/www/

Common Error: "Address Already in Use."

This means another service is using port 80 or 443:

$ sudo systemctl status nginx
× nginx.service - The nginx HTTP and reverse proxy server
     Loaded: loaded (/usr/lib/systemd/system/nginx.service; disabled; preset: disabled)
    Drop-In: /usr/lib/systemd/system/nginx.service.d
             └─php-fpm.conf
     Active: failed (Result: exit-code) since Fri 2026-01-09 10:38:06 UTC; 5s ago
    Process: 110059 ExecStartPre=/usr/bin/rm -f /run/nginx.pid (code=exited, status=0/SUCCESS)
    Process: 110060 ExecStartPre=/usr/sbin/nginx -t (code=exited, status=0/SUCCESS)
    Process: 110061 ExecStart=/usr/sbin/nginx (code=exited, status=1/FAILURE)
        CPU: 34ms

Jan 09 10:38:04 testing-el nginx[110061]: nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)
Jan 09 10:38:04 testing-el nginx[110061]: nginx: [emerg] bind() to [::]:80 failed (98: Address already in use)
Jan 09 10:38:05 testing-el nginx[110061]: nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)
Jan 09 10:38:05 testing-el nginx[110061]: nginx: [emerg] bind() to [::]:80 failed (98: Address already in use)
Jan 09 10:38:05 testing-el nginx[110061]: nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)
Jan 09 10:38:05 testing-el nginx[110061]: nginx: [emerg] bind() to [::]:80 failed (98: Address already in use)
Jan 09 10:38:06 testing-el nginx[110061]: nginx: [emerg] still could not bind()
Jan 09 10:38:06 testing-el systemd[1]: nginx.service: Control process exited, code=exited, status=1/FAILURE
Jan 09 10:38:06 testing-el systemd[1]: nginx.service: Failed with result 'exit-code'.
Jan 09 10:38:06 testing-el systemd[1]: Failed to start The nginx HTTP and reverse proxy server.

$ sudo lsof -i :80
$ sudo lsof -i :443
Note: In the scenario described above, you have two options: you can either change the port for nginx in its configuration or, if another service like Apache or HTTP is not in use, you can stop that service to use nginx instead.

After stopping the other service, remember to restart or reload nginx.

Additional

Adding these directives to your virtual host can increase your performance:

# Enable gzip compression
gzip on;
gzip_vary on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript;

# Browser caching for static files
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2)$ {
    expires 30d;
    add_header Cache-Control "public, immutable";
}

Most Linux distributions include a default NGINX log rotation configuration, but creating custom configurations per virtual host provides better control and organization.

Best Practices

  • Use separate configuration files for each virtual host rather than one large file.
  • Always test the configuration with nginx -t before reloading.
  • Keep logs organized by storing each site's logs in its own directory.
  • Implement security headers to protect against common vulnerabilities.
  • Use SSL/TLS for all production websites (Let's Encrypt provides free certificates).
  • Set appropriate file permissions (755 for directories, 644 for files).
  • Regular backups of configuration files before making changes
  • Monitor logs regularly for errors and suspicious activity.
  • Keep NGINX updated to the latest stable version.
  • Document your configurations with comments for future reference.