Capistrano SSH

2 minute read

A quickie this time. I use Capistrano to deploy my Rails apps. There was a gem for Capistrano 2 that added an ssh command as in:

cap production ssh

This would open an SSH connection to the production server and cd you to the current directory for the app. Super handy if you work on a lot of projects and don’t want remember what the server names and deploy paths for all of them.

The Capistrano 3 has a much different internal structure and the gem is long abandoned. Fortunately, it’s easy to recreate this functionality in a view lines of code. Create lib/capistrano/tasks/ssh.rake and drop in:

desc "ssh to the current directory on the server"
task :ssh do
  on roles(:app), :primary => true do |host|
    command = "cd #{fetch(:deploy_to)}/current && exec $SHELL -l"
    puts command if fetch(:log_level) == :debug
    exec "ssh -l #{host.user} #{host.hostname} -p #{host.port || 22} -t '#{command}'"
  end
end

The fiddly bits are the cd && exec and the -t. -t makes sure we have a terminal allocated on the server. Normally, when executing commands (as opposed to starting a shell), a terminal isn’t needed, but in this case we’re manually starting a shell, and we need it.

SSH, and then cap, will exit when the remote command finish. If we just ran cd the command would login, change directory and then finish. Exec-ing a shell after the cd causes SSH to want for the command, which will only finish when we log out. Exec-ing $SHELL instead of hardcoding bash, given us some flexibility.

It’s a simple thing, but a nice time saver.

And for bonus points, the most common reason to open a shell to the server is to play around in the console. You can easily modify this task to do that with:

desc "open rails console on server"
task :console do
  on roles(:app), :primary => true do |host|
    command = "cd #{fetch(:deploy_to)}/current && bundle exec rails console #{fetch(:rails_env)}"
    puts command if fetch(:log_level) == :debug
    exec "ssh -l #{host.user} #{host.hostname} -p #{host.port || 22} -t '#{command}'"
  end
end

(You could append this to ssh.rake or create a separate console.rake.)

Note that when SSH is used to run a command, it does source your shell variables. If you use RVM or rbenv, you may need to manual set up you environment. For example, an RVM use might need:

command = "source /etc/profile.d/rvm.sh; cd #{fetch(:deploy_to)}/current && bundle exec rails console #{fetch(:rails_env)}"

Using ; instead of && will cause the cd and bundle to run even if the source fails, a quick and dirty way to make this work in cases where some servers are using RVM and some are not.

Tags: ,

Updated:

Comments