Digital Ocean: Ubuntu, Nginx, Unicorn, Rails

UPDATE

I ran this setup from August 2014 to December 2014. It served me pretty well. However, I’ve since moved it back to Heroku.

Honestly, the setup was fun and I certainly enjoyed the smaller bill, but keeping up with security patches, the cost of server monitoring, worrying about firewalls, etc. just got to be a pain.

That being said, if you’re into the Ops side of things, I hope this post serves you well.


I recently decided to move SproutMark off to a VPS. I’d heard great things about Digital Ocean, so decided to make the switch. Although there are a handful of tutorials out there (this one was VERY helpful) on how to setup the Nginx/Unicorn/Rails stack, none had everything I needed. So here are my learnings and experience on how I got set up on Digital Ocean.

Here’s what we’ll be covering

  • Ubuntu
  • Nginx
  • Unicorn
  • Rbenv
  • Rails 4.1
  • PostgreSQL
  • Redis
  • Memcache
  • Mina (for deployments)

Get That VPS Built

  1. Sign Up for Digital Ocean
  2. Click “Create Droplet”
  3. Choose your size. A typical Rails app can run about 150-200mb per process. Multiply that by how many Unicorn processes you want to run to figure out how much RAM you need. For my little app, I’m running 2 unicorns + Sidekiq and am hovering around 800mb so the 1GB plan works fine for me.
  4. Under “Select Image”, choose “Ubuntu 14.04 x64”
  5. Click “Create Droplet”

That’s it? Yes. Well, yeah, so far.

Set Up A User For Deployments

It’s usually not good practice to use your root user to handle deployments, so let’s first start by adding a “deployer” user.

$ ssh root@YOUR_NEW_DROPLET
$ sudo adduser deployer
$ su deployer

SSH Keys

Stop using that root password. Setup and upload an SSH key.

Digital Ocean has a fine document on creating a SSH key and getting it installed. The only part I’d never seen before was ssh-copy-id. It’s quite handy and easy to install with Homebrew.

Rbenv

We all have fond memories of RVM. Well, I have some fond memories, but mostly memories or RVM acting crazy and putting crap all over my system. “Is there a better way?”, you ask. Yes. Yes, there is. Please use Rbenv. Let’s get it installed for your deployer user.

First, some dependencies:

$ sudo apt-get update
$ sudo apt-get install curl git-core build-essential zlib1g-dev libssl-dev libreadline-dev libyaml-dev libsqlite3-dev sqlite3 libcurl4-openssl-dev libxml2-dev libxslt1-dev python-software-properties

Now, we’ll install rbenv into your home directory and add some commands to your .bashrc for completution and shims.

$ git clone https://github.com/sstephenson/rbenv.git ~/.rbenv
$ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc
$ echo 'eval "$(rbenv init -)"' >> ~/.bashrc

Now, let’s restart the shell and make sure Rbenv is install:

$ exec $SHELL
$ type rbenv
#=> "rbenv is a function"

Time to install Ruby!

$ git clone https://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build
$ rbenv install 2.1.2
$ rbenv global 2.1.2

This part may take awhile. Go grab an apple.

Ok, done?

Let’s make sure all is well…

$ ruby -v
ruby 2.1.2p95 (2014-05-08 revision 45877) [x86_64-linux]

If it’s not working, you’ll probably see this:

$ ruby -v
The program 'ruby' can be found in the following packages:
 * ruby
 * ruby1.8
Try: apt-get install <selected package>

^ That’s not good. Re-visit the instructions above and don’t proceed until you get Ubuntu figuring out where Ruby is.

One last step that always trips people up: install Bundler real quick.

$ gem install bundler

You’ll thank me later…

PostgreSQL

Use MySQL if you must, but I ain’t helping you there. Let’s set up PostgreSQL.

Don’t try and get fancy with other DB users. Just use the built-in ‘postgres’ user.

$ sudo apt-get install postgresql postgresql-contrib
$ createuser --pwprompt

Create your database:

$ su postgres
$ psql
psql (9.3.5)
Type "help" for help.

postgres=# CREATE DATABASE yourapp_production;

Memcache, and Redis

These two are a piece of cake. Thanks, ‘apt-get’

Memcache

$ sudo apt-get install memcached

And Redis (for Sidekiq and other fun stuff)

$ apt-get install redis-server

Boot ‘er up and make sure you can get to the Redis console:

$ redis-server /etc/redis/redis.conf
$ redis-cli
127.0.0.1:6379>

If you’re just using Redis for Sidekiq, you’re done. However, Redis is a pretty powerful datastore. For more, check out Redis in Action.

Nginx

