Capistrano is a deployment automation tool built on Ruby, Rake, and SSH. It allows you to deploy your app to a remote server in a single command after initial configurations are done.
Deployment with capistrano is as easy as cap production deploy
. But to be able to hit this command, a lot of configurations need to be added first.
Today we will be looking into setting up capistrano in our API only Rails application for making it easier to deploy to any remote server.
Skills required to follow the tutorial
Intermediate:
- Rails
- Linux skills to work with commands in server where your app has been deployed
You should have
- Existing Rails app
- Linux server already setup to run Rails app
NOTE:
I am using rbenv for ruby so all configurations will be based on that, you can replace it as required for your version manager as required e.g. for rvm.
Step 1: Install Capistrano Gems
Add the following to your Gemfile under development group
group :development do
gem "capistrano"
gem "capistrano-rails"
gem 'capistrano-rbenv'
end
Run the following from command line:
$ bundle install
That should install the latest version of capistrano gems, now let's lock those versions by looking at Gemfile.lock, these versions were what I had in my lock file, yours could be different.
group :development do
gem "capistrano", "~> 3.17", require: false
gem "capistrano-rails", "~> 1.6", require: false
gem 'capistrano-rbenv', '~> 2.2', require: false
end
Step 2: Generate default configuration files
Run the generator to create a basic set of configuration files:
$ bundle exec cap install
Step 3: Require correct plugins in Capfile
Uncomment the following plugins in Capfile located at the root of the project
# require "capistrano/rvm"
require 'capistrano/rbenv'
# require "capistrano/chruby"
require 'capistrano/bundler'
require "capistrano/rails/assets"
require 'capistrano/rails/migrations'
# require 'capistrano/passenger'
Step 4: Update Deploy file
Update the deploy file at config/deploy.rb
with values as required:
- application: Set to the application name or project name
- repo_url: Set to the URL where your code is being stored, you can normally get this with
git remote -v
and copy the URL of origin - linked_files: Set to a list of files that should be shared and persisted over all releases, for e.g. config/database.yml, config/master.key, config/application.yml, etc. These files will not be deleted/reset in every release (other files will!!)
- linked_dirs: Set to a list of folders that should be shared and persisted over all releases, for e.g. log, tmp/pids, node_modules, etc. These folders will not be deleted/reset in every release (other folders will!!)
- keep_releases: Set to a number of previous releases you want to keep in the server after each release, I normally keep this to 3.
- conditionally_migrate: Skip migration if files in "db/migrate" were not modified. I normally set this to "true" but default is "false"
Your deploy file could look something like below:
# frozen_string_literal: true
# config valid for current version and patch releases of Capistrano
lock '~> 3.17.0'
set :application, 'contract-template-editor-api'
set :repo_url, 'git@github.com:truemark/contract-template-editor/api.git'
# Default value for :linked_files is []
set :linked_files, %w[config/application.yml config/database.yml config/master.key]
# Default value for linked_dirs is []
set :linked_dirs, %w[log tmp/pids tmp/cache tmp/sockets vendor/bundle .bundle public/system public/uploads node_modules]
# Default value for keep_releases is 5
set :keep_releases, 3
# Skip migration if files in db/migrate were not modified
# Defaults to false
set :conditionally_migrate, true
Step 5: Update environment specific deploy files
I have normally worked on projects that has staging and production environment and configuration files for these two environment is provided default by Capistrano; each environment specific file is located under config/deploy/{environment}.rb
.
In these files, there should be configurations that can change based on the environment of the Rails application.
Configurations for each environment will be the same with different value based on the environment e.g. server ip address will be different in production and staging.
We will only be looking at config/deploy/production.rb
in this tutorial.
Update the deploy file at config/deploy/production.rb
with values as required:
- server: IP address of the server where you will deploy the app. It is a good idea to sore this value inside env file or rails credentials and take it from there.
- user: name of the user in the server, normally it will be "deploy" but could be username e.g. prabin
- roles: list of accessible roles for this user
- deploy_to: path of the folder to deploy your app to, you can get the path by logging in to the server, going to the folder you want your app to be deployed to and enter the command
pwd
. If you haven't created the folder yet, you can create one e.g. contract-template-api and set the path of that folder. - branch: git branch that will be used to deploy the app, normally "main" or "master"
- stage: environment of the app, should be "production"
- rails_env: same as stage, should be "production"
Your file could look something like below:
server ENV['deploy_server_ip'], user: 'deploy', roles: %w[app db web], primary: 'true'
set :deploy_to, 'path to the folder where app should be deployed e.g. /home/deploy/contract-template-editor/api'
set :branch, 'main'
set :stage, :production
set :rails_env, :production
Step 6: Upload secret keys and files
Secret keys and files should never be committed to git. These files are required for app to function properly. Normally these files are always configured in the developer's machine so they can be uploaded directly from the project to the server using ssh.
Create a new rake task at lib/capistrano/tasks/config_files.rake
and add the following content:
namespace :config_files do
desc 'Upload yml files inside config folder'
task :upload do
on roles(:app) do
execute "mkdir -p #{shared_path}/config"
upload! StringIO.new(File.read('config/database.yml')), "#{shared_path}/config/database.yml"
upload! StringIO.new(File.read('config/application.yml')), "#{shared_path}/config/application.yml"
upload! StringIO.new(File.read('config/master.key')), "#{shared_path}/config/master.key"
end
end
end
We now need to tell capistrano to run this code during deployment, add the following to the end of config/deploy.rb
# ================================================
# ============ From Custom Rake Tasks ============
# ================================================
# ===== See Inside: lib/capistrano/tasks =========
# ================================================
# upload configuration files
before 'deploy:starting', 'config_files:upload'
Step 7: Create a database if deploying for the first time
Create a new rake task at lib/capistrano/tasks/database.rake
and add the following content:
namespace :database do
desc 'Create the database'
task :create do
on roles(:app) do
within release_path do
with rails_env: fetch(:rails_env) do
execute :rake, 'db:create'
end
end
end
end
end
We now need to tell capistrano to run this code during deployment, add the following to the end of the deploy file.
# set this to false after deploying for the first time
set :initial, true
# run only if app is being deployed for the very first time, should update "set :initial, true" above to run this
before 'deploy:migrate', 'database:create' if fetch(:initial)
Step 8: Reload the Rails application after successful deploy
Create a new rake task at lib/capistrano/tasks/application.rake
and add the following content:
namespace :application do
desc 'Reload application'
task :reload do
desc 'Reload app after deployment'
on roles(:app), in: :sequence, wait: 5 do
execute :touch, release_path.join('tmp/restart.txt')
end
end
end
We now need to tell capistrano to run this code during deployment, add the following to the end of the deploy file.
# reload application after successful deploy
after 'deploy:publishing', 'application:reload'
Final file
Your final "config/deploy.rb" file should look similar to this:
# frozen_string_literal: true
# config valid for current version and patch releases of Capistrano
lock '~> 3.17.0'
set :application, '{project name}'
set :repo_url, '{remote git repository where project for the code is stored}'
# Default value for :linked_files is []
append :linked_files, %w[config/application.yml config/database.yml config/master.key]
# Default value for linked_dirs is []
append :linked_dirs, %w[log tmp/pids tmp/cache tmp/sockets vendor/bundle .bundle public/system public/uploads node_modules]
# Default value for keep_releases is 5
set :keep_releases, 3
# Skip migration if files in db/migrate were not modified
# Defaults to false
set :conditionally_migrate, true
# ================================================
# ============ From Custom Rake Tasks ============
# ================================================
# ===== See Inside: lib/capistrano/tasks =========
# ================================================
# upload configuration files
before 'deploy:starting', 'config_files:upload'
set :initial, true
# run only if app is being deployed for the very first time, should update "set :initial, true" above to run this
before 'deploy:migrate', 'database:create' if fetch(:initial)
# reload application after successful deploy
after 'deploy:publishing', 'application:reload'
Deploy the app
From the command line you can now deploy the app to production using the following command:
cap production deploy
The app should be deployed in around 5-10 minutes, if you encounter any error while deploying and need any help, please post a comment below and I will do my best to help you resolve it.
Bonus 1: Whenever for cron jobs
whenever gem is used in Rails applications to schedule cron jobs e.g. send email notification about monthly expenditure on the 1st of each month.
Add the following to the deploy file just below "set :conditionally_migrate, true":
# Skip migration if files in db/migrate were not modified
# Defaults to false
set :conditionally_migrate, true
# Set unique identifier for background jobs
set :whenever_identifier, -> { "#{fetch(:application)}_#{fetch(:stage)}" }
whenever_identifier should be set to unique identifier for cron jobs, this is required if you have deployed multiple applications to same server (normally for staging servers) that require whenever gem so that those applications don't clash with one another for cron jobs. This is optional if you only have one application in the server.
Create a new rake task at lib/capistrano/tasks/whenever.rake
and add the following content which will be responsible for updating cron tasks configured inside the "config/schedule.rb":
namespace :whenever do
desc 'Update cron job'
task :update_crontab do
on roles(:app) do
within current_path do
execute :bundle, :exec, "whenever --update-crontab #{fetch :whenever_identifier} --set 'environment=#{fetch(:stage)}'"
end
end
end
end
We now need to tell capistrano to run this code during deployment, add the following to the deploy file just below "before 'deploy:migrate', 'database:create' if fetch(:initial)":
# run only if app is being deployed for the very first time, should update "set :initial, true" above to run this
before 'deploy:migrate', 'database:create' if fetch(:initial)
# update cron job from whenever schedule file at "config/schedule.rb"
after 'deploy:finishing', 'whenever:update_crontab'
Bonus 2: Sidekiq for background jobs
Sidekiq gem is used in Rails applications to schedule background jobs so as to perform them at a later point without having to stop the execution of other codes.
Capistrano Configurations
Create a new rake task at lib/capistrano/tasks/sidekiq.rake
and add the following content which will be responsible for updating cron tasks configured inside the "config/schedule.rb":
namespace :sidekiq do
desc 'Quieten sidekiq'
task :quiet do
on roles(:app) do
puts capture("pgrep -f 'sidekiq' | xargs kill -TSTP")
end
end
desc 'Restart Sidekiq'
task :restart do
on roles(:app) do
execute :sudo, :systemctl, :restart, :sidekiq
execute :sudo, :systemctl, 'daemon-reload'
end
end
end
We now need to tell capistrano to run this code during deployment, add the following to the deploy file just below "after 'deploy:publishing', 'application:reload'":
# reload application after successful deploy
after 'deploy:publishing', 'application:reload'
# sidekiq related commands
after 'deploy:starting', 'sidekiq:quiet'
after 'deploy:reverted', 'sidekiq:restart'
after 'deploy:published', 'sidekiq:restart'
Now try to deploy the app to production and server will ask for password when running sidekiq commands. To fix that we need to add some more configurations in the remote server.
Server Configurations
We are assuming that sidekiq is already configured in your remote server. If you have not configured it yet, you can refer to the section "Bonus: Setup Sidekiq in Ubuntu Server" at Setup Active Job with Sidekiq in Rails
For capistrano to perform sudo actions without asking for the password, the user used by capistrano, normally "deploy" user should be in the sudo group and you should add commands that need to be executed in the server with sudo access but without using password to "/etc/sudoers" file.
-
Add deploy user to the sudo group
You can add your deploy user to the sudo group with the following command
# add "deploy" user to sudo group $ sudo usermod -aG sudo deploy # verify if the user has been added to the sudo group # result should include "sudo" for the deploy user $ groups deploy deploy : deploy sudo
-
Add commands required for the sidekiq restart and daemon-reload to be performed without password
- Open the sudoers file for the edit with
sudo EDITOR=nano visudo
, this will ensure that the content inside the file is validated before saving so you don't end up with invalid file. If you dosudo nano /etc/sudoers
then it doesn't validate the content so you should never do that. - Add the following below the line
root ALL=(ALL:ALL) ALL
under "# User privilege specification"
deploy ALL=NOPASSWD: /bin/systemctl restart sidekiq deploy ALL=NOPASSWD: /bin/systemctl daemon-reload
- Open the sudoers file for the edit with
-
You can try running above two commands now in the command line of the server and it should run normally without asking for password:
$ sudo systemctl restart sidekiq # doesn't ask for the password and executes the command
Now if you try to deploy again, capistrano won't stop to ask password when running sidekiq commands.
Conclusion
We have come to the finish line, now you should be able to deploy your API only Rails application to the server with one command.
Thanks for reading. Happy tinkering and happy coding!
Latest comments (0)