Grow out your neckbeard. It's time to deploy.

@udit and I grow out our neckbeards a bit in this demo, where we’ll setup a rails app, push it to github and deploy to a linode server with Capistrano. I think @scottmuc would be proud of this one.

TL;DR

I can’t say often enough how fortunate I am to work with so many awesome people @ThoughtWorks. @udit has been playing with a linode server and was cool enough to drop some knowledge on me about standing up and deploying to a server. We spent a few evenings pairing at Coupa Cafe in Palo Alto and this tutorial was the outcome.

This was the finished product:

Success Kid: Deployed to Linode. Actually Worked.

Our App

The demo App name is ‘capteste’ and you can find it at https://github.com/matthewcopeland/capteste. These directions may not work everyone, but they worked for this app.

Tech Stack

Resources

Let’s Go:

terminal
1
2
# SSH to the box as root
ssh root@your.box.number.here
  • If login rejects, open your known hosts file and delete the known the rsa-key that the error refers to.
1
/etc/known_hosts
  • Update your box
terminal
1
2
3
root@li349-144:~# apt-get update

root@li349-144:~# apt-get -y install curl git-core python-software-properties

Nginx

terminal
1
2
3
4
5
6
7
8
root@li349-144:~# add-apt-repository ppa:nginx/stable

root@li349-144:~# apt-get update

root@li349-144:~# apt-get -y install nginx

root@li349-144:~# service nginx start
Starting nginx: nginx.
  • Open your browser and go to your server.
  • If you see a 500 error in your browser, then it may mean that the index.html isn’t present. You’ll be deleting that symlink later anyway.

Postgres - Install and setup

terminal
1
2
3
4
5
6
7
8
9
10
11
12
13
14
root@li349-144:~# add-apt-repository ppa:pitti/postgresql
root@li349-144:~# apt-get update
root@li349-144:~# apt-get install postgresql libpq-dev

root@li349-144:~# sudo -u postgres psql

postgres=# \password
Enter new password: # Press enter to skip
Enter it again: # Press enter to skip

postgres=# create user capteste with password 'captestepasswordthing';
CREATE ROLE
postgres=# create database capteste_production owner capteste;
CREATE DATABASE

Postfix for mail.

terminal
1
2
3
4
5
root@li349-144:~# apt-get install postfix

# You'll be prompted with menu.
# Select 'Internet Site'
# Leave the default system mail name on the next screen.

NodeJS

terminal
1
2
3
root@li349-144:~# add-apt-repository ppa:chris-lea/node.js
root@li349-144:~# apt-get update
root@li349-144:~# apt-get -y install nodejs

Add a User and admin group

We want to stop using root as soon as possible (it can be dangerous if you’re like me and aren’t fully dev-ops savvy). So we’re going to create a new user and set the permissions of that user.

terminal
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
## Add a user for deployment. We'll call it deployer
root@localhost:~# adduser deployer
Adding user `deployer' ...
Adding new group `deployer' (1000) ...
Adding new user `deployer' (1000) with group `deployer' ...
Creating home directory `/home/deployer' ...
Copying files from `/etc/skel' ...

# Push enter to move through these next questions.
Enter new UNIX password:
Retype new UNIX password:
passwd: password updated successfully
Changing the user information for deployer
Enter the new value, or press ENTER for the default
  Full Name []:
  Room Number []:
  Work Phone []:
  Home Phone []:
  Other []:
Is the information correct? [Y/n]


# Add a trusted usergroup and name it admin.
root@localhost:~# addgroup admin
Adding group `admin' (GID 1001) ...
Done.

# Add the deployer to the admin group
root@localhost:~# adduser deployer admin
Adding user `deployer' to group `admin' ...
Adding user deployer to group admin
Done.

Set permissions for deployer

terminal
1
root@localhost:~# visudo
  • scroll down and add deployer permissions to be the same as root
