Apache virtual hosts: run many websites on one server
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
- Step 1 looks only at the address and port the connection came in on. A block for
<VirtualHost 203.0.113.10:80>beats<VirtualHost *:80>for connections to that address, so mixing the two forms hides sites. Use*:80and*:443everywhere unless you really need IP-based hosting. - Step 2 takes the first block, in configuration order, whose
ServerNameorServerAliasmatches. Wildcards such as*.example.comhave no lower priority than exact names. - Once any vhost exists for an address and port, the settings outside all vhosts (the “main server”) no longer answer requests there. They act only as defaults that the vhosts inherit.
NameVirtualHostis not needed since httpd 2.3.11 and has no effect; remove it from old configs.
Before you start
- 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.
- Make sure Apache listens on the port:
Listen 80is in/etc/apache2/ports.confon Debian and inhttpd.confon RHEL. A<VirtualHost *:8080>withoutListen 8080is never reached. - 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-dataorapache:
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
Hostheader matches noServerName/ServerAlias, so the default answers. Look for typos, a missingwwwalias, a site that was never enabled, or a block on a different address or port (203.0.113.10:80versus*:80). - The HTTPS site shows another site’s certificate
- That name has no
*:443block, 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 withRequire 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.comshows 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
- Apache Virtual Host documentation, Apache httpd 2.4
- Name-based Virtual Host Support, Apache httpd 2.4 documentation
- VirtualHost Examples, Apache httpd 2.4 documentation
- An In-Depth Discussion of Virtual Host Matching, Apache httpd 2.4 documentation
- Apache Core Features (VirtualHost, ServerName, ServerAlias, NameVirtualHost), Apache httpd 2.4 documentation
- mod_alias (Redirect), Apache httpd 2.4 documentation
- mod_ssl, Apache httpd 2.4 documentation
- server/vhost.c (apachectl -S output), Apache httpd 2.4.x source
- README.Debian for apache2, Debian Sources
- Debian 000-default.conf, Debian Sources
- a2enmod / a2ensite script, Debian Sources
- Setting up the Apache HTTP web server, RHEL 10 documentation, Red Hat
- apache2 logrotate rules, Debian Sources
- httpd logrotate rules, Fedora Package Sources
- ssl.conf in the Fedora httpd package, Fedora Package Sources
- RFC 2606: Reserved Top Level DNS Names, RFC Editor
- curl man page (--resolve, --header), curl project