├── lib
└── capistrano
│ ├── mb
│ ├── templates
│ │ ├── crontab.erb
│ │ ├── rbenv_bashrc
│ │ ├── logrotate.erb
│ │ ├── pgpass.erb
│ │ ├── version.rb.erb
│ │ ├── csr_config.erb
│ │ ├── postgresql-backup-logrotate.erb
│ │ ├── sidekiq.service.erb
│ │ ├── maintenance.html.erb
│ │ ├── unicorn.service.erb
│ │ ├── ssl_setup
│ │ ├── nginx.erb
│ │ ├── unicorn.rb.erb
│ │ └── nginx_unicorn.erb
│ ├── version.rb
│ ├── compatibility.rb
│ ├── recipe.rb
│ └── dsl.rb
│ ├── tasks
│ ├── provision.rake
│ ├── seed.rake
│ ├── crontab.rake
│ ├── logrotate.rake
│ ├── rake.rake
│ ├── maintenance.rake
│ ├── migrate.rake
│ ├── nginx.rake
│ ├── bundler.rake
│ ├── ufw.rake
│ ├── version.rake
│ ├── user.rake
│ ├── dotenv.rake
│ ├── ssl.rake
│ ├── sidekiq.rake
│ ├── unicorn.rake
│ ├── aptitude.rake
│ ├── rbenv.rake
│ ├── defaults.rake
│ └── postgresql.rake
│ └── mb.rb
├── CHANGELOG.md
├── Gemfile
├── Rakefile
├── .gitignore
├── .github
├── workflows
│ └── push.yml
└── release-drafter.yml
├── LICENSE.txt
├── capistrano-mb.gemspec
└── README.md
/lib/capistrano/mb/templates/crontab.erb:
--------------------------------------------------------------------------------
1 | # Sample crontab (empty)
2 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | Release notes for this project are kept here: https://github.com/mattbrictson/capistrano-mb/releases
2 |
--------------------------------------------------------------------------------
/lib/capistrano/mb/version.rb:
--------------------------------------------------------------------------------
1 | module Capistrano
2 | module MB
3 | VERSION = "0.35.1".freeze
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | # Specify your gem's dependencies in capistrano-mb.gemspec
4 | gemspec
5 |
--------------------------------------------------------------------------------
/lib/capistrano/mb/templates/rbenv_bashrc:
--------------------------------------------------------------------------------
1 | if [ -d $HOME/.rbenv ]; then
2 | export PATH="$HOME/.rbenv/bin:$PATH"
3 | eval "$(rbenv init -)"
4 | fi
5 |
--------------------------------------------------------------------------------
/lib/capistrano/mb/templates/logrotate.erb:
--------------------------------------------------------------------------------
1 | <%= shared_path %>/log/*.log {
2 | daily
3 | nomissingok
4 | rotate 7
5 | compress
6 | delaycompress
7 | notifempty
8 | copytruncate
9 | }
10 |
--------------------------------------------------------------------------------
/lib/capistrano/mb/templates/pgpass.erb:
--------------------------------------------------------------------------------
1 | <%= fetch(:mb_postgresql_host) %>:5432:<%= fetch(:mb_postgresql_database) %>:<%= fetch(:mb_postgresql_user) %>:<%= fetch(:mb_postgresql_password).gsub(/([\\:])/, '\\\\\1') %>
2 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require "bundler/gem_tasks"
2 |
3 | Rake::Task["release"].enhance do
4 | puts "Don't forget to publish the release on GitHub!"
5 | system "open https://github.com/mattbrictson/capistrano-mb/releases"
6 | end
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.gem
2 | *.rbc
3 | .bundle
4 | .config
5 | .yardoc
6 | Gemfile.lock
7 | InstalledFiles
8 | _yardoc
9 | coverage
10 | doc/
11 | lib/bundler/man
12 | pkg
13 | rdoc
14 | spec/reports
15 | test/tmp
16 | test/version_tmp
17 | tmp
18 |
--------------------------------------------------------------------------------
/lib/capistrano/tasks/provision.rake:
--------------------------------------------------------------------------------
1 | # Define an empty provision task.
2 | # This will be filled in by other recipes that contribute additional
3 | # `before` and `during` tasks.
4 |
5 | desc "Install and set up all app prerequisites"
6 | task :provision
7 |
--------------------------------------------------------------------------------
/lib/capistrano/mb/templates/version.rb.erb:
--------------------------------------------------------------------------------
1 | Rails.application.config.version = "<%= git_version[:tag] %>"
2 | Rails.application.config.version_date = Date.parse("<%= git_version[:date] %>")
3 | Rails.application.config.version_time = Time.zone.parse("<%= git_version[:time] %>")
4 |
--------------------------------------------------------------------------------
/lib/capistrano/mb/templates/csr_config.erb:
--------------------------------------------------------------------------------
1 | [ req ]
2 | distinguished_name="req_distinguished_name"
3 | prompt="no"
4 |
5 | [ req_distinguished_name ]
6 | C="<%= fetch(:mb_ssl_csr_country) %>"
7 | ST="<%= fetch(:mb_ssl_csr_state) %>"
8 | L="<%= fetch(:mb_ssl_csr_city) %>"
9 | O="<%= fetch(:mb_ssl_csr_org) %>"
10 | CN="<%= fetch(:mb_ssl_csr_name) %>"
11 |
--------------------------------------------------------------------------------
/.github/workflows/push.yml:
--------------------------------------------------------------------------------
1 | on: push
2 | name: Push
3 | jobs:
4 | draftRelease:
5 | name: Draft Release
6 | runs-on: ubuntu-latest
7 | steps:
8 | - uses: actions/checkout@master
9 | - name: Draft Release
10 | uses: toolmantim/release-drafter@v5.2.0
11 | env:
12 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
13 |
--------------------------------------------------------------------------------
/lib/capistrano/tasks/seed.rake:
--------------------------------------------------------------------------------
1 | mb_recipe :seed do
2 | prior_to "deploy:publishing", "mb:seed"
3 | end
4 |
5 | namespace :mb do
6 | desc "Run rake db:seed"
7 | task :seed do
8 | on primary(:app) do
9 | within release_path do
10 | with :rails_env => fetch(:rails_env) do
11 | execute :rake, "db:seed"
12 | end
13 | end
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/lib/capistrano/tasks/crontab.rake:
--------------------------------------------------------------------------------
1 | mb_recipe :crontab do
2 | during "deploy:published", "mb:crontab"
3 | end
4 |
5 | namespace :mb do
6 | desc "Install crontab using crontab.erb template"
7 | task :crontab do
8 | on roles(:cron) do
9 | tmp_file = "/tmp/crontab"
10 | template "crontab.erb", tmp_file
11 | execute "crontab #{tmp_file} && rm #{tmp_file}"
12 | end
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/lib/capistrano/tasks/logrotate.rake:
--------------------------------------------------------------------------------
1 | mb_recipe :logrotate do
2 | during :provision, "mb:logrotate"
3 | end
4 |
5 | namespace :mb do
6 | desc "Configure logrotate for Rails logs"
7 | task :logrotate do
8 | privileged_on release_roles(:all) do
9 | template "logrotate.erb",
10 | "/etc/logrotate.d/#{application_basename}-logs",
11 | :mode => 644,
12 | :owner => "root:root",
13 | :sudo => true
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/lib/capistrano/tasks/rake.rake:
--------------------------------------------------------------------------------
1 | mb_recipe :rake do
2 | # No hooks
3 | end
4 |
5 | namespace :mb do
6 | desc "Remotely execute a rake task"
7 | task :rake do
8 | if ENV['COMMAND'].nil?
9 | raise "USAGE: cap #{fetch(:stage)} mb:rake COMMAND=my:task"
10 | end
11 |
12 | on primary(:app) do
13 | within current_path do
14 | with :rails_env => fetch(:rails_env) do
15 | execute :rake, ENV['COMMAND']
16 | end
17 | end
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/.github/release-drafter.yml:
--------------------------------------------------------------------------------
1 | name-template: "$NEXT_PATCH_VERSION"
2 | tag-template: "v$NEXT_PATCH_VERSION"
3 | categories:
4 | - title: "⚠️ Breaking Changes"
5 | label: "⚠️ Breaking"
6 | - title: "✨ New Features"
7 | label: "✨ Feature"
8 | - title: "🐛 Bug Fixes"
9 | label: "🐛 Bug Fix"
10 | - title: "📚 Documentation"
11 | label: "📚 Docs"
12 | - title: "🏠 Housekeeping"
13 | label: "🏠 Housekeeping"
14 | change-template: "- $TITLE (#$NUMBER) @$AUTHOR"
15 | no-changes-template: "- No changes"
16 | template: |
17 | $CHANGES
18 |
--------------------------------------------------------------------------------
/lib/capistrano/mb/templates/postgresql-backup-logrotate.erb:
--------------------------------------------------------------------------------
1 | <%= fetch(:mb_postgresql_backup_path) %> {
2 | daily
3 | nomissingok
4 | rotate 30
5 | ifempty
6 | create 600 <%= user %>
7 | dateext
8 | postrotate
9 | /usr/bin/sudo -u <%= user %> PGPASSFILE=<%= fetch(:mb_postgresql_pgpass_path) %> /usr/bin/pg_dump -Fc -Z9 -O -x <%= fetch(:mb_postgresql_dump_options) %> -h <%= fetch(:mb_postgresql_host) %> -U <%= fetch(:mb_postgresql_user) %> -f <%= fetch(:mb_postgresql_backup_path) %> <%= fetch(:mb_postgresql_database) %>
10 | endscript
11 | }
12 |
--------------------------------------------------------------------------------
/lib/capistrano/mb/templates/sidekiq.service.erb:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=sidekiq worker for <%= fetch(:application) %>
3 | After=syslog.target network.target
4 |
5 | [Service]
6 | Environment=RAILS_ENV=<%= fetch(:rails_env) %>
7 | ExecStart=/bin/bash -lc 'exec bin/sidekiq -e <%= fetch(:rails_env) %> -P tmp/pids/sidekiq.pid --concurrency <%= fetch(:mb_sidekiq_concurrency) %>'
8 | PIDFile=<%= current_path %>/tmp/pids/sidekiq.pid
9 | Restart=on-failure
10 | RestartSec=1
11 | StandardError=syslog
12 | StandardInput=null
13 | StandardOutput=syslog
14 | SyslogIdentifier=<%= application_basename %>-sidekiq
15 | TimeoutStopSec=5
16 | User=<%= sidekiq_user %>
17 | WorkingDirectory=<%= current_path %>
18 |
19 | [Install]
20 | WantedBy=multi-user.target
21 |
--------------------------------------------------------------------------------
/lib/capistrano/mb/templates/maintenance.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Maintenance
6 |
17 |
18 |
19 | Maintenance
20 |
21 | Our systems are currently down for <%= reason ? reason : "maintenance" %>
22 | as of <%= Time.now.strftime("%H:%M %Z") %>.
23 |
24 | We’ll be back <%= deadline ? deadline : "shortly" %>.
25 |
26 |
27 |
--------------------------------------------------------------------------------
/lib/capistrano/mb/templates/unicorn.service.erb:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=unicorn for <%= fetch(:application) %>
3 | After=syslog.target network.target
4 |
5 | [Service]
6 | Environment=RAILS_ENV=<%= fetch(:rails_env) %>
7 | ExecReload=/usr/bin/kill -USR2 $MAINPID
8 | ExecStart=/bin/bash -lc 'exec bin/unicorn -c <%= fetch(:mb_unicorn_config) %> -E <%= fetch(:rails_env) %>'
9 | ExecStop=/usr/bin/kill -QUIT $MAINPID
10 | PIDFile=<%= fetch(:mb_unicorn_pid) %>
11 | Restart=always
12 | StandardError=syslog
13 | StandardInput=null
14 | StandardOutput=syslog
15 | SyslogIdentifier=<%= application_basename %>-unicorn
16 | TimeoutStopSec=5
17 | User=<%= unicorn_user %>
18 | WorkingDirectory=<%= current_path %>
19 |
20 | [Install]
21 | WantedBy=multi-user.target
22 |
--------------------------------------------------------------------------------
/lib/capistrano/tasks/maintenance.rake:
--------------------------------------------------------------------------------
1 | mb_recipe :maintenance do
2 | # No hooks for this recipe
3 | end
4 |
5 | namespace :mb do
6 | namespace :maintenance do
7 | desc "Tell nginx to display a 503 page for all web requests, using the "\
8 | "maintenance.html.erb template"
9 | task :enable do
10 | on roles(:web) do
11 | reason = ENV["REASON"]
12 | deadline = ENV["DEADLINE"]
13 |
14 | template "maintenance.html.erb",
15 | "#{current_path}/public/system/maintenance.html",
16 | :binding => binding,
17 | :mode => "644"
18 | end
19 | end
20 |
21 | desc "Remove the 503 page"
22 | task :disable do
23 | on roles(:web) do
24 | execute :rm, "-f", "#{current_path}/public/system/maintenance.html"
25 | end
26 | end
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/lib/capistrano/tasks/migrate.rake:
--------------------------------------------------------------------------------
1 | mb_recipe :migrate do
2 | during "deploy:migrate_and_restart", "deploy"
3 | prior_to "deploy:migrate", "enable_maintenance_before"
4 | during "deploy:published", "disable_maintenance_after"
5 | end
6 |
7 | namespace :mb do
8 | namespace :migrate do
9 | desc "Deploy the app, stopping it and showing a 503 maintenance page "\
10 | "while database migrations are being performed; then start the app"
11 | task :deploy do
12 | set(:mb_restart_during_migrate, true)
13 | invoke :deploy
14 | end
15 |
16 | task :enable_maintenance_before do
17 | if fetch(:mb_restart_during_migrate)
18 | invoke_if_defined "mb:maintenance:enable"
19 | invoke_if_defined "deploy:stop"
20 | end
21 | end
22 |
23 | task :disable_maintenance_after do
24 | if fetch(:mb_restart_during_migrate)
25 | invoke_if_defined "mb:maintenance:disable"
26 | end
27 | end
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/lib/capistrano/tasks/nginx.rake:
--------------------------------------------------------------------------------
1 | mb_recipe :nginx do
2 | during :provision, "configure"
3 | end
4 |
5 | namespace :mb do
6 | namespace :nginx do
7 | desc "Install nginx.conf files and restart nginx"
8 | task :configure do
9 | privileged_on roles(:web) do
10 | template("nginx.erb", "/etc/nginx/nginx.conf", :sudo => true)
11 |
12 | template "nginx_unicorn.erb",
13 | "/etc/nginx/sites-enabled/#{application_basename}",
14 | :sudo => true
15 |
16 | execute "sudo rm -f /etc/nginx/sites-enabled/default"
17 | execute "sudo mkdir -p /etc/nginx/#{application_basename}-locations"
18 | execute "sudo service nginx restart"
19 | end
20 | end
21 |
22 | %w(start stop restart).each do |command|
23 | desc "#{command} nginx"
24 | task command.intern do
25 | privileged_on roles(:web) do
26 | execute "sudo service nginx #{command}"
27 | end
28 | end
29 | end
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/lib/capistrano/tasks/bundler.rake:
--------------------------------------------------------------------------------
1 | mb_recipe :bundler do
2 | prior_to "bundler:install", "gem_install"
3 | end
4 |
5 | namespace :mb do
6 | namespace :bundler do
7 | desc "Install correct version of bundler based on Gemfile.lock"
8 | task :gem_install do
9 | install_command = fetch(:mb_bundler_gem_install_command, nil)
10 | next unless install_command
11 |
12 | on fetch(:bundle_servers) do
13 | within release_path do
14 | if (bundled_with = capture_bundled_with)
15 | execute "#{install_command} -v #{bundled_with}"
16 | end
17 | end
18 | end
19 | end
20 |
21 | def capture_bundled_with
22 | lockfile = fetch(:mb_bundler_lockfile, "Gemfile.lock")
23 | return unless test "[ -f #{release_path.join(lockfile)} ]"
24 |
25 | ruby_expr = 'puts $<.read[/BUNDLED WITH\n (\S+)$/, 1]'
26 | version = capture :ruby, "-e", ruby_expr.shellescape, lockfile
27 | version.strip!
28 | version.empty? ? nil : version
29 | end
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/lib/capistrano/tasks/ufw.rake:
--------------------------------------------------------------------------------
1 | mb_recipe :ufw do
2 | during :provision, "configure"
3 | end
4 |
5 | namespace :mb do
6 | namespace :ufw do
7 | desc "Configure role-based ufw rules on each server"
8 | task :configure do
9 | rules = fetch(:mb_ufw_rules, {})
10 | distinct_roles = rules.values.flatten.uniq
11 |
12 | # First reset the firewall on all affected servers
13 | privileged_on roles(*distinct_roles) do
14 | execute "sudo ufw --force reset"
15 | execute "sudo ufw default deny incoming"
16 | execute "sudo ufw default allow outgoing"
17 | end
18 |
19 | # Then set up all ufw rules according to the mb_ufw_rules hash
20 | rules.each do |command, *role_names|
21 | privileged_on roles(*role_names.flatten) do
22 | execute "sudo ufw #{command}"
23 | end
24 | end
25 |
26 | # Finally, enable the firewall on all affected servers
27 | privileged_on roles(*distinct_roles) do
28 | execute "sudo ufw --force enable"
29 | end
30 | end
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2019 Matt Brictson
2 |
3 | MIT License
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining
6 | a copy of this software and associated documentation files (the
7 | "Software"), to deal in the Software without restriction, including
8 | without limitation the rights to use, copy, modify, merge, publish,
9 | distribute, sublicense, and/or sell copies of the Software, and to
10 | permit persons to whom the Software is furnished to do so, subject to
11 | the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be
14 | included in all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/lib/capistrano/tasks/version.rake:
--------------------------------------------------------------------------------
1 | mb_recipe :version do
2 | during "deploy:updating", "write_initializer"
3 | end
4 |
5 | namespace :mb do
6 | namespace :version do
7 | desc "Write initializers/version.rb with git version and date information"
8 | task :write_initializer do
9 | git_version = {}
10 | branch = fetch(:branch)
11 |
12 | on release_roles(:all).first do
13 | with fetch(:git_environmental_variables) do
14 | within repo_path do
15 | git_version[:tag] = \
16 | capture(:git, "describe", branch, "--always --tag").chomp
17 | git_version[:date] = \
18 | capture(:git, "log", branch, '-1 --format="%ad" --date=short')\
19 | .chomp
20 | git_version[:time] = \
21 | capture(:git, "log", branch, '-1 --format="%ad" --date=iso')\
22 | .chomp
23 | end
24 | end
25 | end
26 |
27 | on release_roles(:all) do
28 | template "version.rb.erb",
29 | "#{release_path}/config/initializers/version.rb",
30 | :binding => binding
31 | end
32 | end
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/lib/capistrano/tasks/user.rake:
--------------------------------------------------------------------------------
1 | mb_recipe :user do
2 | during :provision, %w(add install_public_key)
3 | end
4 |
5 | namespace :mb do
6 | namespace :user do
7 | desc "Create the UNIX user if it doesn't already exist"
8 | task :add do
9 | privileged_on roles(:all) do |host, user|
10 | unless test("sudo grep -q #{user}: /etc/passwd")
11 | execute :sudo, "adduser", "--disabled-password", user, "= 3.3.5"
25 | spec.add_dependency "sshkit", ">= 1.6.1"
26 |
27 | spec.add_development_dependency "bundler", "~> 1.3"
28 | spec.add_development_dependency "rake"
29 | end
30 |
--------------------------------------------------------------------------------
/lib/capistrano/mb/templates/ssl_setup:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Usage:
4 | #
5 | # ssl_setup [--self]
6 | #
7 | # This script is used to generate key and CSR for use HTTPS in Nginx.
8 | #
9 | # --self Generate self-signed certificate in addition to key and CSR.
10 | # name Output files will be named as .key and .csr.
11 | # csr_config Path to file that specifies CSR information. See below.
12 | #
13 | # CSR configuration format:
14 | #
15 | # [ req ]
16 | # distinguished_name="req_distinguished_name"
17 | # prompt="no"
18 | #
19 | # [ req_distinguished_name ]
20 | # C="US"
21 | # ST="California"
22 | # L="San Francisco"
23 | # O="Example Company"
24 | # CN="www.example.com"
25 |
26 | if [[ $1 == --self ]]; then
27 | SELF_SIGN=1
28 | shift
29 | fi
30 |
31 | KEY_NAME=$1
32 | CSR_CONFIG=$2
33 |
34 | openssl req -config $CSR_CONFIG -new -newkey rsa:2048 -nodes -keyout ${KEY_NAME}.key -out ${KEY_NAME}.csr
35 | chmod 600 ${KEY_NAME}.key ${KEY_NAME}.csr
36 | echo "Created ${KEY_NAME}.key"
37 | echo "Created ${KEY_NAME}.csr"
38 |
39 | if [[ -n $SELF_SIGN ]]; then
40 | openssl x509 -req -days 365 -in ${KEY_NAME}.csr -signkey ${KEY_NAME}.key -out ${KEY_NAME}.crt
41 | chmod 600 ${KEY_NAME}.crt
42 | echo "Created ${KEY_NAME}.crt (self-signed)"
43 | fi
44 |
--------------------------------------------------------------------------------
/lib/capistrano/mb.rb:
--------------------------------------------------------------------------------
1 | require "digest"
2 | require "monitor"
3 | require "capistrano/mb/version"
4 | require "capistrano/mb/compatibility"
5 | require "capistrano/mb/dsl"
6 | require "capistrano/mb/recipe"
7 | include Capistrano::MB::DSL
8 |
9 | load File.expand_path("../tasks/provision.rake", __FILE__)
10 | load File.expand_path("../tasks/defaults.rake", __FILE__)
11 | load File.expand_path("../tasks/user.rake", __FILE__)
12 | load File.expand_path("../tasks/aptitude.rake", __FILE__)
13 | load File.expand_path("../tasks/ufw.rake", __FILE__)
14 | load File.expand_path("../tasks/ssl.rake", __FILE__)
15 | load File.expand_path("../tasks/dotenv.rake", __FILE__)
16 | load File.expand_path("../tasks/postgresql.rake", __FILE__)
17 | load File.expand_path("../tasks/nginx.rake", __FILE__)
18 | load File.expand_path("../tasks/unicorn.rake", __FILE__)
19 | load File.expand_path("../tasks/crontab.rake", __FILE__)
20 | load File.expand_path("../tasks/logrotate.rake", __FILE__)
21 | load File.expand_path("../tasks/rbenv.rake", __FILE__)
22 | load File.expand_path("../tasks/maintenance.rake", __FILE__)
23 | load File.expand_path("../tasks/migrate.rake", __FILE__)
24 | load File.expand_path("../tasks/seed.rake", __FILE__)
25 | load File.expand_path("../tasks/version.rake", __FILE__)
26 | load File.expand_path("../tasks/rake.rake", __FILE__)
27 | load File.expand_path("../tasks/sidekiq.rake", __FILE__)
28 | load File.expand_path("../tasks/bundler.rake", __FILE__)
29 |
--------------------------------------------------------------------------------
/lib/capistrano/mb/compatibility.rb:
--------------------------------------------------------------------------------
1 | module Capistrano
2 | module MB
3 | module Compatibility
4 | def self.check
5 | check_capistrano_and_rake_are_loaded
6 | check_blacklisted_capistrano_version
7 | end
8 |
9 | def self.check_capistrano_and_rake_are_loaded
10 | return if defined?(Capistrano::VERSION) && defined?(Rake)
11 |
12 | warn "capistrano/mb must be loaded by Capistrano in order "\
13 | "to work.\nRequire this gem by using Capistrano's Capfile, "\
14 | "as described here:\n"\
15 | "https://github.com/mattbrictson/capistrano-mb#installation"
16 | end
17 |
18 | def self.check_blacklisted_capistrano_version
19 | return unless defined?(Capistrano::VERSION)
20 | return unless Capistrano::VERSION == "3.2.0"
21 |
22 | warn "Capistrano 3.2.0 has a critical bug that prevents "\
23 | "capistrano-mb from working as intended:\n"\
24 | "https://github.com/capistrano/capistrano/issues/1004"
25 | end
26 |
27 | # We can't really rely on anything being loaded at this point, so define
28 | # our own basic colorizing helper.
29 | def self.warn(message)
30 | return $stderr.puts("WARNING: #{message}") unless $stderr.tty?
31 | $stderr.puts("\e[0;31;49mWARNING: #{message}\e[0m")
32 | end
33 | end
34 | end
35 | end
36 |
37 | Capistrano::MB::Compatibility.check
38 |
--------------------------------------------------------------------------------
/lib/capistrano/mb/recipe.rb:
--------------------------------------------------------------------------------
1 | module Capistrano
2 | module MB
3 | class Recipe
4 | attr_reader :name
5 |
6 | def initialize(name)
7 | @name = name.to_s
8 | end
9 |
10 | def enabled?
11 | fetch(:mb_recipes, []).map(&:to_s).include?(name)
12 | end
13 |
14 | def prior_to(task_to_extend, *recipe_tasks)
15 | inject_tasks(:before, task_to_extend, *recipe_tasks)
16 | end
17 |
18 | def during(task_to_extend, *recipe_tasks)
19 | inject_tasks(:after, task_to_extend, *recipe_tasks)
20 | end
21 |
22 | private
23 |
24 | def inject_tasks(method, task_to_extend, *recipe_tasks)
25 | create_task_unless_exists(task_to_extend)
26 |
27 | recipe_tasks.flatten.each do |task|
28 | qualified_task = apply_namespace(task)
29 | create_task_unless_exists("#{qualified_task}:if_enabled") do
30 | invoke qualified_task if enabled?
31 | end
32 | send(method, task_to_extend, "#{qualified_task}:if_enabled")
33 | end
34 | end
35 |
36 | def apply_namespace(task_name)
37 | return task_name if task_name.include?(":")
38 |
39 | "mb:#{name}:#{task_name}"
40 | end
41 |
42 | def create_task_unless_exists(task_name, &block)
43 | unless Rake::Task.task_defined?(task_name)
44 | Rake::Task.define_task(task_name, &block)
45 | end
46 | end
47 | end
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/lib/capistrano/tasks/dotenv.rake:
--------------------------------------------------------------------------------
1 | mb_recipe :dotenv do
2 | during "provision", "update"
3 | during "deploy:updating", "update"
4 | end
5 |
6 | namespace :mb do
7 | namespace :dotenv do
8 | desc "Replace/create .env file with values provided at console"
9 | task :replace do
10 | set_up_prompts
11 |
12 | on release_roles(:all) do
13 | update_dotenv_file
14 | end
15 | end
16 |
17 | desc "Update .env file with any missing values"
18 | task :update do
19 | set_up_prompts
20 |
21 | on release_roles(:all), :in => :sequence do
22 | existing_env = if test("[ -f #{shared_dotenv_path} ]")
23 | download!(shared_dotenv_path)
24 | end
25 | update_dotenv_file(existing_env.is_a?(String) ? existing_env : "")
26 | end
27 | end
28 |
29 | def shared_dotenv_path
30 | "#{shared_path}/#{fetch(:mb_dotenv_filename)}"
31 | end
32 |
33 | def set_up_prompts
34 | fetch(:mb_dotenv_keys).each do |key|
35 | if key.to_s =~ /key|token|secret|password|pepper/i
36 | ask(key, nil, :echo => false)
37 | else
38 | ask(key, nil)
39 | end
40 | end
41 | end
42 |
43 | def update_dotenv_file(existing="")
44 | updated = existing.dup
45 |
46 | fetch(:mb_dotenv_keys).each do |key|
47 | next if existing =~ /^#{Regexp.escape(key.upcase)}=/
48 | updated << "\n" unless updated.end_with?("\n")
49 | updated << "#{key.upcase}=#{fetch(key)}\n"
50 | end
51 |
52 | unless existing == updated
53 | put(updated, shared_dotenv_path, :mode => "600")
54 | end
55 | end
56 | end
57 | end
58 |
--------------------------------------------------------------------------------
/lib/capistrano/tasks/ssl.rake:
--------------------------------------------------------------------------------
1 | mb_recipe :ssl do
2 | during :provision, "generate_dh"
3 | during :provision, "generate_self_signed_crt"
4 | end
5 |
6 | namespace :mb do
7 | namespace :ssl do
8 | desc "Generate an SSL key and CSR for Ngnix HTTPS"
9 | task :generate_csr do
10 | _run_ssl_script
11 | _copy_to_all_web_servers(%w(.key .csr))
12 | end
13 |
14 | desc "Generate an SSL key, CSR, and self-signed cert for Ngnix HTTPS"
15 | task :generate_self_signed_crt do
16 | _run_ssl_script("--self")
17 | _copy_to_all_web_servers(%w(.key .csr .crt))
18 | end
19 |
20 | desc "Generate unique DH group"
21 | task :generate_dh do
22 | privileged_on roles(:web) do
23 | unless test("sudo [ -f /etc/ssl/dhparams.pem ]")
24 | execute :sudo, "openssl dhparam -out /etc/ssl/dhparams.pem 2048"
25 | execute :sudo, "chmod 600 /etc/ssl/dhparams.pem"
26 | end
27 | end
28 | end
29 |
30 | def _run_ssl_script(opt="")
31 | privileged_on primary(:web) do
32 | files_exist = %w(.key .csr .crt).any? do |ext|
33 | test("sudo [ -f /etc/ssl/#{application_basename}#{ext} ]")
34 | end
35 |
36 | if files_exist
37 | info("Files exist; skipping SSL key generation.")
38 | else
39 | config = "/tmp/csr_config"
40 | ssl_script = "/tmp/ssl_script"
41 |
42 | template("csr_config.erb", config, :sudo => true)
43 | template("ssl_setup", ssl_script, :mode => "+x", :sudo => true)
44 |
45 | within "/etc/ssl" do
46 | execute :sudo, ssl_script, opt, application_basename, config
47 | execute :sudo, "rm", ssl_script, config
48 | end
49 | end
50 | end
51 | end
52 |
53 | def _copy_to_all_web_servers(extensions)
54 | # TODO
55 | end
56 | end
57 | end
58 |
--------------------------------------------------------------------------------
/lib/capistrano/tasks/sidekiq.rake:
--------------------------------------------------------------------------------
1 | mb_recipe :sidekiq do
2 | during :provision, "systemd"
3 | during "deploy:start", "start"
4 | during "deploy:stop", "stop"
5 | during "deploy:restart", "restart"
6 | during "deploy:publishing", "restart"
7 | end
8 |
9 | namespace :mb do
10 | namespace :sidekiq do
11 | desc "Install sidekiq systemd config"
12 | task :systemd do
13 | privileged_on roles(fetch(:mb_sidekiq_role)) do |host, user|
14 | sidekiq_user = fetch(:mb_sidekiq_user) || user
15 |
16 | template "sidekiq.service.erb",
17 | "/etc/systemd/system/sidekiq_#{application_basename}.service",
18 | :mode => "a+rx",
19 | :binding => binding,
20 | :sudo => true
21 |
22 | execute :sudo, "systemctl daemon-reload"
23 | execute :sudo, "systemctl enable sidekiq_#{application_basename}.service"
24 |
25 | unless test(:sudo, "grep -qs sidekiq_#{application_basename}.service /etc/sudoers.d/#{user}")
26 | execute :sudo, "touch -f /etc/sudoers.d/#{user}"
27 | execute :sudo, "chmod u+w /etc/sudoers.d/#{user}"
28 | execute :sudo, "echo '#{user} ALL=NOPASSWD: /bin/systemctl start sidekiq_#{application_basename}.service' | sudo tee -a /etc/sudoers.d/#{user}"
29 | execute :sudo, "echo '#{user} ALL=NOPASSWD: /bin/systemctl stop sidekiq_#{application_basename}.service' | sudo tee -a /etc/sudoers.d/#{user}"
30 | execute :sudo, "echo '#{user} ALL=NOPASSWD: /bin/systemctl restart sidekiq_#{application_basename}.service' | sudo tee -a /etc/sudoers.d/#{user}"
31 | execute :sudo, "chmod 440 /etc/sudoers.d/#{user}"
32 | end
33 | end
34 | end
35 |
36 | %w[start stop restart].each do |command|
37 | desc "#{command} sidekiq"
38 | task command do
39 | on roles(fetch(:mb_sidekiq_role)) do
40 | execute :sudo, "systemctl #{command} sidekiq_#{application_basename}.service"
41 | end
42 | end
43 | end
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/lib/capistrano/tasks/unicorn.rake:
--------------------------------------------------------------------------------
1 | mb_recipe :unicorn do
2 | during :provision, %w(systemd config_rb)
3 | during "deploy:start", "start"
4 | during "deploy:stop", "stop"
5 | during "deploy:restart", "restart"
6 | during "deploy:publishing", "restart"
7 | end
8 |
9 | namespace :mb do
10 | namespace :unicorn do
11 | desc "Install unicorn systemd config"
12 | task :systemd do
13 | privileged_on roles(:app) do |host, user|
14 | unicorn_user = fetch(:mb_unicorn_user) || user
15 |
16 | template "unicorn.service.erb",
17 | "/etc/systemd/system/unicorn_#{application_basename}.service",
18 | :mode => "a+rx",
19 | :binding => binding,
20 | :sudo => true
21 |
22 | execute :sudo, "systemctl daemon-reload"
23 | execute :sudo, "systemctl enable unicorn_#{application_basename}.service"
24 |
25 | unless test(:sudo, "grep -qs unicorn_#{application_basename}.service /etc/sudoers.d/#{user}")
26 | execute :sudo, "touch -f /etc/sudoers.d/#{user}"
27 | execute :sudo, "chmod u+w /etc/sudoers.d/#{user}"
28 | execute :sudo, "echo '#{user} ALL=NOPASSWD: /bin/systemctl start unicorn_#{application_basename}.service' | sudo tee -a /etc/sudoers.d/#{user}"
29 | execute :sudo, "echo '#{user} ALL=NOPASSWD: /bin/systemctl stop unicorn_#{application_basename}.service' | sudo tee -a /etc/sudoers.d/#{user}"
30 | execute :sudo, "echo '#{user} ALL=NOPASSWD: /bin/systemctl restart unicorn_#{application_basename}.service' | sudo tee -a /etc/sudoers.d/#{user}"
31 | execute :sudo, "chmod 440 /etc/sudoers.d/#{user}"
32 | end
33 | end
34 | end
35 |
36 | desc "Create config/unicorn.rb"
37 | task :config_rb do
38 | on release_roles(:all) do
39 | template "unicorn.rb.erb", "#{shared_path}/config/unicorn.rb"
40 | end
41 | end
42 |
43 | %w[start stop restart].each do |command|
44 | desc "#{command} unicorn"
45 | task command do
46 | on roles(:app) do
47 | execute :sudo, "systemctl #{command} unicorn_#{application_basename}.service"
48 | end
49 | end
50 | end
51 | end
52 | end
53 |
--------------------------------------------------------------------------------
/lib/capistrano/mb/templates/nginx.erb:
--------------------------------------------------------------------------------
1 | # Based on https://github.com/defunkt/unicorn/blob/master/examples/nginx.conf
2 |
3 | user www-data;
4 | pid /run/nginx.pid;
5 | error_log /var/log/nginx/error.log;
6 |
7 | # you generally only need one nginx worker unless you're serving
8 | # large amounts of static files which require blocking disk reads
9 | worker_processes 1;
10 |
11 | events {
12 | worker_connections 1024; # increase if you have lots of clients
13 | accept_mutex off; # "on" if nginx worker_processes > 1
14 | use epoll; # for Linux 2.6+
15 | }
16 |
17 | http {
18 | # ensure nginx is able to load lots of third-party modules
19 | types_hash_max_size 2048;
20 | server_names_hash_bucket_size 64;
21 |
22 | # nginx will find this file in the config directory set at nginx build time
23 | include mime.types;
24 |
25 | # fallback in case we can't determine a type
26 | default_type application/octet-stream;
27 |
28 | # click tracking!
29 | access_log /var/log/nginx/access.log combined;
30 |
31 | # you generally want to serve static files with nginx since neither
32 | # Unicorn nor Rainbows! is optimized for it at the moment
33 | sendfile on;
34 |
35 | # configure reverse proxy cache
36 | proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=default:8m max_size=1000m inactive=30d;
37 | proxy_temp_path /var/cache/nginx/tmp;
38 |
39 | tcp_nopush on; # off may be better for *some* Comet/long-poll stuff
40 | tcp_nodelay off; # on may be better for some Comet/long-poll stuff
41 |
42 | # we haven't checked to see if Rack::Deflate on the app server is
43 | # faster or not than doing compression via nginx. It's easier
44 | # to configure it all in one place here for static files and also
45 | # to disable gzip for clients who don't get gzip/deflate right.
46 | # There are other gzip settings that may be needed used to deal with
47 | # bad clients out there, see http://wiki.nginx.org/NginxHttpGzipModule
48 | gzip on;
49 | gzip_http_version 1.0;
50 | gzip_proxied any;
51 | gzip_min_length 500;
52 | gzip_disable "MSIE [1-6]\.";
53 | gzip_types text/plain text/xml text/css
54 | text/comma-separated-values
55 | text/javascript application/x-javascript
56 | application/atom+xml;
57 |
58 |
59 | # Allow SSL session resumption
60 | ssl_session_cache shared:SSL:10m;
61 |
62 | include /etc/nginx/conf.d/*.conf;
63 | include /etc/nginx/sites-enabled/*;
64 | }
65 |
--------------------------------------------------------------------------------
/lib/capistrano/mb/templates/unicorn.rb.erb:
--------------------------------------------------------------------------------
1 | # Use at least one worker per core if you're on a dedicated server,
2 | # more will usually help for _short_ waits on databases/caches.
3 | worker_processes <%= fetch(:mb_unicorn_workers) %>
4 |
5 | # Help ensure your application will always spawn in the symlinked
6 | # "current" directory that Capistrano sets up.
7 | working_directory "<%= current_path %>"
8 |
9 | # listen on both a Unix domain socket
10 | # we use a shorter backlog for quicker failover when busy
11 | listen "/tmp/unicorn.<%= application_basename %>.sock", :backlog => 64
12 |
13 | # nuke workers after <%= fetch(:mb_unicorn_timeout) %> seconds (default is 60)
14 | timeout <%= fetch(:mb_unicorn_timeout) %>
15 |
16 | pid "<%= fetch(:mb_unicorn_pid) %>"
17 |
18 | # By default, the Unicorn logger will write to stderr.
19 | # Additionally, some applications/frameworks log to stderr or stdout,
20 | # so prevent them from going to /dev/null when daemonized here:
21 | stderr_path "<%= fetch(:mb_unicorn_log) %>"
22 | stdout_path "<%= fetch(:mb_unicorn_log) %>"
23 |
24 | preload_app true
25 |
26 | # combine Ruby 2.0.0dev or REE with "preload_app true" for memory savings
27 | # http://rubyenterpriseedition.com/faq.html#adapt_apps_for_cow
28 | if GC.respond_to?(:copy_on_write_friendly=)
29 | GC.copy_on_write_friendly = true
30 | end
31 |
32 | before_exec do |server|
33 | # Ensure unicorn picks up our newest Gemfile
34 | ENV['BUNDLE_GEMFILE'] = "<%= current_path %>/Gemfile"
35 | end
36 |
37 | before_fork do |server, worker|
38 |
39 | # the following is highly recomended for Rails + "preload_app true"
40 | # as there's no need for the master process to hold a connection
41 | if defined? ActiveRecord::Base
42 | ActiveRecord::Base.connection.disconnect!
43 | end
44 |
45 | # This allows a new master process to incrementally
46 | # phase out the old master process with SIGTTOU to avoid a
47 | # thundering herd (especially in the "preload_app false" case)
48 | # when doing a transparent upgrade. The last worker spawned
49 | # will then kill off the old master process with a SIGQUIT.
50 | old_pid = "#{server.config[:pid]}.oldbin"
51 | if File.exists?(old_pid) && server.pid != old_pid
52 | begin
53 | sig = (worker.nr + 1) >= server.worker_processes ? :QUIT : :TTOU
54 | Process.kill(sig, File.read(old_pid).to_i)
55 | rescue Errno::ENOENT, Errno::ESRCH
56 | end
57 | end
58 |
59 | # Throttle the master from forking too quickly by sleeping. Due
60 | # to the implementation of standard Unix signal handlers, this
61 | # helps (but does not completely) prevent identical, repeated signals
62 | # from being lost when the receiving process is busy.
63 | sleep 1
64 | end
65 |
66 | after_fork do |server, worker|
67 | # the following is *required* for Rails + "preload_app true"
68 | if defined?(ActiveRecord::Base)
69 | ActiveRecord::Base.establish_connection
70 | end
71 | end
72 |
--------------------------------------------------------------------------------
/lib/capistrano/tasks/aptitude.rake:
--------------------------------------------------------------------------------
1 | mb_recipe :aptitude do
2 | during :provision, %w[check upgrade install]
3 | end
4 |
5 | namespace :mb do
6 | namespace :aptitude do
7 | desc "Verify server is Ubuntu 16.04 or 18.04"
8 | task :check do
9 | privileged_on roles(:all) do
10 | version = capture(:sudo, "lsb_release -a")[/^Release:\s+(\S+)$/, 1]
11 | next if %w[16.04 18.04].include?(version)
12 |
13 | raise "Ubuntu version #{version || "unknown"} is not supported by "\
14 | "capistrano-mb. Only Ubuntu 16.04 and 18.04 are supported. "\
15 | "Downgrade capistrano-mb if you need to use an older version of "\
16 | "Ubuntu."
17 | end
18 | end
19 |
20 | desc "Run `apt update` and then run `apt upgrade`"
21 | task :upgrade do
22 | privileged_on roles(:all) do
23 | _update
24 | _upgrade
25 | end
26 | end
27 |
28 | desc "Run `apt install` for packages required by the roles of "\
29 | "each server."
30 | task :install do
31 | privileged_on roles(:all) do |host|
32 | packages_to_install = []
33 | repos_to_add = []
34 |
35 | _each_package(host) do |pkg, repo|
36 | unless _already_installed?(pkg)
37 | repos_to_add << repo unless repo.nil?
38 | packages_to_install << pkg
39 | end
40 | end
41 |
42 | repos_to_add.uniq.each { |repo| _add_repository(repo) }
43 | _update
44 | packages_to_install.uniq.each { |pkg| _install(pkg) }
45 | end
46 | end
47 |
48 | def _already_installed?(pkg)
49 | test(:sudo,
50 | "dpkg", "-s", pkg,
51 | "2>/dev/null", "|", :grep, "-q 'ok installed'")
52 | end
53 |
54 | def _add_repository(repo)
55 | unless _already_installed?("software-properties-common")
56 | _install("software-properties-common")
57 | end
58 | execute :sudo, "apt-add-repository", "-y '#{repo}'"
59 | end
60 |
61 | def _install(pkg)
62 | execute :sudo, "DEBIAN_FRONTEND=noninteractive apt-get -y install", pkg
63 | end
64 |
65 | def _update
66 | execute :sudo, "DEBIAN_FRONTEND=noninteractive apt-get -y update"
67 | end
68 |
69 | def _upgrade
70 | execute :sudo,
71 | "DEBIAN_FRONTEND=noninteractive apt-get -y "\
72 | '-o DPkg::options::="--force-confdef" '\
73 | '-o DPkg::options::="--force-confold" '\
74 | "upgrade"
75 | end
76 |
77 | def _each_package(host)
78 | return to_enum(:_each_package, host) unless block_given?
79 | hostname = host.hostname
80 |
81 | fetch(:mb_aptitude_packages).each do |package_spec, *role_list|
82 | next unless roles(*role_list.flatten).map(&:hostname).include?(hostname)
83 |
84 | pkg, repo = package_spec.split("@")
85 | yield(pkg, repo)
86 | end
87 | end
88 | end
89 | end
90 |
--------------------------------------------------------------------------------
/lib/capistrano/tasks/rbenv.rake:
--------------------------------------------------------------------------------
1 | mb_recipe :rbenv do
2 | during :provision, %w(install write_vars)
3 | end
4 |
5 | namespace :mb do
6 | namespace :rbenv do
7 | desc "Install rbenv and compile ruby"
8 | task :install do
9 | invoke "mb:rbenv:run_installer"
10 | invoke "mb:rbenv:add_plugins"
11 | invoke "mb:rbenv:modify_bashrc"
12 | invoke "mb:rbenv:compile_ruby"
13 | end
14 |
15 | desc "Install the latest version of Ruby"
16 | task :upgrade do
17 | invoke "mb:rbenv:add_plugins"
18 | invoke "mb:rbenv:update_rbenv"
19 | invoke "mb:rbenv:compile_ruby"
20 | end
21 |
22 | task :write_vars do
23 | on release_roles(:all) do
24 | execute :mkdir, "-p ~/.rbenv"
25 | execute :touch, "~/.rbenv/vars"
26 | execute :chmod, "0600 ~/.rbenv/vars"
27 |
28 | vars = ""
29 |
30 | fetch(:mb_rbenv_vars).each do |name, value|
31 | execute :sed, "--in-place '/^#{name}=/d' ~/.rbenv/vars"
32 | vars << "#{name}=#{value}\n"
33 | end
34 |
35 | tmp_file = "/tmp/rbenv_vars"
36 | put vars, tmp_file
37 | execute :cat, tmp_file, ">> ~/.rbenv/vars"
38 | execute :rm, tmp_file
39 | end
40 | end
41 |
42 | task :run_installer do
43 | installer_url = \
44 | "https://github.com/rbenv/rbenv-installer/raw/master/bin/rbenv-installer"
45 |
46 | on release_roles(:all) do
47 | with :path => "$HOME/.rbenv/bin:$HOME/.rbenv/shims:$PATH" do
48 | execute :curl, "-fsSL", installer_url, "| bash"
49 | end
50 | end
51 | end
52 |
53 | task :add_plugins do
54 | plugins = %w(
55 | sstephenson/rbenv-vars
56 | sstephenson/ruby-build
57 | rkh/rbenv-update
58 | )
59 | plugins.each do |plugin|
60 | git_repo = "https://github.com/#{plugin}.git"
61 | plugin_dir = "$HOME/.rbenv/plugins/#{plugin.split('/').last}"
62 |
63 | on release_roles(:all) do
64 | unless test("[ -d #{plugin_dir} ]")
65 | execute :git, "clone", git_repo, plugin_dir
66 | end
67 | end
68 | end
69 | end
70 |
71 | task :modify_bashrc do
72 | on release_roles(:all) do
73 | unless test("grep -qs 'rbenv init' ~/.bashrc")
74 | template("rbenv_bashrc", "/tmp/rbenvrc")
75 | execute :cat, "/tmp/rbenvrc ~/.bashrc > /tmp/bashrc"
76 | execute :mv, "/tmp/bashrc ~/.bashrc"
77 | end
78 | end
79 | end
80 |
81 | task :compile_ruby do
82 | ruby_version = fetch(:mb_rbenv_ruby_version)
83 | on release_roles(:all) do
84 | force = ENV["RBENV_FORCE_INSTALL"] || begin
85 | ! test("rbenv versions | grep -q '#{ruby_version}'")
86 | end
87 |
88 | if force
89 | execute "CFLAGS=-O3 rbenv install --force #{ruby_version}"
90 | execute "rbenv global #{ruby_version}"
91 | execute "gem install bundler --no-document"
92 | end
93 | end
94 | end
95 |
96 | task :update_rbenv do
97 | on release_roles(:all) do
98 | execute "rbenv update"
99 | end
100 | end
101 | end
102 | end
103 |
--------------------------------------------------------------------------------
/lib/capistrano/tasks/defaults.rake:
--------------------------------------------------------------------------------
1 | namespace :load do
2 | task :defaults do
3 |
4 | set :mb_recipes, %w(
5 | aptitude
6 | bundler
7 | crontab
8 | dotenv
9 | logrotate
10 | migrate
11 | nginx
12 | postgresql
13 | rbenv
14 | seed
15 | ssl
16 | ufw
17 | unicorn
18 | user
19 | version
20 | )
21 |
22 | set :mb_privileged_user, "root"
23 |
24 | set :mb_aptitude_packages,
25 | "build-essential" => :all,
26 | "curl" => :all,
27 | "debian-goodies" => :all,
28 | "git-core" => :all,
29 | "libpq-dev" => :all,
30 | "libreadline-gplv2-dev" => :all,
31 | "libssl-dev" => :all,
32 | "libxml2" => :all,
33 | "libxml2-dev" => :all,
34 | "libxslt1-dev" => :all,
35 | "nginx@ppa:nginx/stable" => :web,
36 | "nodejs" => :all,
37 | "ntp" => :all,
38 | "postgresql" => :db,
39 | "postgresql-client" => :all,
40 | "tklib" => :all,
41 | "ufw" => :all,
42 | "zlib1g-dev" => :all
43 |
44 | set :mb_bundler_lockfile, "Gemfile.lock"
45 | set :mb_bundler_gem_install_command,
46 | "gem install bundler --conservative --no-document"
47 |
48 | set :mb_dotenv_keys, %w(rails_secret_key_base postmark_api_key)
49 | set :mb_dotenv_filename, -> { ".env.#{fetch(:rails_env)}" }
50 |
51 | set :mb_log_file, "log/capistrano.log"
52 |
53 | set :mb_nginx_force_https, false
54 | set :mb_nginx_redirect_hosts, {}
55 |
56 | ask :mb_postgresql_password, nil, :echo => false
57 | set :mb_postgresql_pool_size, 5
58 | set :mb_postgresql_host, "localhost"
59 | set :mb_postgresql_database,
60 | -> { "#{application_basename}_#{fetch(:rails_env)}" }
61 | set :mb_postgresql_user, -> { application_basename }
62 | set :mb_postgresql_pgpass_path,
63 | proc{ "#{shared_path}/config/pgpass" }
64 | set :mb_postgresql_backup_path, -> {
65 | "#{shared_path}/backups/postgresql-dump.dmp"
66 | }
67 | set :mb_postgresql_backup_exclude_tables, []
68 | set :mb_postgresql_dump_options, -> {
69 | options = fetch(:mb_postgresql_backup_exclude_tables).map do |t|
70 | "-T #{t.shellescape}"
71 | end
72 | options.join(" ")
73 | }
74 |
75 | set :mb_rbenv_ruby_version, -> { IO.read(".ruby-version").strip }
76 | set :mb_rbenv_vars, -> {
77 | {
78 | "RAILS_ENV" => fetch(:rails_env),
79 | "PGPASSFILE" => fetch(:mb_postgresql_pgpass_path)
80 | }
81 | }
82 |
83 | set :mb_sidekiq_concurrency, 25
84 | set :mb_sidekiq_role, :sidekiq
85 |
86 | ask :mb_ssl_csr_country, "US"
87 | ask :mb_ssl_csr_state, "California"
88 | ask :mb_ssl_csr_city, "San Francisco"
89 | ask :mb_ssl_csr_org, "Example Company"
90 | ask :mb_ssl_csr_name, "www.example.com"
91 |
92 | # WARNING: misconfiguring firewall rules could lock you out of the server!
93 | set :mb_ufw_rules,
94 | "allow ssh" => :all,
95 | "allow http" => :web,
96 | "allow https" => :web
97 |
98 | set :mb_unicorn_workers, 2
99 | set :mb_unicorn_timeout, 30
100 | set :mb_unicorn_config, proc{ "#{current_path}/config/unicorn.rb" }
101 | set :mb_unicorn_log, proc{ "#{current_path}/log/unicorn.log" }
102 | set :mb_unicorn_pid, proc{ "#{current_path}/tmp/pids/unicorn.pid" }
103 |
104 | set :bundle_binstubs, false
105 | set :bundle_flags, "--deployment --retry=3 --quiet"
106 | set :bundle_path, -> { shared_path.join("bundle") }
107 | set :deploy_to, -> { "/home/deployer/apps/#{fetch(:application)}" }
108 | set :keep_releases, 10
109 | set :linked_dirs, -> {
110 | ["public/#{fetch(:assets_prefix, 'assets')}"] +
111 | %w(
112 | .bundle
113 | log
114 | tmp/pids
115 | tmp/cache
116 | tmp/sockets
117 | public/.well-known
118 | public/system
119 | )
120 | }
121 | set :linked_files, -> {
122 | [fetch(:mb_dotenv_filename)] +
123 | %w(
124 | config/database.yml
125 | config/unicorn.rb
126 | )
127 | }
128 | set :log_level, :debug
129 | set :migration_role, :app
130 | set :rails_env, -> { fetch(:stage) }
131 | set :ssh_options, :compression => true, :keepalive => true
132 |
133 | SSHKit.config.command_map[:rake] = "bundle exec rake"
134 | end
135 | end
136 |
--------------------------------------------------------------------------------
/lib/capistrano/tasks/postgresql.rake:
--------------------------------------------------------------------------------
1 | mb_recipe :postgresql do
2 | during :provision, %w(
3 | create_user
4 | create_database
5 | database_yml
6 | pgpass
7 | logrotate_backup
8 | )
9 | end
10 |
11 | namespace :mb do
12 | namespace :postgresql do
13 | desc "Create user if it doesn't already exist"
14 | task :create_user do
15 | privileged_on primary(:db) do
16 | user = fetch(:mb_postgresql_user)
17 |
18 | unless test("sudo -u postgres psql -c '\\du' | grep -q #{user}")
19 | passwd = fetch(:mb_postgresql_password)
20 | md5 = Digest::MD5.hexdigest(passwd + user)
21 | execute "sudo -u postgres psql -c " +
22 | %Q["CREATE USER #{user} PASSWORD 'md5#{md5}';"]
23 | end
24 | end
25 | end
26 |
27 | desc "Create database if it doesn't already exist"
28 | task :create_database do
29 | privileged_on primary(:db) do
30 | user = fetch(:mb_postgresql_user)
31 | db = fetch(:mb_postgresql_database)
32 |
33 | unless test("sudo -u postgres psql -l | grep -w -q #{db}")
34 | execute "sudo -u postgres createdb -O #{user} #{db}"
35 | end
36 | end
37 | end
38 |
39 | desc "Generate database.yml"
40 | task :database_yml do
41 | yaml = {
42 | fetch(:rails_env).to_s => {
43 | "adapter" => "postgresql",
44 | "encoding" => "unicode",
45 | "database" => fetch(:mb_postgresql_database).to_s,
46 | "pool" => fetch(:mb_postgresql_pool_size).to_i,
47 | "username" => fetch(:mb_postgresql_user).to_s,
48 | "password" => fetch(:mb_postgresql_password).to_s,
49 | "host" => fetch(:mb_postgresql_host).to_s
50 | }
51 | }
52 | fetch(:mb_postgresql_password)
53 | on release_roles(:all) do
54 | put YAML.dump(yaml),
55 | "#{shared_path}/config/database.yml",
56 | :mode => "600"
57 | end
58 | end
59 |
60 | desc "Generate pgpass file (needed by backup scripts)"
61 | task :pgpass do
62 | fetch(:mb_postgresql_password)
63 | on release_roles(:all) do
64 | template "pgpass.erb",
65 | fetch(:mb_postgresql_pgpass_path),
66 | :mode => "600"
67 | end
68 | end
69 |
70 | desc "Configure logrotate to back up the database daily"
71 | task :logrotate_backup do
72 | on roles(:backup) do
73 | backup_path = fetch(:mb_postgresql_backup_path)
74 | execute :mkdir, "-p", File.dirname(backup_path)
75 | execute :touch, backup_path
76 | end
77 |
78 | privileged_on roles(:backup) do |host, user|
79 | template\
80 | "postgresql-backup-logrotate.erb",
81 | "/etc/logrotate.d/postgresql-backup-#{application_basename}",
82 | :owner => "root:root",
83 | :mode => "644",
84 | :binding => binding,
85 | :sudo => true
86 | end
87 | end
88 |
89 | desc "Dump the database to FILE"
90 | task :dump do
91 | on primary(:db) do
92 | with_pgpassfile do
93 | execute :pg_dump,
94 | "-Fc -Z9 -O",
95 | "-x", fetch(:mb_postgresql_dump_options),
96 | "-f", remote_dump_file,
97 | connection_flags,
98 | fetch(:mb_postgresql_database)
99 | end
100 |
101 | download!(remote_dump_file, local_dump_file)
102 |
103 | info(
104 | "Exported #{fetch(:mb_postgresql_database)} "\
105 | "to #{local_dump_file}."
106 | )
107 | end
108 | end
109 |
110 | desc "Restore database from FILE"
111 | task :restore do
112 | on primary(:db) do
113 | exit 1 unless agree(
114 | "\nErase existing #{fetch(:rails_env)} database "\
115 | "and restore from local file: #{local_dump_file}? "
116 | )
117 |
118 | upload!(local_dump_file, remote_dump_file)
119 |
120 | with_pgpassfile do
121 | execute :pg_restore,
122 | "-O -c",
123 | connection_flags,
124 | "-d", fetch(:mb_postgresql_database),
125 | remote_dump_file
126 | end
127 | end
128 | end
129 |
130 | def local_dump_file
131 | ENV.fetch("FILE", "#{fetch(:mb_postgresql_database)}.dmp")
132 | end
133 |
134 | def remote_dump_file
135 | "/tmp/#{fetch(:mb_postgresql_database)}.dmp"
136 | end
137 |
138 | def connection_flags
139 | [
140 | "-U", fetch(:mb_postgresql_user),
141 | "-h", fetch(:mb_postgresql_host)
142 | ].join(" ")
143 | end
144 |
145 | def with_pgpassfile(&block)
146 | with(:pgpassfile => fetch(:mb_postgresql_pgpass_path), &block)
147 | end
148 | end
149 | end
150 |
--------------------------------------------------------------------------------
/lib/capistrano/mb/templates/nginx_unicorn.erb:
--------------------------------------------------------------------------------
1 | # Based on https://github.com/defunkt/unicorn/blob/master/examples/nginx.conf
2 |
3 | upstream unicorn_<%= application_basename %> {
4 | # fail_timeout=0 means we always retry an upstream even if it failed
5 | # to return a good HTTP response (in case the Unicorn master nukes a
6 | # single worker for timing out).
7 | server unix:/tmp/unicorn.<%= application_basename %>.sock fail_timeout=0;
8 | }
9 |
10 | <% [80, 443].each do |port| %>
11 |
12 | <% fetch(:mb_nginx_redirect_hosts).each do |orig, desired| %>
13 | server {
14 | listen <%= port %>;
15 | server_name <%= orig %>;
16 | return 301 <%= fetch(:mb_nginx_force_https) ? "https" : "$scheme" %>://<%= desired %>$request_uri;
17 | }
18 | <% end %>
19 |
20 | server {
21 | listen <%= port %> <%= "spdy" if port == 443 %> default deferred; # for Linux
22 |
23 | <% if port == 80 && fetch(:mb_nginx_force_https) %>
24 | rewrite ^(.*) https://$http_host$1 permanent;
25 | <% else %>
26 |
27 | client_max_body_size 4G;
28 | server_name _;
29 |
30 | # ~2 seconds is often enough for most folks to parse HTML/CSS and
31 | # retrieve needed images/icons/frames, connections are cheap in
32 | # nginx so increasing this is generally safe...
33 | keepalive_timeout 5;
34 |
35 | # path for static files
36 | root <%= current_path %>/public;
37 |
38 | # Capistrano `deploy:web:disable` support
39 | if (-f $document_root/system/maintenance.html) {
40 | return 503;
41 | }
42 | error_page 503 @maintenance;
43 | location @maintenance {
44 | rewrite ^(.*)$ /system/maintenance.html last;
45 | break;
46 | }
47 |
48 | <% if port == 443 %>
49 | ssl on;
50 | ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';
51 | ssl_prefer_server_ciphers on;
52 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
53 | ssl_dhparam /etc/ssl/dhparams.pem;
54 | ssl_certificate /etc/ssl/<%= application_basename %>.crt;
55 | ssl_certificate_key /etc/ssl/<%= application_basename %>.key;
56 |
57 | <% if fetch(:mb_nginx_force_https) %>
58 | add_header Strict-Transport-Security "max-age=631138519";
59 | <% end %>
60 | <% end %>
61 |
62 | # Far-future expires for fingerprinted assets
63 | location ~ "/<%= fetch(:assets_prefix, "assets") %>/.*-[0-9a-f]{32}.*" {
64 | gzip_static on;
65 | expires max;
66 | add_header Cache-Control public;
67 | add_header Cache-Control immutable;
68 | }
69 |
70 | # Gzip for all assets
71 | location ~ ^/(<%= fetch(:assets_prefix, "assets") %>)/ {
72 | gzip_static on;
73 | break;
74 | }
75 |
76 | # Internal-only URI for sending files with X-Accel-Redirect from within
77 | # a release of the Rails app. See also corresponding proxy_set_header in
78 | # @unicorn config below.
79 | location /__send_file_accel {
80 | internal;
81 | alias <%= fetch(:deploy_to) %>;
82 | }
83 |
84 | include /etc/nginx/<%= application_basename%>-locations/*;
85 |
86 | # Prefer to serve static files directly from nginx to avoid unnecessary
87 | # data copies from the application server.
88 | try_files $uri/index.html $uri @unicorn;
89 |
90 | location @unicorn {
91 | # an HTTP header important enough to have its own Wikipedia entry:
92 | # http://en.wikipedia.org/wiki/X-Forwarded-For
93 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
94 |
95 | # this helps Rack set the proper URL scheme for doing HTTPS redirects:
96 | proxy_set_header X-Forwarded-Proto $scheme;
97 |
98 | # pass the Host: header from the client right along so redirects
99 | # can be set properly within the Rack application
100 | proxy_set_header Host $http_host;
101 |
102 | # we don't want nginx trying to do something clever with
103 | # redirects, we set the Host: header above already.
104 | proxy_redirect off;
105 |
106 | # send_file support
107 | proxy_set_header X-Sendfile-Type X-Accel-Redirect;
108 | proxy_set_header X-Accel-Mapping <%= fetch(:deploy_to) %>/=/__send_file_accel/;
109 |
110 | # enable caching (honors cache-control headers sent by Rails)
111 | # lock and use_stale help prevent a cache stampede
112 | proxy_cache default;
113 | proxy_cache_lock on;
114 | proxy_cache_use_stale updating;
115 | add_header X-Cache-Status $upstream_cache_status;
116 |
117 | proxy_pass http://unicorn_<%= application_basename %>;
118 | }
119 |
120 | # Rails error pages
121 | error_page 500 502 503 504 /500.html;
122 | location = /500.html {
123 | root <%= current_path %>/public;
124 | }
125 | <% end %>
126 | }
127 | <% end %>
128 |
--------------------------------------------------------------------------------
/lib/capistrano/mb/dsl.rb:
--------------------------------------------------------------------------------
1 | module Capistrano
2 | module MB
3 | module DSL
4 |
5 | # Invoke the given task. If a task with that name is not defined,
6 | # silently skip it.
7 | #
8 | def invoke_if_defined(task)
9 | invoke(task) if Rake::Task.task_defined?(task)
10 | end
11 |
12 | # Used internally by capistrano-mb to register tasks such that
13 | # those tasks are executed conditionally based on the presence of the
14 | # recipe name in fetch(:mb_recipes).
15 | #
16 | # mb_recipe :aptitude do
17 | # during :provision, %w(task1 task2 ...)
18 | # end
19 | #
20 | def mb_recipe(recipe_name, &block)
21 | Recipe.new(recipe_name).instance_exec(&block)
22 | end
23 |
24 | def compatibility_warning(warning)
25 | Capistrano::MB::Compatibility.warn(warning)
26 | end
27 |
28 | # Helper for calling fetch(:application) and making the value safe for
29 | # using in filenames, usernames, etc. Replaces non-word characters with
30 | # underscores.
31 | #
32 | def application_basename
33 | fetch(:application).to_s.gsub(/[^a-zA-Z0-9_]/, "_")
34 | end
35 |
36 | # Prints a question and returns truthy if the user answers "y" or "yes".
37 | def agree(yes_or_no_question)
38 | $stdout.print(yes_or_no_question)
39 | $stdin.gets.to_s =~ /^y(es)?/i
40 | end
41 |
42 | # Like capistrano's built-in on(), but connects to the server as root.
43 | # To use a user other than root, set :mb_privileged_user or
44 | # specify :privileged_user as a server property.
45 | #
46 | # task :reboot do
47 | # privileged_on roles(:all) do
48 | # execute :shutdown, "-r", "now"
49 | # end
50 | # end
51 | #
52 | def privileged_on(*args, &block)
53 | on(*args) do |host|
54 | if host.nil?
55 | instance_exec(nil, nil, &block)
56 | else
57 | original_user = host.user
58 |
59 | begin
60 | host.user = host.properties.privileged_user ||
61 | fetch(:mb_privileged_user)
62 | instance_exec(host, original_user, &block)
63 | ensure
64 | host.user = original_user
65 | end
66 | end
67 | end
68 | end
69 |
70 | # Uploads the given string or file-like object to the current host
71 | # context. Intended to be used within an on() or privileged_on() block.
72 | # Accepts :owner and :mode options that affect the permissions of the
73 | # remote file.
74 | #
75 | def put(string_or_io, remote_path, opts={})
76 | sudo_exec = ->(*cmd) {
77 | cmd = [:sudo] + cmd if opts[:sudo]
78 | execute *cmd
79 | }
80 |
81 | tmp_path = "/tmp/#{SecureRandom.uuid}"
82 |
83 | owner = opts[:owner]
84 | mode = opts[:mode]
85 |
86 | source = if string_or_io.respond_to?(:read)
87 | string_or_io
88 | else
89 | StringIO.new(string_or_io.to_s)
90 | end
91 |
92 | sudo_exec.call :mkdir, "-p", File.dirname(remote_path)
93 |
94 | upload!(source, tmp_path)
95 |
96 | sudo_exec.call(:mv, "-f", tmp_path, remote_path)
97 | sudo_exec.call(:chown, owner, remote_path) if owner
98 | sudo_exec.call(:chmod, mode, remote_path) if mode
99 | end
100 |
101 |
102 | # Read the specified file from the local system, interpret it as ERb,
103 | # and upload it to the current host context. Intended to be used with an
104 | # on() or privileged_on() block. Accepts :owner, :mode, and :binding
105 | # options.
106 | #
107 | # Templates with relative paths are first searched for in
108 | # lib/capistrano/mb/templates in the current project. This gives
109 | # applications a chance to override. If an override is not found, the
110 | # default template within the capistrano-mb gem is used.
111 | #
112 | # task :create_database_yml do
113 | # on roles(:app, :db) do
114 | # within(shared_path) do
115 | # template fetch(:database_yml_template_path),
116 | # "config/database.yml",
117 | # :mode => "600"
118 | # end
119 | # end
120 | # end
121 | #
122 | def template(local_path, remote_path, opts={})
123 | binding = opts[:binding] || binding
124 |
125 | unless local_path.start_with?("/")
126 | override_path = File.join("lib/capistrano/mb/templates", local_path)
127 |
128 | unless File.exist?(override_path)
129 | override_path = File.join(
130 | "lib/capistrano/fiftyfive/templates",
131 | local_path
132 | )
133 | if File.exist?(override_path)
134 | compatibility_warning(
135 | "Please move #{override_path} from lib/capistrano/fiftyfive "\
136 | "to lib/capistrano/mb to ensure future compatibility with "\
137 | "capistrano-mb."
138 | )
139 | end
140 | end
141 |
142 | local_path = if File.exist?(override_path)
143 | override_path
144 | else
145 | File.expand_path(File.join("../templates", local_path), __FILE__)
146 | end
147 | end
148 |
149 | erb = File.read(local_path)
150 | rendered_template = ERB.new(erb).result(binding)
151 |
152 | put(rendered_template, remote_path, opts)
153 | end
154 | end
155 | end
156 | end
157 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ⚠️ **This project is no longer maintained.** Thanks for your interest in capistrano-mb. I don't use this gem anymore and so I've chosen not to support it going forward. However, many of the lessons I've learned from building capistrano-mb I am now applying to a new project called [tomo](https://github.com/mattbrictson/tomo). Hope to see you there!
2 |
3 | ---
4 |
5 | # capistrano-mb
6 |
7 | **An opinionated Capistrano task library for deploying Rails apps from scratch on Ubuntu 16.04 or 18.04 LTS.**
8 |
9 | [](https://rubygems.org/gems/capistrano-mb)
10 |
11 | Capistrano is great for deploying Rails applications, but what about all the prerequisites, like Nginx and PostgreSQL? Do you have a firewall configured on your VPS? Have you installed the latest OS security updates? Is HTTPS working right?
12 |
13 | The capistrano-mb gem adds a `cap provision` task to Capistrano that takes care of all that. Out of the box, `provision` will:
14 |
15 | * Install the latest `postgresql`, `node.js`, and `nginx` apt packages
16 | * Install all libraries needed to build Ruby
17 | * Lock down your VPS using `ufw` (a simple front-end to iptables)
18 | * Set up `logrotated` for your Rails logs
19 | * Schedule an automatic daily backup of your Rails database
20 | * Generate a self-signed SSL certificate if you need one
21 | * Set up ngnix with the latest SSL practices and integrate it with Unicorn for your Rails app
22 | * Create the `deployer` user and install an SSH public key
23 | * Install `rbenv` and use `ruby-build` to compile the version of Ruby required by your app (by inspecting your `.ruby-version` file)
24 | * And more!
25 |
26 | The gem is named "capistrano-mb" because it is prescribes my ([@mattbrictson](https://github.com/mattbrictson)) personal preferences for automating deployments of Rails projects. I've worked several years as a freelance developer juggling lots of Rails codebases, so its important for me to have a good, consistent server configuration. You'll notice that capistrano-mb is opinionated and strictly uses the following stack:
27 |
28 | * Ubuntu 16.04 or 18.04 LTS
29 | * PostgreSQL
30 | * Unicorn
31 | * Nginx
32 | * rbenv
33 | * dotenv
34 |
35 | In addition, capistrano-mb changes many of Capistrano's defaults, including the deployment location, Bundler behavior, and SSH keep-alive settings. (See [defaults.rake][] for details.)
36 |
37 | Not quite to your liking? Consider forking the project to meet your needs.
38 |
39 | ## Roadmap
40 |
41 | I plan to continue maintaining this project for the benefit of deploying my own Rails apps for the foreseeable future. In practice, this means a new version or two per year. The behavior of capistrano-mb may change as I upgrade my apps to new versions of Rails. For example, at some point I might:
42 |
43 | * Replace Unicorn with Puma
44 | * Switch from dotenv to encrypted credentials
45 | * Add Let's Encrypt
46 | * Use a more robust database backup solution
47 |
48 | *Future changes in capistrano-mb are not guaranteed to have graceful migration paths, so I recommend pinning your Gemfile dependency to a specific version and upgrading with extreme care.*
49 |
50 | ## Quick start
51 |
52 | Please note that this project requires **Capistrano 3.x**, which is a complete rewrite of Capistrano 2.x. The two major versions are not compatible.
53 |
54 | ### 1. Purchase an Ubuntu 16.04 or 18.04 VPS
55 |
56 | To use capistrano-mb, you'll need a clean **Ubuntu 16.04 or 18.04** server to deploy to. The only special requirement is that your public SSH key must be installed on the server for the `root` user.
57 |
58 | Test that you can SSH to the server as `root` without being prompted for a password. If that works, capistrano-mb can take care of the rest. You're ready to proceed!
59 |
60 | ### 2. .ruby-version
61 |
62 | capistrano-mb needs to know the version of Ruby that your app requires, so that it can install Ruby during the provisioning process. Place a `.ruby-version` file in the root of your project containing the desired version, like this:
63 |
64 | ```
65 | 2.5.0
66 | ```
67 |
68 | *If you are using `rbenv`, just run `rbenv local 2.5.0` and it will create this file for you.*
69 |
70 | ### 3. Gemfile
71 |
72 | capistrano-mb makes certain assumptions about your Rails app, namely that it uses [dotenv][] to manage Rails secrets via environment variables, and that it runs on top of PostgreSQL and [unicorn][]. Make sure they are specified in the Gemfile:
73 |
74 | ```ruby
75 | gem "dotenv-rails", ">= 2.0.0"
76 | gem "pg", "~> 0.18"
77 | gem "unicorn"
78 | ```
79 |
80 | Then for the capistrano-mb tools themselves, add these gems to the development group:
81 |
82 | ```ruby
83 | group :development do
84 | gem "capistrano-bundler", :require => false
85 | gem "capistrano-rails", :require => false
86 | gem "capistrano", "~> 3.10", :require => false
87 | gem "capistrano-mb", "~> 0.35.0", :require => false
88 | end
89 | ```
90 |
91 | And then execute:
92 |
93 | ```
94 | $ bundle install
95 | ```
96 |
97 | ### 4. cap install
98 |
99 | If your project doesn't yet have a `Capfile`, run `cap install` with the list of desired stages (environments). For simplicity, this installation guide will assume a single production stage:
100 |
101 | ```
102 | bundle exec cap install STAGES=production
103 | ```
104 |
105 | ### 5. Capfile
106 |
107 | Add these lines to the **bottom** of your app's `Capfile` (order is important!):
108 |
109 | ```ruby
110 | require "capistrano/bundler"
111 | require "capistrano/rails"
112 | require "capistrano/mb"
113 | ```
114 |
115 | ### 6. deploy.rb
116 |
117 | Modify `config/deploy.rb` to set the specifics of your Rails app. At the minimum, you'll need to set these two options:
118 |
119 | ```ruby
120 | set :application, "my_app_name"
121 | set :repo_url, "git@github.com:username/repository.git"
122 | ```
123 |
124 | ### 7. production.rb
125 |
126 | Modify `config/deploy/production.rb` to specify the IP address of your production server. In this example, I have a single 1GB VPS (e.g. at DigitalOcean) that plays all the roles:
127 |
128 | ```ruby
129 | server "my.production.ip",
130 | :user => "deployer",
131 | :roles => %w[app backup cron db web]
132 | ```
133 |
134 | *Note that you must include the `backup` and `cron` roles if you want to make use of capistrano-mb's database backups and crontab features.*
135 |
136 | ### 8. secrets.yml
137 |
138 | Your Rails apps may have a `config/secrets.yml` file that specifies the Rails secret key. capistrano-mb configures dotenv to provide this secret in a `RAILS_SECRET_KEY_BASE` environment variable. You'll therefore need to modify `secrets.yml` as follows:
139 |
140 | ```ruby
141 | production:
142 | secret_key_base: <%= ENV["RAILS_SECRET_KEY_BASE"] %>
143 | ```
144 |
145 | ### 9. Provision and deploy!
146 |
147 | Run capistrano-mb's `provision` task. This will ask you a few questions, install Ruby, PostgreSQL, Nginx, etc., and set everything up. The entire process takes about 10 minutes (mostly due to compiling Ruby from source).
148 |
149 | ```
150 | bundle exec cap production provision
151 | ```
152 |
153 | Once that's done, your app is now ready to deploy!
154 |
155 | ```
156 | bundle exec cap production deploy
157 | ```
158 |
159 | ## Advanced usage
160 |
161 | ### Choosing which recipes to auto-run
162 |
163 | Most of the capistrano-mb recipes are designed to run automatically as part of `cap provision`, for installing and setting up various bits of the Rails infrastructure, like nginx, unicorn, and postgres. Some recipes also contribute to the `cap deploy` process.
164 |
165 | *This auto-run behavior is fully under your control.* In your `deploy.rb`, set `:mb_recipes` to an array of the desired recipes. If you don't want a recipe to execute as part of `deploy`/`provision`, simply omit it from the list.
166 |
167 | The following list will suffice for most out-of-the-box Rails apps. The order of the list is not important.
168 |
169 | ```ruby
170 | set :mb_recipes, %w[
171 | aptitude
172 | bundler
173 | crontab
174 | dotenv
175 | logrotate
176 | migrate
177 | nginx
178 | postgresql
179 | rbenv
180 | seed
181 | ssl
182 | ufw
183 | unicorn
184 | user
185 | version
186 | ]
187 | ```
188 |
189 | Even if you don't include a recipe in the auto-run list, you can still invoke the tasks of those recipes manually at your discretion. Run `bundle exec cap -T` to see the full list of tasks.
190 |
191 | ### Configuration
192 |
193 | Many of the recipes have default settings that can be overridden. Use your
194 | `deploy.rb` file to specify these overrides. Or, you can override per stage.
195 | Here is an example override:
196 |
197 | set :mb_unicorn_workers, 8
198 |
199 | For the full list of settings and their default values, refer to [defaults.rake][].
200 |
201 |
202 | ## Further reading
203 |
204 | Check out my [rails-template][] project, which generates Rails applications with capistrano-mb pre-configured and ready to go.
205 |
206 |
207 | ## History
208 |
209 | This gem used to be called `capistrano-fiftyfive`. If you are upgrading from capistrano-fiftyfive, refer to the [CHANGELOG entry for v0.22.0](CHANGELOG.md#0220-2015-06-22) for migration instructions.
210 |
211 | As of 0.33.0, capistrano-mb no longer supports Ubuntu 12.04 or 14.04. If your server runs one of these older versions, use [capistrano-mb 0.32.0](https://github.com/mattbrictson/capistrano-mb/tree/v0.32.0).
212 |
213 | ## Contributing
214 |
215 | 1. Fork it
216 | 2. Create your feature branch (`git checkout -b my-new-feature`)
217 | 3. Commit your changes (`git commit -am 'Add some feature'`)
218 | 4. Push to the branch (`git push origin my-new-feature`)
219 | 5. Create new Pull Request
220 |
221 |
222 | [Postmark]:https://postmarkapp.com
223 | [cast337]:http://railscasts.com/episodes/337-capistrano-recipes
224 | [cast373]:http://railscasts.com/episodes/373-zero-downtime-deployment
225 | [defaults.rake]:lib/capistrano/tasks/defaults.rake
226 | [rails-template]:https://github.com/mattbrictson/rails-template/
227 | [dotenv]:https://github.com/bkeepers/dotenv
228 | [unicorn]:http://unicorn.bogomips.org/
229 |
--------------------------------------------------------------------------------