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)](https://help.github.com/articles/generating-ssh-keys/) and [How To Set Up SSH Keys (Ocean Digital)](https://www.digitalocean.com/community/tutorials/how-to-set-up-ssh-keys--2). 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 root@euler.webgetinfo.net
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 deployer@euler.webgetinfo.net
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 deployer@euler.webgetinfo.net '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](https://rvm.io/). 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: ```conf 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**: ```ruby root 'pages#index' ``` Add to Gemfile: ```ruby 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)** ```ruby 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)** ```shell #!/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 " 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 git@bitbucket.org: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)** ```ruby 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, "git@bitbucket.org: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: ```yml 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: ```yml 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](https://github.com/JamesDullaghan). He published [this gist](https://gist.github.com/JamesDullaghan/5941259), 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](https://github.com/arthurstomp) 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* me@fschuindt.com