Skip to content

Instantly share code, notes, and snippets.

@kylefdoherty
Last active September 7, 2018 08:44
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save kylefdoherty/ac9f5e57a1a546b743db to your computer and use it in GitHub Desktop.
Save kylefdoherty/ac9f5e57a1a546b743db to your computer and use it in GitHub Desktop.
Notes for myself on How to Deploy a Rails App to DigitalOcean with Ubuntu 14.04, Phusion Passenger & Nginx, Postgres, and Capistrano

How to Deploy a Rails App to DigitalOcean with Ubuntu 14.04, Phusion Passenger & Nginx, Postgres, and Capistrano

1. Setup DigitalOcean Droplet

  • Choose Server Image

choose server image

  • Choose Size (if this is a hobby app you can probably get away with the smallest)

choose server size

  • Choose Region (want to choose the one closest to you)

choose server region

  • Give it a Name (your apps name is a good option)

2. Server (Droplet) Setup

Once you've created your droplet there is a bit of setup you'll want to do to make your server secure and easy to work with. This means we need to SSH into our server.

NOTE: If you aren't sure what SSH is checkout this 2 min youtube video and/or this blog post explaining what it is and how to use it in more detail.

A. Setup Root Login

After creating your "Droplet" (server), you'll be taken to your Droplets page where you can find your server's IP address droplet IP

  • Use the IP address to SSH into your server from the command line w/ ssh root@159.203.172.34

  • When you do this the first time it will ask if you're sure you want to continue (type "yes") and then enter the password DigitalOcean just emailed you.

  • Next it will prompt you to reset your password. Input the password DigitalOcean emailed you again, and then provide a new one. I use 1Password to generate and store my new password.

    NOTE: You have now created your root user, which is the administrative user in a Linux environment. The root user has the ability to make destructive changes, even by accident, and therefore you're discouraged from using the root user unless you absolutely need to and is why the next step is to create a new user.


B. Create New User for our Server (Droplet)

Now that we have the root user setup we want to create a new user for our day to day tasks, such as configuring and deploying our rails app.

  • First, type adduser mynewuser into the terminal, obviously replacing "mynewuser" with whatever you want and setup a password. It will also prompt you to add info like name, phone, etc. but you can skip this by hiting enter.

NOTE: Often people call this new user "deploy" becuase this is the user that will manage the configuration and deployement of the app on this server, but you can use whatever you like.

create new user

  • Next, we need to give our new user "super user" or root privaleges so they can do administrative tasks by typing sudo before the command. To make our new user a "super user" we need to add them to the "sudo" group. Do this by typing:
gpasswd -a mynewuser sudo
The prompt will then output `Adding user deploy to group sudo`

NOTE: to learn more about sudo works, checkout DigitalOcean's Sudoers Tutorial.

Now we have a root user and a new user and we can login as either. Currently you're logged in as your root user and know this becuase your terminal's prompt tells you, root@MyNewApp:~#. If I want to login as the new user you created, just logout by typing exit and ssh back into your server, but as the new user. For example, I can login as the new user I created by typeing ssh mynewuser@104.236.106.70, and entering my password for the user "mynewuser".


C. Add Public Key Authentication (SSH Key) for our New User

The next thing we want to do is setup a public key authentication for our new user. This requires a private SSH key to login and is more secure than a password.

