June 22, 2016

Ultimate Rails on Debian Jessie Setup

Assumptions

  • Your app is called example.
  • Your domain name is example.com.

Disable SSH password connection

Edit /etc/ssh/sshd_config dans set:

PasswordAuthentication no

Make sure your public key has already been added by your provider:

cat .ssh/authorized_keys

Then restart SSH:

systemctl restart ssh

Create a deploy user

Create the user :

adduser deploy

Use a generated password and store it somewhere.

Then, create SSH keys for the created user (no passphrase):

su deploy
cd ~
ssh-keygen -t rsa

Upload the public key to BitBucket/Github:

cat .ssh/id_rsa.pub
exit

Setup public key authentication for deploy too.

As root:

cd ~
cp /root/.ssh/authorized_keys /home/deploy/.ssh/authorized_keys
chown deploy:deploy /home/deploy/.ssh/authorized_keys

You should be able the SSH without password for root and deploy now.

Base setup

As root:

apt-get update
apt-get upgrade

apt-get install zlib1g zlib1g-dev build-essential git-core curl emacs imagemagick nginx ntp
apt-get install mysql-client libmysqlclient-dev libopenssl-ruby1.9.1 libssl-dev libreadline-dev
apt-get install mysql-server monit unattended-upgrades logrotate memcached nodejs
apt-get install libcurl4-gnutls-dev libxml2 libxml2-dev libxslt1-dev ruby-dev
apt-get install mysql-client libmysqlclient-dev libssl-dev libreadline-dev screen
apt-get install libmagickcore-dev libmagickwand-dev graphicsmagick-libmagick-dev-compat

Set a generated password for mysql root and save it somewhere.

Enable automatic security updates

As root:

unattended-upgrades

Setup firewall

As root:

iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -p tcp -m tcp --dport 22 -j ACCEPT
iptables -A INPUT -p tcp -m tcp --dport 80 -j ACCEPT
iptables -A INPUT -p tcp -m tcp --dport 443 -j ACCEPT
iptables -A INPUT -p tcp -m tcp --dport 2812 -j ACCEPT
iptables -A INPUT -i lo -s 127.0.0.1 -j ACCEPT
iptables -A INPUT -p icmp --icmp-type 8 -m state --state NEW,ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -p icmp --icmp-type 0 -m state --state ESTABLISHED,RELATED -j ACCEPT

iptables -P INPUT DROP
iptables -P OUTPUT ACCEPT
iptables -P FORWARD DROP

iptables -L -n -v

Make rules persistent:

apt-get install iptables-persistent

service iptables-persistent save
service iptables-persistent start

Add some SWAP space (if needed)

Create a file and swap on it:

fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile

And check if its OK:

swapon -s
free

Make it persistent on /etc/fstab:

/swapfile   none    swap    sw    0   0

Install rbenv and ruby (as deploy)

export RUBY_VERSION=2.3.0

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

git clone git://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build
exec $SHELL -l

rbenv install $RUBY_VERSION
rbenv global $RUBY_VERSION
gem install bundler
rbenv rehash

wget https://raw.github.com/ryanb/dotfiles/master/gemrc -O .gemrc
echo 'export RAILS_ENV="production"' >> ~/.bash_profile

Configure default MySQL encoding (utfmb4)

Edit /etc/mysql/my.cnf and set these lines at the appropriate places:

[client]
default-character-set = utf8mb4

[mysql]
default-character-set = utf8mb4

[mysqld]
character-set-client-handshake = FALSE
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci

Then relaunch MySQL:

systemctl restart mysql

Create MySQL database for the project

Launch the MySQL CLI client:

mysql -u root -p

Use a generated password and store it somewhere. Then:

mysql> create database `example`;
mysql> create user deploy;
mysql> grant all on `example`.* to 'deploy'@'localhost' identified by 'SuperGeneratedPassword';
mysql> flush privileges;

Setup Capistrano in your project

Gemfile

group :development do
  gem 'capistrano'
  gem 'capistrano-rails'
  gem 'capistrano-rbenv'
  gem 'capistrano-bundler'
  gem 'capistrano-passenger', '0.0.2'
end

Capfile.rb

# Load DSL and Setup Up Stages
require 'capistrano/setup'
require 'capistrano/deploy'
require 'capistrano/bundler'
require 'capistrano/rbenv'
require 'capistrano/rails/migrations'
require 'capistrano/rails/assets'
require 'capistrano/passenger'
# require 'whenever/capistrano'

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

config/deploy.rb

set :scm,       :git
set :format,    :pretty
set :log_level, :debug
set :pty,       true

set :linked_files, []
set :linked_dirs,  %w{log tmp/pids tmp/cache tmp/sockets public/system public/assets private/system}

set :keep_releases, 20

set :rbenv_type, :user
set :rbenv_ruby, File.read('.ruby-version').strip

config/deploy/production.rb :

server 'example.com', user: 'deploy', roles: %w{web app db}

