Ubuntu 18.04 Nginx, Passenger, Puma, Rails


Install RVM

We should install a version manager to keep our ruby versions and gems.

sudo apt-get install libgdbm-dev libncurses5-dev automake libtool bison libffi-dev
gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB
curl -sSL https://get.rvm.io | bash -s stable
source ~/.rvm/scripts/rvm
rvm install 2.5.1
rvm use 2.5.1 --default
ruby -v

Swap File

You should create a swap file if your system does not have enough memory.

# Create swapfile
sudo fallocate -l 1G /swapfile

# The resulting swap file is world-readable and proper permissions should be set to prevent 
# unwanted access to this file
sudo chown root:root /swapfile 
sudo chmod 0600 /swapfile

# Add the file to the swap system
sudo swapon /swapfile

# Format the file for swap
sudo mkswap /swapfile

# Adds line to the end of /etc/fstab to make the change permanent
sudo bash -c "echo '/swapfile       none    swap    sw      0       0 ' >> /etc/fstab"

# Checks that swap file was created
sudo swapon -s

# Restart server
sudo reboot

To remove an old swapfile, use the following commands. The first command removes its entry from the /etc/fstab file.

sudo swapoff -v /swapfile
sudo rm /swapfile

Checking Hard Drive Space

user@ip:~⟫ du -hsc * /home/user/
105M    file
4.0K    file.txt
121M    file_2
16K     dir_1
866M    /home/user/
1.1G    total

user@ip:~⟫ df -h
Filesystem      Size  Used Avail Use% Mounted on
udev            488M     0  488M   0% /dev
tmpfs           100M  3.0M   97M   4% /run
/dev/xvda1      7.7G  5.2G  2.5G  68% /
tmpfs           496M   80K  496M   1% /dev/shm
tmpfs           5.0M     0  5.0M   0% /run/lock
tmpfs           496M     0  496M   0% /sys/fs/cgroup
tmpfs           100M     0  100M   0% /run/user/1000

Install MySQL

Get the package and then run the secure installation.

sudo apt install mysql-server libmysqlclient-dev
sudo mysql_secure_installation

Add a database and database user.

mysql -u root -p

create database devdb;
create user 'devuser'@'localhost' identified by 'password';
grant all on devdb.* to 'devuser' identified by 'password';
grant all privileges on *.* to 'devuser'@'localhost';

Generate SSH key and Add Your Key

ssh-keygen -t rsa -b 4096 -C "email@gmail.com"
vim .ssh/id_rsa.pub
git clone git@gitlab.com:user/app.git

Add your secrets and environment variables.

cd app/
vim config/secrets.yml
vim .env

Install NodeJS

sudo apt-get install nodejs

Install Your Projects Gems And Setup Production Server

bundle install
rails db:migrate RAILS_ENV=production
rails assets:precompile RAILS_ENV=production
rails db:seed RAILS_ENV=production

Install Docker

sudo apt install apt-transport-https ca-certificates curl software-properties-common
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
sudo apt update
apt-cache policy docker-ce
sudo apt install docker-ce
sudo systemctl status docker

If you want to run docker with your user without having to use sudo, then do the following steps.

sudo usermod -aG docker ${USER}
su - ${USER}
id -nG

or

sudo usermod -aG docker username

Installing Nginx

Let's get the nginx package. This should be installed first before Passenger!

sudo apt-get install nginx

Install Passenger

Let's install passenger and add the apt keys to our file.

sudo apt-get install -y dirmngr gnupg
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 561F9B9CAC40B2F7
sudo apt-get install -y apt-transport-https ca-certificates
sudo sh -c 'echo deb https://oss-binaries.phusionpassenger.com/apt/passenger bionic main > /etc/apt/sources.list.d/passenger.list'
sudo apt-get update
sudo apt-get install -y libnginx-mod-http-passenger

Check if the config files are in their place.

if [ ! -f /etc/nginx/modules-enabled/50-mod-http-passenger.conf ]; then sudo ln -s /usr/share/nginx/modules-available/mod-http-passenger.load /etc/nginx/modules-enabled/50-mod-http-passenger.conf ; fi
sudo ls /etc/nginx/conf.d/mod-http-passenger.conf

Check installation.

sudo /usr/bin/passenger-config validate-install

Check memory stats.

sudo /usr/sbin/passenger-memory-stats
sudo /usr/sbin/passenger-status

If you don't see a file at /etc/nginx/conf.d/mod-http-passenger.conf; then you need to create it yourself and set the passenger_ruby and passenger_root config options. Or for newer versions at /etc/nginx/passenger.conf.

passenger_root /usr/lib/ruby/vendor_ruby/phusion_passenger/locations.ini;
passenger_ruby /home/sam/.rvm/gems/ruby-2.5.1/wrappers/ruby;

Let's create a virtual host now.

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

Add the following code to configure your website. If you want a different port, change the port number.

# /etc/nginx/sites-available/portfolio
server {
  listen 4000;
  listen [::]:4000;

  passenger_enabled on;
  root  /home/sam/websites/portfolio/public;

  server_name localhost;
}

Then create a symlink to your sites-enabled path.

sudo ln -s /etc/nginx/sites-available/portfolio /etc/nginx/sites-enabled/

Restart Nginx.

sudo service nginx restart

Add LetsEncrypt Auto Renewal

Follow the guide here.

Installing Puma with systemd

Once you have puma installed, then create a file called puma.service in the systemd directory.

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

Copy and paste the following code.

