Using VirtualDocumentRoot *only* if a suitable document root exists

Solution 1:

I found a workaround which works for me.

I can use an ErrorDocument to pick up the 404 Error in a PHP script. At first, that seemed problematic. If there's no DocumentRoot, where will the script live?

I could use a URL for the error message, served from a different domain. However, in testing I found no way of knowing what the originally requested domain was.

The answer was to Alias a directory and serve the errors out of that, so my vhost looks like this

<VirtualHost *>
  UseCanonicalName Off
  VirtualDocumentRoot /var/www/sandboxes/domains/%0
  ServerName catchall.host
  Alias /errors /var/www/default/errors/
  ErrorDocument 404 /errors/notfound.php
</VirtualHost>

Now, when an unconfigured domain is requested, the script at /var/www/default/errors/notfound.php is invoked instead. This script can check $_SERVER['HTTP_HOST'] to see what domain was requested. If it is actually configured, then we have a regular 404. If it's not configured, we can display some alternate error message. In my case, it's where I show a UI to help set the vhost up.

Solution 2:

I found your question when Googling. I had exactly the same problem and applied the fix as described in Paul's answer. However, for my complex web app, I wasn't happy with routing all requests trough one single notfound.php.

In the end, I managed to fix the problem without external scripts, only by editing my VirtualHost config.

At first, my VirtualHost was configured like this:

<VirtualHost *:80>
    Usecanonicalname Off
    Virtualdocumentroot /mnt/ramdisk/www/cms-%-3.0-development/
</VirtualHost>

I had URLs like client1.domainname.com, client2.domainname.com and whatever.domainname.com

which resolved to /mnt/ramdisk/www/cms-client1-development/, /mnt/ramdisk/www/cms-client2-development/ and /mnt/ramdisk/www/cms-whatever-development/

However, an URL like nonexistent.domainname.com would give me a 404 because the directory /mnt/ramdisk/www/cms-nonexistent-development/ did not exist. I wanted these subdomains to use the directory /mnt/ramdisk/www/cms-default-development/

I fixed this by using ModRewrite and ModProxy:

<VirtualHost *:80>
    Usecanonicalname Off
    Virtualdocumentroot /mnt/ramdisk/www/cms-%-3.0-development/
    RewriteEngine on
    RewriteCond %{HTTP_HOST} ^(.*)\.domainname\.com$ [NC]
    RewriteCond /mnt/ramdisk/www/cms-%1-development/ !-d
    RewriteRule (.*) http://default.domainname.com/$1 [P,L]
</VirtualHost>

What this does is: grab the subdomain (the part before .domainname.com), insert that into the path (the %1) and proxy requests to the default URL only if the directory doesn't exist.

By using a proxy, the process is transparent and the user thinks he's visiting http://nonexistent.domainname.com, while he actually views the content from http://default.domainname.com/.

Solution 3:

I also came across this question googling for apache2 dynamic vhost fallback and Luc's answer helped me a lot with solving my problem, but I still want to show what I did to achieve my goals, mainly because it involved some extra works and because I think it could be helpful for any future googlers...

My goals

  • dynamic vhosting for all domains and subdomains pointing at my VPS
  • foo.com should serve the same content as www.foo.com
  • fallback for unknown domains to some sort of default
  • fallback for unknown subdomains of foo.com to www.foo.com unless the www is not available, fallback to default instead

DNS

I have a couple of domains (and all their subdomains) pointing at my VPS, for example:

  • foo.com
  • bar.com
  • foobar.com

Filesystem

I have the following directories, domains contain directories with the names of the available subdomains, the www directory is required, but the config should be able to deal with the situation where it is not present. Localhost is used as default fallback:

/var
  /www
    /localhost
    /foo.com
       /www
       /bar
    /bar.com
       /foo

Tests