NOTE: You likely already have an SSH key on your computer for using git, but if you aren't sure or need to create one I recommend Github's guide for generating SSH Keys.

  • Once you have your SSH key generated we need to copy it over to our server in one of two ways:

    1. manually
    2. using ssh-copy-id

    Let's first walk through the manaul way so we have a better understanding of what's going on and then do it the easy way with ssh-copy-id.

  • Manually Setting the SSH Key for New User

    • As the root user switch to the new user by typing su - mynewuser (again, replacing "mynewuser" with your user name). Now you're in your new user's home directory.

    • Create a directory called .ssh by typing mkdir .ssh

    • Restrict its permissions by typing chmod 700 .ssh

    *NOTE: chmod is the command that changes the permissions to file structure objects such as directories. In this case, chmod 700 restricts access to the directory from other users. You can read more about this here

    • Next cd into you new .ssh directory and create a new file called authorized_keys by typing touch authorized_keys. If we use the ls command from within the .ssh directory we should see our authorized_keys file.

    • From here we can use the UNIX vi editor to open this file and add our SSH key to it. To do this we type vi authorized_keys to open the file and then hitting i to go into insert mode so you can add your SSH key to the file.

    NOTE: If you're unfamiliar with the vi editor you can learn how to use it here or you can use nano to do the same thing.

    • Now we need to go to our local machine and print out our SSH key by typing cat ~/.ssh/id_rsa.pub. You should get something like this:
    ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDBGTO0tsVejssuaYR5R3Y/i73SppJAhme1dH7W2c47d4gOqB4izP0+fRLfvbz/tnXFz4iOP/H6eCV05hqUhF+KYRxt9Y8tVMrpDZR2l75o6+xSbUOMu6xN+uVF0T9XzKcxmzTmnV7Na5up3QM3DoSRYX/EP3utr2+zAqpJIfKPLdA74w7g56oYWI9blpnpzxkEd3edVJOivUkpZ4JoenWManvIaSdMTJXMy3MtlQhva+j9CgguyVbUkdzK9KKEuah+pFZvaugtebsU+bllPTB0nlXGIJk98Ie9ZtxuY3nCKneB+KjKiXrAvXUPCI9mWkYS/1rggpFmu3HbXBnWSUdf
    localuser@gmail.com
    
    • Copy the entire SSH key (everything that was printed out) and paste it into the VI editor. Now hit esc :wq to save and quite.

    • Now restrict access to this file to just this user by typing:

    chmod 600 authorized_keys
    
    • Finally, type exit once to get back to the root user and exit again to logout of the server. Now try logging in as your new user with ssh mynewuser@104.236.106.70 and you should automatically be logged in without having to give your password.
  • Using ssh-copy-id to set your SSH key ssh-copy-id is going to do what we just did for us automagically. All we have to do is install it and then tell it which user to add our SSH key to.

    • First, from your local machine type ssh-copy-id and hit enter. If you get a message like "zsh: command not found: ssh-copy-id" or something similar you'll need to install it. If you're a Mac user this is as simple as brew install ssh-copy-id if you're on windows you'll have to use the manual way or google around for an equavalent to ssh-copy-id.

    • Next tell ssh-copy-id where to copy the SSH key to by typing ssh-copy-id mynewapp@104.236.106.70 (and of course replacing the user name and ip address with yours). If successful it will tell you it added the key and to try SSH into the server as that user.


D. Disallow SSH Access to the Root Account

Since our new user can do adminstrative tasks by prepending sudo to commands it's recommended that we disable remote root login to improve the security of our server.

  • First, signin as the root user and open the sshd_config file. You can do this by typing:
vi /etc/ssh/sshd_config

or you can use nano.

  • Next, find the line that says PermitRootLogin yes and change it to PermitRootLogin no and saving the file by hitting esc :wq if you're using the vi editor.

  • Finally, we need to restart SSH so it will use our new configuration. Do this by typing service ssh restart

And that is it, we've setup our Ubuntu 14.04 server. There are more steps we can take but they're outside the scope of this guide. To learn more about those read:


3. Intall Ruby with Rbenv

Rbenv lets us easily manage and install versions of Ruby. If you are using RVM I highly recommend switching to Rbenv as it's easier to use and less intrusive than RVM.

A. Install Rbenv

  1. First step is to update apt-get and install the Rbenv & Ruby depenencies.

But WTF is apt-get? Apt is a command line packaging system for managing software. It is the main package management system in Debian and Debian-based Linux distributions like Ubuntu. If you want to learn more about Apt you can read How To Manage Packages In Ubuntu and Debian With Apt-Get & Apt-Cache.

So logged into our server as our new "super user" (i.e. not the root user) type:

sudo apt-get update  

# This updates apt-get

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

# This installs the dependancies
  1. Now we can install rbenv and ruby-build and rbenv-gem-rehash.
- *rbenv* - as mentioned rbenv allows us to easily manage ruby versions on our machine. To install it copy and paste the snippet of code below into your terminal.
```
cd
git clone https://github.com/rbenv/rbenv.git ~/.rbenv
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc
echo 'eval "$(rbenv init -)"' >> ~/.bashrc
exec $SHELL
```

Let's break down what we're doing here line by line:
  - `cd` to ensure we're in the home directory
  - `git clone https://github.com/rbenv/rbenv.git ~/.rbenv` clones rbenv into a directory called `.rbenv`
  - `echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc` adds rbenv to our PATH so it can be found
  - `echo 'eval "$(rbenv init -)"' >> ~/.bashrc` adds a line to our .bashrc that initializes rbenv everytime we open a new bash window. 
  - `exec $SHELL` restarts the shell so that our changes take effect similar to `source ~/.bashrc`.
  • ruby-build - straight from the readme, "ruby-build is an rbenv plugin that provides an rbenv install command to compile and install different versions of Ruby on UNIX-like systems." To install it we use the following code snippet:
git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
echo 'export PATH="$HOME/.rbenv/plugins/ruby-build/bin:$PATH"' >> ~/.bashrc
exec $SHELL

This one is very similar to what we did above. It clones the repo, this time putting ruby-build in ~/.rbenv/plugins which makes sense since it's a plugin. Then it adds it to the PATH and restarts the shell.

  • rbenv-gem-rehash - is also a rbenv plugin and makes it so everytime you install or uninstall ruby gem rbenv rhash is ran, ensuring the newly installed gem executables are visible to rbenv. To do this we simply clone the repo and put it in .rbenv/plugins where we put ruby-build.
git clone https://github.com/rbenv/rbenv-gem-rehash.git ~/.rbenv/plugins/rbenv-gem-rehash

B. Install Ruby

Finally we can install ruby.

rbenv install 2.2.4
rbenv global 2.2.4
ruby -v

C. Install Bundler

We need to install bundler and run rbenv rehash

gem install bundler
rbenv rehash

D. Install NodeJS

We need a javascript runtime for the Rails asset pipeline. To do that we just need to download NodeJS using a PPA (personal package archive) repository.

curl -sL https://deb.nodesource.com/setup_4.x | sudo -E bash -
sudo apt-get install -y nodejs

4. Install Passenger + Nginx

Phusion Passenger is a free application and web server that integrates with apache or nginx webservers, or standalone. Phusion Passenger is not only tried and tested by many large companies but has excellent documentation for almost any configuration you might have.

  1. Install Passenger Packages These code snippets install Phusion Passenger using its APT (Advanced Package Tool). If you already have Nginx installed this will upgrade it to Phusion's version.
# Install PGP key and add HTTPS support for APT
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 561F9B9CAC40B2F7
sudo apt-get install -y apt-transport-https ca-certificates

# Add APT repository
sudo sh -c 'echo deb https://oss-binaries.phusionpassenger.com/apt/passenger trusty main > /etc/apt/sources.list.d/passenger.list'
sudo apt-get update

# Install Passenger + Nginx
sudo apt-get install -y nginx-extras passenger
  1. Enable the Passenger Nginx module and restart Nginx Now we need to edit the nginx.conf located at /etc/nginx/nginx.conf. Open this file and uncomment passenger_root and passenger_ruby. If you can't find a commented out passenger_root checkout Phusions guide here

When finished with this restart Nginx:

sudo service nginx restart
  1. Check Installation was Successful
- Validate the install with:

 ```
sudo /usr/bin/passenger-config validate-install
```

All the checks should pass, but if they don't simply follow the instructions it returns in your terminal.

- Check if Nginx has started the Passenger core processes with:

```
sudo /usr/sbin/passenger-memory-stats
```

You should passenger and nginx process. Something like the image below. If you don't you should refer to the [troubleshooting guide](https://www.phusionpassenger.com/library/admin/nginx/troubleshooting/ruby/).

![processes](https://www.evernote.com/l/AJ-gfvpEt9NEa7zcBuAi_CZy14--ZE_PMhgB/image.png)
  1. Update Regularly You should update regularly to keep your system, nginx, and passenger up to date.
sudo apt-get update
sudo apt-get upgrade

5. Install PostgreSQL

Now we need to setup our database, specifically PostgreSQL. This is pretty simple. All we need to do is install it and then setup our postgres user.

  • Install Postgres
sudo apt-get install postgresql postgresql-contrib libpq-dev
  • Setup Postgres user Now we need to login as the postgres OS user with sudo su - postgres which will allow us to create a postgres user with createuser user_name --pwprompt, obviously replacing user_name with whatever you want. I set it to deploy.
sudo su - postgres
createuser user_name --pwprompt
exit

The password you give postgres here is what you'll use in your_app/current/config/database.yml so make sure you save it somewhere.

  • Create Databse

The last thing we need to do is create the database as Capistrano won't do this for us. We can do this by doing the following:

sudo su 
su postgres
psql

Here we're switching to the root user with sudo su, then using su postgres to switch to the postgres user and finally launching postgrers with psql. From here we can create the database with:

create database your_app_name with owner = your_database_user;

So here we're creating a database and naming it whatever we want, probably the name of our app, and setting the owner to the database user we created above. If this was successfull you will get CREATE DATABASE after running the command above.


6. Setup Capistrano

Capistrano is a remote server and development automation tool. It allows you to automate the deployment and updating of your app. It runs on your local machine and runs commands on your remote server via SSH just like you would and can do this for multiple servers simultaneously.

The real power of Capistrano are the recipes that have already been written by the community that come in the form of gems such as capistrano-rails and capistrano-passenger.

The Capistrano workflow looks like:

  • Install & configure Capistrano once
  • Each time you're ready to deploy, push changes to Git repo & run Capistrano deploy command

And that is it. Pretty simple but first we need to install & configure it.

  1. Install Capistrano

For our setup we need the capistrano, capistrano-rbenv, capistrano-bundler, capistrano-rails, and capistrano-passenger gems. All of these are going to provide us with capistrano recipes so we don't have to write them manually. So first add the following code to you app's Gemfile:

group :development do
  # ...other gems
  gem "capistrano"
  gem 'capistrano-rbenv'
  gem 'capistrano-bundler'
  gem 'capistrano-rails'
  gem 'capistrano-passenger', '>= 0.1.1'
end

In addition to adding Capistrano to your app, you'll also get pre-defined recipes for rbenv, bundler, rails, and passanger so you don't have to write custom ones.

Now run bundle install and bundle exec cap install to "capify" your app. This will create several new directories & files, specifically:

├── Capfile
├── config
│   ├── deploy
│   │   ├── production.rb
│   │   └── staging.rb
│   └── deploy.rb
└── lib
   └── capistrano
          └── tasks
  1. Configure Capistrano

Now we need to configure the following files: Capfile, deploy.rb, and deploy/production.rb (and deploy/staging.rb if you are setting up a staging server, though this is not covered in this guide).

The Capfile The Capfile will come with the following:

require 'capistrano/setup'

# Include default deployment tasks
require 'capistrano/deploy'

# bunch of commented code that we can delete or ignore

# Load custom tasks from `lib/capistrano/tasks` if you have any defined
Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r }

What we care about in this file right now are: require 'capistrano/setup' and require 'capistrano/deploy'. These are going to handle...you guessed it, setup and deploy tasks for us such as executing git clone and git pull.

Capfile Changes Now we need to explicitly require the recipes from the gems we just installed. Change your Capfile to look like the following:

require 'capistrano/setup'
require 'capistrano/deploy'

require 'capistrano/bundler'
require 'capistrano/passenger'
require 'capistrano/rails/assets'
require 'capistrano/rails/migrations'

require 'capistrano/rbenv'
set :rbenv_type, :user # or :system, depends on your rbenv setup
set :rbenv_ruby, '2.3.0'

# Load custom tasks from `lib/capistrano/tasks` if you have any defined
Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r }

