Apache virtual hosts let a single server answer for many domains, each with its own document root, logs and settings. Apache reads the Host header of every request and picks the virtual host whose ServerName or ServerAlias matches. In this tutorial you will configure two sites on Ubuntu 24.04, add a catch-all default, redirect www to the bare domain, run PHP through PHP-FPM for one site and secure both with Let's Encrypt.

Prerequisites

To follow this guide you need:

  • A server running Ubuntu 24.04 LTS, for example a CubePath VPS, with a non-root sudo user.
  • Apache installed and allowed through UFW (sudo apt install apache2 and sudo ufw allow 'Apache Full').
  • Two domain names, each with A records for the bare name and www pointing to your server's IP address. This guide uses your_domain and second_domain as placeholders.

Step 1 - Creating the document roots

Give each site its own directory under /var/www. Keeping a separate html subdirectory leaves room for logs, backups or application code that should not be public:

sudo mkdir -p /var/www/your_domain/html /var/www/second_domain/html

Make your user the owner so you can deploy files without sudo. Apache runs as www-data and only needs read access, which the default permissions already grant:

sudo chown -R "$USER":"$USER" /var/www/your_domain/html /var/www/second_domain/html

Create a distinct test page for each site so you can tell them apart:

echo '<h1>your_domain works</h1>' > /var/www/your_domain/html/index.html
echo '<h1>second_domain works</h1>' > /var/www/second_domain/html/index.html

Verify the ownership and files:

ls -l /var/www/your_domain/html /var/www/second_domain/html
/var/www/second_domain/html:
total 4
-rw-rw-r-- 1 your_user your_user 29 Sep 24 10:40 index.html

/var/www/your_domain/html:
total 4
-rw-rw-r-- 1 your_user your_user 27 Sep 24 10:40 index.html

Step 2 - Creating the first virtual host

Create a configuration file for your_domain in sites-available. Naming the file after the domain keeps things easy to find:

sudo nano /etc/apache2/sites-available/your_domain.conf
<VirtualHost *:80>
    ServerName your_domain
    ServerAlias www.your_domain
    ServerAdmin webmaster@your_domain
    DocumentRoot /var/www/your_domain/html

    <Directory /var/www/your_domain/html>
        Options -Indexes +FollowSymLinks
        AllowOverride All
        Require all granted
    </Directory>

    ErrorLog ${APACHE_LOG_DIR}/your_domain_error.log
    CustomLog ${APACHE_LOG_DIR}/your_domain_access.log combined
</VirtualHost>

What each part does:

  • <VirtualHost *:80> accepts requests on port 80 on every IP address. Apache then matches the Host header against ServerName and ServerAlias.
  • ServerAlias adds more names for the same site. You can list several, separated by spaces, and use wildcards such as *.your_domain.
  • Options -Indexes stops Apache from listing directory contents.
  • AllowOverride All allows .htaccess files. Use None if the site does not need them.
  • Separate log files per site make troubleshooting much easier.

Step 3 - Creating the second virtual host

Copy the first file and replace the domain name:

sudo sed 's/your_domain/second_domain/g' /etc/apache2/sites-available/your_domain.conf | sudo tee /etc/apache2/sites-available/second_domain.conf > /dev/null

Check the result:

grep -E 'ServerName|ServerAlias|DocumentRoot' /etc/apache2/sites-available/second_domain.conf
    ServerName second_domain
    ServerAlias www.second_domain
    DocumentRoot /var/www/second_domain/html

Step 4 - Enabling the sites

Enable both virtual hosts with a2ensite, which creates symbolic links in /etc/apache2/sites-enabled/:

sudo a2ensite your_domain.conf second_domain.conf

Keep the Ubuntu default site, 000-default.conf, enabled for now. Apache uses the first virtual host loaded for a given port as the default, and files load in alphabetical order, so 000-default catches requests for unknown host names or the bare IP address instead of exposing one of your real sites. You will replace its content in Step 5.

Test the configuration and reload:

sudo apache2ctl configtest
sudo systemctl reload apache2
Syntax OK

List how Apache mapped the names to virtual hosts:

sudo apache2ctl -S
VirtualHost configuration:
*:80                   is a NameVirtualHost
         default server localhost (/etc/apache2/sites-enabled/000-default.conf:1)
         port 80 namevhost localhost (/etc/apache2/sites-enabled/000-default.conf:1)
         port 80 namevhost second_domain (/etc/apache2/sites-enabled/second_domain.conf:1)
                 alias www.second_domain
         port 80 namevhost your_domain (/etc/apache2/sites-enabled/your_domain.conf:1)
                 alias www.your_domain

Test each site locally by sending the right Host header:

curl -H "Host: your_domain" http://localhost
curl -H "Host: second_domain" http://localhost
<h1>your_domain works</h1>
<h1>second_domain works</h1>

If DNS is not ready yet and you want to test from your workstation's browser, add both names to the local /etc/hosts file on your computer (not the server), pointing to your_server_ip.

Step 5 - Locking down the default virtual host

Requests to the raw IP address or an unknown domain currently land on Ubuntu's welcome page. Replace it with a virtual host that simply refuses them:

sudo nano /etc/apache2/sites-available/000-default.conf
<VirtualHost *:80>
    ServerName default.invalid
    DocumentRoot /var/www/html

    <Location />
        Require all denied
    </Location>

    ErrorLog ${APACHE_LOG_DIR}/error.log
    CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>

default.invalid uses the reserved .invalid top-level domain, so it never matches a real request. Test and reload, then check that the IP address is refused while your domains still work:

sudo apache2ctl configtest
sudo systemctl reload apache2
curl -s -o /dev/null -w '%{http_code}\n' http://localhost
curl -s -o /dev/null -w '%{http_code}\n' -H "Host: your_domain" http://localhost
403
200

Step 6 - Redirecting www to the bare domain

Serving the same content on two names splits search ranking and cookies. A clean approach is a small dedicated virtual host for the www name. Edit the first site:

sudo nano /etc/apache2/sites-available/your_domain.conf

Remove the ServerAlias www.your_domain line and add this block at the top of the file, above the existing <VirtualHost>:

<VirtualHost *:80>
    ServerName www.your_domain
    Redirect permanent / http://your_domain/
</VirtualHost>

Redirect is provided by mod_alias, which is enabled by default, so no rewrite rules are needed. Repeat for second_domain if you want the same behavior, then test:

sudo apache2ctl configtest
sudo systemctl reload apache2
curl -I -H "Host: www.your_domain" http://localhost
HTTP/1.1 301 Moved Permanently
Location: http://your_domain/

Certbot, used in Step 8, copies this block into the HTTPS configuration. After running it, change the target to https://your_domain/ in both your_domain.conf and your_domain-le-ssl.conf so visitors are redirected in a single hop.

Step 7 - Running PHP for one site with PHP-FPM

Virtual hosts can use different handlers. Here second_domain will run PHP through PHP-FPM, while your_domain stays static. Install PHP-FPM and the Apache proxy modules:

sudo apt install php8.3-fpm
sudo a2enmod proxy_fcgi setenvif

Instead of enabling PHP globally with a2enconf php8.3-fpm, add the handler only to the second site:

sudo nano /etc/apache2/sites-available/second_domain.conf

Inside the <VirtualHost *:80> block for second_domain, below the </Directory> line, add:

    <FilesMatch "\.php$">
        SetHandler "proxy:unix:/run/php/php8.3-fpm.sock|fcgi://localhost"
    </FilesMatch>
    DirectoryIndex index.php index.html

Restart Apache so the new modules load, and create a test file:

sudo apache2ctl configtest
sudo systemctl restart apache2
echo '<?php echo "PHP " . PHP_VERSION . " on second_domain\n";' > /var/www/second_domain/html/info.php

Verify that PHP runs on the second site and not on the first:

curl -H "Host: second_domain" http://localhost/info.php
PHP 8.3.6 on second_domain

Remove the test file afterwards:

rm /var/www/second_domain/html/info.php

Step 8 - Enabling HTTPS for every virtual host

Install Certbot with the Apache plugin:

sudo apt install certbot python3-certbot-apache

Request one certificate per site. Each command covers both names of that site:

sudo certbot --apache -d your_domain -d www.your_domain
sudo certbot --apache -d second_domain -d www.second_domain

For each site, Certbot creates a -le-ssl.conf virtual host on port 443 with the certificate paths, enables mod_ssl and adds a redirect from HTTP to HTTPS. Confirm that Apache now has virtual hosts on both ports:

sudo apache2ctl -S | grep -E '^\*:(80|443)'
*:443                  is a NameVirtualHost
*:80                   is a NameVirtualHost

Check that automatic renewal works:

sudo certbot renew --dry-run

Finally, load https://your_domain and https://second_domain in a browser.

Managing sites later

A few commands cover day-to-day work with virtual hosts:

TaskCommand
Enable a sitesudo a2ensite site.conf
Disable a sitesudo a2dissite site.conf
List active virtual hostssudo apache2ctl -S
Test configurationsudo apache2ctl configtest
Apply changessudo systemctl reload apache2

Troubleshooting

Every domain shows the same site. The requested name matches no ServerName or ServerAlias, so Apache falls back to the default virtual host. Check the spelling in apache2ctl -S and make sure the site is enabled.

AH00548: NameVirtualHost has no effect. This directive is obsolete since Apache 2.4. Remove any NameVirtualHost lines.

403 Forbidden on a real site. Check that the site's <Directory> block has Require all granted, that an index file exists, and that www-data can traverse the path: namei -l /var/www/your_domain/html/index.html.

PHP files download instead of running. The SetHandler block is missing from that virtual host, proxy_fcgi is not enabled, or php8.3-fpm is not running (systemctl status php8.3-fpm).

Certbot fails with a connection or 404 error. DNS for the name does not point to this server yet, or port 80 is blocked. Check with dig +short your_domain and sudo ufw status.

Conclusion

Your server now hosts two independent sites through Apache name-based virtual hosts, with a default host that refuses unknown names, a www redirect, per-site logs, PHP-FPM enabled only where it is needed and HTTPS on every domain. From here you can add more sites by repeating Steps 1 to 4, tune Apache as described in the Apache installation and configuration guide, or compare approaches with the equivalent setup using Nginx server blocks.