How to Get a Ruby on Rails App Up and Running on EC2

Who This Tutorial Is For

This tutorial is for developers who are comfortable with Ruby on Rails but have no particular expertise in system administration.

If you’ve never used AWS before and you don’t even know what EC2 is, that’s okay.

What We’re Going To Do

All we’re going to do is get a database-connected Rails application up and running on EC2. This isn’t meant to be the starting point for a production configuration. It’s only meant to be a Rails/EC2 “hello world” so you can see what needs to happen in order to get a Rails application running on EC2.

Here are the steps we’re going to carry out:

  1. Set Up The EC2 Instance
  2. Install Ruby
  3. Install Passenger and NGINX
  4. Install Rails And Create Rails Project
  5. Get Our EC2 Instance To Serve Our Rails Project
  6. Create An RDS Instance
  7. Observe Our Complete Working Application

Prerequisites

All you need in order to be able to follow this tutorial is to have an AWS account. We’re not going to perform any operations locally so it shouldn’t matter if your computer is Mac, Windows or Linux.

Why EC2?

Before I jump into how to deploy a Rails application to EC2, it might make sense to first talk a little bit about why a person would want to use EC2.

It seems to me that there are three prominent options for deploying a Rails app:

  • Heroku
  • VPS
  • AWS

Let’s briefly discuss some pros and cons of each.

Heroku

Heroku is what I personally have worked with the most. When I’ve been responsible for the deployment decision I’ve chosen Heroku in all but one case. The one and only time I chose not to use Heroku was when I deployed my very first Rails app. I think the decision in that case was more out of ignorance than anything else. When I’ve worked on Rails apps deployed by other people, those apps were usually running on Heroku as well.

The reason I personally have chosen Heroku so many times is because it’s so easy. My first-ever Rails deployment was to a “blank” VPS running Ubuntu. I recall that it was a tedious, time-consuming and frustrating experience. Heroku can be frustrating at times too but for the most part it just works.

There’s a downside to the convenience Heroku provides, though, and that’s the cost. Heroku can get pretty expensive pretty quickly.

By the way, if you don’t know, Heroku uses AWS under the hood. You can think of it as an abstraction layer on top of AWS to make AWS easier.

VPS

It seems to me that the main advantage to deploying Rails on a VPS is the cost. You have to do everything manually if you’re using a VPS but it can be pretty inexpensive.

By the way, let me explain what VPS means if you don’t know. VPS stands for Virtual Private Server. The idea, if I understand correctly, is that a hosting company has an actual physical server sitting somewhere, and on that physical server they have a number of virtual machines running.

To give an example, let’s say Bob buys a VPS account from a hosting company and he’s running Ubuntu Linux. Let’s also say that there’s another customer named Alice who buys a VPS account running Windows. Both Bob’s Ubuntu server and Alice’s Linux server are really just virtual machines running on the same physical server.

AWS

Before I talk about AWS specifically I want to make the distinction between cloud and non-cloud hosting options. A lot of people including my past self have perceived “cloud” just to be an empty and redundant marketing term meaning “internet” or “server”. For our purposes it will be helpful to understand the term a little differently.

Here’s Amazon’s definition of cloud which I think works well: “Cloud computing is the on-demand delivery of compute power, database storage, applications, and other IT resources through a cloud services platform via the internet with pay-as-you-go pricing.”

The significant thing about cloud computing is the on-demand delivery. You can go from having just one server to having ten of the same server. Or you can resize a server in terms of memory or disk space. Or you can spin up more database instances whenever you want.

So Heroku is a super easy but pretty expensive cloud provider, AWS is a less easy and less expensive cloud provider, and a VPS is an even less expensive but even less easy non-cloud option, meaning it doesn’t have the on-demand scaling benefits.

I know you could take issue with the accuracy and precision of the above statements. I’m speaking loosely on purpose in an effort to be understandable.

With that said, let’s start our work.

Set Up The EC2 Instance

AWS will allow us to spin up as many EC2 instances as we want. For our purposes we’ll only need one.

