01 Feb, 2024

Backend

Rails 7

Capistrano

asdf

Deploy Rails 7 app via Capistrano and asdf

It's a step by step guide on how to deploy Rails 7 app with Tailwind CSS via Capistrano and asdf as version manager.

MROY Team

Env:

Ubuntu 22.04
Ruby 3.3.0
Node 20.9.0
Postgresql ≥12.14

To deploy the Rails 7 app via Capistrano and asdf:

  1. Configure the SSH agent and users on the server.
  2. Install Ruby using asdf.
  3. Install Nginx and Passenger.
  4. Install Postgres, create Postgres user and database.
  5. Configure Capistrano and credential files on your server.

How it works in short

Capistrano uses SSH to send instructions from the local computer to the server to obtain an app from a repository (GitHub for example) and then configures the Rails app for production.

Create a new user

Let’s create a new user:

$ adduser deploy

“deploy” is a username. You can come up with your own.
You’ll be asked to enter a password.
The rest of the questions are optional. Just hit Enter.
This user will use Capistrano to give instructions to the server.

Sometimes you’ll need to do a task that requires root privileges. You can achieve it by prepending your command with sudo. To allow this, we need to add our user to the sudo group.

To add user “deploy” to the sudo group:

$ usermod -aG sudo deploy

Now “deploy” will be able to run commands with root privileges.

More on sudo:  
https://serverfault.com/questions/601140/whats-the-difference-between-sudo-su-postgres-and-sudo-u-postgres

Copy the public key

You need to add the key to /home/deploy/.ssh/authorized_keys file.
You can do it using ssh-copy-id command or add it manually to authorized_keys (we’ve described it in detail in the article How to deploy NextJS app on Ubuntu 22.04).

Installing Ruby

Install dependencies for compiling Ruby:

# Update the index of available repositories and their versions
sudo apt-get update

# Install dependencies for compiling Ruby
sudo apt-get install git-core zlib1g-dev build-essential libssl-dev libreadline-dev libyaml-dev libsqlite3-dev sqlite3 libxml2-dev libxslt1-dev libcurl4-openssl-dev software-properties-common libffi-dev

Installing asdf is a two-step process.
First, you install asdf, and then add it to your shell:

# Go to user home directory
cd ~

# Clone the asdf repo
git clone https://github.com/excid3/asdf.git ~/.asdf

# Add asdf to the path
echo '. "$HOME/.asdf/asdf.sh"' >> ~/.bashrc
echo '. "$HOME/.asdf/completions/asdf.bash"' >> ~/.bashrc
echo 'legacy_version_file = yes' >> ~/.asdfrc
echo 'export EDITOR="code --wait"' >> ~/.bashrc
exec $SHELL
More on bashrc:
https://unix.stackexchange.com/questions/129143/what-is-the-purpose-of-bashrc-and-how-does-it-work
https://www.digitalocean.com/community/tutorials/bashrc-file-in-linux

Now we can install asdf plugins for each language we want to use. For Rails, we can install Ruby and Node.js for our frontend Javascript.

asdf plugin add ruby
asdf plugin add nodejs

Install Ruby and set the default version:

# Install ruby ​​(it may take 5–30 mins)
asdf install ruby 3.3.0

# Set version 3.3.0 as global
asdf global ruby 3.3.0

# Update to the latest Rubygems version
gem update --system

Confirm the system default Ruby version matches the version just installed:

which ruby
#=> /home/deploy/.asdf/shims/ruby

ruby -v
#=> 3.3.0

Then install Node.js for handling Javascript in Rails apps:

asdf install nodejs 20.9.0
asdf global nodejs 20.9.0

which node
#=> /home/deploy/.asdf/shims/node
node -v
#=> 20.9.0

# Install yarn for Rails jsbundling/cssbundling or webpacker
npm install -g yarn

Installing Nginx and Passenger

Connected to your instance as a deploy user, we will install Nginx with Passenger to run the Rails app.

Install Nginx and Passenger:
https://www.phusionpassenger.com/docs/advanced_guides/install_and_upgrade/nginx/install/oss/jammy.html

More on nginx:  
https://docs.nginx.com/nginx/admin-guide/basic-functionality/managing-configuration-files/

And then update Nginx config:

sudo nano /etc/nginx/sites-available/default

# /nginx/sites-available/default
server {
    listen 80 default_server;
    listen [::]:80 default_server;

    # Add index.php to the list if you are using PHP
    index index.html index.htm index.nginx-debian.html;

    server_name _;

    # Specify root directory
    root /home/deploy/app-name/current/public;

    # Enable Passenger module
    passenger_enabled on;
    passenger_app_env production;
    passenger_preload_bundler on;
    passenger_app_root /home/deploy/brdn-site/current;

    location /cable {
        passenger_app_group_name app_name_websocket;
        passenger_force_max_concurrent_requests_per_process 0;
    }

    client_max_body_size 100m;

    location ~ ^/(assets/packs) {
        expires max;
        griz_static on;
    }
}

Specify Ruby directory for Passenger:

sudo nano /etc/nginx/conf.d/mod-http-passenger.conf

# mod-http-passenger.conf
passenger_ruby /home/deploy/.asdf/shims/ruby;

Reload Nginx to make sure everything is ok:

sudo service nginx reload

Install Postgresql and create a Postgresql user (in this example, it’s “deploy” too):

# Install Postgresql
sudo apt-get install postgresql postgresql-contrib libpq-dev

The postgres install doesn't create a user for you, so you'll need to create one:

# Create user named "deploy", enter the password
sudo -u postgres createuser --pwprompt deploy

# Create postgresql database named "app_production"
sudo -u postgres createdb -O deploy app_production
More on postgres:  
https://stackoverflow.com/questions/74961535/connection-to-server-on-socket-var-run-postgresql-s-pgsql-5432-failed-fatal  
https://stackoverflow.com/questions/65222869/how-do-i-solve-this-problem-to-use-psql-psql-error-fatal-role-postgres-d
https://stackoverflow.com/questions/31645550/postgresql-why-psql-cant-connect-to-server
https://serverfault.com/questions/601140/whats-the-difference-between-sudo-su-postgres-and-sudo-u-postgres

Add main and additional Capistrano gems to Gemfile:

gem 'capistrano' , '~> 3.18'
gem 'capistrano-rails' , '~> 1.6'
gem 'capistrano-passenger' , '~> 0.2.1'
gem 'capistrano-asdf' , '~> 1.1'

In terminal:

# Install gems from the previous step
bundle install

# Capistrano command to generate files for production
cap install STAGES=production

Add to Capfile:

# Capfile
# Load DSL and set up stages
require "capistrano/setup"

# Include default deployment tasks
require "capistrano/deploy"

# Load the SCM plugin appropriate to your project:
#
# require "capistrano/scm/hg"
# install_plugin Capistrano::SCM::Hg
# or
# require "capistrano/scm/svn"
# install_plugin Capistrano::SCM::Svn
# or
require "capistrano/scm/git"
install_plugin Capistrano::SCM::Git

# Include tasks from other gems included in your Gemfile
#
# For documentation on these, see for example:
#
#   https://github.com/capistrano/rvm
#   https://github.com/capistrano/rbenv
#   https://github.com/capistrano/chruby
#   https://github.com/capistrano/bundler
#   https://github.com/capistrano/rails
#   https://github.com/capistrano/passenger
#
# require "capistrano/rvm"
# require "capistrano/rbenv"
# require "capistrano/chruby"
# require "capistrano/bundler"
# require "capistrano/rails/assets"
# require "capistrano/rails/migrations"
# require "capistrano/passenger"

# Load custom tasks from `lib/capistrano/tasks` if you have any defined
Dir.glob("lib/capistrano/tasks/*.rake").each { |r| import r }

require 'capistrano/bundler'
require 'capistrano/rails/migrations'
require 'capistrano/passenger'
require 'capistrano/asdf'

Update config/deploy.rb:

# config valid for current version and patch releases of Capistrano
lock "~> 3.18.0"

set :application, "app-name"
set :repo_url, 'https://...'
set :git_http_username, 'your-username'
set :git_http_password, 'your-password'

# Default value for :format is :airbrussh.
# You can configure the Airbrussh format using :format_options.
set :format, :airbrussh
set :format_options, command_output: true, log_file: 'log/capistrano.log', color: :auto, truncate: :auto

set :branch, "main"

set :deploy_to, "/home/deploy/#{fetch :application}"

append :linked_files, 'config/database.yml', 'config/credentials/production.key'
append :linked_dirs, 'log', 'tmp/pids', 'tmp/cache', 'tmp/sockets', 'vendor/bundle', '.bundle', 'public/system', 'public/uploads', 'storage'

set :keep_releases, 5

# Default value for :pty is false
# set :pty, true

# Default value for :linked_files is []
# append :linked_files, "config/database.yml", 'config/master.key'

# Default value for linked_dirs is []
# append :linked_dirs, "log", "tmp/pids", "tmp/cache", "tmp/sockets", "public/system", "vendor", "storage"

# Default value for default_env is {}
# set :default_env, { path: "/opt/ruby/bin:$PATH" }

# Default value for local_user is ENV['USER']
# set :local_user, -> { `git config user.name`.chomp }

# Default value for keep_releases is 5
# set :keep_releases, 5

# Uncomment the following to require manually verifying the host key before first deploy.
# set :ssh_options, verify_host_key: :secure

Update config/deploy/production.rb:

server '111.111.111.111', user: 'deploy', roles: %w{app db web}

Run the command to deploy:

cap production deploy

After execution of this command your app should be deployed on the production server and be available on the web.

Possible problems and solutions

Problem

OpenSSH keys only supported if ED25519 is available (NotImplementedError)
net-ssh requires the following gems for ed25519 support:
 * ed25519 (>= 1.2, < 2.0)
 * bcrypt_pbkdf (>= 1.0, < 2.0)
See https://github.com/net-ssh/net-ssh/issues/565 for more information
Gem::MissingSpecError : "Could not find 'ed25519' (~> 1.2) among 197 total gem(s)

Solution

ssh-add /Users/username/.ssh/keyname

Problem

01 deploy@111.111.111.111 0.120s
      02 git archive master | /usr/bin/env tar -x -f - -C /home/deploy/app-name/releases/20240205170649
      02 fatal: not a valid object name: master
      02 tar:
      02 This does not look like a tar archive

Solution:
https://stackoverflow.com/questions/65490209/capistrano-deploy-fails-to-find-github-repository

Problem

raise ArgumentError, "Missing `secret_key_base` for '#{Rails.env}' environment, set this string with `bin/rails credentials:edit`"

Solution
Update config/environments/production.rb:

config.require_master_key = true

Set the secret_key_base in your production secret keys:

EDITOR=nano rails credentials:edit -e production

Commit, push to the repo, and try to deploy again.

Related links:

Problem

Exception while executing as deploy@111.111.111.111: rake exit status: 1 (SSHKit::Runner::ExecuteError)
rake stdout: Nothing written
rake stderr: Missing encryption key to decrypt file with. Ask your team for your master key and write it to /home/deploy/app-name/releases/20240205174512/config/master.key or put it in the ENV['RAILS_MASTER_KEY'].

Solution

# Added to config/deploy.rb:
append :linked_files, 'config/credentials/production.key'

# Added to config/environments/production.rb:
config.require_master_key = true

Problem

ActiveRecord::DatabaseConnectionError: There is an issue connecting to your database with your username/password, username: username_site. (ActiveRecord::DatabaseConnectionError)

Solution

# Check production username and password in config/database.yml

# Set an environment variable
export APP_DATABASE_PASSWORD=XXXXXXXXXXXXXXXXXX

# To check environment variables in your shell:
env

Related links:
https://www.phusionpassenger.com/library/indepth/environment_variables.html#working-with-environment-variables

Problem
404 (NGINX)

Solution
(Configure NGINX)

Problem

Error opening '/home/user/app/current/Passengerfile.json' for reading: Permission denied (errno=13); This error means that the Nginx worker process (PID 4344, running as UID 984) does not have permission to access this file.

Solution

Problem

App 81199 output: I, [2024-02-06T09:42:27.808833 #81199]  INFO -- : [fe87f964-ff44-4b8f-98a8-817aa10fbcf4] Started GET "/" for 159.205.9.175 at 2024-02-06 09:42:27 +0000
App 81199 output: I, [2024-02-06T09:42:27.809919 #81199]  INFO -- : [fe87f964-ff44-4b8f-98a8-817aa10fbcf4] Processing by PagesController#main as HTML
App 81199 output: I, [2024-02-06T09:42:27.812025 #81199]  INFO -- : [fe87f964-ff44-4b8f-98a8-817aa10fbcf4]   Rendered layout layouts/application.html.erb (Duration: 1.2ms | Allocations: 307)
App 81199 output: I, [2024-02-06T09:42:27.812292 #81199]  INFO -- : [fe87f964-ff44-4b8f-98a8-817aa10fbcf4] Completed 500 Internal Server Error in 2ms (ActiveRecord: 0.0ms | Allocations: 480)
App 81199 output: E, [2024-02-06T09:42:27.814056 #81199] ERROR -- : [fe87f964-ff44-4b8f-98a8-817aa10fbcf4]   
App 81199 output: [fe87f964-ff44-4b8f-98a8-817aa10fbcf4] ActionView::Template::Error (The asset "tailwind.css" is not present in the asset pipeline.
App 81199 output: ):
App 81199 output: [fe87f964-ff44-4b8f-98a8-817aa10fbcf4]      5:     <meta name="viewport" content="width=device-width,initial-scale=1">
App 81199 output: [fe87f964-ff44-4b8f-98a8-817aa10fbcf4]      6:     <meta name="csrf-param" content="authenticity_token" />
<meta name="csrf-token" content="CSYE1aR0AKXRab-qQ9gnjw5f_EAaLssDLfTCoMmcUwV_CGbyv2qSJOtQU5hZ59u1V2oagqsDFGzJmsOJ3vD3Ww" />
App 81199 output: [fe87f964-ff44-4b8f-98a8-817aa10fbcf4]      7:     
App 81199 output: [fe87f964-ff44-4b8f-98a8-817aa10fbcf4]      8:     <link rel="stylesheet" href="/assets/tailwind-bbf18a1388bdcfbee9cb01cf6819ece3e2c8e9ba93c9d93ac2f6a9c78b625900.css" data-turbo-track="reload" />
<link rel="stylesheet" href="/assets/inter-font-8c3e82affb176f4bca9616b838d906343d1251adc8408efe02cf2b1e4fcf2bc4.css" data-turbo-track="reload" />
App 81199 output: [fe87f964-ff44-4b8f-98a8-817aa10fbcf4]      9: 
App 81199 output: [fe87f964-ff44-4b8f-98a8-817aa10fbcf4]     10:     <link rel="stylesheet" href="/assets/application-ee19c236fd38b5dcc6fa4844b07d1bc61736bd00aae2a371795148f312647216.css" data-turbo-track="reload" />
App 81199 output: [fe87f964-ff44-4b8f-98a8-817aa10fbcf4]     11:     <script type="importmap" data-turbo-track="reload">{
  "imports": {
    "application": "/assets/application-a9f5fa199a168d6cd5fbde9a8bc926cd043a28437b010cf9e918aaa6baa89442.js",
    "@hotwired/turbo-rails": "/assets/turbo.min-dfd93b3092d1d0ff56557294538d069bdbb28977d3987cb39bc0dd892f32fc57.js",
    "@hotwired/stimulus": "/assets/stimulus.min-dd364f16ec9504dfb72672295637a1c8838773b01c0b441bd41008124c407894.js",
    "@hotwired/stimulus-loading": "/assets/stimulus-loading-3576ce92b149ad5d6959438c6f291e2426c86df3b874c525b30faad51b0d96b3.js",
    "controllers/application": "/assets/controllers/application-368d98631bccbf2349e0d4f8269afb3fe9625118341966de054759d96ea86c7e.js",
    "controllers/header_controller": "/assets/controllers/header_controller-5ef2c1b35f9e828a4784c07de99f6677f79b0e04001bf975f61c93e05e05dbd4.js",
    "controllers": "/assets/controllers/index-2db729dddcc5b979110e98de4b6720f83f91a123172e87281d5a58410fc43806.js",
    "modules/animations": "/assets/modules/animations-e9abb4b3cc54531e9c76bac717c0f1cedc70a944a8703e5b655ceacfbce6d402.js"
  }
}</script>
<link rel="modulepreload" href="/assets/application-a9f5fa199a168d6cd5fbde9a8bc926cd043a28437b010cf9e918aaa6baa89442.js">
<link rel="modulepreload" href="/assets/turbo.min-dfd93b3092d1d0ff56557294538d069bdbb28977d3987cb39bc0dd892f32fc57.js">
<link rel="modulepreload" href="/assets/stimulus.min-dd364f16ec9504dfb72672295637a1c8838773b01c0b441bd41008124c407894.js">
<link rel="modulepreload" href="/assets/stimulus-loading-3576ce92b149ad5d6959438c6f291e2426c86df3b874c525b30faad51b0d96b3.js">
<script src="/assets/es-module-shims.min-4ca9b3dd5e434131e3bb4b0c1d7dff3bfd4035672a5086deec6f73979a49be73.js" async="async" data-turbo-track="reload"></script>
<script type="module">import "application"</script>
App 81199 output: [fe87f964-ff44-4b8f-98a8-817aa10fbcf4]   
App 81199 output: [fe87f964-ff44-4b8f-98a8-817aa10fbcf4] app/views/layouts/application.html.erb:8

Solution
This could be due to various reasons. Here's a potential solution for one of them:
https://stackoverflow.com/questions/65487087/rails-6-capistrano-error-assetsprecompile

Problem

00:26 deploy:cleanup
      Keeping 5 of 6 deployed releases on 111.111.111.111
      01 rm -rf /home/deploy/app-name/releases/20240206085033 /home/deploy/app-name/releases/20240206093740 /home/deploy/app-name/releases/20240206094157
      01 rm:
      01 cannot remove '/home/deploy/app-name/releases/20240206085033/public/assets/actioncable.esm-642a147cbb90e93c6f2bcaeeb817a4a263aa4f971a6d95795835270bd8519dfd.js.gz'
      01 : Permission denied

Solution
https://stackoverflow.com/questions/19546404/capistrano-v3-not-able-to-cleanup-old-releases

Problem
"Unable to connect", Error 403. And in the logs:

directory index of "/home/deploy/app-name/current/public/" is forbidden, client: 111.111.111.111, server: example.com, request: "GET / HTTP/1.1", host: "example.com"

Solution
Something is wrong with Nginx. Check your config. Check permissions. Check Nginx user. Make sure nginx user has necessary rights.

https://serverfault.com/questions/641413/403-forbidden-response-on-a-ubuntu-nginx-passenger-server

Problem
Redirect from HTTP to HTTPS by the browser.

Solution
I don't know exactly why it's happening (happend in Chrome & Firefox). Maybe something was wrong with my Nginx config (but it was correct I believe), maybe browser cache (but I purged it), maybe browser redirect. But after issuing a certificate everything started to work.

Problem
If you manually restart Passenger process:

~/app-name/current passenger-config restart-app
#=>  Phusion Passenger(R) is currently not serving any applications.

Solution
If you are using Capistrano, check Capistrano logs, there should be the same error.
There may be various reasons for this error. It makes sense to start research with Nginx config. In my case I wrote down incorrect passenger_app_root path.

The end.