Live demo · srv1957161.hstgr.cloud

Web Server Configuration:
Apache & Nginx Virtual Hosts

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.

The virtual hosts serving real sites

Apache · site A

apache-a.srv1957161.hstgr.cloud

DocumentRoot /var/www/site-a — also hosts a deliberate 403.

Open site A →

Apache · site B

apache-b.srv1957161.hstgr.cloud

Same Apache process, DocumentRoot /var/www/site-b.

Open site B →

Nginx · site A

nginx-a.srv1957161.hstgr.cloud

root /srv/site-a — also hosts a deliberate 403.

Open site A →

Nginx · site B

nginx-b.srv1957161.hstgr.cloud

Same Nginx process, root /srv/site-b.

Open site B →

Nginx · this page

lab.srv1957161.hstgr.cloud

A third server block on the very same Nginx container.

You are here

Default pages — before any site is configured

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.

Apache default

apache-default.srv1957161.hstgr.cloud

Ubuntu's stock "Apache2 Ubuntu Default Page" from /var/www/html — the first <VirtualHost>, so it also catches unmatched hostnames.

Open →

Nginx default

nginx-default.srv1957161.hstgr.cloud

"Welcome to nginx!" from the packaged page, declared listen 80 default_server.

Open →

Side-by-side comparison

defaults.srv1957161.hstgr.cloud

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.

One server, many document roots

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.

HostnameServerDocument 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

Same job, different syntax

Apache — httpd-vhosts.conf

<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>

Nginx — conf.d/site-a.conf

server {
    listen 80;
    server_name nginx-a.srv1957161.hstgr.cloud;

    root  /srv/site-a;
    index index.html;
    autoindex off;

    location /forbidden/ {
        deny all;
    }
}
PurposeApacheNginx
Match the domainServerName, ServerAliasserver_name
Where files liveDocumentRootroot
Default fileDirectoryIndexindex
Per-path rules<Directory>, <Location>location
Access controlRequire all granted/deniedallow / deny
Enable a sitea2ensitesites-enabled/symlink into sites-enabled/ or conf.d/
Check syntaxapachectl configtestnginx -t
Apply changesapachectl gracefulnginx -s reload

Prove it from the command line

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/

Troubleshooting order that actually works

How this lab is wired

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.