Capistrano 3 + Puma + Apache: A Love Hate Story

sean

The situation

So I found myself in a situation where I needed to deploy a Rails 4 app in a mixed environment with a prerequisite of using Apache. I've set up deployments with capistrano before, but only the version 2.x syntax. This time, I wanted to step up to version 3. No big deal, right?

This turned out to be a perfect combination for banging my head against the wall.

The Setup

I'm not going to go through each trial and tribulation I had. Suffice it to say that Capistrano 3 being so new at the time of my integrating this caused me to learn what hadn't been documented yet. So, we'll just skip to the good bits. The useful bits.

First up, here's the relevant parts of the setup process (If anyone has any questions, please ask in comments!).

Gemfile


...
gem 'puma'
gem 'capistrano',           '~> 3.0', group: :development
gem 'capistrano-puma',        github: "seuros/capistrano-puma"
gem 'capistrano-rbenv',       github: "capistrano/rbenv"
gem 'capistrano-bundler'
gem 'capistrano-rails',       '~> 1.1.0'
...

Terminal


bundle exec capify

Capfile


require 'capistrano/setup'

require 'capistrano/deploy'

require 'capistrano/rbenv'
require 'capistrano-puma'
require 'capistrano/puma/jungle'
require 'capistrano/bundler'
require 'capistrano/rails/assets'
require 'capistrano/rails/migrations'

Dir.glob('lib/capistrano/tasks/*.cap').each { |r| import r }

At this point, you need to configure your deploy environments and your deploy.rb file. I won't be covering getting capistrano itself set up. 90% of this you can pull from the documentation on the Capistrano site (environments, initial start).

So, now that you can properly run cap tasks and push to the server, you may be asking what changes need to be made in order to get everything running with Puma. Well, sirs and madams, this was the trial and error part I ran through. First off, inside your deploy.rb file, you need to change your restart and setup task accordingly.

deploy.rb


desc 'Restart application'
task :restart do
  on roles(:app), in: :sequence, wait: 5 do
    execute "/etc/init.d/puma restart" # assumes puma jungle tools installed
  end
end

task :setup_config do
  on roles(:all) do
    execute "sudo ln -nfs #{current_path}/config/apache.conf /etc/apache2/sites-enabled/#{fetch(:application)}"
    execute "sudo /etc/init.d/puma add #{current_path} deployer #{current_path}/config/puma.rb #{current_path}/log/puma.log"
    execute "mkdir -p #{shared_path}/config"
    execute "cp #{current_path}/config/database.example.yml #{shared_path}/config/database.yml"
    puts "Now edit the config files in #{shared_path}."
  end
end

The first thing you should notice here is inside the setup_config block, I call /etc/init.d/puma. This is from puma's jungle tools. Check out the docs on getting that installed here. If on ubuntu, you could sub this out for an upstart task instead.

The second thing is that I'm referencing a puma conf kept inside the project repo. For reference, here's what I'm using. You can adjust to your liking.

puma.rb

directory "/var/www/crisp-code.com/current"
environment 'production'
workers 1
threads 1, 8

bind 'tcp://0.0.0.0:9951'
pidfile "/var/www/crisp-code.com/current/tmp/puma/pid"
state_path "/var/www/crisp-code.com/current/tmp/puma/state"

preload_app!
activate_control_app

And the third thing you may see if that I'm setting apache in the same way I'm setting the puma config. I found something that came close on stack overflow and made it work. The problems I had with the puma + apache conf I found online was the asset pipeline was serving bad assets. I'm going to take another look at this in the future because of course I want send gzip'd js and css, but it wasn't critical at the time.

apache.conf


<VirtualHost *:80>
  ServerName crisp-code.com
  DocumentRoot /var/www/crisp-code.com/current/public
  
  # Redirect all requests that don't match a file on disk under DocumentRoot get proxied to Puma
  RewriteEngine On
  RewriteCond %{DOCUMENT_ROOT}/%{REQUEST_FILENAME} !-f
  RewriteRule ^/(.*)$ http://127.0.0.1:9951%{REQUEST_URI} [P]
  
  LogLevel warn
  CustomLog ${APACHE_LOG_DIR}/crisp-code.com.access.log combined
  ErrorLog ${APACHE_LOG_DIR}/crisp-code.com.error.log
 
  # Don't allow client to fool Puma into thinking connection is secure
  RequestHeader unset X-Forwarded-Proto

  # Anything under public is open to the world
  <Directory /var/www/crisp-code.com/current/public>
    Satisfy any
    Allow from all
    Require all granted
    Options -MultiViews
  </Directory>

  # Disable ETags (https://github.com/h5bp/server-configs-apache/tree/master/doc#configure-etags)
  # Set Expiration date for all assets to one year in the future
  <Location ^/assets/.*$>
    # Use of ETag is discouraged when Last-Modified is present
    Header unset ETag
    FileETag None
    # RFC says only cache for 1 year
    ExpiresActive On
    ExpiresDefault "access plus 1 year"
  </Location>

  # Compress HTML on the fly
  AddOutputFilterByType DEFLATE text/html 

</VirtualHost>

<VirtualHost *:80>
    ServerAlias www.crisp-code.com
    Redirect 301 / http://crisp-code.com
</VirtualHost>

One Gotcha

I will leave you with one troubleshooting heads up. When worrying about restarting puma via cap tasks, check your permissions. I found that this combo unfortunately likes to fail silently and that I was chasing false leads for a while.

I may have glazed over something important to someone, so if there's anyone out there with questions, please ask in the comments and I'll make additions. Enjoy!

comments powered byDisqus