Apache virtual hosts: run many websites on one server

Updated November 2026

A virtual host is one website inside one Apache server. With name-based virtual hosts, any number of domains can share a single IP address: the browser sends the domain name in the Host header, and Apache picks the <VirtualHost> block whose ServerName or ServerAlias matches. If nothing matches, the first block for that address and port answers. Most “wrong site” problems come from that last rule.

How Apache picks a site

Request arrivesIP, port, Host header 1. Best IP:portexact IP beats * 2. Host = ServerNameor ServerAlias, in order Match:that site No match: first vhost listedfor that IP:port (the default)
Name matching only happens among the vhosts that share the best-matching address and port.

Before you start

  1. Point the domain’s DNS (an A record and, if you have IPv6, an AAAA record) at the server. You can test before DNS is ready; see “Test before DNS” below.
  2. Make sure Apache listens on the port: Listen 80 is in /etc/apache2/ports.conf on Debian and in httpd.conf on RHEL. A <VirtualHost *:8080> without Listen 8080 is never reached.
  3. Create the web folder and a test page. The folder should be readable by Apache but owned by you or a deploy user, not by www-data or apache:
sudo mkdir -p /var/www/example.com/public
echo 'example.com works' | sudo tee /var/www/example.com/public/index.html

Debian and Ubuntu

Create /etc/apache2/sites-available/example.com.conf:

<VirtualHost *:80>
    ServerName example.com
    ServerAlias www.example.com
    ServerAdmin webmaster@example.com
    DocumentRoot /var/www/example.com/public

    <Directory /var/www/example.com/public>
        Options FollowSymLinks
        AllowOverride None
        Require all granted
    </Directory>

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

Enable it, test and reload:

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

a2ensite only creates the link in sites-enabled/; it prints a reminder to reload. The <Directory> block is optional here, because Debian’s apache2.conf already grants access to everything under /var/www/, but it is required if the site lives elsewhere (for example /srv). With it, the file still works if you move it. ${APACHE_LOG_DIR} is /var/log/apache2.

Debian ships 000-default.conf, a vhost without ServerName that serves /var/www/html. Its name sorts first, so it is the default for *:80 and catches every request for a name you have not configured. Keep it, replace it with the catch-all shown below, or disable it with sudo a2dissite 000-default, but then your alphabetically first site becomes the default.

RHEL, AlmaLinux, Rocky and Fedora

Create /etc/httpd/conf.d/example.com.conf with the same block, changing only the log paths:

<VirtualHost *:80>
    ServerName example.com
    ServerAlias www.example.com
    DocumentRoot /var/www/example.com/public

    <Directory /var/www/example.com/public>
        Options FollowSymLinks
        AllowOverride None
        Require all granted
    </Directory>

    ErrorLog /var/log/httpd/example.com-error.log
    CustomLog /var/log/httpd/example.com-access.log combined
</VirtualHost>
sudo apachectl configtest
sudo systemctl reload httpd

Every .conf file in conf.d/ is active as soon as it exists, and the files are read in alphabetical order, so the first vhost file alphabetically becomes the default. Name your catch-all 00-default.conf to control this. If the site lives outside /var/www, give the files the SELinux type httpd_sys_content_t (semanage fcontext and restorecon, as shown in the install guide).

A catch-all default site

Requests that reach your IP address with an unknown or missing host name come from scanners, old DNS records and people typing the bare IP. Rather than showing them your first real site, give them a vhost of their own and make sure it is listed first (000-default.conf on Debian, 00-default.conf on RHEL):

<VirtualHost *:80>
    # Listed first, so it answers every request whose Host matches no other vhost
    ServerName default.invalid
    DocumentRoot /var/www/html
    <Location "/">
        Require all denied
    </Location>
    ErrorLog ${APACHE_LOG_DIR}/default-error.log
    CustomLog ${APACHE_LOG_DIR}/default-access.log combined
</VirtualHost>

Unknown names now get 403 Forbidden. .invalid is a top-level name reserved by RFC 2606 for names that are sure to be invalid, so it cannot clash with a real site. On RHEL, use /var/log/httpd/ instead of ${APACHE_LOG_DIR}. Set an explicit ServerName in every vhost: without one, the vhost inherits the global name or one guessed from the system host name, and matching becomes unpredictable.

