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.

Steps to create a virtual host in Apache:

  1. Select the hostname for the new site.

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

  2. Create the document root directory.
    $ sudo mkdir --parents /var/www/app.internal.example/public
  3. 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
  4. 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>
    EOF

    AllowOverride None disables .htaccess processing for performance and clarity; switch to AllowOverride All only when an application requires it.

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

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

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

  8. 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 #####
  9. 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 #####
  10. 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.

  11. 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 #####