How to Deploy a Rails App to DigitalOcean with Ubuntu 14.04, Phusion Passenger & Nginx, Postgres, and Capistrano
- Choose Server Image
- Choose Size (if this is a hobby app you can probably get away with the smallest)
- Choose Region (want to choose the one closest to you)
- Give it a Name (your apps name is a good option)
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.
After creating your "Droplet" (server), you'll be taken to your Droplets page where you can find your server's IP address
-
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.
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.
- 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".
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:
- manually
- 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 typingmkdir .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 calledauthorized_keys
by typingtouch authorized_keys
. If we use the ls command from within the.ssh
directory we should see ourauthorized_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 hittingi
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 andexit
again to logout of the server. Now try logging in as your new user withssh 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 asbrew 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.
-
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 toPermitRootLogin no
and saving the file by hittingesc :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:
- Additional Recommended Steps for New Ubuntu 14.04 Servers
- How To Protect SSH with Fail2Ban on Ubuntu 14.04
- Initial Server Setup with Ubuntu 14.04 - most of this is covered above.
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.
- 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
- 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
Finally we can install ruby.
rbenv install 2.2.4
rbenv global 2.2.4
ruby -v
We need to install bundler and run rbenv rehash
gem install bundler
rbenv rehash
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
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.
- 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
- 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 uncommentpassenger_root
andpassenger_ruby
. If you can't find a commented outpassenger_root
checkout Phusions guide here
When finished with this restart Nginx:
sudo service nginx restart
- 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)
- Update Regularly You should update regularly to keep your system, nginx, and passenger up to date.
sudo apt-get update
sudo apt-get upgrade
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 withcreateuser user_name --pwprompt
, obviously replacing user_name with whatever you want. I set it todeploy
.
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.
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.
- 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
- 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}
- 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.
- 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
- 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.
- 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;
}
}
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
}