Heroku is different from your typical Rails setup. While your typical Rails setup consists of a load balancer and app servers, there’s no way to access the load balancer, the Heroku router, on Heroku. A request on Heroku hits the SSL endpoint first, goes through the Heroku Router, and hits your application directly.
Aside from balancing requests between your app servers, a load balancer can do a few more things these days. You want them to terminate your SSL connections, enforce domain/SSL redirects, or set some custom headers before sending the request back to your users. Not having access to the load balancer means that these features are forced into your application code.
Usage at Codeship
At Codeship, we run a few middleware to get back some of this functionality:
Our documentation is hosted as a static site on AWS S3. We used rack-reverse-proxy to proxy request from codeship.com/documentation to AWS S3.
We used heroku-deflater to serve precompressed Rails assets.
We used a custom middleware to redirect requests from codeship.io to codeship.com.
We used a custom middleware to set custom HTTP headers for font assets.
All these things don't belong in your application; the responsibility of such things lies in the load balancer.
Installing Nginx on Heroku
Since we don't have access to the Heroku Router, the Dyno is the only place to run our own load balancer. The standard Heroku setup lets you run just one command per Dyno. There are a few tools out there, however, to circumvent that limitation.
You can run multiple processes and applications in your Dyno by using the Heroku Multi Buildpack in combination with the Heroku Runit Buildpack. This allows you to specify any amount of buildpacks to run on one Dyno. Don't try to stretch it too far though; you have limited resources on each dyno.
At Codeship, we are currently using four buildpacks in production: Heroku Runit Buildpack to run multiple processes on each Dyno. Nginx Buildpack to proxy web requests. NodeJS Buildpack to run StatsD alongside on each Dyno. Ruby Buildpack to run our Ruby on Rails application.
Setting Up Nginx on Heroku
Configuring Nginx is pretty straightforward. There are only a few things that are different on Heroku. For example, in combination with the Runit Buildpack, you can't run Nginx as a daemon see nginx.conf:1. We choose to use the l2met log format to stay in line with the Heroku style of logging see nginx.conf:23.
Heroku assumes that your application is ready to consume requests by checking the port on which the application server is running. Since we now have two independent pieces, Nginx and Puma, we need to tell Nginx that it should only listen on the public port after Puma has finished booting. The start-nginx script from the Nginx Buildpack will wait until the /tmp/app-initialized file is present before Nginx binds to the public port.
We need to tell Puma to create /tmp/app-initialized after it’s ready to accept connection. To achieve that, you have to add the following snippet to your puma.rb:
on_worker_fork do FileUtils.touch('/tmp/app-initialized') end
To get a full overview, I set up a sample application that’s pretty close to what we’re running in production. You can find it over at GitHub. I'm happy to take pull requests in order to improve the sample application. Also, feel free to check out our production Nginx config. If you’d like to know why we chose Puma in the first place, you’ll want to check out our blog post on that.
By running Nginx on each Dyno, we were able to remove four middleware from our production stack without losing any functionality. We even gained more flexibility that enables us, for example, to add more redirects very easily. Our setup is now closer to a standard Rails setup and is less dependent on how Heroku works.
So how do you deal with redirects in your applications? I would love to know if there are better ways to do this.