Deploying Rails to DigitalOcean, the Hard Way


### Introduction: Deploying Rails as a n00b

As a relative n00b to the world of web development, I don't have much experience deploying production-level apps. As a student at the Flatiron School, I became familiar with using Heroku to deploy and host my Rails applications. After graduating Flatiron, I continued to use Heroku for both Rails and Ember deployments. It's an amazing platform that makes deployment easy for beginners like myself. I use it for my own deployments and I always recommend that my students at Flatiron use it.

Heroku does have some limitations, however. It provides limited database storage and limited support for file uploads and temporary file storage. Hosting our apps on our own servers, on the other hand, gives us total control over this and much more. So, I began exploring DigitalOcean.

I first tried to deploy a Rails application using the 1-click Ruby on Rails installation that DO offers. I failed. Hard. A friend then turned me on to using Dokku to deploy to DigitalOcean. Dokku is "Docker powered mini-Heroku". It allows you to deploy your app to your very own DigitalOcean server using git. It abstracts away most of the nitty-gritty work of configuring your server and your app for production. For someone who knows very little about that nitty-gritty work (me), or anyone who doesn't wan't to engage in unnecessary nitty-gritty technical work (everyone ever), it's the perfect tool. If you're interested in using Dokku to deploy to your DO server, I highly recommend this blog post by my friend and former classmate, Dan Visintainer.

After successfully using Dokku a handful of times for my DigitalOcean deployments, I felt the time had come to stop avoiding the technical nitty-gritty. I often caution my students to avoid using certain gems, like Devise, until they understand the functionality that the gem is offering and can build it out for themselves. Over this past weekend, I decided it was time to practice what I preach, and deploy to DigitalOcean, the hard way. No Dokku, no 1-click installation, just me, my droplet, and a million different tutorials.

After several hours of piecing together 6+ blog posts and articles, I had finally deployed a super-important cat cataloguing app:

As someone that's never deployed to DigitalOcean without the help of Dokku, I found myself really feeling the lack of a guide that would take me through the process from square zero to finish. So, I decided to make one myself!


Part I: Configuring The Server


Create Your Droplet

Once you sign up for your DO account, click the "create droplet" button. Create a new droplet with an Ubuntu installation:

Then, choose a size and a data center region:

.

Lastly, add your SSH key to your droplet so that you can access your server using SSH, and NOT by typing in your password every time.

Click the "add new" tab under the Add SSH Key heading. Grab your public key by running cat ~/.ssh/id_rsa.pub in your terminal and pasting it into the form field.

Click "Create" and you'll be taken to the dashboard to view your new droplet!

Now we're ready to set up our server.


Setting Up Your Server

Before we do anything else, we need to configure our server. We'll be setting up an additional user through which we'll be interacting with our server. This is a safer approach because we'll be effectively sandboxing our user and allowing them to interact with (i.e. install things into, set-up and change the environment of) a smaller portion of our server.

For this portion of my journey to deployment, I found this article to be particularly helpful and the steps outlined below are largely cribbed from it.

Step 1: Login as Root with SSH

In your terminal, ssh into your server as the root user:

# your machine
$ ssh root@SERVER_IP_ADDRESS

You can find your server IP address by visiting https://cloud.digitalocean.com/droplets:

If this is the first time you're connecting to your server, you'll see the following:

The authenticity of host '123.123.123.123 (123.123.123.123)' can't be established.
ECDSA key fingerprint is
79:95:46:1a:ab:37:11:8e:86:54:36:38:bb:3c:fa:c0.
Are you sure you want to continue connecting (yes/no)?

Your computer is telling you that the server to which you are connecting is not recognized. Type yes and hit enter!

And we're in! If you need help troubleshooting any of these steps, check out this article

What is the Root User?

The root user is the administrative user in a Linux environment that has very broad privileges. Because of the heightened privileges of the root account, you are actually discouraged from using it on a regular basis. This is because part of the power inherent with the root account is the ability to make very destructive changes, even by accident.*

For this reason, we'll be setting up another user with a reduced scope of influence. This is the user we'll be logging in as when we move forward with deploying our app.

Step 2: Create The Alternative User

Okay, let's create that user. Make sure you are logged in as the root user and run the following:

# in your ubuntu installation on your droplet
> adduser rails

We'll call our user "rails" because we'll be deploying a Rails app. You can call your user whatever you want, btw.

