Skip to content

Instantly share code, notes, and snippets.

@epipheus
Last active August 29, 2015 14:28
Show Gist options
  • Save epipheus/3092db55bdcf302c9910 to your computer and use it in GitHub Desktop.
Save epipheus/3092db55bdcf302c9910 to your computer and use it in GitHub Desktop.

Setting Up an Multi-Applications Rails Server on Ubuntu

Using RVM, PostgreSQL, NGINX, Unicorn and Capistrano

Today I will cover how to setup a multi-application Ruby on Rails server with RVM, NGINX, Unicorn, Capistrano and PostgreSQL on a fresh virtual private server running Linux Ubuntu Server 14.04. I will be using Rails 4.2.0 new applications as examples.

My current company asked me to configure this setup, they're using the IBM's Softlayer as the main datacenter, so the server that I am playing with is a Softlayer. Nothing special about it, but in this case I have decided to don't use Chef or anything like that, but to make the entire server by hand, because it's funnier, right? Let's go.

####Creating the first user and SSH security configurations

By default I have only the root user and a plain text password SSH access. I need to fix that.
If you don't know what is it or how to create an SSH Key you should read: Generating SSH keys (GitHub) and How To Set Up SSH Keys (Ocean Digital).

But if you already have one let's move on.
My server IP address is 158.85.44.54 named (euler) and I defined in the company's cloud servers domain to point him with: euler.webgetinfo.net. Let's connect:

wizard:~ fschuindt$ ssh [email protected]
The authenticity of host 'euler.webgetinfo.net (158.85.44.54)' can't be established.
RSA key fingerprint is 18:81:09:56:55:5e:d8:a9:83:07:c6:ba:6e:46:6f:91.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added 'euler.webgetinfo.net,158.85.44.54' (RSA) to the list of known hosts.
Password:
Welcome to Ubuntu 14.04.1 LTS (GNU/Linux 3.13.0-45-generic i686)

 * Documentation:  https://help.ubuntu.com/

The programs included with the Ubuntu system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.

root@euler:~#

The first step is create an Unix user. I don't want to keep using root (nobody wants). So I added the user 'deployer'.

root@euler:~# adduser deployer
Adding user `deployer' ...
Adding new group `deployer' (1000) ...
Adding new user `deployer' (1000) with group `deployer' ...
Creating home directory `/home/deployer' ...
Copying files from `/etc/skel' ...
Enter new UNIX password:
Retype new UNIX password:
passwd: password updated successfully
Changing the user information for deployer
Enter the new value, or press ENTER for the default
	Full Name []: Euler Deployer
	Room Number []:
	Work Phone []:
	Home Phone []:
	Other []:
Is the information correct? [Y/n] Y

Now I will grant root privileges to my new user:

root@euler:~# visudo

Edit this section adding the user to the list like this:

# User privilege specification
root ALL=(ALL:ALL) ALL 
deployer ALL=(ALL:ALL) ALL

Then save: Ctrl+O. And close: Ctrl+X.
It's good for now. I will switch to my user.

root@euler:~# exit
logout
Connection to euler.webgetinfo.net closed.
wizard:~ fschuindt$ ssh [email protected]
Password:
Welcome to Ubuntu 14.04.1 LTS (GNU/Linux 3.13.0-45-generic i686)

 * Documentation:  https://help.ubuntu.com/

The programs included with the Ubuntu system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.

deployer@euler:~$

After log in for the first time, is a good idea to update the system, so:

deployer@euler:~$ sudo apt-get update
deployer@euler:~$ sudo apt-get upgrade

As I don't want to keep using SSH with password authentication, I uploaded my public key (from my local notebook) '~/.ssh/id_rsa.pub' to the server.

On euler.webgetinfo.net:

deployer@euler:~$ mkdir .ssh
deployer@euler:~$ touch .ssh/authorized_keys

