Skip to content

Instantly share code, notes, and snippets.

@ericmathison
Last active November 23, 2023 15:18
Show Gist options
  • Save ericmathison/1d53b3f53fe68b0c351046df3422a4e0 to your computer and use it in GitHub Desktop.
Save ericmathison/1d53b3f53fe68b0c351046df3422a4e0 to your computer and use it in GitHub Desktop.
Deploy instructions for Rails using Puma, Nginx, and Ubuntu 18.04.

Deploy instructions for Rails using Puma, Nginx, and Ubuntu 18.04.

Sign up at www.atlantic.net (Select the Ubuntu 18.04 LTS operating system option). I was grandfathered in on Atlantic.net's $1 a month virtual private server (VPS) hosting. Unlike Heroku, you will have full control over the server. At some point in your application's lifetime this may become important to you for making customizations Heroku isn't capable of. Unless future limitations aren't a concern AND you don't want to learn how to set up a virtual private server, I wouldn't suggest using Heroku. The setup process below would be very similar for any of the other big hosting options like Amazon's EC2, DigitalOcean, etc.

Confirmation email will have log in instructions with IP, password and username.

Some of the commands below are run on the remote machine and others on your local development machine. I will prefix each command with one of the following:

remote-root#
remote$
your-machine$

Command prompts with a # indicate commands to be run by the root user and commands with a $ indicate commands to be run with an ordinary, non-root user. You don't type this part, just the command that comes after it. You will usually need to add sudo to the begining of commands having a prompt with a #. The only exception is an example where we temporarily login as a root user via ssh.

Commands below that are supposed to be run as root should be prefixed with sudo (i.e. sudo apt-get install git instead of apt-get install git) if your prompt ends with a $ instead of a #.

At first we will log in as the root user and your prompt on the server will end with a #. Later, we will log in as an ordinary user and your prompt on the server will have a $ on the end.

In the examples below, replace with the ip address that you were sent in the email (something like: 123.123.123.123) and replace <username> with the username that you would like to use. I suggest a lower case, single word user name like bob, joe, or perhaps even myappname.

Log in with the credentials provided in the email:

your-machine$ ssh root@<ip address>

We are now logged in as the root user on the remote machine.

Since we want to host our rails app here, we don't want to host it using the root user. If the web app had a security flaw allowing shell access, running as root would open up even more vulnerabilities (like being able to install software without a password). We need to make a new user.

First, let's make an admin group:

remote-root# groupadd admin

Let's create a new user:

remote-root# useradd -G admin <username>

