A companion lab for the module on configuring and troubleshooting virtual hosts.
Everything below is a real HTTP response from one Ubuntu server: three Apache vhosts and
five Nginx vhosts, distinguished only by the Host header you send.
DocumentRoot /var/www/site-a — also hosts a deliberate 403.
Same Apache process, DocumentRoot /var/www/site-b.
root /srv/site-a — also hosts a deliberate 403.
A third server block on the very same Nginx container.
Both servers ship a stock welcome page, and both silently fall back to a default server when no hostname matches. Seeing that page instead of your site is a diagnostic, not a failure: the server is fine, your vhost simply was not the one that answered.
Ubuntu's stock "Apache2 Ubuntu Default Page" from /var/www/html — the first
<VirtualHost>, so it also catches unmatched hostnames.
"Welcome to nginx!" from the packaged page, declared
listen 80 default_server.
Both stock pages rendered live, where each file lives, and how the default-server fallback is chosen in each server.
Compare defaults →Two hostnames here have no vhost of their own — typo-apache… and typo-nginx… — and each returns 200 with the stock page rather than a 404. That is the default-server fallback in action.
Two containers serve nine hostnames. Nothing about the URL path decides which site answers —
only ServerName / server_name matching against the request's Host header.
| Hostname | Server | Document root |
|---|---|---|
apache-default… | Apache 2.4 | /var/www/html (default vhost) |
apache-a… | Apache 2.4 | /var/www/site-a |
apache-b… | Apache 2.4 | /var/www/site-b |
nginx-default… | Nginx 1.27 | /srv/default (default_server) |
nginx-a… | Nginx 1.27 | /srv/site-a |
nginx-b… | Nginx 1.27 | /srv/site-b |
defaults… | Nginx 1.27 | /srv/defaults |
lab… | Nginx 1.27 | /srv/lab |
<VirtualHost *:80>
ServerName apache-a.srv1957161.hstgr.cloud
DocumentRoot /var/www/site-a
<Directory /var/www/site-a>
Options -Indexes +FollowSymLinks
AllowOverride None
Require all granted
</Directory>
<Directory /var/www/site-a/forbidden>
Require all denied
</Directory>
</VirtualHost>
server {
listen 80;
server_name nginx-a.srv1957161.hstgr.cloud;
root /srv/site-a;
index index.html;
autoindex off;
location /forbidden/ {
deny all;
}
}
| Purpose | Apache | Nginx |
|---|---|---|
| Match the domain | ServerName, ServerAlias | server_name |
| Where files live | DocumentRoot | root |
| Default file | DirectoryIndex | index |
| Per-path rules | <Directory>, <Location> | location |
| Access control | Require all granted/denied | allow / deny |
| Enable a site | a2ensite → sites-enabled/ | symlink into sites-enabled/ or conf.d/ |
| Check syntax | apachectl configtest | nginx -t |
| Apply changes | apachectl graceful | nginx -s reload |
The clearest way to see vhost selection: keep the IP fixed, change only the Host header.
Same socket, same process, different site.
# Apache: one container, two documents roots
curl -s https://apache-a.srv1957161.hstgr.cloud/ | grep -o '<title>.*</title>'
curl -s https://apache-b.srv1957161.hstgr.cloud/ | grep -o '<title>.*</title>'
# Nginx marks which server block answered, in a response header
curl -sI https://nginx-a.srv1957161.hstgr.cloud/ | grep -i x-lab-vhost
curl -sI https://nginx-b.srv1957161.hstgr.cloud/ | grep -i x-lab-vhost
# 403 Forbidden -- the file exists on disk, the config denies it
curl -s -o /dev/null -w '%{http_code}\n' https://apache-a.srv1957161.hstgr.cloud/forbidden/
curl -s -o /dev/null -w '%{http_code}\n' https://nginx-a.srv1957161.hstgr.cloud/forbidden/
# 404 Not Found -- vhost matched, path did not
curl -s -o /dev/null -w '%{http_code}\n' https://nginx-a.srv1957161.hstgr.cloud/no-such-page.html
# An unmatched Host never reaches these vhosts at all
curl -s -o /dev/null -w '%{http_code}\n' https://nowhere.srv1957161.hstgr.cloud/
apachectl -S lists every vhost and the file:line it came from. nginx -T dumps the full merged config.ServerAlias / server_name entry means a different vhost answers — usually the first one, which looks like "wrong site" or a 404.DocumentRoot / root is the most common 404. Check the path exists and is spelled as configured.x) to be traversed; a missing index.html with directory listing off is a 403, not a 404.apachectl configtest / nginx -t before every reload — a syntax error on restart takes the whole server down, not just one site.Both web servers run as containers on a single 1-vCPU Ubuntu 24.04 host. Port 80/443 belong to a
pre-existing Traefik reverse proxy that terminates TLS (Let's Encrypt) and forwards each request with its
original Host header intact — which is exactly why the vhost matching inside Apache and Nginx
is real rather than staged. In production the same pattern applies whether the front door is Traefik, a
cloud load balancer, or nothing at all.