Apache VirtualHosts are one of those features that feel simple until you inherit a server with twelve site files, three half-finished redirects, one missing ServerName, and an SSL vhost that is somehow answering for the wrong domain. I have spent enough time on those systems that I no longer trust “it should match the right site” as an explanation.
This guide is my practical approach to Apache VirtualHost configuration: how Apache decides which vhost answers, what I still consider essential structure, where old documentation is outdated, and how I validate the result before and after reloads. If you are working on HTTPS at the same time, pair this with my modern Apache SSL settings guide and my Apache SSL installation walkthrough. If redirects are part of the job, my mod_rewrite and HTTPS enforcement guide.
Name-based versus IP-based VirtualHosts
Most Apache deployments today use name-based virtual hosting. Multiple domains share the same IP address, and Apache chooses the vhost based on the Host header.
Example:
<VirtualHost *:80>
ServerName example.com
DocumentRoot /var/www/example.com/public
</VirtualHost>
<VirtualHost *:80>
ServerName example.org
DocumentRoot /var/www/example.org/public
</VirtualHost>
IP-based virtual hosting assigns different IP addresses to different sites. That still exists, but I only use it when I have a strong network or policy reason.
<VirtualHost 192.0.2.10:80>
ServerName example.com
DocumentRoot /var/www/example.com/public
</VirtualHost>
For most current web stacks, name-based vhosts are simpler and entirely normal.
The NameVirtualHost directive: old advice versus current Apache
If you have been reading very old tutorials, you have seen NameVirtualHost *:80 or NameVirtualHost *:443.
That was relevant in Apache 2.2. In Apache 2.4, it is obsolete and should not be added to new configurations. Apache 2.4 handles name-based virtual hosting by default when multiple vhosts share an address and port.
So my advice is straightforward:
- If you are on Apache 2.4, do not add
NameVirtualHostto new configs. - If you inherit an old config with it, confirm whether it is harmless legacy clutter or part of a broader outdated layout.
You can check the Apache version quickly:
apache2 -v
or on some systems:
httpd -v
A clean HTTP VirtualHost
For a standard site on port 80, I keep the structure obvious.
<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 ${APACHE_LOG_DIR}/example.com-error.log
CustomLog ${APACHE_LOG_DIR}/example.com-access.log combined
</VirtualHost>
This covers the basics:
ServerNamedefines the primary hostname.ServerAliasadds alternate names.DocumentRootpoints to site files.<Directory>sets permissions and override behavior.ErrorLogandCustomLogkeep logging per site.
I do not like piling unrelated global behavior into individual vhost files. Keep the vhost about the site, not the whole server personality.
A practical HTTPS VirtualHost
For TLS, I mirror the host structure on port 443.
<VirtualHost *:443>
ServerName example.com
ServerAlias www.example.com
DocumentRoot /var/www/example.com/public
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem
<Directory /var/www/example.com/public>
Options FollowSymLinks
AllowOverride None
Require all granted
</Directory>
ErrorLog ${APACHE_LOG_DIR}/example.com-ssl-error.log
CustomLog ${APACHE_LOG_DIR}/example.com-ssl-access.log combined
</VirtualHost>
Current Apache typically loads SSL via mod_ssl, and certificate file paths depend on your issuance method. For Let’s Encrypt, I usually see the live/ paths above.
Before enabling this, make sure the SSL module is present:
sudo a2enmod ssl
sudo apache2ctl -M | grep ssl
How Apache chooses the default VirtualHost
This is where confusion starts. If Apache does not find a matching ServerName or ServerAlias, it serves the first vhost loaded for that address and port as the default.
That means ordering matters more than many people expect.
On Debian and Ubuntu, site files in sites-enabled/ are processed in lexical order, which is why people often use prefixes like 000-default.conf. On RHEL-like systems the include order depends on the main config and include paths, but the principle is the same: the first applicable vhost wins as default.
I always verify which one Apache thinks is default instead of guessing.
sudo apache2ctl -S
That command is worth memorizing. It prints name-based vhost mappings, ports, and defaults. When Apache is answering with the wrong site, -S usually shows why.
ServerName versus ServerAlias
ServerName is the canonical hostname for the vhost. ServerAlias lists additional hostnames that should match the same site.
ServerName example.com
ServerAlias www.example.com static.example.com
I keep ServerName explicit in every vhost. Missing it is one of the oldest Apache mistakes and it still causes weird matching behavior. Apache may warn at startup, and the wrong vhost may answer requests in ways that are annoying to diagnose.
I also try not to overuse wildcards in ServerAlias unless I really mean it. A loose alias can steal traffic from another planned site.
DocumentRoot and the <Directory> block
A vhost without a matching <Directory> block often works until it does not. Apache access control and override behavior are applied by directory path, not by vhost alone.
Typical pattern:
DocumentRoot /var/www/example.com/public
<Directory /var/www/example.com/public>
Options FollowSymLinks
AllowOverride None
Require all granted
</Directory>
I like to keep AllowOverride None unless .htaccess is truly needed. That is faster and more predictable. If you do need per-directory overrides, be selective rather than enabling everything blindly. I go deeper on that in my Apache .htaccess guide.
A common mistake is pointing DocumentRoot at the right path but writing the <Directory> block for a different path. Then the site returns unexpected 403 errors or ignores the directives you thought were active.
Logging per VirtualHost
Per-site logs save time during troubleshooting.
ErrorLog ${APACHE_LOG_DIR}/example.com-error.log
CustomLog ${APACHE_LOG_DIR}/example.com-access.log combined
I strongly prefer this over throwing every site into one shared log file. When one domain starts producing 404 floods or rewrite loops, I want isolated evidence.
Useful checks:
sudo tail -n 50 /var/log/apache2/example.com-error.log
sudo tail -n 50 /var/log/apache2/example.com-access.log
Path names vary by distribution, but the principle does not.
Redirect-only VirtualHosts
I usually keep HTTP-to-HTTPS redirects in a separate vhost that does only one thing.
<VirtualHost *:80>
ServerName example.com
ServerAlias www.example.com
Redirect permanent / https://example.com/
ErrorLog ${APACHE_LOG_DIR}/example.com-redirect-error.log
CustomLog ${APACHE_LOG_DIR}/example.com-redirect-access.log combined
</VirtualHost>
This is simpler than mixing site content and redirect behavior in the same port 80 vhost.
For more complex redirect logic, mod_rewrite may be appropriate, but I prefer Redirect from mod_alias when a simple full-site redirect is enough. Fewer moving parts, fewer surprises.
Multiple domains in one VirtualHost
Sometimes multiple domains really should serve the same site.
<VirtualHost *:80>
ServerName example.com
ServerAlias www.example.com example.net www.example.net
DocumentRoot /var/www/example.com/public
</VirtualHost>
This is fine when the content and behavior are intentionally shared. It becomes messy when SEO rules, canonical redirects, certificate coverage, or application expectations differ.
My preference is usually one canonical domain and explicit redirects from alternates, rather than silently serving the same content under many hostnames.
Enabling and disabling sites with a2ensite and a2dissite
On Debian-based systems, I use the standard site management commands instead of manually editing symlinks.
sudo a2ensite example.com.conf
sudo a2dissite 000-default.conf
sudo apache2ctl -t
sudo systemctl reload apache2
That sequence is deliberate. I validate config before reload every time.
Module management works similarly:
sudo a2enmod ssl rewrite headers
sudo apache2ctl -t
sudo systemctl reload apache2
On RHEL-like systems there is no a2ensite; you typically manage include files directly and reload httpd.
Default SSL VirtualHosts and SNI surprises
HTTPS adds another wrinkle: certificate selection happens before Apache can fully route by HTTP content, so the default SSL vhost and SNI behavior matter. Modern clients send SNI, which lets Apache choose the matching certificate and vhost by hostname during the TLS handshake. That is the normal case now.
But when SNI is missing, broken, or you are testing with a tool that is not sending the hostname you think it is, Apache falls back to the first SSL vhost loaded for that address and port. That is why a wrong certificate often appears “randomly” to people who are really hitting the default SSL site.
I verify this with both hostname-aware and direct tests:
curl -Ik https://example.com/
curl -Ik --resolve example.com:443:127.0.0.1 https://example.com/
openssl s_client -connect example.com:443 -servername example.com </dev/null
If the certificate or vhost content is wrong, check the first loaded *:443 vhost and the ServerName values before you do anything more exotic.
File layout and naming conventions that save time later
I like boring file names because they make support work easier. For Debian-style layouts, something like this is clear:
000-default.conffor the intentional catch-allexample.com.conffor port 80 content or redirectsexample.com-le-ssl.conforexample.com-ssl.conffor port 443
The exact names matter less than consistency. What I want is a directory where I can tell, at a glance, which file owns which hostname and which one will load first. If I inherit a directory full of names like site1.conf, newsite.conf, and copy-of-working.conf, I expect trouble.
I also keep one vhost per file unless there is a very good reason not to. Bundling unrelated sites together makes reloads and rollbacks harder than they need to be.
Permissions, SELinux, and filesystem mistakes
Sometimes the VirtualHost is correct and the filesystem is not. Apache can match the right site perfectly and still return a 403 or 500 because the document root, parent directory, or security context is wrong.
Basic checks I run:
namei -l /var/www/example.com/public
ls -ld /var/www/example.com /var/www/example.com/public
On SELinux-enabled systems I also check the context, because the file permissions alone may look fine while the label is blocking access:
ls -Zd /var/www/example.com/public
This is not glamorous, but it is common. People spend too long staring at Apache config when the real issue is that the web server cannot traverse a parent directory or serve files with the current context.
Inheritance and overriding behavior
Apache has global config, virtual host config, directory config, and sometimes .htaccess directives all interacting. Inherited behavior is powerful and easy to lose track of.
A few practical lessons:
- A global directive may apply unless a vhost overrides it.
- A
<Directory>block may change behavior inside a vhost without the vhost file showing it clearly. .htaccesscan override behavior ifAllowOverridepermits it.
When troubleshooting, I do not trust the site file alone. I inspect the full effective config:
sudo apache2ctl -t -D DUMP_VHOSTS
sudo apache2ctl -M
If I suspect includes are hiding something, I review the active file tree directly.
The classic missing ServerName problem
If a vhost does not define ServerName, matching behavior can become ambiguous or default unexpectedly. This is one of those Apache problems that keeps surviving because partial configs appear to work right up until a new site is added.
I have seen cases where:
- the wrong site answered on port 80
- the SSL vhost responded with the wrong certificate chain
- a redirect vhost stole traffic from the intended site
The fix is usually boring: define ServerName explicitly everywhere and verify with apache2ctl -S.
Reading apache2ctl -S output
This command tells you how Apache sees the world.
sudo apache2ctl -S
You will see output describing:
- port bindings
- name-based vhosts
- default server per address:port
- config file and line number for each vhost
I look for three things first:
- Is the expected hostname listed?
- Is the intended vhost the default for the port when needed?
- Are any aliases broader than I intended?
When those three checks pass, most matching mysteries disappear.
Canonical hostnames and redirect strategy
A VirtualHost layout becomes easier to reason about when every site has a clear canonical hostname. If example.com is the real site and www.example.com is only an alias, I decide that once and enforce it consistently instead of letting both answer content forever.
A dedicated redirect vhost is usually enough:
<VirtualHost *:80>
ServerName www.example.com
Redirect permanent / https://example.com/
</VirtualHost>
I use the same idea on port 443 when needed so the TLS site presents a certificate that covers the alias and then redirects cleanly. This avoids duplicate content, mixed absolute URL behavior, and the slow creep of “temporary” alternate hostnames becoming permanent by accident.
Common mistakes I keep seeing
Mixing app content and redirect logic in one vhost
That often leads to redirect loops or partial enforcement.
Enabling a site without SSL module or cert paths ready
Then the reload fails or the HTTPS vhost never comes up.
Copying an old tutorial with NameVirtualHost
It is clutter at best on Apache 2.4.
One shared catch-all vhost masking hostname mistakes
Everything looks “up” until somebody notices the wrong site is answering.
No per-site logs
Then diagnosing one broken vhost means sorting through unrelated traffic.
Validation commands I actually run
Before reload:
sudo apache2ctl -t
sudo apache2ctl -S
After reload:
sudo systemctl reload apache2
sudo systemctl status apache2 --no-pager
curl -I http://example.com/
curl -Ik https://example.com/
On RHEL-like systems use httpd service names and paths as appropriate.
If the site is supposed to redirect:
curl -I http://www.example.com/
I want to see the exact Location header, not just trust browser behavior.
Security considerations
VirtualHosts are often where TLS, redirects, headers, and directory permissions come together. I pay attention to:
- correct certificate paths and renewals
- least-permissive directory settings
- avoiding accidental directory indexes
- separating admin or private vhosts cleanly
- keeping old weak SSL examples out of current config
Old snippets that still mention deprecated SSL protocols or weak ciphers do not belong in a modern deployment. My modern Apache SSL configuration guide covers current practice in more detail.
Final thoughts
Apache VirtualHost configuration is not difficult, but it punishes assumptions. The server answers based on clear matching rules, include order, and per-port defaults. If you know how Apache is selecting a vhost and you validate with apache2ctl -S, most “mystery” behavior stops being mysterious.
My own approach is simple: explicit ServerName, minimal and readable vhost files, separate redirect vhosts, per-site logs, and validation before every reload. That keeps multi-site Apache setups predictable, which is exactly what I want from infrastructure. Predictable systems are easier to hand off, audit, and recover during bad changes, especially on servers that host more than one application.