Ruby
Article

Deploy Your Rails App to AWS

By Devdatta Kane

vector illustration of personal computer display showing window with deploy title isolated on white

As developers, we are usually concerned about the development part of any application. We don’t think much about the deployment part as we consider it to be a responsibility of the SysAdmins. But many times, we don’t have a dedicated SysAdmin available, so we have to put on the SysAdmin hat and get things done. There are many options to deploy your Rails application. Today, I will cover how to deploy a Rails application to Amazon Web Services (AWS) using Capistrano.

We will use the Puma + Nginx + PostgreSQL stack. Puma will be the application server, Nginx the reverse proxy, and PostgreSQL is the database server. This stack can be used on MRI Ruby or JRuby as well. Most of the steps remain same for both rubies, but I’ll highlight where they differ as well.

If you have an existing application, you can skip the following section and jump directly to next section.

Sample Rails Application

Let’s create a sample Rails application with a contact model and CRUD. The app uses Rails 4.2 and PostgreSQL:

rails new contactbook -d postgresql

After the application is generated, create a Contact model and CRUD:

cd contactbook
rails g scaffold Contact name:string address:string city:string phone:string email:string

Setup your database username and password in config/database.yml, then create and migrate the database:

rake db:create && rake db:migrate

Let’s check how its working:

rails s

Point your favorite browser to http://localhost:3000/contacts and check if everything is working properly.

Configuring Puma & Capistrano

We will now configure application for deployment. As previously mentioned, Puma is the application server and Capistrano as our deployment tool. Capistrano provides integration for Puma and RVM, so add those gems to the Gemfile. We will also use the figaro gem to save application configuration, such as the production database password and secret key:

gem 'figaro'
gem 'puma'
group :development do
  gem 'capistrano'
  gem 'capistrano3-puma'
  gem 'capistrano-rails', require: false
  gem 'capistrano-bundler', require: false
  gem 'capistrano-rvm'
end

Install the gems via bundler:

bundle install

It’s time to configure Capistrano, first by generating the config file, as follows:

cap install STAGES=production

This will create configuration files for Capistrano at config/deploy.rb and config/deploy/production.rb. deploy.rb is the main configuration file and production.rb contains environment specific settings, such as server IP, username, etc.

Add the following lines into the Capfile, found in the root of the application. The Capfile includes RVM, Rails, and Puma integration tasks when finished:

require 'capistrano/bundler'
require 'capistrano/rvm'
require 'capistrano/rails/assets' # for asset handling add
require 'capistrano/rails/migrations' # for running migrations
require 'capistrano/puma'

Now, edit deploy.rb as follows:

lock '3.4.0'

set :application, 'contactbook'
set :repo_url, 'git@github.com:devdatta/contactbook.git' # Edit this to match your repository
set :branch, :master
set :deploy_to, '/home/deploy/contactbook'
set :pty, true
set :linked_files, %w{config/database.yml config/application.yml}
set :linked_dirs, %w{bin log tmp/pids tmp/cache tmp/sockets vendor/bundle public/system public/uploads}
set :keep_releases, 5
set :rvm_type, :user
set :rvm_ruby_version, 'jruby-1.7.19' # Edit this if you are using MRI Ruby

set :puma_rackup, -> { File.join(current_path, 'config.ru') }
set :puma_state, "#{shared_path}/tmp/pids/puma.state"
set :puma_pid, "#{shared_path}/tmp/pids/puma.pid"
set :puma_bind, "unix://#{shared_path}/tmp/sockets/puma.sock"    #accept array for multi-bind
set :puma_conf, "#{shared_path}/puma.rb"
set :puma_access_log, "#{shared_path}/log/puma_error.log"
set :puma_error_log, "#{shared_path}/log/puma_access.log"
set :puma_role, :app
set :puma_env, fetch(:rack_env, fetch(:rails_env, 'production'))
set :puma_threads, [0, 8]
set :puma_workers, 0
set :puma_worker_timeout, nil
set :puma_init_active_record, true
set :puma_preload_app, false

We will edit the production.rb later, since we don’t know the server IP and other details yet.

Also, create config/application.yml to save any environment specific settings in the development environment. This file is used by the figaro gem to load the settings into environment variables. We will create the same file on the production server as well.

One thing to remember is to exclude config/database.yml and config/application.yml from the Git repository. Both files contain sensitive data which should not be checked into version control for obvious security concerns.

Creating an EC2 Instance

With the application configured and ready for deployment, it’s time to launch a new EC2 instance. Log in to the EC2 Management Console (obviously, you’ll need to sign up for an AWS account):

