Skip to content

Instantly share code, notes, and snippets.

@pedromschmitt
Last active November 21, 2023 21:42
Show Gist options
  • Save pedromschmitt/230c2d97ee3bc7122b62f01c2807d6df to your computer and use it in GitHub Desktop.
Save pedromschmitt/230c2d97ee3bc7122b62f01c2807d6df to your computer and use it in GitHub Desktop.
Setting up a new server and deploy Ruby

Setting up a new server and deploy Ruby on Rails project with Capistrano on VPS (Digital Ocean/Hetzner/etc)

This use Rails 5 or 6, RVM and Puma.

If you want to deploy with RBenv and Passenger, take a look at Phyl Smy script https://github.com/philsmy/cable-guy-example/blob/main/SetUpServer.txt and his video where he recorded each step https://www.youtube.com/watch?v=CZtYDplotiI&t=1s

If you need to deploy to a EC2 on AWS, check this video of how to create the server first: https://www.youtube.com/watch?v=M0avxObh8J8

Script

Dependencies

Safety

This is based on this article: https://medium.com/@sysrex/my-first-10-minutes-on-a-server-d79ea273809b

ssh root@<ip do servidor>'

Add a root password

curl 'https://www.random.org/passwords/?num=2&len=24&format=plain&rnd=new'

passwd

security upgrades later on:

apt-get update  
apt-get upgrade

Deployer user

Create a deployer user: You should never be logging on to a server as root.

useradd deployer
mkdir /home/deployer
mkdir /home/deployer/.ssh
chmod 700 /home/deployer/.ssh
Setup your prefered shell for the deployer user, here we use bash:

usermod -s /bin/bash deployer

Set password and add deployer user to sudo and root groups:

curl 'https://www.random.org/passwords/?num=2&len=24&format=plain&rnd=new'

passwd deployer

addgroup deployer root

adduser deployer sudo

Now access the Sudoers file to give permission to deployer run commands without system ask for password:

vim /etc/sudoers
or
nano /etc/sudoers
or
visudo

Add to last line (/etc/sudoers):

deployer ALL=(ALL) NOPASSWD: ALL

Copy SSH permissions from root to deployer:

rsync --archive --chown=deployer:deployer ~/.ssh /home/deployer

You can add more SSH keys:

vim /home/deployer/.ssh/authorized_keys

Let’s set the right permissions based on the Linux security principal of least privilege:

chmod 400 /home/deployer/.ssh/authorized_keys  
chown deployer:deployer /home/deployer -R

Exit server as a root and log in again as a deployer .

exit
ssh deployer@xxx.xxx.xxx.xxx

Or just type: exec su -l deployer

Remove root login with password (only SSH will work). Run sudo vim /etc/ssh/sshd_config and add/change:

PermitRootLogin no  
PasswordAuthentication no  

If you have a VPN/STATIC IP, you can restrict login to olny that IP, adding to this file:

AllowUsers deployer@(your-VPN-or-static-IP) 
AddressFamily inet

Enable all these rules by restarting the ssh service. You’ll probably need to reconnect (do so by using your deploy user!) sudo service ssh restart

Setting up a firewall

First we’ll want to make sure that we are supporting IPv6. Just open up the config file. vim /etc/default/ufw Set IPv6 to yes. IPV6=yes

For the rest of the ports that we’re going to open up, we can just use the ufw tool from command line which is very handy.

sudo ufw allow 22  
sudo ufw allow 80  
sudo ufw allow 443  
sudo ufw disable  
sudo ufw enable

Or, if you use Static IP:

sudo ufw allow from {your-ip} to any port 22  
sudo ufw allow 80  
sudo ufw allow 443  
sudo ufw disable  
sudo ufw enable
Automated security updates

I like these. They’re not perfect, but it’s better than missing patches as they come out. sudo apt-get install unattended-upgrades

acess sudo vim /etc/apt/apt.conf.d/10periodic Update this file to match this:

APT::Periodic::Update-Package-Lists "1";  
APT::Periodic::Download-Upgradeable-Packages "1";  
APT::Periodic::AutocleanInterval "7";  
APT::Periodic::Unattended-Upgrade "1";

Fail2Ban

sudo apt-get install fail2ban

Rails dependencies (You can use the Go Rails tutorial. I use Puma and RVM. Go Rails use Passenger and Rbenv)

After that, install dependencies:

Adding Node.js 12 repository

curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -

Utilize uma versao diferente se necessário.

Adding Yarn repository

curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list

Redis

sudo add-apt-repository ppa:chris-lea/redis-server