My notebook (wizard is my notebook's name):

wizard:~ fschuindt$ cat ~/.ssh/id_rsa.pub | ssh [email protected] 'cat >> .ssh/authorized_keys'

Now my public key string is recorded in the server '/home/deployer/.ssh/authorized_keys'. That's the place where the SSH server will check for SSH keys authentication with the user 'deployer'.

We should configure the SSH server to don't accept plain text password logins anymore, only SSH keys. Once my public key is located in the authorized_keys file, the OpenSSH will automatically uses it, but password authentications still allowed, we just need to edit the sshd_config file.

$ sudo nano /etc/ssh/sshd_config

Then find the section:

PermitRootLogin yes

Change to:

PermitRootLogin no

And the section:

ChallengeResponseAuthentication yes

To:

ChallengeResponseAuthentication no

Now the section:

# Change to no to disable tunnelled clear text passwords
# PasswordAuthentication yes

Change to:
(Don't forget to uncomment!)

# Change to no to disable tunnelled clear text passwords
PasswordAuthentication no

Save, close and finally:

$ sudo service ssh restart

Now you can only login in the SSH with the private key, so keep an backup of this key in a safe place.
I also disabled login as root. It's a much more safe configuration.

####Installing RVM, Ruby and Rails

The Ruby Version Manager (RVM) is a great way to get Ruby installed in a Unix like system. It's pretty and easy. To do it we need to install the curl and then uses it to install the RVM itself. Using the commands:

$ sudo apt-get install curl
$ gpg --keyserver hkp://keys.gnupg.net --recv-keys D39DC0E3
$ \curl -sSL https://get.rvm.io | bash -s stable
$ source /home/deployer/.rvm/scripts/rvm

That's the default way to get RVM full installed, you can read more at RVM Webpage.
I will pick the Ruby 2.2.0, which is pretty fine nowadays. Install it, set as default Ruby and also install the latest Rubygems:

$ rvm install 2.2.0
$ rvm use 2.2.0 --default
$ rvm rubygems current

And of course, install the Ruby on Rails:

$ gem install rails --no-ri --no-rdoc

There are also some good packages to ensure installation in a Rails production environment:

$ sudo apt-get install build-essential bison openssl libreadline6 libreadline6-dev git-core zlib1g zlib1g-dev libssl-dev libyaml-dev libsqlite3-0 libsqlite3-dev sqlite3 libxml2-dev autoconf libxslt-dev nodejs imagemagick libmagickwand-dev

That is enough by now.

####The PostgreSQL

We need to install and do some changes with PostgreSQL in order to make it work properly with a Rails application.
First we install the following packages:

$ sudo apt-get install postgresql postgresql-server-dev-9.3 libpq-dev

Next install the pg gem:

$ gem install pg -- --with-pg-config=/usr/bin/pg_config

Now I will create a postgres user. Many developers create one user per app, I prefer just one user for all. And again I will name it deployer:

$ sudo -u postgres psql
# create user deployer with password 'YourPasswordHere';

Now grant good privileges to it:

# alter role deployer superuser createrole createdb replication;

After that PostgreSQL will run just fine. To quit the psql just:

# \q

####The NGINX

First, let's configure the bundler:

$ gem install bundler
$ mkdir ~/.bundle
$ nano ~/.bundle/config

Paste the following:

BUNDLE_PATH: vendor/bundle

Save and exit.

Now install and start NGINX:

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

####The applications and pre-deployment

For this post I will create two new Rails apps as example, and deploy them both using Capistrano. Capistrano synchronizes with a Git repository to make the deployment in the server, so it needs to have the app in a remote repository too.

Once it's a company's server, I will use the official Team Account in the company's Bitbucket as the main Git account in the server. The Capistrano will access the repositories from the server, so I have to allow my server's SSH key into the Team Account at the Bitbucket.

But first, I need to create a SSH Key Pair in my server. Again, if you don't know about SSH Keys or how to create them, read the tutorials linked in the section "Creating the first user and SSH security configurations". I will just run the ssh-keygen program in the server.

After it's created, I can see the id_rsa and id_rsa.pub files in my ~/.ssh folder.

deployer@euler:~$ cd .ssh/
deployer@euler:~/.ssh$ ls
authorized_keys  id_rsa  id_rsa.pub

Nice. I will copy the id_rsa.pub content and add it as a new SSH Key in the company's Bitbucket Team Account with the label "deployer@euler". Now the server must be able to access the company's Bitbucket repositories.

Outside the server, in my notebook, I will create two applications:

wizard:Desktop fschuindt$ rails new app1
wizard:Desktop fschuindt$ rails new app2

And create two DNS records in the domain, pointing to the server's IP to get two URLs.

app1.webgetinfo.net.br -> 158.85.44.54 - 14400
app2.webgetinfo.net.br -> 158.85.44.54 - 14400

###The deployment

Let's configure the apps in order to deploy. Note that everything I will do now on must be done for each app you pretend to deploy. In this post I will only show the app1 configuration, you must do the same in the app2 or any other app you like, just changing the app's name when it's needed. Take care.

First step is add the NGINX configurations to app1 in the server:

deployer@euler:~$ sudo nano /etc/nginx/nginx.conf

Look inside the block 'http', in the end, after the line: 'include /etc/nginx/sites-enabled/*;', you should add this code for each app you want to deploy:

upstream app1 { server unix:/home/deployer/unicorn_sockets/unicorn.app1.sock fail_timeout=0; }

server {
  listen 80;
  server_name app1.webgetinfo.net;
  root /home/deployer/app1/current/public;

  location ^~ /assets/ {
    gzip_static on;
    expires max;
    add_header Cache-Control public;
  }

  try_files $uri/index.html $uri @unicorn;
  location @unicorn {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_redirect off;
    proxy_pass http://app1;
  }

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

Don't forget to edit where is needed! App name, DNS, etc.

Save and close.

If you want to add other app, like the app2, you just need to duplicate this inside the http block of the nginx.conf file.

Restart the NGINX:

deployer@euler:~$ sudo service nginx restart

Create the sockets directory:
(Only do this in the first app)

deployer@euler:~$ mkdir unicorn_sockets

And now create an view and a root url for the new apps:
(local notebook)

wizard:Desktop fschuindt$ cd app1/
wizard:app1 fschuindt$ rails g controller Pages index
      create  app/controllers/pages_controller.rb
       route  get 'pages/index'
      invoke  erb
      create    app/views/pages
      create    app/views/pages/index.html.erb
      invoke  test_unit
      create    test/controllers/pages_controller_test.rb
      invoke  helper
      create    app/helpers/pages_helper.rb
      invoke    test_unit
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/pages.coffee
      invoke    scss
      create      app/assets/stylesheets/pages.scss

Edit the "app/views/pages/index.html.erb" to show "app1" in the title instead of "Pages#index". In this way we will know that's is the app1 and not the app2.

Set it as the root url in your config/routes.rb:

root 'pages#index'

Add to Gemfile:

gem 'pg'
gem 'unicorn'
gem 'capistrano'
gem 'rvm-capistrano', require: false

Also move the sqlite3 gem to inside the "group :development, :test do" block. It ensures that we'll use PostgreSQL in production and SQLite in test and development.

And now bundle install:

wizard:app1 fschuindt$ bundle install

Create the config/unicorn.rb, config/unicorn_init.sh files and set the executable permission:

wizard:app1 fschuindt$ touch config/unicorn.rb
wizard:app1 fschuindt$ touch config/unicorn_init.sh
wizard:app1 fschuindt$ chmod +x config/unicorn_init.sh

Add to config/unicorn.rb: (Again, change the app name)

root = "/home/deployer/app1/current"
working_directory root
pid "#{root}/tmp/pids/unicorn.pid"
stderr_path "#{root}/log/unicorn.log"
stdout_path "#{root}/log/unicorn.log"

listen "/home/deployer/unicorn_sockets/unicorn.app1.sock"
worker_processes 2
timeout 30

# Force the bundler gemfile environment variable to
# reference the capistrano "current" symlink
before_exec do |_|
  ENV["BUNDLE_GEMFILE"] = File.join(root, 'Gemfile')
end

And add to the config/unicorn_init.sh:
(Again, change to fit your app needs)

#!/bin/sh
### BEGIN INIT INFO
# Provides:          unicorn
# Required-Start:    $remote_fs $syslog
# Required-Stop:     $remote_fs $syslog
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: Manage unicorn server
# Description:       Start, stop, restart unicorn server for a specific application.
### END INIT INFO
set -e

# Feel free to change any of the following variables for your app:
TIMEOUT=${TIMEOUT-60}
APP_ROOT=/home/deployer/app1/current
PID=$APP_ROOT/tmp/pids/unicorn.pid
CMD="cd $APP_ROOT; bundle exec unicorn -D -c $APP_ROOT/config/unicorn.rb -E production"
AS_USER=deployer
set -u

OLD_PIN="$PID.oldbin"

sig () {
  test -s "$PID" && kill -$1 `cat $PID`
}

oldsig () {
  test -s $OLD_PIN && kill -$1 `cat $OLD_PIN`
}

run () {
  if [ "$(id -un)" = "$AS_USER" ]; then
    eval $1
  else
    su -c "$1" - $AS_USER
  fi
}

case "$1" in
start)
  sig 0 && echo >&2 "Already running" && exit 0
  run "$CMD"
  ;;
stop)
  sig QUIT && exit 0
  echo >&2 "Not running"
  ;;
force-stop)
  sig TERM && exit 0
  echo >&2 "Not running"
  ;;
restart|reload)
  sig HUP && echo reloaded OK && exit 0
  echo >&2 "Couldn't reload, starting '$CMD' instead"
  run "$CMD"
  ;;
upgrade)
  if sig USR2 && sleep 2 && sig 0 && oldsig QUIT
  then
    n=$TIMEOUT
    while test -s $OLD_PIN && test $n -ge 0
    do
      printf '.' && sleep 1 && n=$(( $n - 1 ))
    done
    echo

    if test $n -lt 0 && test -s $OLD_PIN
    then
      echo >&2 "$OLD_PIN still exists after $TIMEOUT seconds"
      exit 1
    fi
    exit 0
  fi
  echo >&2 "Couldn't upgrade, starting '$CMD' instead"
  run "$CMD"
  ;;
reopen-logs)
  sig USR1
  ;;
*)
  echo >&2 "Usage: $0 <start|stop|restart|upgrade|force-stop|reopen-logs>"
  exit 1
  ;;
esac

Setup Capistrano:

wizard:app1 fschuindt$ capify .

At this point I will create the app repository locally and in the Bitbucket. Once it's important to don't have the config/database.yml in your version control, let's copy it as example and add it to the .gitignore file. The same to the config/secrets.yml file. Rails encourage us to use environment variables to set the production secret_key_base, but I have experienced some issues with this approach together with Unicorn, so let's just ignore it in the Git and add it later by hand. If you are using OS X as your development environment is good to ignore the .DS_Store files too, so add to the .gitignore the following lines:

/config/database.yml
/config/secrets.yml
.DS_Store

Copy the database.yml and secrets.yml to be versioned as example only:

wizard:app1 fschuindt$ cp config/database.yml config/database.example.yml
wizard:app1 fschuindt$ cp config/secrets.yml config/secrets.example.yml

Now the repository, using the remote url given by the Bitbucket:

wizard:app1 fschuindt$ git init
wizard:app1 fschuindt$ git add .
wizard:app1 fschuindt$ git commit -m "first commit"
wizard:app1 fschuindt$ git remote add origin [email protected]:GetinfoDevTeam/app1.git

Add to config/deploy.rb, remove what was previously inside and replace for:
(There's some important variables such your server address, repository url, etc. Replace them if you are not using the recently created app1)

require "bundler/capistrano"
require "rvm/capistrano"

server "euler.webgetinfo.net", :web, :app, :db, primary: true

set :application, "app1"
set :user, "deployer"
set :port, 22
set :deploy_to, "/home/deployer/#{application}"
set :deploy_via, :remote_cache
set :use_sudo, false

set :scm, "git"
set :repository, "[email protected]:GetinfoDevTeam/app1.git"
set :branch, "master"


default_run_options[:pty] = true
ssh_options[:forward_agent] = true

after "deploy", "deploy:cleanup" # keep only the last 5 releases

namespace :deploy do
  %w[start stop restart].each do |command|
    desc "#{command} unicorn server"
    task command, roles: :app, except: {no_release: true} do
      run "/etc/init.d/unicorn_#{application} #{command}"
    end
  end

  task :setup_config, roles: :app do
    sudo "ln -nfs #{current_path}/config/nginx.conf /etc/nginx/sites-enabled/#{application}"
    sudo "ln -nfs #{current_path}/config/unicorn_init.sh /etc/init.d/unicorn_#{application}"
    run "mkdir -p #{shared_path}/config"
    put File.read("config/database.example.yml"), "#{shared_path}/config/database.yml"
    put File.read("config/secrets.example.yml"), "#{shared_path}/config/secrets.yml"
    puts "Now edit the config files in #{shared_path}."
  end
  after "deploy:setup", "deploy:setup_config"

  task :symlink_config, roles: :app do
    run "ln -nfs #{shared_path}/config/database.yml #{release_path}/config/database.yml"
    run "ln -nfs #{shared_path}/config/secrets.yml #{release_path}/config/secrets.yml"
  end
  after "deploy:finalize_update", "deploy:symlink_config"

  desc "Make sure local git is in sync with remote."
  task :check_revision, roles: :web do
    unless `git rev-parse HEAD` == `git rev-parse origin/master`
      puts "WARNING: HEAD is not the same as origin/master"
      puts "Run `git push` to sync changes."
      exit
    end
  end
  before "deploy", "deploy:check_revision"
end

Now to the Capfile, again remove what was previously inside and replace for:

load 'deploy'
load 'deploy/assets'
load 'config/deploy'

Commit and push:

wizard:app1 fschuindt$ git commit -am "deployment configurations"
wizard:app1 fschuindt$ git push origin master

Start the deploy:

wizard:app1 fschuindt$ cap deploy:setup

Now is time to edit the /home/deployer/app1/shared/config/database.yml file on the server, and setup PostgreSQL credentials.

Like:

production:
  adapter: postgresql
  encoding: utf8
  database: app1_production
  username: deployer
  password: your-password-here
  host: localhost

You should also edit the /home/deployer/app1/shared/config/secrets.yml. You need to put your app secret production key.

In this case it's ok to have the raw secret_key_base in the file because it's not versioned (The same to the database password in the database.yml). Use the "rake secret" in your local computer to create a key, once you have it, edit your /home/deployer/app1/shared/config/secrets.yml production block like this:

production:
  secret_key_base: your-generated-key-here

Just save and close.
Now create the production database on the server:

deployer@euler:~$ sudo -u postgres psql
[sudo] password for deployer:
psql (9.3.6)
Type "help" for help.
postgres=# create database app1_production with owner deployer encoding='UTF-8' lc_collate='en_US.utf8' lc_ctype='en_US.utf8' template template0;
CREATE DATABASE
postgres=# \q

Now go:

wizard:app1 fschuindt$ cap deploy:cold

After, do this in the server:
(The rm command is only needed in the first app setup)

deployer@euler:~$ sudo rm /etc/nginx/sites-enabled/default
deployer@euler:~$ sudo service nginx restart
 * Restarting nginx nginx                                                                                                                                              [ OK ]
deployer@euler:~$ sudo update-rc.d -f unicorn_app1 defaults
 Adding system startup for /etc/init.d/unicorn_app1 ...
   /etc/rc0.d/K20unicorn_app1 -> ../init.d/unicorn_app1
   /etc/rc1.d/K20unicorn_app1 -> ../init.d/unicorn_app1
   /etc/rc6.d/K20unicorn_app1 -> ../init.d/unicorn_app1
   /etc/rc2.d/S20unicorn_app1 -> ../init.d/unicorn_app1
   /etc/rc3.d/S20unicorn_app1 -> ../init.d/unicorn_app1
   /etc/rc4.d/S20unicorn_app1 -> ../init.d/unicorn_app1
   /etc/rc5.d/S20unicorn_app1 -> ../init.d/unicorn_app1

Back to the local computer:

wizard:app1 fschuindt$ cap deploy

It's done! Visit your app URL to check it. Now you can repeat these steps to deploy another app in the same server. You basically can deploy many apps as you like. Hope you guys enjoyed.

####Credits

I want (and must to) say thanks for a guy who I don't know, named James Dullaghan. He published this gist, which I mostly based me on. So thanks him for the code. And also his gist is a good place for looking clues if you're having troubles.

Also thanks to my friend Arthur Rocha who is an Rails enthusiast and helped me a lot revising the text.

That's it. I will keep updating this document for a while. And I pretend to release new ones about Rails/Linux servers administration. The next will be about some routines in this server like database backup, easy methods to configure files and keep track of the logs with no pain.

Check out my personal blog for new releases later on:
https://schuindtlog.wordpress.com/

April 8, 2015 - Fernando Schuindt
[email protected]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment