8

I'd like to set up an environment where Apache virtual hosts can be dynamically created without reloading the configuration.

I can do this with mod_vhost_alias, I set up my default virtual host something like this

<VirtualHost *>
  UseCanonicalName Off
  VirtualDocumentRoot /var/www/sandboxes/domains/%0
  ServerName catchall.host
</VirtualHost>

That works just fine, but if a request is made for a hostname which isn't currently set up, I get a 404 Not Found error.

What I'd really like to do is only have this VirtualHost kick in only if the document root exists, otherwise have it try to match another vhost (in other words, make the presence of the the VirtualDocumentRoot work in the same way as using a ServerAlias)

I tried making this the second vhost, with the first vhost just handling all requests, but this didn't work - requests for domains where a VirtualDocumentRoot was configured were falling through to the default vhost.

So, how can I have dynamically configured vhosts, but with a fallback to another vhost for any which aren't configured yet?

0

4 Answers 4

8

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.

2
  • Wouldn't that throw a 404 as the status for every request to the default?
    – Kyle
    Commented Mar 5, 2012 at 3:15
  • 1
    The error handler is only invoked if the VirtualDocumentRoot doesn't exist (or it really was a 404, but the notfound.php can easily tell the difference).
    – Paul Dixon
    Commented Mar 5, 2012 at 9:42
3

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/.

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.
1

Just throwing it out there, I get the same result (minus the localhost redirect) as RemyNL, but also including the IP address when connecting directly:

<VirtualHost _default_:80>
    RewriteEngine On
    RewriteCond %{HTTPS} Off [OR] 
    RewriteCond %{HTTP:X-Forwarded-Proto} !https
    RewriteRule ^/(.*) https://%{HTTP_HOST}/$1 [NC,R=301,L]
</VirtualHost>

<VirtualHost _default_:443>
    ServerName a.b.c.d
    ServerAlias *.*.*.*
    VirtualDocumentRoot /var/www/%0
</VirtualHost>

<VirtualHost _default_:443>
    ServerName a.b.c
    ServerAlias *.*.*
    VirtualDocumentRoot /var/www/%-2.0.%-1.0/%-3
</VirtualHost>

<VirtualHost _default_:443>
    ServerName a.b
    ServerAlias *.*
    VirtualDocumentRoot /var/www/%-2.0.%-1.0/www
</VirtualHost>
3
  • 1
    Is _default_ special in any way ?
    – Carel
    Commented Aug 1, 2020 at 22:44
  • @Carel, the _default_ means that the virtual host will be applied on all interfaces. if you use interface address such as 127.0.0.1 it will only apply to localhost address.
    – HelpNeeder
    Commented Aug 3, 2020 at 3:44
  • Thanks @HelpNeeder I'm not sure how I missed this; it is peppered all over the documentation... I checked and all of my configurations seem to have used * in the past.
    – Carel
    Commented Aug 3, 2020 at 13:07

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .