Virtual Hosts¶
A single server can host many websites at once using virtual hosting. The web server inspects the Host: header of each incoming request and routes it to the matching site. This is called name-based virtual hosting, and it is how one IP address serves example.com, shop.example.com, and blog.example.org simultaneously. This page shows the nginx and Apache approaches and a sane directory layout.
Tested on
AlmaLinux 9 / RHEL 9. Debian/Ubuntu differences are noted inline.
Recommended directory layout¶
Give each site its own document root and its own log files. A common convention:
/var/www/
├── example.com/
│ ├── public/ # document root (only this is web-served)
│ └── logs/ # optional per-site app logs
├── shop.example.com/
│ └── public/
└── blog.example.org/
└── public/
Serving from a public/ (or htdocs/) subdirectory keeps application code, configs, and .env files out of the web root.
sudo mkdir -p /var/www/example.com/public /var/www/shop.example.com/public
echo '<h1>example.com</h1>' | sudo tee /var/www/example.com/public/index.html
echo '<h1>shop.example.com</h1>' | sudo tee /var/www/shop.example.com/public/index.html
SELinux on RHEL/AlmaLinux
Every custom document root needs the httpd_sys_content_t label, or you get 403s:
sudo semanage fcontext -a -t httpd_sys_content_t "/var/www(/.*)?/public(/.*)?"
sudo restorecon -Rv /var/www
More detail in SELinux.
nginx approach: one server {} per domain¶
Put one file per site under /etc/nginx/conf.d/ (Debian: under sites-available, then symlink into sites-enabled). Each gets a distinct server_name, root, and its own log files. See Nginx for the full server {} anatomy.
/etc/nginx/conf.d/example.com.conf:
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
root /var/www/example.com/public;
index index.html index.php;
access_log /var/log/nginx/example.com.access.log;
error_log /var/log/nginx/example.com.error.log;
location / {
try_files $uri $uri/ =404;
}
}
/etc/nginx/conf.d/shop.example.com.conf:
server {
listen 80;
listen [::]:80;
server_name shop.example.com;
root /var/www/shop.example.com/public;
index index.html index.php;
access_log /var/log/nginx/shop.example.com.access.log;
error_log /var/log/nginx/shop.example.com.error.log;
location / {
try_files $uri $uri/ =404;
}
}
Test and reload:
nginx default / catch-all server¶
When a request's Host matches no server_name, nginx uses the default server for that port — by default, the first matching server block. To control what unknown hosts get (and to reject scrapers and bare-IP scans), define an explicit default:
# /etc/nginx/conf.d/00-default.conf
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _; # matches nothing real; only reached as the default
return 444; # nginx-specific: close the connection with no response
}
The default_server flag forces this block to handle unmatched requests. Using 444 quietly drops them; you could instead return 404; or serve a generic page.
Apache approach: multiple <VirtualHost> blocks¶
Apache does name-based virtual hosting with several <VirtualHost *:80> blocks, each with its own ServerName and DocumentRoot. See Apache for module and .htaccess details.
On RHEL/AlmaLinux, create /etc/httpd/conf.d/vhosts.conf (or one file per site):
<VirtualHost *:80>
ServerName example.com
ServerAlias www.example.com
DocumentRoot /var/www/example.com/public
<Directory /var/www/example.com/public>
Require all granted
AllowOverride All
</Directory>
ErrorLog /var/log/httpd/example.com-error.log
CustomLog /var/log/httpd/example.com-access.log combined
</VirtualHost>
<VirtualHost *:80>
ServerName shop.example.com
DocumentRoot /var/www/shop.example.com/public
<Directory /var/www/shop.example.com/public>
Require all granted
AllowOverride All
</Directory>
ErrorLog /var/log/httpd/shop.example.com-error.log
CustomLog /var/log/httpd/shop.example.com-access.log combined
</VirtualHost>
Test and reload:
Apache on Debian: a2ensite¶
On Debian, put each vhost in its own file under /etc/apache2/sites-available/ (e.g. example.com.conf, logs in /var/log/apache2/) and enable it:
Apache default / catch-all virtual host¶
Apache uses the first <VirtualHost> for the matching port as the default for unmatched hostnames. To make this deliberate, define a catch-all as the first block:
# Place this BEFORE your real vhosts so it is matched first for unknown hosts
<VirtualHost *:80>
ServerName catchall.invalid
DocumentRoot /var/www/default
<Directory /var/www/default>
Require all granted
</Directory>
</VirtualHost>
You can verify which vhost answers what with httpd -S (RHEL) or apache2ctl -S (Debian), which prints the virtual-host mapping.
Testing locally before DNS is ready¶
Before you point real DNS records at the server, you can test name-based vhosts by overriding name resolution on your client machine via /etc/hosts:
# On your laptop/workstation (Linux/macOS), as root:
# Map the hostnames to the server's IP
echo '203.0.113.10 example.com www.example.com shop.example.com' | sudo tee -a /etc/hosts
(On Windows the file is C:\Windows\System32\drivers\etc\hosts.) Now your browser and curl will send the right Host: header to the server's IP. Remove these lines once real DNS is live.
You can also test without touching /etc/hosts by setting the Host header explicitly:
# Force the Host header so the right server block answers
curl -H 'Host: example.com' http://203.0.113.10/
curl -H 'Host: shop.example.com' http://203.0.113.10/
Verify your work¶
# 1. Config is valid (use the right one for your server)
sudo nginx -t # nginx
sudo apachectl configtest # Apache
# 2. Each vhost returns its own content (Host header decides routing)
curl -s -H 'Host: example.com' http://localhost/ # -> <h1>example.com</h1>
curl -s -H 'Host: shop.example.com' http://localhost/ # -> <h1>shop.example.com</h1>
# 3. Inspect the vhost map (Apache)
sudo httpd -S
# 4. An unknown host hits the catch-all, not a real site
curl -s -H 'Host: random.invalid' http://localhost/ -o /dev/null -w '%{http_code}\n'
Summary¶
- Name-based virtual hosting routes requests to sites by the
Host:header — many sites, one IP. - nginx: one
server {}block per domain (one file per site underconf.d/orsites-available), each with its ownserver_name,root, and logs. Set an explicitdefault_server. - Apache: multiple
<VirtualHost *:80>blocks withServerName+DocumentRoot; usea2ensiteon Debian and define a first/catch-all vhost. Inspect withhttpd -S. - Use the convention
/var/www/<site>/public/and label custom roots withhttpd_sys_content_ton RHEL/AlmaLinux. - Test before DNS by editing the client
/etc/hostsor sending an explicitHostheader with curl.
Next: secure every vhost with TLS in HTTPS with Let's Encrypt.