Hosting more than one site on a single server becomes predictable when each site gets its own Apache virtual host, its own DocumentRoot, and its own logs. A clean virtual host setup makes staging vs production separation easy and keeps the “why is site A showing site B’s homepage?” mystery firmly in the genre of historical fiction.
Name-based virtual hosting works by matching the incoming request’s Host header against the ServerName and ServerAlias directives inside each `<VirtualHost>` block. Once a match is found, Apache applies the virtual host’s rules (directory permissions, directory options, logging) and serves content from the configured DocumentRoot.
The steps below use the Ubuntu and Debian layout where site files live in /etc/apache2/sites-available and are enabled via symlinks in /etc/apache2/sites-enabled using a2ensite. On RHEL-style systems, site snippets are typically placed in /etc/httpd/conf.d and the service name is httpd, so paths and service commands differ. A syntax check before reloading prevents a broken config from blocking the reload and leaving changes unapplied.
Related: How to test your Apache configuration
Related: How to enable or disable Apache modules
Example hostname: app.internal.example
Example document root: /var/www/app.internal.example/public
$ sudo mkdir --parents /var/www/app.internal.example/public
$ sudo tee /var/www/app.internal.example/public/index.html >/dev/null <<'EOF'
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>app.internal.example</title>
</head>
<body>
<h1>app.internal.example virtual host works</h1>
</body>
</html>
EOF
$ sudo tee /etc/apache2/sites-available/app.internal.example.conf >/dev/null <<'EOF'
<VirtualHost *:80>
ServerName app.internal.example
ServerAlias www.app.internal.example
DocumentRoot /var/www/app.internal.example/public
<Directory /var/www/app.internal.example/public>
Options -Indexes +FollowSymLinks
AllowOverride None
Require all granted
</Directory>
ErrorLog ${APACHE_LOG_DIR}/app.internal.example-error.log
CustomLog ${APACHE_LOG_DIR}/app.internal.example-access.log combined
</VirtualHost>
EOF
AllowOverride None disables .htaccess processing for performance and clarity; switch to AllowOverride All only when an application requires it.
$ sudo a2ensite app.internal.example.conf Enabling site app.internal.example. To activate the new configuration, you need to run: systemctl reload apache2
/etc/apache2/sites-enabled/000-default.conf remains the fallback vhost for unknown hostnames; disable it with sudo a2dissite 000-default.conf when a catch-all default site is not desired.
$ sudo apache2ctl configtest Syntax OK
An AH00558 warning indicates a missing global ServerName and does not prevent the virtual host from working.
$ sudo systemctl reload apache2
A reload that fails due to syntax errors leaves the previous configuration in place, so changes appear “ignored” until the error is fixed.
$ sudo systemctl status apache2 --no-pager
● apache2.service - The Apache HTTP Server
Loaded: loaded (/usr/lib/systemd/system/apache2.service; enabled; preset: enabled)
Active: active (running) since Sat 2026-01-10 13:43:16 +08; 38s ago
Docs: https://httpd.apache.org/docs/2.4/
Process: 7605 ExecStart=/usr/sbin/apachectl start (code=exited, status=0/SUCCESS)
Process: 8226 ExecReload=/usr/sbin/apachectl graceful (code=exited, status=0/SUCCESS)
Main PID: 7608 (apache2)
Tasks: 55 (limit: 4546)
Memory: 5.0M (peak: 7.1M)
##### snipped #####
$ sudo apache2ctl -S
VirtualHost configuration:
*:80 is a NameVirtualHost
default server host.example.net (/etc/apache2/sites-enabled/000-default.conf:1)
port 80 namevhost host.example.net (/etc/apache2/sites-enabled/000-default.conf:1)
port 80 namevhost app.internal.example (/etc/apache2/sites-enabled/app.internal.example.conf:1)
alias www.app.internal.example
ServerRoot: "/etc/apache2"
Main DocumentRoot: "/var/www/html"
Main ErrorLog: "/var/log/apache2/error.log"
##### snipped #####
$ curl --silent --include --header 'Host: app.internal.example' http://127.0.0.1/ HTTP/1.1 200 OK Date: Sat, 10 Jan 2026 05:43:54 GMT Server: Apache/2.4.58 (Ubuntu) Last-Modified: Sat, 10 Jan 2026 05:43:54 GMT ETag: W/"c7-6480220971290" Accept-Ranges: bytes Content-Length: 199 Vary: Accept-Encoding Content-Type: text/html <!doctype html> ##### snipped #####
Browser testing without DNS usually requires a hosts entry such as 127.0.0.1 app.internal.example in /etc/hosts.
$ sudo tail --lines=3 /var/log/apache2/app.internal.example-access.log 127.0.0.1 - - [10/Jan/2026:13:43:54 +0800] "GET / HTTP/1.1" 200 452 "-" "curl/8.5.0" ##### snipped #####