Excellent GoRails Article(which is main source for this guide too). I did benefited from lot of other resources online as well.
(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.
sudo apt-get update && sudo apt-get upgrade -y && sudo apt-get autoremove -y && reboot
We will use another use for deployments and will leave root alone. Run these commands one by one.
adduser deployer
adduser deployer sudo
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 yarn, redis, git curl and all other tools we will need for rails apps commonly.
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
sudo add-apt-repository ppa:chris-lea/redis-server
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).
curl -L get.rvm.io | bash -s stable
This will probably fail for first time with instruction to add signature, do that and run this again.
rvm install 2.6.5
rvm use --default 2.6.5
gem install bundler:1.17
We needed bundler 1, if you are on bundler 2, that should already be available on new versions of ruby, so you can skip last command from above.
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. :)
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.
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.
- 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
.
-
Now lets create
github_key
file:nano ~/.ssh/github_key
and paste contents ofgithub_key
from your local, which you just generated and added its public key in github setting of your account. Save this file as well. -
Adjust permission of this file with:
chmod 400 ~/.ssh/github_key
. -
Run:
ssh -T [email protected]
and add the signature by entering yes when asked.
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.
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.
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, '[email protected]: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
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.
- 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.
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 useserver_name _
which will make all traffic on this server to go to our rails app.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
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.
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!
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