Setting up Ruby on Rails with Passenger + Nginx in a CentOS 7 VM running on Google Cloud Platform

Perspective

All commands are written (unless explicitly stated) from the perspective of a non-root user with sudo permissions. The intent is to create a user which will run the application we are creating, but that user will not have sudo permissions.

Machine Setup

I started here:

The only real difference was that I select the CentOS 7 OS.

Tool Setup

From here:

sudo yum install -y curl gpg gcc gcc-c++ make

Install RVM:

sudo yum -y install tar which sudo yum -y install patch libyaml-devel libffi-devel glibc-headers autoconf gcc-c++ glibc-devel readline-devel zlib-devel openssl-devel bzip2 automake libtool bison
sudo gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB
curl -sSL https://get.rvm.io | sudo bash -s stable 
sudo usermod -a -G rvm `whoami`

Setup RVM secure_path:

if sudo grep -q secure_path /etc/sudoers; then sudo sh -c "echo export rvmsudo_secure_path=1 >> /etc/profile.d/rvm_secure_path.sh" && echo Environment variable installed; fi

Start a new shell and install the latest ruby and bundler:

bash
rvm install ruby
rvm --default use ruby
#Using /home/nik/.rvm/gems/ruby-2.6.3
gem install bundler

Then install Node.js for Ruby on Rails

sudo yum install -y epel-release
sudo yum install -y --enablerepo=epel nodejs npm

Nginx+Passenger+Ruby Setup

Then to get nginx + ruby running I followed directions here:

sudo yum install -y epel-release yum-utils
sudo yum-config-manager --enable epel
sudo yum clean all && sudo yum update -y

Date configuration:

date
# if the output of date is wrong, please follow these instructions to install ntp
sudo yum install -y ntp
sudo chkconfig ntpd on
sudo ntpdate pool.ntp.org
sudo service ntpd start

Install Passenger+Nginx

sudo yum install -y pygpgme curl
sudo curl --fail -sSLo /etc/yum.repos.d/passenger.repo https://oss-binaries.phusionpassenger.com/yum/definitions/el-passenger.repo
sudo yum install -y nginx passenger || sudo yum-config-manager --enable cr && sudo yum install -y nginx passenger

Uncomment the following settings in /etc/nginx/conf.d/passenger.conf:

passenger_root /some-filename/locations.ini;
passenger_ruby /usr/bin/ruby;
passenger_instance_registry_dir /var/run/passenger-instreg;

Edit the file and then restart the service:

sudo vi /etc/nginx/conf.d/passenger.conf
sudo service nginx restart
sudo /usr/bin/passenger-config validate-install
sudo /usr/sbin/passenger-memory-stats

User Setup

We should not run our app as root because we will give too much power to the app if it gets hacked, and we should not run it as our user, because we may want different setups for different apps, which may all be running on the same instance.

NEWUSER=myappuser
sudo adduser $NEWUSER
sudo mkdir -p ~$NEWUSER/.ssh 
touch $HOME/.ssh/authorized_keys
sudo sh -c "cat $HOME/.ssh/authorized_keys >> ~$NEWUSER/.ssh/authorized_keys" 
sudo chown -R $NEWUSER: ~$NEWUSER/.ssh 
sudo chmod 700 ~$NEWUSER/.ssh 
sudo sh -c "chmod 600 ~$NEWUSER/.ssh/*"
sudo usermod -a -G rvm $NEWUSER

Git Project Setup

Setup the project “deploy to” directory:

sudo yum install -y git
sudo mkdir -p /var/www/appname
sudo chown $NEWUSER: /var/www/appname

Create a project on your SCM service site (GitHub, BitBucket, etc.).

Then setup ssh so you can clone.

sudo su $NEWUSER
ssh-keygen -t rsa
cat ~/.ssh/id_rsa.pub

Then add the ssh key to the service.

cd /var/www/appname
git clone git@<service>.com:<user>/myproject.git ./

I just copied the example, then moved all of that into my repo:

git clone --bare https://github.com/phusion/passenger-ruby-rails-demo.git ./

Run the Project

From here:

