Blog

08
February 2015

Gavin Pickin

Apache and Tomcat - Save yourself the XML Editing - No more Tomcat restarts

Apache, CFML Server, Lucee, Migrating to Railo, Server Admin, Techie Gotchas

On the Lucee Google group the other day, someone mentioned a way to connect Nginx to Tomcat, so you do not need to bother adding the XML Host for every new website you want to run on Lucee (or Railo). This has always been a pain point for switching from Adobe ColdFusion, to Railo or now Lucee, dealing with a Virtual Host for Apache in httpd.conf, and a XML Host in server.xml and making sure they match, and editing requires a restart. Adobe’s ColdFusion runs on a hacked Tomcat, that they specially tuned to work like jrun used to, but a lot of people don’t like being stuck on their upgrade path too. I decided to look at this Nginx approach, and get it running in Apache, and see what downsides I could find.

I played for a couple of hours, and came up with this. I posted my findings to the Lucee group, and haven’t got much feedback on it, so I assume thats a good sign. If you see anything alarming in what I say below, please let me know. I intend to start using this in the dev environment for sure, and maybe even production.

So why should we care about editing 2 conf files, and restarting both engines?

If you have only a few sites, restarting Tomcat is not painful, but if you have 10+, and do no special start up tuning, you can run 30 seconds to several minutes for Tomcat to restart. Note, I have a few tricks that got my restart from 3 minutess to 3 seconds, but that's for another post. Of course this is downtime for your users, whether it is short or long, we want to limit this where possible, and Apache restarts seem quick, and usually not too damaging to the user experience. Regardless, if you are adding a site, you have to restart or reload Apache, so why reload another system too, right?

Conf files aren’t much fun… and having another please to edit settings can get confusing fast, especially if you don’t edit them often.
I am building a CommandBox CLI tool to help with this, which will be looked at more in the next post, but lets see how we can make our lives easier.

FYI - I’m assuming you have a default Lucee setup in this article, so you have AJP on 8009, HTTP on 8888, so if you see these ports numbers, this is why I'm using these.

If you do not want the explanation… skip to the bottom for how to do it. 
I like to UNDERSTAND not just REMEMBER, but the choice is yours.

So how does a normal request happen, lets look at the rough flow, and see how this approach can help.

  • User entered a URL in their browser.
  • DNS translates the URL to an IP and the request is routed to your Web Server
  • Apache handles the request to www.gpickin.com on port 80.
  • Using VirtualHosts, apache looks for a ServerName or ServerAlias matching www.gpickin.com for port 80. It finds one, looking like this

<VirtualHost *:80>
     ServerName www.gpickin.com
     DocumentRoot /www/www.gpickin.com
<Proxy *>
Allow from 127.0.0.1
</Proxy>
ProxyPreserveHost On
ProxyPassMatch ^/(.+\.cf[cm])(/.*)?$ ajp://localhost:8009/$1$2
</VirtualHost>

  • Using this configuration for the VirtualHost, Apache deals with the requests.
  • The Browser might want the favicon.ico that does not match the regex in ProxyPassMatch, so Apache looks for in the DocumentRoot and serves the favicon.ico if available, or a 404 instead.
  • If the default file is index.cfm Apache sees we have a ProxyPassMatch setup, does index.cfm match? Yes.
  • So Apache sends the request via AJP ( Apache Serv Protocol - an efficient trimmed down version of HTTP ) to Tomcat on Localhost looking at AJP port 8009
  • It passes path and query string in $1 and $2 from the regex.
  • When tomcat is done, body content and headers are passed back to Apache, which returns them to the Browser and the user.

This whole process does not really change in the new method, but a slight tweak affects how Tomcat deals with it.

How does Tomcat deal with the request

As we said, listening on localhost port 8009 Tomcat gets a url request. www.gpickin.com/index.cfm

Tomcat looks in its host files, to see how to deal with this. Traditionally, I would have a Host context setup for every site, and it would look like this.

<Host name=“www.gpickin.com" appBase="webapps">
       <Context path="" docBase="/www/www.gpickin.com" />
</Host>

Tomcat would find this Host and match the name, and then would serve the files, treating the docBase as ROOT of this Context/Website/App and process and return the headers/body as needed.

What happens if Tomcat doesn’t match a Host context?

There is a setting for that, on the Engine element, that looks like this
<Engine name="Catalina" defaultHost="localhost">

Usually its set like this, so if it cannot match www.gpickin.com it would use the Host Context for localhost.
There is one by default.. that looks like this.

<Host name="localhost"  appBase="webapps"  unpackWARs="true" autoDeploy="true">
        <!-- SingleSignOn valve, share authentication between web applications
             Documentation at: /docs/config/valve.html -->
        <!--
        <Valve className="org.apache.catalina.authenticator.SingleSignOn" />
        -->
        <!-- Access log processes all example.
             Documentation at: /docs/config/valve.html
             Note: The pattern used is equivalent to using pattern="common" -->
        <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
               prefix="localhost_access_log" suffix=".txt"
               pattern="%h %l %u %t &quot;%r&quot; %s %b" />
      </Host>

So if it didn’t match www.gpickin.com, the Engine element tells it use localhost’s context, which doesn’t have a docBase set, so it uses ROOT inside webApps, and will serve the index.cfm from there. Which would be the Lucee welcome page. Not exactly what we want when we type in www.gpickin.com

The Solution

The solution, using a single context… but how will Tomcat know how to serve the right files? Right now, it will always serve the files out of the Tomcat install dir /webApps/ROOT/
We setup Tomcat’s default context as the root context, with all the websites being sub directories of the root context.
All of my websites, on my dev machine, are in the /www folder. Lets delete the main context and set up the context with a docBase of /www