[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=sam

# The path to the puma application root
# Also replace the "<WD>" place holders below with this path.
WorkingDirectory=/home/sam/websites/portfolio

# Helpful for debugging socket activation, etc.
# Environment=PUMA_DEBUG=1

# The command to start Puma. This variant uses a binstub generated via
# `bundle binstubs puma --path ./sbin` in the WorkingDirectory
# (replace "<WD>" below)
#ExecStart=<WD>/sbin/puma -b tcp://0.0.0.0:9292 -b ssl://0.0.0.0:9293?key=key.pem&cert=cert.pem

# Variant: Use config file with `bind` directives instead:
ExecStart=/home/sam/.rvm/wrappers/ruby-2.5.1/puma -C /home/sam/websites/portfolio/config/puma.rb
# Variant: Use `bundle exec --keep-file-descriptors puma` instead of binstub

Restart=always

[Install]
WantedBy=multi-user.target

Then enable the service.

sudo systemctl enable puma.service

Create a puma.conf file that will specify the location of each app to start on each line.

sudo vim /etc/puma.conf

# /etc/puma.conf
/home/sam/websites/portfolio

Then create your virtual host.

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

Paste the following code.

# /etc/nginx/sites-available/portfolio
# Use localhost if you don't have a domain
upstream app {
 server unix:/home/sam/websites/portfolio/shared/sockets/puma.sock fail_timeout=0;
}

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

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

  root /home/sam/websites/portfolio/public;

  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;
  }

  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;
  }
}
# /etc/nginx/sites-available/portfolio
server {
  listen 80;
  listen [::]:80;
  server_name samwholst.com, www.samwholst.com;
  return 301 https://samwholst.com$request_uri;
  try_files $uri/index.html $uri @app;

  root /home/sam/websites/portfolio/public;

  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;
  }

  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;
  }
}

server {
  listen [::]:443 ssl; # managed by Certbot
  listen 443 ssl; # managed by Certbot
  include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
  server_name www.samwholst.com;
  ssl on;
  ssl_certificate /etc/letsencrypt/live/www.samwholst.com/fullchain.pem; # managed by Certbot
  ssl_certificate_key /etc/letsencrypt/live/www.samwholst.com/privkey.pem; # managed by Certbot
  ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
  add_header Strict-Transport-Security "max-age=31536000; includeSubdomains; preload";
  return 301 https://samwholst.com$request_uri;
}

server {
  listen [::]:443 ssl; # managed by Certbot
  listen 443 ssl; # managed by Certbot
  include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
  ssl on;
  ssl_certificate /etc/letsencrypt/live/www.samwholst.com/fullchain.pem; # managed by Certbot
  ssl_certificate_key /etc/letsencrypt/live/www.samwholst.com/privkey.pem; # managed by Certbot
  ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

  add_header Strict-Transport-Security "max-age=31536000; includeSubdomains; preload";
  server_name samwholst.com;

  root /home/sam/websites/portfolio/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;
  }

  location /status {
     # If you want to remove /status from the URL, use rewrite.
     rewrite   /status(.*)$  $1  break;
     proxy_pass http://${server_location};
     proxy_set_header X-Real-IP $remote_addr;
     proxy_set_header Host $host;
  }
}

Then create a symlink to the sites-enabled path.

sudo ln -s /etc/nginx/sites-available/portfolio /etc/nginx/sites-enabled/

Add a puma.rb file in your config directory of the app with the following code.

# Puma can serve each request in a thread from an internal thread pool.
# The `threads` method setting takes two numbers: a minimum and maximum.
# Any libraries that use thread pools should be configured to match
# the maximum value specified for Puma. Default is set to 5 threads for minimum
# and maximum; this matches the default thread size of Active Record.
#
threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
threads threads_count, threads_count

# Specifies the `port` that Puma will listen on to receive requests; default is 3000.
#
port        ENV.fetch("PORT") { 3000 }

# Specifies the `environment` that Puma will run in.
#
environment ENV.fetch("RAILS_ENV") { "development" }

# Specifies the number of `workers` to boot in clustered mode.
# Workers are forked webserver processes. If using threads and workers together
# the concurrency of the application would be max `threads` * `workers`.
# Workers do not work on JRuby or Windows (both of which do not support
# processes).
#
# workers ENV.fetch("WEB_CONCURRENCY") { 2 }

# Use the `preload_app!` method when specifying a `workers` number.
# This directive tells Puma to first boot the application and load code
# before forking the application. This takes advantage of Copy On Write
# process behavior so workers use less memory. If you use this option
# you need to make sure to reconnect any threads in the `on_worker_boot`
# block.
#
# preload_app!

# If you are preloading your application and using Active Record, it's
# recommended that you close any connections to the database before workers
# are forked to prevent connection leakage.
#
# before_fork do
#   ActiveRecord::Base.connection_pool.disconnect! if defined?(ActiveRecord)
# end

# The code in the `on_worker_boot` will be called if you are using
# clustered mode by specifying a number of `workers`. After each worker
# process is booted, this block will be run. If you are using the `preload_app!`
# option, you will want to use this block to reconnect to any threads
# or connections that may have been created at application boot, as Ruby
# cannot share connections between processes.
#
# on_worker_boot do
#   ActiveRecord::Base.establish_connection if defined?(ActiveRecord)
# end
#

# Allow puma to be restarted by `rails restart` command.
plugin :tmp_restart

# Change to match your CPU core count
workers 1

# 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

Then restart Nginx and the systemctl daemon.

sudo systemctl daemon-reload
sudo service nginx restart

Install Redis

Let's install the package. After we install it, we are going to tell systemd to manage it.

sudo apt-get update && sudo apt-get install redis-server

Enable the service

sudo systemctl enable redis-server.service

Now open the service file it creates and change supervised to be systemd.

sudo vim /etc/redis/redis.conf

# /etc/redis/redis.conf
supervised systemd

Exit and then restart redis.

sudo systemctl reload redis.service
sudo systemctl status redis
  • Image from neowin.net