One VPS can serve many websites. Apache does this with virtual hosts: separate configuration blocks matched by the domain name in each request (the Host header, or SNI for HTTPS). This guide covers name-based virtual hosts on Ubuntu/Debian, with notes for RHEL-family systems.

If Apache isn’t installed yet, start with installing Apache on Ubuntu.

Point the domains at the server

Create DNS A records for example.com and www.example.com (and for every other site) pointing to your server’s IP. See DNS records explained.

Create a document root per site

sudo mkdir -p /var/www/example.com/public
sudo mkdir -p /var/www/blog.example.net/public
sudo chown -R $USER:www-data /var/www/example.com /var/www/blog.example.net
echo '<h1>example.com works</h1>' > /var/www/example.com/public/index.html

Keep each site in its own directory. A public subfolder lets you keep config and other non-public files beside the web root.

Write the virtual host file

On Debian/Ubuntu, site configs live in /etc/apache2/sites-available/:

# /etc/apache2/sites-available/example.com.conf
<VirtualHost *:80>
    ServerName example.com
    ServerAlias www.example.com
    DocumentRoot /var/www/example.com/public

    <Directory /var/www/example.com/public>
        Options -Indexes +FollowSymLinks
        AllowOverride All
        Require all granted
    </Directory>

    ErrorLog ${APACHE_LOG_DIR}/example.com-error.log
    CustomLog ${APACHE_LOG_DIR}/example.com-access.log combined
</VirtualHost>
  • ServerName is the main domain. ServerAlias lists other names for the same site.
  • AllowOverride All enables .htaccess, which WordPress and Laravel need. If you don’t need .htaccess, None is faster. See .htaccess guide.
  • -Indexes stops Apache from listing directory contents.

Enable it and reload:

sudo a2ensite example.com.conf
sudo apache2ctl configtest
sudo systemctl reload apache2

a2ensite creates a symlink in sites-enabled/. a2dissite removes it.

RHEL, Rocky, AlmaLinux

There is no sites-available. Put the same block in /etc/httpd/conf.d/example.com.conf. Use /var/log/httpd/ for logs. Then run sudo apachectl configtest && sudo systemctl reload httpd. If SELinux is enforcing, give custom web roots the right label: sudo chcon -R -t httpd_sys_content_t /var/www/example.com.

The default site

If a request matches no ServerName or ServerAlias, Apache uses the first virtual host loaded for that IP and port. On Debian that is 000-default.conf. This matters because:

  • Visitors to the bare IP, or to a domain you forgot to configure, see that site.
  • If you disable the default site, an alphabetically early site becomes the catch-all.

A good pattern is to keep a default vhost that serves nothing useful:

<VirtualHost *:80>
    ServerName default.invalid
    DocumentRoot /var/www/default
    <Location />
        Require all denied
    </Location>
</VirtualHost>

Add HTTPS

The easiest route is Certbot. It reads your vhosts, gets certificates, and creates the :443 versions:

sudo apt install certbot python3-certbot-apache
sudo certbot --apache -d example.com -d www.example.com

It writes example.com-le-ssl.conf and can add an HTTP→HTTPS redirect. Details are in Let’s Encrypt for Apache.

Redirect www to non-www (or the reverse)

Inside the *:443 vhost, or in a separate one:

<VirtualHost *:443>
    ServerName www.example.com
    Redirect permanent / https://example.com/
    # SSL directives…
</VirtualHost>

Per-site PHP versions

With PHP-FPM, each vhost can use a different PHP version:

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

Run sudo a2enmod proxy_fcgi setenvif first. To isolate sites better, give each site its own FPM pool running as its own user. See PHP-FPM tuning.

Troubleshooting

Every domain shows the same site. The request isn’t matching your ServerName. Check the spelling, make sure the site is enabled (ls /etc/apache2/sites-enabled/), and run:

sudo apache2ctl -S

This lists every vhost, its file and line, and which one is the default for each port.

403 Forbidden. Either the <Directory> block is missing Require all granted, or Apache (www-data) can’t read the files. Every parent directory needs execute (x) permission. See Linux file permissions.

Changes don’t apply. You edited a file in sites-available that isn’t enabled, or you didn’t reload. Always run configtest before reloading.

HTTPS shows the wrong certificate. The :443 vhost for that name is missing, so Apache served the first SSL vhost. Run Certbot for that domain.

Quick reference

Task Debian/Ubuntu RHEL family
Config location /etc/apache2/sites-available/ /etc/httpd/conf.d/
Enable site a2ensite name (any .conf loads)
Test config apache2ctl configtest apachectl configtest
List vhosts apache2ctl -S httpd -S
Reload systemctl reload apache2 systemctl reload httpd

Prefer Nginx? The equivalent is covered in Nginx server blocks.

  • Apache
  • Web Server
  • Hosting