rvm use ruby-2.6.3
#Using /usr/local/rvm/gems/ruby-2.6.3

Now install all of the gems:

bundle install --deployment --without development test

Generate the secret for Rails

bundle exec rake secret

Put that output into: config/secrets.yml

chmod 700 config db 
chmod 600 config/database.yml config/secrets.yml

Now create the DB and tables:

bundle exec rake assets:precompile db:migrate RAILS_ENV=production

This produced:

rails aborted!
LoadError: Error loading the 'sqlite3' Active Record adapter. Missing a gem it depends on? can't activate sqlite3 (~> 1.3.6), already activated sqlite3-1.4.0. Make sure all dependencies are added to Gemfile.
...

Caused by:
Gem::LoadError: can't activate sqlite3 (~> 1.3.6), already activated sqlite3-1.4.0. Make sure all dependencies are added to Gemfile.
...

Looks like the Gemfile didn’t specify the dependency for ActiveRecord of < 1.4 and as such mine installed the sqlite3 version of 1.4.0 which is not compatible.

An attempt to add the following to the Gemfile:

gem 'sqlite3', '~> 1.3', '< 1.4'

Will cause:

bundle exec rake assets:precompile db:migrate RAILS_ENV=production

We are testing out a production environment. The normal process is to modify and change this stuff on a development machine and submit the changes for both the Gemfile and the Gemfile.lock to the SCM repo and as such when the production server receives the changes it will get them synchronized.

Because we are testing our setup on a production server for now, we will disable this feature, but we must re-enable it before actually going to production!

To do this we will unset the frozen configuration, but first let’s see how our machine is setup so we can restore it later:

$ bundle config frozen
Settings for `frozen` in order of priority. The top value will be used
Set for your local app (/var/www/nitrogen/.bundle/config): true
Set for the current user (/home/nitrogen/.bundle/config): false

So for now:

bundle config --local frozen false
bundle install --deployment --without development test
#Installing sqlite3 1.3.13 (was 1.4.0) with native extensions
passenger-config about ruby-command

Now update the config file:

  • /etc/nginx/conf.d/myapp.conf
server {
    listen 80;
    server_name yourserver.com;

    # Tell Nginx and Passenger where your app's 'public' directory is
    root /var/www/myapp/code/public;

    # Turn on Passenger
    passenger_enabled on;
    passenger_ruby /path-to-ruby;
}

Domain Setup

In GCP under Networking > VPC Network > External IP Addresses > change it to Static

This IP was automatically assigned to my VM. I was able to go to that IP address and see my nginx configuration page.

I then went to my domain host and created an A record to this static IP.

My domain, then resolved to the correct IP address.

Now when I visit the domain instead of seeing the nginx configuration page I see:

500 Internal Server Error

This is because when you connect from the domain it knows and tries to load that site (from the configuration file we just updated). However, while loading the app we hit an error, so let’s see what that was:

sudo tail /var/log/nginx/access.log

Hmmm looks like a permissions issue:

2019/08/07 12:34:00 [alert] 25410#0: *34 Cannot stat '/var/log/nginx/access.log': Permission denied (errno=13); This error means that the Nginx worker process (PID 54321, running as UID 987) does not have permission to access this file. Please read this page to learn how to fix this problem: https://www.phusionpassenger.com/library/admin/nginx/troubleshooting/?a=upon-accessing-the-web-app-nginx-reports-a-permission-denied-error

To verify which user this service is running as (987):

$ grep 987 /etc/passwd
nginx:x:987:876:Nginx web server:/var/lib/nginx:/sbin/nologin

Looking at permissions, they look fine:

$ namei -l /var/www/myapp/code/config.ru
f: /var/www/myapp/code/config.ru
dr-xr-xr-x root root /
drwxr-xr-x root root var
drwxr-xr-x root root www
drwxr-xr-x myappuser myappuser myapp
drwxr-xr-x myappuser myappuser code
-rw-rw-r-- myappuser myappuser config.ru

It looks like nginx should be able to read the file, just to make sure..

sudo -u nginx tail /var/www/myapp/code/config.ru