set :application,    'example'
set :repo_url,       'git@github.com:aurels/example.git'
set :deploy_to,      '/home/deploy/apps/example'
set :branch,         'master'
set :rails_env,      'production'
set :bundle_without, 'development test cucumber'

config/database.yml

production:
  adapter: mysql2
  database: example
  encoding: utf8
  username: deploy
  password: SuperGeneratedPassword
  pool: 1000

Configure nginx

Passenger

Install Phusion Passenger.

Server block

Create a file in /etc/nginx/sites-available/example :

server {
  listen 80;
  server_name example.com;

  add_header Strict-Transport-Security "max-age=63072000; preload"; # HSTS
  add_header X-Frame-Options           "DENY";                      # Deny iframes

  rewrite ^/(.*) https://example.com/$1 permanent;
}

server {
  listen 443;
  server_name example.com;

  ssl                 on;
  ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

  ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE
  ssl_prefer_server_ciphers on;
  ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:ECDHE-RSA-AES128-GCM-SHA256:AES256+EECDH:DHE-RSA-AES128-GCM-SHA256:AES256+EDH:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4";
  ssl_session_cache shared:SSL:10m;

  access_log /var/log/nginx/example.access.log;
  sendfile on;
  root /home/deploy/apps/example/current/public;

  gzip on;
  gzip_disable "msie6";

  passenger_enabled on;
  passenger_ruby /home/deploy/.rbenv/shims/ruby;
  passenger_app_env production;
  passenger_friendly_error_pages off;

  client_max_body_size 300M;
}

Enable the server block:

ln -s /etc/nginx/sites-available/example /etc/nginx/sites-enabled/example

Then restart nginx:

systemctl restart nginx

Deploy for the first time

bundle exec cap production deploy

Configure Certbot / Let’s Encrypt

Install Certbot :

https://certbot.eff.org/#debianjessie-nginx

certbot certonly --webroot -w /home/deploy/apps/example/current/public -d example.com

Setup automatic renewal in CRON with crontab -e:

32 5 * * * /usr/bin/certbot renew --no-self-upgrade --post-hook "service nginx restart"

Configure logrotate

In /etc/logrotate.d/example:

/home/deploy/apps/example/shared/log/*.log {
    daily
    missingok
    rotate 15
    compress
    delaycompress
    notifempty
    copytruncate
}

Setup ElasticSearch (option)

Setup repositories, then:

apt-get update
apt-get upgrade
apt-get update && sudo apt-get install elasticsearch
apt-get install openjdk-7-jre
systemctl start elasticsearch

Setup Monit

In /etc/monit/monitrc :

set daemon 10
set mailserver localhost
set httpd port 2812 and allow monit:APassWordForMonit

check filesystem homefs with path /dev/vda1

check process sshd with pidfile /var/run/sshd.pid

check process nginx with pidfile /var/run/nginx.pid
      start program = "/etc/init.d/nginx start"
      stop  program = "/etc/init.d/nginx stop"

check process mysql with pidfile /var/run/mysqld/mysqld.pid
      start program = "/etc/init.d/mysql start"
      stop  program = "/etc/init.d/mysql stop"

include /etc/monit/conf.d/*.cfg

Reload to apply:

monit reload

Setup backups

As root:

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

git clone git://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build
exec $SHELL -l

rbenv install 2.1.0
rbenv rehash
rbenv global 2.1.0

gem install backup
rbenv rehash

backup generate:config
backup generate:model -t example

Replace the contents of /root/Backup/models/example.rb with:

Model.new(:example, 'Backup of example') do
  ['example'].each do |db_name|
    database MySQL, db_name do |db|
      db.name     = db_name
      db.username = 'deploy'
      db.password = 'SuperGeneratedPassword'
    end
  end

  archive :files do |archive|
    archive.add '/home/deploy/apps/example/shared/public/system/'
    archive.add '/home/deploy/apps/example/shared/private/system/'
  end

  compress_with Gzip do |compression|
    compression.level = 6
  end

  store_with Dropbox do |box|
    box.api_key    = 'xxx'
    box.api_secret = 'yyy'
    box.path       = ''
    box.keep       = 60
  end

  notify_by Mail do |mail|
    mail.on_success = true
    mail.on_failure = true
    mail.from       = 'backup@example'
    mail.to         = ['me@mail.com']
    mail.domain     = 'example.com'
    mail.address    = 'smtp.sendgrid.net'
    mail.user_name  = 'example'
    mail.password   = 'zzz'
  end
end

And add this line to root’s CRONTAB with:

crontab -e

Add this line:

0 0,2,4,6,8,19,12,14,16,18,20,22,23 * * * /bin/bash -l -c 'backup perform --trigger example'

Vous voulez travailler avec moi ?

Je suis développeur et consultant sur des projets informatiques dans ma société, Phonoid. Avec mes amis, nous y développons des solutions sur mesure pour nos clients. Nous travaillons également sur nos futurs produits.