Aliases, www and redirects

ServerAlias takes several names and wildcards: ServerAlias www.example.com *.example.org. Answering on both example.com and www.example.com works, but search engines prefer one address. Pick one and redirect the other in its own small vhost:

<VirtualHost *:80>
    ServerName www.example.com
    Redirect permanent "/" "http://example.com/"
</VirtualHost>

Remove www.example.com from the main site’s ServerAlias when you do this. Once HTTPS works, the port-80 vhost usually does nothing but redirect to https://; Redirects has the patterns.

HTTPS virtual hosts

Name-based hosting works on port 443 too, because browsers send the host name in the TLS handshake (SNI). Each site gets a second block, which needs mod_ssl (sudo a2enmod ssl on Debian, sudo dnf install mod_ssl on RHEL):

<VirtualHost *:443>
    ServerName example.com
    ServerAlias www.example.com
    DocumentRoot /var/www/example.com/public

    SSLEngine on
    SSLCertificateFile    /etc/ssl/example.com/fullchain.pem
    SSLCertificateKeyFile /etc/ssl/example.com/privkey.pem

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

The certificate paths depend on how you get the certificate. Since httpd 2.4.8, SSLCertificateFile can hold the whole chain, and SSLCertificateChainFile is obsolete. On RHEL, the mod_ssl package also adds /etc/httpd/conf.d/ssl.conf, which contains Listen 443 https and a <VirtualHost _default_:443> block with a test certificate. That block counts in the order like any other 443 vhost. Getting certificates, Let’s Encrypt and TLS settings are covered in HTTPS and TLS.

Per-site logs

Separate ErrorLog and CustomLog files per site make problems much easier to trace. On Debian, a vhost without its own CustomLog writes to /var/log/apache2/other_vhosts_access.log in the vhost_combined format, which starts each line with the vhost name and port. If you host many sites, one shared log in that format (%v is the vhost name) keeps the number of open files down. Either way, add new log files to log rotation; the packaged logrotate rules already cover /var/log/apache2/*.log on Debian and /var/log/httpd/*log on RHEL, so keep site logs in those folders with those endings. See Logging.

Test before DNS

You can test a vhost before the domain points at the server. Tell curl which address to use:

curl -i --resolve example.com:80:127.0.0.1 http://example.com/
curl -i -H "Host: example.com" http://127.0.0.1/

To use a browser, add a line such as 203.0.113.10 example.com www.example.com to your own computer’s hosts file (/etc/hosts, or C:\Windows\System32\drivers\etc\hosts) and remove it afterwards.

Read apachectl -S

sudo apachectl -S (apache2ctl -S on Debian) shows how Apache understood your vhosts:

VirtualHost configuration:
*:80                   is a NameVirtualHost
         default server default.invalid (/etc/apache2/sites-enabled/000-default.conf:1)
         port 80 namevhost default.invalid (/etc/apache2/sites-enabled/000-default.conf:1)
         port 80 namevhost example.com (/etc/apache2/sites-enabled/example.com.conf:1)
                 alias www.example.com
ServerRoot: "/etc/apache2"
Main DocumentRoot: "/var/www/html"
Main ErrorLog: "/var/log/apache2/error.log"

Check three things: the default server line is the site you expect; every domain appears as a namevhost or alias under the right address and port; and the file and line numbers point to the files you think you are editing. A site that is missing here is not enabled or not included.

Troubleshooting

Every domain shows the same site
The Host header matches no ServerName/ServerAlias, so the default answers. Look for typos, a missing www alias, a site that was never enabled, or a block on a different address or port (203.0.113.10:80 versus *:80).
The HTTPS site shows another site’s certificate
That name has no *:443 block, so the first 443 vhost answers with its own certificate.
403 Forbidden on a new site
The folder lies outside the granted paths (add the <Directory> block with Require all granted), Apache cannot read or enter one of the parent folders, or, on RHEL, the SELinux label is wrong. The error log names the cause.
The site works on the server but not from outside
DNS still points elsewhere, or a firewall blocks the port. dig +short example.com shows the address in use.
Duplicate names
If two vhosts on the same address and port use the same name, the first one wins and the second never answers.

More error messages are explained in Troubleshooting errors.

Sources