Skip to content

Instantly share code, notes, and snippets.

@ziaulrehman40
Last active December 13, 2022 09:37
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 6 You must be signed in to fork a gist
  • Save ziaulrehman40/19c87cc73dd75ce6e5d717ace8ddcc48 to your computer and use it in GitHub Desktop.
Save ziaulrehman40/19c87cc73dd75ce6e5d717ace8ddcc48 to your computer and use it in GitHub Desktop.
Server setup on ubuntu VPS(DigitalOcean, EC2 or any other) for rails

Excellent GoRails Article(which is main source for this guide too). I did benefited from lot of other resources online as well.

Creating droplet on DigitalOcean(DO):

(Skip this if you are not using DO, and create your own VPS instance on the service of your choice. Steps below may still be helpful.)

There are few options for this step, DO provide us some pre built images for lot of platforms in which they have pre-installed nginx, node and other related dependencies for each image. Rails one is little outdated, I tried that but didn't go very well for me, you can give it a shot if you have enough time. For this guide we will go with bear bone ubuntu 18.04 server. While creating the droplet, you will be asked to set an ssh key, just generate an ssh key with ssh-keygen on your local system and copy contents of .pub file into the textbox provided.

Select your resources and create droplet(provide your ssh key while creating and enable private networking, ipv6 and monitoring, this all is free).

Now ssh with command like ssh root@IP_OF_DROPLET. On initial ssh(i assume you know how to do ssh-add for the key you have set up for your droplet), you will be asked your root password. This password is mailed to you, if you didn't received the password, close the ssh session and reset it from your droplet setting page. Than ssh and provide this password, you have to change it first time, do that and we are good to go now.

After you are in with SSH in VPS:

Update System:

sudo apt-get update && sudo apt-get upgrade -y && sudo apt-get autoremove -y && reboot

Add deployer user for deployments:

We will use another use for deployments and will leave root alone. Run these commands one by one.

adduser deployer
adduser deployer sudo

Setup ssh for this user:

We will be using same ssh keys for both root and deployer user, so we will just copy over the key from root to deployer home.

USER_NAME=deployer
sudo mkdir /home/$USER_NAME/.ssh
sudo cp ~/.ssh/authorized_keys /home/$USER_NAME/.ssh/
sudo chown $USER_NAME.$USER_NAME /home/$USER_NAME/.ssh -R
sudo chmod go-rwx /home/$USER_NAME/.ssh -R

If you want to restrict system access, than you probably should have separate ssh key for this deployer user.

IMPORTANT: At this point, exit the ssh session and ssh back in with deployer user instead of root ssh deployer@IP_OF_DROPLET and do all other configuration from this user.

Lets install some dependencies:

Lets install yarn, redis, git curl and all other tools we will need for rails apps commonly.

We can use NVM(https://github.com/nvm-sh/nvm) for version management of nodejs

curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
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
curl -fsSL https://packages.redis.io/gpg | sudo gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/redis.list
sudo apt-get update
sudo apt-get upgrade
sudo apt-get install git openssl libpq-dev rng-tools dirmngr -y
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 redis-server redis-tools nodejs yarn -y

Lets check and make sure redis is setup to run on boot and is running properly

sudo systemctl enable redis
sudo update-rc.d redis-server enable
sudo update-rc.d redis-server defaults
sudo systemctl restart redis-server.service
sudo systemctl enable redis-server.service
redis-benchmark -q -n 1000 -c 10 -P 5

Last command from above will confirm you have your redis server properly running(if successful).

RVM, ruby and bundler2(if needed):

curl -L get.rvm.io | bash -s stable

This will probably fail for first time with instruction to add signature, do that and run it again, once it succeeds, install ruby version of your choice(specif your required bundler version as per your gemfile.lock).

rvm install 3.1.1
rvm use --default 3.1.1
gem install bundler:2.3.20

Nginx Installation

NOTE: to avoid issues with broken packages etc, you can follow https://www.phusionpassenger.com/library/install/nginx/install/oss/bionic/ for passenger

sudo apt-get install -y apt-transport-https ca-certificates
sudo apt-get install -y nginx-extras passenger
sudo service nginx start

Now visit ip of droplet in any browser and nginx welcome page should be there.

Yay! We have our server running! Well, not quite, some work is still left. :)

Configure swap file(if you want to)

Run sudo swapon -s and if it gives some output, you can skip this step as you already have swap configured. Otherwise run following commands.

 df
 sudo fallocate -l 2048m /mnt/swap_file.swap
 sudo chmod 600 /mnt/swap_file.swap
 sudo mkswap /mnt/swap_file.swap
 sudo swapon /mnt/swap_file.swap
 sudo nano /etc/fstab

Place /mnt/swap_file.swap none swap sw 0 0 at end of this file and save it with ctrl+x and than enter y and than enter(typical nano editor usage).

It would be a good idea to restart your droplet now. Use reboot command.

Setup Github SSH key

There are good guides from github on this like this to generate keys(i went with passphrase less key) and this to add that generated key to your github account.

Follow both of above help articles, lets assume you named your ssh key github_key and see next steps.

  1. Lets add config file to load github keys
touch ~/.ssh/config
nano ~/.ssh/config

Paste in this file(mind the key name if that is different):

Host *
  AddKeysToAgent yes
  IdentityFile ~/.ssh/github_key

And save it as before with ctrl+x and than enter y and than enter.

  1. Now lets creategithub_key file: nano ~/.ssh/github_key and paste contents of github_key from your local, which you just generated and added its public key in github setting of your account. Save this file as well.

  2. Adjust permission of this file with: chmod 400 ~/.ssh/github_key.

  3. Run: ssh -T git@github.com and add the signature by entering yes when asked.

Lets Prepare Deployment Directories

Of-course replace PROJECTNAME with your project name

cd
PROJECTNAME=simplypo
mkdir  apps
mkdir  apps/$PROJECTNAME/
mkdir  apps/$PROJECTNAME/shared
cd apps/$PROJECTNAME/shared/
mkdir config

For my setup, config folder will be a linked directory via capistrano.

I used dotenv for local and production, so i made a .env.production file in shared folder and pasted all environemnt variables, keys etc. You should configure your environment variables with whatever approach you are using.

Setup Database

Note: If you dont want to use managed DB, than instead of these steps, you need to setup your DB in your droplet yourself.

I prefer managed DB instances instead of local installs, it just reduces complexity. DO recently launched managed postgres DBs, lets spin up one of those. Choose your version and setup DB.

DO UI provides you a nice walkthrough to setup security, access rules, connection polling etc. Do all of that as your liking.

I prefer to create a new user for our app, instead of using default user. Create user and get username password and url of DB.

Important: Make sure your database.yml is using proper environment variables we need to set. And save these connection information in your environemnt variables on server properly. For me, i adjusted those in my .env.production file.

Capistrano setup for deployment

You may follow official repo to set capistrano(there are commands to generate file, use those first to get initial files), i will just paste my files here JUST FOR REFERENCE:

Gemfile:

group :development do
  # Deployment
  gem 'capistrano',         require: false
  gem 'capistrano-bundler', require: false
  gem 'capistrano-rails',   require: false
  gem 'capistrano-rake'
  gem 'capistrano-rvm', require: false
  gem 'capistrano-sidekiq'
  gem 'capistrano-yarn'
  gem 'capistrano3-puma', require: false

  # For sudo of capistrano(makes capistarno ask password when needed instead of getting hanged)
  gem 'sshkit-sudo'
end

Capfile

# frozen_string_literal: true

# Load DSL and set up stages
require 'capistrano/setup'

# Include default deployment tasks
require 'capistrano/deploy'
require 'capistrano/rails'
require 'capistrano/yarn'
require 'capistrano/rvm'
require 'capistrano/puma'
require 'capistrano/rake'
require 'capistrano/sidekiq'
require 'sshkit/sudo'

require 'capistrano/scm/git'
install_plugin Capistrano::SCM::Git

install_plugin Capistrano::Puma
install_plugin Capistrano::Puma::Nginx

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

deploy.rb

# frozen_string_literal: true

set :repo_url,        'git@github.com:COMPANY/REPO.git'

# Don't change these unless you know what you're doing
set :pty,             true
set :use_sudo,        false
set :deploy_via,      :remote_cache
set :deploy_to,       "/home/#{fetch(:user)}/apps/#{fetch(:application)}"
set :puma_bind,       "unix://#{shared_path}/tmp/sockets/#{fetch(:application)}-puma.sock"
set :puma_state,      "#{shared_path}/tmp/pids/puma.state"
set :puma_pid,        "#{shared_path}/tmp/pids/puma.pid"
set :puma_access_log, "#{release_path}/log/puma.error.log"
set :puma_error_log,  "#{release_path}/log/puma.access.log"
set :puma_preload_app, true
set :puma_worker_timeout, nil

set :rvm_ruby_version, '2.6.2'

set :keep_releases, 5

## Linked Files & Directories (Default None):
append :linked_dirs, 'log', 'tmp/pids', 'tmp/cache', 'tmp/sockets', 'vendor/bundle', 'public/system', 'public/uploads'
append :linked_files, '.env.production'

set sidekiq_config: 'config/sidekiq.yml'
SSHKit.config.command_map[:sidekiq]    = 'bundle exec sidekiq'
SSHKit.config.command_map[:sidekiqctl] = 'bundle exec sidekiqctl'