Refresh our packages list with the new repositories

sudo apt-get update

Install our dependencies for compiiling Ruby along with Node.js and Yarn

sudo apt-get install git-core curl 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 dirmngr gnupg apt-transport-https ca-certificates nodejs yarn redis-server redis-tools -y

Install ImageMagick if necessary

sudo apt install imagemagick -y

Nginx

sudo apt install nginx -y

Testing Nginx:

systemctl status nginx

Or access your server IP onm browser and see if you get a nginx default message.

Lets create the application folder:

sudo chown deployer:deployer /home/deployer
mkdir apps
sudo chown deployer:deployer apps

RVM

Install rvm to manage ruby versions:

curl -sSL https://get.rvm.io | bash
source /home/deployer/.rvm/scripts/rvm

Ruby

Install ruby (change x.x.x for your version version):

rvm install 2.X.X --default

Check if the ruby version is correct:

ruby -v

Bundler

Install bundler (change x.x.x for your version version):

gem install bundler -v x.x.x --no-document

Rails

Install Rails (change x.x.x for your version version):

gem install rails -v 6.X.X.X --no-document

Check your rails version:

rails -v

Postgres

For Postgres, we're going to start by installing the Postgres server and libpq which will allow us to compile the pg rubygem. Then, we're going to become the postgres linux user who has full access to the database and use that account to create a new database user for our apps. We'll call that user deployer. And finally, the last command will create a database called myapp and make the deployer user owner. Make sure to change myapp_database_name to the name of your application.

sudo apt-get install postgresql postgresql-contrib libpq-dev -y
sudo su - postgres
createuser --superuser --pwprompt deployer
createdb -O deployer **myapp_database_name/changeme**
exit

You can manually connect to your database anytime by running: psql -U deployer -W -h 127.0.0.1 -d myapp_databas_name. Make sure to use 127.0.0.1 when connecting to the database instead of localhost.

Capistrano

Check if you have in your Gemfile at development group:

  gem "capistrano", "~> 3.12", require: false
  gem "capistrano-rails", require: false
  gem "capistrano-yarn"
  gem 'capistrano-rvm'
  gem 'capistrano3-puma'

At your local machine, in app folder, run:

cap install

Update the created files:

  • Capfile
# Load DSL and set up stages
require "capistrano/setup"
# Include default deployment tasks
require "capistrano/deploy"
# Include tasks from other gems included in your Gemfile

# require 'capistrano/rails'
require 'capistrano/bundler' # Rails needs Bundler, right?
require 'capistrano/rails/assets'
require 'capistrano/rails/migrations'

require 'capistrano/rvm'
require 'capistrano/yarn'
# require "whenever/capistrano"
require 'capistrano/delayed_job'

# Load the SCM plugin appropriate to your project:
require "capistrano/scm/git"
install_plugin Capistrano::SCM::Git

require 'capistrano/puma'
install_plugin Capistrano::Puma
install_plugin Capistrano::Puma::Workers
install_plugin Capistrano::Puma::Nginx

# Whenever gem config
# set :whenever_identifier, ->{ "#{fetch(:application)}_#{fetch(:stage)}" }

#Load custom tasks from `lib/capistrano/tasks` if you have any defined
Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r }
  • config/deploy.rb Update the git repo Add at :linked_dirs the folders that have to be shared among the deploy versions. If you use a local Active Storage (without AWS) add a "storage" folder at append :linked_dirs
# config valid for current version and patch releases of Capistrano
lock "~> 3.12.1"

set :application, "NOME-DA-APLICACAO"
# Update repo address
set :repo_url, "git@github.com:xxxxxxxx/xxxxxxxxxxx.git"

# Update folder
set :deploy_to, "/home/deployer/apps/app-name"

#change master.key for secrets.yml, if needed.

append :linked_files, "config/database.yml", "config/master.key"#, "config/local_env.yml", "config/sidekiq.yml"

append :linked_dirs, "log", "tmp"
 
set :keep_releases, 5
set :migration_role, :app

set :puma_pid, "#{shared_path}/tmp/pids/puma.pid"
set :puma_bind, "unix://#{shared_path}/tmp/sockets/puma.sock"
set :puma_access_log, "#{shared_path}/log/puma_access.log"
set :puma_error_log, "#{shared_path}/log/puma_error.log"
 
set :nginx_sites_available_path, "/etc/nginx/sites-available"
set :nginx_sites_enabled_path, "/etc/nginx/sites-enabled"

#Change ruby version
set :rvm_type, :user
set :rvm_ruby_version, '2.X.X'

