April 17, 2015

Configuring Nginx as a Reverse Proxy for Hippo CMS LAB

Jeroen ReijnJeroen Reijn is Lead Architect for  Hippo Professional Services. Likes to gets things done. He's a committer at the  Apache Software Foundation and general Open Source enthusiast. He   Tweets,   BlogsBlogs even more, and shares his  code experiments. Busy guy.

Of late, there have been quite a few questions from within the Hippo CMS community on how to set up Nginx as a reverse proxy for Hippo CMS. Nginx is a high-performance HTTP and reverse proxy server (it can do a lot more though). In the Hippo CMS  documentation we tend to stick to one web/reverse proxy server ( in our case  Apache HTTP ), so we can streamline the documentation. However, Nginx is widely adopted and has been gaining quite some market share by large enterprises. 

In this Hippo labs post, we'll go through a couple of steps and will set up a local environment where we have NGINX acting as a reverse proxy server in front of Hippo CMS.

Step One - Setting up a basic Hippo CMS installation

Before we install NGINX let's first start with a clean and basic Hippo CMS setup. To makes this as easy and reproducible as possible, let's just follow the getting started trail. For this project, I used the Hippo CMS 7.9 based archetype to generate the project. I kept the default 'myhippoproject' name for which references can be found later on in this post. After the Maven build and cargo run, I went to the Essentials setup application at http://localhost:8080/essentials and installed the news and blogs plugin, repackaged and restarted cargo before I continued with the rest of this post.

Step two - Configure Nginx 

Now that we have the Hippo CMS part done, let's move on to Nginx. Since I'm running Mac OS X I'll use homebrew to install Nginx. 

$ brew install nginx

Once the command completes, we have NGINX installed (in my case version 1.6.3). The Nginx configuration can now be found at /usr/local/etc/nginx . Let's first define some host names, which we'll use to mimic a production like environment. 

In the /etc/hosts file, we will add the two domain names which will resolve to 127.0.0.1.

127.0.0.1       cms.local.dev site.local.dev

We will use the cms.local.dev domain to expose the CMS and preview site to our CMS editors and site.local.dev will be used for the actual live site. To validate both domains are working properly, you could try to ping the two domains to see if they actually resolve to your 127.0.0.1. 

Now with NGINX installed and our DNS entries set, we will move on to configure Nginx and register the two 'virtual hosts'.

Putting the CMS behind Nginx

Let's first start with the CMS. To expose the CMS through Nginx, we will need to create a virtual host configuration for the domain  cms.local.dev, which we will store in a file with that exact name.

I always like to use Atom as my text editor, so let's create this file from the command line with Atom by calling:

$ atom /usr/local/etc/nginx/sites-enabled/cms.local.dev

Now to do a proper setup we will have to define two locations:

  • / for the CMS
  • /site/ for the preview site (shown inside the CMS channel manager) 

Each location will proxy to the correct web application running in Tomcat.

server {

  listen       80;
  server_name "cms.local.dev";

  location / {
    # Set headers for proxy header rewriting, like ProxyPassReverse in Apache http
    # See http://wiki.nginx.org/LikeApache
    proxy_set_header X-Forwarded-Host $host;
    proxy_set_header X-Forwarded-Server $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;

    proxy_pass http://localhost:8080/cms/;
    proxy_redirect default;
    proxy_cookie_path /cms/ /;
  }

  location /site/ {
    proxy_pass http://localhost:8080/site/;
  }

}

As you can see we've registered Nginx to listen on port 80, which means we will have to run Nginx later on with sudo, otherwise we can't make it listen on port 80. 

Now that we have also set up Nginx correctly, let's start Nginx:

$ sudo nginx

Make sure that the CMS is running properly by going to  http://cms.local.dev. The final check is to make sure that the channel manager is working properly. In my case it works and I get to see the 'preview' site as expected.

//onehippo-prod.global.ssl.fastly.net/binaries/ninecolumn/content/gallery/connect/labs/deployment/nginx-site-channel-manager.png

Putting the 'live' site behind Nginx

Now that we have configured Nginx for the CMS correctly, let's set up the site. Because I like to keep my configuration as simple and straight forward as possible, let's create a separate Nginx virtual host configuration for the site:

$ atom /usr/local/etc/nginx/sites-enabled/site.local.dev

Now put the following content in this file.

server {

  listen       80;
  server_name "site.local.dev";

  location / {
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Host $host;
    proxy_set_header X-Forwarded-Server $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;

    proxy_pass http://localhost:8080/site/;
    proxy_redirect default;
    proxy_cookie_path /site/ /;
  }

}

Now the important part in this configuration are the proxy_set_header directives. They make sure that the Hippo delivery tier can understand which domain / site is being requested by the visitor.  Make sure you save the new file and let's reload the nginx configuration.

$ sudo nginx -s reload

