LaunchSchool - An Online School for Developers /

Blog

Setting Up Your Production Server With Nginx and Unicorn

Overview

When it comes time to deploy your Rails app to a production environment, there are myriad options available. This article will explain the nginx/unicorn combo, as deployed on Ubuntu linux. After reading this article, not only will you be able to set up your own server with the unicorn Rails server and the nginx web server, but you’ll also understand many of the available configuration options so that you can adjust your server setup to fit your application properly. While I won’t get into the details of comparing the nginx/unicorn combo with other options, I will give an overview of what each component provides and why it is a solid choice for your production server environment.

Unicorn

In this article, I’ll be using unicorn as the Rails server. There are definitely other options out there. Rails servers such as puma and thin are quite similar and have their own benefits. I’m going to use unicorn because it is a tried and tested tool that can also teach the basics of similar Rails servers.

Unicorn is a “Rack HTTP server for fast clients and Unix”1. Let’s dissect that statement for a moment.

First, it’s a Rack HTTP server. This means that it implements the HTTP protocol for Rack applications. I won’t go into the HTTP protocol, but you can read more about it on wikipedia. Rack is a gem that provides a basic interface for ruby applications to respond to web requests. Basically, rack applications must respond with an array of three elements: an HTTP response code, a hash of headers, and the response body that responds to an each block. Modern versions of Rails run on Rack, which enables servers like unicorn, thin, and puma to work with them directly.

Next, unicorn is a server for fast clients and Unix. What is a fast client? A fast client is another application or server that can interface with the Rack server quickly, without much latency. Unicorn is not good as a standalone server: it wasn’t designed to handle slow requests that occur over network connections. It relies on Nginx or Apache to handle the buffering and queuing of web requests so that it doesn’t have to worry about concurrency and event-driven programming2. Unicorn is basically the glue between nginx and rack, where Nginx is the fast client.

Unicorn has some features, while not necessarily unique to it, that make it ideal for a Rails server. One such feature that comes in handy is the ability to deploy changes to your application without causing downtime. The problem of zero-downtime deployment is often more complex than a simple unicorn no-downtime reload, but for the most part it is all you need. Unicorn is made to handle high-CPU loads, and is usually configured so that there are the same number of worker processes as CPUs.

Nginx

From Nginx’s website, “Nginx [engine x] is an HTTP and reverse proxy server, as well as a mail proxy server…”3 We will be using nginx as a reverse proxy server and as a regular HTTP server. It will serve requests to our Rails HTTP server (unicorn) and deliver assets (images, stylesheets, and javascript files) over HTTP. A couple of things that are great about nginx are that it can handle a lot of concurrent requests in parallel, it’s quite easy to configure compared to its competition, and it has a relatively low memory footprint.

The following sections explain, in a fair amount of detail, how to set up your server with unicorn and nginx.

Prerequisites

This tutorial was written under the assumption that the server used is Ubuntu linux, release 12.04 or 14.04. You will need root or sudo access to the server to complete the setup properly. You should also be familiar with the basics of git and the linux command line.

Step 1: Set up unicorn

To set up your server correctly, you’ll first need to set up your application’s code base. If you’re using bundler (and you should be if your app is written in Ruby), then all you need to do is add the unicorn gem to your Gemfile:

1
2
# Gemfile
gem 'unicorn'

Then run bundle install, or just bundle for short, from your application’s directory:

1
2
# From the command line, in your application's root directory.
bundle

One more recommendation I’d like to make is that you use binstubs. You can set your application up with binstubs by running bundle --binstubs. This will create a bin/ directory in your application root with all of the executables for the gems in your Gemfile. Having binstubs will make development easier: you can set your PATH by add the following line to your ~/.bashrc or ~/.bash_profile:

1
PATH=./bin:$PATH

Then, instead of always prefixing your commands with bundle exec, you can type just the command (e.g. rake instead of bundle exec rake). If you don’t want to change your bash’s PATH variable, you can still use bin/rake, bin/rails, bin/unicorn and the like. This will also become handy when configuring your server’s unicorn init script, as you’ll see later in this article.

Configuring config/unicorn.rb

The unicorn command accepts a configuration file as one of its options. The defaults are pretty sane, but you’ll still need to configure a few things to match your application’s setup. The unicorn config file is written in Ruby and is usually located at config/unicorn.rb within your application’s root folder.

