Creating a separate Apache virtual host keeps each site on its own hostname, DocumentRoot, and log files, which makes multi-site hosting predictable and makes it much easier to see which configuration is serving a request.
On a shared listener such as `<VirtualHost *:80>`, Apache first chooses the best IP:port match, then searches the enabled virtual hosts in order for the first matching ServerName or ServerAlias from the request's Host header. If no name matches, the first enabled virtual host for that address and port becomes the fallback, which is why 000-default.conf often catches unknown hostnames until a different default is arranged.
This guide uses the packaged apache2 layout on Ubuntu and Debian, where site files live in /etc/apache2/sites-available and a2ensite creates the symlink in /etc/apache2/sites-enabled. It assumes Apache is already installed and listening on port 80; on RHEL-style systems the file path is usually /etc/httpd/conf.d and the service name is httpd, while the same ServerName, ServerAlias, DocumentRoot, and `<Directory>` concepts still apply. Run a syntax test before reloading, and keep ServerName explicit so startup warnings and fallback matches stay predictable.
Related: How to test Apache configuration
Related: How to enable or disable Apache modules
Related: Location for Apache VirtualHost configuration
Example hostname: app.internal.example
Example document root: /var/www/app.internal.example/public
$ sudo install --directory --mode=0755 /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
Keep the actual hostname in ServerName and ServerAlias, not in the `<VirtualHost>` address. The wildcard listener stays *:80, and Apache uses the request hostname to select the matching virtual host.
AllowOverride None keeps configuration in the virtual host file and stops Apache from reading .htaccess files in that path. Change it only when the application really requires .htaccess overrides.
$ sudo a2ensite app.internal.example.conf Enabling site app.internal.example. To activate the new configuration, you need to run: service apache2 reload
On current Debian-based packages, a2ensite works by creating symlinks under /etc/apache2/sites-enabled.
000-default.conf remains the fallback virtual host for unknown names until you disable it or replace it with another first-loaded catch-all site.
$ sudo apache2ctl configtest AH00558: apache2: Could not reliably determine the server's fully qualified domain name, using 192.0.2.10. Set the 'ServerName' directive globally to suppress this message Syntax OK
The AH00558 line is a global ServerName warning, not a virtual-host syntax failure. Syntax OK means the new site file parsed successfully.
$ sudo systemctl reload apache2
The helper text from a2ensite still prints service apache2 reload on current Debian and Ubuntu packages, but systemctl reload apache2 is the normal equivalent on systemd hosts.
$ sudo apache2ctl -S
VirtualHost configuration:
*:80 is a NameVirtualHost
default server 192.0.2.10 (/etc/apache2/sites-enabled/000-default.conf:1)
port 80 namevhost 192.0.2.10 (/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"
##### snipped #####
If the new hostname is missing here, the site file is still disabled or the syntax test is still failing.
$ curl --silent --include --header 'Host: app.internal.example' http://127.0.0.1/ HTTP/1.1 200 OK Date: Wed, 08 Apr 2026 04:37:11 GMT Server: Apache/2.4.58 (Ubuntu) Last-Modified: Wed, 08 Apr 2026 04:37:09 GMT ETag: "c7-64eeb73f0d3ea" Accept-Ranges: bytes Content-Length: 199 Vary: Accept-Encoding Content-Type: text/html <!doctype html> <html lang="en"> ##### snipped #####
If DNS already points at the server, request the real URL directly instead. For browser testing before DNS is live, add a temporary hosts entry such as 127.0.0.1 app.internal.example on the test machine.
Related: Override Host header in cURL
$ sudo tail --lines=3 /var/log/apache2/app.internal.example-access.log 127.0.0.1 - - [08/Apr/2026:04:37:11 +0000] "GET / HTTP/1.1" 200 450 "-" "curl/8.5.0" ##### snipped #####
A hit in the access log confirms the request reached this virtual host. A missing hit usually means the request matched a different hostname, a different listener, or a different proxy layer first.