Rails Server With Nginx and Puma Ubuntu 14.04 LTS


Part 1 - Setting up Ruby/Rails

Installing rbenv

We will use rbenv to be our Ruby version control. Let's update our app and then get the dependencies we need for our server.

sudo apt-get update
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

Once we have those dependencies, let's install rbenv.

cd ~
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
echo 'export PATH="$HOME/.rbenv/plugins/ruby-build/bin:$PATH"' >> ~/.bash_profile
source ~/.bash_profile

If you are using Ubuntu Desktop, instead of using .bash_profile use .bashrc.

Now that we have rbenv installed in our home directory of our current user, it's time to bring in the big guns.

Installing Ruby

Let's install the most recent version of ruby to date.

rbenv install -v 2.4.1
rbenv global 2.4.1

Using rbenv global will set our ruby version to the version we chose on all of our shells.

Let's verify that the Ruby version we chose is now the one we are using.

ruby -v

If you don't want Ruby Gems to install documentation on your production server, run the following command.

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

Now let's install bundler and rails.

gem install bundler
gem install rails

We now need to run a rehash so that rbenv loads our most current content.

rbenv rehash

Let's also verify that Rails was installed correctly and with what version.

rails -v

If you receive the version, it was installed successfully.

Installing Javascript Runtime

A few Rails features, such as the Asset Pipeline, depend on a Javascript runtime. We will install Node.js to provide this functionality.

sudo add-apt-repository ppa:chris-lea/node.js
sudo apt-get update
sudo apt-get install nodejs

Congrats! We now have everything installed on our system.

Installing MySQL

Rails uses sqlite3 as its default database, so let's get rid of that crap and get a better one.

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

Create Rails App

If you haven't created or cloned an app yet, let's do so now.

rails rew AppName -d mysql

Don't forget to run the following command and paste your encrypted secret key in your config/database.yml file for your production key.

rake secret

Part 2 - Setting Up Production Database

Before we move on, let's create our mysql user so we can apply it to our config/database.yml. Follow my other guide here to do so. Paste the username and password in the said config file so that our app will now use the newly created user. It would be good practice to name the user similar to your app name. You may also want do download the Dotenv ruby gem to store your credentials in, so they don't get pushed up to the version control you are using.

Let's create a production database which will be used later on and also set up the production side of the server.

rails db:create RAILS_ENV=production
rails db:migrate RAILS_ENV=production
rails assets:precompile RAILS_ENV=production

Part 3 - Setting Up Puma

Let's make sure Puma is in our Gemfile.

vim Gemfile

If not we will add it.

gem 'puma'

Then run bundle install.

bundle install

Puma Config

We are going to need to know how many processor cores we have to config puma with, you can find out your current setup with the following command.

grep -c processor /proc/cpuinfo

Now let's add our config to our puma file.

vim config/puma.rb

Add the following code into it below.

# Change to match your CPU core count
workers 2

# Min and Max threads per worker
threads 1, 6

app_dir = File.expand_path("../..", __FILE__)
shared_dir = "#{app_dir}/shared"

# Default to production
rails_env = ENV['RAILS_ENV'] || "production"
environment rails_env

# Set up socket location
bind "unix://#{shared_dir}/sockets/puma.sock"

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

# Set master PID and state locations
pidfile "#{shared_dir}/pids/puma.pid"
state_path "#{shared_dir}/pids/puma.state"
activate_control_app

on_worker_boot do
  require "active_record"
  ActiveRecord::Base.connection.disconnect! rescue ActiveRecord::ConnectionNotEstablished
  ActiveRecord::Base.establish_connection(YAML.load_file("#{app_dir}/config/database.yml")[rails_env])
end

Make sure to change the workers to how many cpu cores your computer has. Save the file and then exit out. We are now going to create the files we mentioned above in the code snippet.

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

Puma Upstart Script

Let's create a script now so that we can start and stop puma with ease.

cd ~
wget https://raw.githubusercontent.com/puma/puma/master/tools/jungle/upstart/puma-manager.conf
wget https://raw.githubusercontent.com/puma/puma/master/tools/jungle/upstart/puma.conf

Now we need to update our puma.conf with our current ubuntu user that will be doing all the work.

vim puma.conf

Replace the following section in the file with your deploy user's name.

setuid YourUser
setgid YourUser

Save and exit. We are now going to copy the script into our /etc/init directory.

sudo cp puma.conf puma-manager.conf /etc/init

The puma-manager.conf script refers to /etc/puma.conf for the applications that it should handle. Let's create that handle file.

sudo vim /etc/puma.conf

Each line in the file should reference a path that you want the puma manger to handle. You simple just put the path to each of your projects in the format below on a separate line.

/path/to/appname

After you save and exit, your application will now start on Upstart, which means that after a reboot, or anything else, your server will automatically restart.

Here are some commands to manually start using puma manger.

sudo start puma-manager
sudo stop puma-manager
sudo restart puma-manager

# For a single server startup
sudo start puma app=/path/to/appname

Our rails production setup is now ready and is listening through puma's shared/sockets/puma.sock socket. There is just one more step to get our server to be accessible to the outside world, we now have to set up a Nginx reverse proxy.

Part 4 - Install Nginx

Install Nginx with the following command.

sudo apt-get install nginx

Now let's edit the default site available config to get our app up and running.

sudo vim /etc/nginx/sites-available/default

Your file is going to have already default config, replace it with the following code. If you are wanting ssl, you will have to do it differently. This setup is for only port 80, http. For https, I will be writing another guide for how to set that up.

upstream app {
  server unix:/path/to/appname/shared/sockets/puma.sock fail_timeout=0;
}

server {
  listen 80;
  listen [::]:80;
  server_name localhost;

  root /path/to/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;

  location ~*  \.(jpg|jpeg|png|gif|ico|css|js)$ {
    expires 365d;
  }
}

server {
  listen 443 ssl;
  listen [::]:443 ssl;
  server_name www.app.com;
  ssl on;
  ssl_certificate /etc/letsencrypt/live/app.com/fullchain.pem; # managed by Certbot
  ssl_certificate_key /etc/letsencrypt/live/app.com/privkey.pem; # managed by Certbot
  ssl_protocols TLSv1.1 TLSv1.2 TLSv1;
  ssl_prefer_server_ciphers on;
  ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!3DES:!MD5:!PSK';
  ssl_session_cache shared:SSL:10m;
  ssl_session_timeout 10m;
  ssl_dhparam /home/ubuntu/ssl_certs/dhparam.pem;

  add_header Strict-Transport-Security "max-age=31536000; includeSubdomains; preload";
  return 301 https://app.com$request_uri;
}

server {
  listen 443 ssl;
  listen [::]:443 ssl;
  server_name app.com;
  add_header Strict-Transport-Security "max-age=31536000; includeSubdomains; preload";
  ssl on;
  ssl_certificate /etc/letsencrypt/live/app.com/fullchain.pem; # managed by Certbot
  ssl_certificate_key /etc/letsencrypt/live/app.com/privkey.pem; # managed by Certbot
  ssl_protocols TLSv1.1 TLSv1.2 TLSv1;
  ssl_prefer_server_ciphers on;
  ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!3DES:!MD5:!PSK';
  ssl_session_cache shared:SSL:10m;
  ssl_session_timeout 10m;
  ssl_dhparam /home/ubuntu/ssl_certs/dhparam.pem;

  root /home/ubuntu/app/public;

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

  location @app {
    proxy_pass http://app;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Host $host:443;
    proxy_set_header X-Forwarded-Server $host;
    proxy_set_header X-Forwarded-Port 443;
    proxy_set_header X-Forwarded-Proto https;
  }

  location /cable {
    proxy_pass http://app;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "Upgrade";
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-Proto https;
    proxy_redirect off;
  }

  location ^~ /.well-known/ {
    root /usr/share/nginx/html;
  }

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

  location ~*  \.(jpg|jpeg|png|gif|ico|css|js)$ {
    expires 365d;
  }
}

