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
Remote
$ adduser deploy
# Create password, skip rest
$ adduser deploy sudo
$ exit
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
Remote - (optional) Disallow root login via ssh
$ sudo vim /etc/ssh/sshd_config
# Set line 'PermitRootLogin yes' to 'PermitRootLogin no'
$ sudo service sshd restart
Remote - (optional) Set hostname
$ sudo hostnamectl set-hostname HOSTNAME
$ sudo hostnamectl
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
Image by rawpixel
Webserver Setup
Nginx install (remote)
$ sudo apt install nginx
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
Config Nginx (remote)
$ 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;
}
}
$ sudo ln -s /etc/nginx/sites-available/APPNAME /etc/nginx/sites-enabled/APPNAME
$ sudo service nginx reload
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
Redis install (remote) - Skip if you don't use ActionCable or Turbo Streams
$ sudo apt install redis
$ sudo service redis-server status
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
$ bundle install
$ bundle exec cap install STAGES=production
Add to Capfile
require 'capistrano/rails'
require 'capistrano/passenger'
require 'capistrano/rbenv'
set :rbenv_type, :user
set :rbenv_ruby, '3.0.3'
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'
Edit config/deploy/production.rb
server 'IP ADDRESS or DOMAIN', user: 'deploy', roles: %w{app db web}
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
Local - Deploy! 🍾🍾🍾
$ bundle exec cap production deploy
# Go visit your page already!
Remote - Connect to console
$ cd APPNAME/current
$ bundle exec rails c
# You need to have the debug gem in prod env enabled! (Gemfile)
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 /
Local
$ dig DOMAIN
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
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
Top comments (4)
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:
/etc/nginx/conf.d/mod-http-passenger.conf
to revise the directive topassenger_instance_registry_dir /tmp;
deploy.rb
withset :passenger_environment_variables, { 'PASSENGER_INSTANCE_REGISTRY_DIR' => '/tmp' }
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
andmaster.key
files into theshared/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!
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?
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.