admin here refers to the admin group. We are creating the new user and immediately adding it to the admin group. By default, Ubuntu is set up to allow any users in the admin group to have root privileges (when using sudo and the user's password).

Next, change the password of our new user:

remote-root# passwd <username>

Make a home directory for our new user:

remote-root# mkdir /home/<username>

Set that home directory's permissions:

remote-root# chown <username>:<username> /home/<username>

Copy your public key from your computer to the server (to authenticate securely without needing a password). ssh-copy-id is generally only available if your computer is running Linux.

your-machine$ ssh-copy-id <username>@<ip address>

As an alternative to the previous command (e.g. if you have a mac), and you happen to have a github account with the public key that you want to authenticate with on your server, you can do the following instead:

remote$ mkdir -p ~/.ssh
remote$ cd ~/.ssh
remote$ wget 'https://github.com/<github username>.keys'
remote$ cat <github username>.keys >> authorized_keys

You can now log in to the server with your normal user:

your-machine$ ssh <username>@<ip address>

You shouldn't need to enter a password this time

Install ufw (uncomplicated firewall), postgres, git, build-essential (for compiling ruby), and nodejs (for JavaScript runtime), and nginx:

remote-root# apt-get install postgresql libpq-dev git ufw build-essential nodejs nginx

Enable firewall (make sure to allow port 22 for ssh before enabling the firewall or you'll be locked out):

remote-root# ufw allow 22
remote-root# ufw allow 80
remote-root# ufw allow 443
remote-root# ufw enable

Restart the server for the firewall setting to take effect.

remote-root# reboot

Set bash as the default shell for our new user:

remote-root# chsh -s /bin/bash <username>

Add these lines to the top of /etc/ssh/sshd_config (with vim for example). Comment out or delete any other lines with PermitRootLogin or PasswordAuthentication settings:

PermitRootLogin no
PasswordAuthentication no
AllowUsers <username>

Restart ssh for the settings to take effect:

remote-root# service ssh restart

Update the installed packages:

remote-root# apt-get update
remote-root# apt-get upgrade

Old Note: In the middle of the upgrade process, I was presented with a dialog stating that "The GRUB boot loader was previously installed to a disk that is no longer present..." asking which device I would like to install the boot loader to. I installed to /dev/sda (which generally refers to a hard drive) instead of /dev/sda1 (a partition on that hard same drive). Apparently it's a "BAD" idea to install to the partition (http://askubuntu.com/a/19705/159696).

Install ruby-install for easy ruby installations:

remote$ dir=/tmp/ruby-install
remote$ tarball=/tmp/ruby-install.tar.gz
remote$ wget -O $tarball https://api.github.com/repos/postmodern/ruby-install/tarball
remote$ mkdir $dir
remote$ tar -C $dir -xvzf $tarball --strip-components 1
remote$ cd $dir
remote-root# make install
remote$ ruby-install ruby

Install ruby (rdoc parsing was causing compilation to fail on my tiny instance so disabled it to proceed):

remote$ ruby-install ruby <optional_version_number> -- --disable-install-rdoc

Note: The first time I ran the previous command to install ruby, the compilation failed with a message saying "gcc: internal compiler error". This was due to insufficient memory (my server had 256 megs). The solution to this was creating a swap file so that the extra memory needed could be stored on disk during the installation. First create a big file full of zeros:

remote$ dd if=/dev/zero of=~/swapfile bs=1024 count=1024000

Then turn that file into a proper swap file:

remote$ mkswap ~/swapfile

Then enable the file for swapping:

remote-root# swapon ~/swapfile

You can check to make sure you have swappable memory with this command:

remote$ free

After you are done installing ruby, you can turn the swap off:

remote-root# swapoff ~/swapfile

Then overwrite any data that was put into the swapfile (it could possibly contain sensitive info like passwords that were stored in memory):

remote$ shred ~/swapfile

And delete it:

remote$ rm ~/swapfile

Add ruby bin directory to $PATH variable:

echo "PATH=/home/deploy/.rubies/ruby-<ruby_version>/bin:$PATH" >> ~/.bash_profile

Clone app (in home directory):

remote$ git clone <url>

Install gem dependencies:

cd <app_directory>
gem install bundler
bundle --without development test

Copy up master key from dev repo to production

your-machine$ rsync -av config/master.key <user>@<domain_or_ip>:/home/<username>/<app_name>/config

Create Database YAML File:

cp config/database.yml.example config/database.yml

Edit database.yml:

vim config/database.yml

Replace the contents of config/database.yml with this (replace app name)

production:
  adapter: postgresql
  encoding: unicode
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  database: <app_name>_production

Create database user. We will be using postgres's peer authentication which basically means that the database user corresponds to the operatinng system user.

remote$ sudo -u postgres createuser -s <os_username>

Create Database and Load Schema:

remote$ cd ~/<app_dir>
remote$ rails db:create RAILS_ENV=production

Only load schema for new apps (not ones with an existing database)

remote$ rails db:schema:load RAILS_ENV=production

For apps with existing databases copy the database to the server and load it like so (assuming a gziped file):

gunzip < database.sql.gz | psql <app_name>_production"

Precompile assets:

RAILS_ENV=production rails assets:precompile

Note: compiling the assets may require the swapfile turned on as well.

At this point, you can test the rails app by running:

sudo ufw allow <port>
RAILS_ENV=production rails s --binding=<ip_address>:<port>

Test it out and then disable that particular port number:

sudo ufw deny 3000

Register domain at namecheap.com. Go to "Manage Domains", select example.com, select "Advanced DNS". Now we will modify our dns settings so that example.com properly refers to our ip address. Fill out the last two column just like they are shown below (replacing with your corresponding ip address):

Host Name     IP Address/URL              Record Type
@             <ip address>                A (address)
www           http://example.com   URL Redirect (301)

Replace the content of /etc/nginx/sites-available/default with:

server {
        listen 80;
        listen [::]:80;
        server_name example.com 69.28.67.250;
        root /home/deploy/ethnic_la/public;

        location / {
                try_files $uri @<app_name>;
        }

        location @<app_nam> {
                proxy_pass http://localhost:3000;
                proxy_set_header  Host $host;
                proxy_set_header X-Forwarded-Proto $scheme;
        }
}

Then symlink it as the default server:

remote-root# ln -sf /etc/nginx/sites-available/<app_name> /etc/nginx/sites-enabled/default

restart nginx:

remote-root# service nginx restart

Nginx note. This command is helpful when having trouble getting nginx to start properly:

remote-root# nginx -t

Setup ssl: (instructions came from here: https://certbot.eff.org/lets-encrypt/ubuntuxenial-nginx)

remote-root# apt-get update
remote-root# apt-get install software-properties-common
remote-root# add-apt-repository ppa:certbot/certbot
remote-root# apt-get update
remote-root# apt-get install python-certbot-nginx
remote-root# certbot --nginx
@ismarsantos
Copy link

How to start puma?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment