Baike.dev
All toolsAI codingTrendingOpen sourceNewsSubmit
Log in
< Back to tools
S

sshkit

> DevOps
Open source

A toolkit for deploying code and assets to servers in a repeatable, testable, reliable way.

1.2K stars0 likes0 views
WebsiteGitHub

About

A toolkit for deploying code and assets to servers in a repeatable, testable, reliable way.

SSHKit is a toolkit for running commands in a structured way on one or more servers.

Example

  • Connect to 2 servers
  • Execute commands as deploy user with RAILS_ENV=production
  • Execute commands in serial (default is :parallel)
require 'sshkit'
require 'sshkit/dsl'
include SSHKit::DSL

on ["1.example.com", "2.example.com"], in: :sequence do |host|
  puts "Now executing on #{host}"
  within "/opt/sites/example.com" do
    as :deploy  do
      with RAILS_ENV: 'production' do
        execute :rake, "assets:precompile"
        execute :rails, "runner", "S3::Sync.notify"
      end
    end
  end
end

Many other examples are in EXAMPLES.md.

Basic usage

The on() method is used to specify the backends on which you'd like to run the commands. You can pass one or more hosts as parameters; this runs commands via SSH. Alternatively you can pass :local to run commands locally. By default SSKit will run the commands on all hosts in parallel.

Running commands

All backends support the execute(*args), test(*args) & capture(*args) methods for executing a command. You can call any of these methods in the context of an on() block.

Note: In SSHKit, the first parameter of the execute / test / capture methods has a special significance. If the first parameter isn't a Symbol, SSHKit assumes that you want to execute the raw command and the as / within / with methods, SSHKit.config.umask and the comand map have no effect.

Typically, you would pass a Symbol for the command name and it's args as follows:

on '1.example.com' do
  if test("[ -f somefile.txt ]")
    execute(:cp, 'somefile.txt', 'somewhere_else.txt')
  end
  ls_output = capture(:ls, '-l')
end

By default the capture methods strips whitespace. If you need to preserve whitespace you can pass the strip: false option: capture(:ls, '-l', strip: false)

Transferring files

All backends also support the upload! and download! methods for transferring files. For the remote backend, the file is transferred with scp by default, but sftp is also supported. See EXAMPLES.md for details.

on '1.example.com' do
  upload! 'some_local_file.txt', '/home/some_user/somewhere'
  download! '/home/some_user/some_remote_file.txt', 'somewhere_local', log_percent: 25
end

Users, working directories, environment variables and umask

When running commands, you can tell SSHKit to set up the context for those commands using the following methods:

as(user: 'un', group: 'grp') { execute('cmd') } # Executes sudo -u un -- sh -c 'sg grp cmd'
within('/somedir') { execute('cmd') }           # Executes cd /somedir && cmd
with(env_var: 'value') { execute('cmd') }       # Executes ENV_VAR=value cmd
SSHKit.config.umask = '077'                     # All commands are executed with umask 077 && cmd

The as() / within() / with() are nestable in any order, repeatable, and stackable.

When used inside a block in this way, as() and within() will guard the block they are given with a check.

In the case of within(), an error-raising check will be made that the directory exists; for as() a simple call to sudo -u -- sh -c ' wrapped in a check for success, raising an error if unsuccessful.

The directory check is implemented like this:

if test ! -d ; then echo "Directory doesn't exist" 2>&1; false; fi

And the user switching test is implemented like this:

if ! sudo -u  whoami > /dev/null; then echo "Can't switch user" 2>&1; false; fi

According to the defaults, any command that exits with a status other than 0 raises an error (this can be changed). The body of the message is whatever was written to stdout by the process. The 1>&2 redirects the standard output of echo to the standard error channel, so that it's available as the body of the raised error.

Helpers such as runner() and rake() which expand to execute(:rails, "runner", ...) and execute(:rake, ...) are convenience helpers for Ruby, and Rails based apps.

Verbosity / Silence

  • raise verbosity of a command: execute "echo DEAD", verbosity: :ERROR
  • hide a command from output: execute "echo HIDDEN", verbosity: :DEBUG

Parallel

Notice on the on() call the in: :sequence option, the following will do what you might expect:

on(in: :parallel) { ... }
on(in: :sequence, wait: 5) { ... }
on(in: :groups, limit: 2, wait: 5) { ... }

The default is to run in: :parallel which has no limit. If you have 400 servers, this might be a problem and you might better look at changing that to run in groups, or sequence.

Groups were designed in this case to relieve problems (mass Git checkouts) where you rely on a contested resource that you don't want to DDOS by hitting it too hard.

Sequential runs were intended to be used for rolling restarts, amongst other similar use-cases.

The default runner can be set with the SSHKit.config.default_runner option. For example:

SSHKit.config.default_runner = :parallel
SSHKit.config.default_runner = :sequence
SSHKit.config.default_runner = :groups
SSHKit.config.default_runner = MyRunner # A custom runner

If more control over the default runner is needed, the SSHKit.config.default_runner_config can be set.

# Set the runner and then the config for the runner
SSHKit.config.default_runner = :sequence
SSHKit.config.default_runner_config = { wait: 5 }

# Or just set everything once
SSHKit.config.default_runner_config = { in: :sequence, wait: 5 }

Synchronisation

The on() block is the unit of synchronisation, one on() block will wait for all servers to complete before it returns.

For example:

…

The Command Map

It's often a problem that programmatic SSH sessions don't have the same environment variables as interactive sessions.

A problem often arises when calling out to executables expected to be on the $PATH. Under conditions without dotfiles or other environmental configuration, $PATH may not be set as expected, and thus executables are not found where expected.

To try and solve this there is the with() helper which takes a hash of variables and makes them available to the environment.

with path: '/usr/local/bin/rbenv/shims:$PATH' do
  execute :ruby, '--version'
end

Will execute:

( PATH=/usr/local/bin/rbenv/shims:$PATH /usr/bin/env ruby --version )

By contrast, the following won't modify the command at all:

with path: '/usr/local/bin/rbenv/shims:$PATH' do
  execute 'ruby --version'
end

Will execute, without mapping the environmental variables, or querying the command map:

ruby --version

(This behaviour is sometimes considered confusing, but it has mostly to do with shell escaping: in the case of whitespace in your command, or newlines, we have no way of reliably composing a correct shell command from the input given.)

Often more preferable is to use the command map.

The command map is used by default when instantiating a Command object

The command map exists on the configuration object, and in principle is quite simple, it's a Hash structure with a default key factory block specified, for example:

puts SSHKit.config.command_map[:ruby]
# => /usr/bin/env ruby

To make clear the environment is being deferred to, the /usr/bin/env prefix is applied to all commands. Although this is what happens anyway when one would simply attempt to execute ruby, making it explicit hopefully leads people to explore the documentation.

One can override the hash map for individual commands:

SSHKit.config.command_map[:rake] = "/usr/local/rbenv/shims/rake"
puts SSHKit.config.command_map[:rake]
# => /usr/local/rbenv/shims/rake

Another opportunity is to add command prefixes:

SSHKit.config.command_map.prefix[:rake].push("bundle exec")
puts SSHKit.config.command_map[:rake]
# => bundle exec rake

SSHKit.config.command_map.prefix[:rake].unshift("/usr/local/rbenv/bin exec")
puts SSHKit.config.command_map[:rake]
# => /usr/local/rbenv/bin exec bundle exec rake

One can also override the command map completely, this may not be wise, but it would be possible, for example:

SSHKit.config.command_map = Hash.new do |hash, command|
  hash[command] = "/usr/local/rbenv/shims/#{command}"
end

This would effectively make it impossible to call any commands which didn't provide an executable in that directory, but in some cases that might be desirable.

Note: All keys should be symbolised, as the Command object will symbolize it's first argument before attempting to find it in the command map.

Interactive commands

(Added in version 1.8.0)

By default, commands against remote servers are run in a non-login, non-interactive ssh session. This is by design, to try and isolate the environment and make sure that things work as expected, regardless of any changes that might happen on the server side. This means that, although the server may have prompted you, and be waiting for it, you cannot send data to the server by typing into your terminal window. Wherever possible, you should call commands in a way that doesn't require interaction (eg by specifying all options as command arguments).

However in some cases, you may want to programmatically drive interaction with a command and this can be achieved by specifying an :interaction_handler option when you execute, capture or test a command.

It is not necessary, or desirable to enable Netssh.config.pty to use the interaction_handler option. Only enable Netssh.config.pty if the command you are calling won't work without a pty.

An interaction_handler is an object which responds to on_data(command, stream_name, data, channel). The interaction_handler's on_data method will be called each time stdout or stderr data is available from the server. Data can be sent back to the server using the channel parameter. This allows scripting of command interaction by responding to stdout or stderr lines with any input required.

For example, an interaction handler to change the password of your linux user using the passwd command could look like this:

class PasswdInteractionHandler
  def on_data(command, stream_name, data, channel)
    puts data
    case data
      when '(current) UNIX password: '
        channel.send_data("old_pw\n")
      when 'Enter new UNIX password: ', 'Retype new UNIX password: '
        channel.send_data("new_pw\n")
      when 'passwd: password updated successfully'
      else
        raise "Unexpected stderr #{stderr}"
    end
  end
end

# ...

execute(:passwd, interaction_handler: PasswdInteractionHandler.new)

Using the SSHKit::MappingInteractionHandler

Often, you want to map directly from a short output string returned by the server (either stdout or stderr) to a corresponding input string (as in the case above). For this case you can specify the interaction_handler option as a hash. This is used to create a SSHKit::MappingInteractionHandler which provides similar functionality to the linux expect library:

execute(:passwd, interaction_handler: {
  '(current) UNIX password: ' => "old_pw\n",
  /(Enter|Retype) new UNIX password: / => "new_pw\n"
})

Note: the key to the hash keys are matched against the server output data using the case equals === method. This means that regexes and any objects which define === can be used as hash keys.

Hash keys are matched in order, which allows for default wildcard matches:

execute(:my_command, interaction_handler: {
  "some specific line\n" => "specific input\n",
  /.*/ => "default input\n"
})

You can also pass a Proc object to map the output line from the server:

execute(:passwd, interaction_handler: lambda { |server_data|
  case server_data
  when '(current) UNIX password: '
    "old_pw\n"
  when /(

Issues· 0 open

View all issuesOpen on GitHub

No open issues yet, or sync has not completed.

> Tags

Rubydevopsrubyssh

No comments yet. Be the first to share.

> Details

PublishedAug 1, 2026
UpdatedSep 17, 2026
CategoryDevOps
PricingOpen source

> Related tools

D
Docker
容器化平台,标准化应用交付
G
GitHub Actions
GitHub 原生 CI/CD 工作流
N
Nginx
高性能 Web 服务器与反向代理