Here’s the part where I started getting scared. I don’t know Nginx well. There. I said it. I’m not ashamed. I got a big head start using Esther Hazzard-Strong’s post, but still had some problems. Here’s the config I ended up with:

Install Nginx:

$ sudo apt-get install nginx
$ sudo service nginx start

You should now be able to view Nginx’s welcome page by visiting your Droplet’s IP in your browser.

You can also check to make sure Nginx is running:

$ ps ax | grep nginx

Now we can get Nginx set up to point to our (upcoming) Rails install. Let’s open up the nginx.conf file:

nano /etc/nginx/nginx.conf

Here are my settings:

user www-data;
worker_processes 4;
pid /run/nginx.pid;

events {
  worker_connections 768;
}

http {

  ##
  # Basic Settings
  ##

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

  server_name_in_redirect 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";

  ##
  # Virtual Host Configs
  ##

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

So that’s the overall Nginx setup. Now let’s set up the conf file for our Rails site. The cheater way to do this is place a file directly into /etc/nginx/sites-enabled. The better way is to place the file into sites-available and then symlink into sites-enabled.

But I’m a cheater…

nano /etc/nginx/sites-enabled/default

Here’s my setup. I’m running SSL, so there are two server blocks. The “listen 80” block takes any non-http request and redirects to the other server block. The second block is for “listen 443” (SSL). If you don’t want/need SSL, you can remove the first server block and swap out 443 to 80 in the second block. You’ll want to remove the ssl_certificate lines, too.

upstream app {
    # Path to Unicorn SOCK file, as defined previously
    server unix:/home/deployer/YOUR_APP_NAME/shared/sockets/unicorn.sock fail_timeo
ut=0;
}

server {
    listen         80;
    return 301 https://$host$request_uri;
}

server {

    listen 443;

    # Application root, as defined previously
    root /home/deployer/YOUR_APP_NAME/current/public;

    ssl on;
    ssl_certificate /etc/nginx/ssl/SSL.crt;
    ssl_certificate_key /etc/nginx/ssl/YOUR_APP_NAME.key;

    server_name www.YOUR_APP_NAME.com YOUR_APP_NAME.com;

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

    access_log /var/log/nginx/YOUR_APP_NAME_access.log combined;
    error_log /var/log/nginx/YOUR_APP_NAME_error.log;

    location @app {
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header Host $http_host;
        proxy_redirect off;
        proxy_pass https://app;
        proxy_set_header   X-Forwarded-Proto https;  # <-- don't need this if you're not running SSL
    }

    error_page 500 502 503 504 /500.html;
    client_max_body_size 4G;
    keepalive_timeout 10;
}

I probably spent more time on that one file above anything. Thank you, internet, for providing tons of random tutorials on setting that up.

Ok, so Nginx is set up. Time to get into the fun stuff.

If you’re looking for a deep dive into Nginx, check out Mastering Nginx.

Unicorn

This was another one that took me awhile, so I’m excited to share this setup with someone. I’m going to assume you know what Unicorn is and why you should use it. Here’s my unicorn.rb Rails initializer (config/unicorn.rb)

# Set your full path to application.
app_dir = File.expand_path('../../', __FILE__)
shared_dir = File.expand_path('../../../shared/', __FILE__)

# Set unicorn options
worker_processes 2
preload_app true
timeout 30

# Fill path to your app
working_directory app_dir

# Set up socket location
listen "#{shared_dir}/sockets/unicorn.sock", :backlog => 64

# Loging
stderr_path "#{shared_dir}/log/unicorn.stderr.log"
stdout_path "#{shared_dir}/log/unicorn.stdout.log"

# Set master PID location
pid "#{shared_dir}/pids/unicorn.pid"

before_fork do |server, worker|
  defined?(ActiveRecord::Base) and ActiveRecord::Base.connection.disconnect!
  old_pid = "#{server.config[:pid]}.oldbin"
  if File.exists?(old_pid) && server.pid != old_pid
    begin
      sig = (worker.nr + 1) >= server.worker_processes ? :QUIT : :TTOU
      Process.kill(sig, File.read(old_pid).to_i)
    rescue Errno::ENOENT, Errno::ESRCH
      # someone else did our job for us
    end
  end
end

after_fork do |server, worker|
  defined?(ActiveRecord::Base) and ActiveRecord::Base.establish_connection
end

before_exec do |server|
  ENV['BUNDLE_GEMFILE'] = "#{app_dir}/Gemfile"
end

Mina

If you’ve ever struggled with the Capistrano DSL, then you’ll be quite happy when you see Mina. It’s quite lovely. And VERY fast.

Add it to your Gemfile.

gem 'mina'

Let’s also add two other gems that will help us manage Unicorn and Sidekiq

gem 'mina-sidekiq', :require => false
gem 'mina-unicorn', :require => false

Create the necessary “deploy.rb”:

$ mina init
Created config/deploy.rb.

Make sure to add your project’s folder inside ‘/home/deployer’. For instance, ‘/home/deployer/YOUR_APP’.

Let’s tweak that deploy.rb file. Here’s my setup:

require 'mina/bundler'
require 'mina/rails'
require 'mina/git'
require 'mina/rbenv'
require 'mina_sidekiq/tasks'
require 'mina/unicorn'

# Basic settings:
#   domain       - The hostname to SSH to.
#   deploy_to    - Path to deploy into.
#   repository   - Git repo to clone from. (needed by mina/git)
#   branch       - Branch name to deploy. (needed by mina/git)

set :domain, 'YOUR DROPLETS IP'
set :deploy_to, '/home/deployer/YOUR_APP/'
set :repository, 'YOUR GIT REPO URL'
set :branch, 'master'
set :user, 'deployer'
set :forward_agent, true
set :port, '22'
set :unicorn_pid, "#{deploy_to}/shared/pids/unicorn.pid"

# Manually create these paths in shared/ (eg: shared/config/database.yml) in your server.
# They will be linked in the 'deploy:link_shared_paths' step.
set :shared_paths, ['config/database.yml', 'log', 'config/secrets.yml']


# This task is the environment that is loaded for most commands, such as
# `mina deploy` or `mina rake`.
task :environment do
  queue %{
echo "-----> Loading environment"
#{echo_cmd %[source ~/.bashrc]}
}
  invoke :'rbenv:load'
  # If you're using rbenv, use this to load the rbenv environment.
  # Be sure to commit your .rbenv-version to your repository.
end

# Put any custom mkdir's in here for when `mina setup` is ran.
# For Rails apps, we'll make some of the shared paths that are shared between
# all releases.
task :setup => :environment do
  queue! %[mkdir -p "#{deploy_to}/shared/log"]
  queue! %[chmod g+rx,u+rwx "#{deploy_to}/shared/log"]

  queue! %[mkdir -p "#{deploy_to}/shared/config"]
  queue! %[chmod g+rx,u+rwx "#{deploy_to}/shared/config"]

  queue! %[touch "#{deploy_to}/shared/config/database.yml"]
  queue  %[echo "-----> Be sure to edit 'shared/config/database.yml'."]

  queue! %[touch "#{deploy_to}/shared/config/secrets.yml"]
  queue %[echo "-----> Be sure to edit 'shared/config/secrets.yml'."]

  # sidekiq needs a place to store its pid file and log file
  queue! %[mkdir -p "#{deploy_to}/shared/pids/"]
  queue! %[chmod g+rx,u+rwx "#{deploy_to}/shared/pids"]
end

desc "Deploys the current version to the server."
task :deploy => :environment do
  deploy do

    # stop accepting new workers
    invoke :'sidekiq:quiet'

    invoke :'git:clone'
    invoke :'deploy:link_shared_paths'
    invoke :'bundle:install'
    invoke :'rails:db_migrate'
    invoke :'rails:assets_precompile'

    to :launch do
      invoke :'sidekiq:restart'
      invoke :'unicorn:restart'
    end
  end
end

I absolutely love how easy it is to read that file!

Ok, let’s run that setup task to create the necessary folders and files on your Droplet. If one doesn’t get created that you need, no worries. Just SSH back in there and create the folder yourself.

$ mina setup
-----> Creating folders... done.

Set Up database.yml and secrets.yml

Let’s get back on your Droplet and set up your database.yml and secrets.yml files:

$ ssh deployer@YOUR_IP
$ nano /home/deployer/YOUR_APP/shared/config/database.yml

Tweak that file:

production:
  adapter: postgresql
  encoding: unicode
  database: APPNAME_production
  username: postgres
  password: DB_PASSWORD_SET_ABOVE
  host: localhost

…and your secrets.yml…

nano /home/deployer/YOUR_APP/shared/config/secrets.yml
production:
  secret_key_base: RUN `rake secret` TO GENERATE A KEY

Deploying

And finally, deploy!

$ mina deploy
-----> Deploying to 2012-06-12-040248
       ...
       Lots of things happening...
       ...
-----> Done.

Conclusion

That’s it! If you have any questions, leave them in the comments. I want to make this document better for the next guy, so let me know if there’s anything I can improve.

Hi there, I'm Jon.

Writer. Musician. Adventurer. Nerd.

Purveyor of GIFs and dad jokes.