You'll be prompted to add a password. Pick a strong password, enter it and hit "enter". You can skip filling in the additional info you're prompted to enter.

We've just created a new account with normal, i.e. not admin, or root, privileges. However, we will want our user to be able to run certain administrative commands. So, we'll give our normal user "super user" privileges. This will allow us to run administrative commands, preceded by sudo.

On Ubuntu, users in the "sudo" group are allowed to use the sudo command. Let's add our "rails" user to that group. Still logged in as the root user, run:

> gpasswd -a rails sudo

Now we're ready for the next step.

Step 3: Add SSH to the new user

Let's set up SSH authentication for our new user. This way, can log in to this user securely using our SSH public key, rather than entering a password every time.

Open another window in your terminal so that you are interacting with your local machine, not the DigitalOcean server to which you are connected as the root user. Then run:

# your computer
$ cat ~/.ssh/id_rsa.pub

Copy the public key that is outputted to the terminal. Now, switch back to the terminal window in which you are connected to your DO droplet and execute the following command:

# digitalocean
> su - rails

Then, create a new directory and set its permissions:

# digitalocean as 'rails' user
> mkdir .ssh
> chmod 700 .ssh

The 700 permission code means the owner of the file can read, write and execute.

Then, create a file in .ssh and open it in the text editor:

# digitalocean as 'rails' user
> nano .ssh/authorized_keys

Paste your public key here and then save and exit the file with CTRL-X, then Y, the ENTER.

Lastly, set the permissions of the authorized_keys file to read and write with:

# digitalocean as 'rails' user
> chmod 600 .ssh/authorized_keys

Now, run the following command, just once, to return to the root user:

# digitalocean as 'rails' user
> exit

Now that we've returned to operating at the root user, let's configure out server to disallow future SSH log-ins as root. This is more secure––we'll only allow ourselves to interact with our server as the sudo user, which we've called "rails". This user is somewhat cordoned off, or sandboxed, within our server. We're therefore interacting with our server more securely, because we're interacting with it via a user that does not have root privileges and therefore cannot execute certain destructive commands. In the future, should we need to exercise some of those privileges, we can scale up the permissions of our "rails" user.

Step 4: Configure SSH For the Root User

As the root user, execute the following to open your ssh configuration file in your text editor:

# digitalocean
> nano /etc/ssh/sshd_config

In this file, find the following line:

# /etc/ssh/sshd_config

PermitRootLogin yes

And change the yes, to no. Save and exit with CTRL + X, then Y, then ENTER.

Now, restart your SSH daemon with:

# digitalocean
> service ssh restart

And that's it!

But, before you log out of the root user, let's confirm that we've configured everything correctly.

Open up another terminal window and try SSH-ing into your server as the rails user:

# local
$ ssh rails#SERVER_IP_ADDRESS

You'll be prompted to enter the password you set for this user, but when we log in in the future, we'll be able to skip this step. Now that we're successfully logged in as the rails user, go back to the window in which you are logged in as root and type exit.

Now that our rails user is all set up, we're ready to get our environment ready to run a Rails app. Note that for the remainder of this tutorial, you should be logged in to your server as the "rails" user.


Setting Up Our Environment For Rails

We'll be using rbenv to install and manage our Ruby versions. This section of the tutorial is mainly drawn from this article.

rbenv is lighter weight and more flexible for RVM, and thus a better fit for our remote server in which we won't be doing a lot of development, just deploying applications we've developed on our local machine.