You can see we've added bundler, passenger, rails/assets, rails/migrations, and rbenv with some additional configuration. Note that you can simply require 'capistrano/rails' and leave out bundler, assets, and migrations as they're the same thing, but I'm doing it here to be explicit about what recipes I'm including.

Deploy.rb This file controls how the recipes we just added in the Capfile do their jobs. You can remove everything from this file and add the following:

set :application, 'your_app_name'
set :repo_url, 'git@example.com:you/your_repo.git'

set :linked_files, fetch(:linked_files, []).push('config/database.yml', 'config/secrets.yml')
set :linked_dirs, fetch(:linked_dirs, []).push('log', 'tmp/pids', 'tmp/cache', 'tmp/sockets', 'vendor/bundle', 'public/system', 'public/uploads')

This tells capistrano our app name and where to put it on our server, specifically var/www/your_app_name and where to get our code from (our repo_url).

We also tell it which files and directories to include in the shared folder Capistrano will setup for us. This is important because these will be files and directories we don't want to overwrite with each deploy or that we want to keep out of git such as config/database.yml.

config/deploy/production.rb This tells Capistrano what servers it should deploy to.

role :app, %w{yourappuser@yourserver.com}
role :web, %w{yourappuser@yourserver.com}
role :db,  %w{yourappuser@yourserver.com}
  1. Setup File Structure

Capistrano is going to setup a specific file structure for us but let's do it ourselves to make the deploy run a bit more smoothly.

sudo mkdir -p /var/www/your_app/shared
sudo chown yourappuser: /var/www/your_app /var/www/your_app/shared

This sets up the basic file structure we need and gives your server user permission to edit these directories and their contents.

  1. Add database.yml & secrets.yml files

Capistrano expects there to be config/database.yml and config/secrets.yml to be in our shared folder, becuase we told it to expect this when we added set :linked_files, fetch(:linked_files, []).push('config/database.yml', 'config/secrets.yml') to our deploy.rb file. So now we need to add these files and restrict permissions.

Add Files

sudo mkdir -p /var/www/your_app/shared/config
sudo vi /var/www/your_app/shared/config/database.yml
sudo vi /var/www/your_app/shared/config/secrets.yml

Here we're going to create our database.yml and secrets.yml files and add the appropriate info to them. Basically just copying what's in our local app to these files.

Restrict Permissions

sudo chown -R yourappuser: /var/www/your_app/shared/config
chmod 600 /var/www/your_app/shared/config/database.yml
chmod 600 /var/www/your_app/shared/config/secrets.yml
  1. Configure Passenger

We need to tell Passenger where to find the Ruby version we're using. In the nginx.conf file we edited earlier, go back in and update to:

passenger_root /usr/lib/ruby/vendor_ruby/phusion_passenger/locations.ini;
passenger_ruby /home/deploy/.rbenv/shims/ruby; # If you use rbenv;

And then

sudo service nginx restart

If you get "ok" you're good. If you get "fail" you probably forgot a semi-colon or something.

  1. Add Nginx Host

Finally, we need tell Nginx where to find our rails app by editing the default file in /etc/nginx/sites-enabled. So with our vim editor we:

sudo vi /etc/nginx/sites-enabled/default

to open the file and paste in:

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

      server_name mydomain.com;
      passenger_enabled on;
      rails_env    production;
      root         /var/www/your_app/current/public;

      # redirect server error pages to the static page /50x.html
      error_page   500 502 503 504  /50x.html;
      location = /50x.html {
          root   html;
      }
}

7. Setup Environment Variables

Our database.yml and secrets.yml files both use environment variables so we need to add those. You probably also have other environment variables you need to add for things like APIs you're using, mailers, etc.

We can easily do this by setting passenger environment variables. All we have to do is add them to our server default file.

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

      server_name mydomain.com;
      passenger_enabled on;
      rails_env    production;
      root         /var/www/your_app/current/public;
      
      passenger_env_var DATABASE_USERNAME foo_db;
      passenger_env_var DATABASE_PASSWORD secret;

      # rest of config code here
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment