├── .gitignore ├── CHANGELOG.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── capistrano-cookbook.gemspec └── lib ├── capistrano ├── .DS_Store ├── cookbook.rb └── cookbook │ ├── .DS_Store │ ├── certbot.rb │ ├── check_revision.rb │ ├── compile_assets_locally.rb │ ├── create_database.rb │ ├── helpers │ ├── setup_config_values.rb │ ├── smart_template.rb │ └── substitute_strings.rb │ ├── logs.rb │ ├── monit.rb │ ├── nginx.rb │ ├── puma_systemd.rb │ ├── run_tests.rb │ ├── setup_config.rb │ ├── sidekiq_systemd.rb │ ├── tasks │ ├── certbot.cap │ ├── check_revision.cap │ ├── compile_assets_locally.cap │ ├── create_database.cap │ ├── logs.cap │ ├── monit.cap │ ├── nginx.cap │ ├── puma_systemd.cap │ ├── run_tests.cap │ ├── setup_config.cap │ └── sidekiq_systemd.cap │ ├── templates │ ├── database.example.yml.erb │ ├── log_rotation.erb │ ├── sidekiq.yml.erb │ └── sidekiq_init.sh.erb │ └── version.rb └── generators └── capistrano └── reliably_deploying_rails ├── bootstrap_generator.rb └── templates ├── Capfile.erb ├── deploy.rb.erb ├── nginx_conf.erb ├── production.rb.erb ├── puma.rb.erb ├── puma.service.erb ├── puma_monit.conf.erb ├── sidekiq.service.capistrano.erb ├── sidekiq_monit.erb └── staging.rb.erb /.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 | DS_Store 19 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 5.0.2 (April 2021) 4 | 5 | - Adds support for automatically adding SSL certificates via Certbot 6 | 7 | ## 5.0.1 (March 2021) 8 | 9 | - Adds full support for deploy (but not config creation) without sudo access 10 | - Refactor config files to use single array of hashes with flags 11 | - Refactor Sidekiq and Monit configurations to copy files directly rather than using symlinks to avoid potential root access leak 12 | - Fixes bug where object identifier was outputted in logs rather than filename 13 | - Fixes nginx not being reloaded after `setup_config` due to shared log directory not yet existing 14 | 15 | ## 5.0.0 (March 2021) 16 | 17 | - Full overhaul to support Rails 6 and Ubuntu 20.04 -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in capistrano-cookbook.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Ben Dixon 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 | # Capistrano::Cookbook 2 | 3 | A collection of Capistrano 3 Compatible tasks to make deploying Rails and Sinatra based applications easier. 4 | 5 | ## Installation 6 | 7 | Add this line to your application's Gemfile: 8 | 9 | gem 'capistrano-cookbook', require: false, group: :development 10 | 11 | And then execute: 12 | 13 | $ bundle 14 | 15 | Or install it yourself as: 16 | 17 | $ gem install capistrano-cookbook 18 | 19 | ## Versioning 20 | 21 | This gem is primarily intended to provide a batteries included approach to getting Rails applications up and running. It is tested with the server configuration in . 22 | 23 | Major versions of the rails server template above and this gem stay in sync. So Version X.*.* of this gem should provide a working out of the box capistrano configuration when used with version X.*.* of the server template. 24 | 25 | Major and minor versions of this gem, the server template and the book [Reliably Deploying Rails Applications](https://leanpub.com/deploying_rails_applications) also stay in sync. Updates to the book are free for life so on a new project, it's always advisable to make sure you have the latest version. 26 | 27 | ## Usage 28 | 29 | ### Boostrap 30 | 31 | To generate a complete Capistrano configuration include the gem in your Gemfile and then use the following Rails Generator: 32 | 33 | ``` 34 | bundle exec rails g capistrano:reliably_deploying_rails:bootstrap --sidekiq --production_hostname='YOUR_PRODUCTION_DOMAIN' --production_server_address='YOUR_PRODUCTION_SERVER' 35 | ``` 36 | 37 | Replacing `YOUR_PRODUCTION_DOMAIN` with the domain name people will access the Rails app on and `YOUR_PRODUCTION_SERVER` with the address or ip that can be used to access the server via SSH, these may be the same thing but often will not be (e.g. if the domain has a CDN or Load Balancer between it and the server you're deploying to), 38 | 39 | This will generate a complete Capistrano configuration. 40 | 41 | You can then execute: 42 | 43 | ``` 44 | bundle exec cap production deploy:setup_config 45 | bundle exec cap production database:create 46 | ``` 47 | 48 | To perform setup tasks and create a new empty database. Followed by: 49 | 50 | ``` 51 | bundle exec cap production deploy 52 | ``` 53 | 54 | To kick off you first deploy. 55 | 56 | ### Including Tasks Manually 57 | 58 | To include all tasks from the gem, add the following to your `Capfile`: 59 | 60 | ```ruby 61 | require 'capistrano/cookbook' 62 | ``` 63 | 64 | Otherwise you can include tasks individually: 65 | 66 | ```ruby 67 | require 'capistrano/cookbook/check_revision' 68 | require 'capistrano/cookbook/compile_assets_locally' 69 | require 'capistrano/cookbook/create_database' 70 | require 'capistrano/cookbook/logs' 71 | require 'capistrano/cookbook/monit' 72 | require 'capistrano/cookbook/nginx' 73 | require 'capistrano/cookbook/run_tests' 74 | require 'capistrano/cookbook/setup_config' 75 | ``` 76 | 77 | ### The Tasks 78 | 79 | #### Check Revision 80 | 81 | Checks that the remote branch the selected stage deploys from, matches the current local version, if it doesn't the deploy will be halted with an error. 82 | 83 | Add the following to `deploy.rb` 84 | 85 | ```ruby 86 | before :deploy, 'deploy:check_revision' 87 | ``` 88 | 89 | #### Compile Assets Locally 90 | 91 | Compiles local assets and then rsyncs them to the production server. Avoids the need for a javascript runtime on the target machine and saves a significant amount of time when deploying to multiple web frontends. 92 | 93 | Add the following to `deploy.rb` 94 | 95 | ``` ruby 96 | after 'deploy:symlink:shared', 'deploy:compile_assets_locally' 97 | ``` 98 | 99 | #### Create Database 100 | 101 | Currently only works with Postgresql on configurations where your web server and db server are the same machine, e.g. single box deployments. This task will: 102 | 103 | * Check to see if a remote `database.yml` exists in `APP_PATH/shared/config`, if not attempt to copy one from `APP_PATH/shared/config` 104 | * If a new `database.yml` is created, it will include a username and database name based on the application name and a random password 105 | * Download the remote `database.yml` 106 | * Create the Postgres user specified in `database.yml` if it doesn't already exist 107 | * Create the Database specified in `database.yml` if it doesn't already exist 108 | * Grant the user all permissions on that database 109 | 110 | Run using: 111 | 112 | ``` bash 113 | cap STAGE database:create 114 | ``` 115 | 116 | #### Logs 117 | 118 | Allows remote log files (anything in `APP_PATH/shared/log`) to be tailed locally with Capistrano rather than SSHing in. 119 | 120 | To tail the log file `APP_PATH/shared/log/production.log` on the `production` stage: 121 | 122 | ``` bash 123 | cap production 'logs:tail[production]' 124 | ``` 125 | 126 | To tail the log file `APP_PATH/shared/log/unicorn.log` 127 | 128 | ``` bash 129 | cap production 'logs:tail[unicorn]' 130 | ``` 131 | 132 | #### Monit 133 | 134 | Provides convenience tasks for restarting the Monit service. 135 | 136 | Available actions are `start`, `stop` and `restart`. 137 | 138 | Usage: 139 | 140 | ```bash 141 | cap STAGE monit:start 142 | cap STAGE monit:stop 143 | cap STAGE monit:restart 144 | ``` 145 | 146 | #### Nginx 147 | 148 | Provides convenience tasks for interacting with Nginx using its `init.d` script as well as an additional task to remove the `default` virtualhost from `/etc/nginx/sites-enabled` 149 | 150 | Available actions are `start`, `stop`, `restart`, `reload`, `remove_default_vhost`. 151 | 152 | `reload` will reload the nginx virtualhosts without restarting the server. 153 | 154 | Usage: 155 | 156 | ```bash 157 | cap STAGE nginx:start 158 | cap STAGE nginx:stop 159 | cap STAGE nginx:restart 160 | cap STAGE nginx:remove_default_vhost 161 | ``` 162 | 163 | #### Run Tests 164 | 165 | Allows a test suite to be automatically run with `rspec`, if the tests pass the deploy will continue, if they fail, the deploy will halt and the test output will be displayed. 166 | 167 | Usage: 168 | 169 | Define the tests to be run in `deploy.rb` 170 | 171 | ``` ruby 172 | set(:tests, ['spec']) 173 | ``` 174 | 175 | and add a hook in `deploy.rb` to run them automatically: 176 | 177 | ``` ruby 178 | before "deploy", "deploy:run_tests" 179 | ``` 180 | 181 | #### Setup Config 182 | 183 | The `deploy:setup_config` tasks provides a simple way to automate the generation of server specific configuration files and the setting up of any required symlinks outside of the applications normal directory structure. 184 | 185 | If no values are provided in `deploy.rb` to override the defaults then this task includes opinionated defaults to setup a server for deployment as explained in the book [Reliably Deploying Rails Applications](https://leanpub.com/deploying_rails_applications) and [this tutorial](http://www.talkingquickly.co.uk/2014/01/deploying-rails-apps-to-a-vps-with-capistrano-v3/). 186 | 187 | Each of the `config_files` will be created in `APP_PATH/shared/config`. 188 | 189 | The task looks in the following locations for a template file with a corresponding name with a `.erb` extension: 190 | 191 | * `config/deploy/STAGE/FILENAME.erb` 192 | * `config/deploy/shared/FILENAME.erb` 193 | * `templates/FILENAME.erb` directory of this gem ([github link](https://github.com/TalkingQuickly/capistrano-cookbook/tree/master/lib/capistrano/cookbook/templates)) 194 | 195 | For any config files included in the `source` part of an entry in the `symlinks` array, a symlink will be created to the corresponding `link` location on the target machine. 196 | 197 | Finally any config files included in `executable_config_files` will be marked as executable. 198 | 199 | This task will also automatically invoke the following tasks: 200 | 201 | * `nginx:remove_default_vhost` 202 | * `nginx:reload` 203 | * `monit:restart` 204 | 205 | To ensure configuration file changes are picked up correctly. 206 | 207 | The defaults are: 208 | 209 | Config Files: 210 | 211 | ``` ruby 212 | set( 213 | :config_files, 214 | %w( 215 | nginx.conf 216 | database.example.yml 217 | log_rotation 218 | monit 219 | unicorn.rb 220 | unicorn_init.sh 221 | )) 222 | ``` 223 | 224 | Symlinks: 225 | 226 | ```ruby 227 | set( 228 | :symlinks, 229 | [ 230 | { 231 | source: "nginx.conf", 232 | link: "/etc/nginx/sites-enabled/{{full_app_name}}" 233 | }, 234 | { 235 | source: "unicorn_init.sh", 236 | link: "/etc/init.d/unicorn_{{full_app_name}}" 237 | }, 238 | { 239 | source: "log_rotation", 240 | link: "/etc/logrotate.d/{{full_app_name}}" 241 | }, 242 | { 243 | source: "monit", 244 | link: "/etc/monit/conf.d/{{full_app_name}}.conf" 245 | } 246 | ] 247 | ) 248 | ``` 249 | 250 | Executable Config Files: 251 | 252 | ```ruby 253 | set( 254 | :executable_config_files, 255 | w( 256 | unicorn_init.sh 257 | ) 258 | ) 259 | ``` 260 | 261 | ## Contributing 262 | 263 | 1. Fork it ( http://github.com/talkingquickly/capistrano-cookbook/fork ) 264 | 2. Create your feature branch (`git checkout -b my-new-feature`) 265 | 3. Commit your changes (`git commit -am 'Add some feature'`) 266 | 4. Push to the branch (`git push origin my-new-feature`) 267 | 5. Create new Pull Request 268 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | -------------------------------------------------------------------------------- /capistrano-cookbook.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/cookbook/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "capistrano-cookbook" 8 | spec.version = Capistrano::Cookbook::VERSION 9 | spec.authors = ["Ben Dixon"] 10 | spec.email = ["ben@talkingquickly.co.uk"] 11 | spec.summary = %q{Selection of Capistrano 3 tasks to reduce boilerplate required when deploying Rails and Sinatra applications} 12 | spec.homepage = "https://github.com/TalkingQuickly/capistrano-cookbook" 13 | spec.license = "MIT" 14 | 15 | spec.files = `git ls-files -z`.split("\x0") 16 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 17 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 18 | spec.require_paths = ["lib"] 19 | 20 | spec.add_dependency 'capistrano', '~> 3.16' 21 | spec.add_dependency 'capistrano3-puma', '~> 5.0.4' 22 | spec.add_dependency 'capistrano-sidekiq', '~> 2.0' 23 | 24 | spec.add_development_dependency "bundler", "~> 1.5" 25 | spec.add_development_dependency "rake" 26 | end 27 | -------------------------------------------------------------------------------- /lib/capistrano/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TalkingQuickly/capistrano-cookbook/52da113660cad6b52b2ef542742b6f262043ba3c/lib/capistrano/.DS_Store -------------------------------------------------------------------------------- /lib/capistrano/cookbook.rb: -------------------------------------------------------------------------------- 1 | require "capistrano/cookbook/version" 2 | 3 | module Capistrano 4 | module Cookbook 5 | require 'capistrano/cookbook/certbot' 6 | require 'capistrano/cookbook/check_revision' 7 | require 'capistrano/cookbook/create_database' 8 | require 'capistrano/cookbook/logs' 9 | require 'capistrano/cookbook/monit' 10 | require 'capistrano/cookbook/nginx' 11 | require 'capistrano/cookbook/puma_systemd' 12 | require 'capistrano/cookbook/run_tests' 13 | require 'capistrano/cookbook/setup_config' 14 | require 'capistrano/cookbook/sidekiq_systemd' 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/capistrano/cookbook/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TalkingQuickly/capistrano-cookbook/52da113660cad6b52b2ef542742b6f262043ba3c/lib/capistrano/cookbook/.DS_Store -------------------------------------------------------------------------------- /lib/capistrano/cookbook/certbot.rb: -------------------------------------------------------------------------------- 1 | load File.expand_path("tasks/certbot.cap", File.dirname(__FILE__)) -------------------------------------------------------------------------------- /lib/capistrano/cookbook/check_revision.rb: -------------------------------------------------------------------------------- 1 | load File.expand_path("tasks/check_revision.cap",File.dirname(__FILE__)) -------------------------------------------------------------------------------- /lib/capistrano/cookbook/compile_assets_locally.rb: -------------------------------------------------------------------------------- 1 | load File.expand_path("tasks/compile_assets_locally.cap", File.dirname(__FILE__)) -------------------------------------------------------------------------------- /lib/capistrano/cookbook/create_database.rb: -------------------------------------------------------------------------------- 1 | load File.expand_path("tasks/create_database.cap", File.dirname(__FILE__)) -------------------------------------------------------------------------------- /lib/capistrano/cookbook/helpers/setup_config_values.rb: -------------------------------------------------------------------------------- 1 | module Capistrano 2 | module Cookbook 3 | class SetupConfigValues 4 | def config_files 5 | fetch(:config_files) || config_files_defaults 6 | end 7 | 8 | private 9 | 10 | def config_files_defaults 11 | base = [ 12 | { 13 | source: 'log_rotation', 14 | destination: "/etc/logrotate.d/#{fetch(:full_app_name)}", 15 | executable: false, 16 | as_root: true 17 | }, 18 | { 19 | source: 'database.example.yml', 20 | destination: "#{shared_path}/config/database.example.yml", 21 | executable: false, 22 | as_root: false 23 | } 24 | ] 25 | 26 | return base unless sidekiq_enabled? 27 | 28 | base + [ 29 | { 30 | source: 'sidekiq.service.capistrano', 31 | destination: "/home/#{fetch(:deploy_user)}/.config/systemd/user/#{fetch(:sidekiq_service_unit_name)}.service", 32 | executable: false, 33 | as_root: false 34 | }, 35 | { 36 | source: "sidekiq_monit", 37 | destination: "/etc/monit/conf.d/#{fetch(:full_app_name)}_sidekiq.conf", 38 | executable: false, 39 | as_root: true 40 | } 41 | ] 42 | end 43 | 44 | def sidekiq_enabled? 45 | defined?(Capistrano::Sidekiq) == 'constant' && Capistrano::Sidekiq.class == Class 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/capistrano/cookbook/helpers/smart_template.rb: -------------------------------------------------------------------------------- 1 | require 'securerandom' 2 | require 'stringio' 3 | 4 | # will first try and copy the file: 5 | # config/deploy/#{full_app_name}/#{from}.erb 6 | # to: 7 | # shared/config/to 8 | # if the original source path doesn exist then it will 9 | # search in: 10 | # config/deploy/shared/#{from}.erb 11 | # otherwise it will search in the gems template directory 12 | # this allows files which are common to all enviros to 13 | # come from a single source while allowing specific 14 | # ones to be over-ridden 15 | # if the target file name is the same as the source then 16 | # the second parameter can be left out 17 | def smart_template(from, to, as_root=false) 18 | if from_erb_path = template_file(from) 19 | from_erb = StringIO.new(ERB.new(File.read(from_erb_path)).result(binding)) 20 | upload!(from_erb, to) unless as_root 21 | sudo_upload!(from_erb, to) if as_root 22 | info "copying: #{from} to: #{to}" 23 | else 24 | error "error #{from} not found" 25 | end 26 | end 27 | 28 | def template_file(name) 29 | if File.exist?((file = "config/deploy/#{fetch(:stage)}/#{name}.erb")) 30 | return file 31 | elsif File.exist?((file = "config/deploy/shared/#{name}.erb")) 32 | return file 33 | elsif File.exist?((file = "config/deploy/templates/#{name}.erb")) 34 | return file 35 | elsif File.exist?(file = File.expand_path("../templates/#{name}.erb",File.dirname(__FILE__))) 36 | return file 37 | end 38 | return nil 39 | end 40 | 41 | def sudo_upload!(file_path, remote_path, mode: '644', owner: 'root:root') 42 | tmp_path = "/tmp/#{SecureRandom.uuid}" 43 | 44 | upload!(file_path, tmp_path) 45 | 46 | execute(:sudo, :mkdir, '-p', File.dirname(remote_path)) 47 | execute(:sudo, :mv, '-f', tmp_path, remote_path) 48 | execute(:sudo, :chmod, mode, remote_path) 49 | execute(:sudo, :chown, owner, remote_path) 50 | end -------------------------------------------------------------------------------- /lib/capistrano/cookbook/helpers/substitute_strings.rb: -------------------------------------------------------------------------------- 1 | # we often want to refer to variables which 2 | # are defined in subsequent stage files. This 3 | # let's us use the {{var}} to represent fetch(:var) 4 | # in strings which are only evaluated at runtime. 5 | 6 | def sub_strings(input_string) 7 | output_string = input_string 8 | input_string.scan(/{{(\w*)}}/).each do |var| 9 | output_string.gsub!("{{#{var[0]}}}", fetch(var[0].to_sym)) 10 | end 11 | output_string 12 | end 13 | -------------------------------------------------------------------------------- /lib/capistrano/cookbook/logs.rb: -------------------------------------------------------------------------------- 1 | load File.expand_path("tasks/logs.cap", File.dirname(__FILE__)) -------------------------------------------------------------------------------- /lib/capistrano/cookbook/monit.rb: -------------------------------------------------------------------------------- 1 | load File.expand_path("tasks/monit.cap", File.dirname(__FILE__)) -------------------------------------------------------------------------------- /lib/capistrano/cookbook/nginx.rb: -------------------------------------------------------------------------------- 1 | load File.expand_path("tasks/nginx.cap",File.dirname(__FILE__)) -------------------------------------------------------------------------------- /lib/capistrano/cookbook/puma_systemd.rb: -------------------------------------------------------------------------------- 1 | load File.expand_path("tasks/puma_systemd.cap", File.dirname(__FILE__)) -------------------------------------------------------------------------------- /lib/capistrano/cookbook/run_tests.rb: -------------------------------------------------------------------------------- 1 | load File.expand_path("tasks/run_tests.cap", File.dirname(__FILE__)) -------------------------------------------------------------------------------- /lib/capistrano/cookbook/setup_config.rb: -------------------------------------------------------------------------------- 1 | load File.expand_path("tasks/setup_config.cap", File.dirname(__FILE__)) -------------------------------------------------------------------------------- /lib/capistrano/cookbook/sidekiq_systemd.rb: -------------------------------------------------------------------------------- 1 | load File.expand_path("tasks/sidekiq_systemd.cap", File.dirname(__FILE__)) -------------------------------------------------------------------------------- /lib/capistrano/cookbook/tasks/certbot.cap: -------------------------------------------------------------------------------- 1 | namespace :certbot do 2 | desc "Setup certbot certificate for the domain defined in `nginx_server_name` in the stage file" 3 | task :install do 4 | on roles(:app) do 5 | return unless fetch(:certbot_enable_ssl) 6 | sudo "certbot --nginx -d #{fetch(:nginx_server_name)} --non-interactive --agree-tos --email #{fetch(:certbot_email)} #{fetch(:certbot_redirect_to_https) ? '--redirect' : ''} #{fetch(:certbot_use_acme_staging) ? '--dry-run' : ''}" 7 | end 8 | end 9 | end -------------------------------------------------------------------------------- /lib/capistrano/cookbook/tasks/check_revision.cap: -------------------------------------------------------------------------------- 1 | namespace :deploy do 2 | desc "checks whether the currently checkout out revision matches the 3 | remote one we're trying to deploy from" 4 | task :check_revision do 5 | branch = fetch(:branch) 6 | unless `git rev-parse HEAD` == `git rev-parse origin/#{branch}` 7 | puts "WARNING: HEAD is not the same as origin/#{branch}" 8 | puts "Run `git push` to sync changes or make sure you've" 9 | puts "checked out the branch: #{branch} as you can only deploy" 10 | puts "if you've got the target branch checked out" 11 | exit 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/capistrano/cookbook/tasks/compile_assets_locally.cap: -------------------------------------------------------------------------------- 1 | namespace :deploy do 2 | desc "compiles assets locally then rsyncs" 3 | task :compile_assets_locally do 4 | run_locally do 5 | execute "RAILS_ENV=#{fetch(:rails_env)} bundle exec rake assets:precompile" 6 | end 7 | on roles(:app) do |role| 8 | run_locally do 9 | execute"rsync -av ./public/assets/ #{role.user}@#{role.hostname}:#{release_path}/public/assets/;" 10 | end 11 | sudo "chmod -R 755 #{release_path}/public/assets/" 12 | end 13 | run_locally do 14 | execute "rm -rf ./public/assets" 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/capistrano/cookbook/tasks/create_database.cap: -------------------------------------------------------------------------------- 1 | # Inspired by https://gist.github.com/dv/10370719 2 | 3 | namespace :database do 4 | desc "fetches the remote database.yml and creates the user and database specified in it currently postgres only. Will only work if your db server also has your rails app on it." 5 | task :create do 6 | on primary(:db) do |host| 7 | unless test("[ -f #{shared_path}/config/database.yml ]") 8 | execute "cp #{shared_path}/config/database.example.yml #{shared_path}/config/database.yml" 9 | end 10 | system("scp -P #{host.port or 22} #{fetch(:deploy_user)}@#{host}:#{shared_path}/config/database.yml db.tmp.yml") 11 | yaml = YAML.load_file("db.tmp.yml") 12 | system("rm db.tmp.yml") 13 | puts yaml 14 | puts fetch(:rails_env) 15 | database_config = yaml[fetch(:rails_env).to_s] 16 | 17 | if test %Q{sudo -u postgres psql postgres -tAc "SELECT 1 FROM pg_roles WHERE rolname='#{database_config["username"]}';" | grep -q 1} 18 | info "Role #{database_config['username']} already exists" 19 | else 20 | execute %Q{sudo -u postgres psql -c "create user #{database_config["username"]} with password '#{database_config["password"]}';"} 21 | end 22 | 23 | # Create the database 24 | if test %Q{sudo -u postgres psql -lqt | cut -d \\| -f 1 | grep -wq #{database_config["database"]}} 25 | info "Database #{database_config['database']} already exists" 26 | else 27 | execute %Q{sudo -u postgres psql -c "create database #{database_config["database"]};"} 28 | execute %Q{sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE #{database_config["database"]} to #{database_config["username"]};"} 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/capistrano/cookbook/tasks/logs.cap: -------------------------------------------------------------------------------- 1 | namespace :logs do 2 | task :tail, :file do |t, args| 3 | if args[:file] 4 | on roles(:app) do 5 | execute "tail -f #{shared_path}/log/#{args[:file]}.log" 6 | end 7 | else 8 | puts "please specify a logfile e.g: 'rake logs:tail[logfile]" 9 | puts "will tail 'shared_path/log/logfile.log'" 10 | puts "remember if you use zsh you'll need to format it as:" 11 | puts "rake 'logs:tail[logfile]' (single quotes)" 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/capistrano/cookbook/tasks/monit.cap: -------------------------------------------------------------------------------- 1 | namespace :monit do 2 | 3 | %w(start stop restart).each do |task_name| 4 | desc "#{task_name} Monit" 5 | task task_name do 6 | on roles(:app), in: :sequence, wait: 5 do 7 | sudo "service monit #{task_name}" 8 | end 9 | end 10 | end 11 | 12 | desc "Reload Monit" 13 | task 'reload' do 14 | on roles(:app), in: :sequence, wait: 5 do 15 | sudo "monit reload" 16 | end 17 | end 18 | 19 | end -------------------------------------------------------------------------------- /lib/capistrano/cookbook/tasks/nginx.cap: -------------------------------------------------------------------------------- 1 | namespace :nginx do 2 | %w(start stop restart reload).each do |task_name| 3 | desc "#{task } Nginx" 4 | task task_name do 5 | on roles(:app), in: :sequence, wait: 5 do 6 | sudo "systemctl #{task_name} nginx" 7 | end 8 | end 9 | end 10 | 11 | desc "Remove default Nginx Virtual Host" 12 | task "remove_default_vhost" do 13 | on roles(:app) do 14 | %w(/etc/nginx/sites-enabled/default /etc/nginx/sites-enabled/000-default).each do |default_filename| 15 | if test("[ -f #{default_filename} ]") 16 | sudo "rm #{default_filename}" 17 | puts "Removed default Nginx Virtualhost: #{default_filename}" 18 | else 19 | puts "Default: #{default_filename} not found, not removed" 20 | end 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/capistrano/cookbook/tasks/puma_systemd.cap: -------------------------------------------------------------------------------- 1 | namespace :puma do 2 | namespace :systemd do 3 | desc 'Reload the puma service via systemd by sending USR1 (e.g. trigger a zero downtime deploy)' 4 | task :reload do 5 | on roles(fetch(:puma_role)) do 6 | if fetch(:puma_systemctl_user) == :system 7 | sudo "#{fetch(:puma_systemctl_bin)} reload-or-restart #{fetch(:puma_service_unit_name)}" 8 | else 9 | execute :loginctl, "enable-linger", fetch(:puma_lingering_user) if fetch(:puma_enable_lingering) 10 | execute "#{fetch(:puma_systemctl_bin)}", "--user", "reload-or-restart", fetch(:puma_service_unit_name) 11 | end 12 | end 13 | end 14 | end 15 | end 16 | 17 | after 'deploy:finished', 'puma:systemd:reload' -------------------------------------------------------------------------------- /lib/capistrano/cookbook/tasks/run_tests.cap: -------------------------------------------------------------------------------- 1 | namespace :deploy do 2 | desc "Runs test before deploying, can't deploy unless they pass" 3 | task :run_tests do 4 | test_log = "log/capistrano.test.log" 5 | tests = fetch(:tests) 6 | tests.each do |test| 7 | puts "--> Running tests: '#{test}', please wait ..." 8 | unless system "bundle exec rspec #{test} > #{test_log} 2>&1" 9 | puts "--> Tests: '#{test}' failed. Results in: #{test_log} and below:" 10 | system "cat #{test_log}" 11 | exit; 12 | end 13 | puts "--> '#{test}' passed" 14 | end 15 | puts "--> All tests passed" 16 | system "rm #{test_log}" 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/capistrano/cookbook/tasks/setup_config.cap: -------------------------------------------------------------------------------- 1 | require 'capistrano/dsl' 2 | require 'capistrano/cookbook/helpers/setup_config_values' 3 | require 'capistrano/cookbook/helpers/substitute_strings' 4 | require 'capistrano/cookbook/helpers/smart_template' 5 | require 'capistrano/cookbook/nginx' 6 | require 'capistrano/cookbook/monit' 7 | require 'securerandom' 8 | 9 | namespace :deploy do 10 | task :setup_config do 11 | conf = ::Capistrano::Cookbook::SetupConfigValues.new 12 | 13 | on roles(:app) do 14 | # make the config dir 15 | execute :mkdir, "-p #{shared_path}/config" 16 | execute :mkdir, "-p /home/#{fetch(:deploy_user)}/.config/systemd/user" 17 | 18 | # config files to be uploaded to shared/config, see the 19 | # definition of smart_template for details of operation. 20 | conf.config_files.each do |file| 21 | smart_template(file[:source], file[:destination], file[:as_root]) 22 | execute(:chmod, "+x #{file[:destination]}") if file[:executable] 23 | end 24 | 25 | # which of the above files should be marked as executable 26 | # conf.executable_config_files.each do |file| 27 | # execute :chmod, "+x #{shared_path}/config/#{file}" 28 | # end 29 | 30 | # symlink stuff which should be... symlinked 31 | # conf.symlinks.each do |symlink| 32 | # sudo "ln -nfs #{shared_path}/config/#{symlink[:source]} #{sub_strings(symlink[:link])}" 33 | # end 34 | 35 | if File.exists?(File.join('config', 'master.key')) 36 | upload! File.join('config', 'master.key'), File.join(shared_path, 'config', 'master.key') 37 | end 38 | end 39 | end 40 | end 41 | 42 | 43 | # remove the default nginx configuration as it will tend to conflict with our configs 44 | before 'deploy:setup_config', 'nginx:remove_default_vhost' 45 | 46 | # make sure that shared directories etc exist before running otherwise the 47 | # initial nginx reload won't work because of the nginx log file directory path 48 | # not existing 49 | before 'deploy:setup_config', 'deploy:check:directories' 50 | before 'deploy:setup_config', 'deploy:check:linked_dirs' 51 | 52 | # After setup config has generated and setup initial files, run the Capistrano Puma 53 | # tasks responsible for uploading config files. Note that `setup_config` creates overrides 54 | # for these in `config/deploy/templates` so we're not using the default ones from the gem 55 | after 'deploy:setup_config', 'puma:config' 56 | after 'deploy:setup_config', 'puma:nginx_config' 57 | after 'deploy:setup_config', 'puma:monit:config' 58 | after 'deploy:setup_config', 'puma:systemd:config' 59 | after 'deploy:setup_config', 'puma:systemd:enable' 60 | after 'deploy:setup_config', 'certbot:install' 61 | 62 | # Enable the sidekiq systemd service so that it's started automatically on (re)boot 63 | after 'deploy:setup_config', 'sidekiq:systemd:enable' if (defined?(Capistrano::Sidekiq) == 'constant' && Capistrano::Sidekiq.class == Class) 64 | 65 | # reload nginx to it will pick up any modified vhosts from setup_config 66 | after 'deploy:setup_config', 'nginx:reload' 67 | 68 | # Restart monit so it will pick up any monit configurations we've added 69 | after 'deploy:setup_config', 'monit:reload' 70 | 71 | -------------------------------------------------------------------------------- /lib/capistrano/cookbook/tasks/sidekiq_systemd.cap: -------------------------------------------------------------------------------- 1 | namespace :sidekiq do 2 | namespace :systemd do 3 | desc 'Install systemd sidekiq service' 4 | task :enable do 5 | on roles fetch(:sidekiq_roles) do |role| 6 | if fetch(:sidekiq_service_unit_user) == :system 7 | execute :sudo, :systemctl, "enable", fetch(:sidekiq_service_unit_name) 8 | else 9 | execute :systemctl, "--user", "enable", fetch(:sidekiq_service_unit_name) 10 | execute :loginctl, "enable-linger", fetch(:sidekiq_systemctl_user) if fetch(:sidekiq_enable_lingering) 11 | end 12 | end 13 | end 14 | end 15 | end -------------------------------------------------------------------------------- /lib/capistrano/cookbook/templates/database.example.yml.erb: -------------------------------------------------------------------------------- 1 | <%= fetch(:rails_env) %>: 2 | adapter: postgresql 3 | timeout: 5000 4 | encoding: utf8 5 | reconnect: false 6 | database: <%= "#{fetch(:application)}_#{fetch(:rails_env)}" %> 7 | pool: 5 8 | username: <%= "#{fetch(:application)}_#{fetch(:rails_env)}" %> 9 | password: <%= SecureRandom.hex(12) %> 10 | host: 127.0.0.1 11 | port: 5432 12 | -------------------------------------------------------------------------------- /lib/capistrano/cookbook/templates/log_rotation.erb: -------------------------------------------------------------------------------- 1 | <%= fetch(:deploy_to) %>/shared/log/*.log { 2 | daily 3 | missingok 4 | rotate 52 5 | compress 6 | delaycompress 7 | notifempty 8 | sharedscripts 9 | endscript 10 | copytruncate 11 | } 12 | -------------------------------------------------------------------------------- /lib/capistrano/cookbook/templates/sidekiq.yml.erb: -------------------------------------------------------------------------------- 1 | <%= fetch(:rails_env) %>: 2 | concurrency: 5 3 | -------------------------------------------------------------------------------- /lib/capistrano/cookbook/templates/sidekiq_init.sh.erb: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | APP_ROOT=<%= current_path %> 5 | PID=$APP_ROOT/tmp/pids/sidekiq.pid 6 | CMD="cd $APP_ROOT; RAILS_ENV=<%= "#{fetch(:rails_env)}" %> nohup bundle exec sidekiq -e <%= "#{fetch(:rails_env)}" %> -C <%= current_path %>/config/sidekiq.yml -i 0 -P $PID >> <%= current_path %>/log/sidekiq.log 2>&1 &" 7 | AS_USER="deploy" 8 | 9 | run () { 10 | if [ "$(id -un)" = "$AS_USER" ]; then 11 | eval $1 12 | else 13 | su -c "$1" - $AS_USER 14 | fi 15 | } 16 | 17 | sig () { 18 | test -s "$PID" && kill -$1 `cat $PID` 19 | } 20 | 21 | case "$1" in 22 | start) 23 | sig 0 && echo >&2 "Already Running" && exit 0 24 | run "$CMD" 25 | ;; 26 | stop) 27 | if kill -0 `cat $PID` 28 | then 29 | cd $APP_ROOT 30 | bundle exec sidekiqctl stop $APP_ROOT/tmp/pids/sidekiq.pid 10 31 | echo "stopping...." 32 | else 33 | echo "not running" 34 | fi 35 | ;; 36 | *) 37 | echo >&2 "Usage: $0 " 38 | exit 1 39 | ;; 40 | esac 41 | -------------------------------------------------------------------------------- /lib/capistrano/cookbook/version.rb: -------------------------------------------------------------------------------- 1 | module Capistrano 2 | module Cookbook 3 | VERSION = "5.0.2" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/generators/capistrano/reliably_deploying_rails/bootstrap_generator.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | 3 | module Capistrano 4 | module ReliablyDeployingRails 5 | module Generators 6 | class BootstrapGenerator < Rails::Generators::Base 7 | source_root File.expand_path('../templates', __FILE__) 8 | desc "Bootstrap everything required to deploy with capistrano to a server configured with: https://github.com/TalkingQuickly/rails-server-template" 9 | class_option :sidekiq, type: :boolean, default: false 10 | class_option :production_hostname, type: :string, default: nil 11 | class_option :production_server_address, type: :string, default: nil 12 | class_option :certbot_enable, type: :boolean, default: false 13 | class_option :certbot_email, type: :string 14 | 15 | def setup 16 | @production_hostname = options[:production_hostname] || 'YOUR_PRODUCTION_HOSTNAME' 17 | @production_server_address = options[:production_server_address] || 'YOUR_PRODUCTION_SERVER_ADDRESS' 18 | @generate_sidekiq = options[:sidekiq] 19 | @certbot_enable = options[:certbot_enable] 20 | @certbot_email = options[:certbot_email] 21 | end 22 | 23 | def check_domain 24 | raise 'The `_` chatacter is not valid in domain names' if @production_hostname.include?('_') 25 | end 26 | 27 | def create_capfile 28 | template "Capfile.erb", 'Capfile' 29 | end 30 | 31 | def create_deployment_configuration 32 | FileUtils.mkdir_p 'config/deploy' 33 | template "deploy.rb.erb", 'config/deploy.rb' 34 | template "production.rb.erb", 'config/deploy/production.rb' 35 | template "staging.rb.erb", 'config/deploy/staging.rb' 36 | end 37 | 38 | def create_local_config_templates 39 | FileUtils.mkdir_p 'config/deploy/templates' 40 | base_path = File.expand_path('../templates', __FILE__) 41 | 42 | templates(@generate_sidekiq).each do |file| 43 | unless File.file?(file) 44 | copy_file(File.join(base_path, file), "config/deploy/templates/#{file}") 45 | end 46 | end 47 | end 48 | 49 | private 50 | 51 | def templates(generate_sidekiq) 52 | return puma_templates unless generate_sidekiq 53 | puma_templates + sidekiq_templates 54 | end 55 | 56 | def puma_templates 57 | %w( 58 | nginx_conf.erb 59 | puma_monit.conf.erb 60 | puma.rb.erb 61 | puma.service.erb 62 | sidekiq_monit.erb 63 | sidekiq.service.capistrano.erb 64 | ) 65 | end 66 | 67 | def sidekiq_templates 68 | %w( 69 | sidekiq_monit.erb 70 | sidekiq.service.capistrano.erb 71 | ) 72 | end 73 | end 74 | end 75 | end 76 | end -------------------------------------------------------------------------------- /lib/generators/capistrano/reliably_deploying_rails/templates/Capfile.erb: -------------------------------------------------------------------------------- 1 | # Load DSL and set up stages 2 | require "capistrano/setup" 3 | 4 | # Include default deployment tasks 5 | require "capistrano/deploy" 6 | 7 | # Load the SCM plugin appropriate to your project: 8 | # 9 | # require "capistrano/scm/hg" 10 | # install_plugin Capistrano::SCM::Hg 11 | # or 12 | # require "capistrano/scm/svn" 13 | # install_plugin Capistrano::SCM::Svn 14 | # or 15 | require "capistrano/scm/git" 16 | install_plugin Capistrano::SCM::Git 17 | 18 | # Include tasks from other gems included in your Gemfile 19 | # 20 | # For documentation on these, see for example: 21 | # 22 | # https://github.com/capistrano/rbenv 23 | # https://github.com/capistrano/bundler 24 | # https://github.com/capistrano/rails 25 | # 26 | require "capistrano/rbenv" 27 | require "capistrano/bundler" 28 | require "capistrano/rails/assets" 29 | require "capistrano/rails/migrations" 30 | require 'capistrano/cookbook' 31 | 32 | require 'capistrano/puma' 33 | install_plugin Capistrano::Puma, load_hooks: false # Default puma tasks 34 | install_plugin Capistrano::Puma::Nginx, load_hooks: false # if you want to upload a nginx site template 35 | install_plugin Capistrano::Puma::Systemd, load_hooks: false # if you use SystemD 36 | install_plugin Capistrano::Puma::Monit, load_hooks: false # if you need the monit tasks 37 | 38 | <% if @generate_sidekiq %> 39 | require 'capistrano/sidekiq' 40 | install_plugin Capistrano::Sidekiq # Default sidekiq tasks 41 | # Then select your service manager 42 | install_plugin Capistrano::Sidekiq::Systemd 43 | <% end %> 44 | 45 | # Load custom tasks from `lib/capistrano/tasks` if you have any defined 46 | Dir.glob("lib/capistrano/tasks/*.rake").each { |r| import r } 47 | -------------------------------------------------------------------------------- /lib/generators/capistrano/reliably_deploying_rails/templates/deploy.rb.erb: -------------------------------------------------------------------------------- 1 | # config valid for current version and patch releases of Capistrano 2 | lock "~> 3.16.0" 3 | 4 | set :application, '<%= Rails.application.class.module_parent_name.underscore %>' 5 | set :deploy_user, 'deploy' 6 | 7 | # setup repo details 8 | set :repo_url, '<%= `git config --get remote.origin.url`.delete!("\n") %>' 9 | 10 | # setup rbenv. 11 | set :rbenv_type, :system 12 | set :rbenv_ruby, '3.0.0' 13 | set :rbenv_prefix, "RBENV_ROOT=#{fetch(:rbenv_path)} RBENV_VERSION=#{fetch(:rbenv_ruby)} #{fetch(:rbenv_path)}/bin/rbenv exec" 14 | set :rbenv_map_bins, %w{rake gem bundle ruby rails} 15 | 16 | # setup certbot for SSL via letsencrypt 17 | set :certbot_enable_ssl, <%= @certbot_enable %> 18 | set :certbot_redirect_to_https, true 19 | set :certbot_email, "<%= @certbot_email %>" 20 | set :certbot_use_acme_staging, false 21 | 22 | # setup puma to operate in clustered mode, required for zero downtime deploys 23 | set :puma_preload_app, false 24 | set :puma_init_active_record, true 25 | set :puma_workers, 3 26 | set :puma_systemctl_user, fetch(:deploy_user) 27 | set :puma_enable_lingering, true 28 | 29 | <% if @generate_sidekiq %> 30 | set :sidekiq_systemctl_user, fetch(:deploy_user) 31 | set :sidekiq_enable_lingering, true 32 | <% end %> 33 | 34 | # how many old releases do we want to keep 35 | set :keep_releases, 5 36 | 37 | # Directories that should be linked to the shared folder 38 | append :linked_dirs, 'log', 'tmp/pids', 'tmp/cache', 'tmp/sockets', 'vendor/bundle', '.bundle', 'public/system', 'public/uploads' 39 | append :linked_files, 'config/database.yml', 'config/master.key' 40 | 41 | # this: 42 | # http://www.capistranorb.com/documentation/getting-started/flow/ 43 | # is worth reading for a quick overview of what tasks are called 44 | # and when for `cap stage deploy` 45 | 46 | namespace :deploy do 47 | end -------------------------------------------------------------------------------- /lib/generators/capistrano/reliably_deploying_rails/templates/nginx_conf.erb: -------------------------------------------------------------------------------- 1 | upstream puma_<%= fetch(:nginx_config_name) %> { <% 2 | @backends = [fetch(:puma_bind)].flatten.map do |m| 3 | etype, address = /(tcp|unix|ssl):\/{1,2}(.+)/.match(m).captures 4 | if etype == 'unix' 5 | "server #{etype}:#{address} #{fetch(:nginx_socket_flags)};" 6 | else 7 | "server #{address.gsub(/0\.0\.0\.0(.+)/, "127.0.0.1\\1")} #{fetch(:nginx_http_flags)};" 8 | end 9 | end 10 | %><% @backends.each do |server| %> 11 | <%= server %><% end %> 12 | } 13 | <% if fetch(:nginx_use_ssl) -%> 14 | server { 15 | listen 80; 16 | server_name <%= fetch(:nginx_server_name) %>; 17 | return 301 https://$host$1$request_uri; 18 | } 19 | <% end -%> 20 | 21 | server { 22 | <% if fetch(:nginx_use_ssl) -%> 23 | <% if fetch(:nginx_use_http2) -%> 24 | listen 443 ssl http2; 25 | <% else -%> 26 | listen 443 ssl; 27 | <% end -%> 28 | <% if fetch(:nginx_ssl_certificate) -%> 29 | ssl_certificate <%= fetch(:nginx_ssl_certificate) %>; 30 | <% else -%> 31 | ssl_certificate /etc/ssl/certs/<%= fetch(:nginx_config_name) %>.crt; 32 | <% end -%> 33 | <% if fetch(:nginx_ssl_certificate_key) -%> 34 | ssl_certificate_key <%= fetch(:nginx_ssl_certificate_key) %>; 35 | <% else -%> 36 | ssl_certificate_key /etc/ssl/private/<%= fetch(:nginx_config_name) %>.key; 37 | <% end -%> 38 | <% else -%> 39 | listen 80; 40 | <% end -%> 41 | server_name <%= fetch(:nginx_server_name) %>; 42 | root <%= current_path %>/public; 43 | try_files $uri/index.html $uri @puma_<%= fetch(:nginx_config_name) %>; 44 | 45 | client_max_body_size 4G; 46 | keepalive_timeout 10; 47 | 48 | error_page 500 502 504 /500.html; 49 | error_page 503 @503; 50 | 51 | location @puma_<%= fetch(:nginx_config_name) %> { 52 | proxy_http_version 1.1; 53 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 54 | proxy_set_header X-Forwarded-Proto $scheme; 55 | proxy_set_header Host $host; 56 | proxy_redirect off; 57 | proxy_set_header Upgrade $http_upgrade; 58 | proxy_set_header Connection "Upgrade"; 59 | proxy_headers_hash_max_size 512; 60 | proxy_headers_hash_bucket_size 128; 61 | <% if fetch(:nginx_use_ssl) -%> 62 | proxy_set_header X-Forwarded-Proto https; 63 | <% else -%> 64 | <% if fetch(:nginx_downstream_uses_ssl) -%> 65 | proxy_set_header X-Forwarded-Proto https; 66 | <% else -%> 67 | proxy_set_header X-Forwarded-Proto http; 68 | <% end -%> 69 | <% end -%> 70 | proxy_pass http://puma_<%= fetch(:nginx_config_name) %>; 71 | # limit_req zone=one; 72 | access_log <%= shared_path %>/log/nginx.access.log; 73 | error_log <%= shared_path %>/log/nginx.error.log; 74 | } 75 | 76 | location ^~ /assets/ { 77 | gzip_static on; 78 | expires max; 79 | add_header Cache-Control public; 80 | } 81 | 82 | location = /50x.html { 83 | root html; 84 | } 85 | 86 | location = /404.html { 87 | root html; 88 | } 89 | 90 | location @503 { 91 | error_page 405 = /system/maintenance.html; 92 | if (-f $document_root/system/maintenance.html) { 93 | rewrite ^(.*)$ /system/maintenance.html break; 94 | } 95 | rewrite ^(.*)$ /503.html break; 96 | } 97 | 98 | if ($request_method !~ ^(GET|HEAD|PUT|PATCH|POST|DELETE|OPTIONS)$ ){ 99 | return 405; 100 | } 101 | 102 | if (-f $document_root/system/maintenance.html) { 103 | return 503; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /lib/generators/capistrano/reliably_deploying_rails/templates/production.rb.erb: -------------------------------------------------------------------------------- 1 | set :stage, :production 2 | set :branch, "master" 3 | 4 | # This is used in the Nginx VirtualHost to specify which domains 5 | # the app should appear on. If you don't yet have DNS setup, you'll 6 | # need to create entries in your local Hosts file for testing. 7 | set :nginx_server_name, '<%= @production_hostname %>' 8 | 9 | # used in case we're deploying multiple versions of the same 10 | # app side by side. Also provides quick sanity checks when looking 11 | # at filepaths 12 | set :full_app_name, "#{fetch(:application)}_#{fetch(:stage)}" 13 | 14 | <% if @generate_sidekiq %> 15 | # Name sidekiq systemd service after the app and stage name so that 16 | # multiple apps and stages can co-exist on the same machine if needed 17 | set :sidekiq_service_unit_name, "sidekiq_#{fetch(:full_app_name)}" 18 | <% end %> 19 | 20 | server '<%= @production_server_address %>', user: 'deploy', roles: %w{web app db}, primary: true 21 | 22 | set :deploy_to, "/home/#{fetch(:deploy_user)}/apps/#{fetch(:full_app_name)}" 23 | 24 | # dont try and infer something as important as environment from 25 | # stage name. 26 | set :rails_env, :production 27 | 28 | # whether we're using ssl or not, used for building nginx 29 | # config file 30 | set :enable_ssl, false -------------------------------------------------------------------------------- /lib/generators/capistrano/reliably_deploying_rails/templates/puma.rb.erb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env puma 2 | 3 | directory '<%= current_path %>' 4 | rackup "<%=fetch(:puma_rackup)%>" 5 | environment '<%= fetch(:puma_env) %>' 6 | <% if fetch(:puma_tag) %> 7 | tag '<%= fetch(:puma_tag)%>' 8 | <% end %> 9 | pidfile "<%=fetch(:puma_pid)%>" 10 | state_path "<%=fetch(:puma_state)%>" 11 | 12 | 13 | threads <%=fetch(:puma_threads).join(',')%> 14 | 15 | <%= puma_plugins %> 16 | 17 | <%= puma_bind %> 18 | <% if fetch(:puma_control_app) %> 19 | activate_control_app "<%= fetch(:puma_default_control_app) %>" 20 | <% end %> 21 | workers <%= puma_workers %> 22 | <% if fetch(:puma_worker_timeout) %> 23 | worker_timeout <%= fetch(:puma_worker_timeout).to_i %> 24 | <% end %> 25 | 26 | <% if puma_daemonize? %> 27 | daemonize 28 | <% end %> 29 | 30 | restart_command '<%= fetch(:puma_restart_command) %>' 31 | 32 | <% if puma_preload_app? %> 33 | preload_app! 34 | <% else %> 35 | prune_bundler 36 | <% end %> 37 | 38 | on_restart do 39 | puts 'Refreshing Gemfile' 40 | ENV["BUNDLE_GEMFILE"] = "<%= fetch(:bundle_gemfile, "#{current_path}/Gemfile") %>" 41 | end 42 | 43 | <% if puma_preload_app? and fetch(:puma_init_active_record) %> 44 | before_fork do 45 | ActiveRecord::Base.connection_pool.disconnect! 46 | end 47 | 48 | on_worker_boot do 49 | ActiveSupport.on_load(:active_record) do 50 | ActiveRecord::Base.establish_connection 51 | end 52 | end 53 | <% end %> 54 | -------------------------------------------------------------------------------- /lib/generators/capistrano/reliably_deploying_rails/templates/puma.service.erb: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Puma HTTP Server for <%= "#{fetch(:application)} (#{fetch(:stage)})" %> 3 | After=network.target 4 | 5 | [Service] 6 | Type=simple 7 | <%="User=#{puma_user(@role)}" if fetch(:puma_systemctl_user) == :system %> 8 | WorkingDirectory=<%= current_path %> 9 | ExecStart=/usr/local/rbenv/bin/rbenv exec bundle exec puma -C <%= fetch(:puma_conf) %> 10 | ExecReload=/bin/kill -USR1 $MAINPID 11 | ExecStop=/bin/kill -TSTP $MAINPID 12 | StandardOutput=append:<%= fetch(:puma_access_log) %> 13 | StandardError=append:<%= fetch(:puma_error_log) %> 14 | <%="EnvironmentFile=#{fetch(:puma_service_unit_env_file)}" if fetch(:puma_service_unit_env_file) %> 15 | 16 | <% fetch(:puma_service_unit_env_vars, []).each do |environment_variable| %> 17 | <%="Environment=#{environment_variable}" %> 18 | <% end %> 19 | 20 | Environment=RBENV_VERSION=<%= fetch(:rbenv_ruby) %> 21 | Environment=RBENV_ROOT=/usr/local/rbenv 22 | 23 | Restart=always 24 | RestartSec=1 25 | 26 | SyslogIdentifier=puma_<%= fetch(:application) %>_<%= fetch(:stage) %> 27 | 28 | [Install] 29 | WantedBy=<%=(fetch(:puma_systemctl_user) == :system) ? "multi-user.target" : "default.target"%> -------------------------------------------------------------------------------- /lib/generators/capistrano/reliably_deploying_rails/templates/puma_monit.conf.erb: -------------------------------------------------------------------------------- 1 | check process <%= puma_monit_service_name %> 2 | with pidfile "<%= fetch(:puma_pid) %>" 3 | start program = "/bin/bash -c 'XDG_RUNTIME_DIR=/run/user/$(id -u) /usr/bin/systemctl start --user <%= fetch(:puma_service_unit_name) %>'" as uid "<%= fetch(:puma_systemctl_user) %>" and gid "<%= fetch(:puma_systemctl_user) %>" 4 | stop program = "/bin/bash -c 'XDG_RUNTIME_DIR=/run/user/$(id -u) /usr/bin/systemctl stop --user <%= fetch(:puma_service_unit_name) %>'" as uid "<%= fetch(:puma_systemctl_user) %>" and gid "<%= fetch(:puma_systemctl_user) %>" 5 | -------------------------------------------------------------------------------- /lib/generators/capistrano/reliably_deploying_rails/templates/sidekiq.service.capistrano.erb: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=sidekiq for <%= "#{fetch(:application)} (#{fetch(:stage)})" %> 3 | After=syslog.target network.target 4 | 5 | [Service] 6 | Type=simple 7 | WorkingDirectory=<%= File.join(fetch(:deploy_to), 'current') %> 8 | ExecStart=/usr/local/rbenv/bin/rbenv exec bundle exec sidekiq -e <%= fetch(:sidekiq_env) %> 9 | ExecReload=/bin/kill -TSTP $MAINPID 10 | ExecStop=/bin/kill -TERM $MAINPID 11 | <%="StandardOutput=append:#{fetch(:sidekiq_log)}" if fetch(:sidekiq_log) %> 12 | <%="StandardError=append:#{fetch(:sidekiq_error_log)}" if fetch(:sidekiq_error_log) %> 13 | <%="User=#{fetch(:sidekiq_user)}" if (fetch(:sidekiq_user) && (fetch(:puma_systemctl_user) == :system)) %> 14 | <%="EnvironmentFile=#{fetch(:sidekiq_service_unit_env_file)}" if fetch(:sidekiq_service_unit_env_file) %> 15 | <% fetch(:sidekiq_service_unit_env_vars, []).each do |environment_variable| %> 16 | <%="Environment=#{environment_variable}" %> 17 | <% end %> 18 | 19 | Environment=RBENV_VERSION=<%= fetch(:rbenv_ruby) %> 20 | Environment=RBENV_ROOT=/usr/local/rbenv 21 | 22 | RestartSec=1 23 | Restart=on-failure 24 | 25 | SyslogIdentifier=sidekiq_<%= fetch(:application) %>_<%= fetch(:stage) %> 26 | 27 | [Install] 28 | WantedBy=<%=(fetch(:sidekiq_systemctl_user) == :system) ? "multi-user.target" : "default.target"%> 29 | 30 | <% # Adapted from: https://github.com/seuros/capistrano-sidekiq/blob/master/lib/generators/capistrano/sidekiq/systemd/templates/sidekiq.service.capistrano.erb %> -------------------------------------------------------------------------------- /lib/generators/capistrano/reliably_deploying_rails/templates/sidekiq_monit.erb: -------------------------------------------------------------------------------- 1 | check process <%= fetch(:sidekiq_service_unit_name) %> matching "sidekiq.*<%= fetch(:full_app_name) %>" 2 | start program = "/bin/bash -c 'XDG_RUNTIME_DIR=/run/user/$(id -u) /usr/bin/systemctl start --user <%= fetch(:sidekiq_service_unit_name) %>'" as uid "<%= fetch(:sidekiq_systemctl_user) %>" and gid "<%= fetch(:sidekiq_systemctl_user) %>" 3 | stop program = "/bin/bash -c 'XDG_RUNTIME_DIR=/run/user/$(id -u) /usr/bin/systemctl stop --user <%= fetch(:sidekiq_service_unit_name) %>'" as uid "<%= fetch(:sidekiq_systemctl_user) %>" and gid "<%= fetch(:sidekiq_systemctl_user) %>" 4 | group <%= fetch(:sidekiq_monit_group) || fetch(:full_app_name) %>-sidekiq 5 | -------------------------------------------------------------------------------- /lib/generators/capistrano/reliably_deploying_rails/templates/staging.rb.erb: -------------------------------------------------------------------------------- 1 | set :stage, :staging 2 | set :branch, "develop" 3 | 4 | # This is used in the Nginx VirtualHost to specify which domains 5 | # the app should appear on. If you don't yet have DNS setup, you'll 6 | # need to create entries in your local Hosts file for testing. 7 | set :nginx_server_name, "REPLACE_WITH_YOUR_STAGING_HOSTNAME" 8 | 9 | # used in case we're deploying multiple versions of the same 10 | # app side by side. Also provides quick sanity checks when looking 11 | # at filepaths 12 | set :full_app_name, "#{fetch(:application)}_#{fetch(:stage)}" 13 | 14 | <% if @generate_sidekiq %> 15 | # Name sidekiq systemd service after the app and stage name so that 16 | # multiple apps and stages can co-exist on the same machine if needed 17 | set :sidekiq_service_unit_name, "#{fetch(:full_app_name)}_sidekiq" 18 | <% end %> 19 | 20 | server 'YOUR_STAGING_SERVER_IP', user: 'deploy', roles: %w{web app db}, primary: true 21 | 22 | set :deploy_to, "/home/#{fetch(:deploy_user)}/apps/#{fetch(:full_app_name)}" 23 | 24 | # dont try and infer something as important as environment from 25 | # stage name. 26 | set :rails_env, :production 27 | 28 | # whether we're using ssl or not, used for building nginx 29 | # config file 30 | set :enable_ssl, false --------------------------------------------------------------------------------