Guide to setup production Ruby On Rails application on Ubuntu 16.04 with rbenv, puma, mina, nginx, systemd, redis, PostgreSQL, MySQL

  1. Add new an user
  2. Configure access to server by ssh key
  3. Setting up stack
    • PostgreSQL
    • MySQL
    • Redis
    • Nginx
    • node.js
    • rbenv
    • Ruby and Bundler
  4. Deploy the application
    • setup a deploy config
    • setup database access
    • setup puma settings
    • setup systemd monitoring script
    • setup nginx server config
    • setup logs rotation

Tips: replace next names with your own.

  • advicy is your the app name.
  • your-server-ip-or-hostname is your ip or hostname of the server.

1. Add new user

Add specific user for every app that will be deployed to this server. A user name will be called same as the app name. In my case it’s ‘advicy’

sudo adduser advicy

Add new user to the sudo group to have sudo’s rights

sudo adduser advicy sudo

2. Configure access to server by ssh key

Sign new ssh session as created user ssh advicy@your-server-ip-or-hostname PasswordAuthentication in etc/ssh/sshd_config must be enabled but disable it after a public key will be added.

Create ssh folder

mkdir -p ~/.ssh && sudo chmod -R 700 ~/.ssh/

Generate ssh key on local machine with a 4096-bit key pair and name id_advicy

ssh-keygen -b 4096 -f ~/.ssh/id_advicy -C "your@email.com"

Copy the public key to the server

scp ~/.ssh/id_advicy.pub advicy@your-server-ip-or-hostname:~/.ssh/authorized_keys

Open your local ssh config and put settings for your server for easy access.

subl ~/.ssh/config

Put this:

  Host advicy
  User advicy
  Hostname your-server-ip-or-hostname
  IdentityFile ~/.ssh/id_advicy

Now access to the server by ssh advicy

*Make your access to the server more secured. *

sudo nano /etc/ssh/sshd_config

Disable a root access and disable access by a password.

PermitRootLogin no
PasswordAuthentication no

Restart the ssh service sudo systemctl restart sshd

3. Configure stack

First update the system sudo apt-get update && sudo apt-get upgrade

Install build-essential package to compile from source and git

sudo apt-get install build-essential git

Install PostgreSQL

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

Change postgres user’s password

sudo passwd postgres

Create postgres role and the database with same name as your user’s name

  sudo -u postgres createuser --interactive
  sudo -u postgres createdb advicy

Always answer ‘n’ no for all questions.

Enter name of role to add: advicy
Shall the new role be a superuser? (y/n) n
Shall the new role be allowed to create databases? (y/n) n
Shall the new role be allowed to create more new roles? (y/n) n

advicy user will don’t have ability to create new databases. Now it can use database with same name advicy. To create new database login under postgres user and create new database.

Install MySQL

sudo apt-get install mysql-server mysql-client libmysqlclient-dev

Secure your MySQL sudo mysql_secure_installation

Log in mysql console and create database and user mysql -u root -p

CREATE DATABASE advicy;
GRANT ALL ON advicy.* TO 'advicy' IDENTIFIED BY 'mysql-password';

Install Redis

sudo apt-get install redis-server

Install NGINX

sudo apt-get install nginx

Install Node.js v6

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

Install rbenv

Install necessary packages

sudo apt-get install libssl-dev libyaml-dev libreadline6-dev zlib1g-dev libncurses5-dev libffi-dev libgdbm3 libgdbm-dev

rbenv need to install for every new users. Be sure you logged in as your app user.

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

Install ruby-build

git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build

Install Ruby (2.3.1)

rbenv install 2.3.1

Setup it as default for this user

rbenv global 2.3.1

Install bundler and setting up gem to don’t generate docs.

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

Deploy an application with Mina

Add mina gem to development group of Gemfile

  gem 'mina', require: false
  gem 'mina-whenever', require: false
  gem 'mina-systemd', require: false

Create a deploy config mina init

Update the deploy config with your settings

require 'mina/bundler'
require 'mina/rails'
require 'mina/git'
require 'mina/rbenv'
require 'mina/whenever'
require 'mina/systemd'

set :domain, 'advicy'
set :deploy_to, '/home/advicy/apps/advicy'
set :repository, 'git@github.com:alexkojin/advicy.git'
set :branch, 'master'
set :rails_env, 'production'
set :user, 'advicy'

set :shared_dirs, fetch(:shared_dirs, []).push('tmp/pids', 'tmp/sockets')
set :shared_files, fetch(:shared_files, []).push('config/database.yml', 'config/puma.rb', 'config/secrets.yml')

task :environment do
  invoke :'rbenv:load'