We’ll need to get to the EC2 dashboard. To get there, go to the Services menu in the upper left-hand corner and then click on EC2.

From the EC2 dashboard, click the blue Launch Instance button.

On the first page of the wizard that appears, find Ubuntu and click Select. Why are we using Ubuntu? No particular reason. I personally am pretty familiar with Ubuntu over other Linux distributions and I assume that’s true of a lot of other developers as well. If you’re not particularly familiar with Ubuntu, don’t worry. It probably won’t matter. But I am assu.ming you’re comfortable with basic Linux concepts and commands.

On the next screen of the wizard, leave the instance type at the default, t2.micro.

EC2 offers various instance types with different levels of memory, disk space and other attributes. Why are we using t2.micro as opposed to any other instance type? Mainly because we really don’t need anything special in order to carry out our “EC2/Rails hello world” exercise. A small server is fine.

The next step is to click Review and Launch.

On the next screen, just click Launch.

You’ll be prompted about something called a key pair. If you’re not exactly clear on what a key pair is, don’t worry. I wasn’t super clear on key pairs before myself, even though I had been using them for a long time.

Here’s how the Amazon docs describe key pairs:

“Amazon EC2 uses public–key cryptography to encrypt and decrypt login information. Public–key cryptography uses a public key to encrypt a piece of data, such as a password, then the recipient uses the private key to decrypt the data. The public and private keys are known as a key pair.”

There was a little bit of fancy language in there, like “public-key cryptography”. I think you can understand the gist of key pairs without necessarily understanding terms like that.

Just think of it this way. Let’s say you’re connecting to an EC2 instance from your Macbook. Your Macbook will have a file on it with a string of characters. That’s your public key. Your EC2 instance (that is, Linux server) will have a different file on it with a different string of characters, and that’s your private key. These two keys match up with each other, and that’s your key pair.

Anyway, you’ll need to create a key pair if you don’t have one already. You’ll be prompted for a name to give the key pair, and then you’ll be given a .pem file to download. The .pem file you download is your public key. I called mine aws-us-east-1.pem and put it in my ~/.ssh/ directory.

Okay, so you’ve followed the wizard steps and created your key pair (or, if you had created a key pair some time in the past, you selected that key pair). What you should see now is a page like the one below. There’s a green box near the top that says, “Your instances are now launching”. Below that headline it says, “The following instance launches have been initiated” and there’s a link with your Instance ID. Click that link.

If you started this tutorial without any EC2 instances, you should now have just one single EC2 instance. Right-click the instance and click Connect. You’ll be presented with a screen like the following.

If you’ve freshly created your public key, you’ll need to change the permissions on it. I put my public key in ~/.ssh/aws-us-east-1.pem. Your path may be different.

If you forget to change the permissions on your public key to 400, you’ll get an error when you try to connect to your EC2 instance. The error will say your permissions are too open and “It is required that your private key files are NOT accessible by others.”

By default your public key’s permissions are probably 644, or -rw-r--r--. The first r means the file is readable by your user, and the second two rs mean the file is readable by other users as well. AWS doesn’t like your public key to be readable by other users. The permissions for your own user can be as open as you want. AWS will allow you to connect if your public key’s permissions are 700 or 600. I guess they just recommend 400 (-r--------) because there’s no advantage in making the permissions any more open than that, only risk.

Now you can connect by running this command:

You’ll of course replace ~/.ssh/aws-us-east-1.pem with the path to your own public key and ec2-34-202-231-182.compute-1.amazonaws.com with the URL of your own EC2 instance.

Once you make contact with the EC2 instance via SSH, you’ll be asked if you want to continue connecting. Say yes, of course.

The next chunk of this tutorial will have us follow this incredibly helpful Digital Ocean tutorial. The author of that post deserves a lot of credit for making this one possible. We don’t be following the Digital Ocean tutorial verbatim, though. I’ve laid out a slightly different set of steps below. We’ll start by installing Ruby.

Install Ruby

First we’ll run sudo apt-get update. If you’re curious as to exactly what sudo apt-get update does, there’s a pretty good Stack Exchange question/answer here.