Translating my goals into testable cases:

  • foo.com should be served from foo.com/www
  • www.foo.com should be served from foo.com/www
  • bar.foo.com should be served from foo.com/bar
  • foo.foo.com should be served from foo.com/www (foo.com/foo does not exist)
  • bar.com should be served from localhost (bar.com/www does not exist)
  • www.bar.com should be served from localhost (bar.com/www does not exist)
  • foo.bar.com should be served from bar.com/foo
  • bar.bar.com should be served from localhost (bar.com/bar does not exist)
  • foobar.com should be served from localhost (foobar.com does not exist)
  • www.foobar.com should be served from localhost (foobar.com does not exist)
  • foo.foobar.com should be served from localhost (foobar.com does not exist)

The Solution

This uses: mod_rewrite, mod_proxy_http and ofcourse mod_vhost_alias.

ServerName my.domain
ServerAdmin [email protected]

<VirtualHost *:80>
    ServerName localhost
    VirtualDocumentRoot /var/www/localhost
</VirtualHost>

<VirtualHost *:80>
    ServerName sub.domain
    ServerAlias *.*.*
    VirtualDocumentRoot /var/www/%-2.0.%-1.0/%-3

    RewriteEngine on

    RewriteCond %{HTTP_HOST} ^(.*)\.(.*)\.(.*)$ [NC]
    RewriteCond /var/www/%2.%3 !-d
    RewriteRule (.*) http://localhost/$1 [P]

    RewriteCond %{HTTP_HOST} ^(.*)\.(.*)\.(.*)$ [NC]
    RewriteCond /var/www/%2.%3/%1 !-d
    RewriteCond /var/www/%2.%3/www !-d
    RewriteRule (.*) http://localhost/$1 [P]

    RewriteCond %{HTTP_HOST} ^(.*)\.(.*)\.(.*)$ [NC]
    RewriteCond /var/www/%2.%3/%1 !-d
    RewriteRule (.*) http://%2.%3/$1 [P]
</VirtualHost>

<VirtualHost *:80>
    ServerName bare.domain
    ServerAlias *.*
    VirtualDocumentRoot /var/www/%-2.0.%-1.0/www

    RewriteEngine on

    RewriteCond %{HTTP_HOST} ^(.*)\.(.*)$ [NC]
    RewriteCond /var/www/%1.%2 !-d [OR]
    RewriteCond /var/www/%1.%2/www !-d
    RewriteRule (.*) http://localhost/$1 [P]
</VirtualHost>

How does this work? There are three virtual hosts defined:

localhost

The localhost serves as a default. All requests that are not resolvable are served by localhost. Setting up a symlink from localhost to any of your domains is like setting up that site as a default.

sub.domain

The sub.domain vhost is taking all requests in the form of *.*.*. By default all requests are served from /domain.com/sub as defined by VirtualDocumentRoot /var/www/%-2.0.%-1.0/%-3.

fallback:

The first RewriteRule takes care of unknown domains, eg. domain.com directory does not exist, by proxying the localhost website.

The second RewriteRule also proxies to localhost when both the domain.com/sub and the domain.com/www directories are not present.

The third RewriteRule proxies to domain.com when domain.com/sub does not exist. We know domain.com/www does exist because of the second rewrite block.

bare.domain

The bare.domain vhost is taking the *.*requests and serves them /domain.com/www

Here the RewriteRule will proxy to localhost when domain.com or domain.com/www do not exist.

^$%.*!!!

I had some trouble wrapping my head around all those $ and % signs in the RewriteCond and RewriteRule so I will explain about them here:

    ServerAlias *.*.*
    VirtualDocumentRoot /var/www/%-2.0.%-1.0/%-3
    RewriteCond %{HTTP_HOST} ^(.*)\.(.*)\.(.*)$ [NC]
    RewriteCond /var/www/%2.%3/%1 !-d
    RewriteRule (.*) http://%2.%3/$1 [P]
  • The * in the ServerAlias are just wildcards.
  • The %n in the VirtualDocumentRoot are from the document name interpolation.
  • The %n in the second RewriteCond refer to the selections (.*) from the first RewriteCond, eg. the parts of the requested domain.
  • The %n in the RewriteRule do too.
  • The $1 in the RewriteRule refers to the selection (.*) at the beginning of the RewriteRule. Which captures everything from the domain till the ? in the request url. Any querystring is automatically added to the url by mod_proxy.