namespace :puma do
  desc 'Create Directories for Puma Pids and Socket'
  task :make_dirs do
    on roles(:app) do
      execute "mkdir #{shared_path}/tmp/sockets -p"
      execute "mkdir #{shared_path}/tmp/pids -p"
    end
  end

  before :start, :make_dirs
end

namespace :deploy do
  desc 'Make sure local git is in sync with remote.'
  task :check_revision do
    on roles(:app) do
      unless `git rev-parse HEAD` == `git rev-parse origin/master`
        puts 'ERROR: HEAD is not the same as origin/master'
        puts 'Run `git push` to sync changes.'
        exit
      end
    end
  end

  desc 'Initial Deploy'
  task :initial do
    on roles(:app) do
      before 'deploy:restart', 'puma:start'
      invoke 'deploy'
    end
  end

  desc 'Restart application'
  task :restart do
    on roles(:app), in: :sequence, wait: 5 do
      invoke! 'puma:restart'
    end
  end

  before :starting,  :check_revision
  after  :finishing, :compile_assets
  after  :finishing, :cleanup
end

production.rb

# frozen_string_literal: true

set :deploy_to, -> { '/home/deployer/apps/PROJECTNAME' }
set :user,            'deployer'
set :rails_env,       'production'
set :application,     'PROJECTNAME'
set :user,            'deployer'
set :branch,          'master'
set :stage,           :production
server 'IP_OF_DROPLET',
       user: 'deployer',
       port: 22,
       roles: %i[web app db sidekiq cronjobs],
       primary: true

set :sidekiq_processes, 1

Configure Nginx using capistrano

Important: We have master branch configured in our capistrano, so make sure you have local and remote master branches synced. (Because capistrano picks configuration from local branch but deploys remote branch by doing git pull on our server.).

cap production puma:nginx_config this command will upload nginx configuration which will give us our template we can tweak to configure nginx.

Now ssh with deployer user.

Edit /etc/nginx/sites-enabled/default with nano or any other editor, comment our port binding and server binding in this file. This file is from where nginx was serving its welcome page. We want our website not nginx welcome page, right? Save and exit.

Our last capistrano command will have generated another file in /etc/nginx/sites-enabled/ directory, now lets edit that.

  1. Make sure paths in this file are generated correctly, if not, this is indication you have something messed up in your capistrano configuration. Which you need to fix, delete this file and try again.
  2. server_name should have your domain in front of it, lets assume we don't have a domain to bind and we are(for now) only using IP to access the server, for that we will use server_name _ which will make all traffic on this server to go to our rails app.
  3. sudo service nginx restart

If anything goes wrong with nginx config, you may use these commands to start from scratch

sudo apt-get purge nginx-common nginx-extras passenger nginx nginx-core
sudo rm -rf /etc/nginx

LETS DEPLOY!

For first time do: cap production deploy:initial. After that everytime you will just do: cap production deploy

If you encounter any issues on initial deploy, debug the errors, fix those and try again.

NOTE: For rails 6, config/secrets.yml is not auto created because rails want you to use rails secrets/credentials feature. So either use those or setup this secrets.yml file, which works through rails 6.0 at-least. Ana populate secret_key_base properly.

Setup Firewall:

You should probably setup firewall as well, unlike amazon AWS, DO by default has no firewall enabled(in bare bone servers). But its dead easy to setup a firewall, they have it all built in their UI. Find that option in network setting of the droplet, enable firewall and open 22, 80, 8080 and 443 ports.

And we are done!

In case of any typos or issues in this guide, please comment those and i will get those fixed. Happy Coding!

Setup SSL

For SSL you can use either free solutions liek CertBot, or install custom purchased ssl keys properly.

I will only list some gotchas.

1- Don't forget to uncomment config.force_ssl = true in production.rb

2- And see this: https://stackoverflow.com/a/21793954/4738391 (you might have this line already in the nginx config, just change it from http to https).

3- In case you are using your own custom purchased SSL instead of CertBot, and you have created new file with touch and have pasted private key by copying pasting in it, you may need to do iconv -c -f UTF8 -t ASCII my.key >> my.key on that file if nginx config on running sudo nginx -t -c /etc/nginx/nginx.conf is giving PRIVATE EKY errors after installing SSL.

4- If http is not being redirected to https yet, force it with: https://stackoverflow.com/a/50618063/4738391

@ziaulrehman40
Copy link
Author

ziaulrehman40 commented Nov 29, 2019

To debug nginx config issues:

nginx -c /etc/nginx/nginx.conf -t

Following might also be of help on EC2:

nginx -c /etc/nginx/nginx.conf -t
sudo chown -R www-data:www-data /var/log/nginx
sudo find /var/log/nginx -type f -exec chmod 666 {} \;
sudo find /var/log/nginx -type d -exec chmod 755 {} \;
nginx -c /etc/nginx/nginx.conf -t
sudo find /run/nginx.pid -type d -exec chmod 755 {} \;
nginx -c /etc/nginx/nginx.conf -t

@ziaulrehman40
Copy link
Author

ziaulrehman40 commented Nov 29, 2019

Sidekiq 6 startup issues with capistrano: seuros/capistrano-sidekiq#224 (comment)

Following is more complete and modified /home/USERNAME/.config/systemd/user/sidekiq-production.service file for reference(mainly log file is configured, see this for detail):

(Replace APPNAME and USER appropriately)

[Unit]
Description=sidekiq for APPNAME (production)
After=syslog.target network.target

[Service]
Type=simple
Environment=RAILS_ENV=production
Environment=MALLOC_ARENA_MAX=2

WorkingDirectory=/home/USER/apps/APPNAME/current

ExecStart=/home/USER/.rvm/gems/ruby-2.6.5/wrappers/bundle exec sidekiq -e production -C config/sidekiq.yml
ExecReload=/bin/kill -TSTP $MAINPID
ExecStop=/bin/kill -TERM $MAINPID

RestartSec=1
Restart=on-failure

SyslogIdentifier=sidekiq

StandardOutput=file:/home/USER/apps/APPNAME/shared/log/sidekiq.log

[Install]
WantedBy=default.target

After savign file, you need to run systemctl --user daemon-reload before trying to deploy again.

@ziaulrehman40
Copy link
Author

ziaulrehman40 commented Nov 29, 2019

Log rotation: https://stackoverflow.com/a/4883967/4738391

In short:

sudo touch /etc/logrotate.d/FILE_NAME
sudo nano /etc/logrotate.d/FILE_NAME

And put the following in the file, change the path and group name in last line if needed.

/home/deploy/apps/APP/shared/log/*.log {
    weekly
    missingok
    rotate 52
    compress
    delaycompress
    notifempty
    copytruncate
    su root deployer
}

Save and exit.

To check if log rotation will work properly, sudo logrotate --force /etc/logrotate.d/FILE_NAME

@ziaulrehman40
Copy link
Author

ziaulrehman40 commented Dec 3, 2019

IMPORTANT NOTE IF YOU ARE USING OR INCLUDING CSS IN ANY JS/JSX FILE IN app/javascript folder. Otherise no styles will be loaded in production builds.

You need to include = stylesheet_pack_tag 'application' in your layouts, it is not needed in development but is required in production. Learnt it the hard way on rails 6 after spending almost half day on this issue.

@ziaulrehman40
Copy link
Author

ziaulrehman40 commented Dec 5, 2019

Install Postgres 12:
https://computingforgeeks.com/install-postgresql-12-on-ubuntu/

Configure DB backups:
https://medium.com/@zek/automated-backups-with-the-ruby-backup-gem-and-amazon-s3-f0f2f986876e

Gotcha: Version 4.2.0 only works with ruby 2.3.x, and other latest version of backup gem work with newer rubies but have this bug: backup/backup#751

So:
Run following

rvm install 2.3
rvm use 2.3
gem install backup -v 4.2.0
backup generate:model --trigger APP_NAME_prod_backup --archives --storages='s3' --databases='postgresql' --compressor='gzip'

than follow along the above article, and in the last for schedule.rb file for whenever gem, instead of;

command "backup perform -t APP_NAME_prod_backup"

add this:

command "rvm use 2.3.8; backup perform -t APP_NAME_prod_backup"

@ziaulrehman40
Copy link
Author

ziaulrehman40 commented Jan 21, 2020

For action cable setup:

  1. Put following in the nginx config(assuming ssl is configured already)
  location /cable {
    proxy_pass http://NAME_OF_UPSTREAM;
    proxy_http_version 1.1;
    proxy_set_header X-Forwarded-Proto https;
    proxy_redirect off;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "Upgrade";
  }
# NAME_OF_UPSTREAM should be replaced properly
  1. Put action_cable_meta_tag in layouts, before JS import tags, if not already placed.

  2. For my setup, mount ActionCable.server => '/cable' was also placed in the routes.rb

  3. config.action_cable.allowed_request_origins = [ENV['SERVER_BASE_URL']] in production.rb (just for security).

GOTCHA(s):
1- r is not a function error in browser: rails/rails#35501 (comment)

@ziaulrehman40
Copy link
Author

If face ruby dependency issue: https://www.phusionpassenger.com/library/install/nginx/install/oss/bionic/

The following packages have unmet dependencies:
 passenger : Depends: ruby (< 1:2.6~) but 2:2.5.0+1bbox1~bionic1 is to be installed
E: Unable to correct problems, you have held broken packages.

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