Next we’ll install a whole bunch of dependencies.

Finally, we install Ruby.

We can verify our Ruby installataion by running ruby -v.

Install Passenger and NGINX

Here’s the first command we’re going to run:

You might find this command mysterious even if you’re fairly familiar with Ubuntu. I didn’t know what this command was when I first saw it, so I had to do some research.

First, what’s apt-key? According to the man page, “apt-key is used to manage the list of keys used by apt to authenticate packages. Packages which have been authenticated using these keys will be considered trusted.”

If you want to be able to understand that definition, you have to understand what “apt” is and what a package is.

So what’s apt? This documentation page says, “The apt command is a powerful command-line tool, which works with Ubuntu’s Advanced Packaging Tool (APT) performing such functions as installation of new software packages, upgrade of existing software packages, updating of the package list index, and even upgrading the entire Ubuntu system.”

Now we can reexamine this sentence and perhaps understand it better: “apt-key is used to manage the list of keys used by apt to authenticate packages.” This kind of makes sense but it doesn’t totally demystify the command itself. For example, what’s the “adv” part?

Here’s what the docs say about adv: “Pass advanced options to gpg. With adv –recv-key you can e.g. download key from keyservers directly into the the trusted set of keys. Note that there are no checks performed, so it is easy to completely undermine the apt-secure(8) infrastructure if used without care.”

Okay, so using adv with --recv-key downloads a key directly into a trusted set of keys. The value 561F9B9CAC40B2F7 must be an identifier for a key. If we paste 561F9B9CAC40B2F7 into Google, the results that come up are all about Passenger.

So the sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 561F9B9CAC40B2F7 must say, “Add the key for Passenger, 561F9B9CAC40B2F7, to APT’s list of trusted keys.”

Once you’ve run that apt-key command, open up /etc/apt/sources.list.d/passenger.list and paste in the following content:

A quick note: when I first tried to follow Digital Ocean’s tutorial, the above line was problematic for me and I didn’t understand why. I later realized that it had to do with my Ubuntu version. Ubuntu 16.04, the version I was using on my EC2 instance, is named Xenial Xerus. The Digital Ocean tutorial used Ubuntu 14.04, Trusty Tahr. When I pasted in the value from the Digital Ocean tutorial, deb https://oss-binaries.phusionpassenger.com/apt/passenger trusty main, I had problems because trusty corresponds with Ubuntu 14.04, not the Ubuntu 16.04 that my EC2 server was running.

So if you’re using a version of Ubuntu other than 16.04, you’ll probably have to change xenial to something else.

Next we make the file we just edited, /etc/apt/sources.list.d/passenger.list, readable only to root.

Now we update the APT cache.

Then we install NGINX and Passenger.

You might wonder how the nginx-extras package differs from the nginx package. According to the docs for this package, “This package provides a version of nginx with the standard modules, plus extra features and modules such as the Perl module, which allows the addition of Perl in configuration files.” That’s kind of vague (what other “extra features and modules” besides Perl?), and I don’t understand why the plain old nginx package wouldn’t have been sufficient for our purposes, but okay.

Now we have to add the passenger_root and passenger_ruby directives to the NGINX configuration file at /etc/nginx/nginx.conf.

Since we modified the NGINX configuration, we have to restart NGINX.

If we were to try to visit our EC2 instance’s URL in the browser right now, it wouldn’t work. That’s because our EC2 instance has a security group that describes what kind of access should be allowed to our instance, and right now that security group is only allowing SSH requests on port 22, not web requests on port 80.

We’ll need to edit our EC2 instance’s security group’s inbound rules. Specifically, we’ll add an inbound rule that says “allow requests on port 80”.

In the AWS web console, go to the Services menu and pick EC2 if you’re not already there. Then, under the “NETWORK & SECURITY” group, click on Security Groups. Right-click your EC2 instance’s security group and click “Edit inbound rules”.

By the way, what if you have multiple security groups and you don’t know to which security group(s) your EC2 instance belongs? You can go to Instances, right-click your instance, hover over the Networking menu item, then click Change Security Groups. You’ll see a list of security groups. The security group(s) that are checked are the ones that correspond to your EC2 instance.