This works fine so wtf…

Well it looks like Security Enhanced Linux strikes again. To fix this:

chcon -Rt httpd_sys_content_t /var/www/myapp/code

Bingo:

Hello world!

Congratulations, you are running this app in Passenger!

How to test if files differ in BASH and test that those differences are expected

A script to test if files are different, and to further test if the differences in those files are the expected differences:

#!/bin/bash

echo "text to find" | tee subject.log
echo "text to find" | tee same.log
echo "text which differs" | tee differs.log

if diff subject.log same.log; then
  echo "They are the same (expected)"
else
  echo "They are different"
fi

if diff subject.txt differs.txt > /dev/null; then
  echo "They are the same"
else
  echo "They are different (expected)"
fi

echo "Capturing diff output..."
mydiff=`diff subject.txt differs.txt`

echo "Printing test output:"
echo "$mydiff"

echo "Test that expected 123 & 12 diff exists:"
if echo "$mydiff" | grep '^< text to find$'; then 
  echo "Found 'text to find'"
else 
  echo "Not Found 'text to find'"
fi
if echo "$mydiff" | grep '^> text which differs$'; then
  echo "Found 'text which differs'"
else
  echo "Not Found 'text which differs'"
fi

Saturate (max out) Memory Utilization

To Max out memory use:

#!/usr/bin/python
 
# To eat 40GB of RAM and hold it for 5 seconds type:
#./memconsume.py 5 40
 
import time
import sys
 
seconds = int(sys.argv[1])
gb = int(sys.argv[2])
 
mem = gb * 1073741824;
cap = 1 * 1073741824;
calls = ( mem // cap );
print "consuming, using %s" % (mem);
 
memstr = []
for call in range( calls ):
    print " adding: %s" % (cap);
    memstr.append(' ' * cap);
 
print "waiting..."
 
time.sleep(seconds)

Creating a Directory Archive (.tar.gz/tar ball) in that Same Directory

It’s pretty simple:

cd ~/my_dir && tar --exclude=my_dir.tar.gz -czf my_dir.tar.gz ./*

The -c argument says to compress, z tells tar to create a gz type, f is to specify the file. The –exclude is the trick here so that tar doesn’t attempt to archive the file it is creating.

I’ve found it best to do this in the directory you are archiving, so yes cd into it first. If you don’t you will end up with problems such as longer paths in the tar archive, the exclusion specified won’t match, etc.

Opening a VMCX File to Run a Windows XP Mode VM from Windows 10

Enable Client Hyper-V:

  1. Press Windows Key
  2. Type (i.e. search) for “windows features”
  3. Select “Turn Windows features on or off”
  4. Press enter
  5. Enable the following:

Hyper-V

Go install Windows XP Mode for Windows 10 from Microsoft.com:

Download the WindowsXPMode_en-us.exe and using 7-zip extract this executable to  folder: “WindowsXPMode_en-us”

Once that is done, use 7-zip to open WindowsXPMode_en-us\sources\xpm and extract: VirtualXPVHD

Rename VirtualXPVHD to VirtualXPVHD.vhd

Start “Hyper-V Manager”

Connect to local server if you are not already, you can tell by checking if you see “Import Virtual Machine…” then you are already connected and can ignore this step.

Action > Virtual Switch Manager > Create New > External Network > [X] Allow management operating system to share this network adapter > Apply > OK

Action > New > Virtual Machine > Next > Name it your preference:

  • WindowsXPTestVM

Generation 1, 1024 MB of RAM is sufficient, configure network to same one you created earlier.

That VM should run and work like a fresh install of Windows XP.

Now we need to open the VMCX file with a text editor and read through it to understand what VHD file and VMC files were loaded for the VMCX file.

Select network “New Virtual Network”

I generally copy these files (which can be several GB) to my local disk so they run faster.

Then I modify the Test VM and add a secondary IDE connection to a new drive which has the older VHD file.

Or you can just try to boot directly into the VHD file:

Action > New > Virtual Machine > Next > Name it your preference:

  • MyVMFromVMCX

Use the VHD file from your VMCX (preferably a local copy).