<Host name="localhost" appBase="webapps">
       <Context path="" docBase="/www/" />
</Host>

Now, we can restart tomcat 1 time, and as we add more sites, we will not touch Server.xml or this host again.
What we’ll do is update the ProxyPassMatch to hit this context and include the relative path, from /www to the Website we want to serve our files.

<VirtualHost *:80>
     ServerName www.gpickin.com
     DocumentRoot /www/www.gpickin.com
<Proxy *>
Allow from 127.0.0.1
</Proxy>
ProxyPreserveHost On
ProxyPassMatch ^/(.+\.cf[cm])(/.*)?$ ajp://localhost:8009/www.gpickin.com/$1$2
</VirtualHost>

See the subtle change?

When we used to call ajp://localhost:8009/$1$2 it would serve from /www
With ajp://localhost:8009/www.gpickin.com/$1$2 it will serve from /www/www.gpickin.com/
If we have a crazy path, we could use ajp://localhost:8009/www.gpickin.com/cfml/wwwroot/$1$2

It doesn’t matter, the context uses the relative path, like one giant website. 
www.gpickin.com/myadmin is translated to ajp://localhost:8009/www.gpickin.com/cfml/wwwroot/myadmin/

So now, Apache serves html out of its root, Tomcat/Lucee serves your cfml out of its root + relative and everything works without add Host Contexts to Lucee/Tomcat, and no more restarts

Wow, that's really simple - but what’s the Catch

Of course, there is always a catch. It didn’t take me too long before I found a couple of gotchas. The fix was easy, so read on.
Component Paths - If I am in the root of my app, and I want to use Component Paths to get to my services.factory… it works. If I am in my models, and I want to, I user services.factory, and it doesn’t work. The reason is component paths search relative first:

  • in /www/www.gpickin.com using services.factory looks in /www/www.gpickin.com/services for factory
  • in /www/www.gpickin.com/models/ using services.factory looks in /www/www.gpickin.com/models/services for factory, then it looks in /www/services for factory.

Of course you could use some DI framework, but the simple fix is set your mapping in the Application.cfc
this.mappings[ "/“ ] = getDirectoryFromPath( getCurrentTemplatePath() );

The other gotcha was in cfincludes, since the Context docBase is /www, if I do a cfinclude template=“/index.cfm” it looks in /www/index.cfm not /www/www.gpickin.com/index.cfm
The mapping works for component paths, as well as cfincludes, so this fix solves both the little issues I found.

Are they the only catches?

You tell me, so far, that's been the gotchas I have found, but I haven’t used it extensively, but if you reset the root in the Application itself, I do not see why it wouldn’t translate from that point on to everything else in your code. If you find anything, or know of anything else, please share, hit me up on twitter, email, or comment right here.
It seems to be a simple but effective fix, eliminating some of the biggest negatives for moving to Railo/Lucee, and if you were already using AJP with ProxyPass, its only a few more characters of code. Of course, you can use the HTTP protocol and ports too, I tested that, and it worked the same. Although, I have heard use AJP, although you might not see the different, in bigger scale its more performant, etc.

Is it a good idea to run everything in one context?

As usual, its one of those answers, it depends. I am not an expert by any stretch of the imagination, and I do not know all the pros and cons of sharing contexts versus each having their own. I believe having separate contexts can be a good thing, although having the choice to group smaller sites, or adding sites quickly using this method has some big wins too. You can always migrate bigger sites to their own contexts, but you can do that at 2am or sometime when it least effects your customers. If someone is an expert that can give us pros and cons, please do, otherwise, do some more research, I know I will.

I think this is a big plus for Dev setups as least, and I know we’ll be using it in production soon, even if its only a temporary measure, so we can add the context and restart tomcat at a time that suits us, but doesn’t slow down our normal procedures and ability to launch a site mid day etc.
Please give me feedback… I hope I covered it all, I hope its accurate, and helpful, and if not, I would like to be able to make it better.

Try it out and come back to find out about my awesome CommandBox CLI tool I’m building, to make this even easier.

Thanks for reading.

by Jamie Jackson
02/09/2015 06:35:19 AM

Do you know about mod_cfml? The installer uses it by default, IIRC. One only needs to configure things on the Apache Web Server side, and the Tomcat contexts are created on the fly.

by Gavin
02/09/2015 08:10:07 AM

Yes, I believe mod_cfml works the same way under the covers to be honest, but thats a big assumption on my part. I will have to test that out, and see what I can figure out there. I assumed it was proxying, similar to this, but maybe it does do a full context.

I know with ProxyPass Apache passes some additional headers, I wonder if we setup the context correctly it would figure set the root correctly.

In my dev, and server environment I use Adobe CF and Railo and now Lucee. The issue is, the Perl mod_cfml stuff didn't seem to allow an easy switch in the Virtual Hosts.

I include different code in my Virtual Host depending on the engine i wish to run that site on.

Maybe I wasn't using the mod_cfml right, when I first tried this.

The other thing is, Mark Drew (Railo Rep at the time) recommended AJP in a cf.Objective talk a while back, and when we was using clustering it was a simple change to go from single instance to clustered instance, all based on the same ProxyPass code.

AJP is a special trimmed down version of the HTTP protocol, and using this I dont have to worry about Perl etc, and I have more control with multiple instances.

Each to their own, the AJP ProxyPass works well for me, so thats what I'm using.

I'll do a little more research and throw a post up about this, and see what others think. Those are my reasons though.

Thanks for the feedback.

Blog Search