Skip to content

Instantly share code, notes, and snippets.

@aquaflamingo
Last active June 22, 2020 17:18
Show Gist options
  • Save aquaflamingo/5706f8677c41b00a9fecb0a59e228134 to your computer and use it in GitHub Desktop.
Save aquaflamingo/5706f8677c41b00a9fecb0a59e228134 to your computer and use it in GitHub Desktop.
Ubuntu 16.04 Unicorn+Nginx, PostgreSQL, active_storage, Amazon S3 on Digital Ocean

πŸš‚ Deploy Ubuntu 16.04 Rails Server on Digital Ocean

using Unicorn, Nginx, PostgreSQL, active_storage, Amazon S3. Combined summary of all guides.

πŸ’§ 1. Basic Droplet/SSH Set Up

Create Droplet: Ubuntu 16.04

Your root password is e-mailed to you.

Create SSH Key [Guide]

Enter .ssh directory:

$ cd ~/.ssh

Generate Key:

$ ssh-keygen

Save DigitalOcean (DO) key with created key-password

$ ~/.ssh/id_do_mac

Save key-password in password manager

Add Public Key To Digital Ocean [Guide]

Copy public key to clip board:

$ cat ~/.ssh/id_do_mac.pub | pbcopy

Add SSH Key to Digital Ocean Account Name MacOS:

Home>Security>Add SSH Key


πŸ’» 2. Initial Server Setup [Guide]

Create Non Root User

    $ ssh root@your_server_ip
    $ adduser deploy

Create deploy-password add to password manager.

Give deploy root privileges:

    $ usermod -aG sudo deploy

Add Public Key Auth to Your Server

Use already created local SSH key and add to the server.

    ## Local Machine
    $ cat ~/.ssh/id_do_mac.pub | pbcopy

Paste SSH Key to Deployed User's Authorized Keys

    ## Server
    $ su - deploy
    $ mkdir ~/.ssh
    $ chmod 700 ~/.ssh
    $ nano ~/.ssh/authorized_keys 
        # Paste Key

Change permissions back:

    $ chmod 600 ~/.ssh/authorized_keys

Remove Password Auth

    $ sudo nano /etc/ssh/sshd_config

Find: PasswordAuthentication change to

    PasswordAuthentication no

Reload: sudo systemctl reload sshd

Test Login: ssh deploy@server_ip

Add Firewall

    $ sudo ufw allow OpenSSH
    $ sudo ufw enable
    # Enter: y
    $ sudo ufw status
    # Status: active
    $ sudo ufw allow 80/tcp
    $ sudo ufw allow 443/tcp

πŸ’Ž 3. Install Ruby on Rails & rbenv [Guide]

Login as deploy (i.e. ssh deploy@server_ip or su - deploy)

Add dependencies, rbenv & rails

Install nodejs dependence for Asset Pipeline

    $ curl -sL https://deb.nodesource.com/setup_8.x -o nodesource_setup.sh 
    $ sudo bash nodesource_setup.sh
    $ sudo apt-get install nodejs
    $ nodejs -v 
        # Output: v8.10.0