Let's see if everything is working when we browse to http://site.local.dev/

//onehippo-prod.global.ssl.fastly.net/binaries/ninecolumn/content/gallery/connect/labs/deployment/site-no-styling.png

Hmm that looks a bit odd. It seems the default styling is missing. Let's see why this is happening. While looking at the HTML source you will notice that the reference to the CSS file in the HTML source code is:

<link rel="stylesheet" href="/site/css/bootstrap.css" type="text/css"/> 

Now that's odd right? The reference to the CSS file still contains the /site/ context path of the web application. To make this work properly we need to register our domain in the hippo delivery tier.

Step three - Configuring the Hippo delivery tier

The reference to the /site/ application context is still in the URL because we've not yet configured the correct virtual host mapping in our Hippo HST configuration. By default the archetype generated project creates a configuration, which listens only to the localhost domain, uses port 8080 and exposes the context path of the site application in the links, so that it runs nicely when using locally with cargo at http://localhost:8080/site/. Now when using an unknown domain it by default falls back to the configuration of the localhost domain, hence it's reusing the cargo based virtual host configuration and appends the /site/ context.

To fix this we will need to go into the Hippo CMS console and navigate to:

/hst:hst/hst:hosts

To not interfere with the localhost cargo based setup and to mimic our DNS entries let's create a new virtual host group called: dev-local.

The HST configuration for the site.local.dev domain requires us to reverse the domain name and create virtual host entries for every part of the domain. So we need to create a tree with the following nodes: dev, local (child of 'dev'), site (child of 'local'). Once we're there, we can create an hst:root mount to tell the HST that this is the root of the domain. For convenience, I've exported the structure to XML, so you can just import the XML file in the CMS console at /hst:hst/hst:hosts.

<?xml version="1.0" encoding="UTF-8"?>
<sv:node xmlns:sv="http://www.jcp.org/jcr/sv/1.0" sv:name="dev-local">
  <sv:property sv:name="jcr:primaryType" sv:type="Name">
    <sv:value>hst:virtualhostgroup</sv:value>
  </sv:property>
  <sv:property sv:name="hst:cmslocation" sv:type="String">
    <sv:value>http://cms.local.dev</sv:value>
  </sv:property>
  <sv:node sv:name="dev">
    <sv:property sv:name="jcr:primaryType" sv:type="Name">
      <sv:value>hst:virtualhost</sv:value>
    </sv:property>
    <sv:node sv:name="local">
      <sv:property sv:name="jcr:primaryType" sv:type="Name">
        <sv:value>hst:virtualhost</sv:value>
      </sv:property>
      <sv:node sv:name="site">
        <sv:property sv:name="jcr:primaryType" sv:type="Name">
          <sv:value>hst:virtualhost</sv:value>
        </sv:property>
        <sv:property sv:name="hst:showcontextpath" sv:type="Boolean">
          <sv:value>false</sv:value>
        </sv:property>
        <sv:property sv:name="hst:showport" sv:type="Boolean">
          <sv:value>false</sv:value>
        </sv:property>
        <sv:node sv:name="hst:root">
          <sv:property sv:name="jcr:primaryType" sv:type="Name">
            <sv:value>hst:mount</sv:value>
          </sv:property>
          <sv:property sv:name="hst:channelpath" sv:type="String">
            <sv:value>/hst:hst/hst:channels/myhippoproject</sv:value>
          </sv:property>
          <sv:property sv:name="hst:mountpoint" sv:type="String">
            <sv:value>/hst:hst/hst:sites/myhippoproject</sv:value>
          </sv:property>
        </sv:node>
      </sv:node>
    </sv:node>
  </sv:node>
</sv:node>

Now within this configuration there are a couple of important definitions that differ from the 'default' setup. For the dev-local virtual host group, we define the CMS to be at the location http://cms.local.dev (see the hst:cmslocation property). This property is used for instance by links produced by the HST, which will point directly to the CMS.

The other two important properties are hst:showcontextpath and hst:showport. The hst:showcontextpath property is set explicitly set to false, which means links generated by the HST will not contain the web application context path. So in our case the /site/ part of the URL will be omitted, which was exactly what was breaking our styling. So now if we save our changes in the CMS Console and refresh the browser it should look like:

//onehippo-prod.global.ssl.fastly.net/binaries/ninecolumn/content/gallery/connect/labs/deployment/capturfiles-apr-21-2015_14.01.32.png

Conclusion

As you've seen it's not that hard to put Nginx in front of Hippo CMS. I've seen Nginx being used in combination with Hippo CMS for other scenarios as well (like SSL offloading). Setting up Nginx is not a lot of work except for setting the correct headers and the right proxy configuration. I hope that this post will help out others trying to set up Hippo CMS with Nginx. In case you see something missing are are running into issues please leave a comment.

Want to see for yourself what it's like to build on Hippo CMS?