rails-aws-1

Click ‘Launch Instance’:

rails-aws-2

Select an Amazon Machine Image (AMI). We will be using ‘Ubuntu Server 14.04 LTS’:

rails-aws-3

Select the instance type as per your requirement. I am picking ‘t2.micro’, because it is free/cheap. For a real production server, you’d want to go bigger. Click ‘Next:Configure Instance Details’ to continue.

rails-aws-4

The default settings are good for our tutorial. Click ‘Next: Add Storage’.

rails-aws-5

The default storage is 8GB. Adjust as per your space requirement. Click ‘Next: Tag Instance’

rails-aws-6

Enter instance name. Click ‘Next: Configure Security Group’.

rails-aws-7

Click ‘Add Rule’. Select ‘HTTP’ from ‘Type’. This is required to make nginx server accessible from the Internet. Click ‘Review and Launch’

rails-aws-8

Check if all setting are correct. Click ‘Launch’

rails-aws-9

Select or create a keypair to connect to instance. You HAVE to have the private key on your local box in order to ssh into the EC2 instance. The key should live in your ~/.ssh directory. Click the checkbox ‘I acknowledge that…’ and click ‘Launch Instance’. Wait for the instance to launch.

rails-aws-10

The instance should be in the ‘running’ state. Select the instance and click ‘Connect’.

rails-aws-11

Note down the ‘Public IP’ address (It’s 52.2.139.74 in the screenshot. Yours will be different.). We will need it to connect to the server.

Setup the Server

We have now provisioned the server and it’s time to setup some basic things. First of all, SSH into the server with our selected private key. Replace ‘Devdatta.pem’ with the full path to your private key:

ssh -i "Devdatta.pem" ubuntu@52.2.139.74

You are logged into the brand new server. Update the existing packages first:

sudo apt-get update && sudo apt-get -y upgrade

Create a user named deploy for deploying the application code:

sudo useradd -d /home/deploy -m deploy

This will create the user deploy along with its home directory. The application will be deployed into this directory. Set the password for deploy user:

sudo passwd deploy

Enter password and confirm it. This password will be required by RVM for the Ruby installation. Also, add the deploy user to sudoers as well. Run sudo visudo and paste the following into the file:

deploy ALL=(ALL:ALL) ALL

Save the file and exit.

As we will be using GitHub to host our Git repository, the deploy user will need access to the repository for deployment. As such, we will generate a key pair for that user now:

su - deploy
ssh-keygen

Do not set a passphrase for the key as it will be used as deploy key.

cat .ssh/id_rsa.pub

Copy the output and set as your deploy key on GitHub.

Capistrano will connect to the server via ssh for deployment as the deploy account. Since AWS allows public key authentication only, copy the public key from your local machine to the deploy user account on the EC2 instance. The public key is your default ~/.ssh/id_rsa.pub key, in most cases. On the server:

nano .ssh/authorized_keys

Paste your local public key into the file. Save and exit.

Git is required for automated deployments via Capistrano, so install Git on the server:

sudo apt-get install git

If you are using JRuby, install a Java Virtual Machine (JVM):

sudo apt-get install openjdk-7-jdk

Installing Nginx

First, install Nginx which is our reverse proxy:

sudo apt-get install nginx

Now, configure the default site as our requirement. Open the site config file:

sudo nano /etc/nginx/sites-available/default

Comment out the existing content and paste the following into the file.

upstream app {
  # Path to Puma SOCK file, as defined previously
  server unix:/home/deploy/contactbook/shared/tmp/sockets/puma.sock fail_timeout=0;
}