terminal
1
2
3
# User privilege specification
root      ALL=(ALL:ALL) ALL
deployer  ALL=(ALL:ALL) ALL
  • Save and exit the file control + x

Security Permissions

  1. Change the ssh port.
  2. Do not allow root login from the outside.
terminal
1
sudo vim /etc/ssh/sshd_config
terminal vim
1
2
3
4
5
# Change this from Yes to no.
PermitRootLogin no

# Change your port access. Our Example uses 3030
Port 3030
  • Exit sshd_config :wp

  • Reload your settings for them to take effect.

terminal
1
2
3
4
5
6
root@localhost:~# /etc/init.d/ssh reload
Rather than invoking init scripts through /etc/init.d, use the service(8)
utility, e.g. service ssh reload

Since the script you are attempting to invoke has been converted to an
Upstart job, you may also use the reload(8) utility, e.g. reload ssh
  • Logout of your box
1
2
3
root@localhost:~# exit
logout
Connection to 96.126.100.112 closed.

Login as Deployer

terminal
1
2
3
$ ssh deployer@96.126.100.112
deployer@96.126.100.112's password:
# Enter your password

Install RVM and Ruby

  • This next command includes all the stuff that is needed for rvm (at the time of this post). You can generate this list manually by running ‘rvm requirements’.
1
2
3
4
5
6
7
8
9
deployer@localhost:~$ sudo /usr/bin/apt-get install build-essential openssl libreadline6 libreadline6-dev curl git-core zlib1g zlib1g-dev libssl-dev libyaml-dev libsqlite3-dev sqlite3 libxml2-dev libxslt-dev autoconf libc6-dev ncurses-dev automake libtool bison subversion pkg-config

... a whole bunch of stuff ...

# When you come to this
Do you want to continue [Y/n]?
# Push Y and move on.

... more stuff ...
  • Install Rvm
terminal
1
2
3
4
5
6
7
8
deployer@localhost:~$ curl -L https://get.rvm.io | bash -s stable --ruby

... some stuff ...

# I got this red error msg.
# Udit assured me that this isn't a huge deal.
# ..even if it looks odd, just move on.
Please note that `rvm 2 ...` was removed, try `2` instead. ( see: 'rvm usage' )
  • Open your .bashrc and add rvm.
terminal
1
deployer@localhost:~$ vi ~/.bashrc
  • Paste this into your bash profile. This loads ruby from rvm.
terminal .bashrc
1
[[ -s "$HOME/.rvm/scripts/rvm" ]] && . "$HOME/.rvm/scripts/rvm"
  • Save and exit your .bashrc with :wq

  • Refresh your .bashrc.

terminal
1
deployer@localhost:~$ . !$
  • Check to see that rvm and Ruby installed. If they were, you’ll see their location.
terminal
1
2
3
4
5
6
deployer@localhost:~$ which rvm
/home/deployer/.rvm/bin/rvm

# In my case ruby didn't install. Hence no response.
deployer@localhost:~$ which ruby
deployer@localhost:~$
  • Ruby usually gets installed rvm, but mine happened to fail. So I’ll install Ruby 1.9.3.
terminal
1
2
3
4
5
6
7
8
9
deployer@localhost:~$ rvm install 1.9.3

... a whole bunch of stuff ...

# When it finishes. Check your ruby location and version.
deployer@localhost:~$ which ruby
/home/deployer/.rvm/rubies/ruby-1.9.3-p194/bin/ruby
deployer@localhost:~$ ruby -v
ruby 1.9.3p194 (2012-04-20 revision 35410) [i686-linux]

Setup Github and Key

You can choose to setup your keys in a few different ways, this is just one method.

  • Connect to github through your box and accept github as a trusted source.
terminal
1
2
3
4
5
6
deployer@localhost:~$ ssh git@github.com
The authenticity of host 'github.com (207.97.227.239)' can't be established.
RSA key fingerprint is 16:27:ac:a5:76:28:2d:36:63:1b:56:4d:eb:df:a6:48.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added 'github.com,207.97.227.239' (RSA) to the list of known hosts.
Permission denied (publickey).
  • Setup your Key