After pasting this code, exit and save. Then we will restart the Nginx server and our website will now be fully functional!

sudo service nginx restart

Now visit your public ip address.

http://public_ip_address_or_domain_name

Make sure gzip is enabled for compression. Feel free to enable additional settings.

sudo vim /etc/nginx/nginx.conf
##
# Gzip Settings
##

gzip on;
gzip_disable "msie6";
gzip_proxied any;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
# gzip_vary on;
# gzip_comp_level 6;
# gzip_buffers 16 8k;
# gzip_http_version 1.1;

Upgrading Nginx

If you find your nginx outdated after a while, update it.

sudo add-apt-repository ppa:nginx/stable
sudo apt-get update
sudo apt-get install nginx

SSHGuard

sudo apt-get install sshguard

This monitors your system and protects against brute force attacks and blocks users ip address with a firewall rule if it notices any suspicious activity.

Systemd Ubuntu 16.04

[Unit]
Description=Puma HTTP Server
After=network.target

# Uncomment for socket activation (see below)
#Requires=puma.socket

[Service]
# Foreground process (do not use --daemon in ExecStart or config.rb)
Type=simple

# Preferably configure a non-privileged user
User=ubuntu
Group=ubuntu

# Specify the path to your puma application root
WorkingDirectory=/home/ubuntu/downloadsecurely

# Helpful for debugging socket activation, etc.
# Environment=PUMA_DEBUG=1
# EnvironmentFile=/home/deployer/app/.env

# The command to start Puma
# ExecStart=<WD>/sbin/puma -b tcp://0.0.0.0:9292 -b ssl://0.0.0.0:9293?key=key.pem&cert=cert.pem
#ExecStart=/usr/local/bin/bundle exec --keep-file-descriptors puma -e production
#ExecStart=/home/ubuntu/.rbenv/shims puma -C /home/ubuntu/downloadsecurely
#ExecStart=/home/ubuntu/downloadsecurely/bin bundle exec puma --daemon
#ExecStart=/home/ubuntu/.rbenv/versions/2.5.0/bin bundle exec puma -C /home/ubuntu/downloadsecurely/config/puma.rb --daemon
#ExecStart=/home/ubuntu/downloadsecurely bundle exec puma 
#ExecStart=/home/ubuntu/.rbenv/shims bundle exec puma -C /home/ubuntu/downloadsecurely/config/puma.rb --daemon
#ExecStop=/home/ubuntu/.rbenv/shims bundle exec pumactl -S /home/ubuntu/downloadsecurely/shared/tmp/pids/puma.state stop
#PIDFile=/home/ubuntu/downloadsecurely/shared/tmp/pids/puma.pid
ExecStart=/home/ubuntu/.rbenv/bin/rbenv exec bundle exec puma -C /home/ubuntu/downloadsecurely/config/puma.rb
ExecStop=/home/ubuntu/.rbenv/bin/rbenv exec bundle exec pumactl -S /home/ubuntu/downloadsecurely/shared/tmp/pids/puma.state stop
Restart=always

[Install]
WantedBy=multi-user.target
# After installing or making changes to either puma.socket or
# puma.service.
systemctl daemon-reload

# Enable both socket and service so they start on boot.  Alternatively
# you could leave puma.service disabled and systemd will start it on
# first use (with startup lag on first request)
systemctl enable puma.socket puma.service

# Initial start up. The Requires directive (see above) ensures the
# socket is started before the service.
systemctl start puma.socket puma.service

# Check status of both socket and service.
systemctl status puma.socket puma.service

# A "hot" restart, with systemd keeping puma.socket listening and
# providing to the new puma (master) instance.
systemctl restart puma.service

# A normal restart, needed to handle changes to
# puma.socket, such as changing the ListenStream ports. Note
# daemon-reload (above) should be run first.
systemctl restart puma.socket puma.service

sudo vim /etc/systemd/system/puma.service