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
sudouser. - Apache installed and allowed through UFW (
sudo apt install apache2andsudo ufw allow 'Apache Full'). - Two domain names, each with
Arecords for the bare name andwwwpointing to your server's IP address. This guide usesyour_domainandsecond_domainas 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 theHostheader againstServerNameandServerAlias.ServerAliasadds more names for the same site. You can list several, separated by spaces, and use wildcards such as*.your_domain.Options -Indexesstops Apache from listing directory contents.AllowOverride Allallows.htaccessfiles. UseNoneif 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:
| Task | Command |
|---|---|
| Enable a site | sudo a2ensite site.conf |
| Disable a site | sudo a2dissite site.conf |
| List active virtual hosts | sudo apache2ctl -S |
| Test configuration | sudo apache2ctl configtest |
| Apply changes | sudo 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.
