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
Steps to create a virtual host in Apache:
- Select the hostname for the new site.
Example hostname: app.internal.example
Example document root: /var/www/app.internal.example/public - Create the document root directory.
$ sudo mkdir --parents /var/www/app.internal.example/public
- Create a simple test page in the document root.
$ 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 - Create the virtual host configuration in /etc/apache2/sites-available.
$ 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> EOFAllowOverride None disables .htaccess processing for performance and clarity; switch to AllowOverride All only when an application requires it.
- Enable the new site configuration.
$ 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.
- Validate the Apache configuration syntax.
$ sudo apache2ctl configtest Syntax OK
An AH00558 warning indicates a missing global ServerName and does not prevent the virtual host from working.
- Reload apache2 to apply the enabled site.
$ 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.
- Confirm the apache2 service is active.
$ 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 ##### - List loaded virtual hosts to confirm the hostname routes to the new config.
$ 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 ##### - Fetch the site locally by forcing the Host header.
$ 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.
- Check the site logs when routing or permissions do not behave as expected.
$ 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 #####
Mohd Shakir Zakaria is a cloud architect with deep roots in software development and open-source advocacy. Certified in AWS, Red Hat, VMware, ITIL, and Linux, he specializes in designing and managing robust cloud and on-premises infrastructures.