Once you have the “Edit inbound rules” modal open, click the Add Rule button. We’re going to select HTTP as the Type and allow traffic from anywhere. After you do that, click Save.

Now if you visit your EC2 instance’s URL in the browser, you should see something there.

Install Rails And Create Rails Project

Let’s install Rails.

We’ll create a project called hello_world. I’m a PostgreSQL man so I always use PostgreSQL as my RDBMS as opposed to MySQL or SQLite.

We’ll need to uncomment the line in our Gemfile that contains therubyracer. If you’re wondering why therubyracer is necessary, this Stack Overflow question/answer might help. therubyracer is a tool that embeds a JavaScript interpreter into Ruby. This is evidently necessary in our case to perform JavaScript compression in the asset pipeline. (Don’t quote me on that, though.)

After modifying our Gemfile we’ll of course need to run bundle install again.

Get Our EC2 Instance To Serve Our Rails Project

We need to go into /etc/nginx/sites-available/default and comment out the following two lines:

Then we’ll go into /etc/nginx/sites-available/hello_world and paste in the following content:

Notice the passenger_app_env development line. This will make it so that visiting the root path of our application will give us the “Yay! You’re on Rails!” page. I want to complete this relatively easy step before we move onto the more involved step of creating and using an actual scaffold with a database connection.

We’ll need to symlink this file to /etc/nginx/sites-enabled/hello_world.

Since we changed configuration, we need to restart NGINX.

Now, if you visit the URL for your EC2 instance in the browser, you should see this:

Having accomplished our intermediary goal of viewing the “Yay! You’re on Rails!” page, we can now change our passenger_app_env from development to production.

There’s no point yet in restarting NGINX and trying to visit the instance’s URL. It won’t work. First we need to get a database going. We’ll also create a scaffold in our Rails application so we actually have something to look at and use.

Create An RDS Instance

To create an RDS instance, first go to Services and choose RDS. From there, click Launch a DB Instance.

Choose PostgreSQL as the RDBMS and then click Select.

We’ll use a production PostgreSQL instance since that’s what we’d do in real life. After that, click Next Step.

On the next screen we’ll need to give our database an Identifier, a Master Username and a Master Password. Let’s use hello-world as the Identifier and hello_world as the Master Username. For the password, use whatever you want. (By the way, why hello-world as the Identifier instead of hello_world? Because AWS doesn’t allow underscores in an Identifier.) Once you’ve entered those values, click Next Step.

The values on the next page page can be left at their defaults. Just click Launch Instance.

After you launch your RDS instance you can monitor its progress under Instances in the RDS Dashboard. In my experience the RDS instance takes forever to launch.

Once the RDS instance is ready we can edit our config/database.yml to make it so our Rails app knows how to connect to our RDS database.

Modify the production section of config/database.yml to match the following. You’ll of course replace yourpassword with your actual password and replace my host URL with your own RDS URL.

To find your RDS’s host URL, go to Instances, click on your instance, pick See Details from the Instance Actions menu, and search the page for Endpoint. (You want the endpoint version that does not have the :5432 at the end.)

By the way, isn’t it bad to store sensitive values like passwords directly in files? No, it’s only bad to store sensitive files in version control. If a hacker gains access to your EC2 instance, they’ll be able to read your configuration values whether they’re stored in environment variables or directly in files.

Now that our RDS credentials are in place, we can create the database itself.

Now we’ll create a scaffold so we have something to work with. Afterword, we’ll run a migration to create the table for our new scaffold.

It will also be helpful to set up a root route.

Now we have to take care of a little plumbing work. We have to set up our Rails secrets.

Copy the value output by rails secrets and paste it into config/secrets.yml under the production section.

Now we need to precompile our assets.

Finally, restart NGINX and we should be good to go.

Observe Our Complete Working Application

Now we can observe our complete working application. If we visit the root route, we should see an (empty) list of people.

If we go to /people/new, we should be able to add a new person.

After we add a person, the person should show up in our people list.