Configure nginx to serve downloads for Redmine

You know, I’m a big fan of nginx. Lately I’ve been using redmine as a project management tool too, and it’s really, really great - I can’t recommend it highly enough! Of course redmine is written in Ruby on Rails, or Rails to be short, but setting up is pretty much non-sweat. Once it’s up and running, you’ll wonder how you could live without it in the past.

Since the webserver running redmine, or Rails to be precise - whatever it is: WEBrick, mongrel, thin, passenger… - is not designed for static file handling, you’ll most likely end up using nginx as a proxy for the upstream Rails webserver, which often listens at port 3000. Assuming you have the directory structure similar to mine, a typical nginx configuration may look like this:

server {
    listen       80;
    server_name  cool.redmine.com;
    error_log /var/www/redmine-2.0.1/log/error.log;
    access_log /var/www/redmine-2.0.1/log/access.log;
    location /(themes|javascripts|stylesheets)$ {
        root /var/www/redmine-2.0.1/public;
    }
    # proxy all other requests to thin webserver
    location / {
        proxy_pass        http://127.0.0.1:3000;
    }
}

This is kindly straightforward: nginx is configured to listen on port 80. Any public requests to the static contents (themes, javascripts, sylesheets) will be served by nginx, directly. All other requests are sent upstream to Rails webserver (thin in this case) listening locally on port 3000. We’re good to go at this point.

“How about the downloads? Shouldn’t we let nginx handle the downloads also? Nginx rocks at it!” you ask. Good question indeed, but not that simple. If we configure nginx that way, all the downloads will be open to public through nginx, which is absolutely not what we want. We want the requests to be authorized by redmine (Rails) first! So with this configuration, Rails will handle the file download requests, authorize them, and send the files on valid authorization, or an error message otherwise.

“OK so let it be. You said we’re good to go? Great, let’s just go then” you say.

Yes. But no, wait.

Indeed, with the configuration above, we’re good to go. But not *that* good. There’s a big problem lying there. And it’s about how Rails handles file downloads. To serve a file download, Rails firstly loads the whole file into memory (read: RAM) and only starts to send chunk by chunk to the client once this loading process is done. It sucks, if you ask. Even worse: the consumed memory will NOT be released, though it may be reused. Now, it sucks by a megaton! If you have a one-gigabyte file, say a Photoshop PSD, your whole server may be dead.

So now to sum up: First, we want all downloads to be authorized by Rails. Second, we want the file transfers to be done by nginx. Seems legit.

The question is, how?

As a both a nginx and Rails newbie, I dug up the whole internet for an answer. Finally, it came, in a form of - wait for it - X-Accel nginx headers. This will be the scenario:

  1. Client (Chrome, Firefox, IE, you name it) sends a request to a download
  2. nginx receives the request
  3. It will then add some indication, literally “This guy asks for a download. Please authorize him. If OK, let me know the where the file is, so that I can serve him.”
  4. It will the pass the request upstream to Rails (thin) as normal
  5. Rails authorizes the request successfully
  6. It will then send an internal request to nginx with the file’s real location
  7. nginx happly serves the file to the lucky user

As simple as it sounds, the very few available tutorials really confused me - they’re written by advanced users, when again I’m a newbie. After hours of trying though, I finally made it. Here is the configuration which works for me:

In /var/www/redmine-2.0.1/config/environments/production.rb, before the ending end I added this line

config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect'

which instructs Rails to detect nginx’s X-Accel-Redirect header and use it instead of native Rails’ send_file. And the above nginx configuration was replaced with this:

server {
    listen       80;
    server_name  cool.redmine.com;
    # same old same old
    error_log /var/www/redmine-2.0.1/log/error.log;
    access_log /var/www/redmine-2.0.1/log/access.log;
    location /(themes|javascripts|stylesheets)$ {
        root /var/www/redmine-2.0.1/public;
    }
    # ! The following two blocks enable nginx to serve downloads instead of Rails !
    location /attachments
    {
        proxy_redirect    off;
        proxy_set_header  X-Sendfile-Type   X-Accel-Redirect;
        proxy_set_header  X-Accel-Mapping   /var/www/redmine-2.0.1/files=/files;
        proxy_pass        http://127.0.0.1:3000;
    }
    location /files {
        root /var/www/redmine-2.0.1/;
        internal;
    }
    # proxy all other requests to thin webserver
    location / {
        proxy_pass        http://127.0.0.1:3000;
    }
}

Two location directives were added: location /attachment and location /files. The first is for public requests (step 1 and 2 in the scenario). The latter is for internal requests (step 6 in the scenario). We don’t want these internal requests to be reachable by the public, hence the internal keyword. Notice these two lines:

proxy_set_header  X-Sendfile-Type   X-Accel-Redirect;
proxy_set_header  X-Accel-Mapping   /var/www/redmine-2.0.1/files=/files;

The first tells Rails that nginx will be serving the file. The second is a map, which can be literally translated into “If this file is located at /var/www/redmine-2.0.1/files, send me an internal request at /files directive.” In our case, the file is indeed at that location, so Rails will request nginx at

location /files {
    root /var/www/redmine-2.0.1/;
    internal;
}

Here, nginx serves the file, beautifully. And that’s how I did it! Let me know if this works for you as well.

You can follow any responses to this entry through the RSS 2.0 feed.