Why is rbenv lighter than RVM? Well, it doesn't install Ruby (we get to do that manually, and to our own specifications) and it doesn't manage gemsets (we'll use Bundler for that). It also doesn't have a configuration file.

rbenv has one job: manage Ruby installations and switch between them. It does so by creating shim files in ~./rbenv/shims which retrieve the correct version of Ruby, as specified by your application. ~./rbenv/shims get added to your PATH, meaning there is no need for rbenv to be loaded into your shell (unlike RVM). To learn more about rbenv and how it compares to RVM, check out the rbenv documentation and this excellent article.

Now that we're all convinced that rbenv is the right tool for the job, let's use it!

Step 1: Installing rbenv

First, we need update apt-get. For those of you who are used to working wit OS X, apt-get is the package management command line program that works with Ubuntu's APT (Advanced Packaging Tool) library.

Make sure you are logged in to your DigitalOcean droplet as the rails user and run:

# digitalocean
> sudo apt-get update

Now, install the rbenv and Ruby dependencies, including openssl and sqlite3.

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

Now, install rbenv with the following commands:

> cd
> git clone git://github.com/sstephenson/rbenv.git .rbenv
> echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile
> echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
> git clone git://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build
> echo 'export PATH="$HOME/.rbenv/plugins/ruby-build/bin:$PATH"' >> ~/.bash_profile
> source ~/.bash_profile

This will do two things for us: install rbenv into the home directory and set the environment variables that will allow rbenv to deliver the right version of Ruby.

Step 2: Installing Ruby

Install Ruby with the following:

> rbenv install -v 2.3.0
> rbenv global 2.3.0

This first installs Ruby and then sets the global version of Ruby, or the version that will be used in all of our shells.

Make sure that everything was installed properly with:

> ruby -v

Let's configure it so that Rubygems will not install documentation locally for every gem that we install:

> echo "gem: --no-document" > ~/.gemrc

Lastly, we'll install bundler:

> gem install bundler

Now we're ready to install Rails!

Step 3: Installing Rails

This part is easy:

> gem install rails

Then, run

> rbenv rehash

Any time we install a gem that includes binaries, or executable scripts, you need to run rbenv rehash so that rbenv can create the necessary shim files.

When we install a gem, provided that gem has executables, binstub files will get generated. Binstubs are wrappers around executables that prepare the environment before calling on the original executable. The shims directory that rbenv adds to $PATH contains binstubs for every executable related to Ruby.

To learn more, check out this section of the rbenv documentation.

We're almost done setting up Ruby and Rails!

Step 6: Installing Javascript Runtime

Recall that the asset pipeline depends on Javascript runtime. So, we'll need to install Node.js.

First, add the Node PPA (Personal Package Archives) to apt-get

> sudo add-apt-repository ppa:chris-lea/node.js

Then, update apt-get and install Node

> sudo apt-get update
> sudo apt-get install nodejs

Last but not least, we need to configure Git.

Step 7: Configure Git

We need to do two things before we are ready to clone down our app and get it up and running on our server in production mode.

Git embeds our username and password in our commits, so let's set up our username and password for GitHub, here on our server.

> git config --global user.name "Your Name"
> git config --global user.email "youremail@domain.com"

Now, let's set up SSH with GitHub. First, we'll need to generate a public key for our "rails" user account on our server.

Logged in as "rails" user, run

> ssh-keygen

This will output:

Generating public/private rsa key pair.
Enter file in which to save the key (/home/rails/.ssh/id_rsa):

Hit ENTER to confirm the name of the file to which the SSH key will be written. You'll then be prompted for a passphrase. You can leave this blank and hit ENTER, twice.

Copy your public key via cat /home/rails/.ssh.id_rsa.pub. Then, go to GitHub, open your settings and add this new SSH key.

Okay, now we're ready to install PostgreSQL onto our server.


Installing PostgreSQL

For this portion, I worked primarily with this article.

First, update apt-get

> apt-get update

Then, run the following:

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

Later, after we clone down our app and bundle install, we'll log into our Postgres server to create a new user and database and matches up to the user and database specified in the config/database.yml file in our Rails app. For now, we're ready to move on to installing Passenger and Nginx.


Part II: Install and Configure Phusion Passenger and Ngnix

This section of the tutorial owes thanks to this excellent article.

For this deployment, we'll be using Phusion Passenger as our web server. Passenger is better at handling a higher volume of traffic than other popular web servers, like Unicorn or Puma. And, as you may have realized, our cat cataloguing website is extremely important and likely to receive lots of traffic.


Installing Passenger and Nginx

First, we need to install a PGP key

> sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 561F9B9CAC40B2F7

Create an APT source file:

> sudo nano /etc/apt/sources.list.d/passenger.list

Add this line to the above file:

> deb https://oss-binaries.phusionpassenger.com/apt/passenger trusty main

Save and exit with CTRL + X, then Y, then ENTER.

Update the owner and permissions of the file;

> sudo chown root: /etc/apt/sources.list.d/passenger.list
> sudo chmod 600 /etc/apt/sources.list.d/passenger.list

Update apt-get:

> sudo apt-get update

Lastly, install Passenger with Ngnix

sudo apt-get install nginx-extras passenger

This last bit may overwrite the Ruby version to an older one. If so, you need to remove the incorrect Ruby location and create a new symlink to the correct Ruby binary file:

> sudo rm /usr/bin/ruby
> sudo ln -s /usr/local/bin/ruby /usr/bin/ruby

Configuring the Web Server

Open the Nginx configuration file in your text editor:

> sudo nano /etc/nginx/nginx.conf

Find and uncomment the following lines:

# passenger_root 
/usr/lib/ruby/vendor_ruby/phusion_passenger/locations.ini;
# passenger_ruby /usr/bin/ruby;

Then, update the path in the passenger_ruby line. When you're done, it should look like this:

passenger_root /usr/lib/ruby/vendor_ruby/phusion_passenger/locations.ini;
passenger_ruby /usr/local/bin/ruby;

Save and exit with CTRL + X, then Y, then ENTER.

Okay, now we're ready to upload our existing Rails app and configure it for production with Nginx.


Part III: Configuring Rails for Production

We're assuming we have a Rails app that is ready for deployment, just waiting for us and hanging out on GitHub.

Step 1: Clone Your App

Since we configured git correctly earlier on in this tutorial (you may not remember, because this process is taking forever), we should be able to clone down our app int our DigitalOcean server:

git clone git@github.com:SophieDeBenedetto/do-testing.git

Once the app is cloned in, run bundle install.

Debugging Bundle Install If you get errors regarding missing gems when you run this command, execute the following:

> bundle pack
> bundle install --path vendor/cache
Step 2: Set Your Production Secrets

cd into your app:

# digitalocean
cd do-testing

Then run rake secret. You should see a generated secret key base and token generated for you:

ef16e86e0ba7f32d2550b1d750962a0aafd0e9c81a42d252becb833166b6e1b1472f74b862a2c5ccd575fe5faf5e01f47b

e113ba434a83b85b3b35c169f129f2

Then, open up the config/secrets.yml file with your text editor:

nano config/secrets.yml

and set your secret key base and token like this:

production:
  secret_key_base: ef16e86e0ba7f32d2550b1d750962a0aafd0e9c81a42d252becb833166 b6e1b1472f74b862a2c5ccd575fe5faf5e01f47b
  secret_token: e113ba434a83b85b3b35c169f129f2
Step 3: Configure the Database for Production

Add the following section to your config/database.yml file (via nano config/database.yml):

production:
  adapter: postgresql
  url: <%= ENV["DATABASE_URL"] %>
  username: digitaloceantesting
  password: <your password>
  host: localhost
  database: digitaloceantesting

Save and exit.

Now, we need to create the database with the user and password specified above. Connect to Postgres by typing

> psql

If you get an error similar to the following:

sudo: unknown user: psql

Create the user:

sudo -u postgres psql

Then, you should be connected to Postgres. Your bash prompt should now look like this:

postgres=#

Create the database, user and password to match what you specified in the database.yml file:

postgres=# create user digitaloceantesting with password '<your password>';
postgres=# create database digitaloceantesting owner digitaloceantesting;

Then, exit Postgres with

postgres=# \q
Step 4: All the Rakes

Run:

> RAILS_ENV=production rake db:migrate
> RAILS_ENV=production rake db:seed
> RAILS_EVN=production rake assets:precompile
Step 5: Configure Ngnix for your App

First, disable the default Nginx configuration.

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

Comment out the following two lines:

listen 80 default_server;
listen [::]:80 default_server ipv6only=on;

Save and exit.

Now we need to create a custom Nginx configuration file for our app:

> sudo nano /etc/nginx/sites-available/do-testing

To this file, we need to add a server block that:

  • enables listening on port 80
  • sets the domain name (in this case the IP address of our droplet)
  • enables Passenger
  • sets the root to the directory of our app.
server {
  listen 80 default_server;
  server_name <IP ADDRESS of your droplet>;
  passenger_enabled on;
  passenger_app_env production;
  root /home/rails/do-testing/public;
}

Save and exit with CTRL + X, Y, ENTER.

Create a symlink for it:

> sudo ln -s /etc/nginx/sites-available/do-testing /etc/nginx/sites-enabled/do-testing

Part IV: Deploy!

We're almost done for real!

Lastly, restart Nginx:

> sudo nginx -s reload

Guess what? We're done!!

Visit: http://your-ip-address

and see your glorious app.

Sources

subscribe and never miss a post!

Blog Logo

Sophie DeBenedetto

comments powered by Disqus
comments powered by Disqus