terminal
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
deployer@localhost:~$ cd ~/.ssh
deployer@localhost:~/.ssh$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/home/deployer/.ssh/id_rsa): # Push enter
Enter passphrase (empty for no passphrase): # Push enter
Enter same passphrase again: # Push enter
Your identification has been saved in /home/deployer/.ssh/id_rsa.
Your public key has been saved in /home/deployer/.ssh/id_rsa.pub.
The key fingerprint is:
# ... some stuff will be here with a little graphic ...
+-------------------+
|                   |
|                   |
|                   |
|                   |
|                   |
|                   |
|                   |
|                   |
|                   |
|                   |
|                   |
|                   |
+-------------------+
  • Check to see if your pub key is there.
terminal
1
2
deployer@localhost:~/.ssh$ ls
id_rsa  id_rsa.pub  known_hosts
  • Print your pub key, then copy it ( I used my mouse to highlight and copy it ).
terminal
1
2
3
4
5
deployer@localhost:~/.ssh$ cat id_rsa.pub
ssh-rsa AxWHOLExBUNCHxOFxRANDOMxLETTERSxANDxNUMBERSxsANDxTHENxSOMExMOREfdsa

# Exit the directory
deployer@localhost:~/.ssh$ cd

Add Capistrano to your App

  • Modify your Gemfile
Gemfile
1
2
gem 'capistrano'
gem 'capistrano-recipes'
  • Install the bundle
terminal
1
$ bundle install
  • Create your deploy.rb by ‘capifying’ your app.
terminal
1
2
3
4
$ capify .
[add] writing './Capfile'
[add] writing './config/deploy.rb'
[done] capified!

Modify /Capfile and /config/deploy.rb

  • In your App, uncomment the asset-pipeline option in /Capfile.
/Capfile
1
2
3
4
5
load 'deploy'
# Uncomment if you are using Rails' asset pipeline
load 'deploy/assets' #uncomment this line.
Dir['vendor/gems/*/recipes/*.rb','vendor/plugins/*/recipes/*.rb'].each { |plugin| load(plugin) }
load 'config/deploy' # remove this line to skip loading any of the default tasks
  • Add capistano to your repo.
terminal
1
2
3
$ git add .
$ git commit -m "Added capistrano."
$ git push origin master

Add Unicorn and Nginx Files to your App

  • Modify your Gemfile
Gemfile
1
gem 'unicorn'
  • Install the bundle
terminal
1
$ bundle install

You’ll need to add 3 files to your App’s config folder. 1. nginx.conf 2. unicorn.rb 3. unicorn_init.sh

/config/unicorn.rb

  • Edit /config/unicorn.rb to reflect your app’s name.
/config/unicorn.rb
1
2
3
4
5
6
7
8
9
root = "/home/deployer/apps/capteste/current"
working_directory root
pid "#{root}/tmp/pids/unicorn.pid"
stderr_path "#{root}/log/unicorn.log"
stdout_path "#{root}/log/unicorn.log"

listen "/tmp/unicorn.capteste.sock"
worker_processes 2
timeout 30

/config/unicorn_init.sh

I was able to use Railscasts’ unicorn_init.sh without modification.

  • Mark unicorn_init.sh as an executible.
terminal
1
$ chmod +x config/unicorn_init.sh
  • Check these into github
terminal
1
2
3
$ git add .
$ git commit -m "Added unicorn and nginxconf."
$ git push origin master

Database.yml and .gitignore

In the railscast, Ryan makes a fresh app and adds /config/database.yml to the .gitignore before his first commit. Capteste wasn’t in the same situation, so we had change my approach a bit.

Note: This approach may not be the most efficient/effective, but it worked.

  • Create the example template, as directed.