First, it’s helpful to set a variable to your application’s root folder:

1
2
3
# Set the current app's path for later reference. Rails.root isn't available at
# this point, so we have to point up a directory.
app_path = File.expand_path(File.dirname(__FILE__) + '/..')

Next, we’ll want to configure the number of worker processes. Unicorn always runs with one master process that can terminate and start one or more worker processes. A good guideline on how many worker processes to set is to match the number of CPU cores your server has. I like to set a different default for production, but you can do whatever meets your needs here:

1
2
3
# The number of worker processes you have here should equal the number of CPU
# cores your server has.
worker_processes (ENV['RAILS_ENV'] == 'production' ? 4 : 1)

Unicorn can listen on ports and/or sockets. I prefer listening on a socket to remind myself that unicorn works best with fast (non-networked) clients. You can set each worker to listen on a separate port in the after_fork block below for testing.

1
2
3
4
5
6
7
8
# You can listen on a port or a socket. Listening on a socket is good in a
# production environment, but listening on a port can be useful for local
# debugging purposes.
listen app_path + '/tmp/unicorn.sock', backlog: 64

# For development, you may want to listen on port 3000 so that you can make sure
# your unicorn.rb file is soundly configured.
listen(3000, backlog: 64) if ENV['RAILS_ENV'] == 'development'

The default unicorn timeout is 60 seconds, so if your application needs a longer timeout (for generating reports or the like), make sure you set a reasonable timeout here:

1
2
3
4
# After the timeout is exhausted, the unicorn worker will be killed and a new
# one brought up in its place. Adjust this to your application's needs. The
# default timeout is 60. Anything under 3 seconds won't work properly.
timeout 300

Use unicorn’s DSL (Domain-Specific Language) to set the working directory, pid file, and standard in and standard out. We’ll be using the pid file location later on, so take note of how you set it here.

1
2
3
4
5
6
7
8
9
10
11
# Set the working directory of this unicorn instance.
working_directory app_path

# Set the location of the unicorn pid file. This should match what we put in the
# unicorn init script later.
pid app_path + '/tmp/unicorn.pid'

# You should define your stderr and stdout here. If you don't, stderr defaults
# to /dev/null and you'll lose any error logging when in daemon mode.
stderr_path app_path + '/log/unicorn.log'
stdout_path app_path + '/log/unicorn.log'

Finish up the file with some Rails-friendly settings:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Load the app up before forking.
preload_app true

# Garbage collection settings.
GC.respond_to?(:copy_on_write_friendly=) &&
  GC.copy_on_write_friendly = true

# If using ActiveRecord, disconnect (from the database) before forking.
before_fork do |server, worker|
  defined?(ActiveRecord::Base) &&
    ActiveRecord::Base.connection.disconnect!
end

# After forking, restore your ActiveRecord connection.
after_fork do |server, worker|
  defined?(ActiveRecord::Base) &&
    ActiveRecord::Base.establish_connection
end

The unicorn repository has a solid example for your reference: unicorn.conf.rb.4

Test that your unicorn configuration works by running the following from your app’s root directory:

1
2
3
# Remember, your STDOUT and STDERR are probably being directed to a file
# somewhere.
bin/unicorn -c config/unicorn.rb

Use Ctrl-C to exit the process.

