Automate deployments with Capistrano
Why automate?
Let’s suppose you have a static HTML website built with Jekyll and you deploy this on an Ubuntu 20.04 server. If you manually deploy the website, the flow for every release will probably look something like this:
- Push changes to remote Git repo (e.g.,
git push origin master
) - SSH into server (e.g.,
ssh sajad@123.44.55.66
) - cd into project folder and run
git pull
to pull latest updates - Install dependencies (e.g.,
bundle install
) - Run a build task to build static HTML pages, CSS and JS files ( e.g.,
bundle exec jekyll build
).
That is tedious. Let’s see how you can automate those steps so that the flow becomes:
- Push changes to remote Git repo (e.g.,
git push origin master
) - Run deploy command (e.g.,
bundle exec cap deploy production
)
That’s what Capistrano tries to do. It lets you encapsulate a bunch of steps (SSH into server, git pull, install dependencies, etc.) into a single command. Let’s take a look.
Install capistrano gem
Add to Gemfile.
gem 'capistrano', '~> 3.16', require: false
Execute:
bundle install
Capify project
bundle exec cap install
This should create the following files:
├── Capfile
├── config
│ ├── deploy
│ │ ├── production.rb
│ │ └── staging.rb
│ └── deploy.rb
└── lib
└── capistrano
└── tasks
Let’s assume you only have a production server so go ahead and delete the config/deploy/staging.rb
file.
Configure config/deploy.rb
The config/deploy.rb
file is used to set global configuration that is shared between multiple environments (e.g., staging, ci, production, etc). Edit config/deploy.rb
so it looks like this:
# frozen_string_literal: true
lock '~> 3.16.0'
set :application, 'whatever'
set :repo_url, '<git-repo>'
set :keep_releases, 5
namespace :app do
desc 'ls'
task :ls do
on roles(:app) do
execute :ls, '-la'
end
end
desc 'printenv'
task :printenv do
on roles(:app) do
execute :printenv
end
end
end
We’ll use the custom app:ls
task to test that Capistrano can successfully SSH into our servers.
Configure config/deploy/production.rb
In Capistrano, a server represents a single server (e.g., EC2 instance or Digital Ocean droplet). Each server can have one or more roles such as web
, app
, or db
. For the example Jekyll website, you can use a single app
role.
Edit config/deploy/production.rb
:
# frozen_string_literal: true
server '<ipv4-address>', 'user': '<ssh-user>', roles: %w[app]
set :deploy_to, '<deploy-path-on-server>'
set :branch, 'master'
Before you proceed further, you’ll want to ensure you’ve configured SSH access to your server so that running ssh <ssh-user>@<ipv4-address>
logs you into the server.
Test Capistrano configuration
Run the following command:
bundle exec cap production app:ls
You should see the list of files and directories in the home directory of the default deploy user in the production
server. If you get any errors, it’s probably because your SSH access isn’t configured correctly..
Test deploy
Run the following command to run a dry run (simulation):
bundle exec cap production deploy --dry-run
If this is successful, go ahead and deploy for real:
bundle exec cap production deploy
(Optional) Setup rbenv
If you’re not using rbenv, skip to the next section.
Add the following to your Gemfile
:
gem 'capistrano-rbenv', '~> 2.2'
Execute:
bundle install
Add the following to your Capfile
:
require 'capistrano/rbenv'
Edit config/deploy.rb
:
set :rbenv_type, :user # or :system, or :fullstaq (for Fullstaq Ruby), depends on your rbenv setup
# If setting from file:
set :rbenv_ruby, File.read('.ruby-version').strip
# in case you set it explicitly:
# set :rbenv_ruby, '3.1.0'
#
set :rbenv_prefix, "RBENV_ROOT=#{fetch(:rbenv_path)} RBENV_VERSION=#{fetch(:rbenv_ruby)} #{fetch(:rbenv_path)}/bin/rbenv exec"
set :rbenv_map_bins, %w{rake gem bundle ruby rails}
set :rbenv_roles, :all # default value
Setup bundler
Add the following to your Gemfile
:
gem 'capistrano-bundler', '~> 2.0'
Execute:
bundle install
Add the following to your Capfile
:
require 'capistrano/bundler'
Edit config/deploy.rb
so that the .bundle
directory is configured as a persistent directory:
append :linked_dirs, '.bundle'
Check that capistrano/bundler
has been configured properly:
bundle exec cap -T | grep bundler
You should see a bunch of tasks listed:
cap bundler:clean # Remove unused gems installed by bundler
cap bundler:config # Configure the Bundler environment for the release so that subsequent
cap bundler:install # Install the current Bundler environment
cap bundler:map_bins # Maps all binaries to use `bundle exec` by default
Execute a dry run to ensure your deployment is still working:
bundle exec cap production deploy --dry-run
You should see bundler tasks (e.g., bundler:install
) in the deploy log.
Add custom task
Let’s add a custom app:build
task that runs bundle exec jekyll build
. Edit config/deploy.rb
:
# frozen_string_literal: true
# config valid for current version and patch releases of Capistrano
lock '~> 3.16.0'
set :application, 'website'
set :repo_url, 'git@github.com:sajadtorkamani/website.git'
set :keep_releases, 5
# Default value for linked_dirs is []
append :linked_dirs, '.bundle'
set :rbenv_type, :user # or :system, or :fullstaq (for Fullstaq Ruby), depends on your rbenv setup
set :rbenv_ruby, File.read('.ruby-version').strip
set :rbenv_prefix, "RBENV_ROOT=#{fetch(:rbenv_path)} RBENV_VERSION=#{fetch(:rbenv_ruby)} #{fetch(:rbenv_path)}/bin/rbenv exec"
set :rbenv_map_bins, %w{rake gem bundle ruby rails}
set :rbenv_roles, :all # default value
namespace :app do
desc 'ls'
task :ls do
on roles(:app) do
execute :ls, '-la'
end
end
desc 'printenv'
task :printenv do
on roles(:app) do
execute :printenv
end
end
desc 'Build Jekyll website'
task :build do
on roles(:app) do
within release_path do
execute :bundle, 'exec', 'jekyll', 'build'
end
end
end
end
after 'bundler:install', 'app:build'
Notice the new app:build
task that will run bundle exec jekyll build
. We’ve also configured this task to run after bundler:install
.
Configure Jekyll
Next, edit config/deploy/production.rb
to set the JEKYLL_ENV
environment variable:
# frozen_string_literal: true
# frozen_string_literal: true
server '<ipv4-address>', 'user': '<ssh-user>', roles: %w[app]
set :deploy_to, '<deploy-path-on-server>'
set :branch, 'master'
set :default_env, {
'JEKYLL_ENV' => 'production'
}
You’ll also want to setup the .jekyll-cache
directory as a shared directory. A shared directory will be reused between different releases so that each release symlinks to the same shared directory.
Edit config/deploy.rb
and update the append :linked_dirs
line:
append :linked_dirs, '.bundle', '.jekyll-cache'
Run a dry run to make sure the app:build
task is configured to execute after bundle:install
:
bundle exec cap production deploy --dry-run
Deploy
Finally, you’re ready to deploy:
bundle exec cap production deploy
(Optional) Quick deployment
Create a bin/deploy
bash script:
touch bin/deploy
Make it executable:
chmod +x bin/deploy
Add the following:
#!/usr/bin/env bash
function deploy {
bundle exec cap production deploy
}
function notify {
notify-send "Deployment successful" # Assumes notify-send is available
}
deploy && notify
Now, you can deploy and get notified of deployment all with a single command:
./bin/deploy
Sources
Thanks for your comment 🙏. Once it's approved, it will appear here.
Leave a comment