server {
  listen 80;
  server_name localhost;

  root /home/deploy/contactbook/public;

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

  location / {
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header Host $host;
    proxy_redirect off;
    proxy_http_version 1.1;
    proxy_set_header Connection '';
    proxy_pass http://app;
  }

  location ~ ^/(assets|fonts|system)/|favicon.ico|robots.txt {
    gzip_static on;
    expires max;
    add_header Cache-Control public;
  }

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

Save the file and exit. We have configured nginx as a reverse proxy to redirect HTTP requests to the Puma application server through a UNIX socket. We will not restart nginx just yet, as the application is ready. Let’s install PostgreSQL now.

Installing PostgreSQL

sudo apt-get install postgresql postgresql-contrib libpq-dev

After postgreSQL is installted, create a production database and its user:

sudo -u postgres createuser -s contactbook

Set the user’s password from psql console:

sudo -u postgres psql

After logging into the console, change the password:

postgres=# \password contactbook

Enter your new password and confirm it. Exit the console with \q. It’s time to create a database for our application:

sudo -u postgres createdb -O contactbook contactbook_production

Installing RVM & Ruby

We will use RVM to install our desired Ruby version:

su - deploy
gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3
\curl -sSL https://get.rvm.io | bash -s stable

This will install RVM into the deploy user’s home directory. Logout and login again to load RVM into the deploy user’s shell. Logout with Ctrl+D and login again with su - deploy.

Now, install Ruby:

For using MRI Ruby – rvm install ruby

For JRuby – rvm install jruby

After Ruby is installed, switch to installed version:

rvm use jruby

OR

rvm use ruby

Install bundler:

gem install bundler --no-ri --no-rdoc

Create the directories and files required by Capistrano. We will create the database.yml and application.yml files to store the database settings and other environment specific data:

mkdir contactbook
mkdir -p contactbook/shared/config
nano contactbook/shared/config/database.yml

Paste the following in database.yml:

production:
  adapter: postgresql
  encoding: unicode
  database: contactbook_production
  username: contactbook
  password: contactbook
  host: localhost
  port: 5432

After that, create application.yml

nano contactbook/shared/config/application.yml

and add the following:

SECRET_KEY_BASE: "8a2ff74119cb2b8f14a85dd6e213fa24d8540fc34dcaa7ef8a35c246ae452bfa8702767d19086461ac911e1435481c22663fbd65c97f21f6a91b3fce7687ce63"

Change the secret to a new secret using the rake secret command.

Okay, we’re almost done with the server. Go back to your local machine to start deployment with Capistrano. Edit the config/deploy/production.rb to set the server IP. Open the file and paste the following into the file. Change the IP address to match with your server’s IP:

server '52.2.139.74', user: 'deploy', roles: %w{web app db}

Now let’s start the deployment using Capistrano:

cap production deploy

Since this is the first deployment, Capistrano will create all the necessary directories and files on the server, which may take some time. Capistrano will deploy the application, migrate the database, and start the Puma application server. Now, login to the server and restart nginx so that our new configuration will reloaded:

sudo service nginx restart

Open up the browser and point it to /contacts. The application should be working properly.

rails-aws-12

Wrap Up

Today, we learned how to deploy Rails application on AWS with Capistrano. Our application, being simple, does not use additional services such as background jobs, so I did not cover that today. But installation and configuration of such services may be required for complex applications. But that’s for another day.

Your comments and views are always welcome.

  • Gurur

    Awesome!

  • Dave Porter

    Thanks for the article, most interesting.
    Just wondering what is necessary if you develop locally with sqlite ?
    Thanks, Dave

  • Avinash

    Awesome!!

  • harimutya

    Thanks for the article !!

  • http://mooktakim.com/ Mooktakim Ahmed

    I don’t think puma will start when the server is restarted.
    You need an upstart service for puma.

  • harimutya

    When I run cap production deploy I received following error. How to fix it ? Thanks
    SSHKit::Runner::ExecuteError: Exception while executing as deploy@xx.xxx.xxx.xx: Authentication failed for user deploy@xx.xxx.xxx.xx

    • Christopher Carlson

      Did you add your public key to ~/.ssh/authorized_keys on the server?
      Also run ssh-add private-key on local?

      • harimutya

        yes, I added public key to .ssh/authorized_keys on server. But still I am getting same error. Is anything else I am missing out?

        • Christopher Carlson

          what about the second part? ssh-add?
          pretty sure this is the issue.

        • Sourav Lahoti

          I am facing the same error. Did you solved the issue??

        • 蔡鴻銘

          Make sure you add the private key to .ssh/authorized_keys when logging as user “deploy”.

      • Denis Erofeev

        How to add private key on local machine?

  • earosonheart

    puma.sock file is never created after deploy and ngnix returns 502 because it can’t find sock file. Why could this happen?

    • smergALERT

      Hi, I ran into this issue also.

      I had not changed contactbook to the name of my app in the
      /etc/nginx/sites-available/default file, lines 3 and 10.

  • 김민정

    how do i see the logs? when i go to puma_access_log path, which is
    :puma_access_log, “#{shared_path}/log/puma_error.log”,
    there`s nothing but production.log. and production.log is empty.

  • RobertJoseph

    Incredibly helpful – thank you!

  • http://www.chiragnayyar.com/ Chirag Nayyar

    Hi thank you for this.. Much needed!!!

  • casamia

    I’m get stucked on the same error…. my message is ‘gem install pg -v ‘0.18.4”
    I checked that pg gem is installed in server but still not working.

    • madaarya

      Maybe you can try install `sudo apt-get install libpq-dev` first

  • Rahul C

    Change the nginx configuration for `root` to `/home/deploy/contactbook/current/public`, if anyone is facing issues where the asset fetching throws a 404 not found.

    • Raj Singh Tut

      Thanks Rahul, that did it for me.

  • Rahul C

    Change the nginx configuration for `root` to `/home/deploy/contactbook/current/public`, if anyone is facing issues where the asset fetching throws a 404 not found.

  • Billu Pillu

    I am getting Nginx welcome page ”

    Welcome to nginx!

    If you see this page, the nginx web server is successfully installed and
    working. Further configuration is required.”

    I have followed each step and also changed nginx configuration for `root` to `/home/deploy/contactbook/current/public

    • akhil

      +Billu Pillu Same problem..Found the solution to this?

      • AAlvAAro

        I had the same problem, don’t forget to run: sudo service nginx restart after all the setup is done

  • Gabriel Matos

    Hi!
    Nice tutorial!!!
    I’ve had a problem and figured out how to solve it, but i think you could correct it:
    When you are in nginx config on line: root /home/deploy/contactbook/public;
    you need to actually point to current release of capistrano, like /home/deploy/contactbook/current/public instead,

    Thanks again for the excelent tutorial :)

  • Byron Bustamante

    HELP – cap production deploy

    iMac-de-Byron:contactbook byronbustamante$ cap production deploy
    (Backtrace restricted to imported tasks)
    cap aborted!
    Net::SSH::AuthenticationFailed: Authentication failed for user deploy@54.183.239.39

    Tasks: TOP => rvm:check
    (See full trace by running task with –trace)
    iMac-de-Byron:contactbook byronbustamante$

  • vaibhavgt

    Thank you boss..

  • Bob Van

    Nice job on this, very helpful. Some pitfalls I dealt with are:

    ‘cap’ CLI commands will not work, apparently because of version issues, so I have to precede with ‘bundle exec cap …’

    ‘SSH::AuthenticationFailed’ because I had not added the ubuntu ‘deploy’ user private SSH key to local devbox (vagrant VM) authorized keys via ssh agent. The agent isn’t available unless I run ‘exec ssh-agent bash’. Also when I exit my session and come back, I have to re add the key.

    I had to fix a couple of deprecation warnings (raise_in_transactional_callbacks, serve_static_assets, log_level) as well as install NodeJS to overcome ‘ExecJS::RuntimeUnavailable’.

    ‘PG::ConnectionBad’ occurred because the deployed database.xml doesn’t use the prod un/pwd I set, but has the dbase user name and password the same. I had deviated from this article by making them different. After setting them the same, the cap deploy worked. I also tweaked ‘pg_hba.conf’ to open up access to the dbase so I might connect with an SQL client.

    I’m testing with a simple rails app which has a png in app/assets/images, and another in public/images. Assets precompile of js & css works OK, but images aren’t being served up. One solution is to set ‘serve_static_files’ as true in production.rb, but I’m not supposed to do this since it’s the job of nginx. I’ve seen a lot of posts about this happening on AWS or Heroku and they get into fixing precompile or incorrectly enabling rails to serve the images, but I’m looking for a correct solution.

    Any suggestions on fixing fixing the static images?

    • Bob Van

      FYI, for the missing image issue, I fixed the public/images by adding ‘images’ to the cache-control public list in the xginx site config (assets|fonts|system|images). I don’t know why the png in assets/images doesn’t appear in the current deployment anywhere, but I don’t need that one anyway.

  • Raj Singh Tut

    I didn’t use the figaro gem but instead I relied on secrets.yml. Just make sure you set your environment variables on your server, otherwise you’ll get a cryptic message –> ‘An unhandled lowlevel error occurred. The application logs may have details.’

  • Gerald Stanley Padgett Espinoz

    Help I’m getting this error

    ** Invoke production (first_time)
    ** Execute production
    ** Invoke load:defaults (first_time)
    ** Execute load:defaults
    ** Invoke rvm:hook (first_time)
    ** Execute rvm:hook
    ** Invoke rvm:check (first_time)
    ** Execute rvm:check
    cap aborted!
    Net::SSH::AuthenticationFailed: Authentication failed for user deploy@53.23.5.2

    • Gerald Stanley Padgett Espinoz

      Solve my problem!

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in Ruby, once a week, for free.