terminal
1
$ cp config/database.yml config/database.example.yml
  • Add database.yml to .gitignore
/.gitignore
1
/config/database.yml
  • Delete /config/database.yml and check that into git.
terminal
1
2
3
$ git rm /config/database.yml
$ git add .
$ git commit -m "deleted database.yml"
  • Copy the example template you made a moment ago to recreate /config/database.yml.
terminal
1
$ cp config/database.example.yml config/database.yml
  • Check your git status to see that database.yml is not in there.
terminal
1
2
$ git status
## database.yml ## shouldn't be in there.

Setup the deploy

Give this a shot. If you’re lucky, it’ll work. If not, become good friends with either Google or @scottmuc.

terminal
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
$ cap deploy:setup

  * executing `deploy:setup'
  * executing "mkdir -p /home/deployer/apps/capteste /home/deployer/apps/capteste/releases /home/deployer/apps/capteste/shared /home/deployer/apps/capteste/shared/system /home/deployer/apps/capteste/shared/log /home/deployer/apps/capteste/shared/pids"
    servers: ["96.126.100.112"]
Password:
    [96.126.100.112] executing command
    command finished in 1462ms
  * executing "chmod g+w /home/deployer/apps/capteste /home/deployer/apps/capteste/releases /home/deployer/apps/capteste/shared /home/deployer/apps/capteste/shared/system /home/deployer/apps/capteste/shared/log /home/deployer/apps/capteste/shared/pids"
    servers: ["96.126.100.112"]
    [96.126.100.112] executing command
    command finished in 219ms
    triggering after callbacks for `deploy:setup'
  * executing `deploy:setup_config'
  * executing "sudo -p 'sudo password: ' ln -nfs /home/deployer/apps/capteste/current/config/nginx.conf /etc/nginx/sites-enabled/capteste"
    servers: ["96.126.100.112"]
    [96.126.100.112] executing command
 ** [out :: 96.126.100.112]
    command finished in 475ms
  * executing "sudo -p 'sudo password: ' ln -nfs /home/deployer/apps/capteste/current/config/unicorn_init.sh /etc/init.d/unicorn_capteste"
    servers: ["96.126.100.112"]
    [96.126.100.112] executing command
 ** [out :: 96.126.100.112]
    command finished in 309ms
  * executing "mkdir -p /home/deployer/apps/capteste/shared/config"
    servers: ["96.126.100.112"]
    [96.126.100.112] executing command
    command finished in 213ms
    servers: ["96.126.100.112"]
 ** sftp upload #<StringIO:0x007fd9f43039e8> -> /home/deployer/apps/capteste/shared/config/database.yml
    [96.126.100.112] /home/deployer/apps/capteste/shared/config/database.yml
    [96.126.100.112] done
  * sftp upload complete
Now edit the config files in /home/deployer/apps/capteste/shared.

Modify database.yml on your box

As directed in the last line above, we need to modify our configuation. Fortunately we only have 1-file to change.

  • Log back into your box as deployer and modify database.yml.
terminal
1
2
3
4
5
$ ssh deployer@96.126.100.112
deployer@96.126.100.112's password: # enter your password

# Open database.yml on your box
deployer@li333-112: vim ~/apps/capteste/shared/config/database.yml
  • Modify database.yml to include only production.
terminal vim database.yml
1
2
3
4
5
6
7
8
production:
  adapter: postgresql
  encoding: unicode
  database: capteste_production
  pool: 5
  host: localhost # ADD this line.
  username: capteste
  password: captestepasswordthing
  • Note: If you had to add databse.yml later on, the way we did in this app, then you’ll have to do this everytime you run $ cap deploy:setup.

Deploy:cold and :check

terminal
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ cap deploy:cold
  * executing `deploy:cold'
  * executing `deploy:update'
 ** transaction: start
  * executing `deploy:update_code'
    updating the cached checkout on all servers
    executing locally: "git ls-remote git@github.com:matthewcopeland/capteste.git master"
    command finished in 3801ms
  * executing "if [ -d /home/deployer/apps/capteste/shared/cached-copy ]; then cd /home/deployer/apps/capteste/shared/cached-copy && git fetch -q origin && git fetch --tags -q origin && git reset -q --hard 43fd9aea3d669a988e91213e58cb33efa2b0cd3a && git clean -q -d -x -f; else git clone -q git@github.com:matthewcopeland/capteste.git /home/deployer/apps/capteste/shared/cached-copy && cd /home/deployer/apps/capteste/shared/cached-copy && git checkout -q -b deploy 43fd9aea3d669a988e91213e58cb33efa2b0cd3a; fi"
    servers: ["96.126.100.112"]
