├── .github ├── release-drafter.yml └── workflows │ └── push.yml ├── .gitignore ├── CHANGELOG.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── capistrano-mb.gemspec └── lib └── capistrano ├── mb.rb ├── mb ├── compatibility.rb ├── dsl.rb ├── recipe.rb ├── templates │ ├── crontab.erb │ ├── csr_config.erb │ ├── logrotate.erb │ ├── maintenance.html.erb │ ├── nginx.erb │ ├── nginx_unicorn.erb │ ├── pgpass.erb │ ├── postgresql-backup-logrotate.erb │ ├── rbenv_bashrc │ ├── sidekiq.service.erb │ ├── ssl_setup │ ├── unicorn.rb.erb │ ├── unicorn.service.erb │ └── version.rb.erb └── version.rb └── tasks ├── aptitude.rake ├── bundler.rake ├── crontab.rake ├── defaults.rake ├── dotenv.rake ├── logrotate.rake ├── maintenance.rake ├── migrate.rake ├── nginx.rake ├── postgresql.rake ├── provision.rake ├── rake.rake ├── rbenv.rake ├── seed.rake ├── sidekiq.rake ├── ssl.rake ├── ufw.rake ├── unicorn.rake ├── user.rake └── version.rake /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Release notes for this project are kept here: https://github.com/mattbrictson/capistrano-mb/releases 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in capistrano-mb.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | [![Gem Version](https://badge.fury.io/rb/capistrano-mb.svg)](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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /capistrano-mb.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path("../lib", __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require "capistrano/mb/version" 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "capistrano-mb" 8 | spec.version = Capistrano::MB::VERSION 9 | spec.author = "Matt Brictson" 10 | spec.email = "matt@mattbrictson.com" 11 | spec.description = \ 12 | "Production-ready provisioning and deployment recipes for Rails 4 and "\ 13 | "Rails 5 stacks. Installs and configures Ruby, Nginx, Unicorn, "\ 14 | "PostgreSQL, dotenv, and more onto Ubuntu using Capistrano." 15 | spec.summary = "Deploy Rails apps from scratch on Ubuntu" 16 | spec.homepage = "https://github.com/mattbrictson/capistrano-mb" 17 | spec.license = "MIT" 18 | 19 | spec.files = `git ls-files`.split($/) 20 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 21 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 22 | spec.require_paths = ["lib"] 23 | 24 | spec.add_dependency "capistrano", ">= 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.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/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 | -------------------------------------------------------------------------------- /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/mb/templates/crontab.erb: -------------------------------------------------------------------------------- 1 | # Sample crontab (empty) 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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/templates/pgpass.erb: -------------------------------------------------------------------------------- 1 | <%= fetch(:mb_postgresql_host) %>:5432:<%= fetch(:mb_postgresql_database) %>:<%= fetch(:mb_postgresql_user) %>:<%= fetch(:mb_postgresql_password).gsub(/([\\:])/, '\\\\\1') %> 2 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/version.rb: -------------------------------------------------------------------------------- 1 | module Capistrano 2 | module MB 3 | VERSION = "0.35.1".freeze 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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/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/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 | -------------------------------------------------------------------------------- /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/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/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/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/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 | -------------------------------------------------------------------------------- /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/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, " binding 31 | end 32 | end 33 | end 34 | end 35 | --------------------------------------------------------------------------------