end

task setup: :environment do
  command %[mkdir -p "#{fetch(:shared_path)}/config"]
  command %[chmod g+rx,u+rwx "#{fetch(:shared_path)}/config"]

  comment %{Be sure to add 'database.yml', 'secrets.yml' and 'puma.rb' in '#{fetch(:shared_path)}/config/' directory}
end

desc "Deploys the current version to the server."
task deploy: :environment do
  deploy do
    invoke :'git:clone'
    invoke :'deploy:link_shared_paths'
    invoke :'bundle:install'
    invoke :'rails:db_migrate'
    invoke :'rails:assets_precompile'
    invoke :'deploy:cleanup'

    on :launch do
      invoke :'systemctl:restart', 'advicy-puma'
      invoke :'systemctl:restart', 'advicy-bg-worker'
      invoke :'whenever:update'
    end
  end
end

Setting up access to a git repository from the server.

Go to the server and create a ssh key.

ssh-keygen -b 4096

Copy ~/.ssh/id_rsa.pub and add it to bitbucket.org, github.com or any other.

Create directory structure mina setup

Setup database.yml and puma.rb

Go to the server and update database.yml, secrets.yml, puma.rb

For MySQL /home/advicy/apps/advicy/shared/config/database.yml

production:
  adapter: mysql2
  database: advicy           
  encoding: utf8
  pool: 5
  username: advicy
  password: MYSQL-PASSWORD
  socket: /var/run/mysqld/mysqld.sock

For PostgreSQL /home/advicy/apps/advicy/shared/config/database.yml

production:
  adapter: postgresql
  encoding: unicode
  database: advicy
  pool: 5
  username: advicy
  password:

/home/advicy/apps/advicy/shared/config/puma.rb

workers 2
threads 2,8

environment 'production'
directory '/home/advicy/apps/advicy/current'
shared_dir = File.expand_path("../..", __FILE__)

stdout_redirect "#{shared_dir}/log/puma.stdout.log", "#{shared_dir}/log/puma.stderr.log", true

rackup DefaultRackup
bind "unix://#{shared_dir}/tmp/sockets/puma.sock"
pidfile "#{shared_dir}/tmp/pids/puma.pid"

prune_bundler

Setup a systemd monitoring script

sudo nano /etc/systemd/system/advicy-puma.service

  [Unit]
  Description=advicy Puma Server

  [Service]
  Type=simple
  User=advicy
  PIDFile=/home/advicy/apps/advicy/shared/tmp/pids/puma.pid
  WorkingDirectory=/home/advicy/apps/advicy/current
  Environment=RAILS_ENV=production
  ExecStart=/home/advicy/.rbenv/shims/bundle exec puma -e production -C ./config/puma.rb config.ru
  ExecReload=/bin/kill -s USR1 $MAINPID
  ExecStop=/bin/kill -s QUIT $MAINPID
  Restart=always

  [Install]
  WantedBy=multi-user.target

Reload systemd

sudo systemctl daemon-reload

and enable service to start automatically on system boot:

sudo systemctl enable advicy-puma.service

Add user priviligies to run systemctl under an user without sudo and password

sudo visudo -f /etc/sudoers.d/advicy

Put there the next code:

advicy ALL=NOPASSWD: /bin/systemctl start advicy-puma, /bin/systemctl stop advicy-puma, /bin/systemctl restart advicy-puma, /bin/systemctl status advicy-puma

Setup nginx config

sudo nano /etc/nginx/sites-available/advicy

upstream advicy {
  server unix:///home/advicy/apps/advicy/shared/tmp/sockets/puma.sock fail_timeout=0;
}

server {
  server_name www.advicy.com;
  return 301 $scheme://advicy.com$request_uri;
}

server {
  listen 80;
  server_name .advicy.com;

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

  root /home/advicy/apps/advicy/current/public;
  try_files $uri/index.html $uri @app;

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

  location ~ ^/assets/ {
    add_header Last-Modified "";
    add_header ETag "";
    gzip_static on;
    expires max;
    add_header Cache-Control public;
  }
}

sudo ln -s /etc/nginx/sites-available/advicy /etc/nginx/sites-enabled/advicy sudo systemctl restart nginx

Deploy

Finally we’re ready to deploy our application on the server

mina deploy or mina deploy --verbose for detailed output.

Rotate logs

Setup the app log’s rotation using logrotate utility

sudo nano /etc/logrotate.d/advicy

Put there the next code:

/home/advicy/apps/advicy/current/log/*.log {
  su advicy advicy
  daily
  size 100M
  missingok
  notifempty
  rotate 7
  compress
  delaycompress
  copytruncate
 }