10. Virtual hosts and mod_rewrite¶
When you’ve only got two ducks, they’re always in a row.—Rich Bowen
This chapter covers using mod_rewrite for dynamic virtual host
configuration — mapping incoming hostnames to document roots, CGI
directories, or entirely different server configurations on the fly.
Chapter 2 introduced mod_vhost_alias as the preferred tool for
mass virtual hosting. This chapter shows the mod_rewrite approach,
which offers more flexibility at the cost of more complexity. The
httpd documentation itself advises: mod_rewrite is usually not the
best way to configure virtual hosts — consider the alternatives first.
10.1. The problem¶
You have dozens — or hundreds, or thousands — of hostnames all
pointing to the same server. Each hostname needs to serve content from
a different directory. The traditional approach is a <VirtualHost>
block for each one:
<VirtualHost *:80>
ServerName www.alice.example.com
DocumentRoot /var/www/vhosts/alice
</VirtualHost>
<VirtualHost *:80>
ServerName www.bob.example.com
DocumentRoot /var/www/vhosts/bob
</VirtualHost>
# ... repeat 500 more times
This doesn’t scale. Every new hostname requires a config change and a server restart. For a shared hosting provider or a platform that provisions sites dynamically, you need the mapping to happen automatically — derive the document root from the hostname at request time, without any per-host configuration.
10.2. Dynamic vhosts with mod_rewrite¶
The core recipe uses mod_rewrite to capture the incoming
hostname and map it to a filesystem path. Here’s the full example:
RewriteEngine On
# Normalize the hostname to lowercase
RewriteMap lowercase int:tolower
# Capture the hostname and map it to a directory
RewriteCond %{HTTP_HOST} ^(.+)$
RewriteRule ^(.*)$ /var/www/vhosts/${lowercase:%1}/$1 [L]
Let’s walk through this:
The
RewriteMapdefines a mapping calledlowercaseusing the built-inint:tolowerfunction — this normalizes the hostname so thatWWW.Example.COMandwww.example.comresolve to the same directory.The
RewriteCondcaptures the entireHostheader into%1.The
RewriteRulecaptures the request path into$1and constructs the full filesystem path:/var/www/vhosts/<hostname>/<path>.
So a request for http://www.alice.example.com/index.html is served
from /var/www/vhosts/www.alice.example.com/index.html.
Note
Remember the backreference distinction: %1 through %9 are
captures from the most recent RewriteCond; $1 through $9
are captures from the RewriteRule pattern. Getting these mixed
up is a very common mistake.
10.2.1. Stripping the www. prefix¶
If your directory structure doesn’t include the www. prefix, strip
it:
RewriteCond %{HTTP_HOST} ^(?:www\.)?(.+)$
RewriteRule ^(.*)$ /var/www/vhosts/${lowercase:%1}/$1 [L]
The non-capturing group (?:www\.)? matches and discards the
www. if present, so %1 contains only the bare domain.
10.3. Using a map file for vhosts¶
Deriving the path from the hostname with regex is convenient, but it
assumes a predictable directory structure. When you need explicit
control — mapping customer-a.example.com to /var/www/sites/customer_a
rather than the hostname itself — a RewriteMap is cleaner:
# /etc/httpd/conf/vhost.map
customer-a.example.com /var/www/sites/customer_a
customer-b.example.com /var/www/sites/customer_b
demo.example.com /var/www/sites/demo
RewriteMap vhostmap txt:/etc/httpd/conf/vhost.map
RewriteMap lowercase int:tolower
RewriteCond %{HTTP_HOST} ^(.+)$
RewriteCond ${vhostmap:${lowercase:%1}|NOTFOUND} !=NOTFOUND
RewriteRule ^(.*)$ ${vhostmap:${lowercase:%1}}/$1 [L]
The second RewriteCond checks that the hostname actually exists in
the map — if it doesn’t, the default NOTFOUND is returned and the
rule is skipped, which lets the request fall through to a default
virtual host.
For high-traffic servers, convert the text map to a DBM hash for faster
lookups (see RewriteMap for the httxt2dbm tool and
dbm: map type).
10.4. Handling aliases and CGI in dynamic vhosts¶
Here’s a complication that catches everyone the first time:
mod_rewrite runs before mod_alias in the request
processing pipeline. That means Alias and ScriptAlias directives
— things like Alias /icons/ /usr/share/httpd/icons/ — get
bypassed by the rewrite rule, because mod_rewrite has already
mapped the URL to a filesystem path before mod_alias gets a
chance.
The fix is to explicitly exclude those paths from rewriting:
# Let Alias and ScriptAlias handle these paths
RewriteCond %{REQUEST_URI} !^/icons/
RewriteCond %{REQUEST_URI} !^/cgi-bin/
RewriteCond %{REQUEST_URI} !^/error/
# Then the vhost mapping
RewriteCond %{HTTP_HOST} ^(.+)$
RewriteRule ^(.*)$ /var/www/vhosts/${lowercase:%1}/$1 [L]
Alternatively, if you need CGI to work within each virtual host’s
directory, you can use the [H=cgi-script] handler flag to force
CGI processing for specific paths:
RewriteRule ^/cgi-bin/(.*)$ /var/www/vhosts/${lowercase:%1}/cgi-bin/$1 [H=cgi-script,L]
This maps each vhost’s /cgi-bin/ to a directory within its own
docroot and ensures the CGI handler runs.
10.5. Why mod_vhost_alias is usually better¶
Chapter 2 introduced mod_vhost_alias, and for mass virtual
hosting, it’s almost always the better choice. Here’s why:
mod_vhost_alias maps hostnames to directories using
interpolation tokens (%0 for the full hostname, %1/%2/etc.
for individual components), and it does so before the Alias and
ScriptAlias resolution phase — meaning those directives still work
correctly:
UseCanonicalName Off
VirtualDocumentRoot /var/www/vhosts/%0
That single directive replaces the entire mod_rewrite recipe
above, including the lowercase map, the RewriteCond, and the
exclusion conditions for /icons/ and /cgi-bin/.
Use mod_rewrite for dynamic virtual hosts only when you need
something mod_vhost_alias can’t do:
Conditional logic — different behavior for certain hostnames (e.g., redirect some to a different server, serve others from a special directory).
Hostname transformations that go beyond token interpolation — for example, looking up the hostname in a database via a
RewriteMap prg:external program.Combining vhost mapping with other rewrite rules — for example, forcing HTTPS and mapping to a vhost directory in the same ruleset.
10.6. Per-user virtual hosts¶
A common variant of dynamic hosting: mapping ~user or
/users/username/ URLs to user home directories. Chapter 2 covered
mod_userdir, which handles this natively:
UserDir public_html
This maps /~alice/ to /home/alice/public_html/. Simple and
effective.
The mod_rewrite version is useful when mod_userdir
doesn’t fit your layout — for example, if user content lives outside
home directories, or you need conditional access:
# Map /users/alice/ to /var/www/users/alice/
RewriteRule ^/users/([^/]+)/?(.*)$ /var/www/users/$1/$2 [L]
Or with access restrictions:
# Only allow user pages for users in the map
RewriteMap validusers txt:/etc/httpd/conf/valid-users.txt
RewriteCond ${validusers:$1|INVALID} !=INVALID
RewriteRule ^/users/([^/]+)/?(.*)$ /var/www/users/$1/$2 [L]
# Everyone else gets a 404
RewriteRule ^/users/ - [R=404]
10.7. Logging for dynamic vhosts¶
When all your virtual hosts share a single configuration, they also share a single log file. The trick is to include the hostname in each log entry so you can distinguish them:
LogFormat "%V %h %l %u %t \"%r\" %>s %b" vhost_common
CustomLog /var/log/httpd/access_log vhost_common
The %V token logs the server name from the request (effectively the
Host header). Now every log line is prefixed with the hostname.
Conditional logging — write different vhosts to different log files:
SetEnvIf Host "^alice\.example\.com$" vhost=alice
SetEnvIf Host "^bob\.example\.com$" vhost=bob
CustomLog /var/log/httpd/alice_access.log common env=vhost=alice
CustomLog /var/log/httpd/bob_access.log common env=vhost=bob
This works for a small number of known vhosts, but doesn’t scale to the mass hosting scenario.
Splitting logs after the fact — for mass hosting, it’s more
practical to log everything to a single file with %V and split it
later. Apache httpd ships with a split-logfile utility that reads
a combined log and writes per-vhost log files:
split-logfile < /var/log/httpd/access_log
This creates files named alice.example.com-access_log,
bob.example.com-access_log, and so on. It requires no
per-vhost configuration, and handles the dynamic nature of mass hosting
cleanly.
For real-time per-vhost logging at scale, piped logging through a script is also an option:
CustomLog "|/usr/local/bin/vhost-log-splitter.sh" vhost_common
But that’s an exercise left to the reader — and to the reader’s tolerance for debugging shell scripts in a production log pipeline.