Commit and push your changes (Gemfile, Gemfile.lock, config/unicorn.rb, and bin/*) and move on to the next step.

Step 2: Set up Ruby, Git, and Your Database

I won’t beat a dead horse here. There’s already a very nice blog post on how to set up Ruby on your server using rbenv. You’ll also want to set up git (also in that article), and your preferred database. Postgresql setup is explained in that article, but you could just as easily set up MySQL using the following command:

1
sudo apt-get install mysql-server mysql-client libmysqlclient-dev

Step 3: Set up Unicorn Init Script

You’ve already set up your application to work with unicorn, so now set up your server to work with unicorn as well. Unicorn should start automatically when your server (re)boots so that you don’t have to worry about that every time your server restarts. An init script can make managing your unicorn processes much easier. In this section, I will show you how to set up an init script for your unicorn process.

Configuring /etc/init.d/unicorn

The unicorn init script should be located in the /etc/init.d folder, and is usually called unicorn. With such a setup on an Ubuntu server, you’ll be able to run commands like sudo service unicorn upgrade or sudo service unicorn status.

The init script is written in bash, and should start with an INIT INFO section5:

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/sh

# File: /etc/init.d/unicorn

### BEGIN INIT INFO
# Provides:          unicorn
# Required-Start:    $local_fs $remote_fs $network $syslog
# Required-Stop:     $local_fs $remote_fs $network $syslog
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: starts the unicorn web server
# Description:       starts unicorn
### END INIT INFO

The above section tells your server (when we add unicorn to the startup scripts) that the $local_fs, $remote_fs, $network, and $syslog boot facilities must be available before this script can start. Likewise, Required-Stop indicates that this script should be stopped before those facilities to prevent conflicts. It also sets some default start and stop init levels, a short description and a multiline description. You probably shouldn’t touch much in the above settings.

In the next part, you’ll set some variables to help get your app started properly:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# Feel free to change any of the following variables for your app:

# ubuntu is the default user on Amazon's EC2 Ubuntu instances.
USER=ubuntu
# Replace [PATH_TO_RAILS_ROOT_FOLDER] with your application's path. I prefer
# /srv/app-name to /var/www. The /srv folder is specified as the server's
# "service data" folder, where services are located. The /var directory,
# however, is dedicated to variable data that changes rapidly, such as logs.
# Reference https://help.ubuntu.com/community/LinuxFilesystemTreeOverview for
# more information.
APP_ROOT=[PATH_TO_RAILS_ROOT_FOLDER]
# Set the environment. This can be changed to staging or development for staging
# servers.
RAILS_ENV=production
# This should match the pid setting in $APP_ROOT/config/unicorn.rb.
PID=$APP_ROOT/tmp/unicorn.pid
# A simple description for service output.
DESC="Unicorn app - $RAILS_ENV"
# If you're using rbenv, you may need to use the following setup to get things
# working properly:
RBENV_RUBY_VERSION=`cat $APP_ROOT/.ruby-version`
RBENV_ROOT="/home/$USER/.rbenv"
PATH="$RBENV_ROOT/bin:$PATH"
SET_PATH="cd $APP_ROOT && rbenv rehash && rbenv local $RBENV_RUBY_VERSION"
# Unicorn can be run using `bundle exec unicorn` or `bin/unicorn`.
UNICORN="bin/unicorn"
# Execute the unicorn executable as a daemon, with the appropriate configuration
# and in the appropriate environment.
UNICORN_OPTS="-c $APP_ROOT/config/unicorn.rb -E $RAILS_ENV -D"
CMD="$SET_PATH && $UNICORN $UNICORN_OPTS"
# Give your upgrade action a timeout of 60 seconds.
TIMEOUT=60

Once you get the variables set up properly, you should be able to copy and paste the rest of the init script. Note the comments for more information on how things work and why.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# Store the action that we should take from the service command's first
# argument (e.g. start, stop, upgrade).
action="$1"

# Make sure the script exits if any variables are unset. This is short for
# set -o nounset.
set -u

# Set the location of the old pid. The old pid is the process that is getting
# replaced.
old_pid="$PID.oldbin"

# Make sure the APP_ROOT is actually a folder that exists. An error message from
# the cd command will be displayed if it fails.
cd $APP_ROOT || exit 1

# A function to send a signal to the current unicorn master process.
sig () {
  test -s "$PID" && kill -$1 `cat $PID`
}

# Send a signal to the old process.
oldsig () {
  test -s $old_pid && kill -$1 `cat $old_pid`
}

# A switch for handling the possible actions to take on the unicorn process.
case $action in
  # Start the process by testing if it's there (sig 0), failing if it is,
  # otherwise running the command as specified above.
  start)
    sig 0 && echo >&2 "$DESC is already running" && exit 0
    su - $USER -c "$CMD"
    ;;

  # Graceful shutdown. Send QUIT signal to the process. Requests will be
  # completed before the processes are terminated.
  stop)
    sig QUIT && echo "Stopping $DESC" exit 0
    echo >&2 "Not running"
    ;;

  # Quick shutdown - kills all workers immediately.
  force-stop)
    sig TERM && echo "Force-stopping $DESC" && exit 0
    echo >&2 "Not running"
    ;;

  # Graceful shutdown and then start.
  restart)
    sig QUIT && echo "Restarting $DESC" && sleep 2 \
      && su - $USER -c "$CMD" && exit 0
    echo >&2 "Couldn't restart."
    ;;

  # Reloads config file (unicorn.rb) and gracefully restarts all workers. This
  # command won't pick up application code changes if you have `preload_app
  # true` in your unicorn.rb config file.
  reload)
    sig HUP && echo "Reloading configuration for $DESC" && exit 0
    echo >&2 "Couldn't reload configuration."
    ;;

  # Re-execute the running binary, then gracefully shutdown old process. This
  # command allows you to have zero-downtime deployments. The application may
  # spin for a minute, but at least the user doesn't get a 500 error page or
  # the like. Unicorn interprets the USR2 signal as a request to start a new
  # master process and phase out the old worker processes. If the upgrade fails
  # for some reason, a new process is started.
  upgrade)
    if sig USR2 && echo "Upgrading $DESC" && sleep 10 \
      && sig 0 && oldsig QUIT
    then
      n=$TIMEOUT
      while test -s $old_pid && test $n -ge 0
      do
        printf '.' && sleep 1 && n=$(( $n - 1 ))
      done
      echo

      if test $n -lt 0 && test -s $old_pid
      then
        echo >&2 "$old_pid still exists after $TIMEOUT seconds"
        exit 1
      fi
      exit 0
    fi
    echo >&2 "Couldn't upgrade, starting 'su - $USER -c \"$CMD\"' instead"
    su - $USER -c "$CMD"
    ;;

  # A basic status checker. Just checks if the master process is responding to
  # the `kill` command.
  status)
    sig 0 && echo >&2 "$DESC is running." && exit 0
    echo >&2 "$DESC is not running."
    ;;

  # Reopen all logs owned by the master and all workers.
  reopen-logs)
    sig USR1
    ;;

  # Any other action gets the usage message.
  *)
    # Usage
    echo >&2 "Usage: $0 <start|stop|restart|reload|upgrade|force-stop|reopen-logs>"
    exit 1
    ;;
esac

The above script doesn’t include all the possible options for signaling the unicorn master process. See the unicorn signal documentation for more possibilities.

Make sure your init script has permission to be executed:

1
sudo chmod +x /etc/init.d/unicorn

Now, you can run sudo service unicorn start to start your unicorn process. Check your logs (whatever you set stderr_path and stdout_path to in unicorn.rb) to troubleshoot issues. When you’ve updated your code base, you can run sudo service unicorn upgrade to gracefully phase in new workers. To make sure unicorn starts on reboot, update the rc.d startup configuration using the following command:

1
sudo update-rc.d unicorn defaults

This will add a symlink to the unicorn init script in the appropriate /etc/rc.d folders, which are used to determine what starts when.

Using unicorn with RVM or rbenv

The above init script works with rbenv, but you can use RVM just as well. The main difference, from my experience, is that you won’t have to set all the extra RBENV_ variables like I have above. In fact, the following should be sufficent:

1
2
3
4
5
6
7
8
9
USER=ubuntu
APP_ROOT=[PATH_TO_RAILS_ROOT_FOLDER]
RAILS_ENV=production
PID=$APP_ROOT/tmp/unicorn.pid
DESC="Unicorn app - $RAILS_ENV"
UNICORN="$APP_ROOT/bin/unicorn"
UNICORN_OPTS="-c $APP_ROOT/config/unicorn.rb -E $RAILS_ENV -D"
CMD="RAILS_ENV=$RAILS_ENV $UNICORN $UNICORN_OPTS"
TIMEOUT=60

Step 4: Nginx configuration

You should now have unicorn properly configured to serve up your application. To expose your application to the world, you’ll now have to configure and start Nginx.

Installing nginx

Installing nginx on ubuntu is very simple. Use apt-get to install a stable version:

1
sudo apt-get install nginx

If you need the most recent stable nginx build, you can use the nginx download documentation to configure your server.

Configuring /etc/nginx/nginx.conf

The nginx install process on ubuntu creates a folder at /etc/nginx. In this folder you’ll find a few configuration files, along with a sites-available and a sites-enabled folder. The sites-available folder is where you’ll configure your application-specific nginx setup, and /etc/nginx/nginx.conf is most likely where you’ll configure global nginx settings. The following is a sensibly configured nginx.conf.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# Run nginx as www-data.
user www-data;
# One worker process per CPU core is a good guideline.
worker_processes 1;
# The pidfile location.
pid /var/run/nginx.pid;

# For a single core server, 1024 is a good starting point. Use `ulimit -n` to
# determine if your server can handle more.
events {
  worker_connections 1024;
}

http {

  ##
  # Basic Settings
  ##

  sendfile on;
  tcp_nopush on;
  tcp_nodelay off;
  keepalive_timeout 65;
  types_hash_max_size 2048;
  server_tokens off;

  include /etc/nginx/mime.types;
  default_type application/octet-stream;

  ##
  # Logging Settings
  ##

  access_log /var/log/nginx/access.log;
  error_log /var/log/nginx/error.log;

  ##
  # Gzip Settings
  ##

  gzip on;
  gzip_disable "msie6";
  gzip_http_version 1.1;
  gzip_proxied any;
  gzip_min_length 500;
  gzip_types text/plain text/xml text/css
    text/comma-separated-values text/javascript
    application/x-javascript application/atom+xml;

  ##
  # Unicorn Rails
  ##

  # The socket here must match the socket path that you set up in unicorn.rb.
  upstream unicorn {
    server unix:/app/path/tmp/unicorn-staging.sock fail_timeout=0;
  }

  ##
  # Virtual Host Configs
  ##

  include /etc/nginx/conf.d/*.conf;
  include /etc/nginx/sites-enabled/*;
}

As noted in the inline comments, make sure the path to the unicorn socket matches what you set up in config/unicorn.rb.

Configuring /etc/nginx/sites-available/sitename

This is the final step in setting up your server: your application-specific nginx configuration. The following file shows the basics of setting things up.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
server {
  listen 80;
  server_name mysite.com; # Replace this with your site's domain.

  keepalive_timeout 300;

  client_max_body_size 4G;

  root /app/root/public; # Set this to the public folder location of your Rails application.

  try_files $uri/index.html $uri.html $uri @unicorn;

  location @unicorn {
          proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
          proxy_set_header Host $http_host;
          proxy_set_header X-Forwarded_Proto $scheme;
          proxy_redirect off;
          # This passes requests to unicorn, as defined in /etc/nginx/nginx.conf
          proxy_pass http://unicorn;
          proxy_read_timeout 300s;
          proxy_send_timeout 300s;
  }

  # You can override error pages by redirecting the requests to a file in your
  # application's public folder, if you so desire:
  error_page 500 502 503 504 /500.html;
  location = /500.html {
          root /app/root/public;
  }
}

To enable the site you just configured, all you have to do is add a symlink to the configured file into the sites-enabled folder, then reload nginx.

1
2
3
4
5
6
7
8
9
# Enter the sites-enabled folder
cd /etc/nginx/sites-enabled

# Add a symlink to your configuration file
sudo ln -s ../sites-available/sitename

# Reload nginx. Make sure to use the `reload` action so that nginx can check
# your configuration before reloading, thereby saving you from causing downtime.
sudo service nginx reload

Make sure all your processes are running:

1
2
sudo service unicorn status
sudo service nginx status

And you’re done!

Conclusion

You should now have a good idea of how to set up your production Rails server to run on unicorn and nginx. I’ll list the steps here for your reference:

  1. Prepare your Rails app:
    • Add the unicorn gem to your Gemfile.
    • Configure unicorn for your Rails app by adding and modifying config/unicorn.rb
  2. Install ruby and other dependencies on your server.
  3. Configure the unicorn init script on your server (/etc/init.d/unicorn).
  4. Install and configure nginx (/etc/nginx/nginx.conf and /etc/nginx/sites-available/sitename).
  5. Confirm everything is running, and you’re done!

References and Resources

  1. http://unicorn.bogomips.org/
  2. http://unicorn.bogomips.org/PHILOSOPHY.html
  3. http://wiki.nginx.org/Main
  4. https://github.com/defunkt/unicorn/blob/v4.8.3/examples/unicorn.conf.rb
  5. https://wiki.debian.org/LSBInitScripts