DEV Community

Cover image for Rails 7 new production install: from zero to deploy (Ubuntu 20.04 edition)
1klap
1klap

Posted on • Updated on

Rails 7 new production install: from zero to deploy (Ubuntu 20.04 edition)

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

$ ssh root@SERVER_IP_ADDRESS
  # Enter password
Enter fullscreen mode Exit fullscreen mode

Remote

$ adduser deploy
  # Create password, skip rest
$ adduser deploy sudo
$ exit
Enter fullscreen mode Exit fullscreen mode

Local - (optional) Add sshkey for easier login

  # 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
Enter fullscreen mode Exit fullscreen mode

Remote - (optional) Disallow root login via ssh

$ sudo vim /etc/ssh/sshd_config
  # Set line 'PermitRootLogin yes' to 'PermitRootLogin no'
$ sudo service sshd restart  
Enter fullscreen mode Exit fullscreen mode

Remote - (optional) Set hostname

$ sudo hostnamectl set-hostname HOSTNAME
$ sudo hostnamectl
Enter fullscreen mode Exit fullscreen mode

rbenv and Ruby install

Remote

$ 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
Enter fullscreen mode Exit fullscreen mode

Baking Ingredients

Image by rawpixel

Webserver Setup

Nginx install (remote)

$ sudo apt install nginx
Enter fullscreen mode Exit fullscreen mode

Passenger install (remote)

$ 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
Enter fullscreen mode Exit fullscreen mode

Config Nginx (remote)

$ sudo rm /etc/nginx/sites-enabled/default
$ sudo vim /etc/nginx/sites-available/APPNAME
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode
$ sudo ln -s /etc/nginx/sites-available/APPNAME /etc/nginx/sites-enabled/APPNAME
$ sudo service nginx reload
Enter fullscreen mode Exit fullscreen mode

Dough formed to bread buns

Image by rawpixel

Database Setup

Postgres install (remote)

$ 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
Enter fullscreen mode Exit fullscreen mode

Redis install (remote) - Skip if you don't use ActionCable or Turbo Streams

$ sudo apt install redis
$ sudo service redis-server status
Enter fullscreen mode Exit fullscreen mode

Bread getting a final touch of flour

Image by rawpixel

Deploy Tool config - Capistrano

Local

Add to Gemfile

group :development do
  # ...
  gem 'capistrano'
  gem 'capistrano-rails'
  gem 'capistrano-passenger'
  gem 'capistrano-rbenv'
end
Enter fullscreen mode Exit fullscreen mode
$ bundle install
$ bundle exec cap install STAGES=production
Enter fullscreen mode Exit fullscreen mode

Add to Capfile

require 'capistrano/rails'
require 'capistrano/passenger'
require 'capistrano/rbenv'

set :rbenv_type, :user
set :rbenv_ruby, '3.0.3'
Enter fullscreen mode Exit fullscreen mode

Edit config/deploy.rb

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'
Enter fullscreen mode Exit fullscreen mode

Edit config/deploy/production.rb

server 'IP ADDRESS or DOMAIN', user: 'deploy', roles: %w{app db web}
Enter fullscreen mode Exit fullscreen mode

Parts of this section are inspired from GoRails

Deployment

Remote - Prepare env vars

$ 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
Enter fullscreen mode Exit fullscreen mode

Local - Deploy! 🍾🍾🍾

$ bundle exec cap production deploy
  # Go visit your page already!
Enter fullscreen mode Exit fullscreen mode

Remote - Connect to console

$ cd APPNAME/current
$ bundle exec rails c
  # You need to have the debug gem in prod env enabled! (Gemfile)
Enter fullscreen mode Exit fullscreen mode

Bread bun being cut open

Image by rawpixel

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

$ top
$ netstat -nlp
$ tail -100 PATH_TO_LOGFILE
$ hostnamectl
$ df -h
$ sudo du -h -d 1 /
Enter fullscreen mode Exit fullscreen mode

Local

$ dig DOMAIN
Enter fullscreen mode Exit fullscreen mode

Bonus #1 - Install SSL

You need to have your DNS A-Record set up beforehand

Remote - Install Certbot

$ 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

Top comments (4)

Collapse
 
kevintriplett profile image
Kevin Triplett • Edited

Also -- very important -- the default passenger instance registry directory is /var/run/passenger-instreg which is blown away after reboot. If passenger can't create that directory, it can't run. There might be something messed up about permissions for my installation, but setting this directory to /tmp solved my problems.

So here's what I had to do:

  1. Edit /etc/nginx/conf.d/mod-http-passenger.conf to revise the directive to passenger_instance_registry_dir /tmp;
  2. Tell Capistrano about this change in deploy.rb with set :passenger_environment_variables, { 'PASSENGER_INSTANCE_REGISTRY_DIR' => '/tmp' }
Collapse
 
kevintriplett profile image
Kevin Triplett • Edited

Worked (almost) great, thanks 1klap! Yours is one of the better deploy recipes that I've found.

Two things were different for me (on Debian 11):

ln -s /etc/nginx/sites-available/APPNAME /etc/nginx/sites-enabled/APPNAME
(Debian didn't understand the "../" when I was in the $HOME directory)

I had to scp the database.yml and master.key files into the shared/config directory:
scp config/database.yml deploy@DOMAIN:APPNAME/shared/config
scp config/master.key deploy@DOMAIN:APPNAME/shared/config

Tip: Do not use special characters with Postgresql, especially "_" in the database name and "@" in the user password.

Thanks again!

Collapse
 
aro7979 profile image
Aro7979

I set up as you indicated, but at the end I get an error
403 Forbidden
nginx/1.18.0 (Ubuntu),
when looking at the error log:
directory index of "/home/deploy/www/" is forbidden
in my settings /etc/nginx/sites-available/myapp root is specified /home/deploy/www/current.
where did i make a mistake?

Collapse
 
satrapal profile image
Lukas Satrapa

Also thanks for the perfect recipe to install a brand new Rails 7 app! Just one note: before deploy I had to install nodejs (via sudo apt-get install nodejs) on the server (remote) to avoid execJs: 'Could not find a JavaScript runtime' error.