namespace :puma do
    desc 'Create Puma dirs'
    task :create_dirs do
      on roles(:app) do
        execute "mkdir #{shared_path}/tmp/sockets -p"
        execute "mkdir #{shared_path}/tmp/pids -p"
      end
    end
   
    desc "Restart Nginx"
    task :nginx_restart do
      on roles(:app) do
        execute "sudo service nginx restart"
      end
    end
   
    before :start, :create_dirs
    after :start, :nginx_restart
  end
  • config/deploy/production.rb
set :branch, 'master'
# Update user 
set :user, 'deployer'
# Update server IP
set :server_address, 'xx.xx.xxx.xxx'

ask(:password, nil, echo: false)
server fetch(:server_address), user: fetch(:user), roles: %w{app db web}

set :nginx_server_name, fetch(:server_address)
set :puma_preload_app, true

Log in on server and generate a SSH key (I usually do that without the password)

ssh-keygen

Copy the SSH key

cat /home/deployer/.ssh/id_rsa.pub

Add this key to your repo at Github, on "deploy keys"

Log out of server and check at your **local machine ** if the deploy is ok:

cap production deploy:check

Capistrano will tell that a linked file does no exist. Let's create the database.yml and the others linked files needed.

Log at server, go to /apps/app-name/shared/config, and create the database.yml:

sudo vim apps/YOUR-APP-NAME/shared/config/database.yml
default: &default
  adapter: postgresql
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  timeout: 5000 
  username: deployer
  password: YOUR DATABASE PASSWORD


production:
  <<: *default
  database: APP-NAME

Now copy your master.key (or create it):

 scp config/master.key deployer@xxxx:/home/deployer/apps/xxxxxx/shared/config/

*if you do not have a master.key you can generate one running: EDITOR=vim rails credentials:edit

Exit server and check deploy again:

cap production deploy:check

Configure Puma:

cap production puma:config

Configure Nginx:

cap production puma:nginx_config

To finalize, run the deploy!!!:

cap production deploy

SSL

Certbot

On remote server Add repository

sudo apt install ca-certificates

When you see the question: 'Press [ENTER] to continue or Ctrl-c to cancel adding it', press ENTER

Update

sudo apt-get update

Install Certbot for Nginx

sudo apt-get install python-certbot-nginx -y

Insert the data on nginx

sudo nano /etc/nginx/sites-available/nomedaaplicacao_production
server_name xxx.xxx.xx.xxx yourdomain.com;

If you use Devise for authentication, to avoid 'ActionController::InvalidAuthenticityToken' error, add below 'proxy_set_header X-Forwarded-Proto http':

    proxy_set_header X-Forwarded-Ssl on;
    proxy_set_header X-Forwarded-Port 443; 
    proxy_set_header X-Forwarded-Host $host;

(see: rails/rails#22965)

Restart Nginx

sudo service nginx reload

Get SSL

sudo certbot --nginx -d yourdomain.com

Certbot will ask your email. When asked: "Please choose whether or not to redirect HTTP traffic to HTTPS, removing HTTP access." Choose "2: Redirect" Check on the end of file etc/nginx/sites-avaliable/ if certbot conf were added:

cat /etc/nginx/sites-available/NOMEDAAPLICACAO_production

App online

After Certbot run again the deploy: cap production deploy

Congrats!!!

Other things

Set hourly database backup

Use a S3 bucket https://github.com/huacnlee/gobackup

Run on servidor: curl -sSL https://git.io/gobackup | bash

test: gobackup -h

create: sudo vim ~/.gobackup/gobackup.yml

add on this file:

models:
  my-app:
    compress_with:
      type: tgz
    store_with:
      type: s3
      keep: 120
      bucket: <bucket-s3>
      region: us-east-1
      path: backups<you can change>
      access_key_id: <add yours>
      secret_access_key: <add yours>
    databases:
      my-app:
        database: <database-name>
        type: postgresql
        host: localhost
        username: deployer
        password: <password>

Run with gobackup perform

To schedule: crontab -e Add (example for backups at each 6 hours):

# Execute GoBackup at each 6 hours
30 0,6,12,18 * * * /usr/local/bin/gobackup perform >> ~/.gobackup/gobackup.log

Or each 1 hour:

# Execute GoBackup hourly
0 * * * * /usr/local/bin/gobackup perform >> ~/.gobackup/gobackup.log

And after a hour, you can check up the execute status by ~/.gobackup/gobackup.log.

To restore database (test it!) psql -U postgres -d your-database -f /path/to/your-backup.sql

Sidekiq

Set Redis no Ubuntu: https://www.digitalocean.com/community/tutorials/how-to-install-and-secure-redis-on-ubuntu-18-04-pt or https://linuxize.com/post/how-to-install-and-configure-redis-on-ubuntu-20-04/

http://www.enquerer.com/run-sidekiq-in-digital-ocean-production-droplet-for-capistrano-deployed-rails-application/ https://github.com/mperham/sidekiq/blob/master/examples/systemd/sidekiq.service https://www.codewithjason.com/restart-sidekiq-automatically-deployment/

Add a sidekiq.yml to config:

---
  :verbose: false
  :concurrency: 10
  :queues:
  - mailers
  - default
  - low

production:
  :pidfile: /home/deployer/apps/application-name/shared/tmp/pids/sidekiq.pid
  :concurrency: 25
staging:
  :pidfile: /home/deploy/apps/application-name/shared/tmp/pids/sidekiq.pid
  :concurrency: 15

At config/deploy.rb add the sidekiq to shared files

append :linked_files, "config/database.yml", "config/master.key", "config/local_env.yml", "config/sidekiq.yml"

create pid on server: sudo touch /home/deployer/apps/my-application-name/shared/tmp/pids/sidekiq.pid sudo chmod 777 /home/deployer/apps/my-application-name/shared/tmp/pids/sidekiq.pid

In Order to start the Sidekiq as service while booting the server we need to make a Sidekiq service in our ubuntu server. So to do so, run the following in ubuntu server:

sudo nano /lib/systemd/system/sidekiq.service https://github.com/mperham/sidekiq/blob/master/examples/systemd/sidekiq.service change patch

  • 'WorkingDirectory=/home/deployer/apps/my-application-name/current'
  • ExecStart=/bin/bash -lc 'bundle exec sidekiq -e production -C config/sidekiq.yml'
#
# This file tells systemd how to run Sidekiq as a 24/7 long-running daemon.
#
# Customize this file based on your bundler location, app directory, etc.
# Customize and copy this into /usr/lib/systemd/system (CentOS) or /lib/systemd/system (Ubuntu).
# Then run:
#   - systemctl enable sidekiq
#   - systemctl {start,stop,restart} sidekiq
#
# This file corresponds to a single Sidekiq process.  Add multiple copies
# to run multiple processes (sidekiq-1, sidekiq-2, etc).
#
# Use `journalctl -u sidekiq -rn 100` to view the last 100 lines of log output.
#
[Unit]
Description=sidekiq
# start us only once the network and logging subsystems are available,
# consider adding redis-server.service if Redis is local and systemd-managed.
After=syslog.target network.target

# See these pages for lots of options:
#
#   https://www.freedesktop.org/software/systemd/man/systemd.service.html
#   https://www.freedesktop.org/software/systemd/man/systemd.exec.html
#
# THOSE PAGES ARE CRITICAL FOR ANY LINUX DEVOPS WORK; read them multiple
# times! systemd is a critical tool for all developers to know and understand.
#
[Service]
#
#      !!!!  !!!!  !!!!
#
# As of v6.0.6, Sidekiq automatically supports systemd's `Type=notify` and watchdog service
# monitoring. If you are using an earlier version of Sidekiq, change this to `Type=simple`
# and remove the `WatchdogSec` line.
#
#      !!!!  !!!!  !!!!
#
Type=notify
# If your Sidekiq process locks up, systemd's watchdog will restart it within seconds.
WatchdogSec=10

WorkingDirectory=/home/deployer/apps/my-application-name/current
# If you use rbenv:
# ExecStart=/bin/bash -lc 'exec /home/deploy/.rbenv/shims/bundle exec sidekiq -e production'
# If you use the system's ruby:
ExecStart=/bin/bash -lc 'bundle exec sidekiq -e production -C config/sidekiq.yml'
# If you use rvm in production, don't.

# Use `systemctl kill -s TSTP sidekiq` to quiet the Sidekiq process

# !!! Change this to your deploy user account !!!
User=deployer
Group=sudo
UMask=0002

# Greatly reduce Ruby memory fragmentation and heap usage
# https://www.mikeperham.com/2018/04/25/taming-rails-memory-bloat/
Environment=MALLOC_ARENA_MAX=2

# if we crash, restart
RestartSec=1
Restart=on-failure

# output goes to /var/log/syslog (Ubuntu) or /var/log/messages (CentOS)
StandardOutput=syslog
StandardError=syslog

# This will default to "bundler" if we don't specify it
SyslogIdentifier=sidekiq

[Install]
WantedBy=multi-user.target

Step 5: To start Sidekiq when the server boots up, we need to create a symlink. In order to do so, run the below command. sudo systemctl enable sidekiq.service

Step 6: Now we can start the Sidekiq service.

sudo service sidekiq start

Step 7: You can check the system log for errors.

sudo cat /var/log/syslog And check if Sidekiq started by

sudo ps aux | grep sidekiq If the Sidekiq is running you can see something like…

user 2609 0.7 3.3 1352776 136960 ? Ssl 06:18 0:15 sidekiq 5.2.7 application-name[0 of 25 busy] Here it is the Sidekiq running successfully as service in your ubuntu droplet.

Now whenever the server boots up the Sidekiq server will also restart. You can manually stop, start, or restart the server by,

sudo service sidekiq start/stop/restart

Update the namespace :puma block on capistrano (config/deploy.rb) to restart sidekiq after each deploy:

namespace :sidekiq do
  desc "Restart Sidekiq"
  task :restart do
    on roles(:app) do
      execute "sudo service sidekiq restart"
    end
  end
end

namespace :deploy do
  desc "Restart Sidekiq"
  task :restart_sidekiq do
    on roles(:app) do
      execute "sudo service sidekiq restart"
    end
  end

  after :finishing, :restart_sidekiq
end

Configuring Logrotate For Rails Production Logs

https://gorails.com/guides/rotating-rails-production-logs-with-logrotate You might be surprised at just how easy to setup logrotate Rails logs is. The reason it is so handy is that a bunch of your operating system software is already using it. We just have to plug in our configuration and we’re set!

The first step is to open up /etc/logrotate.conf using vim or nano. Jump to the bottom of the file an add the following block of code. You’ll want to change the first line to match the location where your Rails app is deployed. Mine is under the deploy user’s home directory. Make sure to point to the log directory with the *.log bit on the end so that we rotate all the log files.

/home/deploy/APPNAME/current/log/*.log {
  daily
  missingok
  rotate 7
  compress
  delaycompress
  notifempty
  copytruncate
}

or

# Rotate Rails application logs based on file size
# Rotate log if file greater than 20 MB
/path/to/your/rails/applicaton/log/*.log {
    size=20M
    missingok
    rotate 52
    compress
    delaycompress
    notifempty
    copytruncate
}

or

# Rotate Rails application logs weekly
/path/to/your/rails/applicaton/log/*.log {
  weekly
  missingok
  rotate 52
  compress
  delaycompress
  notifempty
  copytruncate
}

How It Works This is fantastically easy. Each bit of the configuration does the following:

  • daily – Rotate the log files each day. You can also use weekly or monthly here instead.
  • missingok – If the log file doesn’t exist, ignore it
  • rotate 7 – Only keep 7 days of logs around
  • compress – GZip the log file on rotation
  • delaycompress – Rotate the file one day, then compress it the next day so we can be sure that it won’t interfere with the Rails server
  • notifempty – Don’t rotate the file if the logs are empty
  • copytruncate – Copy the log file and then empties it. This makes sure that the log file Rails is writing to always exists so you won’t get problems because the file does not actually change. If you don’t use this, you would need to restart your Rails application each time. Running Logrotate Since we just wrote this configuration, you’ll want to test it.

To run logrotate manually, we just do: sudo /usr/sbin/logrotate -f /etc/logrotate.conf

You’re going to want to run it a second time to make sure the delaycompress option is working and to actually compress the log. Here’s an example of what you’ll see if you ls the log folder after running logrotate twice:

You can see that the production.log still exists, production.log.1 is a copy of the logs between the first and second run of logrotate, and production.log.2.gz is the 300MB behemoth of a log file that we had before compressed nicely with Gzip. Once we get up to 7 log files, the next time logrotate runs, it will delete the oldest one so that we only have 7 days worth of logs. If you want to keep all the logs around, you can remove the rotate line from the configuration.

Plus, since we just edited the main logrotate.conf file, the cron job will automatically execute the logrotate Rails logs daily!

Adding ENV

Create fiel .env sudo vim /etc/.env and add the vars you need, for example:

SENDGRID_USERNAME=myuser
SENDGRID_PASSWORD=xxxxxxxxxxxxxxxx

Add on bash, run: sudo vim ~/.bashrc And add on first line:

source /etc/.env

Possible errors

Webpacker not compiling - error when run rake assets:precompile on rails 6

https://dev.to/tcgumus/rails-6-webpacker-settings-for-production-1f1e

@bhrama-br
Copy link

Grande Schmitt, tem tutorial para rails 7 e puma 6? :D

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