Ruby + rbenv dependencies`

    $ sudo apt-get update
    $ sudo apt-get install autoconf bison build-essential libssl-dev libyaml-dev libreadline6-dev zlib1g-dev libncurses5-dev libffi-dev libgdbm3 libgdbm-dev

Add rbenv & ruby-build

    $ git clone https://github.com/rbenv/rbenv.git ~/.rbenv
    $ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc
    $ echo 'eval "$(rbenv init -)"' >> ~/.bashrc
    $ source ~/.bashrc
    $ type rbenv
        # output: ...
    $ git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build

Install ruby 2.5.0 & gems

    $ rbenv install 2.5.0
    $ rbenv global 2.5.0
    $ echo "gem: --no-document" > ~/.gemrc
        # No Docs
    $ gem install bundler
    $ gem install rails
        # rails -v: Rails 5.2

🐘 4. Add PostgreSQL

Add dependencies:

    $ sudo apt-get install postgresql postgresql-contrib libpq-dev

Create user (this will be same user in your database.yml, username field for the production tag):

    $ sudo -u postgres createuser -s <APPNAME>

Set password for APPNAME

    $ sudo -u postgres psql
    $ \password <APPNAME>

Save Postgres User and Password in password manager

πŸ”‘ 5. Rails Encrypted Credentials & Active Storage

(for Database Keys, S3 and Secrets)

ON SERVER:

Install rbenv-vars for environment variable management.

    $ cd ~/.rbenv/plugins
    $ git clone https://github.com/sstephenson/rbenv-vars.git

ON LOCAL TERMINAL/PROJECT:

Generate config/master.key for Rails Encrypted Credentials by editing credentials

    bin/rails credentials:edit
        # generates credentials.yml.enc

Adding Postgres Database

Add the postgres production database password to credentials.yml.enc.

    ## credentials.ymc.enc
    database:
        production_password: <PASSWORD-CREATED-IN-STEP-3-ABOVE>

Edit database.yml

    ## database.yml
    production:
        adapter: postgresql
        encoding: unicode
        database: <APPNAME_production>
        host: localhost
        pool: 5
        username: <APPNAME-CREATED-IN-STEP-3-ABOVE>
        password: <%= Rails.application.credentials.dig(:database, :production_password) %>

    ## ...

Copy the RAILS_MASTER_KEY from config/master.key. Save RAILS_MASTER_KEY in Password Manager

    $ cat config/master.key
        # cfda33e2583...

ON SERVER:

Edit rbenv-vars to add the RAILS_MASTER_KEY (because Encrypted Credentials looks for ENV['RAILS_MASTER_KEY'])

    $ nano .rbenv-vars
    ## .rbenv-vars
    RAILS_MASTER_KEY=<MASTER-KEY-HERE>

Adding Amazon S3

  1. Create S3 Bucket

  2. Create an S3 Access Key - Save ACCESS_KEY_ID and SECRET_ACCESS_KEY in Password Manager

ON LOCAL TERMINAL/PROJECT:

Add S3 Keys to Encrypted Credentials

    bin/rails credentials:edit
    # credentials.yml.enc
    aws:
        access_key_id: <s3_key_id>
        secret_access_key: <s3_secret>

Edit storage.yml

    ## storage.yml
    amazon:
        service: S3
        access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
        secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
        region: us-east-1
        bucket: <%= Rails.application.credentials.dig(:aws, :bucket) %>
    ## ...

Add, Commit & Push Changes to Remote Repo

    $ git add -A 
    $ git commit -m "Add and encrypt deploy credentials"
    $ git push origin master

ON SERVER:

Pull Git Repo with Rails app:

    $ git clone https://github.com/username/repo
    $ bundle install
        # Installed

Build Database, and Production Environment

    $ RAILS_ENV=production rake db:create
    $ RAILS_ENV=production rake db:migrate
    $ RAILS_ENV=production rake assets:precompile
    $ RAILS_ENV=production rails server --binding=server_public_IP
        # test http://server_ip:3000

If test fails allow firewall to port 3000 temporarily1:

    $ sudo ufw allow 3000

πŸ¦„ 6. Installing Unicorn [Guide] OR [Passenger Phusion Guide]

Add gem 'unicorn' to Gemfile and bundle.

Edit config/unicorn.rb paste Appendix B: config/unicorn.rb

Save and add logging to Rails App:

    $ mkdir -p shared/pids shared/sockets shared/log

Edit and add unicorn init script from Appendix C: Unicorn Init Script:

    $ sudo nano /etc/init.d/unicorn_APPNAME

Ensure You Change APPNAME in Init Script to actual app

Update permissions for init script:

    $ sudo chmod 755 /etc/init.d/unicorn_appname
    $ sudo update-rc.d unicorn_appname defaults

Run via:

    sudo service unicorn_appname start

If run fails and systemctl reveals little, ensure that you rbenv's installation added the RBENV_ROOT properly. Otherwise edit your deploy ~/.profile file and add the following and resart the service.

    $ export RBENV_ROOT=/home/YOUR_USER_PATH/.rbenv
    $ export PATH=$RBENV_ROOT/shims:$RBENV_ROOT/bin:$PATH

🎑 7. Installing NGINX Reverse Proxy [Guide]

    $ sudo apt-get install nginx

Add NGINX reverse proxy config with block in Appendix D: NGINX Reverse Proxy

    $ sudo vi /etc/nginx/sites-available/default

Ensure You Change APPNAME in NGINX to actual app

Restart NGINX

    $ sudo service nginx restart
        # try: http://server_public_IP

πŸ™€ 8. Complete

Make sure you have all passwords: Appendix A: Inside Password Manager

All System Operational.

Appendix:

A. Inside Password Manager:

    Digital_Ocean:
        root_pass:
        deploy_pass:
    
    Postgres:
        appname:
        appname_password:
    
    Rails_Credentials
        RAILS_MASTER_KEY:
    
    AWS:
        access_key_id:
        secret_access_key:
    # Configures Unicorn with the location of your application, and the location of its socket, logs, and PIDs.
    # set path to application
    app_dir = File.expand_path("../..", __FILE__)
    shared_dir = "#{app_dir}/shared"
    working_directory app_dir


    # Set unicorn options
    worker_processes 2
    preload_app true
    timeout 30

    # Set up socket location
    listen "#{shared_dir}/sockets/unicorn.sock", :backlog => 64

    # Logging
    stderr_path "#{shared_dir}/log/unicorn.stderr.log"
    stdout_path "#{shared_dir}/log/unicorn.stdout.log"

    # Set master PID location
    pid "#{shared_dir}/pids/unicorn.pid"

Replace APPNAME

    #!/bin/sh

    ### BEGIN INIT INFO
    # Provides:          unicorn
    # Required-Start:    $all
    # Required-Stop:     $all
    # Default-Start:     2 3 4 5
    # Default-Stop:      0 1 6
    # Short-Description: starts the unicorn app server
    # Description:       starts unicorn using start-stop-daemon
    ### END INIT INFO

    set -e

    USAGE="Usage: $0 <start|stop|restart|upgrade|rotate|force-stop>"

    # app settings
    USER="deploy"
    APP_NAME="appname"
    APP_ROOT="/home/$USER/$APP_NAME"
    ENV="production"

    # environment settings
    PATH="/home/$USER/.rbenv/shims:/home/$USER/.rbenv/bin:$PATH"
    CMD="cd $APP_ROOT && bundle exec unicorn -c config/unicorn.rb -E $ENV -D"
    PID="$APP_ROOT/shared/pids/unicorn.pid"
    OLD_PID="$PID.oldbin"

    # make sure the app exists
    cd $APP_ROOT || exit 1

    sig () {
    test -s "$PID" && kill -$1 `cat $PID`
    }

    oldsig () {
    test -s $OLD_PID && kill -$1 `cat $OLD_PID`
    }

    case $1 in
    start)
        sig 0 && echo >&2 "Already running" && exit 0
        echo "Starting $APP_NAME"
        su - $USER -c "$CMD"
        ;;
    stop)
        echo "Stopping $APP_NAME"
        sig QUIT && exit 0
        echo >&2 "Not running"
        ;;
    force-stop)
        echo "Force stopping $APP_NAME"
        sig TERM && exit 0
        echo >&2 "Not running"
        ;;
    restart|reload|upgrade)
        sig USR2 && echo "reloaded $APP_NAME" && exit 0
        echo >&2 "Couldn't reload, starting '$CMD' instead"
        $CMD
        ;;
    rotate)
        sig USR1 && echo rotated logs OK && exit 0
        echo >&2 "Couldn't rotate logs" && exit 1
        ;;
    *)
        echo >&2 $USAGE
        exit 1
        ;;
    esac

Replace APPNAME

    upstream app {
        # Path to Unicorn SOCK file, as defined previously
        server unix:/home/deploy/<APPNAME>/shared/sockets/unicorn.sock fail_timeout=0;
    }

    server {
        listen 80;
        server_name localhost;

        root /home/deploy/<APPNAME>/public;

        try_files $uri/index.html $uri @app;

        location @app {
            proxy_pass http://app;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header Host $http_host;
            proxy_redirect off;
        }

        error_page 500 502 503 504 /500.html;
        client_max_body_size 4G;
        keepalive_timeout 10;
    }
    # Configures Unicorn with the location of your application, and the location of its socket, logs, and PIDs.
    # set path to application
    app_dir = File.expand_path("../..", __FILE__)
    shared_dir = "#{app_dir}/shared"
    working_directory app_dir


    # Set unicorn options
    worker_processes 2
    preload_app true
    timeout 30

    # Set up socket location
    listen "#{shared_dir}/sockets/unicorn.sock", :backlog => 64

    # Logging
    stderr_path "#{shared_dir}/log/unicorn.stderr.log"
    stdout_path "#{shared_dir}/log/unicorn.stdout.log"

    # Set master PID location
    pid "#{shared_dir}/pids/unicorn.pid"
    Digital_Ocean:
        root_pass:
        deploy_pass:
    
    Postgres:
        appname:
        appname_password:
    
    Rails_Credentials
        RAILS_MASTER_KEY:
    
    AWS:
        access_key_id:
        secret_access_key:

upstream app { # Path to Unicorn SOCK file, as defined previously server unix:/home/deploy//shared/sockets/unicorn.sock fail_timeout=0; }

server {
    listen 80;
    server_name localhost;

    root /home/deploy/<APPNAME>/public;

    try_files $uri/index.html $uri @app;

    location @app {
        proxy_pass http://app;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_redirect off;
    }

    error_page 500 502 503 504 /500.html;
    client_max_body_size 4G;
    keepalive_timeout 10;
}

#!/bin/sh

### BEGIN INIT INFO
# Provides:          unicorn
# Required-Start:    $all
# Required-Stop:     $all
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: starts the unicorn app server
# Description:       starts unicorn using start-stop-daemon
### END INIT INFO

set -e

USAGE="Usage: $0 <start|stop|restart|upgrade|rotate|force-stop>"

# app settings
USER="deploy"
APP_NAME="appname"
APP_ROOT="/home/$USER/$APP_NAME"
ENV="production"

# environment settings
PATH="/home/$USER/.rbenv/shims:/home/$USER/.rbenv/bin:$PATH"
CMD="cd $APP_ROOT && bundle exec unicorn -c config/unicorn.rb -E $ENV -D"
PID="$APP_ROOT/shared/pids/unicorn.pid"
OLD_PID="$PID.oldbin"

# make sure the app exists
cd $APP_ROOT || exit 1

sig () {
test -s "$PID" && kill -$1 `cat $PID`
}

oldsig () {
test -s $OLD_PID && kill -$1 `cat $OLD_PID`
}

case $1 in
start)
    sig 0 && echo >&2 "Already running" && exit 0
    echo "Starting $APP_NAME"
    su - $USER -c "$CMD"
    ;;
stop)
    echo "Stopping $APP_NAME"
    sig QUIT && exit 0
    echo >&2 "Not running"
    ;;
force-stop)
    echo "Force stopping $APP_NAME"
    sig TERM && exit 0
    echo >&2 "Not running"
    ;;
restart|reload|upgrade)
    sig USR2 && echo "reloaded $APP_NAME" && exit 0
    echo >&2 "Couldn't reload, starting '$CMD' instead"
    $CMD
    ;;
rotate)
    sig USR1 && echo rotated logs OK && exit 0
    echo >&2 "Couldn't rotate logs" && exit 1
    ;;
*)
    echo >&2 $USAGE
    exit 1
    ;;
esac
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment