Rails 7 new production install: from zero to deploy (Ubuntu 20.04 edition)
Tags: rails, ubuntu, nginx, devops
Published: 2022-02-01 12:54:16

This is my recipe to install a brand new Rails 7 app **without** Node pipeline on a fresh Ubuntu 20.04 server with shell access.

Ingredients:
* Ubuntu 20.04
* rbenv and Ruby 3.0.3
* Rails 7 without Node.js
* Postgres and Redis
* Nginx, Passenger, Capistrano

**Note**: Replace UPPERCASE words with your own setup details.
**Note 2**: I use vim to edit files, you can and should replace `vim` with `nano` or any other editor of choice if you're not familiar with it.
**Note 3**: Always use random and long passwords, don't share them between applications and don't lose them. Also never commit unencrypted secrets to public repos.

> This recipe works for me, but I would love to hear your thoughts on how it went for you! If something breaks or you do it differently, leave a comment below so I and the community can discuss and help out.

## SSH Setup

#### Local
```bash
$ ssh root@SERVER_IP_ADDRESS
  # Enter password
```
#### Remote
```bash
$ adduser deploy
  # Create password, skip rest
$ adduser deploy sudo
$ exit
```
#### Local - (optional) Add sshkey for easier login
```bash
  # You need a ssh-key generated beforehand, run command
  # below if you never did that before and follow instructions
  # ssh-keygen 
$ ssh-copy-id deploy@SERVER_IP_ADDRESS
  # Enter password
$ ssh deploy@SERVER_IP_ADDRESS
```
#### Remote - (optional) Disallow root login via ssh
```bash
$ sudo vim /etc/ssh/sshd_config
  # Set line 'PermitRootLogin yes' to 'PermitRootLogin no'
$ sudo service sshd restart  
```
#### Remote - (optional) Set hostname
```bash
$ sudo hostnamectl set-hostname HOSTNAME
$ sudo hostnamectl
```

## rbenv and Ruby install

#### Remote
```bash
$ sudo apt update; sudo apt upgrade -y
$ sudo apt install rbenv
$ rbenv init
  # Follow instructions: append 'eval "$(rbenv init -)"' to ~/.bashrc
$ source ~/.bashrc # or disconnect and reconnect
$ mkdir -p "$(rbenv root)"/plugins
$ sudo apt install git
$ git clone https://github.com/rbenv/ruby-build.git "$(rbenv root)"/plugins/ruby-build
$ git clone https://github.com/rbenv/rbenv-vars.git "$(rbenv root)"/plugins/rbenv-vars
$ curl -fsSL https://github.com/rbenv/rbenv-installer/raw/main/bin/rbenv-doctor | bash
$ rbenv install 3.0.3
```


![Baking Ingredients](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/re1qu01y8lz3lar7w4p0.jpg)
> Image by [rawpixel](https://www.rawpixel.com)


## Webserver Setup

#### Nginx install (remote)
```bash
$ sudo apt install nginx
```
#### Passenger install (remote)
```bash
$ sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 561F9B9CAC40B2F7
$ sudo sh -c 'echo deb https://oss-binaries.phusionpassenger.com/apt/passenger focal main > /etc/apt/sources.list.d/passenger.list'
$ sudo apt install ca-certificates 
$ sudo apt update
$ sudo apt install libnginx-mod-http-passenger
$ 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
  # Verify that file exists
$ sudo vim /etc/nginx/conf.d/mod-http-passenger.conf
  # Change passenger_ruby line to to following (without #): 
  # passenger_ruby /home/deploy/.rbenv/shims/ruby;
$ sudo service nginx restart
$ sudo /usr/bin/passenger-config validate-install
$ sudo /usr/sbin/passenger-memory-stats
```
#### Config Nginx (remote)
```bash
$ sudo rm /etc/nginx/sites-enabled/default
$ sudo vim /etc/nginx/sites-available/APPNAME
```
Insert content below
```
server {
  listen 80;
  listen [::]:80;

  server_name YOUR_DOMAIN.ORG;
  # If you deploy without DNS and SSL, you could leave servername blank like below
  # server_name _;
  root /home/deploy/APPNAME/current/public;
  client_max_body_size 10m; # Set max upload size

  passenger_enabled on;
  passenger_app_env production;
  passenger_env_var RUBYOPT '-r bundler/setup'; # Cf issue: https://github.com/phusion/passenger/issues/2409
  
  # Uncomment if you use ActionCable and/or Turbo Streams
  # location /cable {
  #   passenger_app_group_name APPNAME_ws;
  #   passenger_force_max_concurrent_requests_per_process 0;
  # }  

  location ~ ^/assets {
    expires max;
    gzip_static on;
  }
}
```
```bash
$ sudo ln -s /etc/nginx/sites-available/APPNAME /etc/nginx/sites-enabled/APPNAME
$ sudo service nginx reload
```


![Dough formed to bread buns](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ki715rkehq8ltsk0b43g.jpg)
> Image by [rawpixel](https://www.rawpixel.com)

## Database Setup
#### Postgres install (remote)
```bash
$ sudo apt install postgresql postgresql-contrib libpq-dev
$ sudo service postgresql@12-main status
$ sudo su - postgres
$ createuser --pwprompt deploy
  # Enter password
$ createdb -O deploy DBNAME
  # check db existence with 'psql' then '\l', quit with '\q'
$ exit
```
#### Redis install (remote) - Skip if you don't use ActionCable or Turbo Streams
```bash
$ sudo apt install redis
$ sudo service redis-server status
```


![Bread getting a final touch of flour](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/adkxmbj04hudgwk5lwp7.jpg)
> Image by [rawpixel](https://www.rawpixel.com)

## Deploy Tool config - Capistrano

#### Local
Add to `Gemfile`
```ruby
group :development do
  # ...
  gem 'capistrano'
  gem 'capistrano-rails'
  gem 'capistrano-passenger'
  gem 'capistrano-rbenv'
end
```
```bash
$ bundle install
$ bundle exec cap install STAGES=production
```
Add to `Capfile`
```ruby
require 'capistrano/rails'
require 'capistrano/passenger'
require 'capistrano/rbenv'

set :rbenv_type, :user
set :rbenv_ruby, '3.0.3'
```
Edit `config/deploy.rb`
```ruby
set :application, "APPNAME"
set :repo_url, "git@github.com:USERNAME/APPNAME.git"
# Also works with non-github repos, I roll my own gitolite server
set :deploy_to, "/home/deploy/#{fetch :application}"
set :rbenv_prefix, '/usr/bin/rbenv exec' # Cf issue: https://github.com/capistrano/rbenv/issues/96
append :linked_dirs, 'log', 'tmp/pids', 'tmp/cache', 'tmp/sockets', 'vendor/bundle', '.bundle', 'public/system', 'public/uploads'
```
Edit `config/deploy/production.rb`
```ruby
server 'IP ADDRESS or DOMAIN', user: 'deploy', roles: %w{app db web}
```
> Parts of this section are inspired from [GoRails](https://gorails.com/deploy/ubuntu/20.04)

## Deployment

#### Remote - Prepare env vars
```bash
$ mkdir ~/APPNAME
$ vim ~/APPNAME/.rbenv-vars
  # Set envvars like
  # DATABASE_URL=postgresql://deploy:PASSWORD@127.0.0.1/APPNAME
    # This above can also be set in your database.yml file in your rails project if you prefer...
  # RAILS_MASTER_KEY=YOUR_KEY_IN_master.key
  # SECRET_KEY_BASE=SOME_OTHER_RANDOM_KEY
```
#### Local - Deploy! 🍾🍾🍾
```bash
$ bundle exec cap production deploy
  # Go visit your page already!
```

#### Remote - Connect to console
```bash
$ cd APPNAME/current
$ bundle exec rails c
  # You need to have the debug gem in prod env enabled! (Gemfile)
```


![Bread bun being cut open](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/11l5iqft8jac69wcnv6f.jpg)
> Image by [rawpixel](https://www.rawpixel.com)

## DevOps Toolbox
These tools help you to stay on top of your DevOps game, if you have any other recommendations, leave a comment below.
#### Remote
```bash
$ top
$ netstat -nlp
$ tail -100 PATH_TO_LOGFILE
$ hostnamectl
$ df -h
$ sudo du -h -d 1 /
```
#### Local
```bash
$ dig DOMAIN
```


## Bonus #1 - Install SSL
You need to have your DNS A-Record set up beforehand

#### Remote - Install Certbot
```bash
$ sudo apt install snapd
$ sudo snap install core; sudo snap refresh core
$ sudo snap install --classic certbot
$ sudo ln -s /snap/bin/certbot /usr/bin/certbot
$ sudo certbot --nginx
```

## Bonus #2 - Enable HTTP2
#### Remote
```
$ sudo vim /etc/nginx/sites-available/APPNAME
  # Add http2 to listem command, see below
  # listen [::]:443 ssl http2 ipv6only=on;
  # listen 443 ssl http2;
$ sudo service nginx restart
```

How was your last deploy or are you planning your first one? What were the biggest hurdles you had to jump? What are you doing differently? Feel free to join the discussion below


> Top image by [rawpixel](https://www.rawpixel.com)