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.

Steps to create a virtual host in Apache:

  1. Pick the site hostname and web root path.

    Example hostname: app.internal.example
    Example document root: /var/www/app.internal.example/public

  2. Create the document root directory with readable permissions.
    $ sudo install --directory --mode=0755 /var/www/app.internal.example/public
  3. Write a simple page into the new document root so the final request test has a distinctive response.
    $ 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
  4. Create the site configuration file 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>
    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.

  5. Enable the new site.
    $ 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.

  6. Test the configuration before reloading Apache.
    $ 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.

  7. Reload the service so the enabled site becomes active.
    $ 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.

  8. List the loaded virtual hosts to confirm the new hostname is mapped to the expected file.
    $ 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.

  9. Send a local request with the target Host header so Apache selects the new virtual host without waiting for public DNS.
    $ 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.

  10. Inspect the site-specific access log if the wrong page, a 403 Forbidden response, or an unexpected redirect still appears.
    $ 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.