Password: # Enter the deployer password

# ... lots of stuff .....

 command finished in 2252ms
  • You may have errors. Try this to check dependencies.
terminal
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ cap deploy:check
  * executing `deploy:check'
  * executing "test -d /home/deployer/apps/capteste/releases"
    servers: ["96.126.100.112"]
Password: # Enter your Password
    [96.126.100.112] executing command
    command finished in 2661ms
  * executing "test -w /home/deployer/apps/capteste"
    servers: ["96.126.100.112"]
    [96.126.100.112] executing command
    command finished in 204ms
  * executing "test -w /home/deployer/apps/capteste/releases"
    servers: ["96.126.100.112"]
    [96.126.100.112] executing command
    command finished in 647ms
  * executing "which git"
    servers: ["96.126.100.112"]
    [96.126.100.112] executing command
    command finished in 334ms
  * executing "test -w /home/deployer/apps/capteste/shared"
    servers: ["96.126.100.112"]
    [96.126.100.112] executing command
    command finished in 411ms
You appear to have all necessary dependencies installed

Remove old Nginx files

Nginx comes has a welcome screen (or a 500error in some cases when index.html isn’t present). You’ll need to login to your box as deployer, delete these files and make sure the app restarts correctly.

terminal
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
deployer@li333-112:~$ sudo rm /etc/nginx/sites-enabled/default
[sudo] password for deployer:
deployer@li333-112:~$ sudo service nginx restart
Restarting nginx: nginx.

# This helps to ensure that unicorn restarts correctly.
# Be sure to replace 'capteste' with your app's name.
deployer@li333-112:~$ sudo update-rc.d unicorn_capteste defaults
[sudo] password for deployer:
 Adding system startup for /etc/init.d/unicorn_capteste ...
   /etc/rc0.d/K20unicorn_capteste -> ../init.d/unicorn_capteste
   /etc/rc1.d/K20unicorn_capteste -> ../init.d/unicorn_capteste
   /etc/rc6.d/K20unicorn_capteste -> ../init.d/unicorn_capteste
   /etc/rc2.d/S20unicorn_capteste -> ../init.d/unicorn_capteste
   /etc/rc3.d/S20unicorn_capteste -> ../init.d/unicorn_capteste
   /etc/rc4.d/S20unicorn_capteste -> ../init.d/unicorn_capteste
   /etc/rc5.d/S20unicorn_capteste -> ../init.d/unicorn_capteste
deployer@li333-112:~$ sudo service nginx restart
Restarting nginx: nginx.
deployer@li333-112:~$ exit
logout
Connection to 96.126.100.112 closed.
  • Check your site in a browser.

Update your site and Deploy

  • Make a change to your app.

  • Push the change to github.

terminal
1
2
3
4
5
6
$ git add .
$ git commit -m "Making a change."
$ git push

# Deploy again
$ cap deploy

Next Steps

Pairing on GO! with @fjmartin this next Friday.

Thanks again

Another big thanks again to @udit for the awesome knowledge sharing and pairing. Another thanks to devops gurus like @scottmuc for making it look easy. Oh yeah, and the kind peeps who wrote the articles listed at the top.