capistrano-systemd-multiservice
This gem adds capistrano tasks to control multiple services with systemd.
Installation
Add this line to your application's Gemfile:
gem 'capistrano-systemd-multiservice', require: false
And then execute:
$ bundle
Or install it yourself as:
$ gem install capistrano-systemd-multiservice
Usage
Add these lines to your Capfile:
require "capistrano/systemd/multiservice"
install_plugin Capistrano::Systemd::MultiService.new_service("example1")
install_plugin Capistrano::Systemd::MultiService.new_service("example2")
And put config/systemd/example1.service.erb
(and config/systemd/example2.service.erb
, ...) like this:
[Unit]
Description = <%= fetch(:application) %> application server example1
[Service]
Environment = RAILS_ENV=<%= fetch(:rails_env) %>
Environment = PWD=<%= current_path %>
WorkingDirectory = <%= current_path %>
ExecStart = bundle exec some-application-server start
User = exampleuser
Group = examplegroup
[Install]
WantedBy = multi-user.target
- see systemd.service(5) for details
- when
:application
is set tofoo
, this file will be installed asfoo_example1.service
(andfoo_example2.service
, ...)
And add these lines to config/deploy.rb if you want to reload/restart services on deploy:
after 'deploy:publishing', 'systemd:example1:restart'
after 'deploy:publishing', 'systemd:example2:reload-or-restart'
And then deploy.
# Upload and install systemd service unit files before deploy
cap STAGE systemd:example1:setup systemd:example2:setup
# Deploy as usual
cap STAGE deploy
User services
To have the service installed under your own user rather than root
require "capistrano/systemd/multiservice"
install_plugin Capistrano::Systemd::MultiService.new_service("example1", service_type: 'user')
install_plugin Capistrano::Systemd::MultiService.new_service("example2", service_type: 'user')
If using the user service type services will be installed in your users home directory under /.config/systemd/user
.
Systemd commands on those services can be run by passing a --user
flag, e.g. systemctl --user list-unit-files
Nothing else in setup should require change and Capistrano tasks should remain the same as when installing system services.
Capistrano Tasks
With install_plugin Capistrano::Systemd::MultiService.new_service("example1")
,
following tasks are defined.
systemd:example1:setup
systemd:example1:remove
systemd:example1:validate
systemd:example1:daemon-reload
systemd:example1:start
systemd:example1:stop
systemd:example1:reload
systemd:example1:restart
systemd:example1:reload-or-restart
systemd:example1:enable
systemd:example1:disable
See lib/capistrano/tasks/systemd/multiservice/system_service.rake, lib/capistrano/systemd/multiservice/system_service.rb for details.
Configuration Variables
With install_plugin Capistrano::Systemd::MultiService.new_service("example1")
,
following Configuration variables are defined.
:systemd_example1_role
:systemd_example1_units_src
:systemd_example1_units_dir
:systemd_example1_units_dest
:systemd_example1_instances
:systemd_example1_service
:systemd_example1_instance_services
See lib/capistrano/systemd/multiservice/system_service.rb for details.
Examples
Rails application with unicorn and delayed_job
Capfile
## ...snip...
require 'capistrano/systemd/multiservice'
install_plugin Capistrano::Systemd::MultiService.new_service('unicorn')
install_plugin Capistrano::Systemd::MultiService.new_service('delayed_job')
## ...snip...
config/deploy.rb
## ...snip...
set :application, 'foo'
## ...snip...
set :systemd_delayed_job_instances, ->{ 3.times.to_a }
after 'deploy:restart', 'systemd:unicorn:reload-or-restart'
after 'deploy:restart', 'systemd:delayed_job:restart'
after 'deploy:publishing', 'deploy:restart'
## ...snip...
config/systemd/unicorn.service.erb
This file will be installed as foo_unicorn.service
.
[Unit]
Description = <%= fetch(:application) %> unicorn rack server
[Service]
Environment = PATH=<%= fetch(:rbenv_path) %>/shims:/usr/local/bin:/usr/bin:/bin
Environment = RBENV_VERSION=<%= fetch(:rbenv_ruby) %>
Environment = RBENV_ROOT=<%= fetch(:rbenv_path) %>
Environment = RAILS_ENV=<%= fetch(:rails_env) %>
Environment = PWD=<%= current_path %>
WorkingDirectory = <%= current_path %>
ExecStart = <%= fetch(:rbenv_path) %>/bin/rbenv exec bundle exec unicorn -c <%= current_path %>/config/unicorn.rb
ExecReload = /bin/kill -USR2 $MAINPID
PIDFile = <%= shared_path %>/tmp/pids/unicorn.pid
KillSignal = SIGQUIT
KillMode = process
TimeoutStopSec = 62
Restart = always
User = app-user
Group = app-group
[Install]
WantedBy = multi-user.target
config/systemd/delayed_job.service.erb
This file will be installed as foo_delayed_job.service
.
[Unit]
Description = <%= fetch(:application) %> delayed_job
Requires = <%= fetch(:"#{prefix}_instance_services").join(" ") %>
[Service]
Type = oneshot
RemainAfterExit = yes
ExecStart = /bin/true
ExecReload = /bin/true
[Install]
WantedBy = multi-user.target
config/systemd/delayed_job@.service.erb
This file will be installed as foo_delayed_job@.service
, and creates 3 instanced service units
foo_delayed_job@0.service
, foo_delayed_job@1.service
, foo_delayed_job@2.service
because :systemd_delayed_job_instances
is set to ->{ 3.times.to_a }
in config/deploy.rb
.
[Unit]
Description = <%= fetch(:application) %> delayed_job (instance %i)
PartOf = <%= fetch(:"#{prefix}_service") %>
ReloadPropagatedFrom = <%= fetch(:"#{prefix}_service") %>
[Service]
Type = forking
Environment = PATH=<%= fetch(:rbenv_path) %>/shims:/usr/local/bin:/usr/bin:/bin
Environment = RBENV_VERSION=<%= fetch(:rbenv_ruby) %>
Environment = RBENV_ROOT=<%= fetch(:rbenv_path) %>
Environment = RAILS_ENV=<%= fetch(:rails_env) %>
Environment = PWD=<%= current_path %>
WorkingDirectory = <%= current_path %>
ExecStart = <%= fetch(:rbenv_path) %>/bin/rbenv exec bundle exec bin/delayed_job -p <%= fetch(:application) %> -i %i start
ExecStop = <%= fetch(:rbenv_path) %>/bin/rbenv exec bundle exec bin/delayed_job -p <%= fetch(:application) %> -i %i stop
ExecReload = /bin/kill -HUP $MAINPID
PIDFile = <%= shared_path %>/tmp/pids/delayed_job.%i.pid
TimeoutStopSec = 22
Restart = always
User = app-user
Group = app-group
[Install]
WantedBy = multi-user.target
config/unicorn.rb
shared_path = "/path/to/shared"
worker_processes 5
listen "#{shared_path}/tmp/sockets/unicorn.sock"
pid "#{shared_path}/tmp/pids/unicorn.pid"
stderr_path "#{shared_path}/log/unicorn_stderr.log"
stdout_path "#{shared_path}/log/unicorn_stdout.log"
preload_app true
before_exec do |server|
ENV["BUNDLE_GEMFILE"] = "/path/to/current/Gemfile"
end
before_fork do |server, worker|
if defined? ActiveRecord::Base
ActiveRecord::Base.connection.disconnect!
end
old_pid = "#{server.config[:pid]}.oldbin"
if old_pid != server.pid
begin
sig = (worker.nr + 1) >= server.worker_processes ? :QUIT : :TTOU
Process.kill(sig, File.read(old_pid).to_i)
rescue Errno::ENOENT, Errno::ESRCH
end
end
sleep 1
end
after_fork do |server, worker|
if defined?(ActiveRecord::Base)
ActiveRecord::Base.establish_connection
end
end
Commands to setup systemd services and deploy
# Upload and install systemd service unit files before deploy
cap STAGE systemd:unicorn:setup systemd:delayed_job:setup
# Deploy as usual
cap STAGE deploy
Development
After checking out the repo, run bin/setup
to install dependencies. Then, run rake spec
to run the tests. You can also run bin/console
for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install
. To release a new version, update the version number in version.rb
, and then run bundle exec rake release
, which will create a git tag for the version, push git commits and tags, and push the .gem
file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/groovenauts/capistrano-systemd-multiservice.